Skip to content
Closed
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
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
9 changes: 5 additions & 4 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ jobs:
- name: check_wiki.py --check (research wiki drift)
working-directory: autolens_profiling
run: |
# Every results/notes ledger is linked from wiki/, every campaign page
# Every ledger (wiki/campaigns/**) is linked from wiki/, every campaign page
# has an index row and the full header, every relative wiki link
# resolves. Pure stdlib -- no PyAuto* imports, no PYTHONPATH.
python scripts/misc/tooling/check_wiki.py --check
Expand All @@ -126,9 +126,10 @@ jobs:
- name: check_results_layout.py --check (artefact policy)
working-directory: autolens_profiling
run: |
# results/notes/ holds ledgers only (logs -> results/logs/, sidecars
# The ledger trees hold ledgers only (logs -> results/misc/logs/, sidecars
# beside their result JSON), and every device-recording result JSON
# carries device.provenance unless grandfathered. Pure stdlib.
# carries device.provenance unless grandfathered (results/ and archive/).
# Pure stdlib.
python scripts/misc/tooling/check_results_layout.py --check

- name: wall check_submits.py --check (per-cell --time basis)
Expand All @@ -144,7 +145,7 @@ jobs:
working-directory: autolens_profiling
run: |
# Every timing assertion / gate constant the AST lister finds must
# appear in results/notes/timing_noise_audit_2026_10.md (#362), so
# appear in wiki/campaigns/misc/timing_noise_audit_2026_10.md (#362), so
# a new timing gate is inventoried with its noise model in the PR
# that adds it. Pure stdlib, read-only, deterministic (~3 s).
python scripts/misc/tooling/list_timing_assertions.py --check
Expand Down
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ running a full profile). None of these produce result artifacts.

