Skip to content

Generator contract, shared evaluation layer, and HF-hosted leaderboard - #75

Open
mkeeler43 wants to merge 20 commits into
mainfrom
feat/generator-contract-overhaul
Open

Generator contract, shared evaluation layer, and HF-hosted leaderboard#75
mkeeler43 wants to merge 20 commits into
mainfrom
feat/generator-contract-overhaul

Conversation

@mkeeler43

@mkeeler43 mkeeler43 commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Generator contract, shared evaluation layer, and HF-hosted leaderboard

Replaces 13 near-duplicate evaluate_*.py scripts with one adapter per model
and 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.py and
evaluate_gan_cnn_2d.py differed by 26 lines out of 123, and those 26 lines
were 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 under engiopt/generators/ (68 files, skim)

Mirrors engibench/problems/<name>/. Pure move: file contents are unchanged
apart from the import paths the move forces (engiopt.<model>
engiopt.generators.<model>). Git detects all 47 renames, so git log --follow
still 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:

Fact Example What it buys you
algo_id "gan_2d" The name it appears under everywhere — its folder, its leaderboard row, its HuggingFace repo. One name, so those can never drift apart.
conditional False Whether it listens to the design requirements it is given. Unconditional models are still given them, so a metric can measure that they ignored them.
design_kinds ("2d",) Which problems it can serve. Pointing a 2D model at a 3D problem now fails immediately with a clear message, instead of producing garbage.
checkpoint_files ("generator.pth",) Which files must be present to load it. Missing files fail at load, not halfway through an evaluation.
primary_state_key "generator" Where the weights sit inside the saved file.
output_clip (1e-3, 1.0) The range its designs must stay in, because several simulators are unstable at exactly zero.

The two actions a model must implement:

  • buildgiven a downloaded checkpoint, reconstruct me. The model
    receives the training run's saved settings and local file paths, and returns
    a working network.
  • _samplegiven design requirements, produce designs. This is the only
    genuinely 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, so
the two halves of the stack are learned once.

engiopt/utils/all_generators.py finds models by walking the folder, exactly as
engibench/utils/all_problems.py finds problems — BUILTIN_GENERATORS sits
alongside BUILTIN_PROBLEMS.

Checkpoint storage also changes here. Each training run is filed under a
short hash of its own hyperparameters:

{problem}/cfg_{fingerprint}/seed_{n}/     one per configuration
{problem}/seed_{n}/                        the "standard" one

The outcome: a 50-configuration sweep produces 50 separately loadable models
instead of 50 runs overwriting one folder. The plain {problem}/seed_{n} path
is 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 EvaluationContext that computes the
    expensive optimizer pass once for every performance metric (iog, cog,
    fog, viol share one sweep instead of running four); an Evaluator; and
    the leaderboard, published to HF 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/ — 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:

@register_metric("mmd", family="distribution", cost="cheap", higher_is_better=False)
def mmd(ctx) -> float: ...

Declared cost means 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 --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. Found by a
live run, not by tests.

iog was not an optimality gap. It stored the raw simulated objective while
cog/fog measured against the reference optimum. Verified with an oracle
generator that returns the reference design itself: it now scores iog ≈ 3e-07
where it previously reported the design's raw compliance.

Condition sampling couldn't serve half the problems. photonics2d declares
solver settings that aren't dataset columns; thermoelastic2d encodes boundary
conditions 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. thermoelastic2d has a per-sample weight
spanning 0.0–1.0. Summing or averaging its three objectives meant a purely
structural sample was scored mostly on thermal compliance:

weight sum mean weighted
1.0 (structural only) 42.31 14.10 0.30 ← correct
0.0 (thermal only) 42.31 14.10 42.00

Scalarization is now declared in the eval spec, and the evaluator refuses to
guess
when it's absent.

Gaps ignored objective direction. photonics2d maximizes total_overlap,
so a design that beat the reference scored +0.3 and ranked last. Gaps are now
signed by ObjectiveDirection.

