ScanDataNet プロジェクト全体構造

Python (ScanDataPy) → Rust + TypeScript 全面移行後のファイル階層・クレート分割・段階導入計画 · 作成 2026-05-17
目次
  1. 設計方針 (推奨採用版)
  2. ディレクトリ階層 (全体ツリー)
  3. アーキテクチャ層と Hexagonal の対応
  4. クレート別の役割
  5. ScanDataPy との対応関係
  6. Phase 別の物理的な追加スケジュール
  7. テストデータの方針
  8. 空ディレクトリでの先行配置 (本ドキュメント実施時)
  9. 採用した方針 (前回議論の確定事項)

1. 設計方針 (推奨採用版)

前回の議論で「推奨」とした方針を全採用しています。

論点採用した方針理由
WebSocket プロトコル型scandata-proto として独立クレートRust ↔ TS で型を共有。scandata-ws に同居させると循環依存と TS 自動生成スクリプトが書きにくい
Application 層 (use case)scandata-app として独立クレート純粋に保ち、tokio 依存を持ち込まない (FP 強寄り方針)
Client 配置モノレポ (ScanDataNet/client/)WebSocket メッセージ型の同期コストを下げる最大の利益
テストデータtiny fixtures を Phase L1 で生成し server/tests/fixtures/ に配置。実データは workspace 外 (../../220408/) を参照リポジトリを軽く保つ + CI で確実に回る
設定永続化scandata-settings として独立クレートJSON 永続化は ファイル I/O と用途が違う。混在で scandata-io が肥大化するのを防ぐ
PyO3 ブリッジscandata-py として独立クレート (Phase L5 後半に追加)maturin ビルドの設定が他と衝突。任意機能として隔離

2. ディレクトリ階層 (全体ツリー)

リポジトリルートから見た全構造です。L1〜 はその Phase 開始時に追加されるディレクトリで、最初から物理的に作成しておきます (空フォルダ + .gitkeep)。

