Generator contract, shared evaluation layer, and HF-hosted leaderboard - #75
Generator contract, shared evaluation layer, and HF-hosted leaderboard#75mkeeler43 wants to merge 20 commits into
Conversation
Mirrors engibench/problems/<name>/ so the two halves of the stack are laid out the same way. Pure move: file contents are unchanged apart from the import paths the move forces (engiopt.<model> -> engiopt.generators.<model>). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ackend
engiopt/core.py defines what a model must provide to be evaluated: a few
declarative attributes plus build() and _sample(). Mirrors engibench/core.py.
engiopt/utils/all_generators.py mirrors engibench/utils/all_problems.py.
checkpoint_store drops the W&B read-fallback and the write-only run-scoped
path, which no loader ever read. Checkpoints are now filed per hyperparameter
configuration at {problem}/cfg_{fingerprint}/seed_{n}, with the canonical
{problem}/seed_{n} claimed only by default-hyperparameter runs so a sweep
cannot redefine what a bare model name means. Evaluation metrics can be
attached to a package, keeping a checkpoint self-describing.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The 13 evaluate_*.py scripts were ~79% identical; they differed only in which network to import, how to build it, whether to pass conditions, and a model id string. Each model now supplies a ~45-line adapter and everything else is shared: - engiopt/evaluation/: a metric registry declaring each metric's family, cost, and ranking direction; an EvaluationContext that computes the expensive optimizer pass once for every performance metric; an Evaluator; and the leaderboard, published to HuggingFace by merging rather than overwriting. - engiopt/specs/<problem>/v1.json: frozen, hash-verified evaluation contracts so rows are comparable across people and across time. - python -m engiopt.evaluate replaces the deleted scripts. - engiopt/generators/_template/ is the copy-me starting point for new models. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Training scripts: - 10 of 14 only saved checkpoints when --track was on, so training without W&B silently produced nothing on HuggingFace. VQGAN additionally gated early stopping -- real training logic -- behind it, so under default settings its transformer checkpoint was never written at all. - Each run is now filed under its own hyperparameter configuration via checkpoint_identity(args). Evaluation correctness: - iog stored the raw simulated objective rather than a gap against the reference optimum, unlike cog and fog. An oracle generator returning the reference design now scores ~0 where it previously reported raw compliance. - Condition sampling could not serve photonics2d (whose conditions include solver settings absent from the dataset) or thermoelastic2d (whose boundary conditions are 65x65 matrices). Scalar conditions now feed the model tensor while the simulator still receives every condition. - Multi-objective gaps were summed or averaged, letting an objective a sample does not prioritize dominate its score. Scalarization is declared in the eval spec, and the evaluator refuses to guess when it is absent. - Gaps now respect ObjectiveDirection, so photonics2d -- which maximizes total_overlap -- is no longer ranked in reverse. metrics.metrics() and simulate_failure_ratio() are removed: both became dead once the evaluator landed, and a second evaluation path only invites drift. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two failures on the PR, both surfaced only by CI because it runs the whole repository with the latest ruff while local runs used 0.11. - surrogate_model/run_pe_optimization.py passed active_wandb_run to resolve_checkpoint_reference, which lost that parameter when W&B stopped being a checkpoint source. Caught by mypy; the loader now takes only a model reference. - ruff 0.16 formats Python inside Markdown fences, which reformatted the two new docs, and select = ["ALL"] means new rules enable themselves on every upgrade. PLR0917 is ignored for the same reason PLR0913 already is, and PLC0415 because lazy imports are used deliberately to keep module import cost down and avoid cycles. The stale PLR0913 noqa directives those rules made redundant are removed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
SoheylM
left a comment
There was a problem hiding this comment.
The generator interface and shared evaluator make sense to me, and removing the duplicated evaluation scripts is a good change. I am requesting changes because several parts of the documented evaluation flow do not work end to end yet. I have left the concrete code issues inline.
There are four cross-cutting points as well:
- Checkpoint migration: Issue #73 says to remove the migration playbook after the W&B migration is complete, but the issue is still open and this PR removes both the playbook and W&B loading. On 2026-08-03,
IDEALLab/engiopt-cgan-cnn-2dcontained only.gitattributesandREADME.md;IDEALLab/engiopt-vqganandIDEALLab/engiopt-gan-cnn-2ddid not exist. The README's cGAN evaluation command cannot currently loadbeams2d/seed_1. Please publish and verify the required HF packages before removing the old path, or keep the migration support until that is done. - Documentation: The README still describes separate training and evaluation scripts, says the power-electronics optimizer accepts legacy W&B artifacts, and links to the deleted migration playbook.
run_pe_optimization.pystill uses W&B references in its example and argument descriptions, whileGenerator.resolve_checkpointand the evaluation CLI help still listwandbas a model source. These should match the final migration decision. - Lint CI: With Ruff 0.15.11,
ruff check .reports 20I001errors on this commit. The GitHub job is green because it runsruff check --fixin the temporary checkout without checking whether files changed. Please commit the import fixes and run plainruff check .in CI. Pinning Ruff would also make the check stable. - Workshop integration: The open DCC workshop branch still imports
engiopt.cgan_cnn_2d...innotebook_helpers.py, which is used by the participant notebooks. Please update that branch before these changes reach participants, or keep a temporary import alias if the old path must remain supported.
Verification on the PR head: 76 tests passed with 3 warnings; two of four committed specs failed to load; the thermoelastic adapter shape error and mixed-ranking result were reproduced; the documented cGAN evaluation loaded no model and exited with status 0; git diff --check passed.
| conditions_tensor, conditions, ref_designs, indices = sample_conditions( | ||
| problem=problem, n_samples=self.n_samples, device=device, seed=self.condition_seed | ||
| ) | ||
| digest = _digest(indices, ref_designs) |
There was a problem hiding this comment.
I ran Evaluator.for_problem for all four committed specs using the current EngiBench 0.2.0 package. beams2d/v1 and heatconduction2d/v1 load, but photonics2d/v1 fails with digest ade3d18084b31b5e expected and 1f1bbaa4003ded80 found. thermoelastic2d/v1 fails with 3a1d2ae6c45add7f expected and cb1c6e77cbec5883 found. Both JSON files also record EngiBench 0.2.0, so the package version does not identify the exact dataset used to create them. The digest detects the change but cannot recover the original evaluation rows. Could we pin and load a dataset revision in the spec, or store the selected conditions and reference designs with it? I would also add a CI test that resolves every committed spec.
There was a problem hiding this comment.
Fixed — and chasing your repro turned up the actual cause, which is not what either of
us assumed.
It was never upstream dataset drift. The two specs that failed for you failed because
they were frozen against EngiBench main, not the 0.2.0 release you had installed. Those
two problems point at different datasets in the two versions:
| problem | EngiBench 0.2.0 (PyPI) | EngiBench main | your result |
|---|---|---|---|
| beams2d | beams_2d_50_100_v0 |
same | resolved |
| heatconduction2d | heat_conduction_2d_v0 |
same | resolved |
| photonics2d | photonics_2d_120_120_**v0** |
..._**v1** |
digest mismatch |
| thermoelastic2d | thermoelastic_2d_**v0** |
..._**v1** |
digest mismatch |
So you were sampling a different dataset entirely, and the digest correctly refused. Your
underlying point stands exactly as written — the recorded EngiBench version did not
identify the data — it was just more true than it looked: the problem definition is
what selects the dataset, and a version string pins neither.
Three changes, smallest first:
- Specs pin the dataset.
dataset_id+dataset_revision, recorded at freeze time,
andresolveloads that exact revision. A later upload to the dataset repo can no
longer change what a published row means. - Specs record the problem definition they were frozen against (
problem_conditions,
plusengibench_versionnow carrying the git sha).resolvechecks this before the
digest, so the case you hit reports itself:rather than two hashes that will never match.Eval spec thermoelastic2d/v1 was frozen against a different definition of 'thermoelastic2d'. conditions expected: ['fixed_elements', ..., 'volume_fraction_target', 'rmin', 'weight'] conditions found: ['fixed_elements', ..., 'volfrac', 'rmin', 'weight'] dataset expected: IDEALLab/thermoelastic_2d_v1 dataset found: IDEALLab/thermoelastic_2d_v0 - CI installs EngiBench from git at a pinned commit, since no PyPI release has the v1
problems yet, and the README now says evaluation needs EngiBench from source. This is
the part worth your attention — it is a real constraint on anyone running the specs,
and it should probably relax to a version pin once EngiBench releases.
All four specs are re-frozen and now resolve; the two that failed for you resolve. Nothing
had been published to a leaderboard, so re-freezing cost no history.
CI test added as you asked, plus offline checks so it fails fast without the network:
test_committed_spec_reproduces_its_frozen_conditions— resolves every committed spec
(@pytest.mark.network, parametrised overengiopt/specs/*/*.json, so a new spec is
covered the moment it is added)test_committed_spec_pins_its_dataset— a spec without a pinned revision fails reviewtest_a_differently_defined_problem_reports_itself— the message abovetest_committed_spec_loads_and_round_trips,..._requests_registered_metrics
The repo had no pytest job at all — only ruff and mypy — so I added one.
| return self.spec.n_samples | ||
|
|
||
|
|
||
| def _digest(indices: npt.NDArray[Any], ref_designs: npt.NDArray[Any]) -> str: |
There was a problem hiding this comment.
The field documentation says this digest covers the sampled conditions and reference designs, but _digest only receives the selected indices and reference designs. A condition value can therefore change without changing the digest. Please include the condition names and values in the hash, using a stable representation, in both freeze and resolve.
There was a problem hiding this comment.
Fixed. _digest now takes the sampled conditions dataset and hashes, in sorted column
order, each condition's name and its values, alongside the indices and reference designs.
Numeric columns are rounded to 8 decimals before hashing so float noise well below any
tolerance the metrics use can't invalidate a spec; array-valued columns
(thermoelastic2d's boundary matrices) are covered the same way, and non-numeric columns
fall back to a sorted JSON form. freeze and resolve share the one function, so they
can't drift.
Four tests pin the behaviour you were protecting: changing a condition value, renaming a
column, and editing an array-valued condition each change the digest; reordering columns
does not.
| @property | ||
| def n_conds(self) -> int: | ||
| """Number of scalar conditioning variables for this problem.""" | ||
| return len(self.problem.conditions_keys) |
There was a problem hiding this comment.
n_conds is described as the number of scalar conditions, but this returns all condition keys. For the current thermoelastic2d problem, the evaluator produces 3 scalar columns (volfrac, rmin, and weight) while the problem has 7 condition keys because four boundary conditions are matrices. cgan_cnn_2d builds for 7 inputs and then fails with shape '[2, 7, 1, 1]' is invalid for input of size 6. Other conditional adapters and training scripts use the same all-keys assumption, and VQGAN tries to stack the scalar and matrix columns. Please use the actual scalar condition keys, or save the expected condition schema with the checkpoint, throughout training, loading, and sampling. An adapter test using the thermoelastic condition schema would cover this.
There was a problem hiding this comment.
This was the real bug in the PR — thank you, the thermoelastic2d repro was exactly right.
Both halves of your suggestion are now in, because they solve different failures:
One definition of the scalar schema. New engiopt.transforms.condition_keys(problem)
is the single answer to "which conditions does a model consume", and training scripts,
dataloaders, adapters, and sampling all use it. Generator.n_conds is now
len(self.condition_keys), not len(problem.conditions_keys). Nine training scripts and
seven adapters updated; vqgan's stacking of scalar and matrix columns is fixed by the
same change.
The schema travels with the checkpoint. save_checkpoint_package(..., condition_keys=...)
records it in metadata.json, and condition_keys_for(problem, resolved) prefers the
recorded schema over the problem's current one — so a checkpoint keeps loading after a
problem gains a condition. Checkpoints written before this fall back to the problem's
current scalar keys, which is what those runs actually used.
I also made the mismatch loud rather than latent: if the conditions handed to sample
don't match the columns the checkpoint was trained on, it raises naming both, instead of
failing with a reshape error deep inside the network — or worse, silently conditioning on
the wrong numbers.
Your repro, before and after:
before: RuntimeError: shape '[2, 7, 1, 1]' is invalid for input of size 6
after: problem.conditions_keys: 7 -> scalar: ('volume_fraction_target', 'rmin', 'weight')
condition tensor: (50, 3) sampled: (50, 64, 64) n_conds: 3
Tests in tests/test_condition_schema.py use the thermoelastic schema as you suggested:
7 declared conditions, 3 columns, sampling succeeds, mismatched schemas rejected, recorded
schema preferred over the problem's.
| raise ValueError(f"Metric {metric!r} has no ranking direction; it is diagnostic only.") | ||
|
|
||
| grouped = ( | ||
| frame.groupby(["problem_id", "algo_id"], as_index=False) |
There was a problem hiding this comment.
ROW_KEY includes config_fingerprint and spec_version, but rank groups only by problem_id and algo_id. This combines different hyperparameters and evaluation protocols. For example, a cfg_a/v1 score of 0.1 and a cfg_b/v2 score of 0.9 are reported as one score of 0.5 with n_seeds=2, even when both rows use seed 1. Please require one config/spec before ranking, or include config_fingerprint and spec_version in the grouping. disagreement should follow the same rule.
There was a problem hiding this comment.
Fixed by including both columns, which is the option that keeps a sweep rankable.
New ENTRY_KEY = [problem_id, algo_id, config_fingerprint, spec_version] — ROW_KEY
minus seed, since seeds are what a ranking aggregates over and nothing else should be.
rank groups by it, and disagreement uses the same helper, so the two can't diverge.
Your example is now two rows with n_seeds=1 each rather than one averaged 0.5.
entry_key(frame) raises if none of those columns are present, rather than silently
pooling the whole table into one number.
|
|
||
| try: | ||
| path = hf_hub_download(repo_id=repo_id, filename=LEADERBOARD_FILE, repo_type="dataset", token=token) | ||
| except Exception: # noqa: BLE001 - a missing or private board is not an error here |
There was a problem hiding this comment.
This catches every exception raised by hf_hub_download, including authentication, network, and server errors, and returns an empty frame. push_to_hub can then upload only the new rows and remove the rows already on the Hub. Please catch only the specific repository/file-not-found cases and let other download errors stop the update. A test for a transient download failure would be useful here.
There was a problem hiding this comment.
Fixed. load_from_hub now catches only EntryNotFoundError, RepositoryNotFoundError,
and RevisionNotFoundError — the genuine "nothing published yet" cases. Auth, network,
and server errors propagate, so a transient failure can no longer be laundered into an
empty board that the next push turns into a deletion.
Tests in tests/test_leaderboard_publishing.py cover both directions: a 503 and a 401 each
raise; a missing repo and a missing file each read as empty.
| ] | ||
| print(f"{len(generators)} generator(s) left after skipping already-published rows.") | ||
| if not generators: | ||
| print("No generators loaded; nothing to evaluate.") |
There was a problem hiding this comment.
With the documented cgan_cnn_2d, beams2d, seed 1 command, checkpoint loading fails, this message is printed, and the process exits with status 0. That makes an empty batch run look successful. Please return a non-zero status when none of the explicitly requested models can be loaded or evaluated, while keeping the current skip behavior when only part of a multi-model run fails.
There was a problem hiding this comment.
Fixed. main returns a status and __main__ exits with it:
- nothing could be loaded → 1
- every generator failed to evaluate → 1
- some loaded, some failed → 0 (your "keep the current skip behaviour" case)
- everything skipped by
--skip-existing→ 0, since that's a finished job, not a failed one
The documented command you ran now prints the same message and exits 1.
| start = time.perf_counter() | ||
| with th.no_grad(): | ||
| raw = self._sample(batch, n) | ||
| self.last_sample_seconds = time.perf_counter() - start |
There was a problem hiding this comment.
The timer stops before raw.detach().cpu().numpy(). CUDA and MPS operations can still be pending at this point, so sample_seconds can under-report the actual sampling time. Please synchronize the active device before stopping the timer, or stop after the CPU transfer and state that the transfer is included.
There was a problem hiding this comment.
Fixed by synchronising, which keeps sample_seconds measuring sampling rather than the
CPU transfer. Generator._synchronize_device calls th.cuda.synchronize(self.device) or
th.mps.synchronize() as appropriate and is a no-op on CPU; sample calls it before
stopping the timer. Overridable, and there's a test asserting sample waits.
| results.fog.append(self.scalarize_gap(gaps[-1], i)) | ||
|
|
||
| if conditions: | ||
| target_vol = conditions.get("volfrac") or conditions.get("volume") |
There was a problem hiding this comment.
Every committed spec requests viol, but this only calculates it when a condition named volfrac or volume exists. photonics2d has neither, so after its digest issue is fixed this metric will return NaN. The a or b lookup also ignores a valid target of 0.0. Please define feasibility for each spec, use problem.check_constraints where appropriate, or remove viol from specs where it has no meaning.
There was a problem hiding this comment.
Fixed, taking your check_constraints suggestion as the base definition so the metric is
defined for every problem rather than removed from some specs:
problem.check_constraintsalways applies — EngiBench's own contract, which a new
problem gets for free by declaring constraints- plus the volume budget named by the spec's new
volume_condition, for problems that
have one
Specs now declare it explicitly instead of the code guessing: volfrac (beams2d),
volume (heatconduction2d), volume_fraction_target (thermoelastic2d), none (photonics2d).
Worth noting the old auto-detect never matched thermoelastic2d either — it looked for
volfrac/volume and the key is volume_fraction_target — so viol was silently absent
there too. The a or b lookup is gone; an explicit is not None test means a target of
0.0 counts.
Implementing this surfaced two further problems worth flagging, both fixed and tested:
check_constraintsraised on thermoelastic2d. The HF dataset returns its boundary
matrices as nested lists and EngiBench's bound check doeslower <= value, giving
TypeError: '<=' not supported between 'float' and 'list'. We coerce list-valued
conditions to arrays before the call. This may be worth a fix upstream in EngiBench too.- Dtype alone made designs infeasible. thermoelastic2d's design space is float32 while
its designs are float64, andBox.containsrejects an uncastable dtype outright — so
every design, including the dataset's own optima, scored as violating. We cast to the
space's dtype first, which makes the check about the values.
Verified on all four problems: reference designs feasible, out-of-bounds designs not.
| @@ -99,41 +120,50 @@ def save_checkpoint_package( # noqa: PLR0913 | |||
| metadata_payload["hf_repo_id"] = repo_id | |||
| metadata_payload["hf_package_path"] = package_path | |||
There was a problem hiding this comment.
For a non-default config, the config-specific package is uploaded while this still points to {problem}/seed_n. That call does not upload the canonical path, and only the returned info value is cleared after the config metadata has already been written. The resulting metadata.json can therefore point to a canonical package that this run did not create. Please build metadata separately for the config-specific and canonical uploads.
There was a problem hiding this comment.
Fixed as you described — metadata is now built per upload. A shared base_metadata carries
the run-level facts, and each _upload_package_to_hf call gets {**base_metadata, "hf_package_path": <the path being written>}. The returned info records only paths this
run actually claimed.
Two tests: a non-default run writes one package whose metadata names its own config path
(and info["hf_package_path"] is None), and a default run writes two packages that each
describe themselves.
| method: grid | ||
| command: | ||
| - "python" | ||
| - "engiopt/cgan_cnn_2d/cgan_cnn_2d.py" |
There was a problem hiding this comment.
The beams, heat-conduction, and photonics sweep files still run engiopt/cgan_cnn_2d/cgan_cnn_2d.py, which no longer exists after this move. Please change all three to engiopt/generators/cgan_cnn_2d/cgan_cnn_2d.py and use the current --save-model option.
There was a problem hiding this comment.
Resolved by removing the files. These three were never meant to be committed — they were
local sweep configs that got swept into the file-move commit. Removed here, along with the
two pre-existing sweep YAMLs and cgan_vae/sample_script.ipynb that this PR's reorg moved.
The two affected READMEs referenced the deleted evaluate_*.py scripts as well, so they now
point at python -m engiopt.evaluate.
…d integrity Resolves the thirteen review comments on the generator-contract PR. Three were correctness bugs; the rest close gaps that would have corrupted a shared leaderboard once more than one person wrote to it. Condition schema. `n_conds` counted every entry of `problem.conditions_keys`, including the array-valued and solver-only ones that never reach a dense condition tensor. A cGAN built for thermoelastic2d's seven declared conditions received three columns and failed on reshape. `transforms.condition_keys` is now the single definition of what a model consumes, used by training, loading, and sampling alike, and each checkpoint records the schema it was trained under so a model keeps loading after a problem gains a condition. Handing a model conditions from a different schema now raises rather than silently conditioning on the wrong numbers. Eval specs. The digest covered only the drawn indices, so an upstream edit to a condition value changed every score without tripping the check; it now covers the condition names and values too. Specs also record and load a pinned dataset revision, since an EngiBench version does not identify a dataset. All four specs are re-frozen; photonics2d and thermoelastic2d, which no longer resolved, now do. Feasibility. `viol` only existed for problems with a `volfrac` or `volume` condition, returned NaN for photonics2d, and skipped a legitimate target of 0.0. It is now `problem.check_constraints` plus a volume budget the spec names explicitly. Two further defects surfaced while implementing it: `check_constraints` raised on thermoelastic2d, whose boundary matrices arrive from HuggingFace as lists, and float64 designs were judged infeasible against a float32 design space on dtype alone -- which marked that problem's own dataset optima as violations. Leaderboard integrity. Ranking pooled different hyperparameter configurations and different spec versions into one score; it now groups by both. A failed download read as an empty board, so a transient error could turn the next publish into a deletion; only genuine not-found cases are caught now. Publishing is conditional on the revision it read and re-merges on conflict, rather than overwriting a concurrent publisher. Rows carry the checkpoint revision, a hash of the weights themselves, and the code version, so a score is traceable to the model that earned it. Usability. `local_model_dir` reaches `from_pretrained` and the CLI, so the advertised local source is usable. The CLI exits non-zero when nothing could be evaluated, instead of reporting an empty batch as success. Sample timing synchronises the device before stopping the clock. Each uploaded checkpoint package carries metadata describing itself rather than a path the run may not have written. Testing. The repository ran only ruff and mypy, so a pytest job is added along with tests for the condition schema, publishing races, local package loading, and a network-marked check that every committed spec still resolves against its pinned dataset. Also removes the sweep configs and the per-algorithm notebook that should not have been committed, and two dead helpers this branch had added. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Follow-up to the review-response commit, addressing what CI surfaced. The two specs Soheyl reported as broken were never a case of the dataset drifting upstream. They were frozen against EngiBench main, whose photonics2d and thermoelastic2d read the v1 datasets, while the 0.2.0 release on PyPI still points those two at v0. Sampling a different dataset is why the digests could not match, and why CI could not reproduce them either. So the specs now record the problem definition they were frozen against, not just the dataset: the full condition list plus the EngiBench commit. That is checked before the digest, which turns the failure from two hashes that will never agree into a message naming the conditions and datasets that differ. CI installs EngiBench from git at a pinned commit, since no release carries the v1 problems yet, and the README says so for anyone running an evaluation. Two of my own breaks are fixed here too. A Python block inside CONTRIBUTING_A_MODEL.md was misformatted, which the newer ruff in CI checks and my older local one does not. And the leaderboard publishing tests constructed huggingface_hub errors in a way that only works on older releases, where `response` is optional and unread; they now build a real requests.Response, which every version accepts. Verified against hub 1.26.0 as well as 0.34.4. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Thanks for this — the review caught one genuine correctness bug (the condition count) and Fixes for your commentsCorrectness
Leaderboard integrity
Usability
Changes beyond your comments, and whyFour things are in this branch that you did not ask for. Flagging them so nothing in the
Known limitation, not addressed herethermoelastic2d models are conditioned on 3 scalars while the simulator receives all 7 |
SoheylM
left a comment
There was a problem hiding this comment.
Thank you for the detailed follow-up. Several original issues are fixed, including local loading, exit status, timing, checkpoint metadata, and grouping configurations into separate entries. The remaining inline findings still affect reproducibility or leaderboard correctness, so I am keeping Changes Requested.
The migration and repository cleanup also remain unfinished. Issue #73 is open, the public HF repositories still do not provide the documented checkpoint packages, and the README links to the deleted migration playbook while retaining obsolete W&B source instructions. ruff check . reports 20 import-order errors; CI stays green because it runs with --fix. The DCC workshop branch also still imports the old pre-move module path.
Verification on 810529f: the local suite reports 134 passed and 2 skipped with PyPI EngiBench; all four spec-resolution tests pass when the actual pinned EngiBench source is imported; git diff --check passes; the worktree is clean.
| try: | ||
| spec.check_problem_definition(problem) | ||
| except ProblemDefinitionMismatchError as mismatch: | ||
| pytest.skip(str(mismatch)) |
There was a problem hiding this comment.
CI is green because this skips the two cases the new job is supposed to verify. In run 31012275781, photonics2d/v1 and thermoelastic2d/v1 were skipped, leaving 134 passed and 2 skipped. The git install does not replace the PyPI copy installed earlier because both report version 0.2.0. I ran these four tests with EngiBench source at 0a028c03..., and all four pass. Please force-install that revision and let ProblemDefinitionMismatchError fail this CI test; otherwise the pin can break while CI remains green.
There was a problem hiding this comment.
You are right, and this is the one that mattered. I reported CI as green; it was
green on a false basis. Thank you for checking the run rather than the tick.
Diagnosis matches yours exactly: the git build reports version 0.2.0, the same as the
PyPI build, so pip install "engibench @ git+..." saw the requirement already satisfied
and did nothing. The specs were then verified against the EngiBench that cannot resolve
them, and the skip hid it.
Two changes:
- run: pip install --force-reinstall --no-deps "engibench @ git+...@0a028c03d02b..."
- run: python -c "import engibench.problems.thermoelastic2d.v0 as m; assert m.ThermoElastic2D.dataset_id.endswith('_v1'), m.ThermoElastic2D.dataset_id"--force-reinstall is the fix; the assertion is there so that if this ever silently
reverts, the job fails at the install step rather than in a way that looks like a passing
test run.
And the skip is gone — ProblemDefinitionMismatchError now fails the test, as you asked.
Your reasoning is the deciding one: a mismatch means the pin stopped taking effect, which
is precisely the condition this job exists to catch, so it must never be the quiet
outcome. That also deleted the try/except rather than adding to it.
| ValueError: If the caller's condition columns differ from the | ||
| checkpoint's. | ||
| """ | ||
| if keys and self.condition_keys and tuple(keys) != self.condition_keys: |
There was a problem hiding this comment.
A recorded checkpoint schema cannot currently be a subset of the evaluator scalar keys. VQGAN defaults to dropping constant conditions and records the reduced schema, but the evaluator supplies all scalar keys, so this equality check raises before VQGAN can preprocess them. The same happens after a problem gains a scalar condition, despite the compatibility claim above. If the checkpoint schema is authoritative, please project and reorder a supplied superset to those keys, rejecting only missing required keys.
There was a problem hiding this comment.
Right, and this was a bug I introduced last round: the strict equality check rejected
VQGAN before it could run at all.
Implemented as you described. _select_trained_columns projects a supplied superset onto
the checkpoint's recorded keys, in the checkpoint's order, and raises only when a key the
model needs is genuinely absent:
volfrac, rmin, forcedist (evaluator) -> volfrac (checkpoint recorded) projected
volfrac, rmin (evaluator) -> volfrac, brand_new raises
volfrac, rmin (evaluator) -> rmin, volfrac reordered
This is better than my original in a way worth naming: the checkpoint's schema is now
authoritative in fact, not just in the docstring. It also covers your "after a problem
gains a scalar condition" case, which my version claimed to support and did not.
Tests in tests/test_condition_schema.py cover projection, reordering, and the
missing-key error.
| dataset = conditions.dataset | ||
| if dataset is None: | ||
| return conditions.require_tensor(self.algo_id) | ||
| columns = dataset.column_names |
There was a problem hiding this comment.
This still begins with every condition column. For thermoelastic data that mixes scalars with 65x65 matrices, so drop_constant or the final stack raises the same mixed-shape error. It also re-fits constant removal and normalization on the evaluation sample, whereas training fitted them on the training split. Please save the selected keys and normalization statistics with the checkpoint and apply exactly those here. A VQGAN-specific mixed-condition test would cover this.
There was a problem hiding this comment.
Agreed on all three points, and this was the most serious of the round: the discarded
_mean, _std meant the network received conditions on a different scale than it was
trained on, silently.
Rather than fix the adapter, the contract now does the job for every model. The principle:
a model that preprocesses its conditions records how, and the contract replays it.
- Training records what it fitted:
save_checkpoint_package(..., condition_keys=..., condition_stats={"mean": [...], "std": [...]}). VQGAN passes the statisticsnormalize
returned instead of throwing them away. Generator.sampleprojects to the recorded keys (see C) and applies the recorded
scaling, so_samplereceives the conditions in exactly the form training used.VQGANGenerator._condition_tensoris deleted. The adapter now uses
conditions.require_tensor(...)like every other model.
That removes the mixed-shape crash too: the projection works on the scalar condition
tensor and never touches the 65x65 matrices, so thermoelastic no longer reaches
drop_constant.
The general mechanism was deliberate — we expect more two-stage models, and they should
get this pathway without writing code for it.
Tests: recorded statistics are replayed rather than refitted (with the evaluation sample's
own mean chosen to differ, so a regression fails), and a model with no recorded statistics
is left untouched.
| algo_id = "vqgan" | ||
| conditional = True | ||
| design_kinds = ("2d",) | ||
| checkpoint_files = ("vqgan.pth", "transformer.pth") |
There was a problem hiding this comment.
Conditional VQGAN loading requires and uses cvqgan.pth, but it is absent from checkpoint_files. The resolver content hash therefore covers only vqgan.pth and transformer.pth: two packages with different condition encoders receive the same checkpoint_hash even though they generate differently. Please include every weight file used by build in the provenance hash and test that changing cvqgan.pth changes it.
There was a problem hiding this comment.
Right. Fixed more generally than by extending checkpoint_files, because cvqgan.pth
cannot go in that list — it is required for conditional runs and absent for unconditional
ones, so making it mandatory would break the unconditional variant.
hash_package_contents(root_dir) now hashes every file in the package except
run_config.json and metadata.json, rather than the declared list. Any weight file a
model loads is covered whether or not it declared it, which is the property you actually
want and which generalizes past VQGAN.
metadata.json is excluded deliberately: it records the package path and revision, which
differ between the two locations a single run writes, so including it would give identical
weights two different hashes.
Two tests: changing cvqgan.pth changes the hash; changing metadata.json does not.
| repo_id=repo_id, filename=LEADERBOARD_FILE, repo_type="dataset", revision=revision, token=token | ||
| ) | ||
| # Only "there is nothing published yet" is a normal, empty-board outcome. | ||
| except (EntryNotFoundError, RepositoryNotFoundError, RevisionNotFoundError): |
There was a problem hiding this comment.
These exceptions need separate handling. Hugging Face can raise RepositoryNotFoundError for a private repository without access, including 401/403 responses, so authentication can still become an empty board. When only leaderboard.csv is missing, repo_info has already returned a revision, but this discards it; I reproduced the first upload using parent_commit=None, leaving two first publishers able to overwrite one another. Please treat only a genuine 404 repository as (empty, None), return (empty, revision) for a missing file, and propagate authorization and revision errors.
There was a problem hiding this comment.
Both correct, and the second is the more dangerous one — thank you for reproducing it.
Split into two steps, as you described:
repo_infofailing withRepositoryNotFoundErroris only an empty board when it is a
genuine 404. A 401/403 re-raises, since a repo you cannot see is not a repo with no rows.- A missing
leaderboard.csvin an existing repo now returns(empty, revision). The
revision becomes theparent_commitof the first publish, so two jobs racing to publish
first can no longer overwrite one another.
Tests cover both: a private repo raises, and the first publish into an existing empty repo
carries its revision as parent_commit.
| .sort_values("value", ascending=not spec.higher_is_better) | ||
| .reset_index(drop=True) | ||
| ) | ||
| grouped["rank"] = grouped.index + 1 |
There was a problem hiding this comment.
ENTRY_KEY prevents different configurations and specs from being averaged together, but ranks are still assigned globally across the entire frame. Different problems and spec versions therefore compete with each other; I reproduced one problem receiving ranks 1/2 and another receiving 3/4. Please rank independently within (problem_id, spec_version), with each partition starting at 1. disagreement should use the same partitioning.
There was a problem hiding this comment.
Right — I fixed the grouping and left the ranking global, which was half a fix.
RANK_PARTITION = ["problem_id", "spec_version"], and ranks restart at 1 within each
partition via groupby(partition).cumcount() + 1. disagreement calls rank, so it
follows automatically.
Two tests: two problems each get ranks 1/2 rather than 1/2/3/4, and two spec versions of
one problem each get rank 1.
| remaining = [ | ||
| generator | ||
| for generator in generators | ||
| if not already_evaluated( |
There was a problem hiding this comment.
--skip-existing does not compare checkpoint_hash. If the same configuration and seed are retrained and the canonical package now contains different weights, this skips evaluation because the old row matches these five fields. Please include the loaded checkpoint hash when the board supports it, and add a test showing that a new hash is not skipped.
There was a problem hiding this comment.
Agreed. already_evaluated now takes checkpoint_hash, and --skip-existing passes the
loaded generator's.
One deliberate wrinkle: a row written before the hash column existed has nothing to
compare, so it is treated as a match and still skipped. Forcing a re-run of every
historical row on the first run after this lands would be a surprising cost for no
information.
Three tests: a new hash is not skipped, the same hash is, and a hashless row is.
| design = self.design_for_solver(i) | ||
|
|
||
| self.problem.reset() | ||
| _, opt_history = self.problem.optimize(design, config=conditions) |
There was a problem hiding this comment.
viol cannot reliably report an infeasible design because feasibility is checked only after optimization and simulation. If the optimizer rejects an invalid starting point, evaluation raises before is_infeasible runs; I reproduced that behavior. Requesting only viol also performs the full optimizer pass. Please compute feasibility independently before optimization and keep only the gap metrics in the shared optimization result.
There was a problem hiding this comment.
Good catch, and fixing it made things smaller rather than larger.
Feasibility left OptimizationResults entirely and is now EvaluationContext.feasibility,
computed by a constraint check over the generated designs before any solver runs. Both
consequences you named are gone: an optimizer that refuses an invalid starting point no
longer takes the metric down with it, and requesting viol alone no longer runs a full
optimization pass.
viol is therefore now registered as cheap, which also means it appears in the default
non---include-expensive run — a better default, since feasibility is exactly what you
want to see before spending simulator time.
Two tests: feasibility still reports when optimize raises, and computing viol leaves
optimize_calls == 0.
…time conditions Nine follow-up comments, eight of them correct and four describing defects the previous round introduced. The important one: CI was green because it skipped the two specs the job exists to check. The pinned EngiBench install was a no-op -- the git build reports the same version string as the PyPI one, so pip considered the requirement already satisfied and kept the wrong copy. It is now a --force-reinstall with an explicit assertion that the pin took effect, and a spec frozen against a different EngiBench definition fails rather than skipping, since skipping would go green exactly when the pin has broken. `freeze()` also captures the EngiBench source revision itself, so it reproduces the values already committed in the specs instead of relying on how they were written by hand. Conditions are now presented to a model the way its training data looked. The evaluator supplies every scalar condition, unscaled; a model that trained on a subset, a different order, or a rescaling gets that view reconstructed from what its checkpoint recorded. Previously the contract demanded exact equality, which rejected VQGAN outright -- it drops constant columns during training -- and VQGAN worked around it by re-deriving its preprocessing from the evaluation sample. That was silently wrong: fifty evaluation rows do not have the training split's mean, and "columns that never vary" is a different set in fifty rows than in forty thousand. Training now records the statistics it fitted, sampling replays them, and the adapter's bespoke preprocessing is deleted rather than patched. Any two-stage model that preprocesses conditions gets the same treatment without writing code for it. Feasibility no longer rides along with the optimizer pass. It describes the design as generated, so it is judged before any solver runs -- which means it still reports when the optimizer refuses to start from an invalid design, the case where the answer matters most, and asking for `viol` alone now costs a constraint check instead of a full optimization. It is a cheap metric as a result. Smaller corrections: ranks restart within each problem and spec version rather than running across the whole table; a private repo is no longer mistaken for an empty leaderboard, and a repo whose leaderboard file is merely absent keeps its revision so two first publishers cannot overwrite each other; `--skip-existing` compares the checkpoint hash, so retrained weights are re-scored; and the package hash covers every weight file present rather than only the declared ones, since VQGAN loads a condition encoder it cannot declare. Not adopted: pinning the EngiBench revision as a hard requirement at resolution, or adding it to leaderboard ranking identity. Recording it is right and is done; enforcing it would break every contributor whose EngiBench differs by any commit, and ranking on it would partition the board per build so that two models could not be compared unless scored on byte-identical EngiBench. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
All nine addressed; eight adopted, one half-declined with reasoning (B). The one that mattered most is A: you were right that CI was green because it skipped Four of these were defects the previous round introduced (C, F, G, and A itself). Two of
Where I would push back — B, second half only. Recording the EngiBench revision is 153 tests, up from 136. |
…ion set Projecting the evaluator's conditions onto a checkpoint's recorded schema is right when the dropped column is constant -- it carries no information, which is why training dropped it. beams2d's overhang_constraint is 0 across all fifty test cases, so VQGAN loses nothing the other models have. That is a property of today's data, not a guarantee. A column constant in a training split can vary in a future evaluation set, and then the model is blind to a requirement its competitors can see while nothing says so. Not an error: a model is allowed to ignore conditions, and the conditional-adherence metrics exist to expose exactly that. But it should never be silent. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
I rechecked the latest head. The condition handling, VQGAN preprocessing, feasibility, ranking, Hub publishing, and CI spec verification now look correct. The pinned suite passes all 155 tests without skips. One documentation cleanup from the previous review summary remains. This PR removes the per-model evaluation scripts, removes W&B as a checkpoint source, and deletes The documented default HF repository also still has no checkpoint package. Before presenting the evaluation command as runnable, please either publish that package, retain the migration path until issue #73 is complete, or state clearly that pretrained checkpoints are not available yet. The remaining inline comments are limited to checkpoint identity, replaying training-fitted normalizers, hashless |
Three ways a package could misrepresent itself, all of which reach the leaderboard: `metrics.json` was inside the content hash. It is written by `publish_checkpoint_metrics` into the package it describes, so attaching a score changed the identity of the thing scored: the next `--skip-existing` run would see a new hash, re-evaluate, rewrite the metrics, and change the hash again. No package has metrics attached yet, so this was latent. It is now in DESCRIPTIVE_FILES alongside run_config.json and metadata.json, which also stops it being served back as a loadable checkpoint file. A multi-stage model that dies partway leaves a package that looks complete. VQGAN uploads after each of its three stages so a crash does not lose the earlier work, which means an interrupted run publishes a run_config.json and a metadata.json with no usable weights -- indistinguishable, from the Hub, from a finished run. 4 of the pool's 102 VQGAN runs ended this way, holding vqgan.pth but no transformer.pth. `package_complete` is the distinction, and the load error now says which stage it reached and what the package holds. The same gap applies to the canonical path, which an intermediate stage of a default-hyperparameter run could claim. That publishes the bare model name pointing at something unloadable for as long as the remaining stages take -- hours, for VQGAN -- and permanently if the run dies first. Only a finished run may claim it now. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
VQGAN records the statistics it normalized its conditions by, and the contract replays them. cgan_1d, cgan_bezier, gan_1d, gan_bezier, and diffusion_1d did not: they rebuilt their normalizers from whatever `problem.dataset["train"]` currently holds, at load time. cgan_1d's docstring said so outright. Those bounds are as much a part of the model as its weights -- a design decoded against different bounds is a different design -- but `Normalizer` is a plain class rather than an nn.Module, so nothing ever put them in the state dict. The result is that the same checkpoint, with the same content hash, produces different designs after a dataset revision, with nothing to show it happened. The eval spec's pinned revision does not help: it governs the test conditions, not the training split these read. `normalizer_state` / `load_normalizer_state` record and restore the fitted bounds, duck-typed on min_val/max_val/eps so the five identical copies of `Normalizer` do not have to be unified first -- that is a change to training code this does not need. A checkpoint with nothing recorded is left to fit from the dataset exactly as before, which is what those runs really did. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`already_evaluated` treated a row with no `checkpoint_hash` as a match. You are right that it should not: an absent hash means the identity of those weights is unknown, and unknown is not equal. One hashless row could suppress every future evaluation of that configuration and seed, retrained weights included, until somebody deleted it by hand. Since this schema has never shipped on `main` there is no historical board whose re-evaluation cost the leniency was protecting, so the legacy-row test is updated rather than kept. Rows now also record `engibench_version`: the EngiBench that ran the evaluation, as opposed to the one the spec was frozen against. This is the compromise from the earlier thread -- recorded, not enforced, and not part of ranking identity, so contributors on a slightly different commit can still be compared while the difference stays visible. Deriving that version had the bug you predicted. `git -C` searches parent directories, so a wheel unpacked into a virtualenv inside a checkout would report the *enclosing* repository's sha as EngiBench's -- a wrong sha, worse than none. It now reads PEP 610 `direct_url.json` first, which is where a non-editable `pip install git+...` records the commit and is the case CI hits, and only accepts a git answer when the repository root is the package's own directory and the path is not an installed-package path. Separately, `--config-fingerprints` was applied as a cross product over generators. A fingerprint hashes one algorithm's hyperparameters, so evaluating two models against a flat list asked for packages that were never going to exist and buried the run in load failures that were not failures. `algo:fingerprint` scopes an entry to its owner. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`{problem_id}/seed_{seed}` is what a bare model name resolves to: what
`from_pretrained(problem, seed=1)` reads and what the README's evaluation
command uses. Only a run using the training script's default hyperparameters
writes it, and a sweep has no such run, because every arm sets at least one
flag.
The consequence is currently live on the Hub. cgan_cnn_2d and gan_cnn_2d have
138 published packages each and not one canonical package for any problem, so
the documented `--generators cgan_cnn_2d --seeds 1` fails with a bare "not
found". vqgan and diffusion_2d_cond do have canonical packages, but only
because their swept center configuration happens to coincide with the script
defaults -- luck rather than design, and precisely what makes the bare model
name unreliable.
Two changes. A failed canonical resolution now lists the configurations that
do exist, so the reader learns the package was never written rather than
suspecting a broken path. And `python -m engiopt.promote_checkpoint` copies a
chosen configuration to the canonical path, so an existing sweep arm can
become the default without spending a few hundred GPU-hours retraining a
model we already have. The promoted package keeps its own
`config_fingerprint`, so a leaderboard row still records which configuration
earned the score; only the address changes. An incomplete package is refused.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… mean it The README still advertised W&B artifact refs for the power-electronics optimizer and linked docs/checkpoint_migration_playbook.md, which this branch deletes; `run_pe_optimization.py` still described its arguments as W&B artifacts and used one in its usage example; and `Generator.resolve_checkpoint` still listed `wandb` as a model source. None of those paths exist. The README also documented no leaderboard workflow at all -- `--push-to`, `--skip-existing`, and `--attach-metrics` were undocumented, as was the fact that a sweep leaves the canonical path empty. Both now have a section, along with how an outside contributor submits a model. The ruff job ran `ruff check --fix`, which repairs violations inside the runner and then reports success, so the job was green on code that does not pass. Dropping `--fix` makes it mean what it says, and the 20 pre-existing I001 violations it was hiding are fixed here. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Thanks — all four of the latest round are addressed, each in its own thread. Summary here, The four inline items
Cross-cuttingCheckpoint migration / issue #73. The precondition you set is now met, and I checked
Lint CI. Confirmed your diagnosis exactly: Documentation. README no longer advertises W&B artifact refs or links the deleted DCC workshop branch. Here I would push back on the remedy rather than the observation. Three findings from verifying against the pool1. Nothing in a sweep writes the canonical path — the documented command is broken. Two changes: the resolution error now lists what the repo actually holds instead of a bare 2. A correction to something I said earlier. On 2026-08-06 I reported that VQGAN's The 4 that genuinely did not finish are still the reason the completeness marker is worth 3.
All 61 predate this PR (63 on the previous head, so this branch reduces it by 2). Fixing it Also worth flagging, deliberately not changed
I had a VerificationCI on |
SoheylM
left a comment
There was a problem hiding this comment.
Thanks for the substantial follow-up. The earlier concerns around fitted preprocessing, null checkpoint hashes, EngiBench provenance, Ruff CI, and obsolete W&B documentation have been addressed. I reran the complete test and quality suite against the pinned EngiBench revision: all 201 tests pass, and Ruff, formatting, pre-commit, and pyright are clean. I also tested the documented evaluation path and checkpoint-package behavior directly.
The remaining inline comments concern the current checkpoint publishing and loading contract, reproducibility metadata, spec creation, and documentation. One cross-cutting migration point also remains: the shared registry exposes 14 generators, but the default HF repositories currently exist for only cgan_cnn_2d, diffusion_2d_cond, gan_cnn_2d, and vqgan. Because this PR removes W&B checkpoint loading repository-wide, please either migrate the remaining registered generators, retain a fallback for the unmigrated ones, or explicitly narrow what the shared evaluator advertises as available. Issue #73 should remain open until the chosen scope is complete.
The README sentence saying that each algorithm usually has separate training and evaluation scripts should also be removed, since evaluation is now shared. I am treating the missing device override on Apple silicon as a follow-up rather than a blocker for this PR.
| if not os.path.exists(file_path): | ||
| raise FileNotFoundError(f"Missing checkpoint file {file_name} in {root_dir}") | ||
| return ResolvedCheckpoint(source=source, root_dir=root_dir, files=files, run_config=run_config, metadata=metadata) | ||
| missing = [file_name for file_name, file_path in files.items() if not os.path.exists(file_path)] |
There was a problem hiding this comment.
package_complete=False is currently checked only while constructing the message for missing files. If all required filenames are present, this function returns the package successfully even though it is explicitly marked incomplete. I reproduced that case locally. Please reject a package whenever metadata["package_complete"] is False, before file discovery, and add a test where the required files are present but the marker is false.
There was a problem hiding this comment.
Fixed in c0ea5ce.
_load_package_from_directory now tests metadata[PACKAGE_COMPLETE_FIELD] is False before file discovery and raises _incomplete_package_message. The branch inside _missing_files_message became unreachable once that landed, so I deleted it rather than leave dead defensive code — that function now only explains a finished package that is missing a file, which is a different problem and must not blame the run for stopping early.
Tests: test_an_incomplete_package_is_refused_even_with_every_file_present is exactly your case — every required filename present, marker false — plus test_a_complete_package_still_loads so the guard does not cost ordinary packages their ability to load.
I also checked what this rejects in practice, since it is a new hard failure: 143 published packages across engiopt-vqgan, engiopt-cgan-cnn-2d and engiopt-diffusion-2d-cond, and none is marked incomplete. So it breaks nothing that exists today.
| "checkpoint_backend": checkpoint_backend, | ||
| "checkpoint_files": sorted(checkpoint_files), | ||
| "primary_files": primary_files or sorted(checkpoint_files), | ||
| PACKAGE_COMPLETE_FIELD: package_complete, |
There was a problem hiding this comment.
This completeness marker can be defeated by the current upload behavior. upload_folder at line 474 overwrites matching files but does not remove files already present under package_path that are absent from stage_dir. A rerun or staged VQGAN upload can therefore retain weights from an earlier package generation. Combined with package discovery, this can produce a package containing files from different runs. Please replace or delete the existing contents of this package path as part of the upload. The promotion upload at line 419 needs the same treatment.
There was a problem hiding this comment.
Fixed in c0ea5ce, though not unconditionally — a blanket delete would have destroyed VQGAN's staging, which I think is worth spelling out since it is the case that makes this subtle.
VQGAN stage 2 uploads {vqgan.pth, discriminator.pth} and does not re-upload the cvqgan.pth that stage 1 wrote. delete_patterns="*" there would delete the earlier stage's work — the exact thing staged uploads exist to protect.
So the rule keys off completeness. _stale_file_patterns returns ["*"] when package_complete is True or absent (an ordinary single-stage model is complete and should not have to say so), and None for a stage that is deliberately additive. promote_to_canonical deletes unconditionally, because a promotion copies a whole package and must replace whatever held the canonical path before.
Tests: test_a_complete_upload_clears_whatever_was_at_that_path and test_a_staged_upload_leaves_the_earlier_stages_alone.
| Returns: | ||
| A 16-character hash over the file names and their bytes. | ||
| """ | ||
| weight_files = sorted( |
There was a problem hiding this comment.
The recorded preprocessing state affects generated outputs but is excluded from content_hash because all of metadata.json is excluded. Changing condition_stats, condition_normalizer, or design_normalizer currently leaves the checkpoint hash unchanged; I verified this directly. That allows --skip-existing to reuse scores for behaviorally different packages. Please include a stable, identity-relevant subset of the metadata in the hash, while continuing to exclude mutable fields such as package path, revision, promotion history, and metrics.
There was a problem hiding this comment.
Fixed in c0ea5ce. hash_package_contents now folds identity_metadata(metadata) into the digest alongside the weight bytes.
One deliberate choice worth flagging, since you suggested naming an identity-relevant subset: I implemented it as a denylist, not an allowlist. ADDRESS_METADATA_FIELDS excludes package path, repo id, revision, promotion history and the W&B fields; everything else in metadata.json counts. An allowlist fails the dangerous way round — someone adds an output-affecting field, forgets to list it, and a behaviourally different checkpoint silently inherits an old score. A denylist's failure mode is that a bookkeeping field gets hashed and causes a harmless re-evaluation. I would rather pay that.
Tests: changing design_normalizer or condition_stats moves the hash; the same weights at the cfg and canonical paths still hash alike (they differ only in address fields); a promotion does not move it; and attaching metrics.json still does not, so the --skip-existing fixed point you and I both care about survives.
| return HfApi().dataset_info(dataset_id).sha | ||
|
|
||
|
|
||
| def freeze_spec( |
There was a problem hiding this comment.
freeze_spec cannot set volume_condition or volfrac_tol, although these fields determine viol and the committed beams2d, heatconduction2d, and thermoelastic2d specs use a non-null volume condition. Running the documented CLI for a new version silently produces a different feasibility contract. Please expose both fields through this entry point and cover them in the CLI/spec tests.
There was a problem hiding this comment.
Fixed in c0ea5ce, and widened past the two fields you named — for the reason your comment implies. A parameter this entry point omits is a field it silently resets to the dataclass default, so the bug class is bigger than volume_condition and volfrac_tol.
Every field of the contract is now settable, including the four new ones this round adds (required_seeds, copy_tol, max_copy_rate, copy_corpus_size). Defaults reference EvalSpec.<field> rather than repeating literals.
You asked for test coverage and I had missed it — added in af6b018, thank you: test_freeze_spec_carries_every_contract_field asserts the values reach the frozen spec, and test_freeze_spec_defaults_track_the_dataclass asserts the CLI's defaults are the dataclass's, so the two cannot drift apart without a test failing.
| except importlib.metadata.PackageNotFoundError: | ||
| version = "unknown" | ||
| try: | ||
| sha = subprocess.run( |
There was a problem hiding this comment.
This has the same parent-repository problem that was fixed for EngiBench: git -C searches upward, so an installed EngiOpt package inside another checkout can report that checkout's SHA as its own. Please use the same PEP 610/source-root validation approach here, or extract a shared helper, and test an installed-package path nested inside an unrelated repository.
There was a problem hiding this comment.
Fixed in c0ea5ce by extracting the shared helper, as you suggested.
_source_checkout_commit and _installed_vcs_commit are now one public source_checkout_commit(source_root, *, distribution=...) in spec.py, carrying both guards: an installed-package path is never a checkout, and the repository root must be the package's own parent rather than an ancestor. engibench_version() and code_version() both call it, so the two can no longer disagree about what counts as a checkout.
The existing tests moved to the shared helper and so now cover the EngiOpt path too: test_an_installed_package_is_never_read_as_a_checkout (a site-packages path) and test_a_directory_inside_an_unrelated_repo_is_not_that_repo (a real git init with the package nested below the root).
Observed live: rows now carry code_version: 0.0.1+af6b018..., which is this worktree rather than an enclosing checkout.
| Then evaluate: | ||
| ``` | ||
| python engiopt/cgan_cnn_2d/evaluate_cgan_cnn_2d.py --problem-id "beams2d" --seed 1 --n-samples 10 | ||
| python -m engiopt.evaluate --problem-id "beams2d" --generators cgan_cnn_2d --seeds 1 |
There was a problem hiding this comment.
This exact command still fails because IDEALLab/engiopt-cgan-cnn-2d has no canonical beams2d/seed_1 package. The promotion example below also uses 023dd1fb with seed 1, but that configuration currently exists only for seed 42; a seed-1 configuration that exists is 825831f6. Please publish a canonical package and verify these commands verbatim, or document a configuration and seed that can currently be evaluated.
There was a problem hiding this comment.
Reproduced before fixing, and you are right on both counts — and the first one is worse than "no canonical package": IDEALLab/engiopt-cgan-cnn-2d holds 46 beams2d packages and not one canonical path.
Your 825831f6 is exactly right, and I can say why it is the only candidate: it is the sole cgan_cnn_2d configuration trained on seeds 1–10. The other 45 arms are seed 42 only, so promoting any of them to seed_1 would name a package that does not exist. Same shape for gan_cnn_2d, where the multi-seed arm is 6293adb3.
README now documents commands I ran verbatim:
--generators cgan_cnn_2d --seeds 1 2 3 --config-fingerprints cgan_cnn_2d:825831f6, with a sentence on why the fingerprint is required rather than optional here.--generators diffusion_2d_cond --seeds 1 2 3for the bare-seed case, since diffusion_2d_cond and vqgan do hold canonical packages. Ran it end to end: three rows, one entry,n_seeds=3.- The promotion example now uses
825831f6.
I did not publish a canonical cgan_cnn_2d package — that is a checkpoint-publishing decision, and it overlaps the migration-scope question in your review summary, so it seemed like yours to make rather than mine.
| conditions.keys # condition names, in column order | ||
| ``` | ||
|
|
||
| Most models want `require_tensor`. Reach for `dataset` only when your model |
There was a problem hiding this comment.
This guidance suggests using conditions.dataset to drop columns and re-normalize as VQGAN did, but this PR correctly removed that behavior because evaluation-time refitting changes the model input scale. Please say that dataset is for conditions that cannot be represented in the dense tensor, while any training-fitted preprocessing must be recorded in the checkpoint and replayed. Also, viol is now cheap and runs without --include-expensive, so the feasibility wording here and in the evaluation CLI docstring should be updated.
There was a problem hiding this comment.
Rewritten in c0ea5ce; the viol half finished in af6b018 after I noticed I had only done part of it.
dataset. Now documented as being for conditions that cannot travel in the dense tensor at all — thermoelastic2d's 65x65 boundary matrices, which condition_keys excludes for that reason — with an explicit "do not use it to re-derive preprocessing" and the reason: the 50 evaluation rows have neither the training split's statistics nor the same set of columns that never vary. The replacement path is spelled out (condition_stats, condition_normalizer, design_normalizer recorded at save and replayed at load), plus the new fact that those values are part of the checkpoint content hash, so changing a bound yields a different checkpoint rather than inheriting the old row.
viol. You flagged two places and I had only fixed one. CONTRIBUTING now lists the cheap pass as mmd, dpp, viol, novelty, cond_sens and describes --include-expensive as adding only the gaps. The CLI docstring still said "COG/IOG/FOG, feasibility" until af6b018 — now corrected, with the reason feasibility is cheap: it describes the design as generated, so it still reports when the optimizer refuses to start from an invalid one.
…ieval An internal leaderboard can assume its rows were produced in good faith by someone with no reason to fabricate them. A public one cannot assume either, and the two failure modes need separate answers. Fabrication. A row recorded a checkpoint hash but never a repo, and every default pointed at IDEALLab, so a contributor could neither publish weights nor produce a row anyone could trace. Rows now carry repo, path, revision and hash; `python -m engiopt.verify` follows that address, checks the package still hashes to what was scored, and re-scores it. Rows land unverified and unranked until a runner reproduces them, and because the address is public the runner is just the first auditor rather than a trusted oracle. Verification publishes its own numbers instead of a pass/fail, since one seed on different hardware draws different designs and any tolerance loose enough for that is loose enough for fudging. Gaming. The protocol is public: the spec names the scored conditions and the dataset supplies each optimal design, so `mmd`, `iog` and `fog` are by definition maximised by returning them. Measured on beams2d, a lookup table scores mmd=0.000 and viol=0.00 against the trained cGAN's 0.561 and 0.92 -- it wins every headline metric. `novelty`/`copy_rate` catch it (copy_rate=1.00), and `cond_sens` catches a model that ignores conditions it declares. Both are diagnostic, not ranked: ranking novelty would seat pure noise in first place, which is closing one vector by opening another. Flagged rows are published and left out of the ordering, because deleting a submission is moderation and declining to rank one is a statement about what its number measures. Also closes the ways a score could drift from the thing scored: `required_seeds` stops a best-three-of-twenty median; `ROW_KEY` gains `checkpoint_repo` so two contributors' identically-configured models cannot overwrite each other; the content hash now covers fitted preprocessing, which changes outputs but left the hash untouched; a complete upload clears stale files, while a staged one deliberately does not; an incomplete package is refused before file discovery rather than while explaining a missing file; and `code_version` stops reporting an enclosing repository's sha as its own. Docs: LEADERBOARD.md for the trust model and the copying hole that a v2 spec scored on off-dataset conditions would actually close. README and CONTRIBUTING corrected -- the documented commands now name packages that exist, `--list-generators --check-availability` reports which of the fourteen registered models have published weights, and the guidance to re-fit preprocessing from `conditions.dataset` is replaced by recording it in the checkpoint. 51 new tests. Verified against the live Hub: a real cgan_cnn_2d checkpoint scores end to end with its address recorded and cond_sens=0.171, and all four committed specs still reproduce their frozen digests. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… does Two things the live run turned up. `--generators gan_cnn_2d vqgan --config-fingerprints gan_cnn_2d:6293adb3` scored gan_cnn_2d and never mentioned vqgan. Scoping the only entry to another algorithm left vqgan with an empty fingerprint list, so the loop body never ran: no row, no load attempt, no error. A leaderboard quietly missing an entrant is worse than one reporting a load failure, so a model no entry is scoped to now falls back to its canonical checkpoint. Ruff 0.16 formats Python blocks inside Markdown and CI runs it repo-wide, while the local check here was scoped to engiopt/ and tests/. Reformatted, and the lesson is the general one: run the command CI runs, not a subset of it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`cheap` in this registry means "cannot reach a simulator", which cond_sens honours -- but it draws the batch twice, so selecting it doubles generation cost. Free for a GAN, noticeable for diffusion, and unpleasant for a model that samples a pixel at a time. Documented alongside the --metrics escape hatch, which was already supported and unmentioned. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`ResolvedCheckpoint.reference` was added alongside repo_id/package_path on the assumption verification would want a formatted hf:// string; it builds that inline instead. An unused accessor on a core dataclass is a maintenance claim with no payer. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`FLAG_INCOMPLETE_SEEDS` was declared and never raised: seed coverage is a property of the entry, so `eligible_rows` drops those rows during ranking rather than flagging them individually. A flag constant nobody emits reads like a feature that exists. `_as_float` had been written twice, once per module, for the same job with slightly different blank handling. Promoted to `as_metric_value` in submission, where the NaN rule belongs -- a metric that did not run must be neither flagged as a zero nor reported as a claim the verifier failed to reproduce. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two gaps in my own response to review, found while writing the replies. The CLI's --include-expensive help still listed feasibility as simulator-backed. That was half of Soheyl's point and I had only fixed the other half in CONTRIBUTING; viol has been a constraint check since the earlier round. `freeze_spec` gained parameters for every contract field but no test, and he asked for one. Two now: that the values reach the frozen spec, and that the CLI's defaults are the dataclass's rather than a second copy that can drift without either side failing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Round 2 — all seven comments answered, plus the change that lets this board be publicThreaded reply on each of the seven; short version below, with the two that
Two of these were symptoms of a bigger gap. A row recorded a checkpoint Full write-up in the PR description, and the trust model in On the earlier threads~21 threads from 3 and 6 Aug are still open in the UI. I re-verified each Still open, and I don't think it's mine to closeMigration scope. Ten of fourteen registered generators have no published I also did not publish a canonical |
|
The public submission and verification boundary is not complete yet. The documentation now describes a public leaderboard where outside contributors publish checkpoint-backed rows and an “official runner” later downloads and re-scores them. However, this PR does not define or deploy that runner: More importantly, the submitted artifact is not yet safe for unattended execution. Verification reaches I think this PR should establish a submission contract that a future official runner can consume safely, even if deploying that runner remains follow-up work:
Existing IDEALLab Isolation and runtime controls belong to the eventual runner: no credentials, restricted network access, and CPU/GPU, memory and time limits. Those controls do not have to be deployed in this PR, but they should be tracked before unattended verification is enabled. Deploying and operating the official runner may remain follow-up work, but the safe submission contract cannot. Every external submission accepted after this PR should already contain safetensors weights, validated JSON configuration and preprocessing metadata, a registered adapter identifier, and an immutable repository revision and content hash. Participant-computed metrics may remain visible as |
|
README cleanup: The sentence “Usually, we provide two scripts per algorithm: one to train the model, and one to evaluate it” describes the architecture removed by this PR. Each generator still has its own training script and adapter, but evaluation is now handled centrally by |
|
The migration playbook is being removed before #73 meets its removal criteria. Issue #73 says I do not think this PR needs to migrate every historical checkpoint. However, could we either:
Deleting the migration instructions while the migration decision remains open leaves no documented route for completing the work. When documenting availability, please also distinguish missing checkpoints from incompatible generators. For |
Generator contract, shared evaluation layer, and HF-hosted leaderboard
Replaces 13 near-duplicate
evaluate_*.pyscripts with one adapter per modeland a shared evaluation package, and makes HuggingFace the single home for
checkpoints, metrics, and the leaderboard.
Large, but split into four commits meant to be reviewed one at a time.
Commit 1 is a pure file move you can skim. The design fits in commit 2.
Why
Every model had a training script and an evaluation script. The evaluation
scripts were ~79% identical —
evaluate_cgan_cnn_2d.pyandevaluate_gan_cnn_2d.pydiffered by 26 lines out of 123, and those 26 lineswere only ever four things: which network class to import, how to build it from
run config, whether to pass conditions, and a model-id string.
So adding a metric meant editing 13 files, and adding a model meant copying one.
A leaderboard can't be built on that.
Training scripts are deliberately untouched in structure. Their variation is
real — 340 to 1894 lines of genuinely different algorithms — so unifying them
would relocate complexity rather than concentrate it, and would cost the
single-file readability the repo is built around.
Review guide
1 ·
646617d— Move model packages underengiopt/generators/(68 files, skim)Mirrors
engibench/problems/<name>/. Pure move: file contents are unchangedapart from the import paths the move forces (
engiopt.<model>→engiopt.generators.<model>). Git detects all 47 renames, sogit log --followstill works.
Nothing to review here beyond "yes, that's a move."
2 ·
35f1041— What defines a "generator" (5 files, read this one)The whole design is here. Today a model is defined implicitly, by whatever its
evaluation script happened to do. This commit makes it explicit: a generator is
six facts and two actions.
The six facts a model states about itself. These are plain values, not code:
algo_id"gan_2d"conditionalFalsedesign_kinds("2d",)checkpoint_files("generator.pth",)primary_state_key"generator"output_clip(1e-3, 1.0)The two actions a model must implement:
build— given a downloaded checkpoint, reconstruct me. The modelreceives the training run's saved settings and local file paths, and returns
a working network.
_sample— given design requirements, produce designs. This is the onlygenuinely model-specific step, and it is where models really differ: one line
for a GAN, a 1000-step loop for diffusion, one forward pass per pixel for
PixelCNN++.
Everything else is done once, centrally: finding and downloading the
checkpoint, picking CPU/GPU, choosing which hyperparameter configuration to
load, seeding for reproducibility, timing how long sampling took, reshaping
outputs, and clamping them to the valid range.
The outcome: a new model is ~45 lines, of which ~10 are unique to it. Before
this, adding a model meant copying a 123-line evaluation script and editing
four scattered places in it. The contract mirrors
engibench.core.Problem, sothe two halves of the stack are learned once.
engiopt/utils/all_generators.pyfinds models by walking the folder, exactly asengibench/utils/all_problems.pyfinds problems —BUILTIN_GENERATORSsitsalongside
BUILTIN_PROBLEMS.Checkpoint storage also changes here. Each training run is filed under a
short hash of its own hyperparameters:
The outcome: a 50-configuration sweep produces 50 separately loadable models
instead of 50 runs overwriting one folder. The plain
{problem}/seed_{n}pathis claimed only by a run that used the training script's default settings —
so no amount of hyperparameter searching can change what a bare model name
means. W&B is no longer a checkpoint source at all.
3 ·
a796025— Shared evaluation layer (28 files, −1942 lines)Deletes the 13 eval scripts. Adds:
engiopt/evaluation/— a metric registry declaring each metric's family,cost, and ranking direction; an
EvaluationContextthat computes theexpensive optimizer pass once for every performance metric (
iog,cog,fog,violshare one sweep instead of running four); anEvaluator; andthe leaderboard, published to HF by merging rather than overwriting.
engiopt/specs/<problem>/v1.json— frozen, hash-verified evaluationcontracts, so rows are comparable across people and across time.
python -m engiopt.evaluatereplaces the deleted scripts.engiopt/generators/_template/— copy-me starting point for new models.Metrics are registered functions, not model methods — a metric compares a
generated set against a reference set under a problem, so it belongs to the
comparison:
Declared
costmeans a cheap run provably cannot start a simulator.4 ·
2b9c58d— W&B decoupling + four correctness fixes (29 files, scrutinise this one)These are behaviour changes, not refactors.
Training / W&B. 10 of 14 scripts only saved checkpoints when
--trackwason, so training without W&B silently produced nothing on HuggingFace. VQGAN
additionally gated early stopping — real training logic — behind it, so under
default settings its transformer checkpoint was never written at all. Found by a
live run, not by tests.
iogwas not an optimality gap. It stored the raw simulated objective whilecog/fogmeasured against the reference optimum. Verified with an oraclegenerator that returns the reference design itself: it now scores
iog ≈ 3e-07where it previously reported the design's raw compliance.
Condition sampling couldn't serve half the problems.
photonics2ddeclaressolver settings that aren't dataset columns;
thermoelastic2dencodes boundaryconditions as 65×65 matrices. Scalar conditions now feed the model tensor while
the simulator still receives every condition — the physics is unchanged.
Multi-objective scores leaked.
thermoelastic2dhas a per-sampleweightspanning 0.0–1.0. Summing or averaging its three objectives meant a purely
structural sample was scored mostly on thermal compliance:
Scalarization is now declared in the eval spec, and the evaluator refuses to
guess when it's absent.
Gaps ignored objective direction.
photonics2dmaximizestotal_overlap,so a design that beat the reference scored
+0.3and ranked last. Gaps are nowsigned by
ObjectiveDirection.Also removes
metrics.metrics()andsimulate_failure_ratio()— both dead oncethe evaluator landed. (This was the only remaining mypy error in the codebase.)
Verification
76 tests. Metric correctness is asserted by properties that must hold for any
correct implementation —
mmd(X, X) == 0, collapsed generators score belowvaried ones, a purely structural sample is unaffected by thermal compliance —
rather than by pinning historical numbers.
Verified live against HuggingFace with W&B disabled: trained two
hyperparameter configurations, confirmed they landed in separate packages,
loaded each back by fingerprint, evaluated, pushed the leaderboard incrementally,
and confirmed the first model's row was untouched. VQGAN was exercised
separately as the multi-file, multi-stage case. All test repos deleted after.
Ruff clean; mypy clean.
Migration notes for reviewers
lvaebranch will need adapting. Itsevaluate_*.pyscripts call theremoved
metrics.metrics()and pass W&B artifact arguments that no longerexist. That was accepted deliberately rather than maintaining two evaluation
paths.
layout changed and there is no W&B read-fallback.
surrogate_model/is untouched and sits outside the contract; it's neither agenerator nor evaluated.
Known limitations
cogsums across steps and objectives whilefogaverages objectives — apre-existing inconsistency, left alone rather than changed silently. Worth
settling alongside the metric-suite work.
dppunderflows toward zero on collapsed generators (a 1-epoch GAN scored8.9e-22), so it may lose ordering at the bottom of a real zoo. A log-domainform would fix it.
Generatorcontract isthe submission interface, so opening it later is a policy change, not a
rewrite.
Update — review round 1 addressed (commits
3a2568e,810529f)All 13 of @SoheylM's comments are fixed, each with a threaded reply. Summary comment
below has the full breakdown; the two things worth knowing before re-reading the diff:
1. Evaluation now needs EngiBench from source, and CI pins a commit.
The two specs that would not resolve for Soheyl were not a case of datasets drifting
upstream — they were frozen against EngiBench
main, whosephotonics2dandthermoelastic2dread thev1datasets, while release 0.2.0 still points those two atv0. On 0.2.0 they sample different data entirely. Specs now record the problemdefinition they were frozen against, so that case reports itself instead of surfacing as
a hash mismatch. CI installs EngiBench at a pinned commit; the README says so.
2. Four changes are in the diff that nobody asked for. Flagged so they are not a
surprise: the EngiBench pin above; two extra bug fixes inside the feasibility work
(
check_constraintsraised on thermoelastic2d, and float64 designs were judgedinfeasible against a float32 design space on dtype alone); a pytest job in CI, since the
repo ran only ruff and mypy and the requested spec test had nowhere to live; and the
deletion of two dead helpers this PR had itself added.
Also removed the sweep YAMLs and the per-algorithm notebook that should never have been
committed.
Testing: 136 tests, up from 111. CI green on all four checks.
Known limitation — image conditions
thermoelastic2dmodels are conditioned on 3 scalars while the simulator receives all 7conditions, because its four 65x65 boundary masks cannot travel in a dense condition
tensor. Scoring is correct —
simulateandoptimizeget the full conditions — but amodel cannot currently see where the part is held, loaded, or cooled unless it reads
ConditionBatch.datasetitself, asvqgandoes for its own preprocessing. First-classsupport is being prepared as a follow-up PR; it is purely additive to this contract
(new optional fields, no adapter changes), so it does not block this one.
Update — review round 2: making the board survive being public
All seven remaining comments are fixed, with a threaded reply on each. Two of
them turned out to be the tip of a larger problem, so this round also closes the
gap that would have stopped this leaderboard working as a public one.
The board was a self-report system
A row recorded a checkpoint hash but never a repo, and every default
pointed at
IDEALLab. An outside contributor could neither publish weights norproduce a row anyone could trace back to them, so every number was an assertion
nobody could falsify.
Rows now carry
checkpoint_repo/checkpoint_path/checkpoint_revision/checkpoint_hash— an address, not just a fingerprint — andpython -m engiopt.verifyfollows it: fetch the package at the recordedrevision, confirm it still hashes to what was scored, rebuild through the
registered adapter, score it again. Rows land
verified=falseand unrankeduntil a runner reproduces them.
Verification publishes its own numbers, not a pass/fail on the submitted
ones. Sampling from one seed on different hardware genuinely produces different
designs — CUDA and CPU draw different values from the same generator state — so
any tolerance loose enough for honest variation is loose enough for real
fudging. Re-scoring sidesteps that; the submitted value becomes a claim reported
as corroborated or not.
The runner is not privileged. Because the address is public,
python -m engiopt.verifywith no credentials and no--publishis an audit anyone canrun, which is what keeps the runner honest too.
The protocol is public, so three metrics are copyable by construction
The spec names the scored conditions, and the dataset supplies the optimal
design for each.
mmd,iogandfogare therefore defined as closeness todesigns anyone can look up. Measured on the real beams2d spec, against the
trained models in this repo:
memorizedA lookup table beats every trained model on both headline metrics. That is not
an implementation flaw and it cannot be fixed by hiding which rows are scored,
since the whole dataset is public. So the board measures retrieval instead:
novelty/copy_ratecatch it, andcond_senscatches a model that ignoresconditions it declares it uses.
Both are diagnostic, with no ranking direction, and that is the load-bearing
choice. Ranking on novelty would seat pure noise in first place — zero means
retrieval, but large means only "unlike the data", which a broken model also
achieves. Closing one gaming vector by opening another is not progress. Flagged
rows are published and left out of the ordering: deleting a submission is
moderation, declining to rank one is a statement about what its number measures,
and only the second scales.
cond_sensis not a new feature so much as a claim this PR had already made —registry.pydeclared aconditionsmetric family andcore.pyreferred threetimes to "conditional-adherence metrics", with no such metric existing. The
alternative was deleting the claims.
Other ways a score could drift from the thing scored
required_seeds. An entry must cover seeds 1, 2, 3 to be ranked. A countwould not do: it still permits running twenty and publishing the best three,
which turns a median into a maximum while every individual row stays honest.
ROW_KEYgainscheckpoint_repo. Two contributors who both traincgan_cnn_2don default hyperparameters and seed 1 produced the same key anddifferent weights, so either one's push would silently replace the other's
verified row.
--generators gan_cnn_2d vqgan --config-fingerprints gan_cnn_2d:6293adb3scored gan_cnn_2d and never mentioned vqgan — the emptyfingerprint list meant the loop body never ran, producing no row, no load
attempt and no error. Found by running it, not by a test.
Verification
255 tests, up from 201; CI green on all four checks with no skips.
Exercised against the live Hub rather than fakes: a lookup table scored on the
real beams2d spec (
copy_rate=1.00); four published checkpoints loaded andscored through the contract, including VQGAN's multi-file case; the documented
diffusion_2d_cond --seeds 1 2 3run produced three rows collapsing into oneentry with
n_seeds=3; all four committed specs still reproduce their frozendigests; and 143 published packages were checked to confirm the new
incomplete-package refusal rejects none of them.
New docs: LEADERBOARD.md — the trust model, the flags, and
the design of the v2 spec that would actually close the copying hole rather
than price it (scoring on off-dataset conditions, which needs an optimizer run
per condition and so is not built here).
Still open, deliberately
Migration scope. Ten of the fourteen registered generators have no published
checkpoint.
--list-generators --check-availabilitynow reports that pergenerator instead of implying all fourteen are usable — but reporting is not
choosing between migrating them, keeping a fallback, and narrowing the registry.
That is a project decision, so #73 should stay open.
🤖 Generated with Claude Code