Skip to content

Re/03 core schema - #3

Merged
AdamCorrao merged 13 commits into
refactor/multi-enginefrom
re/03-core-schema
Oct 6, 2026
Merged

AdamCorrao merged 13 commits into
refactor/multi-enginefrom
re/03-core-schema

Conversation

@AdamCorrao

@AdamCorrao AdamCorrao commented Oct 6, 2026 •

Copy link
Copy Markdown
Owner

re/03 — core schema 1.0.0, symmetry validation, version compatibility

Adds the engine-free core schema 1.0.0 (powderline.schema_core), the core
site/cell/ADP symmetry validation (powderline.symmetry), and the
declared schema-version compatibility machinery (powderline.compat).
Nothing at runtime routes through the new models yet: the gsasii/topas/
easydiffraction engine schemas adopt them in re/04–06, and the 0.26.0 path is
unchanged until re/07. Not a gate branch.

Decisions: A61–A80 (master plan §3; A60 superseded by A70). Tolerance rules
signed off by the maintainer (A69, A73, A76–A78).

What's in it

  • Core errors (A61, A62, A70): powderline.symmetry raises a core
    SymmetryError(PowderLineError, ValueError); the TOPAS writer and the
    easydiffraction policy/builder convert it to their own error types (their
    behavior is unchanged, incl. rejecting :R), so no core module references a
    gateway. RecipeValidationError removed: invalid recipes raise pydantic's
    ValidationError.
  • schema_core (A19–A21, A32, A63–A64, A68, A71–A72, A75–A80):
    • RefinableParameter [value, flag] and BoundedRefinableParameter
      [value, flag, min, max] (list-only JSON; Structure* variants allow a
      null value);
    • XRDData, FitRange (+ check_fit_range_within_data),
      ChebyshevBackground;
    • UnitCell, Atom, PhaseStructure;
    • CoreRecipe frame: schema_name, core/engine schema versions,
      metadata ≤ 1 MiB;
    • rules: extra='forbid' everywhere; exact bare element symbols (charged
      types rejected); occupancy 0–1; no volume; numbers must be JSON numbers
      (CoreFloat/CoreInt; strings and booleans rejected, A80).
  • Symmetry validation (A69, A73, A74, A77, A78):
    • Special positions: an image within 5e-6 is a special position, and
      5e-6 ≤ d < 2e-3 is an error. Accepted positions are canonicalized
      (least change): fixed coordinates become exact fractions. Each adjustment
      is reported as a structured warning.
    • Multiplicity: derived from the space group and cross-checked when
      stated.
    • Cell: must be invariant under the group within 1e-9.
    • Uij: must be site-symmetric; differences within 1e-6 Ų are adjusted
      and reported, larger ones are errors.
    • Errors: all structural problems are reported together, each at its
      field (unit_cell, atoms.<label>, .Multiplicity, .Uaniso).
    • Fix: corrected FIXED/FREE/COUPLED site-DOF classification (row/column
      test; the old test misread trigonal/hexagonal sites). It also applies to
      the legacy site_dof/adp_dof (deliberate change Re/03 core schema #3).
  • compat (A40, A65): gateways declare engine_schema_version,
    supported_engine_schemas, requires_core_schema in capabilities();
    check_engine_schema_version enforces them (never version arithmetic);
    powderline.support_matrix() lists them (engine-free).
  • Docs: docs/SCHEMA_HISTORY.md restructured into core + per-engine
    sections with the 0.21–0.26 archive kept verbatim; core 1.0.0 rules
    documented.

Behavior changes vs v0.1.1 (devkit tasks/deliberate-changes.md)

  1. Direct callers of powderline.symmetry get SymmetryError instead of
    TopasTranslationError; gateway error types are unchanged.
  2. The core resolver accepts rhombohedral :R; TOPAS and easydiffraction
    still reject it, with the same message.
  3. The 0.26.0 TOPAS writer's symmetry warnings change for some
    trigonal/hexagonal sites (corrected classification); emitted INP values
    are unchanged.

Verification

  • Freeze held: nothing under gateways/gsasii/, kicker.py,
    _code_hash.json or examples/; the code-hash test is green at every
    commit.
  • GSAS-II characterization: SytSym/GetCSxinel/GetCSuinel vs core for
    all 230 space groups. Exhaustive probe: 14368 points, 0 disagreements.
    Reinstating the old classification fails 13 groups.
  • Independent reviewer probe: all 564 gemmi settings, 0 disagreements.
  • Suites, compared by test ID: default env 1311 passed / 4 skipped
    (baseline 637p/4s; only the 6 deliberate baseline IDs changed); easydiff
    env 55 passed.
  • Invariants: undefined-name probe 0; the engine import-block test covers
    schema_core, compat and support_matrix(); touched files are UTF-8/LF.
  • Not verifiable in the sandbox: the -W docs build (intersphinx
    downloads blocked).

Review

An independent Opus 5.5 review found no blockers. Its findings (S1 strict
numbers, S2 complete per-atom reporting, F1 per-field error locations, nits
N1–N10) are all fixed in the last four commits (8f7991f, f964558,
b5cea07, 9ad114c). No known issues filed. Record: devkit
dossiers/multi-engine/tasks/re03-review-record.md.

For later branches

  • re/04–06: engine schemas build on CoreRecipe + PhaseStructure, use
    CoreFloat/CoreInt, declare the A65 capability keys, and surface
    PhaseStructure.warnings().
  • re/04: gsasii rejects an explicit :1 origin.
  • re/05: the TOPAS writer emits = p/q; for fixed special coordinates and
    decides how to tie Uij.
  • re/06: map Uaniso for easydiffraction.
  • re/07: decide ionic scattering types; correct the DRX_33 C2/m
    multiplicities.

Assisted-by: claude-code:claude-opus-5-5
Assisted-by: claude-code:claude-sonnet-4-5
Assisted-by: claude-code:claude-haiku-4-5

AdamCorrao and others added 13 commits September 29, 2026 17:06
…t it (A61, A62)

Closes the A56/A57 obligation: powderline.symmetry no longer references a
gateway. The _translation_error() helper is gone; every raise is
powderline.exceptions.SymmetryError(PowderLineError, ValueError).

- TOPAS writer: one _symmetry() helper wraps its three symmetry calls,
  converts SymmetryError to TopasTranslationError (chained) and applies
  TOPAS's own rejection of the rhombohedral :R setting (same message).
- easydiffraction policy/builder catch SymmetryError instead of
  TopasTranslationError (no longer importing the TOPAS gateway); the
  policy rejects :R itself.
- Core resolve_space_group/site_dof/adp_dof accept :R (A62);
  cell_constraints refuses :R explicitly (its tied angles are not
  expressible as CellRules).

Gateway-raised error types are unchanged. Direct callers of
powderline.symmetry see SymmetryError, and :R is accepted by the
resolver. Both are recorded in the devkit deliberate-changes table (A34).
Test IDs relabelled: three test_topas_symmetry *_raises_translation_error
tests became *_raises_symmetry_error; the :R test was split in two.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ime until re/04)

