Skip to content

Re/04 gsasii schema - #5

Merged
AdamCorrao merged 12 commits into
refactor/multi-enginefrom
re/04-gsasii-schema
Oct 8, 2026
Merged

AdamCorrao merged 12 commits into
refactor/multi-enginefrom
re/04-gsasii-schema

Conversation

@AdamCorrao

Copy link
Copy Markdown
Owner

re/04: native GSAS-II schemas (gsasii.rietveld, gsasii.spf) and gateway adoption

Merges re/04-gsasii-schema (7 commits on 2810c82, tip 65b95ca) into
refactor/multi-engine. Not a gate branch. Plan: devkit
dossiers/multi-engine/subplans/re-04-gsasii-schema.md; decisions A81–A92,
A93–A123 as they apply to gsasii, A124, A125 (master plan §3).

What this does

  • Native gsasii engine schema 1.0.0 (gateways/gsasii/schema.py, engine-free), built on core 1.0.0:
    • one phase block per phase (core Phase[RefinableParameter] plus scale and peak_broadening);
    • a single-source instrument in blocks (radiation, geometry, corrections, broadening);
    • every refinable is [value, refine_flag], with no bounds (A84).
  • The gateway runs native recipes.
    • run_refinement consumes the validated model (A39). The input-side edits to frozen code are ledger rows R1–R14, all signed in devkit tasks/re04-frozen-edits.md.
    • GSASII_* 0.26.0 recipes still run, through a verbatim legacy copy of every touched function (_legacy_executor.py, generated from 2810c82). The 0.26.0 path runs only unedited code until re/07.
  • Converter scripts/convert_recipe_026.py (0.26.0 → gsasii 1.0.0). It states explicitly what v0.1.1 refined and reports every change. Anything the new schema rejects stops the conversion with every problem listed; nothing is fixed silently.
  • Results on the native path:
    • standard rwp, r_exp, gof, chi2_red from core fitstats (A125);
    • GSAS-II's own values under engine_details, with parameters_requested and parameters_varied;
    • structured warnings: the documented defaults applied, and GSAS-II's refinement messages.

A recipe, before and after (LaB6, abridged)

0.26.0 (GSASII_Rietveld): the structure and parameterization are separate blocks, and the instrument has two sources:

"instrument": {
  "initialization": [{"Lam": [0.1665, 0.1665, false], "SH/L": [0.0005, 0.0005, false],
                      "U": [18.717, 18.717, false], "Zero": [0.0, 0.0, false], /* + 9 more GSAS-II keys */}, {}],
  "parameterization": {"wavelength": [0.1665, false, null, null],
                       "broadening": {"U": [null, true, null, null], /* V..Z alike */},
                       "corrections": {"zero_shift": null, "axial_divergence": null}}
},
"phases": {"LaB6": {
  "structure": {"phase_name": "LaB6", "space_group": "P m -3 m",
                "unit_cell": {"a": 4.15682, /* ... */ "volume": 71.826},
                "atoms": {"B": {"element": "B", "x": 0.5, "y": 0.5, "z": 0.2021, "occupancy": 1.0,
                                "Multiplicity": 6, "ADP": "Uiso", "Uiso": 0.009}, /* La */}},
  "parameterization": {"scale": [1, true, null, null],
                       "unit_cell": {"a": [null, false, null, null], /* ... */},
                       "atoms": {"B": {"x": [null, false, null, null], /* ... */ "ADP": "Uiso"}},
                       "peak_broadening": {"size_broadening": {"model": "isotropic", "isotropic_size": null, "LG_eta": null}, /* strain */}}
}},
"background": {"single_peaks": {"pv_gaussian_sigma": [[1000, true, null, null]], /* ... */}}

gsasii 1.0.0 (gsasii.rietveld, produced by the converter): one phase block, one instrument, every value explicit:

"schema_name": "gsasii.rietveld", "core_schema_version": "1.0.0", "engine_schema_version": "1.0.0",
"instrument": {
  "description": "Pair distribution function (PDF) beamline (port 28-ID-1) at NSLS-II, BNL",
  "radiation":   {"type": "PXC", "wavelength": [0.1665, false]},
  "geometry":    {"bank": 1, "azimuth": 0.0},
  "corrections": {"zero_shift": [0.0, false], "polarization": [0.99, false], "axial_divergence": [0.002, false]},
  "broadening":  {"U": [18.717, true], "V": [0.6003, true], /* W, X, Y, Z */}
},
"phases": {"LaB6": {
  "space_group": "P m -3 m",
  "unit_cell": {"a": [4.15682, false], "b": [4.15682, false], "c": [4.15682, false],
                "alpha": [90.0, false], "beta": [90.0, false], "gamma": [90.0, false]},
  "atoms": {"B":  {"element": "B", "x": [0.5, false], "y": [0.5, false], "z": [0.2021, false],
                   "occupancy": [1.0, false], "Multiplicity": 6, "ADP": "Uiso", "Uiso": [0.009, false]},
            "La": {/* ... */}},
  "scale": [1, true]
  /* peak_broadening left out: 10 µm / 0 microstrain, fixed, with a warning (A107) */
}},
"background": {"chebyshev": {"num_coefficients": 6, "coefficients": [10, 5, 1, 0, 0, 0], "refine_flag": true},
               "single_peaks": {"pv_gaussian_sigma_sq": [[1000, true]], /* positions, intensities, gamma */}}

