Skip to content

Re/02 registry shared core - #2

Merged
AdamCorrao merged 10 commits into
refactor/multi-enginefrom
re/02-registry-shared-core
Sep 29, 2026
Merged

AdamCorrao merged 10 commits into
refactor/multi-enginefrom
re/02-registry-shared-core

Conversation

@AdamCorrao

Copy link
Copy Markdown
Owner

re/02 — gateway registry + shared core modules (G1 gate branch)

This branch doesn't change behaviour. powderline.run(recipe, output_dir, engine=...) works exactly as before, with the same engine= names, schema 0.26.0 and outputs. The difference is that dispatch now goes through a plugin registry. The branch also adds the shared core modules that the schema branches (re/03–06) build on. G1 gate: passed (independent reviewer session, no blockers); the review's findings are fixed in this branch.

What's in it

  • Gateway registry (powderline.registry)
    • Discovers gateways lazily from the powderline.gateways entry-point group: ep.load() runs only when a gateway is first requested.
    • register_gateway() escape hatch for tests and development.
    • GatewayNotInstalledError, plus a clear "re-install" error when no entry points are found at all.
    • Contract: a frozen Gateway dataclass (A54).
  • Engine-free gateway factories: gateways/gsasii/gateway.py, topas/gateway.py, easydiff/gateway.py.
    • Schema layer (validate, capabilities): runs with no engine installed.
    • Runtime layer (run): thin wrappers over today's kicker.run / run_topas_recipe / run_easydiffraction_recipe, importing the engine lazily.
    • Version check: a real run checks the installed engine version against ENGINE_VERSION_SPEC and fails hard if it's out of range (EngineVersionError, A44). Specs: GSAS-II ==5.7.9, TOPAS >=7,<8, easydiffraction >=0.20.1,<0.21. There's a test-only bypass, allow_unsupported_engine_version (A53).
    • Missing engine: raises EngineNotAvailableError, which is also an ImportError, so existing except ImportError code keeps working (A52).
    • Uniform contract: every gateway has validate(recipe, *, verbose=False) (A50, A59).
  • Dispatcher (powderline.engine.run) maps the old engine= names 1:1 to registry names. Its public signature is unchanged.
  • symmetry promoted from topas/ to core powderline.symmetry, ending the easydiff → topas import. The move is verbatim except for one thing: the error is created by a lazy _translation_error() helper, because a top-level import would be circular. This is a temporary fix; re/03 replaces it with a core error (A56/A57).
  • New core modules, created and unit-tested only (gateways start using them in re/04–06):
    • exceptions.py: the error types plus the {code, message, field_path} warning shape (A60 confirms RecipeValidationError subclasses ValueError).
    • fitstats.py: Rwp and Rexp in percent, GoF and reduced χ² unitless. N counts only in-range points with weight > 0; P is the parameter count reported by the engine (A41, A45, A55).
    • reports.py: shared writers for the four standard report files. They reproduce all 37 committed GSAS-II references byte for byte.
  • Moved verbatim: validate + validate_simulation_mode_parameters now live in an engine-free gateways/gsasii/validation.py; kicker re-exports both (A51).
  • Packaging:
    • [project.entry-points."powderline.gateways"] added.
    • packaging>=22 declared in pyproject + pixi.toml. The pixi.lock diff is a single requires_dist line.
    • CLAUDE.md notes that pixi reinstall powderline is needed after editing entry points.
  • known_issues.md KI-13 (watch): every engine, including the default gsasii, now depends on entry-point metadata written at install time. We keep entry points as the only discovery path (A58).

Commits (10 on ae415c8)

SHA Subject
fe49fd0 feat(core): add the exceptions module and structured-warning shape
b9767a7 refactor(gsasii): move validate + validate_simulation_mode_parameters to an engine-free module (verbatim)
b3eec02 refactor(core): promote symmetry out of topas/ into powderline.symmetry
a42a8a7 feat(core): add fitstats — uniform Rwp, Rexp, GoF, reduced chi-squared
9c24109 feat(core): add reports — shared writers for the standardized report files
8d98e6d feat(core): gateway registry + engine-free gateway factories; dispatcher routed through it
4e1bc1b fix(core): route every symmetry raise through one lazy error helper (G1 KI-13)
04b1368 fix(core): fitstats rejects non-finite weights inside the fit range (G1 nit)
33f510c fix(gateways): uniform validate(recipe, *, verbose=False) contract; test hygiene (G1 nits)
a84fd39 docs(known_issues): KI-13 — engine dispatch depends on entry-point metadata (watch)

