重力に縋るな

モノが動くのがすき

そうだ、時計作ろう

みなさん時間測ってますか? 僕はパスタ茹でる時とかに測ります。

また、時間の間隔だけでなく、正確な時刻を知ったり、正確な時刻に合わせて何かアクションをしたい時があります。 たとえば JST 2026年06月21日22時23分24秒きっかりに数十 ns の精度でパスタを茹で始めたい時などがあると思います。

そんな時に便利なのが GNSS(Global Navigation Satellite System)です。 GPS(Global Positioning System)とも呼ばれがちですが、GPS は正確にはアメリカのやつのことだけを指します。 そのため、日本の準天頂衛星システム QZSS(Quasi-Zenith Satellite System)や、ヨーロッパの Gallileo、中国の BeiDou、ロシアの GLONASS といった測位衛星システムの総称が GNSS です。

qzss.go.jp

www.gsi.go.jp

qzss.go.jp

さて、この GNSS でどうやって測位をするのかといえば、この衛星群から正確な(衛星の)位置情報と時刻情報を配信することによって実現されています。

測位信号は電波であり光速で伝搬するため、測位信号に乗った時刻情報と自分の時刻の時間差 Δt から衛星と受信機の距離がわかり、そうすると三次元的には受信機は衛星から球状の場所に位置していることがわかります。 そのため、衛星を3つ使えばこの球の接する1点に受信機が存在することがわかります。 三次元的な三辺測量ですね。

https://www.jaxa.jp/countdown/f18/overview/img/pict_gps1.gif JAXA|今いる場所・時間がわかる測位とは?

ただ、これで正確な測位を実現するためには、かなり正確な時刻が必要になります。光速ってめちゃめちゃ速いですし。 なので測位衛星は原子時計が搭載することで、非常に正確な時刻を配信しています。

しかし一方で、受信機側にはそんなに正確な時計があることはほとんどないでしょう。 また、インターネット等に接続していないような air-gapped な環境でも、というかそういう環境でこそ測位を実現したいものです。 そのため、位置座標だけでなく、受信機自身の時刻も未知変数であり、これも含めて連立方程式で解くために、実際には衛星は4機は必要です。 これが測位衛星がたくさん・色々な軌道に打ち上げられている理由です。地球上のどこにいても4つ以上の測位衛星が目に入るようにしたいですからね。

QZSS(いわゆる「みちびき」)が8の字の軌道とかたまによく言われるのもこれが理由です。 たまに勘違いされますが、8の字というのは地球から見た時/地図上にプロットした時の話であって、軌道の形そのものは8の字ではありません。軌道は(重心である地球を1つ目の焦点とする)楕円にしかならないですからね。

https://qzss.go.jp/overview/services/isos7j00000003cb-img/isos7j00000003du.jpg https://qzss.go.jp/overview/services/isos7j00000003cb-img/isos7j00000003ee.jpg qzss.go.jp

さて、話が大変逸れましたが、そんなわけで現代では無料で超高精度な時刻情報が宇宙から降り注いでいて(そしてそれによって自分の時刻を超高精度に決定できて)とっても便利です。

実際、高精度な時刻を得ることも GNSS の代表的な用途のひとつです。

みなさんはよく NTP などで PC などの時刻同期をしていると思うんですが、NTP には Stratum と呼ばれる階層構造があり、より高精度な偉い時計に同期するような仕組みになっています。

https://www.marubun.co.jp/wp-content/uploads/2026/02/066e0354f09d3de46ba80624cc94a399.webp www.marubun.co.jp

この時に一番偉い時計 Stratum-0 も原子時計であり、日本の場合だと東京都小金井市にある NICT 本部に日本で一番偉い原子時計があり、ここに"本物の JST: Japan Standard Time"があります。これが定義なので真に本物です。

www.nict.go.jp

ちなみに最近は光格子時計なる技術が使われていたり、NICT 支部との分散化による冗長化が行われていたりするみたいです。 これで日本以外や小金井市が吹き飛んでも安心ですね(?)

https://www.nict.go.jp/press/2022/06/img/20220609-02.png www.nict.go.jp

https://www.nict.go.jp/sts/facility_sites_jp_web.svg www.nict.go.jp

さて、そんな偉い Stratum-0 ですが、GNSS 衛星というやつも原子時計を搭載した Stratum-0 です。 時刻の定義という意味での偉さでは NICT や各国の NICT 的な機関の原子時計が最も偉くはあるものの、時刻の正確さという意味ではかなり似たようなオーダーのものというわけですね。

なので、GNSS 受信機を接続して NTP などより正確な時刻を得てその時刻を使うようなシステムが作られることがあります。 Stratum-1 time server を自作するというやつですね。

ここらへんの話は Raspberry Pi 大好き YouTuber であるところの Jeff が色々動画を出していて便利です。

www.youtube.com

ということで、前置きが長くなりましたが時計を作っていこうかなと思います。 使うのはどこのご家庭にもある Raspberry Pi Pico と、以前は秋月で安価にたくさん売っていた GNSS モジュール*1です。 終売品ですが、まあなんか家に転がっていたので。

akizukidenshi.com

アンテナはまあ適当に窓際にでも置きましょう。外に出すのは面倒なので。養生テープで窓の内側に張り付けます。 測位条件としては結構悪めな気もしますが、絶対時刻を得るぐらいならいけるはずです。

さて、今回は数十 ns 精度でパスタを茹でるのが目標なので、ここでひとつ注意すべきことがあります。 それは UART でこの精度の正確な時刻取得/時刻同期を行うことは逆立ちしても不可能だということです。

それが如何に不可能かという話は id:koba789 のスライドでも見てください。

speakerdeck.com

そのため、PPS をちゃんと使う必要があります。 PPS というのは Pulse Per Second のことで、真に正確に "per second" 単位でパルスを出力してくれる機能です。 多くの GNSS 受信機には付いています。もちろん今回の秋月のやつにも付いています。 測位が fix すると LED がチカチカするやつとかもありがちだと思うんですが、あのタイミングとかもだいたいこれですね。

PPS と NMEA の絶対時刻の対応関係

ということで、この PPS の線も配線してやります。 まあ NMEA UART は GP0, GP1 で、PPS は GP2 とかでいいでしょう。

今回のハードウェア構成

ファームウェアは例によって Rust で書きます。最近は embassy で書くと考えることがだいぶ減っていいかんじです。 非同期ランタイムだしこれはこれで独自のエコシステムを築いているので中身をちゃんと把握しようと思うと大変な時もありますけどね。 embassy.dev

デバッグは rust-dap で。これはまあ個人的「いつもの」構成です。仕事でもこういうかんじです。

github.com

あとは defmt でログを吐いてあげまして、

defmt.ferrous-systems.com

このログを喰って色々可視化する Dashboard まで Opus 4.8 に作ってもらえばいいでしょう。これぐらいなら1セッション、なんならここに書いた構成を雑に /goal に突っ込めば一発でできるんではないでしょうか。

......しかし残念ながら、これだけでは話は終わりません。 単純に GPIO を read するだけではろくに精度が出せないのです。 ナイーブにポーリングしたらミリ秒とかのオーダーですし、割り込みで頑張っても数 µs とかのオーダーがせいぜいです。 実際ラズピコでやってみると、前者は ±1.4ms、後者は ±9µs とかでした。

割り込みベースでやっても µs 秒オーダーでばらつく

これではパスタが伸びてしまいます。困ります。

そもそもの問題として、ナノ秒とかのオーダーになってくるとソフトウェアで頑張るのは原理的に厳しいです。 Raspberry Pi Pico に載っている RP2040 の動作周波数は 125MHz なので、数十 ns の間に動かせるのは数命令とかです。 関数呼んだりしたらスタック積んでる間に終わりそうですね。

ということで、正確なパスタを食べたい人のために、ハードウェアが PPS などのパルスを見てタイムスタンプを打ってくれる機能があるデバイスがあったりします。具体的には STM32 の input capture とかですね。使えるピンが限定されているので注意。

なのですが、Raspberry Pi Pico というか RP2040 にはそういう機能はありません。ざんねん。

しかし、直球のそういう機能はないものの、代わりに(?)PIO(Programmable I/O)があります。

www.raspberrypi.com

youtu.be

これは大変 気持ち悪い 便利なペリフェラルで、GPIO に接続できる超軽量なコプロセッサです。 これをうまく活用すると、メインのソフトウェアでは不可能なほど高速に GPIO をピコピコさせることで胡乱なプロトコルをラズピコから喋らせることができます。めちゃめちゃ便利。もはや軽量 FPGA ぐらいの使い勝手です。他のあらゆるマイコンにも欲しい。

PIO の入門は tnisinaga さんのこのスライドが便利です。

speakerdeck.com

さて、こんな便利な物体があればパスタも茹でられそうです。

この PIO に PPS のパルスの立ち上がりを見張らせて、エッジが着た瞬間のカウンタ値を FIFO に入れれば、input capture もどきの完成です。

.wrap_target
low:
    jmp pin rising   ; ① PPS ピンが high なら捕捉へ飛ぶ。low なら次へ落ちる   (1 cycle)
    jmp x-- low      ; ② X-- して、X が 0 でなければ low へ戻る               (1 cycle)
    jmp low          ; ③ X が 0 に達した時だけここに来る。偽エッジを出さず継続  (通常は通らない)
rising:
    in  x, 32        ; 立ち上がり検出。今の X を ISR へ
    push noblock     ; ISR を RX FIFO へ(メイン CPU が後で読む)
high:
    jmp x-- highchk  ; high の間もカウンタは回し続ける
highchk:
    jmp pin high     ; ピンが落ちるまで待つ(同じパルスで再トリガしない)
.wrap

これを雑にまとめるとこんなかんじで、ループとしては実質2命令になっています。

loop:
    if pps_pin == HIGH:        # jmp pin
        capture(x); break
    x -= 1                     # jmp x--

とすると、125MHz では 2 cycle は 16ns です。だいぶパスタ茹でられそうですね。

しかし、まだ安心してパスタを茹でることはできません。 なぜなら、これで分かるのはあくまで PPS の立ち上がりの瞬間だけだからです。 正確には、RP2040 の内部時計から見て PPS がいつ上がったのかが分かるだけです。 そして、この内部時計というやつはあんまり、いやかなり、いやものすごく、正確ではありません。