ScanDataNet/ # リポジトリルート (= 現在の ScanDataNet/) │ ├── server/ # ▼ Rust workspace │ ├── Cargo.toml # workspace ルート │ ├── Cargo.lock │ ├── rust-toolchain.toml │ ├── .cargo/ │ │ └── config.toml # clippy::pedantic 等 │ │ │ ├── crates/ │ │ │ │ │ ├── scandata-io/ # 【1】Adapter: ファイル I/O │ │ │ ├── Cargo.toml │ │ │ └── src/ │ │ │ ├── lib.rs │ │ │ ├── error.rs # ScanIoError (enum + thiserror) │ │ │ ├── types.rs # ScanRecord, FluoData, ElecData, SourceFormat │ │ │ ├── reader.rs # ReaderFn 型 + Registry (拡張性の核) │ │ │ ├── common/ # 形式に依存しない純粋関数 │ │ │ │ ├── mod.rs │ │ │ │ ├── frames.rs # subtract_dark_frame, reshape_fortran_3d, axis_transform │ │ │ │ └── channels.rs # split_channels (純粋関数、FluoData には含めない) │ │ │ └── formats/ # 形式別 (新形式はここに追加) │ │ │ ├── mod.rs # 各形式の register() を呼ぶ │ │ │ ├── tsm/ │ │ │ │ ├── mod.rs # ★ pub fn open_tsm_pair (副作用エントリ) │ │ │ │ ├── header.rs # parse_tsm_header (純粋) │ │ │ │ └── tbn.rs # parse_tbn_header, parse_tbn_elec (純粋) │ │ │ ├── da/ │ │ │ │ ├── mod.rs # ★ pub fn open_da │ │ │ │ ├── header.rs # parse_da_header, section_offsets │ │ │ │ └── sections.rs # parse_da_{imaging,elec,dark}_section │ │ │ └── heka/ # Phase L5 以降 │ │ │ └── mod.rs │ │ │ │ │ ├── scandata-domain/ # 【2】Domain: 純粋関数のみ │ │ │ ├── Cargo.toml # tokio / fs を依存に入れない │ │ │ └── src/ │ │ │ ├── lib.rs │ │ │ ├── error.rs │ │ │ ├── types/ # 旧 value_object.py 相当 │ │ │ │ ├── mod.rs │ │ │ │ ├── trace.rs # TraceData │ │ │ │ ├── image.rs # ImageStack, ImageData, FramesData │ │ │ │ ├── text.rs # TextData │ │ │ │ ├── roi.rs # Roi │ │ │ │ └── identifier.rs # ChannelKey, ExperimentId (旧 KeyManager) │ │ │ ├── modifier/ # 旧 modifier.py 相当 │ │ │ │ ├── mod.rs # trait Modifier + apply_chain (fold) │ │ │ │ ├── time_window.rs │ │ │ │ ├── roi.rs │ │ │ │ ├── average.rs │ │ │ │ ├── blcomp.rs │ │ │ │ ├── df_over_f.rs │ │ │ │ ├── normalize.rs │ │ │ │ └── spike.rs │ │ │ ├── analyze/ # 旧 analyze/ 相当 │ │ │ │ ├── mod.rs │ │ │ │ ├── stats.rs │ │ │ │ └── fit.rs # 多項式 / 指数フィット │ │ │ └── experiment.rs # 旧 builder.py の集約ルート │ │ │ │ │ ├── scandata-app/ # 【3】Application: use case (純粋) │ │ │ ├── Cargo.toml # io + domain に依存。tokio に依存しない │ │ │ └── src/ │ │ │ ├── lib.rs │ │ │ ├── error.rs │ │ │ ├── port/ # Hexagonal Port (trait) │ │ │ │ ├── mod.rs │ │ │ │ ├── file_repo.rs # ファイル読み込み抽象 │ │ │ │ └── settings_repo.rs # 設定永続化抽象 │ │ │ └── usecase/ # 旧 controller_*.py の純粋部分 │ │ │ ├── mod.rs │ │ │ ├── open_file.rs │ │ │ ├── add_roi.rs │ │ │ ├── move_roi.rs │ │ │ ├── apply_modifier.rs │ │ │ └── export_csv.rs │ │ │ │ │ ├── scandata-proto/ # 【4】Shared: WS プロトコル (TS と共有) │ │ │ ├── Cargo.toml # serde, schemars (TS 型生成用) │ │ │ └── src/ │ │ │ ├── lib.rs │ │ │ ├── messages.rs # OpenFile, AddRoi, MoveRoi, ApplyModifier... │ │ │ └── events.rs # サーバー → クライアント通知 │ │ │ │ │ ├── scandata-ws/ # 【5】Adapter: WebSocket (副作用) │ │ │ ├── Cargo.toml # axum, tokio, rmp-serde │ │ │ └── src/ │ │ │ ├── main.rs # サーバーエントリポイント │ │ │ ├── handler.rs # WS ハンドラ │ │ │ ├── session.rs # セッション状態 │ │ │ └── codec.rs # MessagePack エンコーダ │ │ │ │ │ ├── scandata-settings/ # 【6】Adapter: 設定永続化 (副作用) │ │ │ ├── Cargo.toml # serde_json, fs │ │ │ └── src/ │ │ │ ├── lib.rs │ │ │ ├── schema.rs # 旧 setting/*.json のスキーマ │ │ │ └── repo.rs # settings_repo trait の実装 │ │ │ │ │ ├── scandata-cli/ # 【7】開発用 CLI │ │ │ ├── Cargo.toml # anyhow, clap │ │ │ └── src/ │ │ │ ├── main.rs │ │ │ └── commands/ │ │ │ ├── mod.rs │ │ │ ├── tsm_info.rs │ │ │ ├── apply_modifier.rs │ │ │ └── export.rs │ │ │ │ │ └── scandata-py/ # 【8】PyO3 ブリッジ (Phase L5 後半) │ │ ├── Cargo.toml # pyo3, numpy, maturin │ │ ├── pyproject.toml # maturin build 設定 │ │ └── src/ │ │ └── lib.rs # scandata-io / domain を Python に公開 │ │ │ ├── benches/ # criterion ベンチマーク │ │ ├── tsm_load.rs │ │ └── modifier_chain.rs │ │ │ └── tests/ # クロスクレート統合テスト │ ├── fixtures/ # ★ tiny テストデータ (Phase L1 で生成) │ │ ├── tiny_4x4x2.tsm # 4×4 ピクセル × 2 フレーム + dark │ │ ├── tiny_4x4x2.tbn # 対応する電気生理 │ │ ├── tiny_4x4x2.da # DA 形式の最小サンプル │ │ └── README.md # どう作ったかの記録 (再生成スクリプトへのリンク) │ └── e2e_open_file.rs │ ├── client/ # ▼ React + TypeScript (モノレポ) │ ├── package.json │ ├── tsconfig.json │ ├── vite.config.ts │ ├── index.html │ │ │ └── src/ │ ├── main.tsx # エントリポイント │ ├── App.tsx │ │ │ ├── proto/ # scandata-proto から自動生成された型 │ │ ├── messages.ts # schemars → typescript で生成 │ │ └── events.ts │ │ │ ├── ws/ # WebSocket クライアント │ │ ├── connection.ts # 接続管理 │ │ ├── client.ts # 型安全な send/recv │ │ └── codec.ts # MessagePack デコーダ │ │ │ ├── state/ # Zustand store │ │ ├── store.ts │ │ └── slices/ │ │ ├── experiment.ts │ │ ├── modifier.ts │ │ └── ui.ts │ │ │ ├── domain/ # FP コア (純粋, readonly) │ │ ├── trace.ts │ │ ├── roi.ts │ │ └── modifier.ts │ │ │ ├── components/ # React コンポーネント │ │ ├── TracePlot/ │ │ │ ├── index.tsx │ │ │ ├── uplot.ts # uPlot wrapper │ │ │ └── style.css │ │ ├── ImageView/ │ │ │ ├── index.tsx │ │ │ └── regl.ts # WebGL renderer │ │ ├── RoiOverlay/ │ │ │ └── index.tsx │ │ ├── ModifierChainPanel/ │ │ ├── FileBrowser/ │ │ └── common/ │ │ ├── Button.tsx │ │ └── Slider.tsx │ │ │ ├── hooks/ # カスタムフック │ │ ├── useWebSocket.ts │ │ ├── useTrace.ts │ │ └── useImageFrame.ts │ │ │ └── utils/ # FP ヘルパ │ ├── pipe.ts │ └── result.ts │ ├── docs/ # ▼ プロジェクトドキュメント │ ├── architecture.md │ ├── learning_log.md # 学習記録 (週次) │ ├── protocol.md # WebSocket プロトコル仕様 │ ├── benchmarks.md │ └── images/ │ ├── scripts/ # ▼ 開発スクリプト │ ├── gen_ts_types.sh # scandata-proto → TS 型生成 │ ├── run_dev.sh # サーバー + フロント同時起動 │ ├── compare_with_python.py # ScanDataPy との数値比較 │ └── make_tiny_fixtures.py # ★ tiny テストデータ生成 (Phase L1) │ ├── tools/ # ▼ 一時ユーティリティ (検証用) │ └── dump_tsm_npy.py # ScanDataPy 側で .npy ダンプ │ ├── deploy/ # ▼ デプロイ (Phase L5 以降) │ └── docker/ │ ├── Dockerfile.server │ └── Dockerfile.client │ ├── .github/ # ▼ CI (Phase L3 以降) │ └── workflows/ │ └── ci.yml │ ├── .gitignore ├── README.md ├── LICENSE ├── LEARNING_WEB_PROJECT_PLAN.md ├── FUNCTIONAL_PROGRAMMING_GUIDELINES.md ├── PHASE_L1_TSM_PARSER.md └── PROJECT_LAYOUT.md # ★ 本ドキュメントの Markdown 版 (将来作成)

