Skip to content

Split ASE/thermo.py into a package, one module per concern - #172

Merged
isayev merged 1 commit into
mainfrom
refactor/split-thermo
Aug 19, 2026
Merged

isayev merged 1 commit into
mainfrom
refactor/split-thermo

Conversation

@isayev

@isayev isayev commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

What

1895 lines became four modules under Auto3D/ASE/thermo/:

module contents
properties molecular inspection — geometry class, symmetry number, multiplicity, display name. No model, no calculator.
calculator the ASE calculator fronting an Auto3D model, and the conversions into it
vibrations Hessian, Eckart/Sayvetz projection, frequencies
driver the per-record sequence and the run over a file

Dependency order is properties, calculator ← vibrations ← driver, read off the import graph rather than asserted. Each of the 33 top-level statements was compared by ast.dump against its original: byte-identical, none lost, none invented.

A package, not four sibling modules — and not for tidiness

get_logger(__name__) now yields Auto3D.ASE.thermo.properties and friends, which are children of Auto3D.ASE.thermo. Records propagate, so the fifteen-odd tests capturing on logger="Auto3D.ASE.thermo" keep seeing warnings raised anywhere in the four.

Siblings named thermo_properties would have broken every one of them — and broken them silently, because a log-capture assertion that stops seeing records fails by never firing. Demonstrated directly rather than argued: the child propagates, the sibling does not.

__init__.py re-exports calc_thermo alone, permitted by the package-barrel rule for exactly the reason isomers already records: api.rst documents Auto3D.ASE.thermo.calc_thermo at that package path.

The bug this split creates, and the test that didn't exist

_symmetry_default_warned de-duplicates _symmetry_number's sigma=1 warning to once per run, and calc_thermo clears it at the top of each run. The flag lives with _symmetry_number in properties now — so global _symmetry_default_warned in driver would bind a name in driver that _symmetry_number never reads. The reset silently stops working and the warning fires once per process instead of once per run. The assignment goes through the module object instead.

Mutation-testing is what found the real gap. The naive global version passed the entire suite: every existing test of this warning sets the flag itself, so they pass either way, and the per-run reset was never covered at all. test_calc_thermo_resets_the_symmetry_warn_once_flag_in_properties is the missing guard, and the mutation now fails it with a message naming the consequence.

One more consequence worth knowing

create_model is now imported into two modules, so calc_thermo reaches it twice — via _load_hessian_model (driver) and via model_name2model_calculator (calculator). A single monkeypatch used to cover both. One test measuring the ASE relaxation was silently loading a real model until both bindings were patched.

The test repointing was mechanical but not blind

Every misrouted monkeypatch located a name exactly where this split says it belongs — Atoms and create_model with the calculator that constructs them, VibrationsData and _ASE_HAS_VIB_SELECTION with the Hessian code, _electron_count and the warn-once flag with the molecular properties. The tests were an independent check on the grouping, and they agreed with it.

Docs

docs/source/migration-3.0.rst names projected_vibrations by dotted path in a live description of current behavior; updated to its new module. It is the only doc reference to a moved name — api.rst points at calc_thermo, which did not move.

Verification

  • 1771 passed, 1 skipped, 74 deselected (+1 new guard), randomized order.
  • mypy: 68 errors, unchanged. 23 files instead of 22 and 79 checked instead of 75, because one file became four plus an __init__.
  • All 33 moved statements AST-identical to their originals.
  • Mutation-tested both hazards: the naive global reset fails the new test, and the logger-hierarchy property the package decision rests on is demonstrated rather than assumed.

1895 lines became four modules under `Auto3D/ASE/thermo/`:

    properties   molecular inspection -- geometry class, symmetry number,
                 multiplicity, display name. No model, no calculator.
    calculator   the ASE calculator fronting an Auto3D model, and the
                 conversions into it.
    vibrations   Hessian, Eckart/Sayvetz projection, frequencies.
    driver       the per-record sequence and the run over a file.

Dependency order is properties, calculator <- vibrations <- driver, read off
the import graph rather than asserted. Each of the 33 top-level statements
was compared by `ast.dump` against its original: byte-identical, none lost,
none invented.

A PACKAGE, not four sibling modules, and the reason is not tidiness.
`get_logger(__name__)` now yields `Auto3D.ASE.thermo.properties` and
friends, which are *children* of `Auto3D.ASE.thermo`, so records propagate
and the fifteen-odd tests that capture on `logger="Auto3D.ASE.thermo"` keep
seeing warnings raised anywhere in the four. Siblings named
`thermo_properties` would have broken every one of them -- and broken them
silently, because a log-capture assertion that stops seeing records fails by
never firing. Demonstrated directly rather than argued: the child
propagates, the sibling does not.

`__init__.py` re-exports `calc_thermo` alone. That is permitted by the
package-barrel rule for exactly one reason, the same one `isomers` records:
api.rst documents `Auto3D.ASE.thermo.calc_thermo` at that package path.
`SUBPACKAGES_WITH_ALL` gains `thermo` accordingly.

THE BUG THIS SPLIT CREATES, and the test that did not exist:

`_symmetry_default_warned` de-duplicates `_symmetry_number`'s sigma=1
warning to once per run, and `calc_thermo` clears it at the top of each run.
The flag lives with `_symmetry_number` in `properties` now, so `global
_symmetry_default_warned` in `driver` would bind a name *in driver* that
`_symmetry_number` never reads: the reset silently stops working and the
warning fires once per process instead of once per run. The assignment goes
through the module object instead.

Mutation-testing that found the real gap. The naive `global` version passed
the entire suite, because every existing test of this warning sets the flag
itself and so passes either way -- the per-run reset was never covered at
all. `test_calc_thermo_resets_the_symmetry_warn_once_flag_in_properties` is
the missing guard; the mutation now fails it with a message naming the
consequence.

One more consequence worth knowing: `create_model` is now imported into two
modules, so `calc_thermo` reaches it twice -- via `_load_hessian_model`
(driver) and via `model_name2model_calculator` (calculator). A single
monkeypatch used to cover both. One test measuring the ASE relaxation was
silently loading a real model until both bindings were patched.

Test repointing was mechanical but not blind: every misrouted monkeypatch
located a name exactly where this split says it belongs -- `Atoms` and
`create_model` with the calculator that constructs them, `VibrationsData`
and `_ASE_HAS_VIB_SELECTION` with the Hessian code, `_electron_count` and
the warn-once flag with the molecular properties. The tests were an
independent check on the grouping and agreed with it.

`docs/source/migration-3.0.rst` names `projected_vibrations` by dotted path
in a live description of current behavior; updated to its new module. It is
the only doc reference to a moved name -- api.rst points at `calc_thermo`,
which did not move.

Verification:
- 1771 passed, 1 skipped, 74 deselected (+1 new guard), randomized order.
- mypy: 68 errors, unchanged; 23 files instead of 22 and 79 checked instead
  of 75, because one file became four plus an __init__.
- All 33 moved statements AST-identical to their originals.
- Mutation-tested both hazards: the naive `global` reset fails the new test,
  and the logger-hierarchy property the package decision rests on is
  demonstrated rather than assumed.
@isayev
isayev merged commit b402dbe into main Aug 19, 2026
8 checks passed
@isayev
isayev deleted the refactor/split-thermo branch August 19, 2026 22:38
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