Fix silent failure modes surfaced by extensive code review
- energy_simplex_encode: warn when clipping post_E to pre_E discards recorded edep/e_sec instead of silently zeroing them - local/inv_local_frame_rotation: validate and normalize pre_dir instead of silently assuming unit norm; raise on near-zero-norm rows - train(): make --lr authoritative on resume instead of being silently overwritten by the checkpoint's optimizer/scheduler state; print and exit cleanly instead of silently training zero epochs when the checkpoint already meets --epochs; truncate metrics.csv on a fresh run instead of always appending - dwarf update-manifest: check file existence for every manifest line, not just ones whose gen/schema actually changed - pyproject.toml: dev extra now pulls in convert+analysis so the documented `uv sync --extra cpu --extra dev` + `pytest` actually passes collection Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
import warnings
|
||||
|
||||
import numpy as np
|
||||
|
||||
_EPS = 1e-8
|
||||
@@ -37,10 +39,26 @@ def energy_simplex_encode(
|
||||
`energy_simplex_decode` is its inverse (up to the floor softening).
|
||||
"""
|
||||
pre_E = np.maximum(np.asarray(pre_E, dtype=np.float32), _EPS)
|
||||
post_E = np.clip(np.asarray(post_E, dtype=np.float32), 0.0, pre_E)
|
||||
raw_post_E = np.asarray(post_E, dtype=np.float32)
|
||||
post_E = np.clip(raw_post_E, 0.0, pre_E)
|
||||
delta_e = pre_E - post_E
|
||||
lost = np.asarray(edep, dtype=np.float32) + np.asarray(e_sec, dtype=np.float32)
|
||||
edep = np.asarray(edep, dtype=np.float32)
|
||||
e_sec = np.asarray(e_sec, dtype=np.float32)
|
||||
lost = edep + e_sec
|
||||
has_loss = lost > _EPS
|
||||
# Clipping post_E down to pre_E forces delta_e (and thus the rescaled
|
||||
# edep/e_sec below) to 0 even on rows where edep/e_sec were genuinely
|
||||
# recorded as nonzero — warn so this doesn't silently discard real data.
|
||||
discarded = (raw_post_E > pre_E) & has_loss
|
||||
if np.any(discarded):
|
||||
n = int(np.sum(discarded))
|
||||
warnings.warn(
|
||||
f"energy_simplex_encode: {n}/{len(discarded)} step(s) had "
|
||||
"post_E > pre_E (clipped) while recording nonzero edep/e_sec; "
|
||||
"that recorded energy deposit is discarded to keep delta_e "
|
||||
"consistent with the clip.",
|
||||
stacklevel=2,
|
||||
)
|
||||
scale = np.where(has_loss, delta_e / np.maximum(lost, _EPS), 0.0)
|
||||
# Where nothing was recorded as deposited/secondary but energy was lost,
|
||||
# attribute all of delta_e to local deposit.
|
||||
@@ -108,6 +126,26 @@ def _cross_with_z_axis(axis: np.ndarray, v: np.ndarray) -> np.ndarray:
|
||||
return np.concatenate([ay * bz, -ax * bz, ax * by - ay * bx], axis=1)
|
||||
|
||||
|
||||
def _validate_unit_pre_dir(pre_dir: np.ndarray) -> np.ndarray:
|
||||
"""Normalize pre_dir and raise if any row is too degenerate to define a frame.
|
||||
|
||||
`local_frame_rotation`/`inv_local_frame_rotation` treat pre_dir[:, 2] as
|
||||
cos(angle to ẑ), which is only correct for a unit vector. Small float32
|
||||
drift is corrected silently; a near-zero-norm row has no well-defined
|
||||
direction, so it's raised loudly instead of producing a meaningless
|
||||
rotation (previously it fell through to an arbitrary axis with no error).
|
||||
"""
|
||||
pre_dir = np.asarray(pre_dir, dtype=np.float32)
|
||||
norm = np.linalg.norm(pre_dir, axis=1, keepdims=True)
|
||||
if np.any(norm < 1e-6):
|
||||
raise ValueError(
|
||||
f"pre_dir has {int(np.sum(norm < 1e-6))} row(s) with near-zero norm "
|
||||
"(< 1e-6); local/inv_local_frame_rotation require a well-defined "
|
||||
"incoming direction for every row."
|
||||
)
|
||||
return pre_dir / norm
|
||||
|
||||
|
||||
def local_frame_rotation(pre_dir: np.ndarray, post_dir: np.ndarray) -> np.ndarray:
|
||||
"""Rotate post_dir into the local frame where pre_dir maps to ẑ (Rodrigues).
|
||||
|
||||
@@ -115,6 +153,7 @@ def local_frame_rotation(pre_dir: np.ndarray, post_dir: np.ndarray) -> np.ndarra
|
||||
expressed relative to a coordinate system in which the incoming particle
|
||||
travels along +z.
|
||||
"""
|
||||
pre_dir = _validate_unit_pre_dir(pre_dir)
|
||||
cos_t = np.clip(pre_dir[:, 2:3], -1.0, 1.0) # (N,1); dot with ẑ = z-component
|
||||
sin_t = np.sqrt(np.maximum(0.0, 1.0 - cos_t**2)) # (N,1)
|
||||
|
||||
@@ -227,6 +266,7 @@ def inv_local_frame_rotation(
|
||||
|
||||
Applies R^T (same axis, negative angle) to post_dir_local.
|
||||
"""
|
||||
pre_dir = _validate_unit_pre_dir(pre_dir)
|
||||
cos_t = np.clip(pre_dir[:, 2:3], -1.0, 1.0) # dot with ẑ = z-component
|
||||
sin_t = np.sqrt(np.maximum(0.0, 1.0 - cos_t**2))
|
||||
|
||||
|
||||
+21
-2
@@ -103,9 +103,28 @@ def train(
|
||||
start_epoch = ckpt.get("epoch", 0) + 1
|
||||
best_val_loss = ckpt.get("best_val_loss", float("inf"))
|
||||
|
||||
# optimizer/lr_sched.load_state_dict() above restore the checkpoint's
|
||||
# own base LR, which would otherwise silently override an explicit
|
||||
# `lr` argument. Make `lr` authoritative again, applied at whatever
|
||||
# point the cosine/warmup schedule has already reached.
|
||||
lr_sched.base_lrs = [lr for _ in lr_sched.base_lrs]
|
||||
resumed_lr = lr * _lr_lambda(lr_sched.last_epoch)
|
||||
for group in optimizer.param_groups:
|
||||
group["lr"] = resumed_lr
|
||||
|
||||
if start_epoch > epochs:
|
||||
print(
|
||||
f"checkpoint already completed epoch {start_epoch - 1} "
|
||||
f"(>= --epochs {epochs}) — nothing to train"
|
||||
)
|
||||
return
|
||||
|
||||
metrics_path = out_dir / "metrics.csv"
|
||||
write_header = not (resume_path is not None and metrics_path.exists())
|
||||
metrics_file = open(metrics_path, "a", newline="")
|
||||
resuming_existing_metrics = resume_path is not None and metrics_path.exists()
|
||||
write_header = not resuming_existing_metrics
|
||||
metrics_file = open(
|
||||
metrics_path, "a" if resuming_existing_metrics else "w", newline=""
|
||||
)
|
||||
metrics_writer = csv.DictWriter(metrics_file, fieldnames=_METRICS_FIELDS)
|
||||
if write_header:
|
||||
metrics_writer.writeheader()
|
||||
|
||||
Reference in New Issue
Block a user