この内部時計というやつは Raspberry Pi Pico に搭載されている水晶発信器から作られたクロックからなる時計です。 「動作周波数」ってやつの源泉がこれですね。

https://www.marubun.co.jp/wp-content/uploads/2026/02/9c22c8925216cd3e56f35e25440cc8c7.webp www.marubun.co.jp

で、これがどれぐらい不正確なのかというと、この時計は1秒あたり10µs、1日だと0.86秒とかのオーダーでズレます。 かなしい。 まあ、原子時計とかには逆立ちしても勝てないのは仕方ないとしても、もうちょっとイイやつを使うと1~2桁ぐらいはマシになります。 時間にセンシティブなことをするならちゃんとしたクロックを使えと言われる理由がわかりますね。 しかし今回はどこの家にもあるようなモノで作りたいです。どうにか Pico だけで頑張りたい。

また、PPS のパルスの瞬間だけを扱っても仕方がありません。 パスタを茹でている途中に「茹で上がりまでは何 ns だっけ?」と思ったり、ザルを鍋から上げようとしたりする際、PPS のパルスとパルスの間ではこの内部時計を信じるしかないので途端に精度が落ちてしまいます。 そこで、1PPS のパルスのタイミング幅を計測し続けることで、この内部時計というやつがどれぐらいズレているものなのかを推定し続け、クロック周波数を微調整していけば内部時計も正確になっていき、PPS のパルスとパルスの間の時刻も正確に知ることができます。 これを GPSDO(GPS Disciplined Oscillator)/GNSSDO(GNSS Disciplined Oscillator)と言います。 こうすると、衛星からの信号をロストしても、ある程度は正確な時間を刻み続けることができます(holdover といいます)。

今回はこれを真似して、別のピンから PPS と同期したパルス信号を出してみることにしました。 ただ、Raspberry Pi Pico にはこの話で意味のある精度でクロック周波数を弄ることのできる API がありません。 そこで発想を転換して、RP2040 の動作周波数自体を変えるのではなく、あくまで PPS 信号に正確に GNSSDO 信号タイミングを同期することを考えます。 この時に必要になるのが、1PPS のタイミング幅の正確な計測による自クロックと 1PPS の比率(とその傾向)と、GNSSDO 信号と PPS 信号の同期誤差の確認です。

ここで、前者はやるだけとして、問題は後者です。 ここで少し HW に工夫をします。 具体的には、Pico GNSSDO 信号の出力ピンからまた別のピンに入力するループバック接続をすることで、PIO を使って PPS 信号と GNSSDO 信号の誤差を高精度に内部で計測可能にします。

GNSSDO PPS を loopback 接続して PIO で元 PPS と位相誤差を計測する

誤差を計測することができたらやることはひとつですね。そう、制御器を組んでフィードバックループを組んで収束させます。

これにより、どんな制御手法ならどう収束していくのかをファームウェアから観測することもできます。 実際に可視化してみると、PID の各成分が教科書的に出て面白いですね。

GNSSDO(もどき)の制御器ごとの収束の様子(loopback PIO 観測)

また、PIO なら高精度な制御ができることも観測できます。

なんか 18ns とか出てそうですね。最高じゃん。

これで、パスタはたぶんあんまり伸びずに済むようになりました。 しかし、時刻同期/信号の同期という観点ではどうでしょうか?念のためオシロスコープで GPS-R PPS 信号と Pico GNSSDO 信号を比較してみましょう。

オシロスコープで見てみると、GPS-R PPS (CH1) と Pico GNSSDO (CH2) が 100ns オーダーで offset している

なんと 300ns 近くもズレています。このズレは Pico を再起動する度に変化します。 つまり、実はパスタの茹で上げ時刻は正確に合わせられていません。 例えば友人と一緒にパスタを食べる約束をしている場合、友人が原子時計を持っていても、なんと最大 80µs ほど茹で上がりが遅刻してしまう可能性があります。困りますね。

これはなぜかというと、PPS 捕捉に使っている PIO と GPSOD loopback 捕捉に使っている PIO が別の PIO state machine で動いているからです。 それぞれのカウンタは同じクロックで lockstep されて動いているものの、これらの state machine はあくまで別々に動いているため、このカウンタ同士は offset してしまっています。 そのため、制御はちゃんと収束して jitter は抑えられているものの、そもそもズレたカウンタに対して計測・制御をしてしまっているため、ここからは見えない offset 誤差が乗ってしまうのです。

Offset と Jitter の違い

jitter が収束していても固定の offset があっては台無しです。 ただ、PIO はあくまで同じクロック上で動いています。そのためこの offset は固定されているはずです。 そこで、このオフセットを計測します。方法は簡単で、制御を開始する前などたまーに最初に GNSSDO loopback 用 PIO state machine でも PPS を捕捉します。 PIO がどの GPIO にも気軽に接続できるからこそ可能な芸当ですね。

別々の PIO State Machine のカウンタはオフセットしているため、一時的に両方の PIO State Machine から PPS エッジを観測することで校正する

しかし、これで問題は解決したかと思いきや、長時間観測しているとどうにもまたオフセットしていきます。 そのため、定期的に校正をするようにしてみます。

長時間放置しているとだんだんオフセットしていくため、定期的に校正する

こうすると、たまに暴れてオフセットが 100ns とかになることはありつつ、測位が安定していれば全体的にはオフセットは 40ns ぐらいの幅にほとんど収まるようになりました。

定期校正導入後の長時間動作中のランダムな瞬間

ここまで色々やると、ようやくサブ 100ns パスタを安心して食べることができます。

しばらく待つとちゃんとサブ100nsオーダーに収束する

ただ、定期校正が必要なのはちょっと気持ち悪いです。 そこで、もう一つ PIO State Machine を増やして GPS-R PPS を観測してみます。

GPS-R PPS 観測 PIO SM を増やしてズレの原因を切り分ける

これにより、別の PIO で観測していることによって発生しているズレなのか、一時的な PIO の観測ピン切替による影響が発生しているのかを切り分けることができます。 追加したカウンタと GPS-R PPS 用カウンタの差はほとんど無いので、どうやら原因は一時的な観測ピン切替にありそうです。

ということで、校正の切替時に何が起きているかをよく見てみます。

sm.set_config(cfg_gp2);  // 観測先ピン切替

fn set_config(cfg) {
    // レジスタ一式の設定 (観測ピン設定は EXECCTRL の一部)
    write CLKDIV / EXECCTRL / SHIFTCTRL / PINCTRL;
    exec_jmp(top);  // 走行中の SM を top へ強制ジャンプする
}

このうち、レジスタの書き込みが悪いのか、それとも exec_jmp(top) が悪いのかを切り分けるために、set_config() と、一部レジスタのみへの書き込みを先ほど足した追加観測ピンに対してやってみることにします。

追加観測ピンを使ったカウンタが遅れる原因の切り分け

これを見ると、単なるレジスタ書き込みだけでは影響が無いものの、明らかに exec_jmp(top) に相関してカウンタが遅れることがわかります。 この原因は PIO ロジックの中身を思い出してみるとわかります。

// PIO ロジック
top:
    jmp capture if pin high   // 立ち上がっていれば捕捉へ
    X = X - 1                 // 立ち上がり待ち(1 tick 進む)
    jmp top
capture:
    push X to FIFO            // 捕捉
fall:
    X = X - 1                 // 立ち下がり待ち(1 tick 進む)
    jmp fall if pin high      // high の間は繰り返す
    jmp top

つまり、基本的にカウンタ X が下がり続けるはずのところで、突然ホストから top へのジャンプが指令されるため、それによって X = X - 1 がスキップされてしまうタイミングが発生しています。

そこで、この top へのジャンプをなくしてみます。実際に観測ピンの切替に必要なのは EXECCTRL というレジスタへの設定だけなので、もっとシンプルにすることが可能です。

sm.execctrl().modify(|w| w.set_jmp_pin(GP2));

これでまた実験してみると、まだ謎のドリフトは残っているので定期校正自体は必要ですが、定期校正した上での長期的な中央値のオフセットは改善しました。

ドリフトは消えないが、中央値のオフセットは改善した

しかし、対症療法が効くとはいえ、謎のドリフトがあるのは気持ち悪いですね。 デバッグしましょう*2

まずは、GNSSDO 出力側に何か問題があり、制御ループの外でドリフトしている可能性を考えてみます。 この検証のため、途中から露骨に追加でドリフトさせるような処理を注入してみたところ、正しく制御されるので特に最終的なドリフト挙動には変化がみられません。

制御ループの外に途中から意図的に +1ppb のドリフトを注入してみても、正しく打ち消される

また、定期校正と同じ方法で、カウンタのズレの記録だけして校正はしないで放置してみたところ、カウンタのズレと実際の信号のズレはちゃんと相関していそうなことがわかります。

定期校正と同じロジックで、ズレの記録だけしてみる

ここで、PIO をどんなロジックにしていたのかを思い出してみると、PIO のカウンタを 2 cycle ごとに1つ減らす、ということをしていました。 先ほどは擬似コードにしていましたが、実物の GPS-R PPS 補足 PIO アセンブリはこのようになっています。

.wrap_target
low:
    jmp pin rising    ; 立ち上がっていれば捕捉
    jmp x-- low       ; カウンタを 1 減らし、0 以外なら low へ (ふだんはここまでの 2 命令で 1 tick = 2 cycle)
    jmp low           ; カウンタが 0 だった回だけここを通る (+1 cycle)
rising:               ; いまのカウンタの値を FIFO へ送る
    in x, 32
    push noblock
high:
    jmp x-- highchk   ; カウンタを 1 減らす(+1 cycle)
highchk:
    jmp pin high      ; まだ high なら high へ (ここまでの 2 命令で 1 tick = 2 cycle)
.wrap                     ; ここまできたら .wrap_target へ戻る(この巻き戻しは命令 fetch に組み込まれているので 0 cycle)