Verification

  • Default env: 498 passed / 3 skipped at ae415c8 → 637 passed / 4 skipped at a84fd39.
    • Every baseline test has the same outcome.
    • +139 new tests.
    • The +1 skip is a test that only runs when easydiffraction is installed.
  • easydiff env: the pre-existing test_easydiff_* give 54 passed, unchanged. Run with the new registry, gateway, symmetry and fitstats tests: 152 passed / 2 skipped.
  • Unchanged outputs: run(engine="gsasii") and run(engine="topas") on LaB6 produce byte-identical outputs at ae415c8 and on this branch. GSAS-II also matches the committed references within regression-tolerance.
  • Freeze: no hashed GSAS-II module touched; _code_hash.json unchanged. examples/ untouched.
  • No engine imports in core: a subprocess test blocks GSAS-II and easydiffraction, then validates through the registry for all three gateways.
  • No undefined names: a new undefined-name probe finds none across all 27 source modules changed since v0.1.1.
  • Encoding: UTF-8 + LF on every touched file.

G1 gate

An independent reviewer session gave PASS on every checklist item (evidence reproduced, not taken on trust). Findings were resolved on this branch with the maintainer (A57–A60):

  • Two raise sites in symmetry.py would have hit NameError on unreachable paths (a regression from
    An independent reviewer session gave PASS on every checklist item (evidence reproduced, not taken on trust). Findings were resolved on this branch with the maintainer (A57–A60):
  • Two raise sites in symmetry.py would have hit NameError on unreachable paths (a regression from the move). Fixed before merge.
  • The entry-point dependence is filed as KI-13 (watch).
  • Four nits fixed.

Evidence: devkit dossiers/multi-engine/tasks/re02-g1-record.md.

Obligations for later branches

  • re/03: replace the symmetry _translation_error() helper with a core error that each gateway translates into its own type (a deliberate behaviour change for the A34 table). Keep A59's validate contract.
  • re/04–06: gateways return their own schema models from validate(), adopt fitstats/reports, and supply P for fitstats.
  • re/08: env names and pins mirror each ENGINE_VERSION_SPEC; add install-health guidance for KI-13.
  • re/09: wire allow_unsupported_engine_version into the compatibility suite.

Decisions: master plan §3 A50–A60. Devkit record commits: 72a319c, c31768d, 0b8cd50.

🤖 Generated with Claude Code

AdamCorrao and others added 10 commits September 29, 2026 11:48
PowderLineError base plus RecipeValidationError, GatewayNotInstalledError,
EngineNotAvailableError (also an ImportError, A52), EngineVersionError (A44)
and EngineExecutionError; StructuredWarning = {code, message, field_path}.
Engine-free. The registry and gateways raise these from the next commits.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… to an engine-free module (verbatim)

Both functions read only the recipe (no GSAS-II references), but lived in
kicker.py, which imports GSAS-II at module top. Moving them verbatim to
gateways/gsasii/validation.py lets the gsasii gateway's schema layer validate
without GSAS-II installed (A51). kicker re-exports both, so powderline.validate
and every powderline.kicker.* import are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Moves src/powderline/topas/symmetry.py to src/powderline/symmetry.py, ending
the easydiff -> topas cross-adapter import. Importers updated (topas writer,
easydiff policy + builder, test_topas_symmetry); no shim.

One deviation from a verbatim move (A56): the module's relative
'from .errors import TopasTranslationError' cannot survive the move, and an
absolute import at module top is circular (topas/__init__ -> writer ->
symmetry). The import now happens inside the two raising functions, so the
raised type is unchanged. Interim: re/03 makes core symmetry raise a core
error that each gateway translates.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Pure-numpy standard fit statistics (A23), computed once in core. R factors
in percent, GoF/chi2_red dimensionless (A55). N = points inside the fit-range
mask with w > 0 (A55; evidence in the dossier's tasks/fitstats-n-convention.md);
P is the engine-reported varied-parameter count, supplied by each gateway
(A41/A45). Created + unit-tested only; gateways adopt it in re/04-06.

The LaB6 characterization test documents how it differs from GSAS-II's own
numbers: Rwp/GoF agree; GSAS-II's wRmin omits P; N differs by one edge point.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…files

Engine-free writers for refined_parameters.csv, <phase>_unit_cell_report.csv,
fit_profile.txt and <phase>_peak_list_report.csv: they own file naming,
column order, header, delimiter and quoting (csv QUOTE_MINIMAL, like pandas),
UTF-8 + LF; number formatting stays with the caller. Created + unit-tested
only (byte-identical round-trip of all 37 committed GSAS-II references);
gateways adopt them in re/04-06 with byte-identical checks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…her routed through it

- powderline.registry: lazy discovery of the 'powderline.gateways' entry-point
  group (ep.load on first get), register_gateway() escape hatch,
  GatewayNotInstalledError, and a clear stale-install-metadata error when the
  group is empty. Gateway = frozen dataclass (A54); check_engine_version()
  enforces the PEP 440 ENGINE_VERSION_SPEC as a hard error (A44).
- Engine-free gateway.py per gateway (gsasii, topas, easydiff): schema layer
  validate()/capabilities() runs with no engine installed (A50); runtime run()
  wraps today's kicker.run / run_topas_recipe / run_easydiffraction_recipe
  unchanged, importing the engine lazily. Real runs check the engine version
  (GSASII.__version__; configured TOPAS version, skipped when unset;
  easydiffraction distribution version) with a gateway-level
  allow_unsupported_engine_version bypass (A53); a missing engine raises
  EngineNotAvailableError (an ImportError, A52). Specs: GSAS-II ==5.7.9,
  TOPAS >=7,<8, easydiffraction >=0.20.1,<0.21.
- powderline.engine.run: old engine= names map 1:1 to registry names; public
  signature and per-engine outputs unchanged (LaB6 gsasii + topas outputs
  byte-identical to ae415c8).
- pyproject: [project.entry-points."powderline.gateways"]; packaging>=22
  declared in pyproject + pixi.toml (lock: requires_dist only). CLAUDE.md
  notes the reinstall needed after entry-point edits.
- Tests: registry (escape hatch, laziness, error texts, real entry points,
  PYTHONPATH=src mode) + gateways (specs, validate, version checks, dispatcher
  forwarding, import-block schema-only path with GSAS-II/easydiffraction
  blocked).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…G1 KI-13)

The A56 relocation re-imported TopasTranslationError lazily in only two of
the four raising functions; cell_constraints and site_dof would have raised
NameError on their (unreachable with real gemmi groups) fallthrough paths.
All six raise sites now use _translation_error(), the module's single
reference to a gateway, so re/03's switch to a core error is one line (A57).
Tests force all three defensive paths; an undefined-name probe over every
module changed since v0.1.1 reports none.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…G1 nit)

NaN > 0 is False, so a NaN weight was silently dropped from N instead of
raising as documented. Weights are now checked over the whole fit range
before the w > 0 filter.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…est hygiene (G1 nits)

- Every gateway's validate() takes the same explicit keywords (A59); topas
  and easydiffraction previously took none, so registry.get(n).validate(r,
  verbose=True) raised TypeError for them. Tests assert the signature and the
  call through the registry for all three.
- The dispatcher-routing test restores the registry via a fixture, even on
  failure (it cleaned up only on success).
- Recipe reads in test_gateways pin encoding="utf-8" (Windows, G3).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tadata (watch)

Filed by the G1 review (as KI-14; renumbered because the review's KI-13, a
re/02 regression, was fixed before merge and never entered the register).
Decision A58: entry points stay the only discovery path; re/08 adds
install-health guidance, re/10 documents the re-install step.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@AdamCorrao
AdamCorrao merged commit 5c967aa into refactor/multi-engine Sep 29, 2026
2 checks passed
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