Skip to content

Give each engine's element set one source - #167

Merged
isayev merged 1 commit into
mainfrom
refactor/one-element-set
Aug 17, 2026
Merged

isayev merged 1 commit into
mainfrom
refactor/one-element-set

Conversation

@isayev

@isayev isayev commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

The problem

The ANI element set was written five times across three layers, all hand-maintained, with nothing connecting them:

Site Form What it does
models/policy.py frozenset({1, 6, 7, 8, 9, 16, 17}) the gate — raises ConfigurationError
models/species.py keys of ANI2XT_INDEX the remap — raises ValueError
models/species.py "(supported: H, C, N, O, F, S, Cl)" that remap's message
cli/commands/models.py "elements": "H, C, N, O, F, S, Cl" ×2 what auto3d models info prints

All five agreed — by hand. That is the same arrangement the registry replaced for engine names: correct until someone edits one of them. And auto3d models info is precisely where a user checks which elements an engine accepts before choosing it, so a stale copy there misinforms the decision it exists to support.

The change

format_elements renders an element set as symbols ordered by atomic number. That order is not a new convention — it is the order all six existing strings already used, so no user-visible output changes. That is what makes the string tests behavior locks rather than churn.

It lives in species.py with rdkit imported inside the function body, preserving that module's documented lazy-rdkit property and keeping the dependency policy → species with no sibling cycle.

The gate and the remap are connected by an import-time assert, not by derivation. They are the same seven numbers but not the same fact: ANI_ELEMENTS is what ANI2x and ANI2xt were trained on; ANI2XT_INDEX is one engine's 0-based network index order. Defining either in terms of the other would record a provenance that is not true, and would quietly stop being a check. Same construction as model_factory's BUILTIN_ANI_MODELS assert.

AIMNet2 is handled differently, because the knowledge isn't Auto3D's

Each model file declares implemented_species, and AIMNet2Calculator enforces it at call time. Auto3D doesn't define these sets — it only quotes them. So the four literals stay literals (deriving them would mean loading four NNPs to print a help table), and a new slow-tier test pins them to the metadata instead.

They are correct today: all four match, including aimnet2-pd's As → Pd substitution.

Wave 6's third bullet, disposed of rather than dropped

energy_unit needs no adapter member. utils/energy.py already owns it completely — E_tot is Hartree on disk, models produce eV, one module converts, and the adapter contract's docstrings state eV at every boundary.

Recorded, not acted on

All four aimnet registry models report supports_charged_systems = None, so aimnet does not enforce a charge restriction for them and Auto3D's own _requires_aimnet charge test is the only charge guard on that path. Existing, correct behavior — noted so it isn't rediscovered as a surprise.

Not touched: ModelAdapter

check_engine_supports_molecules runs before any model is constructed, from callers holding only an engine name — so "ask the adapter" is the same chicken-and-egg deliberately left behind in D4. And for AIMNet2 there is nothing to ask, since the calculator validates itself.

Verification

  • 1766 passed, 1 skipped, 74 deselected (+5 fast, +4 slow), randomized order.
  • The 4 slow tests pass locally on CPU in 33s, so CI's slow tier pays seconds, not a download.
  • mypy unchanged: 68 errors in 21 files, still "checked 72 source files". No new error in any touched file.
  • Mutation-tested both guards. Widening ANI_ELEMENTS to include bromine without touching the remap fails the import-time assert, naming [35]. Sorting the renderer by symbol instead of by atomic number fails three string tests, including the two that pin ENGINE_INFO.

The ANI element set was written five times across three layers, all
hand-maintained, with nothing connecting them: the numeric frozenset in
`models/policy.py` (the gate that raises ConfigurationError), the keys of
`ANI2XT_INDEX` in `models/species.py` (the remap that raises ValueError),
the symbol string in that remap's error message, and two more copies of
that string in the CLI's `ENGINE_INFO`. All five agreed. They agreed by
hand, which is the same arrangement the registry replaced for engine
*names* -- correct until someone edits one of them, and `auto3d models
info` is exactly where a user checks which elements an engine accepts
before choosing it.

`format_elements` renders an element set as symbols ordered by atomic
number. That order is not a new convention: it is the order all six
existing strings already used, so no user-visible output changes -- which
is what makes the string tests behavior locks rather than churn. It lives
in `species.py` with rdkit imported inside the function body, preserving
that module's documented lazy-rdkit property, and so that the dependency
runs policy -> species with no sibling cycle.

The gate and the remap are now connected by an import-time assert rather
than by derivation. They are the same seven numbers but not the same fact:
`ANI_ELEMENTS` is what ANI2x AND ANI2xt were trained on, `ANI2XT_INDEX` is
one engine's 0-based network index order. Defining either in terms of the
other would record a provenance that is not true and would stop being a
check. Same construction as model_factory's BUILTIN_ANI_MODELS assert.

AIMNet2 is handled differently because the knowledge is not Auto3D's. Each
model file declares `implemented_species`, and `AIMNet2Calculator` enforces
it at call time -- so Auto3D does not define these sets, it only quotes
them. The four literals therefore stay literals (deriving them would mean
loading four NNPs to print a help table) and a new slow-tier test pins them
to the metadata instead. They are correct today: all four match, including
aimnet2-pd's As -> Pd substitution.

Wave 6's third bullet, `energy_unit`, is disposed of rather than dropped:
`utils/energy.py` already owns it completely -- E_tot is Hartree on disk,
models produce eV, one module converts, and the adapter contract's
docstrings state eV at every boundary. No adapter member is needed.

Also recorded, not acted on: all four aimnet registry models report
`supports_charged_systems = None`, so aimnet does not enforce a charge
restriction for them and Auto3D's own `_requires_aimnet` charge test is the
only charge guard on that path. That is existing, correct behavior.

Nothing here touches `ModelAdapter`. `check_engine_supports_molecules` runs
before any model is constructed, from callers holding only an engine name,
so "ask the adapter" is the same chicken-and-egg left behind in D4 -- and
for AIMNet2 there is nothing to ask, since the calculator validates itself.

Verification:
- 1766 passed, 1 skipped, 74 deselected (+5 fast, +4 slow), randomized order.
- The 4 slow tests pass locally on CPU in 33s, so CI's slow tier pays
  seconds, not a download.
- mypy unchanged: 68 errors in 21 files, "checked 72 source files". No new
  error in any touched file.
- Mutation-tested both guards. Widening `ANI_ELEMENTS` to include bromine
  without touching the remap fails the import-time assert, naming [35].
  Sorting the renderer by symbol instead of by atomic number fails three
  string tests, including the two that pin `ENGINE_INFO`.
@isayev
isayev merged commit b5c40b2 into main Aug 17, 2026
8 checks passed
@isayev
isayev deleted the refactor/one-element-set branch August 17, 2026 16:19
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.

1 participant