これをよーく見ると、low(PPS 立ち上がり待ち)のとき、カウンタが 0 の時だけ 1 cycle 分命令が多く実行されることがわかります。 PIO における jmp x-- <label> 命令は、X が0以外の時は <label> にジャンプするのですが、X が 0の時はジャンプせず次の命令に fall through します。 そのため、high の時は jmp x-- highchk はそもそも常に分岐せずカウンタを減らすだけ*3なのですが、low の時は実行命令数が 1 cycle 分増えてしまいます。 これにより、通常時は 2 cycle に一度カウンタが減算される、つまり 16ns ごとにカウントしていたわけですが、このときだけカウンタの進みが 1 cycle = 8ns 止まってしまうわけです。

PIO のこのカウンタは 32bit なので、16 ns * (2**32) ≒ 68.7 s ごとに一周します。 結構頻繁に起きそうですね。

32bit の PIO カウンタが 0 を跨ぐ時だけ 1 tick = 3 cycle になってしまう

そして真の問題は、このロジックで GPS-R PPS と GNSSO loopback の両方の観測を行っていたことです。 今回の場合は GNSSDO loopback のパルス幅と GPS-R PPS のパルス幅を変えていたこともあり、1 tick = cycle になるタイミングがズレてしまい、GPS-R PPS 観測カウンタと GNSSDO loopback 観測カウンタがズレ続けてしまっていました。

そこで、GNSSDO のパルス幅を low の幅含めて GPS-R 1PPS に合わせてみると、だいぶ安定することが分かります。 パルス幅を変えずに low の時の 0 跨ぎも 3 cycle になるようにしてみても安定しますね。

GPS-R PPS 観測カウンタと GNSSDO loopback 観測カウンタの0跨ぎの周期を揃える

ここまでやると(再)、Pico GNSSDO 信号はの精度はオフセット誤差の中央値が +8.0ns、±100ns 内(サブ 100ns 精度)達成度合いは全体の81.4%を達成することができました。 これでそこそこ安心してパスタを食べられそうです。

実際の収束の様子のアニメーション

ちなみに、サブ100ns達成度合いが8割というのはどうにかならんのか、というのが次に思うところですが、これは今回のハードウェア構成だとたぶん厳しいです。 というのも、結局のところ Pico のクロックの精度が悪いんですよね。 具体的には精度が温度によってかなりバラつきます。

ロック後にオシロスコープを眺めながら RP2040 のチップを指で触ったりすると、温度が急上昇して GNSSDO 信号のオフセットがズパーンと数百~数千ns ぐらい吹っ飛んでいく様子を眺めることができます。

こういう時、一般的にはどうするべきかというと、温度補償型水晶発振器(TCXO)というものを使うべきです。 www.macnica.co.jp

ですが、まあなんかどうにかならんのかという気はしますよね。 で、考えてみると、水晶発信器そのものとはちょっと離れてはいるし、精度は低いものの、RP2040 にも温度計が載っているんですよね。なんかなんとかなりそうじゃないですか?

ということで RP2040 温度を使ったフィードフォワード制御実装してみました。 が、手で触るみたいな急な温度変化による急激な変化自体は防げないものです。

急激な温度変化に伴う過渡応答

ただ、これは流石に反応しすぎなので、リミッタは設けました(急激な温度変化の条件を合わせるのは面倒なので比較は雑)。

過渡応答にはリミッタをつける

微妙に効果が分かりにくい温度制御ですが、これが役に立つのは長時間運転した時です。 長時間運転してみると、気温がバラついているとき(これはたぶん夜中のエアコンの制御をもろに喰らってる)、つまり温度外乱が大きいときの GNSSDO オフセットの標準偏差がだいぶ改善していることがわかります。

温度制御の長時間運転での効果

最終的な実装での挙動はこんなかんじになりました。

最終的な実装での GNSSDO 信号のオフセット収束の様子

実装はこちら。 今回の実装のうち、ターゲットフリーな部分を gnssdo crate、PIO による input capture / loopback などの Raspberry Pi Pico 特有部分の実装を rp-pps crate というライブラリに、全部込みのファームウェアを pico-gnss という crate に分けてあります。 麺類や麺類以外のものにご活用ください。

github.com

結論

パスタにちゃんとした保証が欲しいときは qErr 補正機能付きの GNSS 受信機を買って空のひらけたところで TCXO を使った機械で茹でましょう。

*1:数年前に終売していてかなしい。今も中身同じモジュールのやつは打っているんですが、学生ロケットとか CanSat とかロボットみたいなモノを作る時にはあの全部一体なブレイクアウトボードがラクだったんですよね

*2:ちなみに、このデバッグは Opus 4.8 も Fable も沼り続けました

*3:なにもしないのに jmp なのは、単にカウンタを減算するだけの時の PIO の命令というか作法がこうだからです

エーアイに 3D プリントさせる

少し前にエーアイに 3D CAD(OpenSCAD)をやらせてみていました。

sksat.hatenablog.com

ちなみに、輸出規制パンチを喰らう前にこの胡乱ベンチマークに Fable 5 も追加しておきました。 そして結構性能が高くて、ちゃんと比較するためにもっと†実用的†なテストケースを用意しないとなあ、まあやるとしても週末かな、と思っていたらパンチを喰らってしまいました。ざんねん。

vibe-openscad.sksat.dev

それはそれとして、3D モデルをエーアイに作らせるなら、もしくは直接作らせることはしないとしても、エーアイ経由で 3D モデルを用意してシュッと印刷ができると便利そうです。KiCad から基板のモデル作るとかね。

ということで、普段よく使っている Bambu Lab の 3D プリンタを操作するためのライブラリ・CLI ツールを作りました。

cargo binstall bambu-rs でインストールできます。

github.com

以下のように使えます。

$ bambu config add --printer a1 --ip 192.0.2.50 --serial <SERIAL> \
  --access-code <CODE> --model a1mini

$ bambu status
printer: - (a1mini)
state:   FINISH
temps:   nozzle 25.5°C / bed 25.2°C
timelapse: disable
speed:   standard (2)
progress: 100% (layer 240/240)

$ bambu calibrate --confirm --watch
starting calibration: bed level + vibration + motor noise …
calibration started; watching until it finishes …
RUNNING    0%  layer 0/0  N28 B30  [homing_toolhead]
RUNNING    0%  layer 0/0  N28 B60/65  [heatbed_preheating]
RUNNING    5%  layer 0/0  N28 B65  [cleaning_nozzle_tip]
RUNNING   25%  layer 0/0  N28 B65  [auto_bed_leveling]
RUNNING   55%  layer 0/0  N28 B41  [sweeping_xy_mech_mode]
RUNNING   82%  layer 0/0  N28 B31  [calibrating_motor_noise]
RUNNING  100%  layer 0/0  N28 B30
FINISH   100%  layer 0/0  N28 B30
printer: - (a1mini)
state:   FINISH
temps:   nozzle 27.8°C / bed 29.6°C
timelapse: disable
speed:   standard (2)
progress: 100% (layer 0/0)

$ bambu job start ./benchy.gcode.3mf --upload --plate 1 --confirm --watch
uploaded 1187446 bytes to /benchy.gcode.3mf
starting print: /benchy.gcode.3mf
print started; watching for completion / anomalies …
PREPARE    0%  layer 0/240  N28 B55/55  [heatbed_preheating]
PREPARE    0%  layer 0/240  N28 B55  [auto_bed_leveling]
PREPARE    0%  layer 0/240  N150/220 B55  [heating_hotend]
RUNNING    1%  layer 2/240  N220 B55  ETA 1h08m
RUNNING   10%  layer 24/240  N219 B55  ETA 1h01m
RUNNING   35%  layer 84/240  N220 B55  ETA 42m
RUNNING   68%  layer 163/240  N220 B55  ETA 19m
RUNNING   99%  layer 239/240  N219 B55  ETA 1m
FINISH   100%  layer 240/240  N208 B54
printer: - (a1mini)
state:   FINISH
temps:   nozzle 208.4°C / bed 54.1°C
timelapse: disable
speed:   standard (2)
progress: 100% (layer 240/240)

あと、自動化した処理に組み込みやすくするため、--json を付けるとログが JSON で出るようになります。

また、bambu serve にリアルタイム Web Dashboard も組み込んでいるので、ブラウザ経由でジョブを投げたり、印刷の様子を眺めたりすることができます。

Web Dashboard

さらに、プリンタ組み込みのカメラではなく、自由な外部カメラを使うこともできます。これは Dashboard から見えるようにすることもできますし、layer event に合わせて SNS でたまによく見るヌルヌルしたタイムラプス映像を撮ることもできます。

たまによくあるタイムラプス

余談

改造した ATOM cam が転がっているのを思い出したので、A1 mini の近くに放って「3Dプリンタあるから観察して」などと言ってみました

首を振って A1 mini を見つめる Claude

他の適当な Web カメラ でも。Opus 4.8 はだいぶ視力付いたので便利ですね。

こんなかんじで最近色々なデバイスをエーアイに繋いでいるんですが、3D プリンターみたいな動くモノも繋いで色々やっとけと伝えて放置するとたまに不意にガチャガチャやり始めるので、ポルターガイスト感強いですね。

あと、開発途中にエーアイにテキトーな gcode 書かせて実験してたらフィラメントが詰まりました。エーアイ許せねえ。 Human はノズルの掃除や印刷物の除去などをさせられます。

KiCad の diff、kicadiff

基板設計してる時にも diff を見たいです。見たいよね?

KiCad は Git との相性がよい(KiCad 9 からは本体にも連携機能が組み込まれている)ですが、それはあくまでファイル管理の話でしかなく、git diff しても、GitHub Pull Request の Files changed を見てもそこには無慈悲な S 式の差分があるだけです。

ということで diff 可視化ツールを作りました。

github.com

使い方

git diff みたいなかんじで、Git 管理した状態で編集して kicadiff すると、差分を可視化した HTML が生成されます。 kicadiff --open すれば xdg-open で勝手によしなにブラウザなどで開いてくれます。

HTML 出力での差分表示

VSCode で開いているときに kicadiff --open vscode すると生成された HTML のタブが開かれます。 Simple Browser extention とかを使っていれば VSCode で KiCad を弄るとき(?)にも VSCode で完結して diff を見ることができます。 Claude Code に基板設計させたいときなどにどうぞ((やってみたなんとも言えない結果は examples/ に置いてあります))。