- Parameter models (A20, A32, A71, A72): RefinableParameter [value, flag] and
  BoundedRefinableParameter [value, flag, min, max]. A 4-list where bounds are
  unsupported is an error naming the field. The Structure* variants allow
  value null (start from the phase structure's value). Strict boolean flags;
  list-only JSON shape; the JSON Schema describes the arrays.
- XRDData: the 0.26.0 validators ported verbatim; unit metadata (A19).
- FitRange [min, max]: max > min, finite; check_fit_range_within_data for
  engine payloads (A68). ChebyshevBackground ported, strict flag.
- CoreRecipe frame: schema_name "<engine>.<workflow>", core/engine schema
  versions (core checked against SUPPORTED_CORE_SCHEMAS, A40), metadata
  JSON-serializable and capped at METADATA_MAX_BYTES = 1 MiB (A64).
- check_element_symbol: a bare symbol spelled exactly as in gemmi's table;
  charged types rejected, never stripped (A63).
- extra='forbid' everywhere (A21).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ort_matrix() (A40, A65)

Gateways declare engine_schema_version / supported_engine_schemas /
requires_core_schema (a specifier, or a mapping per engine-schema range) as
capabilities() keys. check_engine_schema_version raises ValueError so that
engine schema models can call it from a validator. support_matrix() walks
the registry (engine-free); the in-repo gateways are undeclared until
re/04-06. powderline.support_matrix is re-exported, outside __all__. The
import-block test now imports schema_core and compat and calls
support_matrix() with GSAS-II and easydiffraction blocked.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…antic's ValidationError (A70)

All recipe checks live in the schema models, so pydantic's own
ValidationError reports every problem with its location; a wrapper only
duplicated it (and would have broken except-ValidationError callers,
superseding A60). The class was unused outside its unit test. A new test
shows a SymmetryError raised in a validator joining the aggregated error.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… a 0.21-0.26 archive (A37)

Core 1.0.0 is documented as in development and not yet used. The engine
sections are placeholders for re/04-06. The existing history is kept
verbatim below, with headings moved one level down. A banner states that
the runtime still validates schema 0.26.0 until re/07.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ct site DOF classification

analyze_site(space_group, xyz) is the core schema's site validation (KI-03):
- the space group must name its setting (two-origin :1/:2, rhombohedral
  :H/:R; A62);
- symmetry images closer than 5e-6 put the operation in the stabilizer
  (>= 6 decimals declare a special position);
- an image between 5e-6 and 2e-3 raises SymmetryError naming the exact
  special position and its multiplicity (ambiguous band);
- canonical coordinates: the barycenter of the stabilizer images, with fixed
  axes set to their exact fraction (denominator <= 48); `adjusted` reports
  a difference above 1e-12;
- multiplicity = |G| / |stabilizer|, with consistency checks; coordinate
  and Uij DOF classification.

DOF classification fix (P1): FIXED is now a row test on the stabilizer
projector, FREE a column test. The old column-only test misread trigonal and
hexagonal sites, whose rotations are not orthogonal in fractional
coordinates (e.g. 6h (x, 2x, 1/4) gave x FIXED). The legacy site_dof/adp_dof
(0.26.0 TOPAS writer warnings) share the corrected helper. Recorded as a
deliberate change.

tests/test_symmetry_characterization.py compares multiplicity, coordinate
DOF and Uij DOF with GSAS-II's SytSym/GetCSxinel/GetCSuinel for all 230
space groups (GSAS-II symbol verified per group by matching the orbits),
plus canonicalization, ambiguous-band and committed-example checks. With the
old column test reinstated, 13 trigonal/hexagonal groups fail. Skipped
without GSAS-II.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…on (A77, A78)

- check_cell(space_group, cell): the metric tensor must be invariant under
  every group rotation; compared with the group-averaged cell within
  CELL_TOL = 1e-9 (float noise only). Errors name each parameter and its
  symmetric value. Covers all crystal systems incl. monoclinic unique axes
  and rhombohedral :R.
- check_uij(space_group, xyz, uij): U must be invariant under the site
  stabilizer (R U R^T, the GSAS-II convention). Within UIJ_TOL = 1e-6 A^2
  the symmetric values are returned with adjusted=True; beyond that,
  SymmetryError.
- Canonical coordinates and symmetric Uij use the least-change
  (orthogonal) projection onto the symmetric subspace, anchored at the
  stabilizer barycenter. The Reynolds average alone is oblique on
  hexagonal/trigonal axes and moved free components (e.g. U11 on 6h).
  Changes <= 1e-12 snap back to the stated value; SiteAnalysis gains `adp`.
- tests/test_symmetry_core.py (core-only, no GSAS-II); the characterization
  canonicalization tests now assert exact values (the previous np.allclose
  silently allowed rtol=1e-5) and the coupled least-change result.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…re (A35, A62-A63, A69, A73-A78)

PhaseStructure validates the structure once, in core:
- explicit space-group setting (A62);
- cell lengths > 0, angles in (0, 180), no volume (A75), cell consistent
  with the group (A77);
- exact bare element symbols (A63);
- occupancy 0..1 (A76);
- ADP 'Uiso' with Uiso or 'Uaniso' with all six Uij, Uij consistent with
  the site symmetry, adjusted within 1e-6 A^2 (A78);
- per-atom analyze_site: ambiguous band = error, stated Multiplicity
  cross-checked (A69), special-position coordinates replaced by their exact
  canonical values (A73).
Every adjustment becomes a StructuredWarning (special_position_adjusted /
adp_symmetry_adjusted, field_path relative to the structure), available
from warnings(). The site analyses are available from site(label). All atom
problems are reported together in one error.

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

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

Review of PR #3 (S2, F1, N2):
- All of an atom's problems are reported: a wrong Multiplicity no longer
  skips that atom's Uaniso check.
- Each problem is a separate pydantic error at its field: unit_cell,
  atoms.<label> (position), atoms.<label>.Multiplicity,
  atoms.<label>.Uaniso, raised together as one ValidationError
  (ValidationError.from_exception_data). Locations get the enclosing path
  when PhaseStructure is embedded in an engine model, also from JSON.
  The atom label is now in the location, not repeated in the message.
- Canonicalization no longer modifies a caller-supplied Atom: adjusted
  atoms are copies (model_copy).
- Docstring + SCHEMA_HISTORY: problems are reported per field, once the
  individual fields are valid (pydantic skips model validators after a
  field error).

Tests: one atom with two problems; cell + position + multiplicity
locations; nested JSON locations; caller Atom untouched.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… band message, compat input check

Review of PR #3 (N1, N4-N8, N10):
- check_element_symbol suggests a spelling only for case/whitespace
  differences (' Fe' -> 'Fe'); gemmi's prefix reading ('Nax' -> Na) is no
  longer offered as a fix (N1).
- ChebyshevBackground rejects non-finite coefficients, like every other
  numeric core field (N7).
- analyze_site's ambiguous-band error names the special position and its
  multiplicity only when the nearby operations form a group (new
  _is_group); otherwise it says "a special position" instead of printing a
  non-multiplicity (N10).
- compat.required_core_schemas raises ValueError (not AttributeError) for a
  requires_core_schema that is neither a specifier nor a mapping;
  support_matrix() documents that malformed declarations raise (N8).
- Docstrings: UnitCell cites A75 (N4); oblique projector singular values
  (N5); topas/errors.py no longer says symmetry raises it (N6).

Tests: element hints; non-finite coefficients; _is_group; band message;
compat non-mapping. The compat first-match test now has two matching keys
in both orders (it could not tell first-wins from highest-bound-wins); the
misleadingly named picks_highest_version_match is replaced by
skips_non_matching_keys.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ema/symmetry tests

Review of PR #3 (N3):
- The 11 bare pytest.raises(ValidationError) in test_schema_core (strict
  flags, null values) now assert the exact (location, type) of every
  error, so they cannot pass on the wrong field.
- The cubic b != a test asserted '"b" in msg' (always true); it now checks
  "b = 4.2 (symmetric value" and the unit_cell location.
- "Returned unchanged" checks use == instead of abs(...) < 1e-15
  (check_uij, canonical 1/3 and 2/3, free U11 on 6h); the code returns
  exact values.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…c core fields (A80)

Review of PR #3 (S1). pydantic's default lax mode read "0.25" and true as
numbers (x: true -> 1.0, Multiplicity: true -> 1), against A73 and the
generated JSON Schema. Every numeric core field now uses one of two types,
exported for the engine schemas:
- CoreFloat = Annotated[float, Strict()]: any number (int, float, numpy
  scalar); a string, bytes or a boolean is a float_type error;
- CoreInt: a whole number (4 or 4.0, as JSON Schema's integer, numpy ints
  included); a string, bytes or a boolean is an int_type error, 4.5 an
  int_from_float error.
Covers parameter values and bounds, FitRange, XRDData arrays, Chebyshev
coefficients and num_coefficients, the unit cell, atom coordinates,
occupancy, Uiso, Uaniso and Multiplicity. Values that were accepted as
numbers keep their value; the JSON Schema is unchanged. metadata stays
free-form (A21). The legacy 0.26.0 schema is untouched.

Tests: every numeric field rejects "0.5", "1", true, false and accepts
int / numpy float / numpy int as a float; whole-number rules for both
integer fields; quoted numbers rejected from JSON. SCHEMA_HISTORY states
the rule.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@AdamCorrao
AdamCorrao merged commit 4b61201 into refactor/multi-engine Oct 6, 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