The converter's report for it:

payload.xrd_data.UID: moved to metadata.xrd_data.UID
payload.instrument.corrections.axial_divergence: SH/L 0.0005 -> 0.002 (GSAS-II computed with 0.002 anyway, EB-37; A91, deliberate change)
payload.background.single_peaks: pv_gaussian_sigma renamed pv_gaussian_sigma_sq (the value always was σ², A89)
payload.phases.LaB6.unit_cell.volume: dropped (derived from a..gamma, A75)
payload.phases.LaB6.peak_broadening.size_broadening: left out: the documented default 10 um applies, where v0.1.1 used GSAS-II's 1 um (A87, A107; deliberate change)
payload.phases.LaB6.peak_broadening.strain_broadening: left out: the documented default 0 applies, where v0.1.1 used GSAS-II's 1000 microstrain (A87, A107; deliberate change)

(The committed LaB6 example itself stops in the converter: its background peak
has γ = 1e-5, below GSAS-II's silent floor of 0.1 (A86, EB-35). The example
above raises it to 0.1 first. The example is fixed at re/07.)

Commits

Commit Content
e2481db gsasii schemas; schema_core.collect_warnings; validate() routes gsasii.* recipes; schema tests (77) + GSAS-II proof tests (1054)
809c329 verbatim legacy executor (generated, not edited)
ba068d2 ledger R1–R10: recipe reads in run_refinement, setters, project; routing (gateways/gsasii/routing.py)
c936153 0.26.0 → gsasii converter + 43 tests
38d6007 legacy copy extended for batch 2 (21 functions + SCHEMA_EXECUTORS)
b8923dd R13: GSAS-II site check after phase setup (A117); core symmetry.orbit
65b95ca R11 fit statistics / engine details / warnings, R12 peak-fit error (EB-38), R14 core report writers (native only, A83); kicker.run warnings and validate_only summary; server response fields; SCHEMA_HISTORY gsasii 1.0.0

Rules the gsasii schema enforces

  • Space group: the canonical gemmi name, translated to GSAS-II's symbol. Refused settings:
    • origin choice 1 (EB-03);
    • 9 settings GSAS-II cannot express: 7 it doesn't parse, and 2 (P 21 n m, A b a m) it reads as another setting (EB-41).
    • Proven by a general-position orbit test over all 564 canonical names.
  • Cell refinement: refining part of one of GSAS-II's coarser cell groups is an error (monoclinic {a, c, β}; triclinic and :R: all six). Matches GSAS-II's own cellVary dispatch for all 522 settings (A95).
  • GSAS-II floors: SH/L ≥ 0.002 (A91); background-peak floors (A86) and Peak List floors (A90) are errors.
  • Simulation: every flag must be false (A88).
  • Other: scale ≥ 0 and required; phase names and atom labels unique ignoring case (A112).
  • Documented defaults (A119):
    • size/strain left out → 10 µm / 0, with a warning (A87, A107);
    • background left out → none (one fixed 0.0 term), instead of GSAS-II's constant 1.0.
  • At run time (A117): GSAS-II misreads some 2-fold sites in R32, R-3m and R-3c. That gives 4× the intensity with nothing refined (EB-40, proven with two equivalent orientations).
    • Such a site is an error that names an equivalent position GSAS-II reads correctly.
    • So is a site GSAS-II can't name ('sp', EB-05), but only when a coordinate or Uaniso is refined.