Claude Code の編集時に使うための kicadiff hook サブコマンドもあります。 Edit/Write tool とかの PostToolUse hook から呼ぶ想定です。 具体的には examples/blink/.claude/settings.json を見てください。

GitHub Action も用意したので、Pull Request でこんなこともできます。

PR description や PR comment での表示
github.com

実装

Bun でババーン

仕組み

kicad-cli で svg/png をレイヤーごとに生成して重ねる、以上

おわりに

3D CAD だけでなく基板設計もいまだにかなり人間の仕事で大変

エーアイ、3D CAD もやってほしい 2026

Claude Code などの Coding Agent を使っていると本当に便利です。 特に最近のモデルの性能は本当に高いですし、最近だともはや普通の話ですが手で直接コードを書くことの方が少ないというかもはやほとんどなくなっています。

で、それはいいとして。じゃあもうエーアイでなんでもかんでもできる、と言われるとんなわけないだろとなります。 その中でも特に顕著なのが、時空間認識能力の欠如です。

まあこれは仕組みを考えてみると仕方のないことです。今一番 AI と呼ばれているものは基本的に LLM をベースにしている技術なので、これは「言語」モデルなわけです。 そのため、「言語」の空間内に収まる問題を扱うのは非常に得意ですが、そこに収まらない問題は不得意です。 もちろん「言語」の空間に収まる問題はとても広範に渡るのでそれでも十分に有用ではありますし、だからこそ今とても持て囃されているわけです。 特にコード書く時に多くの問題は「言語」の中に収まりがちですし。

しかし、なまじ「言語」の空間に収まる問題がエーアイで薙ぎ倒せるようになったので、他もどうにかなってほしいものです。 あと、そもそも「言語」に問題が収まるかどうかはグラデーションでしかないということもあり、自然と適用範囲を拡大していったら突然性能がガタ落ちして風邪を引くような感覚があります。

例えば時間感覚のなさでいうと、普通にソフトウェア開発をしているときでも、短時間のうちに色々操作をしたらうまくいかない、みたいな挙動の調査というか発見とか苦手ですよね。あととりあえず sleep 5 してみるやつとか。sleep してない時以外の Agent との会話ラリーの間にも時間経ってるんですよね、エーアイ君はあんまりご存知無いと思うんですが......

ただ、時間の方の問題は、しばらくやってるといくらかやりようはあるのかなと思えるようになってきました。まあ機敏な computer use みたいなやつはまだまだ大変そうな気はしつつここはかなり投資が入ってきていそうですし、応答が速いモデルみたいなのもできてきています。......というのは基盤モデルの応答が速くなれば Agent の認識のズレが軽減される方向での解決ですが、それ以外にも、脊髄反射的な役割を持たせた小さいモデルを別で動かしておく*1とか、ずっと録画的なことをしておいて即座に反応する必要をなくす/タイミング情報を残して Agent に伝えやすくするハーネスを作るとか、結構実用的なパターンが発生してきている気がします。ロジックアナライザのログ取っておいて渡すとかね。

なんですが、空間認識能力の欠如は結構厳しいなとずっと思っています*2。そしてどうにかなってほしい。 ハーネス(物理)かしめてほしいし、コンポーネントの組み立て(物理)してほしい......というのは高望みだとしても*3。CAD 設計とか基板のアートワーク設計とかしてほしい*4です。してくれ。

ということで(?)、どうにかなんないかなあと思う度にエーアイに CAD やらせる実験を去年あたりからちまちまやっていました。 世の中には OpenSCAD や CadQuery という、コードで 3D CAD が可能な物体があるので、これは OpenSCAD でやっていました。

まあめんどくさいですね。構造設計みたいなやつってあんまり設計意図から最終的な出力の間にあんまり言語が挟まっていないような感覚があります。なのでこれをあんまり三次元空間に気持ちが無い相手に、言語という手段だけで設計意図を伝えてコードで開発させるのはだいぶしんどいです。言語化が大変すぎる。特に去年ぐらいのモデルだと自分でコード書くのとほとんど変わらないぐらいの言語化が必要で、これは非実用的だなあと思ったのでした。

その後、Opus 4.5 にだいぶ感動していたので、年始にまた試してみていました。 ただ、これも意図の汲み取り力が上がったことによって一発目の出力は多少はマシになったようななってないような、という印象でした。

OpenSCAD は CLI からレンダリング結果を出力できることに気付いたので、これでフィードバックループを組んだりもしてみたんですが、Opus 4.5 の視力は深海魚並みなので効果はいまひとつでした。Z fighting を一生理解してくれません。

ただ、この時はモデルの性能が上がったというよりは僕が(Fusion 360 で)3D CAD とそれの設計のカンをだいぶ得られていたのもあり、トータルでは多少は実用的にはなりました。OpenSCAD を手で書くのと手で parametric なことをやるのがめんどいのをやや仲介できることを実用的と呼ぶならですが...... あともちろん強い制限はあり、(OpenSCAD の利点が活きる)parametric な設計や、modular な設計に利点がある、ないしはそのような作り方をしても困らないようなモノの設計にのみ使っていました。 具体的にはコネクタがめっちゃ付いてる箱、みたいな治具をたくさん作る、みたいなやつです。

......というのが前座で、その後 Opus 4.7 が出て視力が多少はマシになったりしたのと、そういえば Gemini は multi-modal だけはいいよなと思って Gemini 3.1 Pro preview で適当なモデルを作らせてみたらそんなに悪くないモデルが出てきたな、と思うなどしていました。

そこで、エーアイの空間認識能力ないしは OpenSCAD 力をその時なんとなく試しただけで測りたくないなと思い、大量に比較して一覧できる Web サイトを作ってみた、というのが今回の話です。

vibe-openscad.sksat.dev github.com

ここでは、色々なモデルプロバイダの色々なモデルに対して色々な OpenSCAD タスクを色々なカタチで投げてその結果を一覧できるようにしてあります。 立方体に穴開けるカンタンなタスクだとこんなかんじです。さすがに多くのモデルが平然とできますね。 単なるモデル違いだけでなく、Claude Code みたいなハーネスをつけたときにはどうか、みたいな区別をするために、何もしていないものは bare と呼んでいます。また、reasoning effort ごとでの違いや、ここに表示している OpenSCAD のレンダリング結果の png を注入してイテレーションを回した時なども出しています。

中央に貫通穴を持つ立方体

vibe-openscad.sksat.dev

で、こんなタスクでも、ほとんどのモデルが平然とこなす一方で、gpt-5-nano-2025-08-07gemini-flash-lite-3.1-preview とかは Z fight 起こしたりしています。3D プリントする時とかにはどうせ厚さ0の板とか存在しないに等しいのでまあ悪くはないんですが、ちょっと怪しさがありますね。

Z fight してる

vibe-openscad.sksat.dev

階段状のピラミッドを作るのも、gemini flash-lite 2.5 の thinking off の時以外は悪くないです。

階段状のピラミッド

問題はその次です。エーアイにはどうやらマグカップを作るのはかなり難しいタスクのようです。

Claude 作マグカップ
GPT 作マグカップ
Gemini 作マグカップ
vibe-openscad.sksat.dev

gemini flash-lite 2.5 はだいぶ前衛的なマグカップを作りました。マグカップとは。 vibe-openscad.sksat.dev

ちなみに、各試行ごとの詳細ページもあり、単発のレンダリング結果だけでなく、STL をグリグリして眺めることや、コードの中身を見ることもできます。openscad-wasm なる物体を見つけたので、パラメータを動的に弄って STL を変えられるようにもしておきました(あんまり安定はしてないです)。

各タスクに対するモデルごとの試行詳細画面

また、イテレーションさせた時の diff 表示などもあります。gemini flash-lite 2.5 は例によって頓珍漢なことをずっとやっていますが。

他 variant/iteration との比較表示

そして、ここまではまだモデルごとの差はそこまで大きくなかったのですが、金具の設計のような実務的なタスクをやらせてみるとだいぶ差がついてきます。

皿穴付き L 字金具を設計するタスク

これを見ると、ちょうど直近リリースされたモデルである GPT 5.5、Opus 4.7、Gemini 3.1 Preview あたりはまあまあ実用的かもなという印象を受けます。他のタスクを見ても GPT 5.5 と Gemini Pro 3.1 Preview はだいぶマシな印象です。

vibe-openscad.sksat.dev vibe-openscad.sksat.dev

蝶番とか作らせてみるととより顕著に差が出ますね。

バット蝶番を設計するタスク

さらにより実務的な課題として、データシートにある外形情報からモデリングをさせるタスクなども与えてみています。

データシートの外形情報からモデリングさせるタスク

https://jp.sharp/products/device/doc/opto/gp2y0d413k_e.pdf

データシート内の外形情報(一例)

これがちゃんとできると、データシートもらってから 3D CAD 内でのフィットチェックであったり 3D プリントして組み立て性を確認したりするやつがだいぶラクになるんですよね。特に最近の 3D プリンタはメンテフリーで安定した造型を高速に行うことができますし、ものによっては自動でスライスしたりジョブ投げたりもできるので、やろうと思えば人間は「**のモデル作って刷っといて」とエージェントに言って、プリント完了通知がきたらビルドプレートからモデルを剥がすだけ、みたいな体験も作れそうです。

ちなみに、データシートとかは Google Drive に突っ込んでおくと、最近だと Google Workspace CLI でファイル検索してダウンロードさせて読ませる、とかもできて便利ですね。まあシンプルに検索とダウンロードができればなんでもよくはあるんですが。仕事だとメールも読ませられるのも結構便利ですね。メール下手人間なんで。

github.com

それはそれとして、こういうベンチマーク的な遊び、金かかりますね。

マネ~

あと、この記事を公開するかと思ってタイムラインを見たら Claude に Autodesk Fusion connector がきていました。ホンマかいな。

*1:Devin のブラウザ操作とかはこれやってるんじゃないですかねえ。ブラウザ操作得意すぎるし速すぎると思う。まあ単純に VM が海の向こうにあるから全部レイテンシが小さいというオチもまあまあありますが......

