Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 21 additions & 4 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,13 @@ tokeye run "shots/*.npy" --output-dir results # batch inference
tokeye download big_tf_unet # pre-fetch weights, print cache path
tokeye example # write a synthetic demo signal

# Mode-analysis suite
tokeye modespec modes.yaml # classic Mirnov mode-number analysis (vendored pymodespec)
tokeye elmspec "shots/*.npy" # ELM events from the transient channel
tokeye alfvenspec "shots/*.npy" # Alfvén-eigenmode boxes/masks (ae_tf_maskrcnn)
tokeye eigspec # interactive modal ID / SSI (vendored eigspec port)
tokeye modesearch # mode database — design stage, prints the plan

# Lint
uv run ruff check .

Expand All @@ -39,9 +46,11 @@ Source code lives in `src/tokeye/` (installed as `tokeye` package via `uv_build`

### Models (`models/`)
Three model families, each with a `model_*.py` and `config_*.py`:
- **big_tf_unet** — primary transformer U-Net for spectrogram segmentation
- **ae_tf_maskrcnn** — alternative Mask R-CNN approach
- **ae_tf_boxrcnn** — alternative Box R-CNN approach
- **big_tf_unet** — primary transformer U-Net for spectrogram segmentation (HF: `nc1/big_tf_unet`)
- **ae_tf_maskrcnn** — Mask R-CNN instance detector, used by `tokeye alfvenspec` (HF: `nc1/ae_tf_maskrcnn`)
- **ae_tf_boxrcnn** — alternative Box R-CNN approach (not registered, no weights)

`hub.MODEL_REGISTRY` order is load-bearing: `_build_from_state_dict` probes specs in insertion order, so `big_tf_unet` must stay first. `ModelSpec.repo_id` overrides `DEFAULT_REPO_ID` per model.

Shared building blocks in `models/modules/`: `unet.py` (base U-Net), `nn.py` (layers), `bsn.py` (boundary segmentation network).

Expand All @@ -53,7 +62,15 @@ Gradio web interface launched via `tokeye app` (console script) or `python -m to
- **Annotate** (`app/tabs/annotate.py`) — manual labeling interface
- **Utilities** (`app/tabs/utilities.py`) — miscellaneous tools

Shared core modules (used by both the app and the `tokeye` CLI) live directly under `src/tokeye/`: `hub.py` (model registry + Hugging Face auto-download), `transforms.py` (STFT), `inference.py` (model inference), `api.py` (the `TokEye` class — public Python API, lazily exported from the package root), `batch.py` (headless batch runner), `examples.py` (synthetic demo signal), `cli.py` (the `tokeye` console entry point).
Shared core modules (used by both the app and the `tokeye` CLI) live directly under `src/tokeye/`: `hub.py` (model registry + Hugging Face auto-download), `transforms.py` (STFT), `inference.py` (model inference, U-Net contract), `api.py` (the `TokEye` class — public Python API, lazily exported from the package root), `batch.py` (headless batch runner), `examples.py` (synthetic demo signal), `cli/` (the `tokeye` console entry point — one module per subcommand, heavy imports deferred into `_handle` functions).

### Mode-analysis suite
- `modespec/classic/` — **vendored** pymodespec (classic Mirnov mode-number analysis); `modespec/deep/` reserves the next-gen single-chord engine (sibling `integratedmode` project). Vendored code policy: minimal-touch, style rules relaxed in `ruff.toml`, every local change listed in the directory's `PROVENANCE.md`.
- `elmspec/` — ELM event extraction from the transient channel (`events.py` is pure numpy, model plumbing in the CLI handler).
- `alfvenspec/` — R-CNN detection wrapper (`inference.py`; list-of-images contract, windowed processing for wide spectrograms).
- `eigspec/` — **vendored** eigspec MATLAB-toolbox port (modal ID, SSI, random projection); sklearn-dependent clustering behind the `eigspec` extra.
- `modesearch/` — design-stage scaffold only (mode database vision).
- Suite roadmap and future ideas: `docs/ROADMAP.md`.

### Training (`training/`)
Multi-step data pipelines (step_0 through step_7) for preparing training data from raw signals. Two regimes: `big_tf_unet/` (original) and `big_tf_unet_multiscale/` (enhanced). Uses PyTorch Lightning.
Expand Down
23 changes: 19 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,20 @@ tokeye download big_tf_unet # on the login node; prints the cached path
tokeye run ... --model big_tf_unet # on the compute node — model is already cached
```

## Mode-analysis suite

Beyond segmentation, `tokeye` bundles the analyses DIII-D researchers usually reach for separate tools to get. Each is a subcommand; `--help` on any of them shows the full flags.

| Command | What it does |
| --- | --- |
| `tokeye modespec <config.yaml>` | Classic Mirnov mode analysis (vendored [pymodespec](src/tokeye/modespec/classic/PROVENANCE.md), the Python port of the IDL `modespec` tool): power spectrograms, matched-filter toroidal mode-number fits, per-shot mode CSVs. Data fetch needs MDSplus (GA cluster / conda-forge) or a local cache; an example config ships at `src/tokeye/modespec/classic/modes.yaml`. |
| `tokeye elmspec INPUTS...` | ELM detection from the segmentation model's transient channel: per-event time intervals plus per-shot count, ELM frequency (with `--fs`), and duty cycle, written to `elm_events.csv` / `elm_summary.csv`. |
| `tokeye alfvenspec INPUTS...` | Alfvén-eigenmode detection with the `ae_tf_maskrcnn` instance model: per-detection boxes/scores (`ae_detections.csv`) and instance masks. Wide spectrograms are processed in training-width windows automatically. |
| `tokeye eigspec [SCRIPT]` | Interactive modal identification and spectral analysis (vendored [eigspec](src/tokeye/eigspec/PROVENANCE.md), the Python port of the MATLAB toolbox): stochastic subspace ID, AR/PCA, random-projection spectral analysis, clustering (clustering needs `pip install tokeye[eigspec]`). |
| `tokeye modesearch` | Design stage — prints the plan for a searchable database of detected modes. |

The suite roadmap (including the next-generation `modespec --engine deep`) lives in [docs/ROADMAP.md](docs/ROADMAP.md).

## Web app guide

`tokeye app` (or `python -m tokeye.app`) launches a Gradio interface with three tabs:
Expand Down Expand Up @@ -152,11 +166,12 @@ This creates a `.venv/`; activate it with `source .venv/bin/activate`, or prefix

## Models

| Registry name | HF file | Description |
| --- | --- | --- |
| `big_tf_unet` | `big_tf_unet_251210.pt` | Transformer U-Net trained on multiscale (multiwindow, multihop) spectrograms. |
| Registry name | HF repo | HF file | Description |
| --- | --- | --- | --- |
| `big_tf_unet` | [`nc1/big_tf_unet`](https://huggingface.co/nc1/big_tf_unet) | `big_tf_unet_251210.pt` | Transformer U-Net trained on multiscale (multiwindow, multihop) spectrograms. |
| `ae_tf_maskrcnn` | `nc1/ae_tf_maskrcnn` | `ae_tf_maskrcnn_251223.pt` | Mask R-CNN instance detector for Alfvén-eigenmode activity (used by `tokeye alfvenspec`). |

Weights are hosted on [Hugging Face](https://huggingface.co/nc1/big_tf_unet) and download automatically the first time a registry name is used (cached in `~/.cache/huggingface`). Override the source repo with the `TOKEYE_HF_REPO` environment variable.
Weights download automatically the first time a registry name is used (cached in `~/.cache/huggingface`). Override the default repo with the `TOKEYE_HF_REPO` environment variable (per-model repos are fixed in the registry).

To use a local checkpoint instead, put `.pt`/`.pt2` files in a `model/` directory (picked up by the app's model dropdown) or pass a path directly via `--model PATH`.

Expand Down
95 changes: 95 additions & 0 deletions docs/ROADMAP.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
# TokEye roadmap — toward the go-to mode-analysis tool

TokEye's goal is to cover the jobs DIII-D researchers currently spread across
separate tools (modespec, ad-hoc ELM scripts, per-group AE workflows), so one
install answers "what modes are in this shot?". This file tracks the suite and
collects future ideas worth building.

## Suite status (0.12.0)

| Tool | Command | Status |
|------|---------|--------|
| Segmentation | `tokeye run`, `tokeye app` | shipped (big_tf_unet) |
| modespec (classic) | `tokeye modespec <config.yaml>` | shipped — vendored pymodespec (Mirnov n-number fits; needs MDSplus or a cache) |
| modespec (deep) | `tokeye modespec --engine deep` | reserved — single-chord CO2 n-inference, developed in the sibling `integratedmode` project |
| elmspec | `tokeye elmspec INPUTS...` | shipped — ELM events from the transient channel |
| alfvenspec | `tokeye alfvenspec INPUTS...` | shipped, deliberately thin — ae_tf_maskrcnn boxes/masks; awaiting EP-group requirements |
| eigspec | `tokeye eigspec [SCRIPT]` | shipped — vendored (MIT) with import + SSI numeric fixes (see its PROVENANCE.md; fixes worth upstreaming) |
| modesearch | `tokeye modesearch` | design stage — prints the plan |

## Near-term engineering

- **Upload `ae_tf_maskrcnn` weights to `nc1/ae_tf_maskrcnn`** (registry entry
and upload-script probe are in place; needs a write-scoped HF token).
- **Upstream the eigspec fixes**: the vendored copy fixes two numeric bugs in
`covariance_driven_ssi` (Hankel channel-interleave, spurious transpose) plus
import-breaking syntax errors — push these back to PlasmaControl/eigspec and
audit the sibling SSI variants (`ssi1ca`, `ssicca`) for the same layout bug.
- **AE weights provenance**: score calibration and a labeled validation set
for alfvenspec before promoting it beyond "runs the model".

## Mode catalogue schema (the keystone)

A single record type that every detector emits, so downstream tools compose:

shot, machine, diagnostic, t_start, t_end, f_low, f_high,
n (nullable), m (nullable), amplitude, confidence,
detector, detector_version, artifact_ref

- `big_tf_unet` masks → connected regions → records (coherent/transient class)
- `modespec` CSV rows → records with `n` filled
- `elmspec` events → transient records tagged ELM
- `alfvenspec` boxes → records tagged AE

Once this exists, modesearch is "crawler + storage + filters" rather than a
research project. It also gives papers a uniform unit of comparison across
detectors.

## modesearch build-out

1. Crawler: batch job over shot archives (local HDF5 first; MDSplus/toksearch
where reachable) running the suite and emitting catalogue records.
2. Storage: start boring — one parquet/SQLite per campaign; revisit only if
query load demands it.
3. Query CLI: `tokeye modesearch find --n 2 --f 2e3:4e3 --no-elm` → shot list
with matching events.
4. Consumers: the fusion-world-model shot designer learns mode-occurrence
statistics conditioned on plasma parameters; shotsearch intersection
("shots near this setup that developed a locked mode").

## Ideas that would be extremely useful to mode researchers

- **Mode-number labeling of TokEye masks.** Fuse modespec n-fits with U-Net
regions: overlap a mask region with the (t, f) support of an n-fit and the
region inherits the mode number. Turns "coherent activity" into "n=2 TM",
which is what people actually search for.
- **Mode trajectory tracking.** Follow a detected mode's (f, amplitude, n)
through time: frequency chirps, mode locking (f → 0), rotation braking.
Locked-mode precursors as a first-class query.
- **Cross-diagnostic confirmation.** The same mode seen on Mirnov, CO2, ECE,
and BES with consistent frequency is real; single-diagnostic detections get
a lower confidence. The catalogue schema's `diagnostic` field enables this.
- **ELM database.** elmspec over campaigns → ELM frequency/size statistics vs
pedestal parameters; ELM-free-window finder for AE/TM studies.
- **AE taxonomy.** Classify alfvenspec detections (TAE/RSAE/EAE/BAE) from
frequency-vs-time shape and q-profile context — the EP group's actual need;
gather their requirements before building.
- **Sawtooth/MRE integration.** The vendored classic tree already carries ECE
sawtooth and MRE helpers (`ece_sawteeth.py`, `mre_utils.py`); surface them
as first-class detectors emitting catalogue records.
- **Inter-shot mode.** A between-shots summary (30 s budget): run the suite on
the last shot, print/annotate the mode inventory for the control room.
- **Cross-machine record.** TJ-II validation already exists for the U-Net;
keep the catalogue schema machine-agnostic so C-Mod/NSTX-U/MAST-U archives
can be crawled without schema surgery.
- **Confidence calibration.** Per-detector reliability curves (detected vs
human-labeled) so catalogue confidences are comparable across detectors —
prerequisite for any world-model consumer treating them as probabilities.
- **OMFIT/toksearch hooks.** Thin adapters so existing GA workflows can call
`tokeye.api.TokEye` and the suite CLIs without leaving their environment.

## Non-goals (for now)

Real-time control integration (inter-shot is the nearer target), automatic
retraining pipelines, and cross-machine transfer learning beyond what the
existing TJ-II validation demonstrates.
8 changes: 7 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "uv_build"

[project]
name = "tokeye"
version = "0.11.0"
version = "0.12.0"
description = "Automatic classification and localization of fluctuating signals in spectrograms"
readme = "README.md"
requires-python = ">=3.13"
Expand All @@ -22,9 +22,15 @@ dependencies = [
"pydantic",
"huggingface-hub>=0.30",
"tqdm",
"pyyaml", # tokeye.modespec.classic configs (already transitive via gradio)
]

[project.optional-dependencies]
# Optional extra for tokeye.eigspec clustering (sklearn imports are
# function-local upstream, so the base package imports without it).
eigspec = [
"scikit-learn",
]
# Mirror of the `train` dependency-group so consumers can `pip install tokeye[train]`.
# The training/ablation pipeline modules (src/tokeye/training/) import these; the
# core package and the Gradio app do not require them.
Expand Down
10 changes: 10 additions & 0 deletions ruff.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,14 @@ ignore = [
# for research scripts (late imports after sys.path setup, terse one-liners, etc.)
"scripts/**" = ["E402", "E702", "E741", "SIM115", "PTH208"]
"src/tokeye/extra/eval/**" = ["E702"]
# Vendored code (see PROVENANCE.md in each dir): style rules relaxed to keep
# the upstream diff minimal; correctness rules (F821 etc.) stay active.
"src/tokeye/modespec/classic/**" = [
"E", "W", "I", "UP", "C4", "FA", "ISC", "ICN", "RET", "SIM", "TID", "TC",
"PTH", "TD", "NPY", "F401", "F841",
]
"src/tokeye/eigspec/**" = [
"E", "W", "I", "UP", "C4", "FA", "ISC", "ICN", "RET", "SIM", "TID", "TC",
"PTH", "TD", "NPY", "F401", "F841", "F811", "F541",
]

34 changes: 27 additions & 7 deletions scripts/upload_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,15 +42,33 @@
from huggingface_hub import HfApi
from huggingface_hub.utils import HfHubHTTPError, LocalTokenNotFoundError

from tokeye.hub import DEFAULT_MODEL, DEFAULT_REPO_ID, MODEL_REGISTRY
from tokeye.hub import DEFAULT_MODEL, MODEL_REGISTRY, repo_for

if TYPE_CHECKING:
from collections.abc import Callable

_PROBE_SHAPE = (1, 1, 64, 64)

def _probe_segmentation(model: nn.Module) -> None:
"""Forward pass for the (B, 1, H, W) -> (B, 2, H, W) U-Net contract."""
model(torch.randn(1, 1, 64, 64))

def verify_checkpoint(path: Path, builder: Callable[[], nn.Module]) -> None:

def _probe_rcnn(model: nn.Module) -> None:
"""Forward pass for the torchvision R-CNN list-of-images contract."""
model([torch.randn(3, 64, 64)])


_PROBES: dict[str, Callable[[nn.Module], None]] = {
"big_tf_unet": _probe_segmentation,
"ae_tf_maskrcnn": _probe_rcnn,
}


def verify_checkpoint(
path: Path,
builder: Callable[[], nn.Module],
probe: Callable[[nn.Module], None] = _probe_segmentation,
) -> None:
"""Refuse (via ``SystemExit``) to proceed unless ``path`` is a good checkpoint.

"Good" means: a weights-only state dict that loads strictly into a fresh
Expand Down Expand Up @@ -86,7 +104,7 @@ def verify_checkpoint(path: Path, builder: Callable[[], nn.Module]) -> None:
model.eval()
with torch.no_grad():
try:
model(torch.randn(*_PROBE_SHAPE))
probe(model)
except Exception as exc:
raise SystemExit(
f"error: {path} loaded but failed a forward-pass sanity "
Expand Down Expand Up @@ -123,8 +141,8 @@ def build_parser() -> argparse.ArgumentParser:
)
parser.add_argument(
"--repo",
default=DEFAULT_REPO_ID,
help="Target Hugging Face Hub repo id (default: %(default)s).",
default=None,
help="Target Hugging Face Hub repo id (default: the model's registry repo).",
)
parser.add_argument(
"--create",
Expand All @@ -138,11 +156,13 @@ def main() -> int:
args = build_parser().parse_args()
spec = MODEL_REGISTRY[args.model]
file_path = _resolve_file(args.model, args.file)
if args.repo is None:
args.repo = repo_for(args.model)

if not file_path.exists():
raise SystemExit(f"error: checkpoint not found: {file_path}")

verify_checkpoint(file_path, spec.builder)
verify_checkpoint(file_path, spec.builder, _PROBES.get(args.model, _probe_segmentation))

api = HfApi()
try:
Expand Down
18 changes: 18 additions & 0 deletions src/tokeye/alfvenspec/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
"""Alfvén-eigenmode detection tools (energetic-particle group).

Deliberately thin for now: ``tokeye alfvenspec`` runs the ``ae_tf_maskrcnn``
instance-detection model over spectrograms and writes per-detection boxes,
scores, and masks. Deeper EP-group workflows (AE taxonomy, cross-diagnostic
checks) land here once their requirements are gathered — see docs/ROADMAP.md.
"""

from __future__ import annotations

from tokeye.alfvenspec.inference import (
DEFAULT_WINDOW_COLS,
detect,
detect_windowed,
write_detections_csv,
)

__all__ = ["DEFAULT_WINDOW_COLS", "detect", "detect_windowed", "write_detections_csv"]
Loading
Loading