Evidence

  • Default suite: 2731 passed, 4 skipped (baseline at 2810c82: 1518 passed, 4 skipped). Every baseline test passes; the 21 test_run_refinement_missing_keys tests now run as [legacy] and [native].
  • easydiff env: 55 passed, same IDs as the baseline.
  • Equivalence (devkit probes/re04_equivalence_probe.py): 8 examples run on both paths, each 0.26.0 recipe edited only to state the new defaults. Results:
    • identical GSAS-II Rwp, y_calc (maximum difference 0.0) and refined values;
    • every report file byte-identical between the paths;
    • the Iparm1 built from the instrument blocks equals each example's.
    • The examples: LaB6, LaB6_simulation, LaB6_singlepeakfit, and the DRX_33 variants (base, atomrefine with 39 refined parameters, anisoADP, strainonly, simulation).
  • Native runs (tests/test_gsasii_native_run.py):
    • standard statistics vs GSAS-II's (LaB6: 6.5345 vs 6.5346);
    • refined zero_shift, polarization and axial_divergence are actually varied (A103);
    • the site-check errors, and the SPF run.
  • Checks: server mode verified with a fresh server; undefined-name probe 0; LF only; examples/ untouched; code hash updated (manifest now includes the legacy copy).

Behaviour changes vs v0.1.1

Rows 6–17 in devkit tasks/deliberate-changes.md, reviewed at G2. Notably:

  • new size/strain and background defaults;
  • the SH/L and peak floors are errors;
  • strict refine flags (retires KI-02);
  • on the native path, rwp is the uniform Rwp. For the two simulation examples, GSAS-II reports wR 100 %, while the uniform Rwp against their placeholder observations is 1787 % / 6820 % (EB-52; to be documented in re/10).

Left for later branches

  • re/07: migrate the examples with the converter (fix the LaB6 background peak and SH/L); remove the legacy copy, the routing's legacy branch and the server's null-key cleanup.
  • re/10: user docs for the statistics differences (EB-52), the defaults and their warnings.
  • re/05, re/06: reuse collect_warnings, the fit-report pattern, the A125 keys and the canary registration.

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

…ema 1.0.0)

- gateways/gsasii/schema.py (engine-free): recipes on CoreRecipe, the phase
  GsasiiPhase(Phase[RefinableParameter]) with scale and peak_broadening flat
  (A93), the single-source instrument blocks (A99, A103), background and
  Peak List peaks; RefinableParameter only, no bounds (A84).
- GSAS-II rules: space-group settings GSAS-II reads as themselves (canonical
  name -> GSAS-II symbol; :1 and 9 unreadable settings refused, EB-03,
  EB-41); GSAS-II cell refinement groups (A95); SH/L >= 0.002 (A91);
  background-peak and Peak List floors (A86, A90); simulation refines
  nothing (A88); scale >= 0; phase names unique ignoring case (A112).
- Documented defaults (A119): size/strain left out = 10 um / 0 with a
  structured warning (A87, A107); background left out = one fixed 0.0 term.
- Version declarations ==1.0.0 in capabilities() (A65, A81).
- schema_core.collect_warnings: a validated model's structured warnings with
  full field paths (A115).
- gsasii_iparm1: GSAS-II's Iparm1 built from the instrument block.
- validate() / the gateway route gsasii.* recipes to the native schema;
  GSASII_* recipes keep the legacy validation until re/07.
- Tests: schema rules (77); the space-group translation and cell groups
  proven against GSAS-II for all 564 canonical names (1054); canary
  registration (A122); import-block test validates a native recipe.

Assisted-by: claude-code:claude-opus-5-5
Assisted-by: claude-code:claude-sonnet-5-5
…er re/04)

gateways/gsasii/_legacy_executor.py holds byte-identical copies, taken at
2810c82, of every function the re/04 frozen-edits ledger touches (R1-R10),
and of every function on the 0.26.0 path from run_refinement that calls one
of them (18 functions + SCHEMA_EXECUTORS). The functions it imports are
unedited and shared. So a GSASII_* recipe runs only unedited code and the
before/after-refactor comparison stays exact (maintainer, ledger sign-off
2026-10-08). Generated, not edited; deleted with the 0.26.0 schema in re/07.

Not yet called: routing to it lands with the ledger edits. Added to the
code-hash manifest (script, tripwire test, _code_hash.json).

Assisted-by: claude-code:claude-opus-5-5
Input-side edits to the frozen gateway, signed by the maintainer 2026-10-08
(devkit tasks/re04-frozen-edits.md). Only recipe reads change; nothing GSAS-II
does to the project changes:
- R1 run_refinement takes the native model: Iparm1 built from the instrument
  blocks (gsasii_iparm1), fit_range from the core model, getattr for the
  workflow-specific single_peaks/phases, the instrument dumped whole,
  'gsasii.spf' check, SCHEMA_EXECUTORS gains the native names;
- R2 instrument setter reads radiation.wavelength and
  corrections.polarization (the corrections loop skips polarization);