*2:今回の話のスコープ外ですが、座標変換とかもかなり苦手なタスクの印象があります。まあ変な座標系を生やさない限りは丁寧にやれば testable にするのは割と自明なので、TDD でやれば収束はするんですが。これの前の記事のシミュレータの座標変換ライブラリもそうやって作っています。

*3:まァ†Physical AI†なのかもしれません

*4:まあ基板設計は KiCAD あるからまだやる気になるんですが、マトモに使いやすい 3D CAD が全部プロプライエタリで自動化ないしは趣味との相性が悪くてしんどい

orts: 人工衛星シミュレーションプラットフォームを作りました

人工衛星向けのシミュレーションプラットフォームである orts をリリースしました。

github.com

単一のツールではなく monorepo になっており、数値計算や座標変換、地球周辺環境などのライブラリの集合体のような実装になっています。計算ロジック部分はすべてフル Rust 実装です。

また、Web viewer も付属しており、計算結果をリアルタイムないし後からブラウザで簡単に可視化することができます。

Realtime Viewer

各ライブラリを使ってもらうのでもよいですが、まずはじめは orts-cli を使ってみてもらうのがよいと思います。あまり込み入ったユースケースでなければこれで十分な場合が多いかもしれません。

cargo-binstall に対応しているので、ビルド済みバイナリを簡単にセットアップすることができます。

$ cargo binstall orts-cli
$ orts serve   # open http://localhost:9001 with browser

Realtime Viewer

この cli バイナリには Realtime Viewer を同梱しているので、serve サブコマンドを走らせてブラウザを開くだけで簡単なシミュレーションを行うことができます。Configure から Preset もしくは TLE などで軌道を設定すると、バックエンドでシミュレーションが開始され、その様子をブラウザからリアルタイムで軌道や姿勢の様子を観察することができます。

ISS Preset のシミュレーション

この Viewer では可視化をどのような座標系でやるかを動的に選択でき、ECI/ECEF の切り替えや機体中心での可視化を高速に行うことができます。

機体中心での表示

このプロジェクト内の座標変換ライブラリである arika を WebAssembly ビルドして Viewer でも使っているため、可視化のための座標変換をシミュレーション本体から分離した上で、ブラウザ内で高速に、かつミスなく・一貫性をもって行うことができます。

また、Viewer では時系列グラフも提供しており、様々なメトリクスをさっと確認することができます。 これには DuckDB-Wasm と uPlot を活用しています。 duckdb.org github.com

github.com

さらに、この2つの組み合わせとその Web worker 化、time range を切り替えた時の downsampling などを組み合わせたライブラリとして uneri というものを用意しており、Viewer でもこれを使っています。 sksat.github.io

データフォーマット(Rerun ベース ECS データモデル)

シミュレータを作る時にいつも悩まされるのがデータ保存のフォーマットです。 より正確には、悩まされるというよりは CSV でエイっと作ってしまって CSV に苦しんだり、大量の浮動小数点数を文字列にする効率が悪すぎたり精度が落ちてしまったりして困ることがまあまあよくあります。

このために注目したのが Rerun というデータプラットフォームです。

rerun.io

これは Robotics など向けのデータロギング・可視化のためのプラットフォームで、Rust を含む複数の言語向けのライブラリが整備されています。最近はめちゃめちゃ Physical AI を意識していそうです。

Entity Component System(ECS)のコンセプトや、Apache Arrow ベースの Chunk を使っているといったコンセプトがちゃんとイマドキで効率もよくていいなと思って採用しています。ただ、Viewer に関してはあんまり気持ちが合わなかったりどうにも不安定なことが多いので自作しました。

また、可視化プラットフォームあるあるとして、場合によって/ユースケースによって見たいデータの性質がまったく異なるので、ハンズフリーというかノーコードなプラットフォームだけで頑張るのは根本的に無理があるという問題があると思います。よくあるところだと、Grafana は痒いところに微妙に手が届かない、みたいなやつです。

ただこれに関しては、今の時代はプラットフォーム部分はライブラリないしはテンプレートという形で用意しておき、その場その場でエーアイパワーで viewer を濫造してしまえばいいと思っています。 実際、ここ数ヶ月ぐらいは仕事でも様々な Dashboard を濫造しています。みなさんがどのぐらいエーアイ使ってるか可視化するやつとかね。

そのため、orts-cli に同梱している Viewer も別にこれだけを使ってもらうのではなく、とりあえずさっとリアルタイムで簡単なことが見られて便利、という用途を想定しています。

さらに、rrd だとそれはそれで雑に別のスクリプトで解析するのが面倒だったりもするので、orts-cli には convert サブコマンドがあり、CSV への変換もサポートしています*1。 CSV から matplotlib で雑に可視化する Python スクリプト書くのはどのエーアイも超得意ですからね。

$ orts convert --format csv hoge.rrd --output hoge.csv

example の紹介

「で、何ができるの?」という疑問にお答えするために、example を色々紹介しようと思います。 色々できる単一のシミュレータ、というよりは汎用的なシミュレータライブラリという側面の方が強いので、方向性の違う具体から考える方がわかりやすいためです。

アポロ11号の軌道計算

sksat.github.io

これは単にやってみた以上のことはないですが、ナイーブな軌道計算です。 ただこういう複数の天体の重力を受けるようなある程度長時間の計算って、雑にやるのはそんなに難しくないんですが高精度にやるのは結構難しいので、ある種のベンチマークとしてよいなと思っています。 そして NASA がデータを公開しているので評価もしやすいです。

軌道寿命解析

sksat.github.io

https://raw.githubusercontent.com/sksat/orts/main/orts/examples/orbital_lifetime/altitude_history.png

これは見た目的にはあんまりおもしろくないですが、仕事ではまあまあやるやつですね。 仕事で作っている人工衛星はほとんどが地球低軌道と呼ばれる比較的低高度の軌道に投入する人工衛星なので、薄いとはいえしっかり存在している大気抵抗の影響でだんだん高度が落ちていってしまいます。

そのため、これがどのくらいのペースで落ちてくるのかが人工衛星の寿命に直結します。 これが宇宙活動法の申請書類やら衛星コンステレーションの設計やらで結構重要なんですよね。

また、地球周辺環境ライブラリである tobari の内部で NRLMSISE-00 などの大気モデルをフル Rust で再実装したため、これの正確性の E2E での検証にもなっています。

この例では某弊社の人工衛星 AE1b の公開情報から計算・検証を行っています。

WebAssembly Controller Plugin

これはかなり特徴的な機能だと思います。人工衛星側の姿勢制御などのロジックを WASM の Plugin として動的にシミュレーションに組み込むことができる機能です。 これにより、制御則を単体で検証するようなことが非常に容易になります。

以下は例えば Bdot という角速度を減衰させるのによく使われる制御則を実装したものです。

struct Component;

impl bindings::Guest for Component {
    fn run(config: String) -> Result<(), String> {
        let cfg = Config::from_json(&config)?;

        let gain = cfg.gain;
        let max_moment = cfg.max_moment;
        let mut prev_b: Option<[f64; 3]> = None;
        let mut prev_t: f64 = 0.0;

        loop {
            let input = match wait_tick() {
                Some(input) => input,
                // ホストが shutdown を要求している。
                None => return Ok(()),
            };
            let b = input
                .sensors
                .magnetometers
                .first()
                .ok_or("magnetometer not available")?;

            let b_mag_sq = b.x * b.x + b.y * b.y + b.z * b.z;
            if b_mag_sq < 1e-60 {
                send_command(&zero_moment());
                // prev_b/prev_t を更新しない — near-zero サンプルは無視
                continue;
            }

            let cmd = match prev_b {
                Some(prev) => {
                    let dt = input.t - prev_t;
                    if dt < 1e-15 {
                        // dt ≈ 0: prev_b/prev_t を更新せず zero command のみ送る
                        send_command(&zero_moment());
                        continue;
                    }
                    let db_x = (b.x - prev[0]) / dt;
                    let db_y = (b.y - prev[1]) / dt;
                    let db_z = (b.z - prev[2]) / dt;
                    Command {
                        mtq: Some(MtqCommand::Moments(vec![
                            clamp(-gain * db_x, -max_moment, max_moment),
                            clamp(-gain * db_y, -max_moment, max_moment),
                            clamp(-gain * db_z, -max_moment, max_moment),
                        ])),
                        rw: None,
                    }
                }
                None => zero_moment(),
            };

            send_command(&cmd);
            prev_b = Some([b.x, b.y, b.z]);
            prev_t = input.t;
        }
    }
}

bindings::export!(Component with_types_in bindings);

本当に B を dot しているだけであることがわかります。このシンプルさが Bdot 則の魅力です。

これと binding だけの crate を cargo component build --release すると bdot_fnitite_diff.wasm がビルドできます。

そして、orts.toml に以下のような設定を書いて orts run します。

body = "earth"
dt = 0.1
output_interval = 1.0
duration = 55500.0  # ~10 orbits at 500 km altitude

satellites
id = "bdot-demo"
sensors = ["magnetometer", "gyroscope"]

[satellites.orbit]
type = "circular"
altitude = 500

[satellites.attitude]
inertia_diag = [1, 1, 1]
mass = 50
initial_quaternion = [1, 0, 0, 0]
initial_angular_velocity = [0.1155, 0.1155, -0.1155]  # |ω| ≈ 0.2 rad/s

[satellites.controller]
type = "wasm"
path = "bdot.wasm"

[satellites.controller.config]
gain = 1e6
sample_period = 1.0

[satellites.magnetorquers]
type = "three_axis"
max_moment = 10.0

Bdot が動いている様子

sksat.github.io

RW を使った姿勢制御ももちろんできます。

sksat.github.io

RW を使った姿勢制御

推進器も使えるので軌道制御もできます。 sksat.github.io

ホーマン遷移のシミュレーション結果

WASM Component の仕組みを使った AOCS FSW SILS (Software-in-the-Loop-Simulation)

SILS という概念があります。実際に搭載するソフトウェアないしそれに近しいロジックを実装したソフトウェアをクロスコンパイルかなにかしてシミュレータにリンクすることで、単なる制御則を検証するのではなく、その実装を検証しよう、みたいな概念があります。*2 *3 *4

