Apply ruff format and document lint/type tooling in CLAUDE.md

First repo-wide ruff format pass, plus a note in CLAUDE.md to run
ruff and ty periodically.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-18 17:40:27 +02:00
co-authored by Claude Sonnet 4.6
parent 53fd2e4405
commit 8cebc4809d
20 changed files with 668 additions and 307 deletions
+160 -80
View File
@@ -54,9 +54,16 @@ class Coord(str, Enum):
@app.command()
def train(
data: Annotated[Path, typer.Argument(help="Parquet file or directory of parquet files")],
config: Annotated[Optional[Path], typer.Option(help="TOML config file (overridden by explicit flags)")] = None,
mode: Annotated[Optional[Mode], typer.Option(help="Generative model: flow matching or DDPM")] = None,
data: Annotated[
Path, typer.Argument(help="Parquet file or directory of parquet files")
],
config: Annotated[
Optional[Path],
typer.Option(help="TOML config file (overridden by explicit flags)"),
] = None,
mode: Annotated[
Optional[Mode], typer.Option(help="Generative model: flow matching or DDPM")
] = None,
epochs: Annotated[Optional[int], typer.Option()] = None,
batch_size: Annotated[Optional[int], typer.Option()] = None,
lr: Annotated[Optional[float], typer.Option()] = None,
@@ -64,25 +71,55 @@ def train(
n_blocks: Annotated[Optional[int], typer.Option()] = None,
emb_dim: Annotated[Optional[int], typer.Option()] = None,
val_fraction: Annotated[Optional[float], typer.Option()] = None,
seed: Annotated[Optional[int], typer.Option(help="Random seed for reproducibility")] = None,
validate_every: Annotated[Optional[int], typer.Option(help="Run marginal+KL validation every N epochs (0 disables)")] = None,
shuffle_buffer: Annotated[int, typer.Option(help="Rows held in RAM per worker for shuffling")] = 65536,
out: Annotated[Optional[Path], typer.Option(help="Checkpoint dir (default: auto from hyperparams)")] = None,
device: Annotated[Optional[str], typer.Option(help="cpu | cuda | mps (default: auto)")] = None,
seed: Annotated[
Optional[int], typer.Option(help="Random seed for reproducibility")
] = None,
validate_every: Annotated[
Optional[int],
typer.Option(help="Run marginal+KL validation every N epochs (0 disables)"),
] = None,
shuffle_buffer: Annotated[
int, typer.Option(help="Rows held in RAM per worker for shuffling")
] = 65536,
out: Annotated[
Optional[Path],
typer.Option(help="Checkpoint dir (default: auto from hyperparams)"),
] = None,
device: Annotated[
Optional[str], typer.Option(help="cpu | cuda | mps (default: auto)")
] = None,
num_workers: Annotated[Optional[int], typer.Option()] = None,
resume: Annotated[Optional[Path], typer.Option(help="Checkpoint .pt to resume training from")] = None,
resume: Annotated[
Optional[Path], typer.Option(help="Checkpoint .pt to resume training from")
] = None,
) -> None:
"""Train the GIANT surrogate model."""
cli_train = {k: v for k, v in {
"mode": mode.value if mode is not None else None,
"epochs": epochs, "batch_size": batch_size, "lr": lr,
"val_fraction": val_fraction, "num_workers": num_workers, "seed": seed,
"validate_every": validate_every,
}.items() if v is not None}
cli_model = {k: v for k, v in {
"hidden_dim": hidden_dim, "n_blocks": n_blocks, "emb_dim": emb_dim,
}.items() if v is not None}
cfg = gconfig.merge_cli_overrides(gconfig.DEFAULT_CONFIG, config, cli_train, cli_model)
cli_train = {
k: v
for k, v in {
"mode": mode.value if mode is not None else None,
"epochs": epochs,
"batch_size": batch_size,
"lr": lr,
"val_fraction": val_fraction,
"num_workers": num_workers,
"seed": seed,
"validate_every": validate_every,
}.items()
if v is not None
}
cli_model = {
k: v
for k, v in {
"hidden_dim": hidden_dim,
"n_blocks": n_blocks,
"emb_dim": emb_dim,
}.items()
if v is not None
}
cfg = gconfig.merge_cli_overrides(
gconfig.DEFAULT_CONFIG, config, cli_train, cli_model
)
t, m = cfg["train"], cfg["model"]
_device = torch.device(device) if device else gconfig.auto_device()
@@ -99,26 +136,45 @@ def train(
typer.echo(f"out_dir: {out_dir}")
run_train_job(
data=data, cfg=cfg, out_dir=out_dir, device=_device,
shuffle_buffer=shuffle_buffer, num_workers=t["num_workers"],
resume=resume, echo=typer.echo,
data=data,
cfg=cfg,
out_dir=out_dir,
device=_device,
shuffle_buffer=shuffle_buffer,
num_workers=t["num_workers"],
resume=resume,
echo=typer.echo,
)
@app.command()
def predict(
data: Annotated[Path, typer.Argument(help="Parquet file or directory of parquet files")],
checkpoint: Annotated[Path, typer.Option(help="Path to checkpoint .pt file (best.pt or last.pt)")],
coord: Annotated[Coord, typer.Option(
help="global: full physical units, world frame (default). "
"local: raw 9D model output (denormalised only, local frame, "
"log-scaled scalars) alongside the matching ground-truth target "
"for the same input file — requires post-step columns."
)] = Coord.global_,
out: Annotated[Optional[Path], typer.Option(help="Output parquet path (default: <data>_predicted[_local].parquet)")] = None,
data: Annotated[
Path, typer.Argument(help="Parquet file or directory of parquet files")
],
checkpoint: Annotated[
Path, typer.Option(help="Path to checkpoint .pt file (best.pt or last.pt)")
],
coord: Annotated[
Coord,
typer.Option(
help="global: full physical units, world frame (default). "
"local: raw 9D model output (denormalised only, local frame, "
"log-scaled scalars) alongside the matching ground-truth target "
"for the same input file — requires post-step columns."
),
] = Coord.global_,
out: Annotated[
Optional[Path],
typer.Option(
help="Output parquet path (default: <data>_predicted[_local].parquet)"
),
] = None,
batch_size: Annotated[int, typer.Option(help="Inference batch size")] = 4096,
steps: Annotated[int, typer.Option(help="Flow matching ODE steps")] = 10,
device: Annotated[Optional[str], typer.Option(help="cpu | cuda | mps (default: auto)")] = None,
device: Annotated[
Optional[str], typer.Option(help="cpu | cuda | mps (default: auto)")
] = None,
) -> None:
"""Run trained model on a parquet file and save predictions."""
_device = torch.device(device) if device else gconfig.auto_device()
@@ -127,7 +183,10 @@ def predict(
# --- Load checkpoint ---
ckpt = torch.load(checkpoint, map_location="cpu", weights_only=False)
if "model_config" not in ckpt:
typer.echo("error: checkpoint has no model_config — retrain with the current code", err=True)
typer.echo(
"error: checkpoint has no model_config — retrain with the current code",
err=True,
)
raise typer.Exit(1)
model_cfg = ckpt["model_config"]
@@ -145,7 +204,9 @@ def predict(
# --- Output path ---
if out is None:
stem = data.stem if data.is_file() else data.name
suffix = "_predicted_local.parquet" if coord == Coord.local else "_predicted.parquet"
suffix = (
"_predicted_local.parquet" if coord == Coord.local else "_predicted.parquet"
)
out = data.parent / f"{stem}{suffix}"
typer.echo(f"output: {out}")
@@ -162,10 +223,14 @@ def predict(
N = len(chunk["event_id"])
if coord == Coord.local:
cond_cont, cond_cat, target_raw, _, _ = build_features(chunk, pdg_map, mat_map)
cond_cont, cond_cat, target_raw, _, _ = build_features(
chunk, pdg_map, mat_map
)
cond_cont = cond_norm.transform(cond_cont)
else:
cond_cont, cond_cat = build_cond_features(chunk, pdg_map, mat_map, cond_norm)
cond_cont, cond_cat = build_cond_features(
chunk, pdg_map, mat_map, cond_norm
)
# Inference in batch_size slices
pred_parts = []
@@ -180,22 +245,30 @@ def predict(
raw = tgt_norm.inverse_transform(pred)
if coord == Coord.local:
table = pa.table({
"event_id": chunk["event_id"],
"pdg": chunk["pdg"],
"pre_x": chunk["pre_pos"][:, 0],
"pre_y": chunk["pre_pos"][:, 1],
"pre_z": chunk["pre_pos"][:, 2],
"pre_E": chunk["pre_E"],
"pre_dx": chunk["pre_dir"][:, 0],
"pre_dy": chunk["pre_dir"][:, 1],
"pre_dz": chunk["pre_dir"][:, 2],
"material": chunk["material"],
"layer_id": chunk["layer_id"],
"n_sec": chunk["n_sec"],
**{f"pred_{name}": raw[:, j] for j, name in enumerate(LOCAL_TARGET_NAMES)},
**{f"true_{name}": target_raw[:, j] for j, name in enumerate(LOCAL_TARGET_NAMES)},
})
table = pa.table(
{
"event_id": chunk["event_id"],
"pdg": chunk["pdg"],
"pre_x": chunk["pre_pos"][:, 0],
"pre_y": chunk["pre_pos"][:, 1],
"pre_z": chunk["pre_pos"][:, 2],
"pre_E": chunk["pre_E"],
"pre_dx": chunk["pre_dir"][:, 0],
"pre_dy": chunk["pre_dir"][:, 1],
"pre_dz": chunk["pre_dir"][:, 2],
"material": chunk["material"],
"layer_id": chunk["layer_id"],
"n_sec": chunk["n_sec"],
**{
f"pred_{name}": raw[:, j]
for j, name in enumerate(LOCAL_TARGET_NAMES)
},
**{
f"true_{name}": target_raw[:, j]
for j, name in enumerate(LOCAL_TARGET_NAMES)
},
}
)
else:
step_length = inv_log_transform(raw[:, 0])
delta_e = inv_log_transform(raw[:, 1])
@@ -205,7 +278,9 @@ def predict(
post_dir_local = raw[:, 3:6].copy()
norms = np.linalg.norm(post_dir_local, axis=1, keepdims=True)
post_dir_local /= np.where(norms < 1e-8, 1.0, norms)
post_dir_world = inv_local_frame_rotation(chunk["pre_dir"], post_dir_local)
post_dir_world = inv_local_frame_rotation(
chunk["pre_dir"], post_dir_local
)
# Same for the travel direction, then reconstruct post_pos from
# the single shared step_length so the two stay consistent.
@@ -216,34 +291,38 @@ def predict(
chunk["pre_pos"], chunk["pre_dir"], step_length, travel_dir_local
)
table = pa.table({
"event_id": chunk["event_id"],
"pdg": chunk["pdg"],
"pre_x": chunk["pre_pos"][:, 0],
"pre_y": chunk["pre_pos"][:, 1],
"pre_z": chunk["pre_pos"][:, 2],
"pre_E": chunk["pre_E"],
"pre_dx": chunk["pre_dir"][:, 0],
"pre_dy": chunk["pre_dir"][:, 1],
"pre_dz": chunk["pre_dir"][:, 2],
"material": chunk["material"],
"layer_id": chunk["layer_id"],
"n_sec": chunk["n_sec"],
"step_length": step_length,
"delta_e": delta_e,
"edep": edep,
"post_dx": post_dir_world[:, 0],
"post_dy": post_dir_world[:, 1],
"post_dz": post_dir_world[:, 2],
"post_x": post_pos_world[:, 0],
"post_y": post_pos_world[:, 1],
"post_z": post_pos_world[:, 2],
})
table = pa.table(
{
"event_id": chunk["event_id"],
"pdg": chunk["pdg"],
"pre_x": chunk["pre_pos"][:, 0],
"pre_y": chunk["pre_pos"][:, 1],
"pre_z": chunk["pre_pos"][:, 2],
"pre_E": chunk["pre_E"],
"pre_dx": chunk["pre_dir"][:, 0],
"pre_dy": chunk["pre_dir"][:, 1],
"pre_dz": chunk["pre_dir"][:, 2],
"material": chunk["material"],
"layer_id": chunk["layer_id"],
"n_sec": chunk["n_sec"],
"step_length": step_length,
"delta_e": delta_e,
"edep": edep,
"post_dx": post_dir_world[:, 0],
"post_dy": post_dir_world[:, 1],
"post_dz": post_dir_world[:, 2],
"post_x": post_pos_world[:, 0],
"post_y": post_pos_world[:, 1],
"post_z": post_pos_world[:, 2],
}
)
table = table.replace_schema_metadata({
PREDICT_COORD_METADATA_KEY: coord.value,
PREDICT_SCHEMA_VERSION_KEY: PREDICT_SCHEMA_VERSION,
})
table = table.replace_schema_metadata(
{
PREDICT_COORD_METADATA_KEY: coord.value,
PREDICT_SCHEMA_VERSION_KEY: PREDICT_SCHEMA_VERSION,
}
)
if writer is None:
writer = pq.ParquetWriter(out, table.schema)
@@ -255,5 +334,6 @@ def predict(
typer.echo(f"wrote {total:,} rows → {out}")
if __name__ == "__main__":
app()