- R3/R5 the phase block itself (A93), name = dict key (A100);
- R4 [value, flag] values; canonical name -> GSAS-II symbol (A105, EB-41);
- R6-R8 unpack_refinement_parameter reads [value, flag] too, the 6 bare
  unpacks + background peaks use it, has_active_refinement_parameter counts
  2-element lists (else atom refinement was silently skipped);
- R9 background peaks read pv_gaussian_sigma_sq (A89);
- R10 extractor reads use getattr.

Routing (gateways/gsasii/routing.py): kicker.run_refinement sends a native
model to executors.run_refinement and a 0.26.0 RecipeModel to the verbatim
legacy copy; the server and the client's in-process fallback validate a
received dict with the matching schema.

Tests: test_run_refinement_missing_keys runs every case on both executors.
Default suite 2673 passed, 4 skipped; every 2810c82 baseline test passes
(the 21 retargeted ones in both modes); regression references untouched.

Assisted-by: claude-code:claude-opus-5-5
Assisted-by: claude-code:claude-sonnet-5-5
scripts/convert_recipe_026.py states explicitly what v0.1.1 refined and
reports every change: structure + parameterization merged into one phase
block (the parameterization value won), refine flags expanded to the symmetry
and GSAS-II cell groups v0.1.1 refined together (A95), symmetry-fixed flags
false, ADP type/flags as GSAS-II used them, v0.1.1's silent defaults written
out, Multiplicity dropped where it disagrees (A101), space group to the
canonical name as GSAS-II read it (A105), instrument blocks from Iparm1 +
parameterization (A103), SH/L to GSAS-II's 0.002 floor (A91),
pv_gaussian_sigma -> pv_gaussian_sigma_sq (A89), bounds dropped (A84).
Anything the gsasii schema rejects stops the conversion with every problem
listed; nothing is fixed silently. The symmetry rewriting stays private to
the converter (A124). Engine-free.

Tests (43): one per rule, both ADP-switch directions, GSAS-II's reading of
every example's symbol, and every committed example (LaB6 and
LaB6_simulation stop on the background-peak gamma floor, example_template on
its placeholder instrument).

Equivalence (devkit probe re04_equivalence_probe.py): for 8 examples the
legacy path on the edited 0.26.0 recipe and the native path on its
conversion give identical Rwp, ycalc and refined values.

Assisted-by: claude-code:claude-opus-5-5
Assisted-by: claude-code:claude-sonnet-5-5
Regenerated from 2810c82 by the same call-graph closure: adds verbatim
execute_spf_refinement (R12), export_refined_parameters_csv,
_extract_fit_profile (R14) and their caller _extract_refined_parameters, so a
0.26.0 recipe still runs only unedited code (21 functions +
SCHEMA_EXECUTORS; the 22 names it imports are byte-identical to 2810c82).
Code-hash manifest updated.

Assisted-by: claude-code:claude-opus-5-5
GSAS-II misreads some orientations of the 2-fold sites in R32, R-3m and R-3c
(:H): it calls them general with double the multiplicity, which gives 4x the
Bragg intensity with nothing refined (EB-40; devkit probe
re04_eb40_orientation_probe.py). Some sites it cannot name ('sp'), and then
refining a coordinate or Uaniso crashes its constraint setup (EB-05).

- core symmetry.orbit(space_group, xyz): the equivalent positions in gemmi's
  operation order, reduced to [0, 1), one per position (engine-free);
- gateways/gsasii/sites.py: after add_phases_from_dict, each atom's GSAS-II
  (site_sym, mult) is compared with core's multiplicity; a mismatch, or 'sp'
  with a refined coordinate/Uaniso, is an EngineExecutionError naming the
  first orbit member GSAS-II reads correctly (never a silent substitution);
- executors.run_refinement calls it (native path only; one line, signed).

Tests land with the batch-2 run tests (tests/test_gsasii_native_run.py).

Assisted-by: claude-code:claude-opus-5-5
… writers (ledger R11, R12, R14)

Signed by the maintainer 2026-10-08 (devkit tasks/re04-frozen-edits.md,
batch 2); native path only, the 0.26.0 path runs the verbatim legacy copy.

- R11 gateways/gsasii/fit_report.py, merged into run_refinement's result:
  rwp, r_exp, gof, chi2_red from core fitstats over the fit window (A23,
  A125); engine_details with GSAS-II's own Rwp, GOF (sqrt of reduced chi^2 in
  a Rietveld refinement, EB-52), chi2, Nobs, parameters_requested (one per
  refined symmetry tie group) and parameters_varied (Rvals Nvars, which counts
  parameters GSAS-II then drops, EB-33; Peak List sigDict for SPF); GSAS-II's
  refinement message (EB-32) as a structured warning.