別にこれも下回りを mock した AOCS FSW 実装を WASM にビルドしてやれば繋がるだろうと思ったのでやってみました。 WASM Component にさえばなればいいのでプログラミング言語非依存で、C言語で作られた FSW とも繋げられます。 ただ公開されているシンプルな AOCS FSW 実装があんまりなかったので、今回は NOS3 というシミュレータ向けに作られている generic な AOCS FSW(もどき)実装を組み込んでいました。

sksat.github.io

これは SILS なので、本体の実装は一切弄らず、build.rs で clone してきた上で、センサやアクチュエータといったインターフェース部分を繋ぎ込む wrapper 部分だけを Rust で書いたものを WASM Component にビルドしてテストしています。

NOS3 ADCS Bdot

これもちゃんと動いていそうですね。

複数衛星の模擬

もちろんできます。

衛星4機の軌道面内位相調整

sksat.github.io

衛星ひとつひとつに個別の WASM plugin を割り当てることもできますし、ひとつの WASM plugin を大量の衛星群に割り当てるようなこともできます。

これで衛星コンステレーションの模擬も色々できるでしょう。

計算の正確性/妥当性検証

シミュレータでもっとも重要なところでありながら、いくらでもナアナアにできるので意外とごまかされていることが多いか、既存実装に乗っかるためにビルドが複雑怪奇になっていたりすることの多い部分です。デカい無意味なレゴブロック*5を積み上げるのってカンタンなんですよね。 orts ではフル Rust 実装によるシンプルなビルド・テストのしやすさ・型安全性を優先したため、基本的な概念のほとんどを再実装しました。

まず、数値積分については汎用 ODE Solver crate である utsuroi を作りました。 RK4 / Dormand-Prince 4(5) / DOP853 / Störmer-Verlet / Yoshida 4次・6次・8次などのソルバーを実装しています。 これの妥当性は解析解が存在する調和振動子や2体問題などを使って検証しています。

docs.rs

次に、座標変換については arika という crate を作りました。 ここでは IAU2006/2000A による precession-nutation、ERA、polar motion、EOP (IERS finals2000a.daily の取り込み)、WGS-84、天体の Meeus 解析 ephemeris、JPL Horizons からの ephemeris 取得などを実装しています。 この検証は、IAU SOFA の互換実装である ERFA や、Orekit での計算結果を oracle fixture とした E2E test によって行っています。

docs.rs

また、LEO(地球低軌道)ではかなり大きな影響が出る地球周辺環境については、地球周辺環境モデル群を tobari という crate を作りました。ここでは Harris-Priester / NRLMSISE-00 などの大気モデル、IGRF のような磁場モデル、CSSI / GFZ からの宇宙天気データ取り込みなどを実装しています。

docs.rs

tobari は wasm ビルドして色々グリグリ可視化できるようにもしておいたのでよかったら見てみてください。

tobari Live Demo
sksat.github.io

そして、orts crate 本体ではこれらのライブラリを組み合わせて、姿勢・軌道のダイナミクス計算をしています。 ここでも Orekit を oracle とした E2E test をたくさん組んでいるため、(Orekit などを信じれば)E2E でのシミュレーションシステムとしての正確性は完全新規実装にしてはある程度確保できているといえるでしょう。

ちなみに、arikatobari などのライブラリはある程度 no_std 対応しています。

Capability-based 力学モデル

こういうシミュレータを作ろうとすると、†すべて†を計算できるようなシミュレータを組もうとしてしまいがちで、組んだら組んだで色々な概念が密結合になってしまって個別での検証がやりにくくなってしまい、結局何を検証したかったのか/検証できているのかがよくわからなくなる、という本末転倒なことが構造的に起きてしまいがちです。

たとえば、しばらく前に紹介した Bdot 則は磁場・角速度・MTQ さえあれば最低限の制御則としての検証はクイックにできるはずですが、シミュレータが密結合だと微妙にバグっているときになぜか SRP や大気モデルの実装を疑わないといけない、というヘンテコなことになりかねませんし、いざなってしまうとここの切り分けは至難の技です。実用的な例では多くの場合机上で簡単に検討できないから数値計算しているわけですし、なおさらです。

そのため orts では、用途に応じて軽い系と複雑な系を載せ替えやすい設計を型レベルで行っています。 具体的には、力学モデルを Model<S, F> というひとつの trait に統一した上で、モデル側が「自分が必要とする状態」を capability として型に書く設計にしています。

impl<S: HasAttitude> Model<S> for PdController { ... }            // 姿勢だけ必要
impl<S: HasOrbit> Model<S, F> for AtmosphericDrag<F> { ... }      // 軌道だけ必要
impl<S: HasAttitude + HasOrbit + HasMass> Model<S> for Thruster { ... }  // フル

orts 自体の開発について

フル Rust 実装なので、特段面倒な依存関係は無く、Rust toolchain のみでビルドできます(Viewer は React/Typescript)。

ただ規模が規模なので、しばらく開発しているとこんなことになります。毎週 disk full の勢い。 まあだいたい Rerun SDK が datafusion を引き連れてくるのが悪い。

この問題もあって、衛星側のロジックの検証をするときは制御則側を WASM Component にビルドしてもらってビルド済みの CLI と組み合わせてシミュレーションしてもらうのがよいだろうなと考えています。もしくは CPU とストレージを買いましょう(今買うのはやや癪ですが必要経費です)。*6

開発期間は約2ヶ月(趣味開発なので土日が中心)ですが、LoC はプロジェクト全体だとちょうど10万行ほど、Rust 部分に限定しても8万行ほどです。これは完全に Coding Agent 大活用の成せる技ですね。あくまで LoC だけの話をすれば最初の数日で3万行ぐらいはあった覚えがありますし。そしてこのほとんど(crate などにもよるんですが30~70%)はテストコードです。

まあでも年始にもシミュレータ作っててそこでの経験でアイデアが固まったところがあるので、実態としては3ヶ月強ぐらいでしょうか。エーアイパワーのおかげで side-project が大量に同時進行してしまっているとはいえ、集中的にやったにしてはだいぶかかってしまったかな、ぐらいの印象です。とはいえ開発の律速は主に CPU/RAM *7と、レビュアーとして酷使していた Codex (GPT-5.4) の rate limit ですが。

ちなみに CI はこんなことになっています。

test がたくさんあるということは、Actions の実行時間が増えるということ

さいごに

Claude Design がリリースされたので、Opus 4.7 に orts のロゴを作ってもらいました。 Orbit とオーツ麦の言葉遊びです。

orts logo

また、今回ちゃんとリリースしたのは趣味でやっていたつもりが十分以上に実用的になってしまったので仕事で使いたく/同僚に使ってほしくなってしまったからです*8。マトモで使いやすいシミュレータ(プラットフォーム)が全然無いというのは正直この4年以上ずっと困っていた問題だったんですよねえ。そしてなぜ長いこと(完全には)解決できていなかったのかと言えば、シミュレータを「ちゃんと」作るのってすごい面倒だからなんですよね。言い訳ですがモノを作ったりチームを作ったりしていると時間が溶けがち。そういった問題が主に Opus 4.6 の腕力で解決したのは本当に便利でした*9

そして、色々書いたもののまだだいぶ荒削りなので、今後も引き続き開発していければと思います。というかたぶん必要なのでします。よかったら star とかしてくれるとうれしいです。

*1:シミュレーション時の CSV への直接出力もそれはそれでできるようにはしてあります

*2:と僕の中では整理しているんですが、正直そこも一般的に合意できるものなのか未だによくわかっていません。「なんかシミュレーターとくっつけたらシミュレーションができるらしいから、なんかうれしそう」ぐらいの意味の無い発言すら見かけることがあるように感じています。

*3:一方で、FSW: Flight Software をクロスコンパイルしておくと、ろくすっぽ姿勢制御とかに関心がなかったとしても、なんとなく FSW の挙動を確認したりテストできて便利ではあります。しかしそれは雑に下回りを mock してクロスコンパイルしておくと、セマンティクスや通信プロトコルなどのシステム的に上位の概念を手動ないし自動でテストできるとうれしいという話です。しかし、日本の「宇宙」のみなさんはどうにもソフトウェア技術がなぜ・なにを・どのように問題を解決しているのかの解釈が浅く、ゴチャついた概念に責務ではなく雑な性質を元に名前を付けてゴチャついたまま会話をしてこんがらがっていることがやたら多いように感じています。まあこれがソフトウェアだけなら別にそこに専門性がない人々が積み上げてきた分野だからなと思うこともあるんですが、果たしてそれはどうかなという気持ちになることもままあります。"AOCS" とかも大概そういうかんじです。

*4:"HILS" とかいう、より何も言っていない、なんの責務も明らかでないし人によって思い描いているものがまったく違う、結果として結局何をなんのためにどのように検証したかったのかもよくわからないまま曖昧に謎のモジャモジャの通信線がたくさん PC に繋がりまくって「ループが回って......ループが回るとうれしい!」みたいなことになっていることがしばしばある最悪の言葉もあります。駆逐したい。この世から一匹残らず。

*5:デジタルツインと呼ばれていることもあります

*6:僕のメインマシンと Devin の VM のストレージは溢れました。かなしい。

*7:並列開発があんまりできない!!!とはいえ僕の VSCode には plan だけとか途中までとか走らせた Claude Code session のタブが常にめちゃめちゃ並んではいました。

*8:あと、private repo でやってたら後半エーアイぐらい Actions に金かかり始めたから(?)

*9:ただ Opus 4.6 は視力が深海魚並みなので可視化結果の検証とか Viewer のデバッグとかが微妙にずっと効率上がりませんでした。Opus 4.7 は深海魚よりはだいぶマシな目玉持ってるのでそこに期待したいのと、最近 mizchi から Visual Regression Test のための目玉用のモデルは別のを使っていてちょうどよいものを探しているという話を聞いてまあそうだよなあと思ったりしていました。

Kernel/VM 探検隊@つくば No.3 の運営をやった

Kernel/VM 探検隊 @ つくば No.3 が 03/20 があり、これの運営をやっていました。