**Timing assertions and gates.** Every place a measured time is compared to a cutoff (CI tests,
production qualification, campaign go/no-go rules) is inventoried with its noise model and verdict
in `results/notes/timing_noise_audit_2026_10.md` (#362). `lint.yml` runs
in `wiki/campaigns/misc/timing_noise_audit_2026_10.md` (#362). `lint.yml` runs
`scripts/misc/tooling/list_timing_assertions.py --check`, so a new timing gate needs a row there.
Never raise a timing budget to get CI green; an unresolved measurement is INCONCLUSIVE, never a pass.

Expand Down
46 changes: 23 additions & 23 deletions README.md

Large diffs are not rendered by default.

103 changes: 100 additions & 3 deletions _profile_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -733,11 +733,92 @@ def _nproc(*args: str) -> int | None:
return info


#: Legacy result folders a writer may still name; ``resolve_output_paths`` places their rows
#: in the mirrored tree (autolens_profiling#442 PR 2).
_LEGACY_RESULT_DIRS = (("results", "runtime"), ("results", "breakdown"))
_REPO_ROOT = Path(__file__).resolve().parent


def _setup_hints(cli: ProfileCLI, default_basename: str, hints: dict | None) -> dict:
"""Setup facts known before the result JSON exists, in the JSON's own keys."""
import socket
import sys

sys.path.insert(0, str(_REPO_ROOT / "scripts" / "misc" / "tooling"))
from build_migration_map import INSTRUMENTS

instrument = cli.instrument
if instrument is None:
tokens = f"_{default_basename}_"
instrument = next((i for i in INSTRUMENTS if f"_{i}_" in tokens), None)
backend = "cpu"
if "jax" in sys.modules:
backend = sys.modules["jax"].default_backend()
doc = {"use_mixed_precision": bool(cli.use_mixed_precision)}
if cli.config_name is None:
# A config name carries its host tier; an unnamed run is placed by the host it runs on.
doc["device"] = {"backend": backend, "provenance": {"host": socket.gethostname()}}
if instrument:
doc["instrument"] = instrument
doc.update(hints or {})
return doc


def _in_legacy_results(path: Path) -> bool:
try:
rel = Path(path).resolve().relative_to(_REPO_ROOT)
except ValueError:
return False
return rel.parts[:2] in _LEGACY_RESULT_DIRS


def place_output_path(
json_path: Path, cli: ProfileCLI, default_basename: str, hints: dict | None = None
) -> Path:
"""``mirrored_output_path`` with the setup facts the CLI knows (see ``resolve_output_paths``).

For writers that finish a legacy name themselves (suffixes appended after
``resolve_output_paths(..., place=False)``) and then place it.
"""
if not _in_legacy_results(json_path):
return Path(json_path)
placed = mirrored_output_path(json_path, _setup_hints(cli, default_basename, hints))
placed.parent.mkdir(parents=True, exist_ok=True)
return placed


def mirrored_output_path(legacy_path: Path, doc: dict) -> Path:
"""Where a result a writer would have put at ``legacy_path`` lives in the mirrored tree.

``legacy_path`` under ``results/runtime/`` or ``results/breakdown/`` is placed by the
day-zero migration map's own classification (``scripts/misc/tooling/
build_migration_map.destination``): ``results/<dataset_type>/<variant>/<instrument>/
<setup_id>/<host_class>[/<model_variant>]/<measurement>.json`` for a canonical row,
``.../scans|experiments/<name>/`` for a scan or arm, ``results/misc/<group>/`` when no
cell is derivable. Any other path is returned unchanged.
"""
import sys

legacy_path = Path(legacy_path)
if not _in_legacy_results(legacy_path):
return legacy_path
rel = legacy_path.resolve().relative_to(_REPO_ROOT)
sys.path.insert(0, str(_REPO_ROOT / "scripts" / "misc" / "tooling"))
from build_migration_map import destination

new, kind = destination(_REPO_ROOT, rel.as_posix(), doc)
if kind == "misc":
print(f"[results] no derivable cell for {rel.as_posix()}; writing {new}")
return _REPO_ROOT / new


def resolve_output_paths(
cli: ProfileCLI,
default_dir: Path,
default_basename: str,
cell: str | None = None,
hints: dict | None = None,
place: bool = True,
) -> tuple[Path, Path]:
"""Resolve (json_path, png_path) for the per-cell write.

Expand All @@ -759,6 +840,17 @@ def resolve_output_paths(
Delaunay cell's ``delaunay_<config_name>.json`` (autolens_profiling#219).
Callers that pass nothing keep the pre-existing behaviour exactly.
- ``cli.output_dir`` overrides ``default_dir`` when set.
- A ``default_dir`` under ``results/runtime/`` or ``results/breakdown/`` (the pre-#442
layout) is not written: the row is placed in the mirrored tree by
:func:`mirrored_output_path` from the legacy name plus the setup facts known now
(``--instrument`` or the instrument token of ``default_basename``,
``--use-mixed-precision``, the host and JAX backend). ``hints`` adds JSON-keyed facts
the CLI does not carry (e.g. ``{"transformer": "TransformerNUFFT"}``). An explicit
``--output-dir`` is honoured verbatim: ``sweep.py`` -> ``aggregate.py`` and the HPC
submits still name legacy cell folders whose config-grammar file names they parse,
until the catalogue builder reads the mirrored tree (#442 PR 3). ``place=False``
returns the legacy name for a caller that appends to it and then calls
:func:`place_output_path`.
- When ``cli.use_sparse_operator`` is set, ``_sparse`` is appended to the
resolved basename so dense and sparse JSONs from the same config don't
clobber each other.
Expand All @@ -769,7 +861,6 @@ def resolve_output_paths(
the label already says the size.
"""
results_dir = cli.output_dir if cli.output_dir is not None else default_dir
results_dir.mkdir(parents=True, exist_ok=True)
if cli.config_name is None:
basename = default_basename
else:
Expand All @@ -787,7 +878,13 @@ def resolve_output_paths(
# Keep the two rectangular-mesh families' results disjoint; the
# bilinear default keeps the unsuffixed (pre-split) filenames.
basename = f"{basename}_rtu"
return results_dir / f"{basename}.json", results_dir / f"{basename}.png"
json_path = results_dir / f"{basename}.json"
if place and cli.output_dir is None and _in_legacy_results(json_path):
placed = place_output_path(json_path, cli, default_basename, hints)
return placed, placed.with_suffix(".png")
if place or not _in_legacy_results(json_path):
results_dir.mkdir(parents=True, exist_ok=True)
return json_path, results_dir / f"{basename}.png"


def auto_simulate_if_missing(
Expand Down Expand Up @@ -847,7 +944,7 @@ def check_pinned(got, expected, *, label: str, rtol: float = 1e-4):

Profiling runs **record and flag** drift; they never adjudicate library
correctness (that is autolens_workspace_test's remit — see
``results/notes/design_lock_in.md``). Returns ``None`` when ``got`` is
``wiki/campaigns/misc/design_lock_in.md``). Returns ``None`` when ``got`` is
within ``rtol`` of ``expected``; otherwise prints a loud warning and
returns a drift record for the result JSON, which PyAutoHeart's vitals
scan picks up. Never raises — a changed computation must not kill a
Expand Down
17 changes: 17 additions & 0 deletions archive/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# archive

Dead experiments and frozen evidence moved out of `results/` in the day-zero move
(autolens_profiling#442): dated-suffix snapshots, `_lever` and `_probe` experiments, the
pre-2026-09 series, host-less versioned summaries, `results/baselines/PreOptimizationTimes/`
and the frozen evidence packs (`clipper_campaign/`, `point_source_cpu_2026_09_17_reported/`).

Layout: `archive/YYYY/MM/<slug>/<path under results/ or inside the pack>`. `YYYY/MM` is the
file's own date (file name or JSON), else the latest date in its slug, else the date of a
ledger that cites it; `archive/undated/<slug>/` when no date is derivable. Every file's old
path, new path and sha256 is in `catalogue/migration_day_zero.json`; nothing here changed
bytes.

Archived rows are history: they are never a current measurement. The catalogue builder still
reads them (`catalogue/registry.json` source `archive/**/*.json`) so its output is unchanged
by the move; the `profiling-summary@3` builder counts them as `archive_record_count`
(#442 PR 3). Never write new results here.
2 changes: 1 addition & 1 deletion baseline/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ This is a **draft campaign specification**, not a baseline. Phase 6 prepares
collection and review; it authorizes no jobs and accepts no measurements.
Pulse owns the pending [campaign task](https://github.com/PyAutoLabs/PyAutoPulse/issues/17).
The project owns [campaign.json](campaign.json), its dry-run tooling and the
[campaign journal](../wiki/campaigns/setup_baseline.md).
[campaign journal](../wiki/campaigns/misc/setup_baseline.md).

## Inspect without running profiles

Expand Down
2 changes: 1 addition & 1 deletion baseline/campaign.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"status": "draft",
"purpose": "Proposed fresh setup baseline; no measurements or acceptance authorized.",
"inventory": {
"catalogue/registry.json": "c481d41abd51a7f4088d31fcd5c14d08a0fa9b7c99328f88abb7d138f3217c55",
"catalogue/registry.json": "967ce77cf2f6a59bcdb7cc881d886c49fe465d3fc11ad2a6e356e51cbd59e49d",
"catalogue/script_routes.json": "f9fea1de9fbc431cbd70bb36d90bd7a731d2cc5d21bcbd335cba3448068ee088"
},
"software": {
Expand Down
Loading
Loading