Prove the Diffractometer Assembly materializes + exercises the affordance-union gate#538
Merged
Merged
Conversation
…rcises the affordance-union gate
The Diffractometer is the catalog Assembly that presents the Positioner
Role by composing a Goniometer + zero-or-more detector-arm RotaryStage +
a reciprocal-space PseudoAxis. It is fleet-attested (4-ID, 8-ID, ~9 more)
but had never been materialized as a Fixture -- so the register_fixture
affordance-union check (Watch Item 10) on a presents_as Assembly had never
actually run on a composed instrument (the existing Microscope fixture
tests use an Assembly with empty presents_as, which SKIPS the gate).
This is that first exercise, end-to-end over the REST surface using the
real seeded Positioner Role + constituent Family ids:
- happy path: Goniometer (covers Homeable+Limitable) + detector-arm
RotaryStage + reciprocal-space PseudoAxis -> the constituent affordance
union covers Positioner's required set -> fixture registers (201).
- negative: Goniometer stripped of Homeable/Limitable AND no detector-arm
RotaryStage bound (detector_arm is ZeroOrMore) -> no constituent
supplies the required set -> 409 (FixtureCannotPresentRoleError).
Gate-semantics note recorded in the test: the affordance union is taken
across ALL bound constituents, so a presents_as Assembly is satisfied when
ANY bound part supplies the required affordances (union coverage, the
intended Watch-Item-10 v1 semantics -- a coarse "the instrument contains
the capability" check, not "this specific slot provides it").
SPEC-1 assessed alongside: the EmissionSpectrometer / SpectrometerArm
notes correctly model those as single Families presenting Detector /
Positioner and defer only the narrow child-Asset-per-crystal question --
they do NOT claim to be CORA Assemblies, so no note change is warranted.
Co-Authored-By: Claude <noreply@anthropic.com>
xmap
added a commit
that referenced
this pull request
Jul 3, 2026
…g at runtime The #539 catalog conversions introduced a "mixed" method shape (a Detector role_kind slot PLUS a needed_family coverage requirement for a no-Role family: single_crystal_diffraction / surface_diffraction / grazing_incidence_scattering keep PseudoAxis, transmission_xray_microscopy keeps ZonePlate). That coexistence was asserted from the schema (both fields, no XOR) but never exercised at runtime -- the #539 commit was catalog-only, and catalog methods are docs-only, so no binding fired. This adds the missing proof over the REST surface: - happy: a Camera (Detector) + a PseudoAxis asset -> define_plan accepts (needed_family coverage holds) AND the Detector role_kind slot binds. - negative: a Plan with only the Camera (no PseudoAxis) -> define_plan 409 (needed_family_ids not covered), even though the Detector slot could bind -- proving the role slot and the family-coverage set are independent and both enforced. Closes the coverage gap between #539 (catalog) and the runtime binding machinery, matching the proof discipline used for tomography (#534), xpcs/Shutter (#534), and the Diffractometer fixture (#538). Co-Authored-By: Claude <noreply@anthropic.com>
xmap
added a commit
that referenced
this pull request
Jul 3, 2026
* feat(catalog): bind the new fleet technique-methods by Role
Extends the role_kind binding to the ~20 technique-methods that landed on
main from the fleet-expansion work, applying the same per-method
abstraction assessment used for the original method set.
Full role-mappable (14) -> required_roles, every family presents a Role:
powder_diffraction, small_wide_angle_scattering -> [Detector]
radiography, xray_emission_spectroscopy -> [Detector]
(radiography: Scintillator drops -- detector-Assembly constituent)
macromolecular_crystallography -> [Positioner, Detector]
coherent_diffraction_imaging, ptychography -> [Detector, Positioner]
xray_photoelectron_spectroscopy, hard_xray_photoelectron_spectroscopy,
angle_resolved_photoemission -> [Detector, Positioner]
serial_crystallography -> [Positioner, Detector, Controller]
xray_fluorescence_mapping -> [Sensor, Positioner]
resonant_inelastic_scattering, inelastic_scattering -> [Positioner]
(SpectrometerArm presents Positioner)
Mixed (4) -> Detector role + the no-Role family kept anatomical:
single_crystal_diffraction, surface_diffraction,
grazing_incidence_scattering -> roles [Detector] + needed [PseudoAxis]
transmission_xray_microscopy -> roles [Detector] + needed [ZonePlate]
The reciprocal-space PseudoAxis and the ZonePlate objective present no
Role (computed coordinate / imaging optic, not functional contracts), so
they stay on needed_families; each carries a note explaining the mix.
Kept family_id (assessed, unchanged): absorption_spectroscopy +
continuous_absorption_spectroscopy (Monochromator only, no Role),
energy_characterization, beamline_energy_change (the deferred energy
abstraction), hexapod_reboot (device-specific recovery).
A Method may carry both required_roles and needed_families (independent
fields, no XOR) -- that is what the mixed methods use. All Role targets
verified against catalog presents_as (EmissionSpectrometer->Detector,
SpectrometerArm->Positioner, EnergyDispersiveSpectrometer->Sensor, etc.).
Co-Authored-By: Claude <noreply@anthropic.com>
* test(recipe): prove the mixed role_kind + needed_family method binding at runtime
The #539 catalog conversions introduced a "mixed" method shape (a Detector
role_kind slot PLUS a needed_family coverage requirement for a no-Role
family: single_crystal_diffraction / surface_diffraction /
grazing_incidence_scattering keep PseudoAxis, transmission_xray_microscopy
keeps ZonePlate). That coexistence was asserted from the schema (both
fields, no XOR) but never exercised at runtime -- the #539 commit was
catalog-only, and catalog methods are docs-only, so no binding fired.
This adds the missing proof over the REST surface:
- happy: a Camera (Detector) + a PseudoAxis asset -> define_plan accepts
(needed_family coverage holds) AND the Detector role_kind slot binds.
- negative: a Plan with only the Camera (no PseudoAxis) -> define_plan
409 (needed_family_ids not covered), even though the Detector slot
could bind -- proving the role slot and the family-coverage set are
independent and both enforced.
Closes the coverage gap between #539 (catalog) and the runtime binding
machinery, matching the proof discipline used for tomography (#534),
xpcs/Shutter (#534), and the Diffractometer fixture (#538).
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Materializes and proves the Diffractometer Assembly end-to-end — the composed multi-circle scattering instrument that presents the Positioner Role by composing a Goniometer + zero-or-more detector-arm RotaryStage + a reciprocal-space PseudoAxis.
It is fleet-attested (4-ID, 8-ID, ~9 more) and defined in the catalog, but had never been materialized as a Fixture — so the
register_fixtureaffordance-union check (Watch Item 10, shipped in #534) on apresents_asAssembly had never actually run on a composed instrument. The existing Microscope fixture tests use an Assembly with emptypresents_as, which skips the gate.This is that first exercise, over the REST surface, using the real seeded Positioner Role + constituent Family ids:
Homeable+Limitable) + detector-arm RotaryStage + reciprocal-space PseudoAxis → constituent affordance union covers Positioner's required set → fixture registers (201).Homeable/LimitableAND no detector-arm RotaryStage bound (detector_armis ZeroOrMore) → no constituent supplies the required set → 409FixtureCannotPresentRoleError.Gate-semantics finding (recorded in the test)
The negative case surfaced that the affordance union is taken across all bound constituents — so a
presents_asAssembly is satisfied when any bound part supplies the required affordances. This is the intended Watch-Item-10 v1 semantics (union coverage: a coarse "the instrument contains the capability" check, not "this specific slot provides it"). Documented as a watch-item, not changed.SPEC-1 assessed (no change)
The EmissionSpectrometer / SpectrometerArm catalog notes correctly model those as single Families (presenting Detector / Positioner) and defer only the narrow child-Asset-per-crystal question. They do not claim to be CORA Assemblies, so no note change is warranted.
Test plan
pytest tests/contract -k "fixture or assembl or diffractometer"(117 passed, incl. the 2 new)Generated with Claude Code