https://kernelvm.connpass.com/event/381371/

これはかなり久しぶりのつくば開催でした(というか僕は前回を知らない)。なのでどんな体制でやるかといった企画から運営までを新規で考えてやる必要がありました。

どうやら Kernel/VM 探検隊 @ つくば No.2 は 2014 年開催だったようです。

https://connpass.com/event/7293/

今回のつくば開催は、「つくばで k/vm やりたいよなあ」という話をつくば周辺のオタク(要出典:誰?)でたまに話していたものが、去年夏の Kernel/VM 探検隊 @東京 No.18 の後あたりから主に id:koba789 が発起人となって曖昧に Kernel/VM Discord にスレ(#event-org/つくば説)が立ったところから始まりました。

とはいえ、その時からいた id:koba789n.takana も僕も正直ずっと忙しかったのであんまり話は進まずでした。その後 Kernel/VM 探検隊 @ 北陸 Part 8 の後にエイヤで大雑把な時期を3月末に決めました。あんまり記憶が無いんですが、投稿時間からして懇親会でしこたま日本酒を飲んだ*1後ですねこれは。

曖昧な開催時期決定

その後もずっと全員普段は忙しい面子でやっていたため、年明け後は定期的に同期的な打ち合わせを入れながら話を詰めていきました。飯喰いながらとか。本格的な企画が始まったのは 01/12 とかですね。

大学のラウンジで考えた初期タイムスケジュール案

このタイミングから、僕は共有の Google drive を整理して議事録作ったりとかロジ寄りのことをするようになっていました。 あと、元の3人だけだと当日の運営やネットワーク敷設などの調整がつけられないので、適切な現役生(現在の大学の技術的側面やその調整先に精通ないし所属している休学ないし事実上の休学を繰り替えしたりしていないものを指す)を見繕って巻き込むなどしていました。

議事録とか ToDo とかを単一の Google Docs に突っ込んでいた

この翌日には n.takana が大学支援室に出向き、教室の予約状況を確認してもらったり、彼の指導教員経由で教室を借りるための依頼を開始してもらったりしていました。

3日後からは id:maseBB などによる大学探検隊が開始され、「この教室は流石にキャパが少ないかも」「この教室は名目上のキャパは多いが Jetstar すぎる」「プロジェクターって借りられるんだっけ?」などを話しながら具体的な教室の候補を考え始めていました。

ただ、その後は大学探検隊や大学当局とのやりとりといった I/O 待ちでストールしていました。 そのため、条件は変わるがいずれかの教室が確保できるだろうことは確実であり、その確認はしていること、どうせ大学内(それも第3エリアのどこかになるのは確実)なので学内の人間はどうとでもなる・学外の人間からは「筑波大」の時点で移動コストや認知負荷のオーダーが変わらないことなどから、01/24 に見切り発車で connpass でイベントを公開しました。そうしないと人々のネタ準備の時間が減ってしまうのでな。

(なお、僕はここで応募をしそびれて一瞬で枠が埋まって異常な状態に突入していました)

で、その後思ったよりもあまりにも応募が多かったので、元の70人規模の教室から200人規模の教室に変更し、connpass の定員も拡張しました。

結局150人以上ちゃんと来ていた気がする。すごい。

ネットワーク

ここは完全に人々に任せていたので何が起きていたので、僕自身は細かいことはあんまり把握できてないです。id:maseBB や centra や野良の筑波大生が色々やってくれて助かりました。

しかしなぜかコンソールケーブルが自作されたり、id:hikalium さんがめちゃめちゃ手伝ってくれたりしていました。

当日準備など

ネットワーク敷設・配信準備などで id:KOBA789, id:maseBB, centra などがてんやわんやしていたので、僕はそれ以外の仕事というか、「実はやらないとダメでは?」みたいなところを気付き次第スポット的にやっていました。 具体的には、

  • 発表者からタイトルを回収して connpass や内部タイムテーブルに反映
  • 色々作る
  • 運営記録用の写真を撮る
  • 集金準備と集金
  • 懇親会の案内

などをやっていました。色々作るというのは、

会場案内のたまによくあるアレ

こういうやつとか、

準備中とかにスライドに案内を映すやつ

こういうやつとかのことですね。

あと、単なる参加はともかく懇親会はちゃんと集金状況確認しないといけないじゃんとなって集金確認用の名簿を作ったり、そういや OtakuAssembly のためにそこそこ真面目な金庫持ってたなということを思い出して発表中に一時帰宅して金庫を確保するなどしていました(車出してくれたりした totsugekitai もありがとう)。たぶん現地にいた人からはずっと集金係として見えていたと思います。だいぶ時間は経っていたものの、OtakuAssembly の会計で鍛えた札束整理筋みたいなものは結構残っていた気がします。

集金用の名簿は Claude Code で作りました。connpass の ID が普段と違う人なども多少いたので、登録順でのソート・アイコン併記などをしつつチェックボックスなども html で作ってもらいました。べんり。

vibe 集金名簿

懇親会

懇親会は百香亭筑波大学店で実施しました。筑波大学関係者ならこの一文で「それはそう」と思ってくれるでしょう。そのぐらい大規模な懇親会に最適な店です。大学からも近い。

どのくらい最適なのかというと、80人の予約を涼しい顔で平然と受けてくれます。さらに、元々は2700円のコースがあったんですが、端数があると集金が大変なので「これってちょうど3000円とかになりませんかねぇ」と言ったら「じゃあ2、3品足しとくね」で受けてくれました。かなり原文ママです。慣れすぎだろ。

あと結局定格80名から6人ほど溢れて希望者が存在したので追加できないかの連絡を当日の数時間前にしたんですが、対応してくれました。すごすぎる。 電話応対してくれた centra もとても助かりました。

ただまあ定格は本当に物理的な上限という感はあり、懇親会参加者には(物理的に)無理を強いてしまいました。そこは申し訳ないところでもありつつ、結果的には†柔軟な対応†により不幸なく充実した懇親会になったのかなとも思います。

そしてなにより、あれだけの人数いても百香亭の異常な飯の出力帯域により(席位置によっては間欠的になったとしても)喰いっぱぐれて腹を空かせる人を発生させることはなかったと思いますし、百香亭店主は金庫にゴロっと入ったほとんど千円札の金での支払いも普通に対応してくれました。ありがとう......

とくに関係のない終わり

ということで Kernel/VM の運営をやったんですが、参加はこれまで散々しているし運営もやったのに発表はしたことが無いこと、そして LT 枠がちょっと増えてることに気付いたので、次の関西ではなんかやります。たぶん。

仕事でもこういうのでもなんだかんだで裏方業をやりがちなので、なんというか正面からのアウトプットみたいなのはここ数年やや欠けているところかもしれません。あとなんというか広い意味で「やるべきこと」とかをやりがちなので、ヘンなことをしたいですね。

*1:僕は普段の日常生活では基本的にまったくといっていいほどアルコールを摂取しないんですが、飲酒自体はそこそこの量が technically possible ですし、飲むか飲まないかに関わらず概念としての懇親会ないし飲み会はかなり好きな方なので適宜誘ってもらって全然 OK です。脚注で表明することなのか?これ

全自動考証機械

Claude's C Compiler

今月頭に、Anthropic が Opus 4.6 のエージェントを並列でループさせることによって Linux 6.9 を複数のアーキテクチャターゲット向けにコンパイルすることが可能な C コンパイラを作るというブログとそのリポジトリを公開していました。

www.anthropic.com github.com

とても面白い試みだと思います。

ちなみに、以下 Issue が話題になってなんだ大したことないな、みたいな受け取り方をされていることもありましたが、これは単に include path が通っていないだけで、自作 C コンパイラとしてはそこまで違和感は無いかなと思います。 github.com

まあ、そういう easy さは無いのに intrinsics 関係のヘッダだけは結構丁寧に書いてあるのはいい意味で愚直な Opus 4.6 らしいなとは思いました。おそらくビルドする Linux の構成の中で使われている最小限のセットとかなんでしょう。 github.com

エージェントループの設計は本当にシンプルです。 ここは Claude Code と同じく Anthropic の Geek らしさというか、UNIX 哲学っぽさを感じるところですね。

#!/bin/bash

while true; do
    COMMIT=$(git rev-parse --short=6 HEAD)
    LOGFILE="agent_logs/agent_${COMMIT}.log"

    claude --dangerously-skip-permissions \
           -p "$(cat AGENT_PROMPT.md)" \
           --model claude-opus-X-Y &> "$LOGFILE"
done

また、この実験ではエージェントの並列化に主眼が置かれていたため、このループを Docker コンテナに押し込め、複数のコンテナで複数のループを同時並列させている点も面白いところです。 あくまでシンプルかつ初期の実験として割り切ってそれ以上の並列化のための仕組みは作らず、conflict は許容して push に失敗したら自分で直させるようにしているのはなるほどなと思ったところです。まあ奴等変更の意図を汲んで conflict を適切に解消するのかなり上手いですからねぇ。

得られた教訓として書かれている高品質なテストハーネスとテストの設計が重要であること、エージェントのためにテストハーネスを作る(コンテキスト消費を抑える、時間感覚の無さを補うように作る)、参考にできる既存実装があるならそれをオラクルとするテストを作る、という話は趣味・仕事での個人的な経験とも整合していたので、まあそうですよねとなりました。

SOLAR LINE

急に文脈が接続していない話をします。 ニコニコ動画などで活動されているゆえぴこさんという方が作る動画がとても好きです。

個人的には大統領りりせシリーズやムネオハウスも重厚でめちゃめちゃ好きです。 www.nicovideo.jp www.nicovideo.jp www.nicovideo.jp

これを Unity で一人で作っているの、何事?

何事?と思っていると中の話が同時並行で投稿されたりもします。何事?

www.nicovideo.jp

ハコワールドやゆるいと見せかけて風刺が効いてたりするやつも本当にいいです。 www.nicovideo.jp

www.nicovideo.jp

最近は SOLAR LINE という長編が投稿されていて、今季覇権アニメ(?)としてずっと観ていました。そして数日前に完結しました。よかったです。

www.nicovideo.jp

SOLAR LINE は太陽系内を駆け巡るタイプの SF 作品です。

どうでもいい補足をしておくと、僕は意外なことに(?)宇宙っぽい SF 作品が大好きとかではあんまり無いです。こんな仕事 とか、こんなゼミ とかを常日頃やっているので勘違いされてることがたまにありますが、ガンダムとか STAR WARS とかのよくあるやつも全然知らないです*1

この理由は単純に幼少期にそういう作品を読んだり観たりしていないからでもありつつ、なまじ「宇宙」のことを知っている状態が先にあったということもあって、不思議部分の整合性が中途半端だったり、特に不思議要素として描かれていないヘンな描写に対してスルーできない面倒な人間として育ったという部分が大きいです。

明確に好きだと言える宇宙っぽい SF 作品(?)(そもそもカテゴリ分けがあまり分かっていない)は今のところ

  • 『宇宙への秘密の鍵』:これだけは小学生の時に読んでた
  • 『2001: A Space Odyssey』:はい
  • 『The Martian』:イモ喰え
  • 『Contact』:SETI
  • 『ツインスター・サイクロン・ランナウェイ』:百合

みたいなかんじです。書いてみるといくつかあるものの、厳選した結果とかではなくそもそもの分母がめちゃめちゃ小さいないしは1:1ぐらいです。

そんな僕ですが、SOLAR LINE はちゃんと楽しむことができました。太陽系内の惑星から惑星(たまに衛星)へ次々と軌道遷移していくテンポの良さと、その際の違和感の無い物理的な描写のバランスが取れていてとてもよかったです。各所にふんだんに出てくる数値も、パッと見では条件さえ整えばオーダーは合っていそうに思えます。

ここで疑問がひとつ。これどれぐらい整合性取れてるんだろう。 ゆえぴこさんのいつもの丁寧な仕事™振りのことも考えると、もしやこれ結構ちゃんと考証されているのでは?

全自動考証機械

ということでようやく本題です。考証の考証、してみたいですよね。

自分でやってみてもよかったんですが、最近はやっぱりエーアイが便利です。 そして何よりエーアイは Web 開発がかなり得意なので、自分で何か検証みたいなことをする時も、最近はもっぱら横に Claude Code を置いてリアルタイム可視化のための Web app を同時に作る、みたいなことはかなり日常的になっています。

リッチな、特にインタラクティブな可視化ができるとかなり便利で、単純に手触りがいいだけでなく、ちょっとパラメータを変えてまた可視化、みたいなフィードバックループの遅さを改善することができますし、それによって現状の人間の大きな優位性のひとつである視覚・そこからの「直感」の獲得・「直感」による誤りの高速な発見というメリットを最大限に活かすことができます。

それはそれとして、あと単純に Claude's C Compiler がまだ記憶に新しかったので、せっかくなのでやってみたくなったんですよね。エージェントの無限ループ。ここでようやく話を繋げることができました。

ということで、Claude Code を使った AI Agent のループによる SOLAR LINE (ほぼ)全自動考証をやってみました。

sksat.github.io

リポジトリはこれです。 github.com

所々不自然な部分はあるものの、おもしろい物体にはなったのではないかと思います。

最初に用意したのは Claude's C Compiler とほとんど同様のシェルスクリプトとそれを突っ込む用の Docker コンテナの設定と簡素な Design Doc です。

github.com

今回の実験には主に Opus 4.6 を用い、レビュー用途に Codex CLI 経由で GPT 5.2 を使いました。 これは普段の Claude Code での個人的な開発でもよく取る構成です。GPT はコードを書くのには不便極まりない*2ですが、なんというか「地頭の良さ」のようなものはとてもあるので、レビューはとても鋭くて便利です。

あと、エージェントループの並列化は行わず、1ループのみの実験としました。 並列はまあ、適切な環境分離と conflict が小さく済むようなアーキテクチャ分離ができてれば、後は UI とかの問題かな、と Devin とかを酷使しながら感じています。 今回の実験においては、C コンパイラのような「正解」の無い問題に対してエージェントに主導権を渡した時にどうなるのかが個人的な関心事項でした。

他に意識していたことは、LLM は直接動画を読むことはできないということです。 VLM みたいなものを頑張ればあるいは、ということはあるかもしれませんが、いずれにしろその分野はまだまだという印象があるのと、一方で「言語」の世界にさえ落ちてしまえば今の frontier model の性能ならかなりの情報を察することができるだろうなという感覚があるのと、単純に手元に Claude Max subscription があるのでそれぐらいしか手段が無いためです*3。 そのため、最初に動画からの文字起こしをさせることにしていました。

結果

エージェントがタスクを整理してどんどん進めていく、ということそのもので困ることはあまりありませんでした。 本質的にプロセスをどうこうする必要もあまりないタスクなので、Anthropic のブログ記事にあったような pkill -9 bash のように自爆することもありませんでした。

ただ、最初はずっと文字起こしで問題を起こしていましたね。 YouTube での自動文字起こしと whisper での文字起こしを併用していたんですが、動作させていたコンテナに GPU リソースを渡していなかったのでずっと CPU ベースで whisper を走らせていて、エージェントが whisper を待っている時間の方が明確に長かったです。

さらに、文字起こしの精度が微妙で、それに引き摺られて誤った解釈を引き回し続けていることがそこそこありました。 ライって誰だよ。 ここは GPU を与えるか、gpt-4o-transcribe か gemini あたりの token を与えて LLM ベースの文字起こしをさせるか迷ったんですが、途中でループを止めたりするのもなんか違う気がしたのでそのままやらせてみることにしました。他の手段として動画に焼かれている字幕の認識などもタスクとしては積んであるものの、これは記事を書いている今もまだ道半ばというかんじなので、そのためにも見守ることにしていました。

しかし、それ以外の Web 開発的なところは特に詰まるところはなく、さすがの Opus 4.6 というかんじでした

アニメーションする軌道遷移図

とはいえ考証として見ると問題はかなり多く、途中でかなり人間による介入を行いました。 人間による介入は claude -p で指定している AGNET_PROMPT.md 内に追加指示用の場所を設けることで行ったのですが、これにも課題がありました。

最大の問題は、Claude が以前のセッションで生成した誤りや勘違いに引き摺られてミスり続けることです。 特に今回の場合は『正解』や『仕様』の設定しにくい問題設定だったので、AI Agent に実装の後にテストを書かせると単にパスするだけの無意味なテストを書く(なので TDD が効く)ように、微妙な物体の山が積み上がっていくようなやつが発生しやすかったです。 中でも、各エピソードごとの分析を元にエピソード間の整合性を分析する部分においてそのような挙動が見られました。

この対策として、途中の介入では以下のような指示を行いました。

  • 分析のためのコードは使い捨てにするのではなく CLI から操作しやすいインターフェースにする
  • 各エピソード分析記事内の各分析の再試をしやすいデータ構造にして、常に各分析を走らせる

これはようは「分析」をテストとしてみてテストハーネスを作るようなことですね。 これによって分析の修正の容易さや分析の修正からの他への影響の検知などはだいぶマシになっていた印象があります。

一方で、こういった追加指示は端的な方向性のみ指示して設計は Claude に任せるスタイルだとどうしてもイマイチな印象も正直ありました。 というかまあ、分析の中身とか可視化のやり方については結構な頻度で具体的な追加の指示を突っ込んでいました。なので全自動というのは実態としてはかなり FAKE です。

普段のやり方との差分を考えると、じっくり plan してから作業とかしていないのでまあそうかなとは思いつつ、じゃあどうするとよいのかまではあんまり見えてないです。AskUserQuestion をいいかんじにファイル化して適当な非同期インターフェースで聞きつつ、情報が集まるまでは他のタスクを実行する、ぐらいのループの制御はするといいんですかねぇ。

ただ、「その軌道遷移は成立するけど物語としてそんなに時間かかるのは不自然だろ」みたいなやつとかに気付けない、みたいな問題は仕組みの問題というよりは LLM の時間感覚のなさというか、物理世界接地していなさを感じる部分でもありました。

後日談というか、今回のオチ

で、どうなったかというと、2~3日で 200 USD Max の weekly rate limit が売り切れました。かなしい。 Anthropic 社員には rate limit とかなくてこういう実験をしやすくてズル(それはそう)

You've hit your limit

微妙に関係ないですが、Opus 4.6 fast とかはああいう社内での色々な実験のフィードバックループ速度を上げるのに便利なんだろうなと思うなどもしました。

しかし、自動分析という今回の実験自体にはまだまだ課題はあったものの、SOLAR LINE 自体の描写の正確性はやはり高いなとやりながらも感じていました。もちろん、ものすごく複雑な軌道設計とかがあるわけではないですし、規模の都合上かなり惑星の位置関係の都合が良さそうな想定ではあるなとは思ったものの、専門でない人間がナイーブに考えたらよくやるようなミスというか、詰めの甘さみたいなものは感じず、結果として自動分析自体のデバッグもしやすくて便利でした(分析レポートを読んで違和感を覚えたら分析側が大体おかしい)。

まだ分析としてヘンであったり詰めの甘いところは多々あるのと、この記事を書いている間に weekly rate limit がリセットされたので、またたまにループを回して中身のブラッシュアップをしていければと思います。

*1:正確にはガンダムは最近水星の魔女とジークアクスは観ました。ゆえぴこさんの更新待ちの間に AbemaTV の無料公開期間とかで......あと後者はタイムラインでの連日のネタバレ合戦がウザすぎて......でも結局 not for me かなあとは思いました。わざわざ言うことでもないんですが。

*2:これは用途や使う人によっても印象が分かれているところではあると思いますが、GPT 系は勝手にすべてを一撃でやり切ろうとするのでフィードバックループの構築には明らかに不向きで、それは human in loop みたいな場合でもそう、という印象が強いです。あくまで「回答」を生成する対話のために訓練されており、問題解決能力はあまり無いという印象です。ここまで LLM が盛り上がって競争が激化して、各社のモデルが似たようなところに収斂するのではなく明確に「個性」が出る方向になっているのは面白いなと思っています。

*3:GPU は RTX 3090 一枚ぐらいしかないので、最近また多少盛り上がりがあるとはいえ Local LLM でやるのはまだしんどいです