Also removes metrics.metrics() and simulate_failure_ratio() — both dead once
the 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 below
varied 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

  • The lvae branch will need adapting. Its evaluate_*.py scripts call the
    removed metrics.metrics() and pass W&B artifact arguments that no longer
    exist. That was accepted deliberately rather than maintaining two evaluation
    paths.
  • All checkpoints must be retrained, which was already the plan — the storage
    layout changed and there is no W&B read-fallback.
  • surrogate_model/ is untouched and sits outside the contract; it's neither a
    generator nor evaluated.

Known limitations

  • cog sums across steps and objectives while fog averages objectives — a
    pre-existing inconsistency, left alone rather than changed silently. Worth
    settling alongside the metric-suite work.
  • dpp underflows toward zero on collapsed generators (a 1-epoch GAN scored
    8.9e-22), so it may lose ordering at the bottom of a real zoo. A log-domain
    form would fix it.
  • Leaderboard submissions are internal for now. The Generator contract is
    the 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, whose photonics2d and
thermoelastic2d read the v1 datasets, while release 0.2.0 still points those two at
v0. On 0.2.0 they sample different data entirely. Specs now record the problem
definition 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_constraints raised on thermoelastic2d, and float64 designs were judged
infeasible 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

thermoelastic2d models are conditioned on 3 scalars while the simulator receives all 7
conditions, because its four 65x65 boundary masks cannot travel in a dense condition
tensor. Scoring is correct — simulate and optimize get the full conditions — but a
model cannot currently see where the part is held, loaded, or cooled unless it reads
ConditionBatch.dataset itself, as vqgan does for its own preprocessing. First-class
support 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 nor
produce 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 — and
python -m engiopt.verify follows it: fetch the package at the recorded
revision
, confirm it still hashes to what was scored, rebuild through the
registered adapter, score it again. Rows land verified=false and unranked
until 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.verify with no credentials and no --publish is an audit anyone can
run, 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, iog and fog are therefore defined as closeness to
designs anyone can look up. Measured on the real beams2d spec, against the
trained models in this repo:

mmd viol copy_rate cond_sens
lookup table 0.000 0.00 1.00 0.0 flagged memorized
cgan_cnn_2d 0.561 0.92 0.00 0.171
gan_cnn_2d 1.049 1.00 0.00 0.0 unconditional, correctly unflagged
vqgan 0.007 0.46 0.02 0.386

A 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_rate catch it, and cond_sens catches a model that ignores
conditions 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_sens is not a new feature so much as a claim this PR had already made —
registry.py declared a conditions metric family and core.py referred three
times 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 count
    would 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_KEY gains checkpoint_repo. Two contributors who both train
    cgan_cnn_2d on default hyperparameters and seed 1 produced the same key and
    different weights, so either one's push would silently replace the other's
    verified row.
  • A silent skip. --generators gan_cnn_2d vqgan --config-fingerprints gan_cnn_2d:6293adb3 scored gan_cnn_2d and never mentioned vqgan — the empty
    fingerprint 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 and
scored through the contract, including VQGAN's multi-file case; the documented
diffusion_2d_cond --seeds 1 2 3 run produced three rows collapsing into one
entry with n_seeds=3; all four committed specs still reproduce their frozen
digests; 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-availability now reports that per
generator 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

mkeeler43 and others added 5 commits July 24, 2026 13:56
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>
@mkeeler43
mkeeler43 requested a review from SoheylM July 24, 2026 12:37

@SoheylM SoheylM left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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-2d contained only .gitattributes and README.md; IDEALLab/engiopt-vqgan and IDEALLab/engiopt-gan-cnn-2d did not exist. The README's cGAN evaluation command cannot currently load beams2d/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.py still uses W&B references in its example and argument descriptions, while Generator.resolve_checkpoint and the evaluation CLI help still list wandb as a model source. These should match the final migration decision.
  • Lint CI: With Ruff 0.15.11, ruff check . reports 20 I001 errors on this commit. The GitHub job is green because it runs ruff check --fix in the temporary checkout without checking whether files changed. Please commit the import fixes and run plain ruff check . in CI. Pinning Ruff would also make the check stable.
  • Workshop integration: The open DCC workshop branch still imports engiopt.cgan_cnn_2d... in notebook_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.