3. アーキテクチャ層と Hexagonal の対応

Hexagonal Architecture の Adapter / Application / Domain がクレート単位で物理分離されています。 これにより「Domain から fs を import していない」がコンパイラレベルで保証されます。

Client 層 (TypeScript, ブラウザ) client/ ─ React + uPlot + regl + Zustand Shared Protocol (Rust ↔ TS 型共有) scandata-proto ─ WS メッセージ型 (schemars → TS 自動生成) Adapter 層 ⚡副作用 scandata-io ファイル I/O (.tsm / .da / .heka) scandata-ws WebSocket サーバー (axum + tokio) scandata-settings JSON 永続化 (serde_json + fs) scandata-py (任意) PyO3 ブリッジ (AI 解析連携用) Application 層 (純粋, use case) scandata-app ─ port (trait) + usecase (純粋関数) Domain 層 (純粋関数のみ、外部依存ゼロ) scandata-domain ─ types / modifier / analyze / experiment
図 1. 5 層構造 (Client / Shared Proto / Adapter / Application / Domain) とクレート対応。下位ほど純粋、上位ほど副作用。
FP / Hexagonal の強制力: Domain クレート (scandata-domain) の Cargo.tomltokio, std::fs 関連クレートを書かなければ、 「Domain から副作用を呼んだコード」はコンパイルエラーで止まる。これがクレート分割の最大の利益。

4. クレート別の役割

クレート主な依存純粋?役割
scandata-ioAdapterndarray, byteorder, thiserror副作用TSM/TBN/DA/HEKA を ScanRecord に変換
scandata-domainDomainndarray, thiserror のみ純粋TraceData / Modifier / Roi / analyze。fs/tokio 一切なし
scandata-appApplicationscandata-io, scandata-domain純粋port (trait) + usecase。tokio 依存しない
scandata-protoSharedserde, schemars純粋WebSocket メッセージ型。TS と共有
scandata-wsAdapteraxum, tokio, rmp-serde, scandata-app, scandata-proto副作用WS ハンドラ + セッション管理
scandata-settingsAdapterserde_json, scandata-app (port impl)副作用JSON 設定ファイル永続化
scandata-cliAdapterclap, anyhow, scandata-app副作用tsm_info 等の開発用 CLI
scandata-pyAdapter (任意)pyo3, numpy, maturin, scandata-io副作用Python に open_tsm 等を公開 (AI 解析用)

依存方向の不変条件

# Domain は誰にも依存しない (ndarray, thiserror のみ)
scandata-domain  ────►  (外部 crate のみ)

# Application は io + domain に依存。tokio に依存しない (純粋保持)
scandata-app     ────►  scandata-io, scandata-domain

# 副作用クレートは app と proto を使う
scandata-ws      ────►  scandata-app, scandata-proto, scandata-io  (+ tokio, axum)
scandata-cli     ────►  scandata-app, scandata-io                  (+ clap, anyhow)
scandata-settings────►  scandata-app (port impl)                   (+ serde_json, fs)
scandata-py      ────►  scandata-io, scandata-domain               (+ pyo3, numpy)

5. ScanDataPy との対応関係

