gbdraw は、細菌やウイルスといった微生物や、ミトコンドリアや葉緑体などの細胞小器官(オルガネラ)のゲノムダイアグラムを作成するツールです。GenBank/DDBJ形式やGFF3 + FASTAを読み込み、環状・直鎖状のゲノム図や、複数ゲノム間の配列類似性を示す比較図を作成できます。CLIから利用できるほか、現在はWebアプリとして、インストールせずに図の生成・調整・保存まで行えます。
環状ゲノム全体の可視化から、遺伝子クラスター、複数ゲノムの比較まで、同じアプリ上で作図できます。実際の作例はこちらから↓
現在はこんなアプリですが、最初からここまで作るつもりだったわけではありません。手作業での描き直し、インストールの手間、サーバーの維持費など、その時々の不便を解消していった結果こうなりました。この記事では、gbdrawが今の形になるまでを、各段階で困ったことと技術選択の理由を交えて振り返ります。
ポスドクの暇つぶし
2022年の春に大学院を卒業後、横浜の海沿いにある研究所でポスドクを始めたころでした。実験の準備等が整うまでヒマだったので、そろそろプログラミングを覚えようと思いつきました。研究上、CLIの解析プログラムを走らせるシェルスクリプトなどは頻繁に扱っていましたが、解析プログラムそのものを作ったことはありませんでした。
勉強がてらこまごまとしたスクリプトを作り始めました。Jupyter Notebookで書く練習をしていました。ライブラリをロードする行や各関数を各セルにしてぽちぽち実行することで、Pythonがどうやって動くかを覚えていきました。
今からすればスパムのようなものですが、ORF予測やGenBankファイルのparserのようなスクリプトを書いては、小さな記事にして投稿していました。これらの細々としたスクリプトの中には、のちにgbdrawの追加機能の原型として役に立ったものもありました。
ゲノム描画スクリプトを自作する
手作業ではやってられない
私はウイルスや細菌といった微生物のゲノムを研究で扱うことが多く、ゲノムのダイアグラム(どの領域に何の遺伝子が載っているか)を描画することがよくありました。出来合いの描画アプリやライブラリのスタイルが好みではないため、私はいつもInkscapeというアプリをつかって(今も使ってます)自分で描くようにしていました。「こういう図が描きたい」というイメージをもとに一つひとつ図を仕上げるのは、工芸品を手作りするような趣がありますが、とにかく時間がかかります。
ある学生時代の研究を論文にまとめるにあたり、大型ウイルスゲノムを複数並べ、配列が似通った領域をリボンで結ぶ比較解析の図を何枚も描く必要がありました。1枚描くだけで手作業なら何日もかかります。しかも論文を進めるうちに解析をやり直す必要が出てくることがあり、そのたびに遺伝子の座標や配列の始点がずれます。描き直すたびに途方もない時間と労力を浪費することになります。さすがにこれは自分で絵を描くのではなく、プログラムにやらせなければならないと悟りました。
出来合いのものが気に入らない
遺伝子/ゲノム描画ライブラリは当時からいくつもありましたが、イントロンが描けない (イントロンを持つウイルスでした)、SVGが洗練されていない (図形が複数の直線で表現される、文字がテキストでなくパスになっている)など、いずれも納得のいくものではありませんでした。あるライブラリを使おうと1週間試行錯誤した挙句、このライブラリでは自分のやりたいことができないことに気づくこともしばしばでした。
さすがに懲りて、Pythonを覚えてきたところだしひとつ大きなスクリプトを書いてみようと思いつきました。つまり、
ゲノム配列データを読み込む→座標上のフィーチャーの配置を取得する→図形の記述に変換する→SVGファイルに保存する
という一連の流れを実行するスクリプトを自分で書くのです。
当時の私には、SVGでなければならないというこだわりがありました。(少なくとも小さなゲノムの場合は)ファイルサイズを節約できる、無限に拡大縮小できる、ブラウザで開いてラベルテキストをコピペできるという利点がありました。当時はあまり流行っていない(ように見えた)のが不思議でしたが、だんだん増えてきた気がします。
「車輪の再発明」を回避できるか
Pythonで描画といえばmatplotlibだろうということで、最初はmatplotlibでなんとかしようと試みました。直鎖状ゲノムは割とすんなり描けました。ですが、環状ゲノムで矢印形に遺伝子を描画するのがなかなかうまくいきませんでした。
「車輪を再発明するな」という言葉があります。メジャーなライブラリでできそうなことを自分で再実装するのは非常に愚かしく感じられます。ただ、開発中に習得できるであろう技術や使い勝手という効用をてんびんにかけると、使いづらいライブラリに固執してなんとか使えるようになるよりも、自分の車に合う車輪を自作したほうが最終的にペイするのではないかと思いました。
目的に合った車輪を自作する
いよいよ「自分でいちから作ろう」ということになりました。ゲノムと座標上のフィーチャーをSVG要素に変換するロジックを書きました。環状のゲノム上のフィーチャーを表現するため、三角関数を高校生ぶりに使いました。SVGの出力には、svgwriteというライブラリを使いました。とうとう環状ゲノム用と直鎖状ゲノム用のスクリプトができました。これで、学生時代の論文の図をやっと納得する形で描くことができました。
CLIツールとして公開する
「あなたが書いたスクリプトをほかの人が使えるようにしよう。論文などで引用してくれるかもしれない」という記述をどこかでみて、「これだ」と思いました。そこで、自分用だったスクリプトを整理し、引数を指定して実行できるCLIツールとして整備することにしました。作成時は、みんなSVGを使えばいいじゃんと思いSVGだけでスタートしましたが、やはりPNGなどラスターも必要だということで、リリース時にCairoSVGをdependencyに追加しました。ほかのライブラリもあるなかでCairoSVGを選んだ理由は、フィーチャーの埋め込みラベルを表現するため曲線パスに沿って配置したテキストが、ほかのライブラリではどうしてもうまく表現できなかったからです。
GUI化:ColabからサーバーレスWebアプリへ
Colab Notebook
再現可能性を重視したCLIの描画スクリプトとしてスタートしましたが、思いのほかGUIの需要が高いことに気づきました。また、そもそも単なるお試しにわざわざインストールを要求するのはイケてないなと考えました。そこで、タンパク質の3D構造予測アプリColabFoldに触発されて、Google Colabのノートブックを作ることにしました。
これはこれで機能したのですが、起動時にボタンを押して依存関係をインストールしたり、Googleアカウントを持っていないと使えないなど、どうもコレジャナイ感が拭えませんでした。
Streamlit
Pythonアプリを簡便にGUI化する方法を探した結果、Streamlitアプリにたどり着きました。ここらへんから、Web GUIはほぼAIに書かせるようになりました。StreamlitのインターフェイスはたしかGeminiに書かせています。
幸い好評で、利用者もついてきたのですが、複数人が同時に利用するとサーバーの処理が苦しくなったり、ユーザーがデータをアップロードする必要があるなど、拡張性や将来性に不安がありました。
そこで、アプリをStreamlit Community Cloudから引き揚げ、自前ドメインを取ってGoogle Cloud Runでホストすることにしました。
しかしここでも、botの巡回でサーバーが起動して課金が始まる、当時のStreamlitではコンテナ側をHTTP/2で受けられず、Cloud RunのHTTP/1.1リクエスト上限(32 MiB)に引っかかるためアップロード可能なファイルサイズが制限されるなど、行き詰まりを感じました。
ブラウザ内実行へ:ユーザー側で計算する
そこで、そもそも自前のサーバーではなくユーザー側で計算してもらえばいいじゃないかということに思い至りました。そうすればサーバー側の計算資源に縛られず、ファイルをアップロードする必要がないから回線速度を気にしなくていいし、セキュリティリスクも低減できます。
ということでインターフェイスをJavaScriptで作り直しました。描画処理はPyodideを使って、Pythonの描画処理をブラウザ内で動かす構成にしました。とりあえずGitHub Pagesにデプロイすることにしました。
相同性検索もブラウザ内で動かす
比較図そのものは、初期のスクリプトから描けました。ただし、BLASTを別の環境で実行し、その結果をファイルに保存して読み込ませる必要がありました。せっかくGUIでダイアグラムを描けるようになっても、相同性検索はコマンドラインでやってくださいというのは片手落ちな気がします。相同性検索そのものもブラウザの中で実行したいな、いやできるだろと考えるようになりました。
相同性検索の再実装(LOSAT)
さすがにPythonやJavaScriptでBLASTを再実装するわけにはいかないし、CやらC++だので書かれたプログラムをブラウザで動かすための仕組みが何かしらあるだろうと調べてみました。やはり世の中には良いものがあるもので、WebAssemblyというものがありました。そこでまずwasmifyされたBLASTを探しましたが、なかなか見当たりません。なぜだろう?なら自分でWasm化しようと思い立ったのですが、そもそもBLASTそのものが巨大であることに気づきました。これ全部移植はしんどい。てかそういえばcondaでBLASTインストールする時もめちゃくちゃdependencies多かったな。だからgbdrawのdependencyにもBLAST入れなかったんだよな。なので必要な機能だけを再実装してコンパクトなスタンドアローンのプログラムを作ろうと思いました(BLASTはアメリカ政府機関が作ったパブリックドメインなので著作権は大丈夫でした)。
というわけで相同性検索プログラム (LOSAT) を作りました。なんか流行ってるのと、Wasm化が容易なのとでRustを使うことにしました。「Rustで書くことにしました」と言わなかったのは、自分では1行も書いてないからです。ここらへんから、gbdrawのPython側本体やGUIもチャットではなくコーディングエージェント上で書かせるようになりました。NCBI BLASTのC/C++実装を参照しながら、CodexにRustへ移植させる形で作っていきました。その後、少なくともgbdrawで使う範囲ではほぼ同じ出力を再現できるところまで来ました。
WebAssemblyでマルチスレッドを動かす
ブラウザで検索が動くようになると、今度はCPUの複数のコアを使って処理したくなりました。Wasmでmultithreadingをやるには、HTTPのヘッダーをいじる必要がありました。これがGitHub Pagesではできなさそうだったので、ヘッダーを設定できるCloudflare Pagesへ配信先を移しました。
作図ツールから「編集するアプリ」へ
相同性検索までブラウザ内で完結するようになると、今度は「図を生成して終わり」ではなく、生成した図をその場で調整できるようにしました。ラベルの位置を直したり、表示する情報を選んだり、図上のフィーチャーを操作したり、作業状態を保存してあとから再開したり、といった機能を少しずつ追加していきました。Undo/Redoも入れ、保存したSessionから同じ状態を復元できるようにし、SVG自体にもマウスオーバーなどのインタラクションを持たせるようになりました。
このあたりから、gbdrawは単に入力ファイルからSVGを生成するプログラムではなくなり、いよいよ複雑なGUIアプリになってきました。生成だけなら入力から出力への一方向の処理で済みますが、編集できるようになると、元データ、現在の設定、ユーザーが加えた変更、画面上の一時的な状態など複雑な要素を区別して保持する必要が出てきます。
なんかだんだん雲行きが怪しくなってきました。
行き詰まり
コーディングエージェントを使うようになってからは機能追加そのものが非常に速くなりました。以前なら時間と労力を考えて諦めていたような機能でも、とりあえず実装まで持っていけます。でも機能とGUIを急拡大させていくうちに、ここを直すとあっちが壊れる、新しい機能を足しているうちに以前できていた操作ができなくなる、モグラたたきのような状態になってしまいました。
AIは利用者の「当たり前」を共有していない
原因を調べていくと、本来は独立して扱えるべき状態や処理が抱き合わせになっていた例がしばしば見つかりました。例えば、
- 凡例の位置をリセットする関数に、凡例そのものの編集をリセットする処理まで入っていた
- フィーチャーにマウスオーバーするだけで要約だけでなく詳細情報をあらかじめロードしていた
どうやら、コーディングエージェントは、「凡例に関する処理」「フィーチャーに関する処理」のように、同じ対象に関係することを、ひとまとまりの仕事として扱いがちなようです。しかし、「同じものを扱うこと」と「一緒に実行してよいこと」は違います。 利用者には、「位置を戻しても、時間をかけて編集した内容は残っていて当然」「情報を少し確認したいだけで、重い処理まで始まったら困る」といった暗黙の期待があります (あまりに当然なのでいちいち明示しません)。でもAIはそこをくみ取るのが難しいようです。「何をひとまとまりの仕事とするか」という関心の分離 (separation of concerns)のセンスが利用者とずれているため、ユーザーにとっては結合度が高い状態になりがちなようです。
ユーザーから見て「これでいいや」というプロダクトも、表面こそ言語化された要件定義に合致していても、その内部では、本来別々に扱いたい状態や処理が結びついていることがあります。そのため、後から一方の振る舞いだけを変えようとしたときや、別の順番で操作したときに、抱き合わせになっていた状態や処理まで巻き込まれ、問題が顕在化するのです。モグラたたき状態になっていたのはこれが大きな一因だったようです。
正解をどう定義するか
どうやらバグやregressionを防ぐには、何が正しいかを実装コードとは別に定義する必要があるようです。そもそも何をひとまとまりの関心として扱うべきかという感覚が私とAIで一致していないので、「関心の分離を徹底してください」「regressionを起こさないでください」と指示しても意味がありません。毎回毎回「位置を戻しても編集内容は維持する」「要約の表示に詳細情報の準備を必須にしない」などとエージェントに伝えているヒマはありません。そこで、何を正しい状態として維持するかをコードとは別に定義し、変更時に照合する仕組みを作ってみました。
まず、アーキテクチャ適応度関数(architectural fitness function)を使ったチェックを導入しました。同じ仕様や状態を複数の場所で決めるようになっていないか、同じ処理を行うコードパスが増えていないか、古い経路を残したまま新しい経路を追加していないか、といった構造上の重複を監視します。既存の問題を一度に全部直すのではなく、とりあえず現在の状態から新しい違反が増えない方式にしました。
また、現在のコードや既存テストをそのまま正解とはみなさず、ユーザーから見て何が起こるべきかという仕様上の決定を別に記録するようにしました。曖昧さやregressionが問題になった挙動について正しい振る舞いを明文化し、それを実装が満たしているか確認するテストも作成しました。
さらに、いくつかの重要な操作については、どの実装箇所や処理経路が、ユーザーから見たどの挙動に関係するのかを紐づけました。例えば、Generate時に使う設定を組み立てる処理を変更した場合には、「Sessionを保存・再読込しても同じ設定で再生成できる」といった関連する要件と、その要件を確認するテストを辿れるようにしています。
こうして、一度「この挙動が正しい」「この処理はここが担当する」と認識したものについては、変更時に関連するチェックを行い、同じregressionを繰り返しにくくしました。
しかし、これで安心とはいきません。まだ明示的に定義していない境界に遭遇したとき、それが既存仕様から自明に決まるのか、新たに人間が仕様を決める必要があるのかを判断する部分は、依然としてエージェントの裁量に依存しています。「分からなければユーザーに聞く」と指示しても、何を「分からない」と認識するか自体をエージェントが判断するからです。
Regressionの恐怖から解放されるのは、当分先のことになりそうです。
まとめ
振り返ると、gbdrawは最初から現在の形を目指して設計したものではありませんでした。手作業で図を描くのが面倒だったのでPythonで自動化し、インストールが面倒だったのでGUIを作り、サーバーを介する制約が嫌だったので計算をブラウザへ移し、相同性検索を別途実行するのが面倒だったのでLOSATを作りました。JavaScriptに味をしめて、最終的には生成した図をブラウザ上で直接編集できるようにしました。機能をどんどん追加しているうちに、増えた機能を壊さずに変更し続けることの方が難しくなってきました。
gbdraw自体はまだ開発途中です。これからしばらくは機能を増やすだけでなく、これまで暗黙だった仕様や設計上の前提を少しずつ明文化し、安心して変更できる状態を作っていくつもりです。コーディングエージェントによって、機能の実装速度が以前とは比較にならないほど上がりました。それでも最後まで私の手元に残る仕事は、何が正しいかを決めることかもしれません。