Comment thread engiopt/evaluation/spec.py Outdated
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)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

  1. Specs pin the dataset. dataset_id + dataset_revision, recorded at freeze time,
    and resolve loads that exact revision. A later upload to the dataset repo can no
    longer change what a published row means.
  2. Specs record the problem definition they were frozen against (problem_conditions,
    plus engibench_version now carrying the git sha). resolve checks this before the
    digest, so the case you hit reports itself:
    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
    
    rather than two hashes that will never match.
  3. 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 over engiopt/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 review
  • test_a_differently_defined_problem_reports_itself — the message above
  • test_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.

Comment thread engiopt/evaluation/spec.py Outdated
return self.spec.n_samples


def _digest(indices: npt.NDArray[Any], ref_designs: npt.NDArray[Any]) -> str:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread engiopt/core.py Outdated
@property
def n_conds(self) -> int:
"""Number of scalar conditioning variables for this problem."""
return len(self.problem.conditions_keys)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread engiopt/evaluation/leaderboard.py Outdated
raise ValueError(f"Metric {metric!r} has no ranking direction; it is diagnostic only.")

grouped = (
frame.groupby(["problem_id", "algo_id"], as_index=False)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread engiopt/evaluation/leaderboard.py Outdated

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread engiopt/evaluate.py Outdated
]
print(f"{len(generators)} generator(s) left after skipping already-published rows.")
if not generators:
print("No generators loaded; nothing to evaluate.")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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-existing0, since that's a finished job, not a failed one

The documented command you ran now prints the same message and exits 1.

Comment thread engiopt/core.py
start = time.perf_counter()
with th.no_grad():
raw = self._sample(batch, n)
self.last_sample_seconds = time.perf_counter() - start

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread engiopt/evaluation/context.py Outdated
results.fog.append(self.scalarize_gap(gaps[-1], i))

if conditions:
target_vol = conditions.get("volfrac") or conditions.get("volume")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed, taking your check_constraints suggestion as the base definition so the metric is
defined for every problem rather than removed from some specs:

  1. problem.check_constraints always applies — EngiBench's own contract, which a new
    problem gets for free by declaring constraints
  2. 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_constraints raised on thermoelastic2d. The HF dataset returns its boundary
    matrices as nested lists and EngiBench's bound check does lower <= 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, and Box.contains rejects 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.