- R12 execute_spf_refinement: refine_peaks' bare TypeError when DoPeakFit
  fails becomes a clear EngineExecutionError (EB-38).
- R14 (A83, native only): the refined-parameters, fit-profile, unit-cell and
  peak-list tails write through core reports writers, cells formatted as
  to_csv did (gateways/gsasii/report_rows.py); files byte-identical to the
  legacy path's on 8 examples (devkit tasks/re04-equivalence-batch2.json).
- kicker.run: a native recipe's validation warnings are collected once and
  carried to the result ahead of the run's; the validate_only summary reports
  core/engine schema versions and warnings.
- server: the response model carries the new keys (they were dropped in
  server mode); a 0.26.0 result still has none of them.
- docs/SCHEMA_HISTORY.md: the gsasii 1.0.0 section.
- Tests: tests/test_gsasii_native_run.py (statistics, engine details,
  warnings, validate_only, refined zero/polarization/SH-L are varied, EB-40
  and EB-05 site errors naming a working position, orbit, EB-38, SPF);
  server response model; mock tests patch the new readers.

Assisted-by: claude-code:claude-opus-5-5
- wavelength > 0 and crystallite size > 0 are validation errors (GSAS-II
  fails on both); LG_eta and polarization stay unlimited (A126)
- Peak List peak positions must lie inside the fit window, like background
  peaks (A126)
- single peak fitting: a refine flag on the width source not in use is an
  error (U..Z with use_instrument_profile false; a peak's sigma^2/gamma with
  true), since GSAS-II ignores it silently (A127, EB-53)
- the JSON Schema shows no null for blocks validation never accepts as null;
  core absent_not_null_schema made public for the engine schemas
- peak_broadening and its parts: the "leave it out" message for null
- any gsasii.* schema_name routes to the native schema, so a typo is
  reported at schema_name with the valid names

Assisted-by: claude-code:claude-opus-5-5
- a refinement that diverged (non-finite calculated pattern in the fit
  window) is an EngineExecutionError saying so; GSAS-II does not flag it
  (A130, EB-52)
- every native result has simulation_mode; a simulation gets the standard
  statistics; an undefined statistic is None, never NaN (A129)
- gsasii_parameters_not_varied warning when GSAS-II varied fewer parameters
  than the recipe requests (A128, EB-53)
- fit_report accepts the peak fit's own Rvals (SPF GOF, used by ledger R16)
- GSASClient adds a native recipe's validation warnings on its own paths;
  kicker.run only in-process (A115); server response carries simulation_mode
- site-check messages: payload.-rooted paths, exact decimals where one
  exists, and a Uaniso must be transformed with the position (A117, A120)
- pandas_csv_rows matches to_csv for float32 and non-float missing values

Assisted-by: claude-code:claude-opus-5-5
- R15: execute_spf_refinement's EB-38 handler (R12) converts only
  DoPeakFit's None result ("'NoneType' object is not subscriptable"); any
  other TypeError keeps its own traceback
- R16: execute_spf_refinement returns the peak fit's Rvals and
  run_refinement passes them to fit_report, so SPF engine_details has
  GSAS-II's own GOF (for a peak fit, the reduced chi^2, EB-52)

Reads only; GSAS-II side unchanged. The legacy copy keeps both functions
verbatim from 2810c82. Code hash updated. Signed by the maintainer
2026-10-08 (devkit tasks/re04-frozen-edits.md, batch 3).

Assisted-by: claude-code:claude-opus-5-5
- ":R" / ":H" suffixes read as GSAS-II read them ("R -3 m:R" was turned
  into ":H"); a test checks every gemmi spelling v0.1.1's GSAS-II read
  converts to the same setting
- an explicit origin choice ":1" stops: v0.1.1 ran origin-1 coordinates as
  origin 2, so neither structure is known
- a partial parameterization Uaniso and an unknown correction key stop
  (v0.1.1 raised on both)
- every 0.26.0 key the conversion does not carry is reported (keys v0.1.1
  ignored, unused size/strain fields holding a value, parameterization atoms
  with no structure atom); the old Uij key is read as Uaniso, as v0.1.1 did

Assisted-by: claude-code:claude-opus-5-5
…R review

Wavelength and crystallite size > 0, Peak List peaks inside the fit window,
the single-peak-fitting width source, simulation_mode, None for undefined
statistics, divergence as a failure, the not-varied warning (A126-A131).

Assisted-by: claude-code:claude-opus-5-5
@AdamCorrao
AdamCorrao merged commit 139e72d into refactor/multi-engine Oct 8, 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