ScanDataPy (Python)ScanDataNet (Rust + TS)備考
__main__.pyserver/.../scandata-ws/src/main.rs + client/src/main.tsxエントリポイント 2 系統に分離
common_class.py (FileService, WholeFilename)scandata-io/types.rsパス系は io 寄り
common_class.py (KeyManager)scandata-domain/types/identifier.rsキーはドメイン
model/file_io.pyscandata-io/formats/{tsm,da,heka}/形式別サブモジュール
model/value_object.pyscandata-domain/types/{trace,image,text,roi}.rs不変 struct
model/modifier.pyscandata-domain/modifier/*.rsModifier 1 ファイル
model/builder.pyscandata-app/usecase/open_file.rs + scandata-domain/experiment.rs集約と use case に分離
model/model.py (DataService, Repository)scandata-app/port/ + scandata-app/usecase/Port trait + use case
model/analyze/scandata-domain/analyze/純粋関数で実装
view/view.py (PyQt6)client/src/components/React に置換
view/plotting/client/src/components/{TracePlot,ImageView}/uPlot + regl
controller/controller_main.pyscandata-app/usecase/ + client/src/state/ロジックと UI 状態に分離
controller/controller_axes.pyclient/src/components/ 内のローカル状態UI 側に移動
controller/controller_live_view.pyscandata-app/usecase/ + WS ハンドラ副作用は WS 層に
setting/*.jsonscandata-settings/ + JSON 互換維持段階的に置換
test_pyqt.pyclient/src/components/*.test.tsxフロント側のテストへ

6. Phase 別の物理的な追加スケジュール

最初から空フォルダを全部作っておきますが、各 Phase で実体を書き込む順番は以下の通りです。

Phaseこの時点で書き始めるクレート / ディレクトリ主な成果物
L0 Rust 基礎 server/Cargo.toml (workspace 雛形), scandata-cli tsm-info CLI 動作
L1 TSM パーサ scandata-io, server/tests/fixtures/ (tiny データ), scripts/make_tiny_fixtures.py open_tsm_pair + .npy 一致テスト
L2 ドメイン層 scandata-domain, scandata-app (骨組み) Modifier chain + 数値が ScanDataPy と ±0.001% 一致
L3 WebSocket scandata-proto, scandata-ws, scandata-settings (終盤), .github/workflows/ CLI クライアントから WS でファイル操作
L4 フロント client/ 全体, scripts/gen_ts_types.sh ブラウザでファイル開く + ROI + トレース表示
L5 機能パリティ 各クレート充填, benches/, scandata-py (後半), deploy/ ScanDataPy の主要機能を Web 版で完全再現
L6 主用途切替 ScanDataPy 凍結, README.md 最終化 主ツールが ScanDataNet に

7. テストデータの方針

方針: 大きな研究用 .tsm ファイルを Git で管理しない。 代わりに 4×4 ピクセル × 2 フレーム + dark frame 程度の極小ファイルを Phase L1 で生成し、 server/tests/fixtures/ に置く。実データとの一致確認は workspace 外の ../../220408/ 等を参照。

tiny fixtures の生成 (Phase L1 で実施)

scripts/make_tiny_fixtures.py で、ScanDataPy の TsmFileIo と互換な 最小バイナリを生成します。具体的には:

ファイルサイズ内容
tiny_4x4x2.tsm約 3 KB2880 byte FITS ヘッダ (NAXIS1=4, NAXIS2=4, NAXIS3=3) + 4×4×3 int16 (2 fluo + 1 dark)
tiny_4x4x2.tbn約 100 byte4 byte ヘッダ + 1 ch × 2 frame × bnc=2 の float64 トレース
tiny_4x4x2.da約 6 KB5120 byte ヘッダ + 4×4×2 imaging + 8ch×2×2 elec + 4×4 dark

生成スクリプトの骨格

# scripts/make_tiny_fixtures.py
import numpy as np
from pathlib import Path

OUT = Path(__file__).parent.parent / "server/tests/fixtures"
OUT.mkdir(parents=True, exist_ok=True)

def make_tsm(num_x=4, num_y=4, num_frames=2, exposure_s=0.01):
    header = (
        f"NAXIS1  = {num_x:>20d}"
        f"NAXIS2  = {num_y:>20d}"
        f"NAXIS3  = {num_frames+1:>20d}"      # +1 for dark
        f"EXPOSURE= {exposure_s:>20.6f}"
    ).ljust(2880, ' ').encode('ascii')
    # 簡単な決定的データ (テストで検証しやすいパターン)
    data = np.arange(num_x * num_y * (num_frames + 1), dtype=np.int16) + 100
    data = data.reshape(num_x, num_y, num_frames + 1, order='F')
    return header + data.tobytes(order='F')

(OUT / "tiny_4x4x2.tsm").write_bytes(make_tsm())
# ... tbn / da も同様

fixtures/README.md (生成と意図の記録)

# Tiny test fixtures

これらは小さな決定的テストデータです。再生成する場合:

```
python ../../../scripts/make_tiny_fixtures.py
```

実データとの一致確認は workspace 外の以下を参照:
- ../../220408/20408A001new.tsm  (Phase L1 数値検証用)
- ../../70127A/                  (Phase L1 Stage 3 DA 検証用)

8. 空ディレクトリでの先行配置 (本ドキュメント実施時)

全 Phase で必要になるディレクトリを最初に物理配置します。各空フォルダには .gitkeep を置いて Git で追跡可能にします。

配置内容意図
クレート 8 個の src/ サブディレクトリまで全展開各 Phase 着手時に「どこに何を書くか」を迷わない
各空ディレクトリに .gitkeepGit は空ディレクトリを追跡しないため
Cargo.toml 類は書かない実装時に Phase に応じて書く (空フォルダで十分)
fixtures/README.md書くtiny データ生成方法を残しておく
注意: 既存の ScanDataNet/ 配下には LEARNING_WEB_PROJECT_PLAN.md 等の Markdown が既に置かれています。 これらは Phase L0 で git init される時点までは現状の位置で OK。 Phase L0 で正式に Git リポジトリ化するときに、必要なら docs/ 配下に移動するか検討します。

9. 採用した方針 (前回議論の確定事項)

議題選択肢採用
ファイル形式の拡張機構(A) trait + dyn / (B) 関数ポインタ + Registry / (C) enum_dispatch(B) FP 強寄り、Python 連携時に素直
trait 化のタイミングStage 1 / Stage 3 後 / 最後まで不要Stage 3 完了後に切り出し
src/io/ サブディレクトリ作る / 作らない作らない (クレート名が scandata-io で冗長)
副作用 (open_*) の配置1 ファイル (io.rs) / 形式別 mod.rs形式別 mod.rs に分散
共通出力型形式ごとの struct / ScanRecord 統一ScanRecord 統一
WebSocket プロトコルscandata-ws 内 / scandata-proto 独立scandata-proto 独立
Application 層scandata-ws 内 / scandata-app 独立scandata-app 独立
Client 配置モノレポ / 別リポジトリモノレポ
テストデータGit LFS / tiny 生成 / 外部参照tiny 生成 + 外部参照
PyO3 (AI 連携)最初から / 任意機能 / 不採用Phase L5 後半に scandata-py として追加

作成 2026-05-17 · ScanDataPy → ScanDataNet 全面移行計画