Comment thread engiopt/checkpoint_store.py Outdated
@@ -99,41 +120,50 @@ def save_checkpoint_package( # noqa: PLR0913
metadata_payload["hf_repo_id"] = repo_id
metadata_payload["hf_package_path"] = package_path

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

mkeeler43 and others added 2 commits August 5, 2026 15:30
…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>
@mkeeler43

Copy link
Copy Markdown
Contributor Author

Thanks for this — the review caught one genuine correctness bug (the condition count) and
several things that would have quietly corrupted the leaderboard once more than one person
was writing to it. All 13 are addressed, and each has a threaded reply above.

Fixes for your comments

Correctness

Leaderboard integrity

Usability

Changes beyond your comments, and why

Four things are in this branch that you did not ask for. Flagging them so nothing in the
diff is a surprise:

  1. EngiBench must now come from git, and CI pins a commit. Not a preference — no PyPI
    release has the photonics2d/thermoelastic2d v1 problems, so on 0.2.0 those two specs
    cannot resolve at all. This is the root cause of what you reported in Add GAN 1D implementation #1. See that reply;
    it is the change most worth a second opinion.
  2. Two extra bug fixes inside made algos and metrics compatible with eval_model.py files #11. Implementing feasibility surfaced that
    check_constraints raised on thermoelastic2d (HuggingFace returns its boundary
    matrices as lists, which EngiBench's bound checks cannot compare), and that float64
    designs were judged infeasible against a float32 design space on dtype alone — which
    marked that problem's own dataset optima as violations. Both are fixed and tested here
    because viol is wrong without them. The list/array crash may deserve a fix in EngiBench
    too.
  3. A pytest job in CI. The repo ran only ruff and mypy, so the spec test you asked for
    in Add GAN 1D implementation #1 had nowhere to live. 136 tests now run on every push (111 before).
  4. Two dead helpers deleted_discover_reference_files (a near-duplicate of live code,
    no callers) and problem_id_of (public, undocumented, unused). Both were added by this
    PR, so this is removing our own dead weight rather than unrelated tidying.

Known limitation, not addressed here

thermoelastic2d models are conditioned on 3 scalars while the simulator receives all 7
conditions, because the four 65x65 boundary matrices cannot travel in a dense condition
tensor. Scoring is correct — simulate and optimize get the full conditions — but a
model cannot currently see where the part is bolted or heated unless it reads
ConditionBatch.dataset itself, as vqgan does for its own preprocessing. Giving image
conditions first-class support in the contract is worth a follow-up; it is a modelling
capability rather than a bug, and it predates this PR.

@SoheylM SoheylM left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread tests/test_specs.py Outdated
try:
spec.check_problem_definition(problem)
except ProblemDefinitionMismatchError as mismatch:
pytest.skip(str(mismatch))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread engiopt/evaluation/spec.py Outdated
Comment thread engiopt/core.py Outdated
ValueError: If the caller's condition columns differ from the
checkpoint's.
"""
if keys and self.condition_keys and tuple(keys) != self.condition_keys:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread engiopt/generators/vqgan/adapter.py Outdated
dataset = conditions.dataset
if dataset is None:
return conditions.require_tensor(self.algo_id)
columns = dataset.column_names

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 statistics normalize
    returned instead of throwing them away.
  • Generator.sample projects to the recorded keys (see C) and applies the recorded
    scaling, so _sample receives the conditions in exactly the form training used.
  • VQGANGenerator._condition_tensor is 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")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread engiopt/evaluation/leaderboard.py Outdated
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):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both correct, and the second is the more dangerous one — thank you for reproducing it.

Split into two steps, as you described:

  • repo_info failing with RepositoryNotFoundError is 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.csv in an existing repo now returns (empty, revision). The
    revision becomes the parent_commit of 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.

Comment thread engiopt/evaluation/leaderboard.py Outdated
.sort_values("value", ascending=not spec.higher_is_better)
.reset_index(drop=True)
)
grouped["rank"] = grouped.index + 1

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread engiopt/evaluate.py
remaining = [
generator
for generator in generators
if not already_evaluated(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

--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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
@mkeeler43

Copy link
Copy Markdown
Contributor Author

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
the two specs it exists to verify. My EngiBench pin was a no-op — the git build reports the
same version string as the PyPI build, so pip kept the wrong copy. I had reported this PR
as "CI green" on that basis, which was wrong. It is now a --force-reinstall with an
explicit assertion that the pin took effect, and a spec mismatch fails rather than skips.

Four of these were defects the previous round introduced (C, F, G, and A itself). Two of
the fixes made the code smaller rather than larger:

  • viol is now cheap — decoupled from the optimizer pass, so it survives an optimizer
    that rejects an invalid design, and costs a constraint check rather than a full run.
  • VQGAN's bespoke preprocessing is deleted, not patched. The contract now replays
    whatever preprocessing a checkpoint recorded, so any two-stage model gets the pathway
    without writing code for it. This also fixed a silent correctness bug: VQGAN was
    refitting its condition normalization on the 50 evaluation rows and discarding the
    training statistics, so the network saw a scale it was never trained on.

Where I would push back — B, second half only. Recording the EngiBench revision is
right and is done. Enforcing it at resolution would refuse to evaluate for any contributor
whose EngiBench differs by a commit, and putting it in ranking identity would partition the
board per build, so two models could not be compared unless scored on byte-identical
EngiBench. Traceability is what the concern justifies, and the revision is recorded in the
spec with code_version already on every row. Happy to add engibench_version to the
provenance columns as well if you want it visible per row — that is the useful half
without the cost. Say the word and I will.

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>
Comment thread engiopt/checkpoint_store.py Outdated
Comment thread engiopt/generators/cgan_1d/adapter.py
Comment thread engiopt/evaluation/leaderboard.py Outdated
@SoheylM

SoheylM commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

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 docs/checkpoint_migration_playbook.md, but the README still says there are two scripts per algorithm, still lists legacy W&B checkpoint references, and still links to the deleted playbook. run_pe_optimization.py and one Generator.resolve_checkpoint docstring retain the removed W&B source as well. Please update those references. W&B experiment tracking and dashboard documentation can remain; only the checkpoint-loading claims are obsolete.

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 --skip-existing behavior, and runtime EngiBench provenance. I am not requesting further VQGAN work, a visual leaderboard, repository-wide Ruff cleanup, or workshop-branch changes in this PR.

mkeeler43 and others added 5 commits August 10, 2026 13:56
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>
@mkeeler43

Copy link
Copy Markdown
Contributor Author

Thanks — all four of the latest round are addressed, each in its own thread. Summary here,
plus the cross-cutting items, plus three findings from verifying against the finished
checkpoint pool that you should know before re-reviewing.

The four inline items

Item Outcome
1 metrics.json inside the checkpoint hash Fixed. Also found a second instance in _discover_package_files
2 Five adapters refitting normalizers at load Fixed for all five; bounds recorded and replayed
3 Hashless rows treated as a match Took your version; legacy-row test updated
4 Runtime engibench_version, and the git -C bug Both done; the sha logic now uses PEP 610 direct_url.json first

Cross-cutting

Checkpoint migration / issue #73. The precondition you set is now met, and I checked
rather than assuming. All four architectures load from HF and score end to end:

beams2d  vqgan        mmd=0.0103  viol=0.36    (98 of 102 runs complete)
beams2d  cgan_cnn_2d  mmd=0.0277  viol=0.38

IDEALLab/engiopt-cgan-cnn-2d and -gan-cnn-2d hold 138 packages each,
-diffusion-2d-cond 139, -vqgan 102, -constrained-plvae-2d 54. So the W&B path is no
longer load-bearing for anything, and I think #73 can close with this PR rather than block
it. If you would rather keep it open until the leaderboard is actually published, that is
reasonable too — but the checkpoints themselves are there and verified.

Lint CI. Confirmed your diagnosis exactly: ruff check . reported 20 I001 on the
old head, and the job was green only because it ran --fix inside the runner. --fix is
gone and the 20 violations are committed. ruff check . is now clean at the un-fixed
setting CI uses.

Documentation. README no longer advertises W&B artifact refs or links the deleted
migration playbook; run_pe_optimization.py's argument docs and usage example are updated;
resolve_checkpoint no longer lists wandb as a source. The README also gained a
leaderboard section, which was previously undocumented — --push-to, --skip-existing,
--attach-metrics, and how an outside contributor submits a model.

DCC workshop branch. Here I would push back on the remedy rather than the observation.
The import break is real, but that branch is being overhauled separately and its notebooks
are changing substantially for IDETC, so a temporary import alias would be dead weight
added to main to serve a branch that will not use it by the time participants see it.
I would rather fix the import as part of that overhaul, and I am happy for that to be a
stated dependency of the workshop rather than of this PR.

Three findings from verifying against the pool

1. Nothing in a sweep writes the canonical path — the documented command is broken.
{problem}/seed_{seed} is what --seeds 1 resolves, and only a default-hyperparameter run
writes it. Every sweep arm sets at least one flag, so cgan_cnn_2d and gan_cnn_2d have
138 packages each and zero canonical packages. The README command fails for both.
(vqgan and diffusion_2d_cond do have them, but only because their center configuration
coincidentally equals the script defaults — luck, not design.)

Two changes: the resolution error now lists what the repo actually holds instead of a bare
"not found", and python -m engiopt.promote_checkpoint promotes an existing arm to the
canonical path rather than spending a few hundred GPU-hours retraining a model we have.

2. A correction to something I said earlier. On 2026-08-06 I reported that VQGAN's
packages held nothing but cvqgan.pth and attributed it to walltime exhaustion. That was
wrong: I sampled the Hub mid-sweep, and VQGAN uploads after each of its three stages, so a
run still training is indistinguishable from one that died. The pool finished at the
original epoch settings, 98 of 102 complete. I had staged an epoch-budget change on the
back of that diagnosis and have dropped it.

The 4 that genuinely did not finish are still the reason the completeness marker is worth
having: they hold vqgan.pth and no transformer.pth, and without package_complete they
look exactly like finished packages until you try to load one.

3. mypy in CI is passing vacuously. Not fixed here, flagging it because it is the
same shape as the two you already caught. The job installs only mypy numpy pytest, so
with torch/gymnasium/diffusers absent the ignore_missing_imports = true overrides turn
every library type into Any. Reproduced both ways:

  • CI's exact environment: Success: no issues found in 74 source files
  • Same commit with the real dependency stack: 61 errors in 10 files

All 61 predate this PR (63 on the previous head, so this branch reduces it by 2). Fixing it
means either installing the stack in that job or committing to burning down 61 pre-existing
errors, so I have deliberately left it rather than expand this PR further. Worth its own
issue.

Also worth flagging, deliberately not changed

dpp is closer to its floor than it looks. The determinant is a product of n eigenvalues
below 1, so it decays exponentially: on the two real checkpoints above it returned 1.3e-29
and 8.4e-15, and on Gaussian designs at sigma=10 it hits exactly 0.0 from around
n=200, at which point every model ties while the column still looks like a score. Two
training scripts already work around this — cgan_cnn_3d and cgan_vae both log
np.log(max(final_dpp, tiny)).

I had a log_dpp metric written and dropped it from this PR: at the n=50 the specs draw,
dpp is nonzero and ordinally valid, so nothing here is actually broken by it, and adding a
metric no spec references would have shipped dead code into an already large PR. Suggest a
spec v2 adopting it as a separate change — happy to open that if you agree.

Verification

CI on e6231bc: 201 passed, 0 skipped — counts rather than the badge, since the skip
was the failure mode last round; the four spec-resolution tests run for real. Locally:
ruff check . and ruff format --check clean without --fix, pre-commit including pyright
green, worktree clean. Every claim above about the Hub was checked against the live repos
rather than inferred.

@SoheylM SoheylM left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread engiopt/evaluation/evaluator.py Outdated
except importlib.metadata.PackageNotFoundError:
version = "unknown"
try:
sha = subprocess.run(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread README.md Outdated
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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 3 for 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.

Comment thread CONTRIBUTING_A_MODEL.md Outdated
conditions.keys # condition names, in column order
```

Most models want `require_tensor`. Reach for `dataset` only when your model

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

mkeeler43 and others added 5 commits August 11, 2026 14:40
…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>
@mkeeler43

Copy link
Copy Markdown
Contributor Author

Round 2 — all seven comments answered, plus the change that lets this board be public

Threaded reply on each of the seven; short version below, with the two that
opened into something larger called out.

Comment Fix
package_complete only checked while building the message Checked before file discovery; the old branch became unreachable and was deleted rather than left as dead defensive code
upload_folder retains stale files Deletes when the upload is the whole package, and deliberately not for a staged one — a blanket delete would have destroyed VQGAN's stage 1
Preprocessing excluded from content_hash Folded in via identity_metadata, as a denylist so a future output-affecting field cannot be silently missed
freeze_spec cannot set volume_condition / volfrac_tol Every contract field now settable; you asked for tests and I had missed them — added
code_version has the parent-repository bug Shared source_checkout_commit(...) helper, used by both sides
README command still fails Reproduced first; your 825831f6 confirmed and both commands now run verbatim
CONTRIBUTING conditions.dataset guidance + viol Rewritten; the viol half was only half-done until af6b018

Two of these were symptoms of a bigger gap. A row recorded a checkpoint
hash but never a repo, and every default pointed at IDEALLab — so an
outside contributor could neither publish weights nor produce a row anyone could
trace. Every number was an assertion nobody could falsify. Rows now carry a full
address and python -m engiopt.verify re-runs from it; nothing is ranked until a
runner reproduces it. Separately, the protocol being public means a lookup table
scores mmd=0.000 and viol=0.00 on the real beams2d spec — better than every
trained model here — so the board now measures retrieval (novelty/copy_rate)
and condition-blindness (cond_sens) and declines to rank what trips them.

Full write-up in the PR description, and the trust model in
LEADERBOARD.md.

On the earlier threads

~21 threads from 3 and 6 Aug are still open in the UI. I re-verified each
against current code rather than assuming your summary covered them, and they
are all fixed — spot-checking the ones I trusted least: sweep YAMLs deleted, the
VQGAN adapter no longer refits on the evaluation sample, hash_package_contents
covers cvqgan.pth by listing the directory, local_model_dir reaches both
from_pretrained and the CLI, the device is synchronised before the sample timer
stops, and feasibility is computed independently of optimization. The two you
flagged as hidden by skips now genuinely run: 255 collected, 255 passed, zero
skipped. Happy to bulk-resolve those, or leave them for you.

Still open, and I don't think it's mine to close

Migration scope. Ten of fourteen registered generators have no published
checkpoint. --list-generators --check-availability now reports that per
generator instead of implying all fourteen are usable, but reporting is not
choosing between migrating them, keeping a fallback, and narrowing the registry.
#73 should stay open.

I also did not publish a canonical cgan_cnn_2d package. That is a
checkpoint-publishing decision that overlaps the above, so it seemed yours.

@mkeeler43
mkeeler43 requested a review from SoheylM August 11, 2026 15:29
@SoheylM

SoheylM commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

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: ideallab-ci is currently only a value stored in verified_by. There is no workflow, schedule, queue, or specified execution environment. We should not assume any particular future infrastructure.

More importantly, the submitted artifact is not yet safe for unattended execution. Verification reaches generator_cls.build(...), after which adapters deserialize participant-controlled .pth files using torch.load; VQGAN explicitly uses weights_only=False. The revision and content hash establish which bytes were evaluated, but they do not make those bytes safe to load.

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:

  • Require safetensors for new external submissions and reject pickle-based .pth/.pt files on the public path.
  • Make EngiOpt export safetensors automatically so this does not become a manual participant step.
  • Keep reconstruction settings and preprocessing state in validated JSON.
  • Accept only registered EngiOpt adapters. New executable model code must still enter through a reviewed code PR.
  • Before loading weights, validate the configuration and safetensors header against adapter-specific expectations: tensor names, shapes, supported dtypes, file size, tensor count and total element count.
  • Pin the checkpoint repository revision and content hash, as this PR already does.
  • Provide a submission mechanism that does not require IDEALLab write access. The current --push-to IDEALLab/engiopt-leaderboard performs a direct upload, despite LEADERBOARD.md saying outside contributors need no IDEALLab access. A pending manifest through a Hugging Face Community PR (create_pr=True) would be one straightforward option.
  • Keep participant results verified=false; only the eventual official runner should publish verified scores.

Existing IDEALLab .pth checkpoints can temporarily use a separate, explicit allowlisted legacy path, with a follow-up issue to migrate them. A user-controlled repository or URL must not be able to select that path.

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 verified=false until the official runner is available, but the submitted package must be directly consumable by that runner without conversion, migration, or resubmission.

@SoheylM

SoheylM commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

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 python -m engiopt.evaluate. Could we replace it with: “Each generator provides its own training script and adapter; evaluation is handled through the shared python -m engiopt.evaluate command.”

@SoheylM

SoheylM commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

The migration playbook is being removed before #73 meets its removal criteria.

Issue #73 says docs/checkpoint_migration_playbook.md should be deleted after the historical W&B checkpoint migration is complete. This PR deletes the file while leaving #73 open, and the PR description explicitly says migration scope remains unresolved.

I do not think this PR needs to migrate every historical checkpoint. However, could we either:

  1. Restore and update the playbook for the new package layout, leaving Remove checkpoint migration playbook after W&B checkpoint migration is complete #73 open until migration is complete; or
  2. Record an explicit decision not to migrate the remaining checkpoints, update the supported-generator documentation accordingly, and close or replace Remove checkpoint migration playbook after W&B checkpoint migration is complete #73.

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 beams2d, only seven registered generators support the problem kind; at the time of this review four have published checkpoints and three compatible families are missing. Counting all fourteen makes unrelated 1D/3D generators look like failed migrations.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants