From 794d0d8bc66a52d60b89bca696291bb587388074 Mon Sep 17 00:00:00 2001 From: Pete Jemian Date: Wed, 17 Jun 2026 15:33:37 -0500 Subject: [PATCH 1/7] refactor(#299) rename alpha_i/beta_out -> incidence/emergence (deprecate aliases) Add 'incidence' and 'emergence' as the canonical surface-frame reference-constraint names. The legacy names 'alpha_i' and 'beta_out' are kept as forwarding aliases that emit DeprecationWarning at every entry point and canonicalize to the new names before storage. mode.py: REFERENCE_NAMES gains the canonical names and retains the legacy ones as deprecated; _DEPRECATED_REFERENCE_NAME_ALIASES maps each legacy spelling to its canonical form. ReferenceConstraint.__init__ canonicalizes on construction; ConstraintSet.with_constraint_values accepts the legacy kwargs as aliases (only when the canonical constraint is actually present, so a genuine typo still surfaces as KeyError rather than a confusing deprecation followed by KeyError). forward.py: _OUTPUT_EXTRA_KEYS extended with canonical names plus legacy aliases so existing extras-dict declarations continue to receive computed values. The computers dict routes both spellings through the same callable. _solve_surface and _surface_residual dispatch on canonical names only (legacy names cannot reach them because they are canonicalized upstream). diffractometer.py: required_reference_vector dispatches on canonical names; docstring table updated. benchmark.py: _prepare_mode surface-mode branch dispatches on canonical names. Tests: parametrized rows in test_reference_constraint_construction and test_with_constraint_values_replaces_values switch to canonical names; new test_reference_constraint_deprecated_name_canonicalized and test_with_constraint_values_accepts_deprecated_alias_kwarg cover the deprecation-alias paths. Two failing assertion strings in test_with_constraint_values_unknown_key_raises_KeyError updated for the renamed constraint. Deprecation infrastructure is tagged 'REMOVE-AT-V1.0:' for the release-time cleanup tracked by #301. Contributed by: OpenCode (argo/claudeopus47) --- src/ad_hoc_diffractometer/benchmark.py | 6 +- src/ad_hoc_diffractometer/diffractometer.py | 6 +- src/ad_hoc_diffractometer/forward.py | 57 +++++--- src/ad_hoc_diffractometer/mode.py | 137 ++++++++++++++++---- tests/test_mode.py | 104 +++++++++++++-- 5 files changed, 250 insertions(+), 60 deletions(-) diff --git a/src/ad_hoc_diffractometer/benchmark.py b/src/ad_hoc_diffractometer/benchmark.py index cc2824af..2aaab0fc 100644 --- a/src/ad_hoc_diffractometer/benchmark.py +++ b/src/ad_hoc_diffractometer/benchmark.py @@ -100,7 +100,7 @@ def _prepare_mode(geometry, mode_name: str) -> None: - fixed_psi modes: sets ``azimuth`` - double_diffraction modes: sets h2/k2/l2 extras - zone modes: sets z0/z1 extras to a generic (h,k,0) plane - - surface/reference modes (alpha_i, beta_out, a_eq_b): + - surface/reference modes (incidence, emergence, a_eq_b): sets ``surface_normal`` """ geometry.mode_name = mode_name @@ -123,10 +123,10 @@ def _prepare_mode(geometry, mode_name: str) -> None: if cname == "psi" and geometry.azimuth is None: geometry.azimuth = (0, 0, 1) - # Surface modes: set surface_normal if needed + # Surface modes: set surface_normal if needed. for c in cs._constraints: cname = getattr(c, "_name", getattr(c, "name", "")) - if cname in ("alpha_i", "beta_out", "a_eq_b"): + if cname in ("incidence", "emergence", "a_eq_b"): if geometry.surface_normal is None: geometry.surface_normal = (0, 0, 1) diff --git a/src/ad_hoc_diffractometer/diffractometer.py b/src/ad_hoc_diffractometer/diffractometer.py index 68a63076..7034932b 100644 --- a/src/ad_hoc_diffractometer/diffractometer.py +++ b/src/ad_hoc_diffractometer/diffractometer.py @@ -1267,8 +1267,8 @@ def required_reference_vector(self) -> str | None: ===================================== ================================= ReferenceConstraint name Required attribute on the geometry ===================================== ================================= - ``"alpha_i"`` :attr:`surface_normal` - ``"beta_out"`` :attr:`surface_normal` + ``"incidence"`` :attr:`surface_normal` + ``"emergence"`` :attr:`surface_normal` ``"a_eq_b"`` :attr:`surface_normal` ``"psi"`` :attr:`azimuth` ``"naz"`` :attr:`azimuth` @@ -1322,7 +1322,7 @@ def required_reference_vector(self) -> str | None: rc: ReferenceConstraint | None = mode.reference_constraint if rc is None: return None - if rc.name in {"alpha_i", "beta_out", "a_eq_b"}: + if rc.name in {"incidence", "emergence", "a_eq_b"}: return "surface_normal" if rc.name in {"psi", "naz"}: return "azimuth" diff --git a/src/ad_hoc_diffractometer/forward.py b/src/ad_hoc_diffractometer/forward.py index c23837a6..289f1d24 100644 --- a/src/ad_hoc_diffractometer/forward.py +++ b/src/ad_hoc_diffractometer/forward.py @@ -472,7 +472,17 @@ def compute_forward( # The callables are imported lazily inside :func:`_populate_output_extras` to # avoid a circular import at module load time (``reference`` imports from # ``forward``). -_OUTPUT_EXTRA_KEYS = ("alpha_i", "beta_out", "psi", "omega") +_OUTPUT_EXTRA_KEYS = ( + "incidence", + "emergence", + "psi", + "omega", + # REMOVE-AT-V1.0: deprecated aliases kept so existing extras-dict + # declarations continue to receive computed values until callers + # migrate. + "alpha_i", + "beta_out", +) def _populate_output_extras( @@ -480,7 +490,7 @@ def _populate_output_extras( mode, solutions: list[dict[str, float]], ) -> None: - """Populate output-slot extras (alpha_i, beta_out, psi, omega) per solution. + """Populate output-slot extras (incidence, emergence, psi, omega) per solution. Issue #292. A subset of declarative modes (psic, sixc, zaxis, s2d2 surface modes; the fixed_psi_* family; fixed_omega_*) declare placeholder slots @@ -495,10 +505,14 @@ def _populate_output_extras( -------- * Only keys actually declared in ``mode.extras`` are touched. * A key declared but whose required reference vector is unset on the - geometry (e.g. ``alpha_i`` without ``surface_normal``) is left as + geometry (e.g. ``incidence`` without ``surface_normal``) is left as ``None``; a debug-level log message records why. * Empty ``solutions`` leaves every slot as an empty list. * Each successful call **replaces** the prior contents of the slot. + * The deprecated keys ``"alpha_i"`` / ``"beta_out"`` are populated + identically to their canonical counterparts + ``"incidence"`` / ``"emergence"``. REMOVE-AT-V1.0: drop this + bullet alongside the legacy keys. """ if mode is None or not getattr(mode, "extras", None): return @@ -514,10 +528,13 @@ def _populate_output_extras( from .reference import psi_angle as _psi_angle computers: dict[str, callable] = { - "alpha_i": _incidence_angle, - "beta_out": _exit_angle, + "incidence": _incidence_angle, + "emergence": _exit_angle, "psi": _psi_angle, "omega": _omega_pseudo, + # REMOVE-AT-V1.0: deprecated aliases routed to the same computers. + "alpha_i": _incidence_angle, + "beta_out": _exit_angle, } for key in relevant: @@ -3056,9 +3073,9 @@ def _solve_surface( Supports ReferenceConstraint modes where the constraint is: - - ``"alpha_i"`` — incidence angle fixed at target value - - ``"beta_out"`` — exit angle fixed at target value - - ``"a_eq_b"`` — symmetric reflection: alpha_i = beta_out + - ``"incidence"`` — incidence angle fixed at target value + - ``"emergence"`` — emergence angle fixed at target value + - ``"a_eq_b"`` — symmetric reflection: incidence = emergence The solver builds a baseline angles dict (applying all fixed sample/detector constraints and setting the detector stage to ttheta_deg), then performs a @@ -3079,9 +3096,11 @@ def _solve_surface( from .mode import ReferenceConstraint - # Extract the reference constraint + # Extract the reference constraint. ``rc.name`` is always canonical + # because :class:`ReferenceConstraint` canonicalizes the deprecated + # aliases (``"alpha_i"`` / ``"beta_out"``) at construction time. rc = next(c for c in mode.constraints if isinstance(c, ReferenceConstraint)) - target_name = rc.name # "alpha_i", "beta_out", or "a_eq_b" + target_name = rc.name # "incidence", "emergence", or "a_eq_b" target_value = rc.value # float or True # Build baseline angles dict with all fixed constraints applied @@ -3197,18 +3216,18 @@ def _surface_residual( Returns a float residual in degrees (zero = constraint satisfied). """ - from .reference import exit_angle as _beta_out - from .reference import incidence_angle as _alpha_i + from .reference import exit_angle as _emergence_angle + from .reference import incidence_angle as _incidence_angle - if target_name == "alpha_i": - ai = _alpha_i(geometry, angles=angles) + if target_name == "incidence": + ai = _incidence_angle(geometry, angles=angles) return ai - float(target_value) - if target_name == "beta_out": - bo = _beta_out(geometry, angles=angles) + if target_name == "emergence": + bo = _emergence_angle(geometry, angles=angles) return bo - float(target_value) - # a_eq_b: alpha_i = beta_out - ai = _alpha_i(geometry, angles=angles) - bo = _beta_out(geometry, angles=angles) + # a_eq_b: incidence = emergence (symmetric reflection) + ai = _incidence_angle(geometry, angles=angles) + bo = _emergence_angle(geometry, angles=angles) return ai - bo diff --git a/src/ad_hoc_diffractometer/mode.py b/src/ad_hoc_diffractometer/mode.py index 12b45955..17b65fc5 100644 --- a/src/ad_hoc_diffractometer/mode.py +++ b/src/ad_hoc_diffractometer/mode.py @@ -39,8 +39,10 @@ condition between Q and an external reference vector n̂ (surface normal, polarization axis, etc.) stored on the geometry. The named options are physical pseudo-angles from You (1999) and Lohmeier & Vlieg (1993): -``"psi"``, ``"alpha_i"``, ``"beta_out"``, ``"a_eq_b"``, ``"naz"``. At -most one reference constraint is allowed. +``"psi"``, ``"incidence"``, ``"emergence"``, ``"a_eq_b"``, ``"naz"``. +``"alpha_i"`` and ``"beta_out"`` are accepted as deprecated aliases for +``"incidence"`` and ``"emergence"`` respectively (issue #299). At most +one reference constraint is allowed. Classes ------- @@ -195,7 +197,18 @@ def __init__( # --------------------------------------------------------------------------- REFERENCE_NAMES: frozenset[str] = frozenset( - {"psi", "alpha_i", "beta_out", "a_eq_b", "naz", "omega"} + { + "psi", + "incidence", + "emergence", + "a_eq_b", + "naz", + "omega", + # REMOVE-AT-V1.0: legacy alias names accepted on input and + # canonicalized to the new names by ReferenceConstraint.__init__. + "alpha_i", + "beta_out", + } ) """Valid reference constraint names (physical pseudo-angles). @@ -206,13 +219,40 @@ def __init__( geometry (no reference vector required): - ``"psi"`` — azimuthal angle of n̂ about Q (You 1999, eq. 23) -- ``"alpha_i"`` — angle of incidence (incident beam vs. surface plane) -- ``"beta_out"`` — angle of exit (diffracted beam vs. surface plane) -- ``"a_eq_b"`` — relational: alpha_i = beta_out (symmetric reflection) +- ``"incidence"`` — angle of incidence (incident beam vs. surface plane) +- ``"emergence"`` — angle of emergence (diffracted beam vs. surface plane) +- ``"a_eq_b"`` — relational: incidence = emergence (symmetric reflection) - ``"naz"`` — azimuthal angle of n̂ in the lab frame - ``"omega"`` — SPEC ``OMEGA`` pseudo-angle (``Q[6]``): angle between Q and the plane of the chi circle (psic family); see :func:`~ad_hoc_diffractometer.reference.omega_pseudo` + +The legacy names ``"alpha_i"`` and ``"beta_out"`` are also accepted as +deprecated aliases for ``"incidence"`` and ``"emergence"`` respectively. +Constructing a :class:`ReferenceConstraint` with a legacy name emits +:class:`DeprecationWarning` and canonicalizes to the new name; the +stored ``name`` attribute is always one of the canonical values listed +above. See :data:`_DEPRECATED_REFERENCE_NAME_ALIASES`. +REMOVE-AT-V1.0: the preceding paragraph documents the deprecation +aliases and should be deleted alongside them. +""" + +# REMOVE-AT-V1.0: this entire mapping (and every site that consults it) +# is the deprecation infrastructure for issue #299. +_DEPRECATED_REFERENCE_NAME_ALIASES: dict[str, str] = { + "alpha_i": "incidence", + "beta_out": "emergence", +} +"""Mapping from deprecated reference-constraint name to its canonical form. + +``"alpha_i"`` / ``"beta_out"`` are kept as forwarding aliases so existing +user code, saved sessions, and out-of-tree YAML files continue to work. + +:class:`ReferenceConstraint`, the YAML loader, the forward solver's output +extras, and ``from_dict`` all consult this map. When a legacy name is +supplied, a :class:`DeprecationWarning` is emitted and the canonical name +is used for all downstream storage and comparison. Use the canonical +names (``"incidence"`` / ``"emergence"``) in new code. """ QAZ: str = "qaz" @@ -315,7 +355,7 @@ def __setitem__(self, key: str, value: Any) -> None: f"effect on forward(). Set the reference vector on the " f"geometry instead: use 'g.surface_normal = (h, k, l)' " f"for surface-mode constraints " - f"(alpha_i / beta_out / a_eq_b), or " + f"(incidence / emergence / a_eq_b), or " f"'g.azimuth = (h, k, l)' for " f"psi / naz constraints. See " f"AdHocDiffractometer.required_reference_vector to " @@ -847,14 +887,16 @@ class ReferenceConstraint: ``"psi"`` Azimuthal angle of n̂ about Q (You 1999, eq. 23). - ``"alpha_i"`` + ``"incidence"`` Angle of incidence: angle between the incident beam and the surface - plane (perpendicular to n̂). - ``"beta_out"`` - Angle of exit: angle between the diffracted beam and the surface - plane. + plane (perpendicular to n̂). Accepts the deprecated alias + ``"alpha_i"`` (REMOVE-AT-V1.0). + ``"emergence"`` + Angle of emergence: angle between the diffracted beam and the + surface plane. Accepts the deprecated alias ``"beta_out"`` + (REMOVE-AT-V1.0). ``"a_eq_b"`` - Relational: alpha_i = beta_out (symmetric reflection). + Relational: ``incidence = emergence`` (symmetric reflection). ``value`` must be ``True``. ``"naz"`` Azimuthal angle of n̂ in the lab frame (You 1999). @@ -862,8 +904,11 @@ class ReferenceConstraint: Parameters ---------- name : str - One of ``"psi"``, ``"alpha_i"``, ``"beta_out"``, ``"a_eq_b"``, - ``"naz"``. + One of ``"psi"``, ``"incidence"``, ``"emergence"``, ``"a_eq_b"``, + ``"naz"``, ``"omega"``. The deprecated aliases ``"alpha_i"`` and + ``"beta_out"`` are also accepted (with a :class:`DeprecationWarning`) + and canonicalized to ``"incidence"`` and ``"emergence"`` respectively + before storage. REMOVE-AT-V1.0: drop the alias clause. value : float or bool Target value in degrees (or ``True`` for ``"a_eq_b"``). @@ -873,6 +918,8 @@ class ReferenceConstraint: ReferenceConstraint('psi', 90.0) >>> ReferenceConstraint("a_eq_b", True) ReferenceConstraint('a_eq_b', True) + >>> ReferenceConstraint("incidence", 5.0) + ReferenceConstraint('incidence', 5.0) """ category: str = "reference" @@ -880,10 +927,29 @@ class ReferenceConstraint: def __init__(self, name: str, value: float | bool) -> None: if name not in REFERENCE_NAMES: + # Compose the user-visible list of valid names from the canonical + # set only; deprecated aliases are accepted on input but should + # not be advertised in the error message for invalid names. + canonical = sorted( + REFERENCE_NAMES - _DEPRECATED_REFERENCE_NAME_ALIASES.keys() + ) raise ValueError( - f"ReferenceConstraint name must be one of {sorted(REFERENCE_NAMES)}; " - f"got {name!r}." + f"ReferenceConstraint name must be one of {canonical}; got {name!r}." + ) + # REMOVE-AT-V1.0: canonicalize deprecated aliases. Emit a + # DeprecationWarning so callers see the recommended replacement, + # but the construction itself still succeeds. + if name in _DEPRECATED_REFERENCE_NAME_ALIASES: + import warnings + + canonical_name = _DEPRECATED_REFERENCE_NAME_ALIASES[name] + warnings.warn( + f"ReferenceConstraint name {name!r} is deprecated; " + f"use {canonical_name!r} instead. (issue #299)", + DeprecationWarning, + stacklevel=2, ) + name = canonical_name if name == "a_eq_b" and value is not True: raise ValueError( "ReferenceConstraint('a_eq_b', value): value must be True; " @@ -938,7 +1004,7 @@ def has_reference_vector(self, geometry: AdHocDiffractometer) -> bool: For ``"psi"`` and ``"naz"``: requires :attr:`~geometry.AdHocDiffractometer.azimuth` to be set. - For ``"alpha_i"``, ``"beta_out"``, and ``"a_eq_b"``: requires + For ``"incidence"``, ``"emergence"``, and ``"a_eq_b"``: requires :attr:`~geometry.AdHocDiffractometer.surface_normal` to be set. For ``"omega"``: no reference vector is required (always returns @@ -963,8 +1029,8 @@ def is_implemented(self, geometry: AdHocDiffractometer) -> bool: Implemented constraints: - - ``"alpha_i"`` — requires :attr:`~geometry.AdHocDiffractometer.surface_normal` - - ``"beta_out"`` — requires :attr:`~geometry.AdHocDiffractometer.surface_normal` + - ``"incidence"`` — requires :attr:`~geometry.AdHocDiffractometer.surface_normal` + - ``"emergence"`` — requires :attr:`~geometry.AdHocDiffractometer.surface_normal` - ``"a_eq_b"`` — requires :attr:`~geometry.AdHocDiffractometer.surface_normal` - ``"psi"`` — requires :attr:`~geometry.AdHocDiffractometer.azimuth`. The forward solver treats ψ as a **validation filter**: for a given @@ -989,7 +1055,7 @@ def is_implemented(self, geometry: AdHocDiffractometer) -> bool: if self._name == "omega": # Implemented for any geometry with a chi sample stage. return any(s.name == "chi" for s in geometry.sample_stages) - # alpha_i, beta_out, a_eq_b — implemented when surface_normal is set + # incidence, emergence, a_eq_b — implemented when surface_normal is set return geometry.surface_normal is not None def to_dict(self) -> dict: @@ -1424,7 +1490,32 @@ def with_constraint_values(self, **updates: float | bool) -> ConstraintSet: ) name_to_index[cname] = idx - unknown = sorted(k for k in updates if k not in name_to_index) + # REMOVE-AT-V1.0: accept the deprecated reference-constraint + # kwargs ``alpha_i`` / ``beta_out`` as aliases for ``incidence`` + # / ``emergence``. The alias only fires when the set actually + # contains the canonical constraint; otherwise the kwarg falls + # through to the regular unknown-key path so a genuine typo is + # not masked by a confusing deprecation warning followed by a + # KeyError. After removal, this whole canonicalization loop + # becomes a single ``canonicalized = updates`` assignment (or + # the variable goes away entirely). + canonicalized: dict[str, float | bool] = {} + for key, new_value in updates.items(): + canonical = _DEPRECATED_REFERENCE_NAME_ALIASES.get(key) + if canonical is not None and canonical in name_to_index: + import warnings + + warnings.warn( + f"with_constraint_values: kwarg {key!r} is deprecated; " + f"use {canonical!r} instead. (issue #299)", + DeprecationWarning, + stacklevel=2, + ) + canonicalized[canonical] = new_value + else: + canonicalized[key] = new_value + + unknown = sorted(k for k in canonicalized if k not in name_to_index) if unknown: available = sorted(name_to_index) raise KeyError( @@ -1434,7 +1525,7 @@ def with_constraint_values(self, **updates: float | bool) -> ConstraintSet: ) new_constraints: list[AnyConstraint] = list(self._constraints) - for name, new_value in updates.items(): + for name, new_value in canonicalized.items(): idx = name_to_index[name] original = new_constraints[idx] # Each settable-value constraint is constructed with the same diff --git a/tests/test_mode.py b/tests/test_mode.py index 4734bd8a..f8c4ae07 100644 --- a/tests/test_mode.py +++ b/tests/test_mode.py @@ -737,8 +737,8 @@ def test_detector_constraint_evaluate_qaz(): "name, value, context", [ pytest.param("psi", 90.0, does_not_raise(), id="psi"), - pytest.param("alpha_i", 5.0, does_not_raise(), id="alpha_i"), - pytest.param("beta_out", 5.0, does_not_raise(), id="beta_out"), + pytest.param("incidence", 5.0, does_not_raise(), id="incidence"), + pytest.param("emergence", 5.0, does_not_raise(), id="emergence"), pytest.param("a_eq_b", True, does_not_raise(), id="a_eq_b-true"), pytest.param("naz", 0.0, does_not_raise(), id="naz"), pytest.param( @@ -762,6 +762,43 @@ def test_reference_constraint_construction(name, value, context): assert rc.category == "reference" +# REMOVE-AT-V1.0: this test and its parametrize block exist solely to +# pin the deprecation-alias canonicalization behavior; delete the whole +# function (and its decorator) when the aliases are removed. +@pytest.mark.parametrize( + "legacy_name, canonical_name, value", + [ + pytest.param("alpha_i", "incidence", 5.0, id="alpha_i-to-incidence"), + pytest.param("beta_out", "emergence", 5.0, id="beta_out-to-emergence"), + ], +) +def test_reference_constraint_deprecated_name_canonicalized( + legacy_name, canonical_name, value +): + """Issue #299: legacy reference-constraint names are canonicalized. + + Constructing a ReferenceConstraint with a deprecated alias + (``"alpha_i"`` / ``"beta_out"``) emits :class:`DeprecationWarning` + and stores the canonical name (``"incidence"`` / ``"emergence"``). + The constraint behaves identically to one constructed with the + canonical name; this is the contract that lets existing user code + and saved sessions continue to work through the deprecation cycle. + """ + context = pytest.warns( + DeprecationWarning, + match=re.escape( + f"ReferenceConstraint name {legacy_name!r} is deprecated; " + f"use {canonical_name!r} instead. (issue #299)" + ), + ) + with context: + rc = ReferenceConstraint(legacy_name, value) + assert rc.name == canonical_name + assert rc.value == value + # Equal to the constraint built directly with the canonical name. + assert rc == ReferenceConstraint(canonical_name, value) + + def test_reference_constraint_value_coerced_to_float(): rc = ReferenceConstraint("psi", 90) assert isinstance(rc.value, float) @@ -2448,12 +2485,18 @@ def _b3_constraint_set(): Three settable-value constraints: two SampleConstraint plus one ReferenceConstraint. Used by several with_constraint_values tests that exercise the multi-value path. + + The reference constraint is built with the canonical name + ``"incidence"`` (issue #299). The YAML mode name retains its + historical ``fixed_alpha_i_*`` spelling because mode names are a + separate identifier from the constraint-name vocabulary; that + rename, if any, is out of scope for this issue. """ return ConstraintSet( [ SampleConstraint("chi", 0.0), SampleConstraint("phi", 0.0), - ReferenceConstraint("alpha_i", 0.0), + ReferenceConstraint("incidence", 0.0), ], computed=["mu", "eta", "nu", "delta"], extras={"n_hat": REQUIRED, "alpha_i": None, "beta_out": None}, @@ -2494,10 +2537,10 @@ def _b3_constraint_set(): ), pytest.param( _b3_constraint_set, - {"alpha_i": 5.0}, - {"chi": 0.0, "phi": 0.0, "alpha_i": 5.0}, + {"incidence": 5.0}, + {"chi": 0.0, "phi": 0.0, "incidence": 5.0}, does_not_raise(), - id="reference-alpha_i-5", + id="reference-incidence-5", ), pytest.param( # ReferenceConstraint('a_eq_b', ...) only accepts True @@ -2516,15 +2559,15 @@ def _b3_constraint_set(): ), pytest.param( _b3_constraint_set, - {"chi": 15.0, "phi": 30.0, "alpha_i": 5.0}, - {"chi": 15.0, "phi": 30.0, "alpha_i": 5.0}, + {"chi": 15.0, "phi": 30.0, "incidence": 5.0}, + {"chi": 15.0, "phi": 30.0, "incidence": 5.0}, does_not_raise(), id="b3-three-values", ), pytest.param( _b3_constraint_set, {}, - {"chi": 0.0, "phi": 0.0, "alpha_i": 0.0}, + {"chi": 0.0, "phi": 0.0, "incidence": 0.0}, does_not_raise(), id="empty-updates-no-change", ), @@ -2598,7 +2641,7 @@ def test_with_constraint_values_unknown_key_raises_KeyError(): match=re.escape( "with_constraint_values: no constraint(s) named " "['cho'] in this ConstraintSet; available names " - "are ['alpha_i', 'chi', 'phi']." + "are ['chi', 'incidence', 'phi']." ), ): cs.with_constraint_values(cho=45.0) @@ -2688,11 +2731,48 @@ def test_with_constraint_values_round_trips_via_to_dict(): """Replace then restore via to_dict comparison: identical dicts.""" cs = _b3_constraint_set() original_dict = cs.to_dict() - intermediate = cs.with_constraint_values(chi=15.0, phi=30.0, alpha_i=5.0) - restored = intermediate.with_constraint_values(chi=0.0, phi=0.0, alpha_i=0.0) + intermediate = cs.with_constraint_values(chi=15.0, phi=30.0, incidence=5.0) + restored = intermediate.with_constraint_values(chi=0.0, phi=0.0, incidence=0.0) assert restored.to_dict() == original_dict +# REMOVE-AT-V1.0: this test pins the deprecation-alias kwarg behavior +# of with_constraint_values; delete the whole function (and its +# decorator) when the aliases are removed. +@pytest.mark.parametrize( + "legacy_kwarg, canonical_kwarg, value", + [ + pytest.param("alpha_i", "incidence", 5.0, id="alpha_i-to-incidence"), + pytest.param("beta_out", "emergence", 5.0, id="beta_out-to-emergence"), + ], +) +def test_with_constraint_values_accepts_deprecated_alias_kwarg( + legacy_kwarg, canonical_kwarg, value +): + """Issue #299: ``with_constraint_values`` accepts the legacy kwargs. + + Passing ``alpha_i=...`` / ``beta_out=...`` is canonicalized to + ``incidence=...`` / ``emergence=...`` with a + :class:`DeprecationWarning`. The replacement value reaches the + underlying :class:`ReferenceConstraint` exactly as if the canonical + name had been used. + """ + cs = ConstraintSet( + [ReferenceConstraint(canonical_kwarg, 0.0)], + extras={"n_hat": REQUIRED}, + ) + with pytest.warns( + DeprecationWarning, + match=re.escape( + f"with_constraint_values: kwarg {legacy_kwarg!r} is deprecated; " + f"use {canonical_kwarg!r} instead. (issue #299)" + ), + ): + new = cs.with_constraint_values(**{legacy_kwarg: value}) + new_values = {c.name: c.value for c in new.constraints if hasattr(c, "name")} + assert new_values[canonical_kwarg] == value + + # --------------------------------------------------------------------------- # Documentation-placeholder extras warning (issue #294) # --------------------------------------------------------------------------- From 0313a6607135464b016799951f302a6266af6bc0 Mon Sep 17 00:00:00 2001 From: Pete Jemian Date: Wed, 17 Jun 2026 16:10:39 -0500 Subject: [PATCH 2/7] refactor(#299) back-fill legacy alias slots in output extras Originally planned as a separate Step 3 in the form of an aliasing _ExtrasDict that forwards alpha_i / beta_out reads and writes to the canonical incidence / emergence slots. That design fought the dict-bulk-copy fast path used by the ConstraintSet constructor and the YAML loader, and was solving a problem (out-of-tree callers reading result.extras['alpha_i'] after the YAML migration) that a much smaller back-fill in _populate_output_extras solves directly. After populating each declared canonical output slot, mirror the list into the corresponding legacy slot regardless of whether the legacy slot was declared in the mode. The mirror uses dict.__setitem__ so it bypasses any future _ExtrasDict overrides and does not emit spurious deprecation warnings on internal writes. Adds a regression test that builds a mode declaring only the canonical slots (incidence, emergence) and verifies the legacy slots (alpha_i, beta_out) are populated by the back-fill rather than by the main computer-dict loop. Marked REMOVE-AT-V1.0 for the cleanup tracked by #301. Contributed by: OpenCode (argo/claudeopus47) --- src/ad_hoc_diffractometer/forward.py | 14 +++++++ tests/test_regression_issue_292.py | 62 ++++++++++++++++++++++++++++ 2 files changed, 76 insertions(+) diff --git a/src/ad_hoc_diffractometer/forward.py b/src/ad_hoc_diffractometer/forward.py index 289f1d24..db544bab 100644 --- a/src/ad_hoc_diffractometer/forward.py +++ b/src/ad_hoc_diffractometer/forward.py @@ -569,6 +569,20 @@ def _populate_output_extras( failure, ) + # REMOVE-AT-V1.0: back-fill the legacy slot names so out-of-tree + # callers reading ``result.extras["alpha_i"]`` / ``["beta_out"]`` + # continue to see the populated value after the in-tree YAML files + # have been migrated to declare only the canonical slots. Without + # this back-fill a caller would see KeyError immediately after the + # YAML migration in Step 4 of the same PR, even though the canonical + # slot is populated. Mirror writes use ``dict.__setitem__`` so they + # are not subject to any future ``_ExtrasDict`` aliasing path and + # do not emit spurious deprecation warnings on internal writes. + _LEGACY_OUTPUT_KEY_MIRROR = {"incidence": "alpha_i", "emergence": "beta_out"} + for canonical_key, legacy_key in _LEGACY_OUTPUT_KEY_MIRROR.items(): + if canonical_key in mode.extras: + dict.__setitem__(mode.extras, legacy_key, mode.extras[canonical_key]) + # --------------------------------------------------------------------------- # Constraint-set dispatcher diff --git a/tests/test_regression_issue_292.py b/tests/test_regression_issue_292.py index 155bcfa8..4df81e27 100644 --- a/tests/test_regression_issue_292.py +++ b/tests/test_regression_issue_292.py @@ -331,3 +331,65 @@ def _explode(*_args, **_kwargs): "leaving mode.extras['omega'] as None" in rec.getMessage() for rec in caplog.records ), "expected a debug log record naming the soft-failed slot" + + +# --------------------------------------------------------------------------- +# REMOVE-AT-V1.0: deprecation back-fill for issue #299 alias keys. +# --------------------------------------------------------------------------- + + +def test_populate_extras_back_fills_legacy_alias_slots(): + """Canonical-slot population back-fills the legacy alias slots. + + Issue #299 renamed the surface-frame output slots ``"alpha_i"`` / + ``"beta_out"`` to ``"incidence"`` / ``"emergence"``. An in-tree + YAML mode after migration declares only the canonical slot, but + out-of-tree callers that read ``result.extras["alpha_i"]`` in + v0.11.x and earlier must continue to see the populated value + through the deprecation cycle. ``_populate_output_extras`` writes + the same per-solution list into the legacy slot whenever the + canonical slot is declared, so the legacy read path keeps working + even after the YAML is migrated. + + Hand-build a mode declaring only the canonical slots so this test + exercises the back-fill in isolation (the in-tree YAML still + declares legacy slots at the Step 1+2 boundary, and the legacy + slot is populated by the main computer-dict loop in that case). + """ + from ad_hoc_diffractometer.mode import REQUIRED + from ad_hoc_diffractometer.mode import ConstraintSet + from ad_hoc_diffractometer.mode import ReferenceConstraint + from ad_hoc_diffractometer.mode import SampleConstraint + + g = _setup_cubic("psic") + # Build a B3-style ConstraintSet that declares only the canonical + # extras slots — no ``alpha_i`` / ``beta_out`` keys. The back-fill + # is the only path that can put values into those legacy slots. + g.modes["__back_fill_probe"] = ConstraintSet( + [ + SampleConstraint("chi", 0.0), + SampleConstraint("phi", 0.0), + ReferenceConstraint("incidence", 0.0), + ], + computed=["mu", "eta", "nu", "delta"], + extras={ + "n_hat": REQUIRED, + "incidence": None, + "emergence": None, + }, + cut_points={"eta": -180.0}, + ) + g.surface_normal = (0, 0, 1) + g.mode_name = "__back_fill_probe" + sols = g.forward(0, 1, 1) + assert len(sols) > 0 + + extras = g.modes["__back_fill_probe"].extras + # Canonical slots populated as before. + assert isinstance(extras["incidence"], list) + assert isinstance(extras["emergence"], list) + assert len(extras["incidence"]) == len(sols) + # Legacy slots back-filled with the same lists (identity, not just + # equality — the back-fill aliases the canonical list object). + assert extras["alpha_i"] is extras["incidence"] + assert extras["beta_out"] is extras["emergence"] From cc8f6c1bce287fa46dba938fcbb584b367e19f01 Mon Sep 17 00:00:00 2001 From: Pete Jemian Date: Wed, 17 Jun 2026 16:51:25 -0500 Subject: [PATCH 3/7] refactor(#299) migrate YAML loader and packaged YAML to canonical names MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Loader: new _rewrite_legacy_reference_names() walks the parsed document, rewrites legacy reference-constraint names ('alpha_i' / 'beta_out') and extras-dict keys to the canonical names ('incidence' / 'emergence'), and emits one DeprecationWarning per file naming the source. Performed before constraint construction so the downstream ReferenceConstraint.__init__ never sees a legacy name and the per-constraint warnings introduced in the first commit on this branch do not fire for YAML-loaded geometries. JSON schema (geometries/schema.json): the reference.name enum now lists the canonical names first. Legacy names are retained as accepted values so unmigrated out-of-tree YAML continues to validate. A description field on the enum documents the deprecation and points at issue #299. No schema_revision bump: the change is backward compatible at the schema level and the deprecation-warning channel already delivers the migration signal. Packaged YAML files (psic, sixc, zaxis, s2d2): every reference constraint name and every extras-dict key migrated to the canonical spelling. Two YAML-comment occurrences of 'alpha_i' as a symbol name in psic.yml updated to 'incidence' for accuracy. Mode names (fixed_alpha_i_*, fixed_beta_out_*, fixed_alpha_i_fixed_chi_fixed_phi) are out of scope and untouched: mode names are a separate identifier axis from the reference-constraint vocabulary. test_regression_issue_267.py: 26 hand-built Python references bulk-renamed to match the migrated YAML so the declarative-vs-reference equivalence check still passes. test_geometry_loader.py: three new tests cover the loader rewriting path — single-warning-per-file, drops-legacy-when-mixed-with-canonical, and skips-rewriting-when-already-canonical. All deprecation infrastructure tagged 'REMOVE-AT-V1.0:' for the release-time cleanup tracked by #301. Contributed by: OpenCode (argo/claudeopus47) --- src/ad_hoc_diffractometer/geometries/psic.yml | 44 +++--- src/ad_hoc_diffractometer/geometries/s2d2.yml | 4 +- .../geometries/schema.json | 3 +- src/ad_hoc_diffractometer/geometries/sixc.yml | 16 +- .../geometries/zaxis.yml | 10 +- src/ad_hoc_diffractometer/geometry_loader.py | 83 ++++++++++ tests/test_geometry_loader.py | 149 ++++++++++++++++++ tests/test_regression_issue_267.py | 42 ++--- 8 files changed, 292 insertions(+), 59 deletions(-) diff --git a/src/ad_hoc_diffractometer/geometries/psic.yml b/src/ad_hoc_diffractometer/geometries/psic.yml index 60576396..a4d3beb7 100644 --- a/src/ad_hoc_diffractometer/geometries/psic.yml +++ b/src/ad_hoc_diffractometer/geometries/psic.yml @@ -53,7 +53,7 @@ documentation: | - 9 horizontal-scattering-plane mirror modes (eta = delta = 0). - 1 hybrid mode fixed_alpha_i_fixed_chi_fixed_phi (#264) that - fixes chi, phi, and alpha_i and lets every other angle float. + fixes chi, phi, and incidence and lets every other angle float. - 2 zone modes (You 1999 §6, SPEC ``setmode 5``): scattering vector Q is confined to the plane defined by two reciprocal-lattice @@ -119,23 +119,23 @@ modes: constraints: - {type: sample, stage: mu, value: 0.0} - {type: detector, stage: nu, value: 0.0} - - {type: reference, name: alpha_i, value: 0.0} + - {type: reference, name: incidence, value: 0.0} computed: [eta, chi, phi, delta] extras: n_hat: REQUIRED - alpha_i: null - beta_out: null + incidence: null + emergence: null fixed_beta_out_vertical: constraints: - {type: sample, stage: mu, value: 0.0} - {type: detector, stage: nu, value: 0.0} - - {type: reference, name: beta_out, value: 0.0} + - {type: reference, name: emergence, value: 0.0} computed: [eta, chi, phi, delta] extras: n_hat: REQUIRED - alpha_i: null - beta_out: null + incidence: null + emergence: null alpha_eq_beta_vertical: constraints: @@ -145,8 +145,8 @@ modes: computed: [eta, chi, phi, delta] extras: n_hat: REQUIRED - alpha_i: null - beta_out: null + incidence: null + emergence: null # Per @jwkim-anl, issue #264: drop the bisect(eta, delta) constraint # entirely. "vertical" means nu = 0 (detector pin); mu is pinned @@ -171,20 +171,20 @@ modes: psi: null # Per @jwkim-anl, issue #264: a useful psic mode that fixes chi and - # phi and the incidence angle alpha_i, leaving mu, eta, nu, delta - # to be determined from the Bragg condition + the alpha_i target. + # phi and the incidence angle, leaving mu, eta, nu, delta + # to be determined from the Bragg condition + the incidence target. # Two free sample stages and two free detector stages — solved by a # dedicated 4-D Newton. fixed_alpha_i_fixed_chi_fixed_phi: constraints: - {type: sample, stage: chi, value: 0.0} - {type: sample, stage: phi, value: 0.0} - - {type: reference, name: alpha_i, value: 0.0} + - {type: reference, name: incidence, value: 0.0} computed: [mu, eta, nu, delta] extras: n_hat: REQUIRED - alpha_i: null - beta_out: null + incidence: null + emergence: null # SPEC ``setmode d1 0 0 0`` family — issue #264. # OMEGA = angle between Q and the plane of the chi circle @@ -243,23 +243,23 @@ modes: constraints: - {type: sample, stage: eta, value: 0.0} - {type: detector, stage: delta, value: 0.0} - - {type: reference, name: alpha_i, value: 0.0} + - {type: reference, name: incidence, value: 0.0} computed: [mu, chi, phi, nu] extras: n_hat: REQUIRED - alpha_i: null - beta_out: null + incidence: null + emergence: null fixed_beta_out_horizontal: constraints: - {type: sample, stage: eta, value: 0.0} - {type: detector, stage: delta, value: 0.0} - - {type: reference, name: beta_out, value: 0.0} + - {type: reference, name: emergence, value: 0.0} computed: [mu, chi, phi, nu] extras: n_hat: REQUIRED - alpha_i: null - beta_out: null + incidence: null + emergence: null alpha_eq_beta_horizontal: constraints: @@ -269,8 +269,8 @@ modes: computed: [mu, chi, phi, nu] extras: n_hat: REQUIRED - alpha_i: null - beta_out: null + incidence: null + emergence: null # Per @jwkim-anl, issue #264: drop the bisect(mu, nu) constraint # entirely. "horizontal" means delta = 0 (detector pin); eta is diff --git a/src/ad_hoc_diffractometer/geometries/s2d2.yml b/src/ad_hoc_diffractometer/geometries/s2d2.yml index c9e8377f..d03de4f3 100644 --- a/src/ad_hoc_diffractometer/geometries/s2d2.yml +++ b/src/ad_hoc_diffractometer/geometries/s2d2.yml @@ -73,5 +73,5 @@ modes: computed: [mu, Z, nu, delta] extras: n_hat: REQUIRED - alpha_i: null - beta_out: null + incidence: null + emergence: null diff --git a/src/ad_hoc_diffractometer/geometries/schema.json b/src/ad_hoc_diffractometer/geometries/schema.json index d605a188..33579aa4 100644 --- a/src/ad_hoc_diffractometer/geometries/schema.json +++ b/src/ad_hoc_diffractometer/geometries/schema.json @@ -238,7 +238,8 @@ "type": { "const": "reference" }, "name": { "type": "string", - "enum": ["psi", "alpha_i", "beta_out", "a_eq_b", "naz", "omega"] + "description": "Reference-constraint pseudo-angle name. The canonical names are 'psi', 'incidence', 'emergence', 'a_eq_b', 'naz', 'omega'. The legacy names 'alpha_i' and 'beta_out' are accepted for backward compatibility (REMOVE-AT-V1.0) and rewritten to 'incidence' and 'emergence' respectively by the loader, which emits one DeprecationWarning per file (issue #299).", + "enum": ["psi", "incidence", "emergence", "a_eq_b", "naz", "omega", "alpha_i", "beta_out"] }, "value": { "oneOf": [ diff --git a/src/ad_hoc_diffractometer/geometries/sixc.yml b/src/ad_hoc_diffractometer/geometries/sixc.yml index 241cd28d..1dee0051 100644 --- a/src/ad_hoc_diffractometer/geometries/sixc.yml +++ b/src/ad_hoc_diffractometer/geometries/sixc.yml @@ -87,23 +87,23 @@ modes: constraints: - {type: sample, stage: alpha, value: 0.0} - {type: sample, stage: chi, value: 0.0} - - {type: reference, name: alpha_i, value: 0.0} + - {type: reference, name: incidence, value: 0.0} computed: [omega, delta, gamma] extras: n_hat: REQUIRED - alpha_i: null - beta_out: null + incidence: null + emergence: null fixed_beta_zaxis: constraints: - {type: detector, stage: gamma, value: 0.0} - {type: sample, stage: chi, value: 0.0} - - {type: reference, name: beta_out, value: 0.0} + - {type: reference, name: emergence, value: 0.0} computed: [omega, delta, alpha] extras: n_hat: REQUIRED - alpha_i: null - beta_out: null + incidence: null + emergence: null alpha_eq_beta_zaxis: constraints: @@ -113,5 +113,5 @@ modes: computed: [omega, delta, alpha, gamma] extras: n_hat: REQUIRED - alpha_i: null - beta_out: null + incidence: null + emergence: null diff --git a/src/ad_hoc_diffractometer/geometries/zaxis.yml b/src/ad_hoc_diffractometer/geometries/zaxis.yml index 771b2772..56ed01cc 100644 --- a/src/ad_hoc_diffractometer/geometries/zaxis.yml +++ b/src/ad_hoc_diffractometer/geometries/zaxis.yml @@ -64,12 +64,12 @@ stages: modes: zaxis: constraints: - - {type: reference, name: alpha_i, value: 0.0} + - {type: reference, name: incidence, value: 0.0} computed: [Z, delta, gamma] extras: n_hat: REQUIRED - alpha_i: null - beta_out: null + incidence: null + emergence: null reflectivity: constraints: @@ -77,5 +77,5 @@ modes: computed: [Z, delta, alpha, gamma] extras: n_hat: REQUIRED - alpha_i: null - beta_out: null + incidence: null + emergence: null diff --git a/src/ad_hoc_diffractometer/geometry_loader.py b/src/ad_hoc_diffractometer/geometry_loader.py index f6f2152e..9f2650fc 100644 --- a/src/ad_hoc_diffractometer/geometry_loader.py +++ b/src/ad_hoc_diffractometer/geometry_loader.py @@ -88,6 +88,7 @@ from .factories import KAPPA_ALPHA_DEFAULT from .kappa import KappaPseudoAngleConvention from .kappa import kappa_axis_from_eulerian +from .mode import _DEPRECATED_REFERENCE_NAME_ALIASES # noqa: PLC2701 from .mode import REQUIRED from .mode import BisectConstraint from .mode import ConstraintSet @@ -678,6 +679,81 @@ def _is_geometry_yaml_text(text: str) -> tuple[bool, Any]: # --------------------------------------------------------------------------- +def _rewrite_legacy_reference_names( + doc: dict[str, Any], + *, + source_label: str, +) -> None: + """REMOVE-AT-V1.0: rewrite legacy reference-constraint names in place. + + Walks the parsed YAML document, finds any occurrence of the + deprecated reference-constraint names (``"alpha_i"`` / ``"beta_out"``) + in mode constraint specs and extras-dict keys, rewrites them to the + canonical names (``"incidence"`` / ``"emergence"``), and emits one + :class:`DeprecationWarning` per file naming the source and listing + the legacy names found. + + Performing the rewrite at file-load granularity (rather than per + constraint, as the :class:`ReferenceConstraint` constructor would + do) collapses what could be N warnings per file (one per legacy + occurrence) into a single warning that names the file. The + downstream :class:`ReferenceConstraint` construction never sees a + legacy name and therefore does not re-warn. + + Operates in-place on ``doc``. A document containing no legacy + names is left untouched and no warning fires. + """ + modes = doc.get("modes") + if not isinstance(modes, dict): + return + found: set[str] = set() + for mode_spec in modes.values(): + if not isinstance(mode_spec, dict): + continue + # Rewrite constraint spec names. + constraints = mode_spec.get("constraints") + if isinstance(constraints, list): + for c in constraints: + if not isinstance(c, dict): + continue + if c.get("type") != "reference": + continue + cname = c.get("name") + if ( + isinstance(cname, str) + and cname in _DEPRECATED_REFERENCE_NAME_ALIASES + ): + found.add(cname) + c["name"] = _DEPRECATED_REFERENCE_NAME_ALIASES[cname] + # Rewrite extras-dict keys. + extras = mode_spec.get("extras") + if isinstance(extras, dict): + legacy_keys = [ + k for k in list(extras) if k in _DEPRECATED_REFERENCE_NAME_ALIASES + ] + for legacy_key in legacy_keys: + canonical_key = _DEPRECATED_REFERENCE_NAME_ALIASES[legacy_key] + found.add(legacy_key) + # Preserve insertion order: replace the legacy key with + # the canonical key at the same position. If the + # canonical key is already present (mixed legacy / + # canonical declaration), drop the legacy entry. + if canonical_key in extras: + del extras[legacy_key] + else: + extras[canonical_key] = extras.pop(legacy_key) + if found: + warnings.warn( + f"{source_label}: reference-constraint names " + f"{sorted(found)!r} are deprecated; YAML rewritten to use " + f"the canonical names " + f"{sorted(_DEPRECATED_REFERENCE_NAME_ALIASES[n] for n in found)!r}." + f" (issue #299)", + DeprecationWarning, + stacklevel=4, + ) + + def _construct_from_doc( doc: dict[str, Any], *, @@ -705,6 +781,13 @@ def _construct_from_doc( _validate_marker(doc) _check_unknown_keys(doc, _TOP_LEVEL_KEYS, "at top level") + # REMOVE-AT-V1.0: rewrite legacy reference-constraint names and + # extras keys in the parsed document. Performed here, once per + # file, so the downstream ReferenceConstraint construction path + # never sees a legacy name and does not emit per-constraint + # deprecation warnings. + _rewrite_legacy_reference_names(doc, source_label=source_label) + if "name" not in doc: raise GeometrySchemaError( f"{source_label}: missing required top-level key 'name'." diff --git a/tests/test_geometry_loader.py b/tests/test_geometry_loader.py index a273c2d5..42946274 100644 --- a/tests/test_geometry_loader.py +++ b/tests/test_geometry_loader.py @@ -1378,3 +1378,152 @@ def test_basis_defensive_det_check_via_monkeypatch(monkeypatch): with pytest.raises(GeometrySchemaError, match="degenerate or non-orthonormal"): _validate_basis(basis) monkeypatch.setattr(np.linalg, "det", real_det) + + +# --------------------------------------------------------------------------- +# REMOVE-AT-V1.0: loader rewriting of legacy reference-constraint names +# (issue #299). +# --------------------------------------------------------------------------- + +_LEGACY_REFERENCE_YAML = """\ +ad_hoc_diffractometer_geometry: + schema_revision: 1 +name: legacy_alias +documentation: A geometry whose YAML still uses the legacy reference names. +basis: BL +stages: + - {name: mu, axis: -transverse, parent: null, role: sample} + - {name: eta, axis: -transverse, parent: mu, role: sample} + - {name: chi, axis: +longitudinal, parent: eta, role: sample} + - {name: phi, axis: -transverse, parent: chi, role: sample} + - {name: nu, axis: +vertical, parent: null, role: detector} + - {name: delta, axis: -transverse, parent: nu, role: detector} +modes: + legacy_incidence: + default: true + constraints: + - {type: sample, stage: mu, value: 0.0} + - {type: sample, stage: eta, value: 0.0} + - {type: sample, stage: chi, value: 0.0} + - {type: reference, name: alpha_i, value: 0.0} + computed: [phi, nu, delta] + extras: + n_hat: REQUIRED + alpha_i: null + beta_out: null +""" + + +def test_loader_rewrites_legacy_reference_names_emits_single_warning(): + """Loading YAML with legacy ``alpha_i`` / ``beta_out`` names emits one + DeprecationWarning per file and produces a geometry with canonical + constraint and extras keys. + + Issue #299. An out-of-tree YAML file written against the v0.11.x + API still uses the deprecated reference-constraint names; the loader + rewrites them in-place and surfaces a single deprecation warning + naming the file rather than per-constraint warnings from + :class:`ReferenceConstraint.__init__`. + """ + import warnings + + with warnings.catch_warnings(record=True) as captured: + warnings.simplefilter("always") + g = load_geometry_file(_LEGACY_REFERENCE_YAML) + + # Exactly one DeprecationWarning fires, from the loader's rewriting + # path; the per-constraint warning from ReferenceConstraint.__init__ + # is suppressed because the loader rewrites the names before + # construction. + dep_warnings = [w for w in captured if issubclass(w.category, DeprecationWarning)] + rewriting_warnings = [w for w in dep_warnings if "YAML rewritten" in str(w.message)] + assert len(rewriting_warnings) == 1, ( + f"expected exactly one loader rewriting DeprecationWarning; got " + f"{len(rewriting_warnings)}: {[str(w.message) for w in rewriting_warnings]}" + ) + msg = str(rewriting_warnings[0].message) + assert "alpha_i" in msg and "beta_out" in msg + assert "incidence" in msg and "emergence" in msg + assert "issue #299" in msg + + # Per-constraint warnings from ReferenceConstraint.__init__ are + # absent (they would fire only if a legacy name reached the + # constructor, which the loader rewrite prevents). + per_constraint_warnings = [ + w for w in dep_warnings if "ReferenceConstraint name" in str(w.message) + ] + assert per_constraint_warnings == [], ( + f"loader rewrite should suppress per-constraint warnings; got " + f"{[str(w.message) for w in per_constraint_warnings]}" + ) + + # The loaded geometry has the canonical constraint name and the + # canonical extras keys (no legacy spellings). + cs = g.modes["legacy_incidence"] + from ad_hoc_diffractometer.mode import ReferenceConstraint + + ref_constraints = [c for c in cs.constraints if isinstance(c, ReferenceConstraint)] + assert len(ref_constraints) == 1 + assert ref_constraints[0].name == "incidence" + assert "incidence" in cs.extras + assert "emergence" in cs.extras + assert "alpha_i" not in cs.extras + assert "beta_out" not in cs.extras + + +def test_loader_drops_legacy_extras_key_when_canonical_already_present(): + """If a YAML extras dict declares both the legacy and the canonical + key (a malformed mixed declaration that nothing in the in-tree + code produces but an out-of-tree author might write), the loader + drops the legacy entry rather than overwriting the canonical one. + The canonical slot's value wins; the legacy slot's value is + discarded. + """ + mixed_yaml = _LEGACY_REFERENCE_YAML.replace( + "alpha_i: null\n beta_out: null", + "alpha_i: null\n incidence: null\n beta_out: null", + ) + import warnings + + with warnings.catch_warnings(record=True) as captured: + warnings.simplefilter("always") + g = load_geometry_file(mixed_yaml) + # Loader still fires the rewriting warning (legacy names were present). + dep_warnings = [ + w + for w in captured + if issubclass(w.category, DeprecationWarning) + and "YAML rewritten" in str(w.message) + ] + assert len(dep_warnings) == 1 + # Canonical key survives; legacy key is gone. + cs = g.modes["legacy_incidence"] + assert "incidence" in cs.extras + assert "alpha_i" not in cs.extras + + +def test_loader_skips_rewriting_when_yaml_is_already_canonical(): + """No DeprecationWarning fires for YAML that already uses the canonical + names — the rewriting path activates only when legacy names are + found. This protects in-tree YAML files (migrated to canonical + names) from triggering spurious deprecation noise at every + ``make_geometry()`` call. + """ + import warnings + + canonical_yaml = _LEGACY_REFERENCE_YAML.replace("alpha_i", "incidence").replace( + "beta_out", "emergence" + ) + with warnings.catch_warnings(record=True) as captured: + warnings.simplefilter("always") + load_geometry_file(canonical_yaml) + + dep_warnings = [ + w + for w in captured + if issubclass(w.category, DeprecationWarning) and "issue #299" in str(w.message) + ] + assert dep_warnings == [], ( + f"canonical YAML must not trigger any issue-#299 deprecation " + f"warnings; got {[str(w.message) for w in dep_warnings]}" + ) diff --git a/tests/test_regression_issue_267.py b/tests/test_regression_issue_267.py index 0edea15c..eda385a0 100644 --- a/tests/test_regression_issue_267.py +++ b/tests/test_regression_issue_267.py @@ -188,19 +188,19 @@ def _reference_psic() -> AdHocDiffractometer: [ SampleConstraint("mu", 0.0), DetectorConstraint("nu", 0.0), - ReferenceConstraint("alpha_i", 0.0), + ReferenceConstraint("incidence", 0.0), ], computed=["eta", "chi", "phi", "delta"], - extras={"n_hat": REQUIRED, "alpha_i": None, "beta_out": None}, + extras={"n_hat": REQUIRED, "incidence": None, "emergence": None}, ), "fixed_beta_out_vertical": ConstraintSet( [ SampleConstraint("mu", 0.0), DetectorConstraint("nu", 0.0), - ReferenceConstraint("beta_out", 0.0), + ReferenceConstraint("emergence", 0.0), ], computed=["eta", "chi", "phi", "delta"], - extras={"n_hat": REQUIRED, "alpha_i": None, "beta_out": None}, + extras={"n_hat": REQUIRED, "incidence": None, "emergence": None}, ), "alpha_eq_beta_vertical": ConstraintSet( [ @@ -209,7 +209,7 @@ def _reference_psic() -> AdHocDiffractometer: ReferenceConstraint("a_eq_b", True), ], computed=["eta", "chi", "phi", "delta"], - extras={"n_hat": REQUIRED, "alpha_i": None, "beta_out": None}, + extras={"n_hat": REQUIRED, "incidence": None, "emergence": None}, ), "fixed_psi_vertical": ConstraintSet( [ @@ -224,10 +224,10 @@ def _reference_psic() -> AdHocDiffractometer: [ SampleConstraint("chi", 0.0), SampleConstraint("phi", 0.0), - ReferenceConstraint("alpha_i", 0.0), + ReferenceConstraint("incidence", 0.0), ], computed=["mu", "eta", "nu", "delta"], - extras={"n_hat": REQUIRED, "alpha_i": None, "beta_out": None}, + extras={"n_hat": REQUIRED, "incidence": None, "emergence": None}, ), "fixed_omega_vertical": ConstraintSet( [ @@ -275,19 +275,19 @@ def _reference_psic() -> AdHocDiffractometer: [ SampleConstraint("eta", 0.0), DetectorConstraint("delta", 0.0), - ReferenceConstraint("alpha_i", 0.0), + ReferenceConstraint("incidence", 0.0), ], computed=["mu", "chi", "phi", "nu"], - extras={"n_hat": REQUIRED, "alpha_i": None, "beta_out": None}, + extras={"n_hat": REQUIRED, "incidence": None, "emergence": None}, ), "fixed_beta_out_horizontal": ConstraintSet( [ SampleConstraint("eta", 0.0), DetectorConstraint("delta", 0.0), - ReferenceConstraint("beta_out", 0.0), + ReferenceConstraint("emergence", 0.0), ], computed=["mu", "chi", "phi", "nu"], - extras={"n_hat": REQUIRED, "alpha_i": None, "beta_out": None}, + extras={"n_hat": REQUIRED, "incidence": None, "emergence": None}, ), "alpha_eq_beta_horizontal": ConstraintSet( [ @@ -296,7 +296,7 @@ def _reference_psic() -> AdHocDiffractometer: ReferenceConstraint("a_eq_b", True), ], computed=["mu", "chi", "phi", "nu"], - extras={"n_hat": REQUIRED, "alpha_i": None, "beta_out": None}, + extras={"n_hat": REQUIRED, "incidence": None, "emergence": None}, ), "fixed_psi_horizontal": ConstraintSet( [ @@ -725,19 +725,19 @@ def _reference_sixc() -> AdHocDiffractometer: [ SampleConstraint("alpha", 0.0), SampleConstraint("chi", 0.0), - ReferenceConstraint("alpha_i", 0.0), + ReferenceConstraint("incidence", 0.0), ], computed=["omega", "delta", "gamma"], - extras={"n_hat": REQUIRED, "alpha_i": None, "beta_out": None}, + extras={"n_hat": REQUIRED, "incidence": None, "emergence": None}, ), "fixed_beta_zaxis": ConstraintSet( [ DetectorConstraint("gamma", 0.0), SampleConstraint("chi", 0.0), - ReferenceConstraint("beta_out", 0.0), + ReferenceConstraint("emergence", 0.0), ], computed=["omega", "delta", "alpha"], - extras={"n_hat": REQUIRED, "alpha_i": None, "beta_out": None}, + extras={"n_hat": REQUIRED, "incidence": None, "emergence": None}, ), "alpha_eq_beta_zaxis": ConstraintSet( [ @@ -746,7 +746,7 @@ def _reference_sixc() -> AdHocDiffractometer: ReferenceConstraint("a_eq_b", True), ], computed=["omega", "delta", "alpha", "gamma"], - extras={"n_hat": REQUIRED, "alpha_i": None, "beta_out": None}, + extras={"n_hat": REQUIRED, "incidence": None, "emergence": None}, ), } return AdHocDiffractometer( @@ -777,14 +777,14 @@ def _reference_zaxis() -> AdHocDiffractometer: ] modes = { "zaxis": ConstraintSet( - [ReferenceConstraint("alpha_i", 0.0)], + [ReferenceConstraint("incidence", 0.0)], computed=["Z", "delta", "gamma"], - extras={"n_hat": REQUIRED, "alpha_i": None, "beta_out": None}, + extras={"n_hat": REQUIRED, "incidence": None, "emergence": None}, ), "reflectivity": ConstraintSet( [ReferenceConstraint("a_eq_b", True)], computed=["Z", "delta", "alpha", "gamma"], - extras={"n_hat": REQUIRED, "alpha_i": None, "beta_out": None}, + extras={"n_hat": REQUIRED, "incidence": None, "emergence": None}, ), } return AdHocDiffractometer( @@ -820,7 +820,7 @@ def _reference_s2d2() -> AdHocDiffractometer: "reflectivity": ConstraintSet( [ReferenceConstraint("a_eq_b", True)], computed=["mu", "Z", "nu", "delta"], - extras={"n_hat": REQUIRED, "alpha_i": None, "beta_out": None}, + extras={"n_hat": REQUIRED, "incidence": None, "emergence": None}, ), } return AdHocDiffractometer( From 8bc16ab1481a68e9369376b30bbc0b1f54ac93c4 Mon Sep 17 00:00:00 2001 From: Pete Jemian Date: Wed, 17 Jun 2026 17:35:43 -0500 Subject: [PATCH 4/7] refactor(#299) rename Python methods and reference long-form wrappers Complete the Option D method-name rename that earlier commits on this branch had quietly skipped. Canonical names match the constraint-string vocabulary established by the first commit: surface.alpha_i() -> surface.incidence() surface.alpha_f() -> surface.emergence() AdHocDiffractometer.alpha_i() -> AdHocDiffractometer.incidence() AdHocDiffractometer.alpha_f() -> AdHocDiffractometer.emergence() reference.exit_angle() -> reference.emergence_angle() The legacy spellings are kept as forwarding stubs that emit DeprecationWarning on every call and delegate to the canonical implementation. reference.incidence_angle() is already English and already canonical-aligned; its internal call is updated to use the canonical geometry method. Internal callers updated: forward.py's _populate_output_extras and _surface_residual now import emergence_angle; surface.py's is_specular and is_evanescent call the canonical incidence/emergence functions directly; diffractometer.py docstrings naming the methods updated. Tests: test_surface.py's 13 call sites renamed to canonical methods; four new deprecation-cycle tests cover the alpha_i/alpha_f forwarders (both the geometry method and the standalone surface-module function variants). test_reference.py renamed its exit_angle calls and test function names to emergence_angle; one new test covers the exit_angle forwarder. test_regression_issue_292.py updated. test_mode.py adds test_reference_constraint_from_dict_accepts_legacy_name to pin the legacy-session round-trip contract. All deprecation infrastructure tagged REMOVE-AT-V1.0 for the cleanup tracked by #301. Contributed by: OpenCode (argo/claudeopus47) --- src/ad_hoc_diffractometer/diffractometer.py | 51 +++++++++--- src/ad_hoc_diffractometer/forward.py | 8 +- src/ad_hoc_diffractometer/reference.py | 46 ++++++++--- src/ad_hoc_diffractometer/surface.py | 57 ++++++++++--- tests/test_mode.py | 43 ++++++++++ tests/test_reference.py | 49 ++++++++--- tests/test_regression_issue_292.py | 4 +- tests/test_surface.py | 91 ++++++++++++++++----- 8 files changed, 277 insertions(+), 72 deletions(-) diff --git a/src/ad_hoc_diffractometer/diffractometer.py b/src/ad_hoc_diffractometer/diffractometer.py index 7034932b..9a19a145 100644 --- a/src/ad_hoc_diffractometer/diffractometer.py +++ b/src/ad_hoc_diffractometer/diffractometer.py @@ -1206,8 +1206,8 @@ def surface_normal(self) -> tuple[float, float, float] | None: The surface normal defines the direction perpendicular to the sample surface in reciprocal space. It is used by the surface geometry - calculations (:meth:`alpha_i`, :meth:`alpha_f`, :meth:`q_components`, - :meth:`is_specular`, :meth:`is_evanescent`). + calculations (:meth:`incidence`, :meth:`emergence`, + :meth:`q_components`, :meth:`is_specular`, :meth:`is_evanescent`). When ``None``, the surface calculations fall back to :attr:`azimuth` if that is set. @@ -1445,7 +1445,7 @@ def detector_offset(self, value: tuple[float, float] | None) -> None: # Surface geometry: incidence / emergence / Q-decomposition methods # ------------------------------------------------------------------ - def alpha_i(self, angles: dict[str, float] | None = None) -> float: + def incidence(self, angles: dict[str, float] | None = None) -> float: """ Angle of incidence αᵢ (degrees). @@ -1465,7 +1465,7 @@ def alpha_i(self, angles: dict[str, float] | None = None) -> float: See Also -------- - alpha_f : Angle of emergence. + emergence : Angle of emergence. Examples -------- @@ -1473,14 +1473,14 @@ def alpha_i(self, angles: dict[str, float] | None = None) -> float: >>> g.wavelength = 1.5406 >>> g.surface_normal = (0, 0, 1) >>> ub_identity(g.sample) - >>> g.alpha_i({"alpha": 5.0, "Z": 0.0, "delta": 20.0, "gamma": 0.0}) + >>> g.incidence({"alpha": 5.0, "Z": 0.0, "delta": 20.0, "gamma": 0.0}) 5.0 """ - from .surface import alpha_i as _alpha_i + from .surface import incidence as _incidence - return _alpha_i(self, angles) + return _incidence(self, angles) - def alpha_f(self, angles: dict[str, float] | None = None) -> float: + def emergence(self, angles: dict[str, float] | None = None) -> float: """ Angle of emergence αf (degrees). @@ -1500,11 +1500,38 @@ def alpha_f(self, angles: dict[str, float] | None = None) -> float: See Also -------- - alpha_i : Angle of incidence. + incidence : Angle of incidence. """ - from .surface import alpha_f as _alpha_f + from .surface import emergence as _emergence + + return _emergence(self, angles) + + # REMOVE-AT-V1.0: deprecated method aliases for the canonical + # incidence / emergence methods. Emit DeprecationWarning on every + # call and delegate to the canonical method. + def alpha_i(self, angles: dict[str, float] | None = None) -> float: + """Deprecated alias for :meth:`incidence`. REMOVE-AT-V1.0.""" + import warnings + + warnings.warn( + "AdHocDiffractometer.alpha_i() is deprecated; " + "use .incidence() instead. (issue #299)", + DeprecationWarning, + stacklevel=2, + ) + return self.incidence(angles) + + def alpha_f(self, angles: dict[str, float] | None = None) -> float: + """Deprecated alias for :meth:`emergence`. REMOVE-AT-V1.0.""" + import warnings - return _alpha_f(self, angles) + warnings.warn( + "AdHocDiffractometer.alpha_f() is deprecated; " + "use .emergence() instead. (issue #299)", + DeprecationWarning, + stacklevel=2, + ) + return self.emergence(angles) def q_components(self, angles: dict[str, float] | None = None) -> dict[str, float]: """ @@ -1522,7 +1549,7 @@ def q_components(self, angles: dict[str, float] | None = None) -> dict[str, floa See Also -------- - alpha_i, alpha_f : Incidence and emergence angles. + incidence, emergence : Incidence and emergence angles. """ from .surface import q_components as _q_components diff --git a/src/ad_hoc_diffractometer/forward.py b/src/ad_hoc_diffractometer/forward.py index db544bab..ca9e632f 100644 --- a/src/ad_hoc_diffractometer/forward.py +++ b/src/ad_hoc_diffractometer/forward.py @@ -522,19 +522,19 @@ def _populate_output_extras( return # Lazy imports — ``reference`` imports from ``forward``. - from .reference import exit_angle as _exit_angle + from .reference import emergence_angle as _emergence_angle from .reference import incidence_angle as _incidence_angle from .reference import omega_pseudo as _omega_pseudo from .reference import psi_angle as _psi_angle computers: dict[str, callable] = { "incidence": _incidence_angle, - "emergence": _exit_angle, + "emergence": _emergence_angle, "psi": _psi_angle, "omega": _omega_pseudo, # REMOVE-AT-V1.0: deprecated aliases routed to the same computers. "alpha_i": _incidence_angle, - "beta_out": _exit_angle, + "beta_out": _emergence_angle, } for key in relevant: @@ -3230,7 +3230,7 @@ def _surface_residual( Returns a float residual in degrees (zero = constraint satisfied). """ - from .reference import exit_angle as _emergence_angle + from .reference import emergence_angle as _emergence_angle from .reference import incidence_angle as _incidence_angle if target_name == "incidence": diff --git a/src/ad_hoc_diffractometer/reference.py b/src/ad_hoc_diffractometer/reference.py index c9c91c9d..e29be383 100644 --- a/src/ad_hoc_diffractometer/reference.py +++ b/src/ad_hoc_diffractometer/reference.py @@ -5,8 +5,8 @@ Provides standalone functions for computing the physical pseudo-angles that appear in :class:`~mode.ReferenceConstraint` conditions: incidence angle, -exit angle, azimuthal angle ψ, lab-frame azimuthal angle naz, and the -SPEC ``OMEGA`` pseudo-angle (angle between Q and the chi-circle plane). +emergence angle, azimuthal angle ψ, lab-frame azimuthal angle naz, and +the SPEC ``OMEGA`` pseudo-angle (angle between Q and the chi-circle plane). These functions require the geometry's :attr:`surface_normal` or :attr:`azimuth` to be set before calling, **except** for @@ -18,8 +18,9 @@ :func:`incidence_angle` Angle of incidence α_i between the incident beam and the sample surface. -:func:`exit_angle` - Angle of exit β_out between the diffracted beam and the sample surface. +:func:`emergence_angle` + Angle of emergence α_f between the diffracted beam and the sample surface. + Accepts the deprecated alias :func:`exit_angle` (REMOVE-AT-V1.0). :func:`psi_angle` Azimuthal angle ψ of the reference vector n̂ about Q (You 1999, eq. 23). @@ -126,18 +127,18 @@ def incidence_angle( * Lohmeier & Vlieg (1993), §4.2. """ _require_surface_normal(geometry) - return geometry.alpha_i(angles=angles) + return geometry.incidence(angles=angles) -def exit_angle( +def emergence_angle( geometry: AdHocDiffractometer, angles: dict[str, float] | None = None, ) -> float: """ - Compute the angle of exit β_out in degrees. + Compute the angle of emergence α_f in degrees. The angle between the diffracted beam and the sample surface. - Positive when the diffracted beam exits through the front face. + Positive when the diffracted beam emerges through the front face. Requires :attr:`~geometry.AdHocDiffractometer.surface_normal` to be set. @@ -152,7 +153,7 @@ def exit_angle( Returns ------- float - Exit angle β_out in degrees. + Emergence angle α_f in degrees. Raises ------ @@ -161,11 +162,30 @@ def exit_angle( References ---------- + * Lohmeier & Vlieg (1993), §4.2, eq. 16. * You (1999), eq. 11. - * Lohmeier & Vlieg (1993), §4.2. """ _require_surface_normal(geometry) - return geometry.alpha_f(angles=angles) + return geometry.emergence(angles=angles) + + +# REMOVE-AT-V1.0: deprecated alias for the canonical emergence_angle +# function. Emit DeprecationWarning on every call and delegate. +def exit_angle( + geometry: AdHocDiffractometer, + angles: dict[str, float] | None = None, +) -> float: + """Deprecated alias for :func:`emergence_angle`. REMOVE-AT-V1.0.""" + import warnings + + warnings.warn( + "ad_hoc_diffractometer.reference.exit_angle() is deprecated; " + "use ad_hoc_diffractometer.reference.emergence_angle() instead. " + "(issue #299)", + DeprecationWarning, + stacklevel=2, + ) + return emergence_angle(geometry, angles) def psi_angle( @@ -484,8 +504,8 @@ def omega_pseudo( Notes ----- - Unlike :func:`incidence_angle`, :func:`exit_angle`, :func:`psi_angle`, - and :func:`naz_angle`, this function does **not** require any + Unlike :func:`incidence_angle`, :func:`emergence_angle`, + :func:`psi_angle`, and :func:`naz_angle`, this function does **not** require any reference vector (``surface_normal`` / ``azimuth``) to be set on the geometry. OMEGA is a pure motor-frame quantity defined by the diffractometer's internal geometry. diff --git a/src/ad_hoc_diffractometer/surface.py b/src/ad_hoc_diffractometer/surface.py index fceb1d40..5d931fd0 100644 --- a/src/ad_hoc_diffractometer/surface.py +++ b/src/ad_hoc_diffractometer/surface.py @@ -12,13 +12,13 @@ Public API ---------- -alpha_i(geometry, angles=None) +incidence(geometry, angles=None) Angle of incidence αᵢ in degrees — angle between the incoming beam - and the sample surface. + and the sample surface. Accepts the deprecated alias ``alpha_i``. -alpha_f(geometry, angles=None) +emergence(geometry, angles=None) Angle of emergence αf in degrees — angle between the diffracted beam - and the sample surface. + and the sample surface. Accepts the deprecated alias ``alpha_f``. q_components(geometry, angles=None) Decompose Q into Q⊥ (perpendicular to sample surface) and Q‖ @@ -203,7 +203,7 @@ def _surface_vectors( # --------------------------------------------------------------------------- -def alpha_i( +def incidence( geometry: AdHocDiffractometer, angles: dict[str, float] | None = None, ) -> float: @@ -240,7 +240,7 @@ def alpha_i( >>> g.wavelength = 1.5406 >>> g.surface_normal = (0, 0, 1) >>> ahd.ub_identity(g.sample) - >>> ai = g.alpha_i({"alpha": 5.0, "Z": 0.0, "delta": 20.0, "gamma": 0.0}) + >>> ai = g.incidence({"alpha": 5.0, "Z": 0.0, "delta": 20.0, "gamma": 0.0}) >>> round(ai, 4) 5.0 @@ -255,7 +255,7 @@ def alpha_i( return math.degrees(math.asin(abs(sin_ai))) -def alpha_f( +def emergence( geometry: AdHocDiffractometer, angles: dict[str, float] | None = None, ) -> float: @@ -294,6 +294,43 @@ def alpha_f( return math.degrees(math.asin(abs(sin_af))) +# REMOVE-AT-V1.0: deprecated aliases for the canonical incidence / +# emergence functions. Emit DeprecationWarning on every call and +# delegate to the canonical implementation. +def alpha_i( + geometry: AdHocDiffractometer, + angles: dict[str, float] | None = None, +) -> float: + """Deprecated alias for :func:`incidence`. REMOVE-AT-V1.0.""" + import warnings + + warnings.warn( + "ad_hoc_diffractometer.surface.alpha_i() is deprecated; " + "use ad_hoc_diffractometer.surface.incidence() instead. " + "(issue #299)", + DeprecationWarning, + stacklevel=2, + ) + return incidence(geometry, angles) + + +def alpha_f( + geometry: AdHocDiffractometer, + angles: dict[str, float] | None = None, +) -> float: + """Deprecated alias for :func:`emergence`. REMOVE-AT-V1.0.""" + import warnings + + warnings.warn( + "ad_hoc_diffractometer.surface.alpha_f() is deprecated; " + "use ad_hoc_diffractometer.surface.emergence() instead. " + "(issue #299)", + DeprecationWarning, + stacklevel=2, + ) + return emergence(geometry, angles) + + def q_components( geometry: AdHocDiffractometer, angles: dict[str, float] | None = None, @@ -387,8 +424,8 @@ def is_specular( ValueError If any required attribute is not set. """ - ai = alpha_i(geometry, angles) - af = alpha_f(geometry, angles) + ai = incidence(geometry, angles) + af = emergence(geometry, angles) return abs(ai - af) <= atol @@ -436,5 +473,5 @@ def is_evanescent( "and is not stored on the geometry. " "Typical values: 0.1°–0.5° for hard X-rays." ) - ai = alpha_i(geometry, angles) + ai = incidence(geometry, angles) return ai < critical_angle_deg diff --git a/tests/test_mode.py b/tests/test_mode.py index f8c4ae07..9f762324 100644 --- a/tests/test_mode.py +++ b/tests/test_mode.py @@ -799,6 +799,49 @@ def test_reference_constraint_deprecated_name_canonicalized( assert rc == ReferenceConstraint(canonical_name, value) +# REMOVE-AT-V1.0: pins the legacy-session round-trip contract — a dict +# saved by ad_hoc_diffractometer <= v0.11.x with a legacy +# reference-constraint name still loads via from_dict, emits one +# DeprecationWarning, and produces a constraint stored under the +# canonical name. +@pytest.mark.parametrize( + "legacy_name, canonical_name", + [ + pytest.param("alpha_i", "incidence", id="alpha_i-to-incidence"), + pytest.param("beta_out", "emergence", id="beta_out-to-emergence"), + ], +) +def test_reference_constraint_from_dict_accepts_legacy_name( + legacy_name, canonical_name +): + """A saved session with a legacy reference-constraint name still loads. + + ``from_dict`` calls ``__init__``, which canonicalizes the legacy + name and emits one :class:`DeprecationWarning`. The reconstructed + constraint compares equal to one built directly with the canonical + name, so any subsequent ``to_dict`` write produces the canonical + spelling. + """ + legacy_session_dict = { + "type": "ReferenceConstraint", + "name": legacy_name, + "value": 5.0, + } + with pytest.warns( + DeprecationWarning, + match=re.escape( + f"ReferenceConstraint name {legacy_name!r} is deprecated; " + f"use {canonical_name!r} instead. (issue #299)" + ), + ): + rc = ReferenceConstraint.from_dict(legacy_session_dict) + assert rc.name == canonical_name + # A second to_dict round-trip writes the canonical name. + assert rc.to_dict()["name"] == canonical_name + # Equal to the constraint built directly with the canonical name. + assert rc == ReferenceConstraint(canonical_name, 5.0) + + def test_reference_constraint_value_coerced_to_float(): rc = ReferenceConstraint("psi", 90) assert isinstance(rc.value, float) diff --git a/tests/test_reference.py b/tests/test_reference.py index 0606175e..4fd0d33c 100644 --- a/tests/test_reference.py +++ b/tests/test_reference.py @@ -5,7 +5,8 @@ Covers: - incidence_angle: requires surface_normal; raises when None - - exit_angle: requires surface_normal; raises when None + - emergence_angle: requires surface_normal; raises when None + (also accepts the deprecated alias ``exit_angle``) - psi_angle: requires azimuth; raises when None - naz_angle: requires surface_normal; raises when None; vertical n̂ gives 0 - ReferenceConstraint.is_implemented(): True when reference set, False when None @@ -26,7 +27,10 @@ from ad_hoc_diffractometer import AdHocDiffractometer from ad_hoc_diffractometer import ReferenceConstraint from ad_hoc_diffractometer import ub_identity -from ad_hoc_diffractometer.reference import exit_angle +from ad_hoc_diffractometer.reference import emergence_angle +from ad_hoc_diffractometer.reference import ( # noqa: F401 — deprecation-test import + exit_angle, +) from ad_hoc_diffractometer.reference import incidence_angle from ad_hoc_diffractometer.reference import natural_psi from ad_hoc_diffractometer.reference import naz_angle @@ -79,25 +83,25 @@ def test_incidence_angle_uses_current_angles_when_none(): # --------------------------------------------------------------------------- -# exit_angle +# emergence_angle # --------------------------------------------------------------------------- -def test_exit_angle_raises_without_surface_normal(): - """exit_angle raises ValueError when surface_normal is None.""" +def test_emergence_angle_raises_without_surface_normal(): + """emergence_angle raises ValueError when surface_normal is None.""" g = _setup_psic() with pytest.raises(ValueError, match=re.escape("surface_normal must be set")): - exit_angle(g) + emergence_angle(g) -def test_exit_angle_with_surface_normal(): - """exit_angle returns a float in [-90, 90] when surface_normal is set.""" +def test_emergence_angle_with_surface_normal(): + """emergence_angle returns a float in [-90, 90] when surface_normal is set.""" g = _setup_psic() g.surface_normal = (0, 0, 1) g.mode_name = "bisecting_vertical" sols = g.forward(1, 0, 0) for s in sols: - af = exit_angle(g, angles=s) + af = emergence_angle(g, angles=s) assert isinstance(af, float) assert -90.0 <= af <= 90.0 @@ -111,7 +115,7 @@ def test_specular_condition_alpha_i_equals_alpha_f(): sols = g.forward(1, 0, 0) for s in sols: ai = incidence_angle(g, angles=s) - af = exit_angle(g, angles=s) + af = emergence_angle(g, angles=s) # At bisecting in vertical plane with transverse surface normal, ai ≈ af assert ai == pytest.approx(af, abs=1e-6) @@ -515,7 +519,7 @@ def test_surface_beta_out_fixed_constraint_satisfied(factory, mode_name, h, k, l solutions = g.forward(h, k, l) assert len(solutions) > 0 for sol in solutions: - bo = exit_angle(g, angles=sol) + bo = emergence_angle(g, angles=sol) assert bo == pytest.approx(0.0, abs=1e-4) @@ -535,7 +539,7 @@ def test_surface_a_eq_b_constraint_satisfied(factory, mode_name, h, k, l): # no assert len(solutions) > 0 for sol in solutions: ai = incidence_angle(g, angles=sol) - bo = exit_angle(g, angles=sol) + bo = emergence_angle(g, angles=sol) assert ai == pytest.approx(bo, abs=1e-4) @@ -804,3 +808,24 @@ def test_natural_psi_independent_of_motor_state(): for stage in g._stages.values(): # noqa: SLF001 stage.angle = 17.5 assert natural_psi(g, 1, 1, 0) == pytest.approx(baseline, abs=1e-12) + + +# REMOVE-AT-V1.0: deprecation-cycle coverage for the legacy long-form +# wrapper ``reference.exit_angle``. + + +def test_exit_angle_function_deprecated(): + """exit_angle() emits DeprecationWarning and delegates to emergence_angle.""" + g = _setup_psic() + g.surface_normal = (0, 0, 1) + g.mode_name = "bisecting_vertical" + sols = g.forward(1, 0, 0) + assert sols, "fixture should return at least one solution" + with pytest.warns( + DeprecationWarning, + match=r"reference\.exit_angle\(\) is deprecated; " + r"use ad_hoc_diffractometer\.reference\.emergence_angle", + ): + value = exit_angle(g, angles=sols[0]) + expected = emergence_angle(g, angles=sols[0]) + assert value == pytest.approx(expected, abs=1e-12) diff --git a/tests/test_regression_issue_292.py b/tests/test_regression_issue_292.py index 4df81e27..6dd21b99 100644 --- a/tests/test_regression_issue_292.py +++ b/tests/test_regression_issue_292.py @@ -37,7 +37,7 @@ import pytest import ad_hoc_diffractometer as ahd -from ad_hoc_diffractometer.reference import exit_angle +from ad_hoc_diffractometer.reference import emergence_angle from ad_hoc_diffractometer.reference import incidence_angle from ad_hoc_diffractometer.reference import omega_pseudo from ad_hoc_diffractometer.reference import psi_angle @@ -114,7 +114,7 @@ def test_psic_b3_populates_alpha_i_and_beta_out_extras( mode.extras["alpha_i"], mode.extras["beta_out"], sols, strict=True ): assert ai_stored == pytest.approx(incidence_angle(g, angles=sol), abs=1e-8) - assert bo_stored == pytest.approx(exit_angle(g, angles=sol), abs=1e-8) + assert bo_stored == pytest.approx(emergence_angle(g, angles=sol), abs=1e-8) assert ai_stored == pytest.approx(alpha_target, abs=1e-3) diff --git a/tests/test_surface.py b/tests/test_surface.py index fc0c5cb3..7c2d7b0e 100644 --- a/tests/test_surface.py +++ b/tests/test_surface.py @@ -37,8 +37,14 @@ import ad_hoc_diffractometer as ahd from ad_hoc_diffractometer import ub_identity from ad_hoc_diffractometer.surface import _surface_vectors -from ad_hoc_diffractometer.surface import alpha_f -from ad_hoc_diffractometer.surface import alpha_i +from ad_hoc_diffractometer.surface import ( # noqa: F401 — deprecation-test imports + alpha_f, +) +from ad_hoc_diffractometer.surface import ( # noqa: F401 — deprecation-test imports + alpha_i, +) +from ad_hoc_diffractometer.surface import emergence +from ad_hoc_diffractometer.surface import incidence from ad_hoc_diffractometer.surface import is_evanescent from ad_hoc_diffractometer.surface import is_specular from ad_hoc_diffractometer.surface import q_components @@ -226,7 +232,7 @@ def test_surface_normal_fallback_to_azimuth(): g.surface_normal = None g.azimuth = (0, 0, 1) # Should not raise and should produce a result - ai = g.alpha_i({"mu": 5.0, "Z": 0.0, "nu": 0.0, "delta": 0.0}) + ai = g.incidence({"mu": 5.0, "Z": 0.0, "nu": 0.0, "delta": 0.0}) assert pytest.approx(ai, abs=1e-6) == 5.0 @@ -250,7 +256,7 @@ def test_alpha_i_s2d2_equals_mu(mu, expected_ai, context): """In s2d2 with surface_normal=(0,0,1), alpha_i = mu exactly.""" g = _make_s2d2() with context: - ai = g.alpha_i({"mu": mu, "Z": 0.0, "nu": 0.0, "delta": 0.0}) + ai = g.incidence({"mu": mu, "Z": 0.0, "nu": 0.0, "delta": 0.0}) assert ai == pytest.approx(expected_ai, abs=1e-6) @@ -272,7 +278,7 @@ def test_alpha_i_zaxis_equals_alpha(alpha_val, expected_ai, context): """In zaxis with surface_normal=(0,0,1), alpha_i = alpha exactly.""" g = _make_zaxis() with context: - ai = g.alpha_i({"alpha": alpha_val, "Z": 0.0, "delta": 10.0, "gamma": 0.0}) + ai = g.incidence({"alpha": alpha_val, "Z": 0.0, "delta": 10.0, "gamma": 0.0}) assert ai == pytest.approx(expected_ai, abs=1e-6) @@ -294,7 +300,7 @@ def test_alpha_f_s2d2_equals_nu_at_delta_zero(nu, expected_af, context): """In s2d2 with delta=0 and surface_normal=(0,0,1), alpha_f = nu exactly.""" g = _make_s2d2() with context: - af = g.alpha_f({"mu": 0.0, "Z": 0.0, "nu": nu, "delta": 0.0}) + af = g.emergence({"mu": 0.0, "Z": 0.0, "nu": nu, "delta": 0.0}) assert af == pytest.approx(expected_af, abs=1e-6) @@ -302,7 +308,7 @@ def test_alpha_f_s2d2_zero_at_any_in_plane_delta(): """In s2d2 with nu=0, alpha_f = 0 for any delta (in-plane only).""" g = _make_s2d2() for delta in [0.0, 10.0, 20.0, 45.0]: - af = g.alpha_f({"mu": 0.0, "Z": 0.0, "nu": 0.0, "delta": delta}) + af = g.emergence({"mu": 0.0, "Z": 0.0, "nu": 0.0, "delta": delta}) assert af == pytest.approx(0.0, abs=1e-6) @@ -353,7 +359,7 @@ def test_alpha_f_zaxis_formula(delta, gamma, expected_af, context): """alpha_f for zaxis under the standard BL1967 detector composition.""" g = _make_zaxis() with context: - af = g.alpha_f({"alpha": 0.0, "Z": 0.0, "delta": delta, "gamma": gamma}) + af = g.emergence({"alpha": 0.0, "Z": 0.0, "delta": delta, "gamma": gamma}) assert af == pytest.approx(expected_af, abs=1e-6) @@ -366,7 +372,7 @@ def test_alpha_i_always_nonnegative(): """alpha_i is always in [0°, 90°] regardless of sign of mu.""" g = _make_s2d2() for mu in [-20.0, -10.0, -5.0, 0.0, 5.0, 10.0, 20.0]: - ai = g.alpha_i({"mu": mu, "Z": 0.0, "nu": 0.0, "delta": 0.0}) + ai = g.incidence({"mu": mu, "Z": 0.0, "nu": 0.0, "delta": 0.0}) assert 0.0 <= ai <= 90.0 @@ -374,7 +380,7 @@ def test_alpha_f_always_nonnegative(): """alpha_f is always in [0°, 90°].""" g = _make_s2d2() for nu in [-20.0, -10.0, 0.0, 10.0, 20.0]: - af = g.alpha_f({"mu": 0.0, "Z": 0.0, "nu": nu, "delta": 0.0}) + af = g.emergence({"mu": 0.0, "Z": 0.0, "nu": nu, "delta": 0.0}) assert 0.0 <= af <= 90.0 @@ -531,7 +537,7 @@ def test_psic_alpha_i_from_mu(): """In psic with surface_normal=(0,0,1), alpha_i = mu (rotation about +x).""" g = _make_psic() for mu in [0.0, 2.0, 5.0, 10.0]: - ai = g.alpha_i( + ai = g.incidence( {"mu": mu, "eta": 0.0, "chi": 0.0, "phi": 0.0, "nu": 0.0, "delta": 0.0} ) assert ai == pytest.approx(mu, abs=1e-6) @@ -541,7 +547,7 @@ def test_psic_alpha_f_from_nu(): """In psic with surface_normal=(0,0,1), alpha_f = nu when delta=0.""" g = _make_psic() for nu in [0.0, 2.0, 5.0, 10.0]: - af = g.alpha_f( + af = g.emergence( {"mu": 0.0, "eta": 0.0, "chi": 0.0, "phi": 0.0, "nu": nu, "delta": 0.0} ) assert af == pytest.approx(nu, abs=1e-6) @@ -556,8 +562,8 @@ def test_default_angles_uses_current_stage_angles(): """When angles=None, current stage angles are used.""" g = _make_s2d2() g.set_angle("mu", 5.0) - ai_explicit = g.alpha_i({"mu": 5.0, "Z": 0.0, "nu": 0.0, "delta": 0.0}) - ai_default = g.alpha_i() # uses current angles + ai_explicit = g.incidence({"mu": 5.0, "Z": 0.0, "nu": 0.0, "delta": 0.0}) + ai_default = g.incidence() # uses current angles assert ai_explicit == pytest.approx(ai_default, abs=1e-10) @@ -566,16 +572,63 @@ def test_default_angles_uses_current_stage_angles(): # --------------------------------------------------------------------------- -def test_standalone_alpha_i(): +def test_standalone_incidence(): + g = _make_s2d2() + angles = {"mu": 5.0, "Z": 0.0, "nu": 0.0, "delta": 0.0} + assert incidence(g, angles) == pytest.approx(5.0, abs=1e-6) + + +def test_standalone_emergence(): + g = _make_s2d2() + angles = {"mu": 0.0, "Z": 0.0, "nu": 5.0, "delta": 0.0} + assert emergence(g, angles) == pytest.approx(5.0, abs=1e-6) + + +# REMOVE-AT-V1.0: deprecation-cycle coverage for the legacy method +# names ``alpha_i`` / ``alpha_f`` on AdHocDiffractometer, and the +# legacy standalone functions ``surface.alpha_i`` / ``surface.alpha_f``. + + +def test_geometry_alpha_i_method_deprecated(): + g = _make_s2d2() + with pytest.warns( + DeprecationWarning, + match=r"AdHocDiffractometer\.alpha_i\(\) is deprecated; use \.incidence", + ): + ai = g.alpha_i({"mu": 5.0, "Z": 0.0, "nu": 0.0, "delta": 0.0}) + assert ai == pytest.approx(5.0, abs=1e-6) + + +def test_geometry_alpha_f_method_deprecated(): + g = _make_s2d2() + with pytest.warns( + DeprecationWarning, + match=r"AdHocDiffractometer\.alpha_f\(\) is deprecated; use \.emergence", + ): + af = g.alpha_f({"mu": 0.0, "Z": 0.0, "nu": 5.0, "delta": 0.0}) + assert af == pytest.approx(5.0, abs=1e-6) + + +def test_surface_alpha_i_function_deprecated(): g = _make_s2d2() angles = {"mu": 5.0, "Z": 0.0, "nu": 0.0, "delta": 0.0} - assert alpha_i(g, angles) == pytest.approx(5.0, abs=1e-6) + with pytest.warns( + DeprecationWarning, + match=r"surface\.alpha_i\(\) is deprecated; use ad_hoc_diffractometer\.surface\.incidence", + ): + ai = alpha_i(g, angles) + assert ai == pytest.approx(5.0, abs=1e-6) -def test_standalone_alpha_f(): +def test_surface_alpha_f_function_deprecated(): g = _make_s2d2() angles = {"mu": 0.0, "Z": 0.0, "nu": 5.0, "delta": 0.0} - assert alpha_f(g, angles) == pytest.approx(5.0, abs=1e-6) + with pytest.warns( + DeprecationWarning, + match=r"surface\.alpha_f\(\) is deprecated; use ad_hoc_diffractometer\.surface\.emergence", + ): + af = alpha_f(g, angles) + assert af == pytest.approx(5.0, abs=1e-6) def test_standalone_q_components(): @@ -642,4 +695,4 @@ def test_surface_normal_zero_in_phi_frame_raises(): # Set UB to all-zeros (degenerate) g.sample.UB = np.zeros((3, 3)) with pytest.raises(ValueError, match=re.escape("zero vector")): - g.alpha_i({"mu": 5.0, "Z": 0.0, "nu": 0.0, "delta": 0.0}) + g.incidence({"mu": 5.0, "Z": 0.0, "nu": 0.0, "delta": 0.0}) From e12f82a0f1d70ec80cabff3124907b60afcfd6e3 Mon Sep 17 00:00:00 2001 From: Pete Jemian Date: Thu, 18 Jun 2026 13:11:45 -0500 Subject: [PATCH 5/7] refactor(#299) rename modes (incidence/emergence/specular) and a_eq_b->specular YAML mode keys: fixed_alpha_i_* -> fixed_incidence_*, fixed_beta_out_* -> fixed_emergence_*, alpha_eq_beta_* -> specular_*, fixed_alpha_zaxis / fixed_beta_zaxis -> fixed_incidence_zaxis / fixed_emergence_zaxis. Constraint: a_eq_b -> specular. ReferenceConstraint canonicalizes the deprecated alias; value-check and dispatch sites in mode.py, forward.py, diffractometer.py, benchmark.py migrate to the canonical 'specular' name. ModeDict: new _DEPRECATED_MODE_NAME_ALIASES map and _canonicalize_mode_name helper. ModeDict.__getitem__ / __setitem__ / __delitem__ canonicalize legacy mode names with DeprecationWarning; __contains__ accepts legacy names silently. AdHocDiffractometer.mode_name setter canonicalizes so the getter never reports a deprecated name. Tests: 137 substitutions across nine test files bulk-renamed to the canonical mode names and constraint name. Existing alias-cycle parametrize blocks extended to cover a_eq_b -> specular. New test_modedict_legacy_mode_name_canonicalized (3 parametrized cases) and test_geometry_mode_name_setter_canonicalizes_legacy_alias. Docs (43 substitutions across 10 files): mode-name and constraint-name mentions migrated to canonical throughout the howto/, geometries/, glossary.md, concepts.md, and reference/declarative_geometry_schema.md. All deprecation infrastructure tagged REMOVE-AT-V1.0 for the cleanup tracked by #301. Contributed by: OpenCode (argo/claudeopus47) --- docs/source/concepts.md | 4 +- docs/source/geometries/psic.md | 52 +++--- docs/source/geometries/s2d2.md | 4 +- docs/source/geometries/sixc.md | 18 +- docs/source/geometries/zaxis.md | 8 +- docs/source/glossary.md | 8 +- docs/source/howto/constraints.md | 32 ++-- docs/source/howto/modes.md | 8 +- docs/source/howto/surface.md | 26 +-- .../reference/declarative_geometry_schema.md | 6 +- src/ad_hoc_diffractometer/benchmark.py | 4 +- src/ad_hoc_diffractometer/diffractometer.py | 11 +- src/ad_hoc_diffractometer/forward.py | 6 +- src/ad_hoc_diffractometer/geometries/psic.yml | 24 +-- src/ad_hoc_diffractometer/geometries/s2d2.yml | 2 +- src/ad_hoc_diffractometer/geometries/sixc.yml | 8 +- .../geometries/zaxis.yml | 2 +- src/ad_hoc_diffractometer/mode.py | 135 +++++++++++---- tests/test_benchmark.py | 8 +- tests/test_diffractometer.py | 4 +- tests/test_forward.py | 22 +-- tests/test_mode.py | 161 ++++++++++++++---- tests/test_reference.py | 48 +++--- tests/test_regression_issue_264.py | 14 +- tests/test_regression_issue_267.py | 30 ++-- tests/test_regression_issue_279.py | 28 +-- tests/test_regression_issue_292.py | 10 +- 27 files changed, 429 insertions(+), 254 deletions(-) diff --git a/docs/source/concepts.md b/docs/source/concepts.md index e5b57b3d..cb96a43a 100644 --- a/docs/source/concepts.md +++ b/docs/source/concepts.md @@ -356,8 +356,8 @@ reference vector n̂ (surface normal, polarization axis, etc.): ```python from ad_hoc_diffractometer import ReferenceConstraint -ReferenceConstraint("alpha_i", 5.0) # incidence angle fixed -ReferenceConstraint("a_eq_b", True) # alpha_i = beta_out (symmetric) +ReferenceConstraint("incidence", 5.0) # incidence angle fixed +ReferenceConstraint("specular", True) # incidence = emergence (symmetric) ``` Taxonomy rules: at most one {class}`~ad_hoc_diffractometer.mode.DetectorConstraint`, diff --git a/docs/source/geometries/psic.md b/docs/source/geometries/psic.md index d7843d95..1f6e7f41 100644 --- a/docs/source/geometries/psic.md +++ b/docs/source/geometries/psic.md @@ -106,11 +106,11 @@ The scattering plane is locked vertical by `mu = 0` and `nu = 0`; | **Computed** | eta, phi, delta | | **Constant during** `forward()` | chi, mu = 0, nu = 0 | -### `fixed_alpha_i_vertical` +### `fixed_incidence_vertical` Incidence angle α_i fixed at declared value (default 0°) in the vertical scattering plane. -Override at run time with `g.modes["fixed_alpha_i_vertical"].with_constraint_values(alpha_i=...)` — see {doc}`../howto/constraints`. +Override at run time with `g.modes["fixed_incidence_vertical"].with_constraint_values(incidence=...)` — see {doc}`../howto/constraints`. Set ``g.surface_normal = (h, k, l)`` before calling ``forward()``. | | | @@ -118,13 +118,13 @@ Set ``g.surface_normal = (h, k, l)`` before calling ``forward()``. | **Computed** | eta, chi, phi, delta | | **Constant during** `forward()` | mu = 0, nu = 0 | | **Extras (input)** | n̂ → set `g.surface_normal = (h, k, l)` | -| **Extras (output)** | alpha_i (incidence angle), beta_out (exit angle) | +| **Extras (output)** | incidence (incidence angle), emergence (exit angle) | -### `fixed_beta_out_vertical` +### `fixed_emergence_vertical` Exit angle β_out fixed at declared value (default 0°) in the vertical scattering plane. -Override at run time with `g.modes["fixed_beta_out_vertical"].with_constraint_values(beta_out=...)` — see {doc}`../howto/constraints`. +Override at run time with `g.modes["fixed_emergence_vertical"].with_constraint_values(emergence=...)` — see {doc}`../howto/constraints`. Set ``g.surface_normal = (h, k, l)`` before calling ``forward()``. | | | @@ -132,9 +132,9 @@ Set ``g.surface_normal = (h, k, l)`` before calling ``forward()``. | **Computed** | eta, chi, phi, delta | | **Constant during** `forward()` | mu = 0, nu = 0 | | **Extras (input)** | n̂ → set `g.surface_normal = (h, k, l)` | -| **Extras (output)** | alpha_i, beta_out | +| **Extras (output)** | incidence, emergence | -### `alpha_eq_beta_vertical` +### `specular_vertical` Symmetric reflection: α_i = β_out in the vertical scattering plane. Set ``g.surface_normal = (h, k, l)`` before calling ``forward()``. @@ -144,7 +144,7 @@ Set ``g.surface_normal = (h, k, l)`` before calling ``forward()``. | **Computed** | eta, chi, phi, delta | | **Constant during** `forward()` | mu = 0, nu = 0 | | **Extras (input)** | n̂ → set `g.surface_normal = (h, k, l)` | -| **Extras (output)** | alpha_i, beta_out | +| **Extras (output)** | incidence, emergence | ### `fixed_psi_vertical` @@ -167,7 +167,7 @@ validated ψ. See {doc}`../howto/surface`. | **Extras (input)** | n̂ → set `g.azimuth = (h, k, l)`; ψ target via `with_constraint_values(psi=...)` | | **Extras (output)** | psi (computed azimuth) | -### `fixed_alpha_i_fixed_chi_fixed_phi` +### `fixed_incidence_fixed_chi_fixed_phi` Issue #264. Two sample stages (`chi`, `phi`) and the incidence angle α_i are all fixed; the four remaining angles `mu`, `eta`, `nu`, `delta` @@ -175,7 +175,7 @@ are solved jointly from the Bragg condition plus the α_i target. This is a 4-D Newton solve that routes through the ``_solve_free_detectors`` solver (both detector stages float to lift the detector arm out of plane as needed). -Override any of the three pinned values at run time with `g.modes["fixed_alpha_i_fixed_chi_fixed_phi"].with_constraint_values(chi=..., phi=..., alpha_i=...)` — see {doc}`../howto/constraints`. +Override any of the three pinned values at run time with `g.modes["fixed_incidence_fixed_chi_fixed_phi"].with_constraint_values(chi=..., phi=..., incidence=...)` — see {doc}`../howto/constraints`. Set ``g.surface_normal = (h, k, l)`` before calling ``forward()``. @@ -184,7 +184,7 @@ Set ``g.surface_normal = (h, k, l)`` before calling ``forward()``. | **Computed** | mu, eta, nu, delta | | **Constant during** `forward()` | chi, phi, α_i = target | | **Extras (input)** | n̂ → set `g.surface_normal = (h, k, l)` | -| **Extras (output)** | alpha_i, beta_out | +| **Extras (output)** | incidence, emergence | ### `fixed_omega_vertical` @@ -296,11 +296,11 @@ is kinematically infeasible in this mode. | **Computed** | mu, phi, nu | | **Constant during** `forward()` | chi, eta = 0, delta = 0 | -### `fixed_alpha_i_horizontal` +### `fixed_incidence_horizontal` Incidence angle α_i fixed at declared value (default 0°) in the horizontal scattering plane. -Override at run time with `g.modes["fixed_alpha_i_horizontal"].with_constraint_values(alpha_i=...)` — see {doc}`../howto/constraints`. +Override at run time with `g.modes["fixed_incidence_horizontal"].with_constraint_values(incidence=...)` — see {doc}`../howto/constraints`. Set ``g.surface_normal = (h, k, l)`` before calling ``forward()``. | | | @@ -308,13 +308,13 @@ Set ``g.surface_normal = (h, k, l)`` before calling ``forward()``. | **Computed** | mu, chi, phi, nu | | **Constant during** `forward()` | eta = 0, delta = 0 | | **Extras (input)** | n̂ → set `g.surface_normal = (h, k, l)` | -| **Extras (output)** | alpha_i, beta_out | +| **Extras (output)** | incidence, emergence | -### `fixed_beta_out_horizontal` +### `fixed_emergence_horizontal` Exit angle β_out fixed at declared value (default 0°) in the horizontal scattering plane. -Override at run time with `g.modes["fixed_beta_out_horizontal"].with_constraint_values(beta_out=...)` — see {doc}`../howto/constraints`. +Override at run time with `g.modes["fixed_emergence_horizontal"].with_constraint_values(emergence=...)` — see {doc}`../howto/constraints`. Set ``g.surface_normal = (h, k, l)`` before calling ``forward()``. | | | @@ -322,9 +322,9 @@ Set ``g.surface_normal = (h, k, l)`` before calling ``forward()``. | **Computed** | mu, chi, phi, nu | | **Constant during** `forward()` | eta = 0, delta = 0 | | **Extras (input)** | n̂ → set `g.surface_normal = (h, k, l)` | -| **Extras (output)** | alpha_i, beta_out | +| **Extras (output)** | incidence, emergence | -### `alpha_eq_beta_horizontal` +### `specular_horizontal` Symmetric reflection: α_i = β_out in the horizontal scattering plane. Set ``g.surface_normal = (h, k, l)`` before calling ``forward()``. @@ -334,7 +334,7 @@ Set ``g.surface_normal = (h, k, l)`` before calling ``forward()``. | **Computed** | mu, chi, phi, nu | | **Constant during** `forward()` | eta = 0, delta = 0 | | **Extras (input)** | n̂ → set `g.surface_normal = (h, k, l)` | -| **Extras (output)** | alpha_i, beta_out | +| **Extras (output)** | incidence, emergence | ### `fixed_psi_horizontal` @@ -449,23 +449,23 @@ the Hkl/Soleil `E6C` `hkl` engine, and You (1999). | `bisecting_vertical` | `(2,0,5,0,0)` | `bissector_vertical` | §5.1 | | `fixed_phi_vertical` | `(2,0,4,2,0)` | `constant_phi_vertical` | §5.2 | | `fixed_chi_vertical` | `(2,0,3,2,0)` | `constant_chi_vertical` | §5.2 | -| `fixed_alpha_i_vertical` | `(2,2,2,0,0)` | — | §6.1 | -| `fixed_beta_out_vertical` | `(2,3,2,0,0)` | — | §6.2 | -| `alpha_eq_beta_vertical` | `(2,1,2,0,0)` | — | §6.3 | +| `fixed_incidence_vertical` | `(2,2,2,0,0)` | — | §6.1 | +| `fixed_emergence_vertical` | `(2,3,2,0,0)` | — | §6.2 | | `fixed_psi_vertical` | `(2,4,2,0,0)` | `psi_constant_vertical` | §6.4 | -| `fixed_alpha_i_fixed_chi_fixed_phi` | `(2,2,3,4,0)` ‡ | — | §6.1 | +| `fixed_incidence_fixed_chi_fixed_phi` | `(2,2,3,4,0)` ‡ | — | §6.1 | | `fixed_omega_vertical` | `setmode d1 0 0 0` | — | §5 (Q[6]) | | `double_diffraction_vertical` | — | `double_diffraction_vertical` | §6.5 | +| `specular_vertical` | `(2,1,2,0,0)` | — | §6.3 | | `zone_vertical` | `setmode 5` | (TODO `HklEngine "zone"`) | §6 | | `bisecting_horizontal` | `(1,0,6,0,0)` | `bissector_horizontal` | §5.1 | | `fixed_phi_horizontal` | `(1,0,4,1,0)` † | — | §5.2 | | `fixed_chi_horizontal` | `(1,0,3,1,0)` † | — | §5.2 | -| `fixed_alpha_i_horizontal` | `(1,2,1,0,0)` | — | §6.1 | -| `fixed_beta_out_horizontal` | `(1,3,1,0,0)` | — | §6.2 | -| `alpha_eq_beta_horizontal` | `(1,1,1,0,0)` | — | §6.3 | +| `fixed_incidence_horizontal` | `(1,2,1,0,0)` | — | §6.1 | +| `fixed_emergence_horizontal` | `(1,3,1,0,0)` | — | §6.2 | | `fixed_psi_horizontal` | `(1,4,1,0,0)` | `psi_constant_horizontal` | §6.4 | | `fixed_omega_horizontal` | `setmode d1 0 0 0` | — | §5 (Q[6]) | | `double_diffraction_horizontal` | — | `double_diffraction_horizontal` | §6.5 | +| `specular_horizontal` | `(1,1,1,0,0)` | — | §6.3 | | `zone_horizontal` | `setmode 5` | (TODO `HklEngine "zone"`) | §6 | | `lifting_detector_phi` | `setmode 0 0 2 3 5` ‡ | `lifting_detector_phi` | §5.4 | | `lifting_detector_mu` | `setmode 0 0 1 3 4` ‡ | `lifting_detector_mu` | §5.4 | diff --git a/docs/source/geometries/s2d2.md b/docs/source/geometries/s2d2.md index 62718877..245ddb83 100644 --- a/docs/source/geometries/s2d2.md +++ b/docs/source/geometries/s2d2.md @@ -79,7 +79,7 @@ Override at run time with `g.modes["fixed_mu"].with_constraint_values(mu=...)` ### `reflectivity` {class}`~ad_hoc_diffractometer.mode.ReferenceConstraint`: -symmetric reflection — incidence angle equals exit angle (alpha_i = beta_out). +symmetric reflection — incidence = emergence. Requires ``g.surface_normal = (h, k, l)`` — see {doc}`../howto/surface`. | | | @@ -87,7 +87,7 @@ Requires ``g.surface_normal = (h, k, l)`` — see {doc}`../howto/surface`. | **Computed** | mu, Z, nu, delta | | **Constant during** `forward()` | — | | **Extras (input)** | n̂ → set `g.surface_normal = (h, k, l)` | -| **Extras (output)** | alpha_i, beta_out | +| **Extras (output)** | incidence, emergence | ## API reference diff --git a/docs/source/geometries/sixc.md b/docs/source/geometries/sixc.md index 9a8d7aff..7dfc1720 100644 --- a/docs/source/geometries/sixc.md +++ b/docs/source/geometries/sixc.md @@ -105,44 +105,44 @@ Override at run time with `g.modes["fixed_alpha_5c"].with_constraint_values(alph | **Computed** | omega, chi, phi, delta, gamma | | **Constant during** `forward()` | alpha, gamma = 0 | -### `fixed_alpha_zaxis` +### `fixed_incidence_zaxis` {class}`~ad_hoc_diffractometer.mode.SampleConstraint` × 2 + {class}`~ad_hoc_diffractometer.mode.ReferenceConstraint`: Z-axis mode with fixed incidence angle. Requires ``g.surface_normal = (h, k, l)`` — see {doc}`../howto/surface`. -Override any of the three pinned values at run time with `g.modes["fixed_alpha_zaxis"].with_constraint_values(alpha=..., chi=..., phi=...)` — see {doc}`../howto/constraints`. +Override any of the three pinned values at run time with `g.modes["fixed_incidence_zaxis"].with_constraint_values(alpha=..., chi=..., phi=...)` — see {doc}`../howto/constraints`. | | | |---|---| | **Computed** | omega, delta, gamma | | **Constant during** `forward()` | alpha (= β_in), chi, phi | | **Extras (input)** | n̂ → set `g.surface_normal = (h, k, l)` | -| **Extras (output)** | alpha_i (incidence angle), beta_out (exit angle) | +| **Extras (output)** | incidence (incidence angle), emergence (exit angle) | -### `fixed_beta_zaxis` +### `fixed_emergence_zaxis` {class}`~ad_hoc_diffractometer.mode.DetectorConstraint` + {class}`~ad_hoc_diffractometer.mode.SampleConstraint` + {class}`~ad_hoc_diffractometer.mode.ReferenceConstraint`: Z-axis mode with fixed exit angle. Requires ``g.surface_normal = (h, k, l)`` — see {doc}`../howto/surface`. -Override at run time with `g.modes["fixed_beta_zaxis"].with_constraint_values(gamma=..., chi=...)` — see {doc}`../howto/constraints`. +Override at run time with `g.modes["fixed_emergence_zaxis"].with_constraint_values(gamma=..., chi=...)` — see {doc}`../howto/constraints`. | | | |---|---| | **Computed** | omega, delta, alpha | | **Constant during** `forward()` | gamma (= β_out), chi | | **Extras (input)** | n̂ → set `g.surface_normal = (h, k, l)` | -| **Extras (output)** | alpha_i, beta_out | +| **Extras (output)** | incidence, emergence | -### `alpha_eq_beta_zaxis` +### `specular_zaxis` {class}`~ad_hoc_diffractometer.mode.SampleConstraint` × 2 + {class}`~ad_hoc_diffractometer.mode.ReferenceConstraint`: Z-axis mode, symmetric reflection (α = γ, β_in = β_out). Requires ``g.surface_normal = (h, k, l)`` — see {doc}`../howto/surface`. -Override the chi or phi pin at run time with `g.modes["alpha_eq_beta_zaxis"].with_constraint_values(chi=..., phi=...)` — see {doc}`../howto/constraints`. +Override the chi or phi pin at run time with `g.modes["specular_zaxis"].with_constraint_values(chi=..., phi=...)` — see {doc}`../howto/constraints`. | | | |---|---| | **Computed** | omega, delta, alpha, gamma | | **Constant during** `forward()` | chi, phi | | **Extras (input)** | n̂ → set `g.surface_normal = (h, k, l)` | -| **Extras (output)** | alpha_i, beta_out | +| **Extras (output)** | incidence, emergence | ## API reference diff --git a/docs/source/geometries/zaxis.md b/docs/source/geometries/zaxis.md index fd761eee..263c5679 100644 --- a/docs/source/geometries/zaxis.md +++ b/docs/source/geometries/zaxis.md @@ -72,26 +72,26 @@ See {doc}`../howto/constraints` for the extras dict pattern. {class}`~ad_hoc_diffractometer.mode.ReferenceConstraint`: surface normal aligned with the Z-axis; alpha directly equals the incidence angle β_in, gamma directly equals the exit angle β_out. -Override the α_i target at run time with `g.modes["zaxis"].with_constraint_values(alpha_i=...)` — see {doc}`../howto/constraints`. +Override the α_i target at run time with `g.modes["zaxis"].with_constraint_values(incidence=...)` — see {doc}`../howto/constraints`. | | | |---|---| | **Computed** | Z, delta, gamma | | **Constant during** `forward()` | — | | **Extras (input)** | n̂ → set `g.surface_normal = (h, k, l)` | -| **Extras (output)** | alpha_i (= alpha), beta_out (= gamma) | +| **Extras (output)** | incidence (= alpha), emergence (= gamma) | ### `reflectivity` {class}`~ad_hoc_diffractometer.mode.ReferenceConstraint`: -symmetric reflection — alpha_i = beta_out (alpha = gamma). +symmetric reflection — incidence = emergence (alpha = gamma). | | | |---|---| | **Computed** | Z, delta, alpha, gamma | | **Constant during** `forward()` | — | | **Extras (input)** | n̂ → set `g.surface_normal = (h, k, l)` | -| **Extras (output)** | alpha_i, beta_out | +| **Extras (output)** | incidence, emergence | ## API reference diff --git a/docs/source/glossary.md b/docs/source/glossary.md index 58e5686b..91f29ea1 100644 --- a/docs/source/glossary.md +++ b/docs/source/glossary.md @@ -214,8 +214,8 @@ n̂ (reference vector) Assigning a value here has no effect on ``forward()`` (and emits a ``UserWarning`` directing the caller at the geometry attribute below — issue #294). - * - On the geometry, when used by ``"alpha_i"``, ``"beta_out"``, - ``"a_eq_b"`` reference constraints + * - On the geometry, when used by ``"incidence"``, ``"emergence"``, + ``"specular"`` reference constraints - {attr}`~ad_hoc_diffractometer.diffractometer.AdHocDiffractometer.surface_normal` - The actual stored vector. Set via ``g.surface_normal = (h, k, l)``. @@ -313,10 +313,10 @@ Surface normal the vector perpendicular to the sample surface. Stored on the geometry as {attr}`~ad_hoc_diffractometer.diffractometer.AdHocDiffractometer.surface_normal` - and consumed by the ``"alpha_i"``, ``"beta_out"``, and ``"a_eq_b"`` + and consumed by the ``"incidence"``, ``"emergence"``, and ``"specular"`` {class}`~ad_hoc_diffractometer.mode.ReferenceConstraint` modes (plus {func}`~ad_hoc_diffractometer.reference.incidence_angle` / - {func}`~ad_hoc_diffractometer.reference.exit_angle` and + {func}`~ad_hoc_diffractometer.reference.emergence_angle` and {mod}`~ad_hoc_diffractometer.surface` helpers). Set with ``g.surface_normal = (h, k, l)`` (a length-3 sequence of numbers; ``(0, 0, 0)`` is rejected). Default is ``None``. In per-mode diff --git a/docs/source/howto/constraints.md b/docs/source/howto/constraints.md index c0819516..e894f18e 100644 --- a/docs/source/howto/constraints.md +++ b/docs/source/howto/constraints.md @@ -61,17 +61,17 @@ This is implemented for all geometries with two or more detector stages **Reference constraint** — expresses a condition between Q and a reference vector n̂ (surface normal, polarization axis, etc.). -The incidence/exit-angle constraints (``alpha_i``, ``beta_out``, -``a_eq_b``) are implemented when ``surface_normal`` is set; +The incidence/emergence-angle constraints (``incidence``, ``emergence``, +``specular``) are implemented when ``surface_normal`` is set; ``psi`` and ``naz`` are not yet implemented as forward constraints. See {doc}`surface`: ```python from ad_hoc_diffractometer import ReferenceConstraint -ReferenceConstraint("psi", 90.0) # azimuthal angle of n̂ about Q -ReferenceConstraint("alpha_i", 5.0) # incidence angle -ReferenceConstraint("a_eq_b", True) # alpha_i = beta_out (symmetric) +ReferenceConstraint("psi", 90.0) # azimuthal angle of n̂ about Q +ReferenceConstraint("incidence", 5.0) # incidence angle +ReferenceConstraint("specular", True) # incidence = emergence (symmetric) ``` **Rules:** at most one {class}`~ad_hoc_diffractometer.mode.DetectorConstraint`, @@ -146,8 +146,8 @@ readable for your case. `ConstraintSet.with_constraint_values(**updates)` returns a fresh `ConstraintSet` with the named constraint values replaced. Each keyword argument names a constraint by its `.name` attribute (a stage name for -sample / detector constraints, a reference name like `alpha_i` / -`beta_out` / `psi` / `a_eq_b` for reference constraints). Constraint +sample / detector constraints, a reference name like `incidence` / +`emergence` / `psi` / `specular` for reference constraints). Constraint order, `computed`, `extras`, and `cut_points` are preserved. ```python @@ -155,10 +155,10 @@ import ad_hoc_diffractometer as ahd g = ahd.make_geometry("psic") -# Multiple values at once (psic B3 mode: chi, phi, and the alpha_i target): -g.modes["fixed_alpha_i_fixed_chi_fixed_phi"] = ( - g.modes["fixed_alpha_i_fixed_chi_fixed_phi"] - .with_constraint_values(chi=15.0, phi=30.0, alpha_i=5.0) +# Multiple values at once (psic B3 mode: chi, phi, and the incidence target): +g.modes["fixed_incidence_fixed_chi_fixed_phi"] = ( + g.modes["fixed_incidence_fixed_chi_fixed_phi"] + .with_constraint_values(chi=15.0, phi=30.0, incidence=5.0) ) # Single value (psic fixed_chi_vertical: default chi=90° → 45°): @@ -219,16 +219,16 @@ For a reference-target value (surface modes), pass a new ```python from ad_hoc_diffractometer import ReferenceConstraint -# psic B3 mode: pin the incidence angle alpha_i at 5° instead of the +# psic B3 mode: pin the incidence angle at 5° instead of the # YAML default 0°. -g.modes["fixed_alpha_i_fixed_chi_fixed_phi"] = ConstraintSet( +g.modes["fixed_incidence_fixed_chi_fixed_phi"] = ConstraintSet( [ SampleConstraint("chi", 0.0), SampleConstraint("phi", 0.0), - ReferenceConstraint("alpha_i", 5.0), # was 0.0; now 5.0 + ReferenceConstraint("incidence", 5.0), # was 0.0; now 5.0 ], - computed=g.modes["fixed_alpha_i_fixed_chi_fixed_phi"].computed, - extras=dict(g.modes["fixed_alpha_i_fixed_chi_fixed_phi"].extras), + computed=g.modes["fixed_incidence_fixed_chi_fixed_phi"].computed, + extras=dict(g.modes["fixed_incidence_fixed_chi_fixed_phi"].extras), ) ``` diff --git a/docs/source/howto/modes.md b/docs/source/howto/modes.md index d47139bb..7bdf6524 100644 --- a/docs/source/howto/modes.md +++ b/docs/source/howto/modes.md @@ -71,10 +71,10 @@ g.mode_name = "fixed_chi" sols_45 = g.forward(1, 0, 0) # chi = 45° # Several values at once (e.g. psic B3 mode: two sample stages plus -# the alpha_i target — three pinned values in one call): -g.modes["fixed_alpha_i_fixed_chi_fixed_phi"] = ( - g.modes["fixed_alpha_i_fixed_chi_fixed_phi"] - .with_constraint_values(chi=15.0, phi=30.0, alpha_i=5.0) +# the incidence target — three pinned values in one call): +g.modes["fixed_incidence_fixed_chi_fixed_phi"] = ( + g.modes["fixed_incidence_fixed_chi_fixed_phi"] + .with_constraint_values(chi=15.0, phi=30.0, incidence=5.0) ) ``` diff --git a/docs/source/howto/surface.md b/docs/source/howto/surface.md index a16f1565..30abef2c 100644 --- a/docs/source/howto/surface.md +++ b/docs/source/howto/surface.md @@ -14,14 +14,14 @@ chosen by the active mode's | ReferenceConstraint name | Set on the geometry | Recipe | |---|---|---| -| `alpha_i`, `beta_out`, `a_eq_b` | `surface_normal` | `g.surface_normal = (h, k, l)` | +| `incidence`, `emergence`, `specular` | `surface_normal` | `g.surface_normal = (h, k, l)` | | `psi`, `naz` | `azimuth` | `g.azimuth = (h, k, l)` | | `omega` (SPEC pseudo-angle) | (none required) | — | Don't want to memorise the table? Ask the geometry directly: ```python -g.mode_name = "fixed_alpha_i_fixed_chi_fixed_phi" +g.mode_name = "fixed_incidence_fixed_chi_fixed_phi" attr = g.required_reference_vector # → 'surface_normal' setattr(g, attr, (0, 0, 1)) # equivalent to g.surface_normal = ... ``` @@ -85,8 +85,8 @@ internally using the UB matrix. Two separate reference vectors may be set: - **`surface_normal`** — the direction perpendicular to the sample surface, - used by `alpha_i`, `alpha_f`, `incidence_angle`, `exit_angle`, and - surface modes (`zaxis`, `reflectivity`, `alpha_eq_beta_zaxis`). + used by `incidence`, `emergence`, `incidence_angle`, `emergence_angle`, and + surface modes (`zaxis`, `reflectivity`, `specular_zaxis`). - **`azimuth`** — the direction used to define ψ = 0, used by `psi_angle` and `fixed_psi_*` modes. @@ -134,10 +134,10 @@ print(g.azimuth) # (1.0, 0.0, 0.0) g.azimuth = (0, 0, 1) ``` -## Compute incidence and exit angles +## Compute incidence and emergence angles ```python -from ad_hoc_diffractometer import incidence_angle, exit_angle +from ad_hoc_diffractometer import emergence_angle, incidence_angle g.surface_normal = (0, 0, 1) g.mode_name = "bisecting_vertical" @@ -145,8 +145,8 @@ solutions = g.forward(1, 0, 0) for sol in solutions: ai = incidence_angle(g, angles=sol) - af = exit_angle(g, angles=sol) - print(f"alpha_i = {ai:.4f}° beta_out = {af:.4f}°") + af = emergence_angle(g, angles=sol) + print(f"incidence = {ai:.4f}° emergence = {af:.4f}°") ``` Both functions use the current stage angles when `angles=None`: @@ -191,10 +191,10 @@ print(f"naz = {naz:.4f}°") naz is the azimuthal angle of the surface normal n̂ projected onto the horizontal plane of the lab frame. -## Symmetric reflection condition (α_i = β_out) +## Specular reflection condition (incidence = emergence) ```python -from ad_hoc_diffractometer import incidence_angle, exit_angle +from ad_hoc_diffractometer import emergence_angle, incidence_angle g.surface_normal = (0, 0, 1) g.mode_name = "bisecting_vertical" @@ -202,9 +202,9 @@ solutions = g.forward(1, 0, 0) for sol in solutions: ai = incidence_angle(g, angles=sol) - af = exit_angle(g, angles=sol) + af = emergence_angle(g, angles=sol) is_sym = abs(ai - af) < 0.01 # within 0.01° - print(f"alpha_i={ai:.3f}° beta_out={af:.3f}° symmetric={is_sym}") + print(f"incidence={ai:.3f}° emergence={af:.3f}° specular={is_sym}") ``` Alternatively, use the built-in `is_specular()` method on the geometry: @@ -270,7 +270,7 @@ only if it matches the stored target — otherwise it returns an empty list. - {doc}`constraints` — constraint framework and run-time mode customisation - {func}`~ad_hoc_diffractometer.reference.incidence_angle` -- {func}`~ad_hoc_diffractometer.reference.exit_angle` +- {func}`~ad_hoc_diffractometer.reference.emergence_angle` - {func}`~ad_hoc_diffractometer.reference.psi_angle` - {func}`~ad_hoc_diffractometer.reference.naz_angle` - {class}`~ad_hoc_diffractometer.mode.ReferenceConstraint` diff --git a/docs/source/reference/declarative_geometry_schema.md b/docs/source/reference/declarative_geometry_schema.md index 658ff012..756a772d 100644 --- a/docs/source/reference/declarative_geometry_schema.md +++ b/docs/source/reference/declarative_geometry_schema.md @@ -284,9 +284,9 @@ any others. | `detector` | `stage`, `value` | {class}`~ad_hoc_diffractometer.mode.DetectorConstraint` | | `reference` | `name`, `value` | {class}`~ad_hoc_diffractometer.mode.ReferenceConstraint` | -For `reference`, `name` is one of `"psi"`, `"alpha_i"`, `"beta_out"`, -`"a_eq_b"`, `"naz"`, `"omega"`; `value` is a float for the angular -constraints and the literal `true` for `a_eq_b`. The `"omega"` name +For `reference`, `name` is one of `"psi"`, `"incidence"`, `"emergence"`, +`"specular"`, `"naz"`, `"omega"`; `value` is a float for the angular +constraints and the literal `true` for `specular`. The `"omega"` name selects the SPEC `OMEGA` pseudo-angle (the angle between Q and the plane of the chi circle, SPEC `psic` `def OMEGA 'Q[6]'`); it applies to psic-family geometries (those with a sample stage named `chi`) and diff --git a/src/ad_hoc_diffractometer/benchmark.py b/src/ad_hoc_diffractometer/benchmark.py index 2aaab0fc..06ec9375 100644 --- a/src/ad_hoc_diffractometer/benchmark.py +++ b/src/ad_hoc_diffractometer/benchmark.py @@ -100,7 +100,7 @@ def _prepare_mode(geometry, mode_name: str) -> None: - fixed_psi modes: sets ``azimuth`` - double_diffraction modes: sets h2/k2/l2 extras - zone modes: sets z0/z1 extras to a generic (h,k,0) plane - - surface/reference modes (incidence, emergence, a_eq_b): + - surface/reference modes (incidence, emergence, specular): sets ``surface_normal`` """ geometry.mode_name = mode_name @@ -126,7 +126,7 @@ def _prepare_mode(geometry, mode_name: str) -> None: # Surface modes: set surface_normal if needed. for c in cs._constraints: cname = getattr(c, "_name", getattr(c, "name", "")) - if cname in ("incidence", "emergence", "a_eq_b"): + if cname in ("incidence", "emergence", "specular"): if geometry.surface_normal is None: geometry.surface_normal = (0, 0, 1) diff --git a/src/ad_hoc_diffractometer/diffractometer.py b/src/ad_hoc_diffractometer/diffractometer.py index 9a19a145..5fb33d88 100644 --- a/src/ad_hoc_diffractometer/diffractometer.py +++ b/src/ad_hoc_diffractometer/diffractometer.py @@ -18,6 +18,7 @@ from .constants import ZHAT from .mode import ConstraintSet from .mode import ModeDict +from .mode import _canonicalize_mode_name # noqa: PLC2701 from .reflection import Reflection from .reflection import ReflectionList from .sample import _DEFAULT_LATTICE @@ -712,6 +713,12 @@ def mode_name(self) -> str | None: @mode_name.setter def mode_name(self, name: str | None) -> None: + # REMOVE-AT-V1.0: canonicalize legacy mode-name aliases at the + # setter boundary so ``self._mode_name`` always holds the + # canonical name and the mode_name getter never reports a + # deprecated spelling. + if name is not None: + name = _canonicalize_mode_name(name, operation="set") if name is not None and name not in self._modes: available = sorted(self._modes.keys()) raise ValueError( @@ -1269,7 +1276,7 @@ def required_reference_vector(self) -> str | None: ===================================== ================================= ``"incidence"`` :attr:`surface_normal` ``"emergence"`` :attr:`surface_normal` - ``"a_eq_b"`` :attr:`surface_normal` + ``"specular"`` :attr:`surface_normal` ``"psi"`` :attr:`azimuth` ``"naz"`` :attr:`azimuth` ``"omega"`` (none) @@ -1322,7 +1329,7 @@ def required_reference_vector(self) -> str | None: rc: ReferenceConstraint | None = mode.reference_constraint if rc is None: return None - if rc.name in {"incidence", "emergence", "a_eq_b"}: + if rc.name in {"incidence", "emergence", "specular"}: return "surface_normal" if rc.name in {"psi", "naz"}: return "azimuth" diff --git a/src/ad_hoc_diffractometer/forward.py b/src/ad_hoc_diffractometer/forward.py index ca9e632f..a8b505a7 100644 --- a/src/ad_hoc_diffractometer/forward.py +++ b/src/ad_hoc_diffractometer/forward.py @@ -3089,7 +3089,7 @@ def _solve_surface( - ``"incidence"`` — incidence angle fixed at target value - ``"emergence"`` — emergence angle fixed at target value - - ``"a_eq_b"`` — symmetric reflection: incidence = emergence + - ``"specular"`` — specular reflection: incidence = emergence The solver builds a baseline angles dict (applying all fixed sample/detector constraints and setting the detector stage to ttheta_deg), then performs a @@ -3114,7 +3114,7 @@ def _solve_surface( # because :class:`ReferenceConstraint` canonicalizes the deprecated # aliases (``"alpha_i"`` / ``"beta_out"``) at construction time. rc = next(c for c in mode.constraints if isinstance(c, ReferenceConstraint)) - target_name = rc.name # "incidence", "emergence", or "a_eq_b" + target_name = rc.name # "incidence", "emergence", or "specular" target_value = rc.value # float or True # Build baseline angles dict with all fixed constraints applied @@ -3239,7 +3239,7 @@ def _surface_residual( if target_name == "emergence": bo = _emergence_angle(geometry, angles=angles) return bo - float(target_value) - # a_eq_b: incidence = emergence (symmetric reflection) + # specular: incidence = emergence (symmetric reflection) ai = _incidence_angle(geometry, angles=angles) bo = _emergence_angle(geometry, angles=angles) return ai - bo diff --git a/src/ad_hoc_diffractometer/geometries/psic.yml b/src/ad_hoc_diffractometer/geometries/psic.yml index a4d3beb7..865a54c7 100644 --- a/src/ad_hoc_diffractometer/geometries/psic.yml +++ b/src/ad_hoc_diffractometer/geometries/psic.yml @@ -46,13 +46,13 @@ documentation: | Mode families (24 modes total): - 9 vertical-scattering-plane modes (mu = nu = 0): bisecting, - fixed_phi, fixed_chi, fixed_alpha_i, fixed_beta_out, - alpha_eq_beta, fixed_psi, fixed_omega (#264), + fixed_phi, fixed_chi, fixed_incidence, fixed_emergence, + specular, fixed_psi, fixed_omega (#264), double_diffraction. - 9 horizontal-scattering-plane mirror modes (eta = delta = 0). - - 1 hybrid mode fixed_alpha_i_fixed_chi_fixed_phi (#264) that + - 1 hybrid mode fixed_incidence_fixed_chi_fixed_phi (#264) that fixes chi, phi, and incidence and lets every other angle float. - 2 zone modes (You 1999 §6, SPEC ``setmode 5``): scattering vector @@ -115,7 +115,7 @@ modes: - {type: detector, stage: nu, value: 0.0} computed: [eta, phi, delta] - fixed_alpha_i_vertical: + fixed_incidence_vertical: constraints: - {type: sample, stage: mu, value: 0.0} - {type: detector, stage: nu, value: 0.0} @@ -126,7 +126,7 @@ modes: incidence: null emergence: null - fixed_beta_out_vertical: + fixed_emergence_vertical: constraints: - {type: sample, stage: mu, value: 0.0} - {type: detector, stage: nu, value: 0.0} @@ -137,11 +137,11 @@ modes: incidence: null emergence: null - alpha_eq_beta_vertical: + specular_vertical: constraints: - {type: sample, stage: mu, value: 0.0} - {type: detector, stage: nu, value: 0.0} - - {type: reference, name: a_eq_b, value: true} + - {type: reference, name: specular, value: true} computed: [eta, chi, phi, delta] extras: n_hat: REQUIRED @@ -175,7 +175,7 @@ modes: # to be determined from the Bragg condition + the incidence target. # Two free sample stages and two free detector stages — solved by a # dedicated 4-D Newton. - fixed_alpha_i_fixed_chi_fixed_phi: + fixed_incidence_fixed_chi_fixed_phi: constraints: - {type: sample, stage: chi, value: 0.0} - {type: sample, stage: phi, value: 0.0} @@ -239,7 +239,7 @@ modes: - {type: detector, stage: delta, value: 0.0} computed: [mu, phi, nu] - fixed_alpha_i_horizontal: + fixed_incidence_horizontal: constraints: - {type: sample, stage: eta, value: 0.0} - {type: detector, stage: delta, value: 0.0} @@ -250,7 +250,7 @@ modes: incidence: null emergence: null - fixed_beta_out_horizontal: + fixed_emergence_horizontal: constraints: - {type: sample, stage: eta, value: 0.0} - {type: detector, stage: delta, value: 0.0} @@ -261,11 +261,11 @@ modes: incidence: null emergence: null - alpha_eq_beta_horizontal: + specular_horizontal: constraints: - {type: sample, stage: eta, value: 0.0} - {type: detector, stage: delta, value: 0.0} - - {type: reference, name: a_eq_b, value: true} + - {type: reference, name: specular, value: true} computed: [mu, chi, phi, nu] extras: n_hat: REQUIRED diff --git a/src/ad_hoc_diffractometer/geometries/s2d2.yml b/src/ad_hoc_diffractometer/geometries/s2d2.yml index d03de4f3..df5088b0 100644 --- a/src/ad_hoc_diffractometer/geometries/s2d2.yml +++ b/src/ad_hoc_diffractometer/geometries/s2d2.yml @@ -69,7 +69,7 @@ modes: reflectivity: constraints: - - {type: reference, name: a_eq_b, value: true} + - {type: reference, name: specular, value: true} computed: [mu, Z, nu, delta] extras: n_hat: REQUIRED diff --git a/src/ad_hoc_diffractometer/geometries/sixc.yml b/src/ad_hoc_diffractometer/geometries/sixc.yml index 1dee0051..c696cd91 100644 --- a/src/ad_hoc_diffractometer/geometries/sixc.yml +++ b/src/ad_hoc_diffractometer/geometries/sixc.yml @@ -83,7 +83,7 @@ modes: - {type: detector, stage: gamma, value: 0.0} computed: [omega, chi, phi, delta, gamma] - fixed_alpha_zaxis: + fixed_incidence_zaxis: constraints: - {type: sample, stage: alpha, value: 0.0} - {type: sample, stage: chi, value: 0.0} @@ -94,7 +94,7 @@ modes: incidence: null emergence: null - fixed_beta_zaxis: + fixed_emergence_zaxis: constraints: - {type: detector, stage: gamma, value: 0.0} - {type: sample, stage: chi, value: 0.0} @@ -105,11 +105,11 @@ modes: incidence: null emergence: null - alpha_eq_beta_zaxis: + specular_zaxis: constraints: - {type: sample, stage: chi, value: 0.0} - {type: sample, stage: phi, value: 0.0} - - {type: reference, name: a_eq_b, value: true} + - {type: reference, name: specular, value: true} computed: [omega, delta, alpha, gamma] extras: n_hat: REQUIRED diff --git a/src/ad_hoc_diffractometer/geometries/zaxis.yml b/src/ad_hoc_diffractometer/geometries/zaxis.yml index 56ed01cc..28b384ff 100644 --- a/src/ad_hoc_diffractometer/geometries/zaxis.yml +++ b/src/ad_hoc_diffractometer/geometries/zaxis.yml @@ -73,7 +73,7 @@ modes: reflectivity: constraints: - - {type: reference, name: a_eq_b, value: true} + - {type: reference, name: specular, value: true} computed: [Z, delta, alpha, gamma] extras: n_hat: REQUIRED diff --git a/src/ad_hoc_diffractometer/mode.py b/src/ad_hoc_diffractometer/mode.py index 17b65fc5..b60417a5 100644 --- a/src/ad_hoc_diffractometer/mode.py +++ b/src/ad_hoc_diffractometer/mode.py @@ -39,7 +39,7 @@ condition between Q and an external reference vector n̂ (surface normal, polarization axis, etc.) stored on the geometry. The named options are physical pseudo-angles from You (1999) and Lohmeier & Vlieg (1993): -``"psi"``, ``"incidence"``, ``"emergence"``, ``"a_eq_b"``, ``"naz"``. +``"psi"``, ``"incidence"``, ``"emergence"``, ``"specular"``, ``"naz"``. ``"alpha_i"`` and ``"beta_out"`` are accepted as deprecated aliases for ``"incidence"`` and ``"emergence"`` respectively (issue #299). At most one reference constraint is allowed. @@ -201,13 +201,14 @@ def __init__( "psi", "incidence", "emergence", - "a_eq_b", + "specular", "naz", "omega", # REMOVE-AT-V1.0: legacy alias names accepted on input and # canonicalized to the new names by ReferenceConstraint.__init__. "alpha_i", "beta_out", + "a_eq_b", } ) """Valid reference constraint names (physical pseudo-angles). @@ -221,18 +222,18 @@ def __init__( - ``"psi"`` — azimuthal angle of n̂ about Q (You 1999, eq. 23) - ``"incidence"`` — angle of incidence (incident beam vs. surface plane) - ``"emergence"`` — angle of emergence (diffracted beam vs. surface plane) -- ``"a_eq_b"`` — relational: incidence = emergence (symmetric reflection) +- ``"specular"`` — specular reflection (relational: incidence = emergence) - ``"naz"`` — azimuthal angle of n̂ in the lab frame - ``"omega"`` — SPEC ``OMEGA`` pseudo-angle (``Q[6]``): angle between Q and the plane of the chi circle (psic family); see :func:`~ad_hoc_diffractometer.reference.omega_pseudo` -The legacy names ``"alpha_i"`` and ``"beta_out"`` are also accepted as -deprecated aliases for ``"incidence"`` and ``"emergence"`` respectively. -Constructing a :class:`ReferenceConstraint` with a legacy name emits -:class:`DeprecationWarning` and canonicalizes to the new name; the -stored ``name`` attribute is always one of the canonical values listed -above. See :data:`_DEPRECATED_REFERENCE_NAME_ALIASES`. +The legacy names ``"alpha_i"``, ``"beta_out"``, and ``"a_eq_b"`` are also +accepted as deprecated aliases for ``"incidence"``, ``"emergence"``, and +``"specular"`` respectively. Constructing a :class:`ReferenceConstraint` +with a legacy name emits :class:`DeprecationWarning` and canonicalizes to +the new name; the stored ``name`` attribute is always one of the canonical +values listed above. See :data:`_DEPRECATED_REFERENCE_NAME_ALIASES`. REMOVE-AT-V1.0: the preceding paragraph documents the deprecation aliases and should be deleted alongside them. """ @@ -242,19 +243,70 @@ def __init__( _DEPRECATED_REFERENCE_NAME_ALIASES: dict[str, str] = { "alpha_i": "incidence", "beta_out": "emergence", + "a_eq_b": "specular", } """Mapping from deprecated reference-constraint name to its canonical form. -``"alpha_i"`` / ``"beta_out"`` are kept as forwarding aliases so existing -user code, saved sessions, and out-of-tree YAML files continue to work. +``"alpha_i"`` / ``"beta_out"`` / ``"a_eq_b"`` are kept as forwarding aliases +so existing user code, saved sessions, and out-of-tree YAML files continue +to work. :class:`ReferenceConstraint`, the YAML loader, the forward solver's output extras, and ``from_dict`` all consult this map. When a legacy name is supplied, a :class:`DeprecationWarning` is emitted and the canonical name is used for all downstream storage and comparison. Use the canonical -names (``"incidence"`` / ``"emergence"``) in new code. +names (``"incidence"``, ``"emergence"``, ``"specular"``) in new code. +""" + +# REMOVE-AT-V1.0: this mapping (and every site that consults it) is the +# deprecation infrastructure for the issue #299 mode-name renames. +_DEPRECATED_MODE_NAME_ALIASES: dict[str, str] = { + "fixed_alpha_i_vertical": "fixed_incidence_vertical", + "fixed_alpha_i_horizontal": "fixed_incidence_horizontal", + "fixed_alpha_i_fixed_chi_fixed_phi": "fixed_incidence_fixed_chi_fixed_phi", + "fixed_alpha_zaxis": "fixed_incidence_zaxis", + "fixed_beta_out_vertical": "fixed_emergence_vertical", + "fixed_beta_out_horizontal": "fixed_emergence_horizontal", + "fixed_beta_zaxis": "fixed_emergence_zaxis", + "alpha_eq_beta_vertical": "specular_vertical", + "alpha_eq_beta_horizontal": "specular_horizontal", + "alpha_eq_beta_zaxis": "specular_zaxis", +} +"""Mapping from deprecated mode name to its canonical form. + +The legacy mode-name strings are kept as forwarding aliases so existing +user code (e.g. ``g.mode_name = "fixed_alpha_i_vertical"``) and saved +sessions continue to work. :class:`ModeDict`'s read / write / delete / +membership-test methods and :attr:`AdHocDiffractometer.mode_name`'s +setter all consult this map. When a legacy name is supplied, a +:class:`DeprecationWarning` is emitted and the canonical name is used +for all downstream storage and lookup. """ + +def _canonicalize_mode_name(name: str, *, operation: str) -> str: + """REMOVE-AT-V1.0: rewrite a legacy mode-name alias to its canonical form. + + Emits :class:`DeprecationWarning` when the rewrite fires. + ``operation`` is one of ``"read"`` / ``"write"`` / ``"delete"`` and + is interpolated into the warning text so the caller sees which + access path triggered it. Returns ``name`` unchanged when no alias + applies. + """ + canonical = _DEPRECATED_MODE_NAME_ALIASES.get(name) + if canonical is None: + return name + import warnings + + warnings.warn( + f"Mode name {name!r} {operation} is deprecated; " + f"use {canonical!r} instead. (issue #299)", + DeprecationWarning, + stacklevel=3, + ) + return canonical + + QAZ: str = "qaz" """Special name for the qaz detector pseudo-angle (You 1999, eq. 18). @@ -355,7 +407,7 @@ def __setitem__(self, key: str, value: Any) -> None: f"effect on forward(). Set the reference vector on the " f"geometry instead: use 'g.surface_normal = (h, k, l)' " f"for surface-mode constraints " - f"(incidence / emergence / a_eq_b), or " + f"(incidence / emergence / specular), or " f"'g.azimuth = (h, k, l)' for " f"psi / naz constraints. See " f"AdHocDiffractometer.required_reference_vector to " @@ -895,29 +947,31 @@ class ReferenceConstraint: Angle of emergence: angle between the diffracted beam and the surface plane. Accepts the deprecated alias ``"beta_out"`` (REMOVE-AT-V1.0). - ``"a_eq_b"`` - Relational: ``incidence = emergence`` (symmetric reflection). - ``value`` must be ``True``. + ``"specular"`` + Specular reflection: relational condition ``incidence = emergence``. + ``value`` must be ``True``. Accepts the deprecated alias + ``"a_eq_b"`` (REMOVE-AT-V1.0). ``"naz"`` Azimuthal angle of n̂ in the lab frame (You 1999). Parameters ---------- name : str - One of ``"psi"``, ``"incidence"``, ``"emergence"``, ``"a_eq_b"``, - ``"naz"``, ``"omega"``. The deprecated aliases ``"alpha_i"`` and - ``"beta_out"`` are also accepted (with a :class:`DeprecationWarning`) - and canonicalized to ``"incidence"`` and ``"emergence"`` respectively - before storage. REMOVE-AT-V1.0: drop the alias clause. + One of ``"psi"``, ``"incidence"``, ``"emergence"``, ``"specular"``, + ``"naz"``, ``"omega"``. The deprecated aliases ``"alpha_i"``, + ``"beta_out"``, and ``"a_eq_b"`` are also accepted (with a + :class:`DeprecationWarning`) and canonicalized to ``"incidence"``, + ``"emergence"``, and ``"specular"`` respectively before storage. + REMOVE-AT-V1.0: drop the alias clause. value : float or bool - Target value in degrees (or ``True`` for ``"a_eq_b"``). + Target value in degrees (or ``True`` for ``"specular"``). Examples -------- >>> ReferenceConstraint("psi", 90.0) ReferenceConstraint('psi', 90.0) - >>> ReferenceConstraint("a_eq_b", True) - ReferenceConstraint('a_eq_b', True) + >>> ReferenceConstraint("specular", True) + ReferenceConstraint('specular', True) >>> ReferenceConstraint("incidence", 5.0) ReferenceConstraint('incidence', 5.0) """ @@ -950,13 +1004,13 @@ def __init__(self, name: str, value: float | bool) -> None: stacklevel=2, ) name = canonical_name - if name == "a_eq_b" and value is not True: + if name == "specular" and value is not True: raise ValueError( - "ReferenceConstraint('a_eq_b', value): value must be True; " + "ReferenceConstraint('specular', value): value must be True; " f"got {value!r}." ) self._name = name - self._value: float | bool = True if name == "a_eq_b" else float(value) # type: ignore[arg-type] + self._value: float | bool = True if name == "specular" else float(value) # type: ignore[arg-type] @property def name(self) -> str: @@ -965,7 +1019,7 @@ def name(self) -> str: @property def value(self) -> float | bool: - """Target value in degrees, or ``True`` for ``"a_eq_b"``.""" + """Target value in degrees, or ``True`` for ``"specular"``.""" return self._value def evaluate( @@ -1004,7 +1058,7 @@ def has_reference_vector(self, geometry: AdHocDiffractometer) -> bool: For ``"psi"`` and ``"naz"``: requires :attr:`~geometry.AdHocDiffractometer.azimuth` to be set. - For ``"incidence"``, ``"emergence"``, and ``"a_eq_b"``: requires + For ``"incidence"``, ``"emergence"``, and ``"specular"``: requires :attr:`~geometry.AdHocDiffractometer.surface_normal` to be set. For ``"omega"``: no reference vector is required (always returns @@ -1031,7 +1085,7 @@ def is_implemented(self, geometry: AdHocDiffractometer) -> bool: - ``"incidence"`` — requires :attr:`~geometry.AdHocDiffractometer.surface_normal` - ``"emergence"`` — requires :attr:`~geometry.AdHocDiffractometer.surface_normal` - - ``"a_eq_b"`` — requires :attr:`~geometry.AdHocDiffractometer.surface_normal` + - ``"specular"`` — requires :attr:`~geometry.AdHocDiffractometer.surface_normal` - ``"psi"`` — requires :attr:`~geometry.AdHocDiffractometer.azimuth`. The forward solver treats ψ as a **validation filter**: for a given (h,k,l) and UB, ψ is a pure phi-frame quantity that is the same for @@ -1055,7 +1109,7 @@ def is_implemented(self, geometry: AdHocDiffractometer) -> bool: if self._name == "omega": # Implemented for any geometry with a chi sample stage. return any(s.name == "chi" for s in geometry.sample_stages) - # incidence, emergence, a_eq_b — implemented when surface_normal is set + # incidence, emergence, specular — implemented when surface_normal is set return geometry.surface_normal is not None def to_dict(self) -> dict: @@ -1423,7 +1477,7 @@ def with_constraint_values(self, **updates: float | bool) -> ConstraintSet: exactly match the ``.name`` attribute of an existing :class:`SampleConstraint`, :class:`DetectorConstraint`, or :class:`ReferenceConstraint` in the set. Values are floats - (or ``bool`` for the ``"a_eq_b"`` :class:`ReferenceConstraint`). + (or ``bool`` for the ``"specular"`` :class:`ReferenceConstraint`). Returns ------- @@ -1636,6 +1690,12 @@ class ModeDict: Only :class:`ConstraintSet` instances may be stored. Keys are mode names (str). Iteration follows insertion order. + Deprecated mode-name aliases are canonicalized on every read, write, + delete, and membership test (issue #299). Iteration / ``keys()`` / + ``values()`` / ``items()`` return canonical keys only; a legacy name + accessed via ``__getitem__`` returns the canonical mode but does not + appear under its legacy spelling in iteration. + Parameters ---------- modes : dict[str, ConstraintSet] or None @@ -1668,15 +1728,26 @@ def __setitem__(self, name: str, mode: ConstraintSet) -> None: f"ModeDict values must be ConstraintSet instances; " f"got {type(mode).__name__!r} for key {name!r}." ) + # REMOVE-AT-V1.0: canonicalize legacy mode-name aliases on write. + name = _canonicalize_mode_name(name, operation="write") self._data[name] = mode def __getitem__(self, name: str) -> ConstraintSet: + # REMOVE-AT-V1.0: canonicalize legacy mode-name aliases on read. + name = _canonicalize_mode_name(name, operation="read") return self._data[name] def __delitem__(self, name: str) -> None: + # REMOVE-AT-V1.0: canonicalize legacy mode-name aliases on delete. + name = _canonicalize_mode_name(name, operation="delete") del self._data[name] def __contains__(self, name: object) -> bool: + # REMOVE-AT-V1.0: legacy mode-name aliases are "contained" iff the + # canonical name is present. No warning here — ``in`` tests are + # frequently defensive and per-test warnings would be noisy. + if isinstance(name, str) and name in _DEPRECATED_MODE_NAME_ALIASES: + name = _DEPRECATED_MODE_NAME_ALIASES[name] return name in self._data def __len__(self) -> int: diff --git a/tests/test_benchmark.py b/tests/test_benchmark.py index fd4a2279..2020a53d 100644 --- a/tests/test_benchmark.py +++ b/tests/test_benchmark.py @@ -138,9 +138,9 @@ def test_zone_extras_replaced(self, geometry_name, mode_name): @pytest.mark.parametrize( "geometry_name, mode_name", [ - pytest.param("sixc", "fixed_alpha_zaxis", id="sixc-alpha-zaxis"), - pytest.param("sixc", "fixed_beta_zaxis", id="sixc-beta-zaxis"), - pytest.param("sixc", "alpha_eq_beta_zaxis", id="sixc-a-eq-b"), + pytest.param("sixc", "fixed_incidence_zaxis", id="sixc-alpha-zaxis"), + pytest.param("sixc", "fixed_emergence_zaxis", id="sixc-beta-zaxis"), + pytest.param("sixc", "specular_zaxis", id="sixc-a-eq-b"), ], ) def test_surface_modes_set_normal(self, geometry_name, mode_name): @@ -154,7 +154,7 @@ def test_surface_mode_preserves_existing_normal(self): """If surface_normal is already set, _prepare_mode leaves it.""" g = _setup_geometry("sixc") g.surface_normal = (1, 0, 0) - _prepare_mode(g, "fixed_alpha_zaxis") + _prepare_mode(g, "fixed_incidence_zaxis") assert g.surface_normal == (1.0, 0.0, 0.0) diff --git a/tests/test_diffractometer.py b/tests/test_diffractometer.py index 365473b8..8d566c31 100644 --- a/tests/test_diffractometer.py +++ b/tests/test_diffractometer.py @@ -2326,7 +2326,7 @@ def test_geometry_to_dict_version_unknown_on_metadata_error(): [ pytest.param( "psic", - "fixed_alpha_i_fixed_chi_fixed_phi", + "fixed_incidence_fixed_chi_fixed_phi", "surface_normal", id="psic-B3-surface_normal", ), @@ -2397,7 +2397,7 @@ def test_required_reference_vector_usable_with_setattr(): import ad_hoc_diffractometer as ahd g = ahd.make_geometry("psic") - g.mode_name = "fixed_alpha_i_fixed_chi_fixed_phi" + g.mode_name = "fixed_incidence_fixed_chi_fixed_phi" attr = g.required_reference_vector assert attr == "surface_normal" setattr(g, attr, (0, 0, 1)) diff --git a/tests/test_forward.py b/tests/test_forward.py index 04dd2ad0..17994160 100644 --- a/tests/test_forward.py +++ b/tests/test_forward.py @@ -1386,9 +1386,9 @@ def test_sixc_round_trip(mode_name, h, k, l): # noqa: E741 @pytest.mark.parametrize( "mode_name", [ - pytest.param("fixed_alpha_zaxis", id="fixed_alpha_zaxis"), - pytest.param("fixed_beta_zaxis", id="fixed_beta_zaxis"), - pytest.param("alpha_eq_beta_zaxis", id="alpha_eq_beta_zaxis"), + pytest.param("fixed_incidence_zaxis", id="fixed_incidence_zaxis"), + pytest.param("fixed_emergence_zaxis", id="fixed_emergence_zaxis"), + pytest.param("specular_zaxis", id="specular_zaxis"), ], ) def test_sixc_zaxis_stub_not_implemented(mode_name): @@ -1450,13 +1450,13 @@ def test_sixc_four_circle_omega_equals_delta_half(): "lifting_detector_eta", "fixed_psi_vertical", "fixed_psi_horizontal", - "fixed_alpha_i_vertical", - "fixed_beta_out_vertical", - "alpha_eq_beta_vertical", - "fixed_alpha_i_fixed_chi_fixed_phi", - "fixed_alpha_i_horizontal", - "fixed_beta_out_horizontal", - "alpha_eq_beta_horizontal", + "fixed_incidence_vertical", + "fixed_emergence_vertical", + "specular_vertical", + "fixed_incidence_fixed_chi_fixed_phi", + "fixed_incidence_horizontal", + "fixed_emergence_horizontal", + "specular_horizontal", "fixed_omega_vertical", "fixed_omega_horizontal", "zone_vertical", @@ -3208,7 +3208,7 @@ def test_psic_fixed_alpha_i_fixed_chi_fixed_phi_round_trip( def test_psic_fixed_alpha_i_fixed_chi_fixed_phi_requires_surface_normal(): """B3 mode is_implemented=False until surface_normal is set.""" g = _setup_cubic(psic, a=4.0) - cs = g.modes["fixed_alpha_i_fixed_chi_fixed_phi"] + cs = g.modes["fixed_incidence_fixed_chi_fixed_phi"] assert cs.is_implemented(g) is False g.surface_normal = (0, 0, 1) assert cs.is_implemented(g) is True diff --git a/tests/test_mode.py b/tests/test_mode.py index 9f762324..ff0f382f 100644 --- a/tests/test_mode.py +++ b/tests/test_mode.py @@ -739,13 +739,13 @@ def test_detector_constraint_evaluate_qaz(): pytest.param("psi", 90.0, does_not_raise(), id="psi"), pytest.param("incidence", 5.0, does_not_raise(), id="incidence"), pytest.param("emergence", 5.0, does_not_raise(), id="emergence"), - pytest.param("a_eq_b", True, does_not_raise(), id="a_eq_b-true"), + pytest.param("specular", True, does_not_raise(), id="specular-true"), pytest.param("naz", 0.0, does_not_raise(), id="naz"), pytest.param( - "a_eq_b", + "specular", False, pytest.raises(ValueError, match=re.escape("must be True")), - id="a_eq_b-false-raises", + id="specular-false-raises", ), pytest.param( "unknown", @@ -770,6 +770,7 @@ def test_reference_constraint_construction(name, value, context): [ pytest.param("alpha_i", "incidence", 5.0, id="alpha_i-to-incidence"), pytest.param("beta_out", "emergence", 5.0, id="beta_out-to-emergence"), + pytest.param("a_eq_b", "specular", True, id="a_eq_b-to-specular"), ], ) def test_reference_constraint_deprecated_name_canonicalized( @@ -805,14 +806,15 @@ def test_reference_constraint_deprecated_name_canonicalized( # DeprecationWarning, and produces a constraint stored under the # canonical name. @pytest.mark.parametrize( - "legacy_name, canonical_name", + "legacy_name, canonical_name, value", [ - pytest.param("alpha_i", "incidence", id="alpha_i-to-incidence"), - pytest.param("beta_out", "emergence", id="beta_out-to-emergence"), + pytest.param("alpha_i", "incidence", 5.0, id="alpha_i-to-incidence"), + pytest.param("beta_out", "emergence", 5.0, id="beta_out-to-emergence"), + pytest.param("a_eq_b", "specular", True, id="a_eq_b-to-specular"), ], ) def test_reference_constraint_from_dict_accepts_legacy_name( - legacy_name, canonical_name + legacy_name, canonical_name, value ): """A saved session with a legacy reference-constraint name still loads. @@ -825,7 +827,7 @@ def test_reference_constraint_from_dict_accepts_legacy_name( legacy_session_dict = { "type": "ReferenceConstraint", "name": legacy_name, - "value": 5.0, + "value": value, } with pytest.warns( DeprecationWarning, @@ -839,7 +841,80 @@ def test_reference_constraint_from_dict_accepts_legacy_name( # A second to_dict round-trip writes the canonical name. assert rc.to_dict()["name"] == canonical_name # Equal to the constraint built directly with the canonical name. - assert rc == ReferenceConstraint(canonical_name, 5.0) + assert rc == ReferenceConstraint(canonical_name, value) + + +# REMOVE-AT-V1.0: ModeDict mode-name alias-map deprecation coverage. +@pytest.mark.parametrize( + "legacy_mode_name, canonical_mode_name", + [ + pytest.param( + "fixed_alpha_i_vertical", + "fixed_incidence_vertical", + id="fixed_alpha_i_vertical-to-fixed_incidence_vertical", + ), + pytest.param( + "fixed_beta_out_horizontal", + "fixed_emergence_horizontal", + id="fixed_beta_out_horizontal-to-fixed_emergence_horizontal", + ), + pytest.param( + "alpha_eq_beta_vertical", + "specular_vertical", + id="alpha_eq_beta_vertical-to-specular_vertical", + ), + ], +) +def test_modedict_legacy_mode_name_canonicalized(legacy_mode_name, canonical_mode_name): + """Issue #299: ModeDict canonicalizes legacy mode-name aliases on access. + + A read of a legacy mode-name key emits :class:`DeprecationWarning` + and returns the canonical mode. ``__contains__`` returns ``True`` + for the legacy name (no warning, since defensive ``in`` tests are + common). Iteration / ``keys()`` return the canonical name only. + """ + import ad_hoc_diffractometer as ahd + + g = ahd.make_geometry("psic") + + # __getitem__ rewrites and warns. + with pytest.warns( + DeprecationWarning, + match=re.escape( + f"Mode name {legacy_mode_name!r} read is deprecated; " + f"use {canonical_mode_name!r} instead. (issue #299)" + ), + ): + legacy_mode = g.modes[legacy_mode_name] + canonical_mode = g.modes[canonical_mode_name] + assert legacy_mode is canonical_mode + + # __contains__ accepts the legacy name without a warning. + assert legacy_mode_name in g.modes + assert canonical_mode_name in g.modes + + # Iteration returns canonical names only. + assert canonical_mode_name in list(g.modes) + assert legacy_mode_name not in list(g.modes) + + +def test_geometry_mode_name_setter_canonicalizes_legacy_alias(): + """Setting ``g.mode_name`` with a legacy alias is accepted, warns, and + stores the canonical name so the getter never reports a deprecated + spelling. + """ + import ad_hoc_diffractometer as ahd + + g = ahd.make_geometry("psic") + with pytest.warns( + DeprecationWarning, + match=re.escape( + "Mode name 'fixed_alpha_i_vertical' set is deprecated; " + "use 'fixed_incidence_vertical' instead. (issue #299)" + ), + ): + g.mode_name = "fixed_alpha_i_vertical" + assert g.mode_name == "fixed_incidence_vertical" def test_reference_constraint_value_coerced_to_float(): @@ -849,7 +924,7 @@ def test_reference_constraint_value_coerced_to_float(): def test_reference_constraint_a_eq_b_value_is_true(): - rc = ReferenceConstraint("a_eq_b", True) + rc = ReferenceConstraint("specular", True) assert rc.value is True @@ -869,7 +944,7 @@ def test_reference_constraint_to_dict_from_dict(): def test_reference_constraint_to_dict_from_dict_a_eq_b(): - rc = ReferenceConstraint("a_eq_b", True) + rc = ReferenceConstraint("specular", True) d = rc.to_dict() assert d["value"] is True rc2 = ReferenceConstraint.from_dict(d) @@ -879,12 +954,12 @@ def test_reference_constraint_to_dict_from_dict_a_eq_b(): @pytest.mark.parametrize( "name, value, surface_normal, expected", [ - # alpha_i/beta_out/a_eq_b: implemented when surface_normal is set + # alpha_i/beta_out/specular: implemented when surface_normal is set pytest.param("alpha_i", 0.0, (0, 0, 1), True, id="alpha_i-with-sn"), pytest.param("alpha_i", 0.0, None, False, id="alpha_i-no-sn"), pytest.param("beta_out", 0.0, (0, 0, 1), True, id="beta_out-with-sn"), - pytest.param("a_eq_b", True, (0, 0, 1), True, id="a_eq_b-with-sn"), - pytest.param("a_eq_b", True, None, False, id="a_eq_b-no-sn"), + pytest.param("specular", True, (0, 0, 1), True, id="specular-with-sn"), + pytest.param("specular", True, None, False, id="specular-no-sn"), # psi/naz: never implemented (no forward solver yet) pytest.param("psi", 0.0, (0, 0, 1), False, id="psi-not-implemented"), pytest.param("naz", 0.0, (0, 0, 1), False, id="naz-not-implemented"), @@ -1012,8 +1087,8 @@ def test_reference_constraint_hash(): rc1 = ReferenceConstraint("psi", 90.0) rc2 = ReferenceConstraint("psi", 90.0) assert hash(rc1) == hash(rc2) - # a_eq_b with bool value also hashable - rc3 = ReferenceConstraint("a_eq_b", True) + # specular with bool value also hashable + rc3 = ReferenceConstraint("specular", True) assert isinstance(hash(rc3), int) @@ -1855,13 +1930,13 @@ def test_fivec_modes_round_trip_serialisation(): "bisecting_4c", "fixed_gamma_5c", "fixed_alpha_5c", - "fixed_alpha_zaxis", - "fixed_beta_zaxis", - "alpha_eq_beta_zaxis", + "fixed_incidence_zaxis", + "fixed_emergence_zaxis", + "specular_zaxis", } _SIXC_IMPLEMENTED = {"bisecting_4c", "fixed_gamma_5c", "fixed_alpha_5c"} -_SIXC_STUBS = {"fixed_alpha_zaxis", "fixed_beta_zaxis", "alpha_eq_beta_zaxis"} +_SIXC_STUBS = {"fixed_incidence_zaxis", "fixed_emergence_zaxis", "specular_zaxis"} def test_sixc_factory_mode_names(): @@ -1919,9 +1994,13 @@ def test_sixc_surface_mode_implemented_with_surface_normal(mode_name): pytest.param("bisecting_4c", True, id="bisecting_4c-has-bisect"), pytest.param("fixed_gamma_5c", True, id="fixed_gamma_5c-has-bisect"), pytest.param("fixed_alpha_5c", True, id="fixed_alpha_5c-has-bisect"), - pytest.param("fixed_alpha_zaxis", False, id="fixed_alpha_zaxis-no-bisect"), - pytest.param("fixed_beta_zaxis", False, id="fixed_beta_zaxis-no-bisect"), - pytest.param("alpha_eq_beta_zaxis", False, id="alpha_eq_beta_zaxis-no-bisect"), + pytest.param( + "fixed_incidence_zaxis", False, id="fixed_incidence_zaxis-no-bisect" + ), + pytest.param( + "fixed_emergence_zaxis", False, id="fixed_emergence_zaxis-no-bisect" + ), + pytest.param("specular_zaxis", False, id="specular_zaxis-no-bisect"), ], ) def test_sixc_mode_has_bisect(mode_name, expected_has_bisect): @@ -1933,20 +2012,30 @@ def test_sixc_mode_has_bisect(mode_name, expected_has_bisect): "mode_name, extras_key, expected_value", [ pytest.param( - "fixed_alpha_zaxis", "n_hat", "REQUIRED", id="fixed_alpha_zaxis-n_hat" - ), - pytest.param( - "fixed_alpha_zaxis", "alpha_i", None, id="fixed_alpha_zaxis-alpha_i-output" + "fixed_incidence_zaxis", + "n_hat", + "REQUIRED", + id="fixed_incidence_zaxis-n_hat", ), pytest.param( - "fixed_beta_zaxis", "n_hat", "REQUIRED", id="fixed_beta_zaxis-n_hat" + "fixed_incidence_zaxis", + "alpha_i", + None, + id="fixed_incidence_zaxis-alpha_i-output", ), pytest.param( - "fixed_beta_zaxis", "beta_out", None, id="fixed_beta_zaxis-beta_out-output" + "fixed_emergence_zaxis", + "n_hat", + "REQUIRED", + id="fixed_emergence_zaxis-n_hat", ), pytest.param( - "alpha_eq_beta_zaxis", "n_hat", "REQUIRED", id="alpha_eq_beta_zaxis-n_hat" + "fixed_emergence_zaxis", + "beta_out", + None, + id="fixed_emergence_zaxis-beta_out-output", ), + pytest.param("specular_zaxis", "n_hat", "REQUIRED", id="specular_zaxis-n_hat"), ], ) def test_sixc_zaxis_extras_declared(mode_name, extras_key, expected_value): @@ -2523,7 +2612,7 @@ def test_qaz_residual_nonzero(): def _b3_constraint_set(): - """Build a ConstraintSet matching psic 'fixed_alpha_i_fixed_chi_fixed_phi'. + """Build a ConstraintSet matching psic 'fixed_incidence_fixed_chi_fixed_phi'. Three settable-value constraints: two SampleConstraint plus one ReferenceConstraint. Used by several with_constraint_values tests @@ -2586,19 +2675,19 @@ def _b3_constraint_set(): id="reference-incidence-5", ), pytest.param( - # ReferenceConstraint('a_eq_b', ...) only accepts True + # ReferenceConstraint('specular', ...) only accepts True # (the constraint expresses the *boolean* condition # alpha_i = beta_out; there is no False variant). This # case verifies the bool branch of with_constraint_values # by re-applying the only legal value. lambda: ConstraintSet( - [ReferenceConstraint("a_eq_b", True)], + [ReferenceConstraint("specular", True)], extras={"n_hat": REQUIRED}, ), - {"a_eq_b": True}, - {"a_eq_b": True}, + {"specular": True}, + {"specular": True}, does_not_raise(), - id="reference-a_eq_b-True", + id="reference-specular-True", ), pytest.param( _b3_constraint_set, diff --git a/tests/test_reference.py b/tests/test_reference.py index 4fd0d33c..fcd466f4 100644 --- a/tests/test_reference.py +++ b/tests/test_reference.py @@ -242,9 +242,11 @@ def test_naz_angle_vertical_normal_returns_zero(): "beta_out", 0.0, "surface_normal", None, False, id="beta_out-no-sn" ), pytest.param( - "a_eq_b", True, "surface_normal", (0, 0, 1), True, id="a_eq_b-with-sn" + "specular", True, "surface_normal", (0, 0, 1), True, id="specular-with-sn" + ), + pytest.param( + "specular", True, "surface_normal", None, False, id="specular-no-sn" ), - pytest.param("a_eq_b", True, "surface_normal", None, False, id="a_eq_b-no-sn"), # psi: implemented when azimuth is set pytest.param( "psi", @@ -296,9 +298,11 @@ def test_reference_constraint_is_implemented( pytest.param( "beta_out", 0.0, "surface_normal", (0, 0, 1), True, id="beta_out-with-sn" ), - pytest.param("a_eq_b", True, "surface_normal", None, False, id="a_eq_b-no-sn"), pytest.param( - "a_eq_b", True, "surface_normal", (0, 0, 1), True, id="a_eq_b-with-sn" + "specular", True, "surface_normal", None, False, id="specular-no-sn" + ), + pytest.param( + "specular", True, "surface_normal", (0, 0, 1), True, id="specular-with-sn" ), pytest.param("psi", 0.0, "azimuth", None, False, id="psi-no-ar"), pytest.param("psi", 0.0, "azimuth", (0, 0, 1), True, id="psi-with-ar"), @@ -420,9 +424,9 @@ def _setup_surface(factory, surface_normal=(0, 0, 1)): pytest.param(zaxis, "zaxis", id="zaxis-zaxis"), pytest.param(zaxis, "reflectivity", id="zaxis-reflectivity"), pytest.param(s2d2, "reflectivity", id="s2d2-reflectivity"), - pytest.param(sixc, "fixed_alpha_zaxis", id="sixc-fixed_alpha_zaxis"), - pytest.param(sixc, "fixed_beta_zaxis", id="sixc-fixed_beta_zaxis"), - pytest.param(sixc, "alpha_eq_beta_zaxis", id="sixc-alpha_eq_beta_zaxis"), + pytest.param(sixc, "fixed_incidence_zaxis", id="sixc-fixed_incidence_zaxis"), + pytest.param(sixc, "fixed_emergence_zaxis", id="sixc-fixed_emergence_zaxis"), + pytest.param(sixc, "specular_zaxis", id="sixc-specular_zaxis"), ], ) def test_surface_mode_is_implemented_with_surface_normal(factory, mode_name): @@ -437,9 +441,9 @@ def test_surface_mode_is_implemented_with_surface_normal(factory, mode_name): pytest.param(zaxis, "zaxis", id="zaxis-zaxis"), pytest.param(zaxis, "reflectivity", id="zaxis-reflectivity"), pytest.param(s2d2, "reflectivity", id="s2d2-reflectivity"), - pytest.param(sixc, "fixed_alpha_zaxis", id="sixc-fixed_alpha_zaxis"), - pytest.param(sixc, "fixed_beta_zaxis", id="sixc-fixed_beta_zaxis"), - pytest.param(sixc, "alpha_eq_beta_zaxis", id="sixc-alpha_eq_beta_zaxis"), + pytest.param(sixc, "fixed_incidence_zaxis", id="sixc-fixed_incidence_zaxis"), + pytest.param(sixc, "fixed_emergence_zaxis", id="sixc-fixed_emergence_zaxis"), + pytest.param(sixc, "specular_zaxis", id="sixc-specular_zaxis"), ], ) def test_surface_mode_not_implemented_without_surface_normal(factory, mode_name): @@ -454,15 +458,19 @@ def test_surface_mode_not_implemented_without_surface_normal(factory, mode_name) pytest.param(zaxis, "zaxis", 0, 1, 0, id="zaxis-zaxis"), pytest.param(zaxis, "reflectivity", 0, 0, 1, id="zaxis-reflectivity"), pytest.param(s2d2, "reflectivity", 0, 1, 0, id="s2d2-reflectivity"), - pytest.param(sixc, "fixed_alpha_zaxis", 0, 1, 0, id="sixc-fixed_alpha_zaxis"), - pytest.param(sixc, "fixed_beta_zaxis", 0, 1, 0, id="sixc-fixed_beta_zaxis"), + pytest.param( + sixc, "fixed_incidence_zaxis", 0, 1, 0, id="sixc-fixed_incidence_zaxis" + ), + pytest.param( + sixc, "fixed_emergence_zaxis", 0, 1, 0, id="sixc-fixed_emergence_zaxis" + ), pytest.param( sixc, - "alpha_eq_beta_zaxis", + "specular_zaxis", 0, 1, 0, - id="sixc-alpha_eq_beta_zaxis", + id="sixc-specular_zaxis", ), ], ) @@ -480,7 +488,7 @@ def test_surface_mode_returns_solutions(factory, mode_name, h, k, l): # noqa: E pytest.param(zaxis, "zaxis", 0, 1, 0, id="zaxis-zaxis-alpha_i=0"), pytest.param( sixc, - "fixed_alpha_zaxis", + "fixed_incidence_zaxis", 0, 1, 0, @@ -504,7 +512,7 @@ def test_surface_alpha_i_fixed_constraint_satisfied(factory, mode_name, h, k, l) [ pytest.param( sixc, - "fixed_beta_zaxis", + "fixed_emergence_zaxis", 0, 1, 0, @@ -528,11 +536,11 @@ def test_surface_beta_out_fixed_constraint_satisfied(factory, mode_name, h, k, l [ pytest.param(zaxis, "reflectivity", 0, 0, 1, id="zaxis-reflectivity"), pytest.param(s2d2, "reflectivity", 0, 1, 0, id="s2d2-reflectivity"), - pytest.param(sixc, "alpha_eq_beta_zaxis", 0, 1, 0, id="sixc-alpha_eq_beta"), + pytest.param(sixc, "specular_zaxis", 0, 1, 0, id="sixc-alpha_eq_beta"), ], ) def test_surface_a_eq_b_constraint_satisfied(factory, mode_name, h, k, l): # noqa: E741 - """a_eq_b modes: alpha_i ≈ beta_out in all solutions.""" + """specular modes: alpha_i ≈ beta_out in all solutions.""" g = _setup_surface(factory) g.mode_name = mode_name solutions = g.forward(h, k, l) @@ -671,7 +679,7 @@ def test_omega_pseudo_independent_of_chi(): pytest.param("psi", True, id="psi"), pytest.param("alpha_i", True, id="alpha_i"), pytest.param("beta_out", True, id="beta_out"), - pytest.param("a_eq_b", True, id="a_eq_b"), + pytest.param("specular", True, id="specular"), pytest.param("naz", True, id="naz"), pytest.param("omega", True, id="omega"), pytest.param("not_a_pseudo_angle", False, id="invalid"), @@ -685,7 +693,7 @@ def test_reference_constraint_accepts_omega(name, expected): else pytest.raises(ValueError, match=re.escape("ReferenceConstraint name")) ) with context: - if name == "a_eq_b": + if name == "specular": ReferenceConstraint(name, True) else: ReferenceConstraint(name, 0.0) diff --git a/tests/test_regression_issue_264.py b/tests/test_regression_issue_264.py index 178ac5ff..da8b31d8 100644 --- a/tests/test_regression_issue_264.py +++ b/tests/test_regression_issue_264.py @@ -13,7 +13,7 @@ solver dispatch that handles them. 2. Three additional psic modes were introduced or revised per the - @jwkim-anl review: ``fixed_alpha_i_fixed_chi_fixed_phi`` (B3), + @jwkim-anl review: ``fixed_incidence_fixed_chi_fixed_phi`` (B3), ``lifting_detector_eta`` (B4), plus the ``lifting_detector_phi`` / ``lifting_detector_mu`` revisions (C3/C4) that drop the ``qaz = 90`` constraint and fix every sample @@ -31,7 +31,7 @@ satisfy the Bragg condition end-to-end. - The dispatcher routes each new/revised mode to its intended solver branch (``_solve_omega_mode`` for ``fixed_omega_*``, - ``_solve_free_detectors`` for ``fixed_alpha_i_fixed_chi_fixed_phi``, + ``_solve_free_detectors`` for ``fixed_incidence_fixed_chi_fixed_phi``, ``lifting_detector_eta``, and the revised ``lifting_detector_phi`` / ``lifting_detector_mu``). @@ -76,7 +76,7 @@ _ISSUE_264_NEW_MODES = { "fixed_omega_vertical", "fixed_omega_horizontal", - "fixed_alpha_i_fixed_chi_fixed_phi", + "fixed_incidence_fixed_chi_fixed_phi", "lifting_detector_eta", } _ISSUE_264_REVISED_MODES = { @@ -305,7 +305,7 @@ def test_fixed_alpha_i_fixed_chi_fixed_phi_routes_to_free_detectors(): """B3 routes to ``_solve_free_detectors`` only after ``surface_normal`` is set (otherwise the mode is a stub).""" g = _setup_psic_cubic() - cs = g.modes["fixed_alpha_i_fixed_chi_fixed_phi"] + cs = g.modes["fixed_incidence_fixed_chi_fixed_phi"] # Without surface_normal, the alpha_i ReferenceConstraint reports # is_implemented=False, so the mode is a stub regardless of dispatch. assert cs.is_implemented(g) is False @@ -325,7 +325,7 @@ def test_fixed_alpha_i_fixed_chi_fixed_phi_routes_to_free_detectors(): [ pytest.param("fixed_omega_vertical", 1, 0, 0, False, id="fov-100"), pytest.param("fixed_omega_horizontal", 0, 0, 1, False, id="foh-001"), - pytest.param("fixed_alpha_i_fixed_chi_fixed_phi", 0, 1, 1, True, id="b3-011"), + pytest.param("fixed_incidence_fixed_chi_fixed_phi", 0, 1, 1, True, id="b3-011"), pytest.param("lifting_detector_eta", 1, 1, 0, False, id="le-110"), pytest.param("lifting_detector_phi", 1, 0, 0, False, id="lp-100"), # Under issue #280 ub_identity, (0,1,0) is unreachable on psic @@ -565,11 +565,11 @@ def test_omega_constraint_unimplemented_without_chi_stage(): [0.0, 3.0, 7.5], ) def test_b3_alpha_i_target_satisfied(alpha_target): - """B3 ``fixed_alpha_i_fixed_chi_fixed_phi`` solutions satisfy + """B3 ``fixed_incidence_fixed_chi_fixed_phi`` solutions satisfy alpha_i == target within tolerance.""" g = _setup_psic_cubic() g.surface_normal = (0, 0, 1) - cs = g.modes["fixed_alpha_i_fixed_chi_fixed_phi"] + cs = g.modes["fixed_incidence_fixed_chi_fixed_phi"] # Override alpha_i target with the parametrized value. g.modes["__b3_test"] = ConstraintSet( [ diff --git a/tests/test_regression_issue_267.py b/tests/test_regression_issue_267.py index eda385a0..c85f31a3 100644 --- a/tests/test_regression_issue_267.py +++ b/tests/test_regression_issue_267.py @@ -184,7 +184,7 @@ def _reference_psic() -> AdHocDiffractometer: ], computed=["eta", "phi", "delta"], ), - "fixed_alpha_i_vertical": ConstraintSet( + "fixed_incidence_vertical": ConstraintSet( [ SampleConstraint("mu", 0.0), DetectorConstraint("nu", 0.0), @@ -193,7 +193,7 @@ def _reference_psic() -> AdHocDiffractometer: computed=["eta", "chi", "phi", "delta"], extras={"n_hat": REQUIRED, "incidence": None, "emergence": None}, ), - "fixed_beta_out_vertical": ConstraintSet( + "fixed_emergence_vertical": ConstraintSet( [ SampleConstraint("mu", 0.0), DetectorConstraint("nu", 0.0), @@ -202,11 +202,11 @@ def _reference_psic() -> AdHocDiffractometer: computed=["eta", "chi", "phi", "delta"], extras={"n_hat": REQUIRED, "incidence": None, "emergence": None}, ), - "alpha_eq_beta_vertical": ConstraintSet( + "specular_vertical": ConstraintSet( [ SampleConstraint("mu", 0.0), DetectorConstraint("nu", 0.0), - ReferenceConstraint("a_eq_b", True), + ReferenceConstraint("specular", True), ], computed=["eta", "chi", "phi", "delta"], extras={"n_hat": REQUIRED, "incidence": None, "emergence": None}, @@ -220,7 +220,7 @@ def _reference_psic() -> AdHocDiffractometer: computed=["eta", "chi", "phi", "delta"], extras={"n_hat": REQUIRED, "psi": None}, ), - "fixed_alpha_i_fixed_chi_fixed_phi": ConstraintSet( + "fixed_incidence_fixed_chi_fixed_phi": ConstraintSet( [ SampleConstraint("chi", 0.0), SampleConstraint("phi", 0.0), @@ -271,7 +271,7 @@ def _reference_psic() -> AdHocDiffractometer: ], computed=["mu", "phi", "nu"], ), - "fixed_alpha_i_horizontal": ConstraintSet( + "fixed_incidence_horizontal": ConstraintSet( [ SampleConstraint("eta", 0.0), DetectorConstraint("delta", 0.0), @@ -280,7 +280,7 @@ def _reference_psic() -> AdHocDiffractometer: computed=["mu", "chi", "phi", "nu"], extras={"n_hat": REQUIRED, "incidence": None, "emergence": None}, ), - "fixed_beta_out_horizontal": ConstraintSet( + "fixed_emergence_horizontal": ConstraintSet( [ SampleConstraint("eta", 0.0), DetectorConstraint("delta", 0.0), @@ -289,11 +289,11 @@ def _reference_psic() -> AdHocDiffractometer: computed=["mu", "chi", "phi", "nu"], extras={"n_hat": REQUIRED, "incidence": None, "emergence": None}, ), - "alpha_eq_beta_horizontal": ConstraintSet( + "specular_horizontal": ConstraintSet( [ SampleConstraint("eta", 0.0), DetectorConstraint("delta", 0.0), - ReferenceConstraint("a_eq_b", True), + ReferenceConstraint("specular", True), ], computed=["mu", "chi", "phi", "nu"], extras={"n_hat": REQUIRED, "incidence": None, "emergence": None}, @@ -721,7 +721,7 @@ def _reference_sixc() -> AdHocDiffractometer: ], computed=["omega", "chi", "phi", "delta", "gamma"], ), - "fixed_alpha_zaxis": ConstraintSet( + "fixed_incidence_zaxis": ConstraintSet( [ SampleConstraint("alpha", 0.0), SampleConstraint("chi", 0.0), @@ -730,7 +730,7 @@ def _reference_sixc() -> AdHocDiffractometer: computed=["omega", "delta", "gamma"], extras={"n_hat": REQUIRED, "incidence": None, "emergence": None}, ), - "fixed_beta_zaxis": ConstraintSet( + "fixed_emergence_zaxis": ConstraintSet( [ DetectorConstraint("gamma", 0.0), SampleConstraint("chi", 0.0), @@ -739,11 +739,11 @@ def _reference_sixc() -> AdHocDiffractometer: computed=["omega", "delta", "alpha"], extras={"n_hat": REQUIRED, "incidence": None, "emergence": None}, ), - "alpha_eq_beta_zaxis": ConstraintSet( + "specular_zaxis": ConstraintSet( [ SampleConstraint("chi", 0.0), SampleConstraint("phi", 0.0), - ReferenceConstraint("a_eq_b", True), + ReferenceConstraint("specular", True), ], computed=["omega", "delta", "alpha", "gamma"], extras={"n_hat": REQUIRED, "incidence": None, "emergence": None}, @@ -782,7 +782,7 @@ def _reference_zaxis() -> AdHocDiffractometer: extras={"n_hat": REQUIRED, "incidence": None, "emergence": None}, ), "reflectivity": ConstraintSet( - [ReferenceConstraint("a_eq_b", True)], + [ReferenceConstraint("specular", True)], computed=["Z", "delta", "alpha", "gamma"], extras={"n_hat": REQUIRED, "incidence": None, "emergence": None}, ), @@ -818,7 +818,7 @@ def _reference_s2d2() -> AdHocDiffractometer: computed=["Z", "nu", "delta"], ), "reflectivity": ConstraintSet( - [ReferenceConstraint("a_eq_b", True)], + [ReferenceConstraint("specular", True)], computed=["mu", "Z", "nu", "delta"], extras={"n_hat": REQUIRED, "incidence": None, "emergence": None}, ), diff --git a/tests/test_regression_issue_279.py b/tests/test_regression_issue_279.py index ba512d87..3657fe1f 100644 --- a/tests/test_regression_issue_279.py +++ b/tests/test_regression_issue_279.py @@ -8,8 +8,8 @@ ``2θ`` angle to the **last** stage in ``geometry.detector_stages`` unconditionally. For psic that last stage is ``delta``, and the horizontal surface modes -(``fixed_alpha_i_horizontal``, ``fixed_beta_out_horizontal``, -``alpha_eq_beta_horizontal``) declare a +(``fixed_incidence_horizontal``, ``fixed_emergence_horizontal``, +``specular_horizontal``) declare a :class:`~ad_hoc_diffractometer.mode.DetectorConstraint` that pins ``delta = 0``. The solver therefore overwrote the pinned value moments after applying it and left the truly active detector stage @@ -17,8 +17,8 @@ ``nu = 0`` that violated the mode's own constraint. The mirror failure occurred in the vertical surface modes -(``fixed_alpha_i_vertical``, ``fixed_beta_out_vertical``, -``alpha_eq_beta_vertical``), where the mode pins ``nu = 0`` and +(``fixed_incidence_vertical``, ``fixed_emergence_vertical``, +``specular_vertical``), where the mode pins ``nu = 0`` and the active detector stage is ``delta`` — but the dispatch picked ``delta`` for both roles regardless, so the constraint happened to agree with the active stage by accident (the resulting ``nu = 0`` @@ -64,17 +64,17 @@ # Surface-reference modes on psic whose DetectorConstraint pins the # inner (delta) stage so the active detector for 2θ must be ``nu``. _PSIC_HORIZONTAL_SURFACE_MODES = ( - "fixed_alpha_i_horizontal", - "fixed_beta_out_horizontal", - "alpha_eq_beta_horizontal", + "fixed_incidence_horizontal", + "fixed_emergence_horizontal", + "specular_horizontal", ) # Surface-reference modes on psic whose DetectorConstraint pins the # outer (nu) stage so the active detector for 2θ must be ``delta``. _PSIC_VERTICAL_SURFACE_MODES = ( - "fixed_alpha_i_vertical", - "fixed_beta_out_vertical", - "alpha_eq_beta_vertical", + "fixed_incidence_vertical", + "fixed_emergence_vertical", + "specular_vertical", ) @@ -170,21 +170,21 @@ def test_psic_vertical_surface_honors_nu_pin(mode_name, context): # must not alter the existing healthy round-trip count. pytest.param( sixc, - "fixed_alpha_zaxis", + "fixed_incidence_zaxis", 1, 0, 0, does_not_raise(), - id="sixc-fixed_alpha_zaxis-100", + id="sixc-fixed_incidence_zaxis-100", ), pytest.param( sixc, - "alpha_eq_beta_zaxis", + "specular_zaxis", 1, 0, 0, does_not_raise(), - id="sixc-alpha_eq_beta_zaxis-100", + id="sixc-specular_zaxis-100", ), # zaxis reflectivity mode: confirm at least one returned # solution still round-trips, i.e. the legacy path is intact. diff --git a/tests/test_regression_issue_292.py b/tests/test_regression_issue_292.py index 6dd21b99..715d6fd6 100644 --- a/tests/test_regression_issue_292.py +++ b/tests/test_regression_issue_292.py @@ -55,7 +55,7 @@ def _setup_cubic(name, a=4.0): # --------------------------------------------------------------------------- -# B3 surface mode: psic / fixed_alpha_i_fixed_chi_fixed_phi +# B3 surface mode: psic / fixed_incidence_fixed_chi_fixed_phi # (the only fixed_alpha_i_* mode that is currently implemented for psic). # --------------------------------------------------------------------------- @@ -79,7 +79,7 @@ def test_psic_b3_populates_alpha_i_and_beta_out_extras( with context: g = _setup_cubic("psic") g.surface_normal = (0, 0, 1) - cs = g.modes["fixed_alpha_i_fixed_chi_fixed_phi"] + cs = g.modes["fixed_incidence_fixed_chi_fixed_phi"] # Update the reference-constraint target via the public API: # rebuild the constraint set with the requested alpha_i value # (the YAML defaults to 0.0; we want non-zero targets too). @@ -88,7 +88,7 @@ def test_psic_b3_populates_alpha_i_and_beta_out_extras( from ad_hoc_diffractometer import ReferenceConstraint from ad_hoc_diffractometer import SampleConstraint - g.modes["fixed_alpha_i_fixed_chi_fixed_phi"] = ConstraintSet( + g.modes["fixed_incidence_fixed_chi_fixed_phi"] = ConstraintSet( [ SampleConstraint("chi", 0.0), SampleConstraint("phi", 0.0), @@ -97,12 +97,12 @@ def test_psic_b3_populates_alpha_i_and_beta_out_extras( computed=cs.computed, extras={"n_hat": REQUIRED, "alpha_i": None, "beta_out": None}, ) - g.mode_name = "fixed_alpha_i_fixed_chi_fixed_phi" + g.mode_name = "fixed_incidence_fixed_chi_fixed_phi" sols = g.forward(h, k, l) assert len(sols) > 0 - mode = g.modes["fixed_alpha_i_fixed_chi_fixed_phi"] + mode = g.modes["fixed_incidence_fixed_chi_fixed_phi"] assert isinstance(mode.extras["alpha_i"], list) assert isinstance(mode.extras["beta_out"], list) assert len(mode.extras["alpha_i"]) == len(sols) From 03ec7309e33d353a8c160a1638abef3fa993e138 Mon Sep 17 00:00:00 2001 From: Pete Jemian Date: Thu, 18 Jun 2026 21:49:34 -0500 Subject: [PATCH 6/7] refactor(#299) adopt pre-v1.0 no-deprecation policy and rip out alias infrastructure Establishes the project's pre-v1.0 operating directives in AGENTS.md and removes every deprecation-cycle artefact accumulated during this PR (alias maps, forwarder methods/functions, alias-aware loader, mode-name canonicalization, deprecation tests) plus the comparable infrastructure introduced by the merged PR #300 / #298 (`azimuthal_reference` property / kwarg / from_dict fallback / test block). AGENTS.md: new 'Operating directives (pre-v1.0)' section. Five directives: no deprecation cycles; scope is the issue; no volunteered follow-up issues; response length matches the request; when in doubt, ask one short question. Pre-commit: new local pygrep hook 'no-pre-v1.0-deprecations' forbids REMOVE-AT-V1.0, DeprecationWarning, and _DEPRECATED_*_ALIASES patterns in src/ and tests/. Removed in src/: - mode.py: _DEPRECATED_REFERENCE_NAME_ALIASES, _DEPRECATED_MODE_NAME_ALIASES, _canonicalize_mode_name, ReferenceConstraint.__init__ alias canonicalization, with_constraint_values alias-kwarg block, ModeDict alias overrides - diffractometer.py: alpha_i / alpha_f deprecated methods, azimuthal_reference property + constructor kwarg + from_dict legacy-key fallback, mode_name setter canonicalization - surface.py: alpha_i / alpha_f deprecated functions - reference.py: exit_angle deprecated function - forward.py: legacy entries in _OUTPUT_EXTRA_KEYS, legacy entries in computers dict, back-fill loop - geometry_loader.py: _rewrite_legacy_reference_names function and its call site - geometries/schema.json: legacy 'alpha_i' / 'beta_out' / 'a_eq_b' enum values Removed in tests/: - 5 ReferenceConstraint deprecation tests in test_mode.py - 7-test TestAzimuthDeprecation block in test_diffractometer.py - 3 loader-rewrite tests + fixture in test_geometry_loader.py - exit_angle deprecation test in test_reference.py - 4 method-deprecation tests in test_surface.py - back-fill test in test_regression_issue_292.py Renamed throughout: every remaining legacy-name occurrence in tests and docs migrated to the canonical incidence / emergence / specular vocabulary (72 substitutions in tests, 2 deprecation-mention paragraphs removed from glossary.md and howto/surface.md). Closes the v1.0 cleanup tracking work that issue #301 (now closed as obsolete) was created to track. QC: 2623 tests pass, 100% coverage; pre-commit clean (all hooks including the new policy hook); doc build succeeds; grep for deprecation infrastructure across src/, tests/, docs/source/ returns zero matches. Net diff: -975 lines. Contributed by: OpenCode (argo/claudeopus47) --- .pre-commit-config.yaml | 9 + AGENTS.md | 44 ++++ docs/source/glossary.md | 5 +- docs/source/howto/surface.md | 13 - src/ad_hoc_diffractometer/diffractometer.py | 93 +------ src/ad_hoc_diffractometer/forward.py | 39 +-- .../geometries/schema.json | 3 +- src/ad_hoc_diffractometer/geometry_loader.py | 83 ------- src/ad_hoc_diffractometer/mode.py | 178 +------------- src/ad_hoc_diffractometer/reference.py | 20 -- src/ad_hoc_diffractometer/surface.py | 41 +--- tests/test_diffractometer.py | 123 ---------- tests/test_forward.py | 10 +- tests/test_geometry_loader.py | 149 ----------- tests/test_mode.py | 232 ++---------------- tests/test_reference.py | 65 ++--- tests/test_regression_issue_264.py | 12 +- tests/test_regression_issue_279.py | 2 +- tests/test_regression_issue_292.py | 115 ++------- tests/test_surface.py | 115 +++------ 20 files changed, 188 insertions(+), 1163 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7fed3b0e..ab07dda1 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -45,6 +45,15 @@ repos: pass_filenames: false always_run: true + - id: no-pre-v1.0-deprecations + name: Forbid deprecation-cycle infrastructure (pre-v1.0 policy) + language: pygrep + entry: '(REMOVE-AT-V1\.0|DeprecationWarning|_DEPRECATED_[A-Z_]+_ALIASES)' + files: ^(src/ad_hoc_diffractometer|tests)/.*\.py$ + # See the "Operating directives (pre-v1.0)" section of AGENTS.md. + # Rename freely. No aliases, no DeprecationWarning emitters, + # no REMOVE-AT-V1.0 markers, no alias-map constants. + - repo: https://github.com/PyCQA/isort rev: 5.13.2 hooks: diff --git a/AGENTS.md b/AGENTS.md index faefdba9..4dd3f4c5 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,6 +4,50 @@ This file provides context for AI coding agents working on this project. --- +## Operating directives (pre-v1.0) + +These rules apply for every issue and every PR until the project tags +v1.0. They override anything in the rest of this file or in the +agent's general training that conflicts. + +### No deprecation cycles + +Rename freely. Do not add forwarding aliases, `DeprecationWarning` +emitters, alias maps, `REMOVE-AT-V1.0` markers, or any other +deprecation-cycle infrastructure for any rename, signature change, or +removal. The whole package is still pre-1.0; users have no API +stability contract yet. Make the change cleanly, update every +in-tree call site, document the change in `CHANGES.md`, move on. + +### Scope is the issue + +The issue text defines the scope. If something falls under the +issue's title or stated motivation, it is in scope by default. Do +not unilaterally narrow the scope or split work into "follow-up" +issues. If the scope is genuinely ambiguous, ask exactly one +clarifying question, then proceed. + +### No volunteered follow-up issues + +Do not file new issues, propose new issues, or build "future v1.0 +cleanup" tracking unless the user asks. Pre-1.0 cleanup is just +"edit the code now." + +### Response length matches the request + +When the user asks a brief question, answer briefly: a sentence or a +short list, no tables, no preamble, no recap. When the user asks +for analysis or a plan, longer is fine. Match the form of the +request. + +### When in doubt, ask one short question + +A single clarifying question is cheaper than building the wrong +thing and unwinding it. Do not stack multiple questions; ask one, +wait, then proceed. + +--- + ## Attribution Always sign your work. Every commit message, pull request description, and diff --git a/docs/source/glossary.md b/docs/source/glossary.md index 91f29ea1..7ff1ed7d 100644 --- a/docs/source/glossary.md +++ b/docs/source/glossary.md @@ -27,10 +27,7 @@ Azimuthal reference vector per-mode ``Extras (input)`` tables this same vector is referred to by its mathematical symbol **n̂** (rendered as the ``n_hat`` key in ``mode.extras``). See {term}`n̂ (reference vector)` for the full - surface-form table, and {doc}`howto/surface`. The attribute was - named ``azimuthal_reference`` before v0.12.0 (issue #298); the old - name remains as a deprecated forwarding alias and will be removed - in a future release. + surface-form table, and {doc}`howto/surface`. B matrix The matrix that encodes the reciprocal lattice and maps Miller indices diff --git a/docs/source/howto/surface.md b/docs/source/howto/surface.md index 30abef2c..cac5119b 100644 --- a/docs/source/howto/surface.md +++ b/docs/source/howto/surface.md @@ -29,15 +29,6 @@ setattr(g, attr, (0, 0, 1)) # equivalent to g.surface_normal = ... `required_reference_vector` returns `'surface_normal'`, `'azimuth'`, or `None`. -```{note} -The `azimuth` attribute was named `azimuthal_reference` before -v0.12.0 (issue #298). The old name remains as a deprecated forwarding -alias — both the property access and the constructor keyword emit -`DeprecationWarning` — and will be removed in a future release. -`required_reference_vector` now returns `'azimuth'` where it -previously returned `'azimuthal_reference'`. -``` - ## What the `n̂` placeholder in `mode.extras` means Every per-geometry mode table shows a row like @@ -236,10 +227,6 @@ g2 = AdHocDiffractometer.from_dict(d) print(g2.surface_normal) # (0.0, 0.0, 1.0) ``` -`from_dict()` also accepts the legacy key `"azimuthal_reference"` -for backward compatibility with sessions saved by -ad_hoc_diffractometer ≤ v0.11.x (issue #298). - ## Reference constraint modes Modes that use a ``ReferenceConstraint`` require the appropriate reference diff --git a/src/ad_hoc_diffractometer/diffractometer.py b/src/ad_hoc_diffractometer/diffractometer.py index 5fb33d88..42e23ad8 100644 --- a/src/ad_hoc_diffractometer/diffractometer.py +++ b/src/ad_hoc_diffractometer/diffractometer.py @@ -9,7 +9,6 @@ import builtins import logging -import warnings import numpy as np @@ -18,7 +17,6 @@ from .constants import ZHAT from .mode import ConstraintSet from .mode import ModeDict -from .mode import _canonicalize_mode_name # noqa: PLC2701 from .reflection import Reflection from .reflection import ReflectionList from .sample import _DEFAULT_LATTICE @@ -75,10 +73,6 @@ class AdHocDiffractometer: Azimuthal reference direction as Miller indices (h, k, l). Used by :meth:`psi` to compute the azimuthal angle ψ. ``None`` (default) means no reference is set. Must be a non-zero vector. - azimuthal_reference : tuple of float or None, optional - Deprecated alias for ``azimuth``. Accepted for backward - compatibility; emits :class:`DeprecationWarning` if used. Will - be removed in a future release. modes : dict[str, ConstraintSet] or ModeDict or None, optional Named diffraction modes available for this geometry. Keys are mode names (str); values are :class:`~mode.DiffractionMode` @@ -111,7 +105,6 @@ def __init__( kappa_alpha_deg: float | None = None, kappa_pseudo_angle_convention=None, azimuth: tuple[float, float, float] | None = None, - azimuthal_reference: tuple[float, float, float] | None = None, modes: dict | ModeDict | None = None, default_mode: str | None = None, cut_points: dict[str, float] | None = None, @@ -123,25 +116,6 @@ def __init__( self.wavelength = wavelength # validated via property setter self.kappa_alpha_deg = kappa_alpha_deg self.kappa_pseudo_angle_convention = kappa_pseudo_angle_convention - # Resolve azimuth / azimuthal_reference (deprecated alias). If both - # are supplied and disagree, raise; if only the deprecated form is - # supplied, warn and accept it. - if azimuthal_reference is not None: - if azimuth is not None and tuple(azimuth) != tuple(azimuthal_reference): - raise ValueError( - "Cannot specify both 'azimuth' and 'azimuthal_reference' " - "with different values; 'azimuthal_reference' is the " - "deprecated alias for 'azimuth'." - ) - warnings.warn( - "The 'azimuthal_reference' constructor keyword is deprecated " - "since v0.12.0; use 'azimuth' instead. The old name will be " - "removed in a future release.", - DeprecationWarning, - stacklevel=2, - ) - if azimuth is None: - azimuth = azimuthal_reference self.azimuth = azimuth # validated via property setter self._surface_normal: tuple[float, float, float] | None = None self._detector_distance: float | None = None @@ -616,38 +590,6 @@ def azimuth(self, value: tuple[float, float, float] | None) -> None: ) self._azimuth = (h, k, l) - @property - def azimuthal_reference(self) -> tuple[float, float, float] | None: - """ - Deprecated alias for :attr:`azimuth`. - - .. deprecated:: 0.12.0 - Use :attr:`azimuth` instead. This alias will be removed in a - future release. - - Reads and writes are forwarded to :attr:`azimuth`; every access - emits a :class:`DeprecationWarning`. - """ - warnings.warn( - "AdHocDiffractometer.azimuthal_reference is deprecated since " - "v0.12.0; use 'azimuth' instead. The old name will be removed " - "in a future release.", - DeprecationWarning, - stacklevel=2, - ) - return self._azimuth - - @azimuthal_reference.setter - def azimuthal_reference(self, value: tuple[float, float, float] | None) -> None: - warnings.warn( - "AdHocDiffractometer.azimuthal_reference is deprecated since " - "v0.12.0; use 'azimuth' instead. The old name will be removed " - "in a future release.", - DeprecationWarning, - stacklevel=2, - ) - self.azimuth = value - # ------------------------------------------------------------------ # Diffraction modes # ------------------------------------------------------------------ @@ -713,12 +655,6 @@ def mode_name(self) -> str | None: @mode_name.setter def mode_name(self, name: str | None) -> None: - # REMOVE-AT-V1.0: canonicalize legacy mode-name aliases at the - # setter boundary so ``self._mode_name`` always holds the - # canonical name and the mode_name getter never reports a - # deprecated spelling. - if name is not None: - name = _canonicalize_mode_name(name, operation="set") if name is not None and name not in self._modes: available = sorted(self._modes.keys()) raise ValueError( @@ -1513,33 +1449,6 @@ def emergence(self, angles: dict[str, float] | None = None) -> float: return _emergence(self, angles) - # REMOVE-AT-V1.0: deprecated method aliases for the canonical - # incidence / emergence methods. Emit DeprecationWarning on every - # call and delegate to the canonical method. - def alpha_i(self, angles: dict[str, float] | None = None) -> float: - """Deprecated alias for :meth:`incidence`. REMOVE-AT-V1.0.""" - import warnings - - warnings.warn( - "AdHocDiffractometer.alpha_i() is deprecated; " - "use .incidence() instead. (issue #299)", - DeprecationWarning, - stacklevel=2, - ) - return self.incidence(angles) - - def alpha_f(self, angles: dict[str, float] | None = None) -> float: - """Deprecated alias for :meth:`emergence`. REMOVE-AT-V1.0.""" - import warnings - - warnings.warn( - "AdHocDiffractometer.alpha_f() is deprecated; " - "use .emergence() instead. (issue #299)", - DeprecationWarning, - stacklevel=2, - ) - return self.emergence(angles) - def q_components(self, angles: dict[str, float] | None = None) -> dict[str, float]: """ Decompose Q into components parallel and perpendicular to the surface. @@ -2250,7 +2159,7 @@ def from_dict(cls, d: dict) -> "AdHocDiffractometer": description=d.get("description", ""), wavelength=d.get("wavelength"), kappa_alpha_deg=d.get("kappa_alpha_deg"), - azimuth=d.get("azimuth", d.get("azimuthal_reference")), + azimuth=d.get("azimuth"), modes=restored_modes if restored_modes else None, default_mode=d.get("mode_name"), cut_points=d.get("cut_points"), diff --git a/src/ad_hoc_diffractometer/forward.py b/src/ad_hoc_diffractometer/forward.py index a8b505a7..d40157f1 100644 --- a/src/ad_hoc_diffractometer/forward.py +++ b/src/ad_hoc_diffractometer/forward.py @@ -477,11 +477,6 @@ def compute_forward( "emergence", "psi", "omega", - # REMOVE-AT-V1.0: deprecated aliases kept so existing extras-dict - # declarations continue to receive computed values until callers - # migrate. - "alpha_i", - "beta_out", ) @@ -509,10 +504,6 @@ def _populate_output_extras( ``None``; a debug-level log message records why. * Empty ``solutions`` leaves every slot as an empty list. * Each successful call **replaces** the prior contents of the slot. - * The deprecated keys ``"alpha_i"`` / ``"beta_out"`` are populated - identically to their canonical counterparts - ``"incidence"`` / ``"emergence"``. REMOVE-AT-V1.0: drop this - bullet alongside the legacy keys. """ if mode is None or not getattr(mode, "extras", None): return @@ -532,9 +523,6 @@ def _populate_output_extras( "emergence": _emergence_angle, "psi": _psi_angle, "omega": _omega_pseudo, - # REMOVE-AT-V1.0: deprecated aliases routed to the same computers. - "alpha_i": _incidence_angle, - "beta_out": _emergence_angle, } for key in relevant: @@ -569,20 +557,6 @@ def _populate_output_extras( failure, ) - # REMOVE-AT-V1.0: back-fill the legacy slot names so out-of-tree - # callers reading ``result.extras["alpha_i"]`` / ``["beta_out"]`` - # continue to see the populated value after the in-tree YAML files - # have been migrated to declare only the canonical slots. Without - # this back-fill a caller would see KeyError immediately after the - # YAML migration in Step 4 of the same PR, even though the canonical - # slot is populated. Mirror writes use ``dict.__setitem__`` so they - # are not subject to any future ``_ExtrasDict`` aliasing path and - # do not emit spurious deprecation warnings on internal writes. - _LEGACY_OUTPUT_KEY_MIRROR = {"incidence": "alpha_i", "emergence": "beta_out"} - for canonical_key, legacy_key in _LEGACY_OUTPUT_KEY_MIRROR.items(): - if canonical_key in mode.extras: - dict.__setitem__(mode.extras, legacy_key, mode.extras[canonical_key]) - # --------------------------------------------------------------------------- # Constraint-set dispatcher @@ -653,7 +627,7 @@ def _solve_constraint_set( # Free-detectors mode (issue #264 — both detector stages float to # satisfy Bragg jointly with the remaining sample stages, optionally - # with one ReferenceConstraint such as alpha_i). + # with one ReferenceConstraint such as incidence). if _is_free_detectors_mode(geometry, mode): return _solve_free_detectors(geometry, Q_phi, ttheta_deg, mode) @@ -3056,7 +3030,7 @@ def _is_surface_mode(geometry: AdHocDiffractometer, mode) -> bool: return False # Defer to :func:`_is_free_detectors_mode` for psic-family modes # with 2 free sample stages and 2 free detector stages (issue - # #264 — the B3 mode ``fixed_alpha_i_fixed_chi_fixed_phi``). The + # #264 — the B3 mode ``fixed_incidence_fixed_chi_fixed_phi``). The # ``chi`` stage check restricts this exclusion to psic; existing # zaxis/s2d2/sixc surface modes (which also have free detectors) # stay on :func:`_solve_surface` where the legacy 1-D Newton works @@ -3110,9 +3084,6 @@ def _solve_surface( from .mode import ReferenceConstraint - # Extract the reference constraint. ``rc.name`` is always canonical - # because :class:`ReferenceConstraint` canonicalizes the deprecated - # aliases (``"alpha_i"`` / ``"beta_out"``) at construction time. rc = next(c for c in mode.constraints if isinstance(c, ReferenceConstraint)) target_name = rc.name # "incidence", "emergence", or "specular" target_value = rc.value # float or True @@ -3822,12 +3793,12 @@ def _is_free_detectors_mode(geometry: AdHocDiffractometer, mode) -> bool: Used by :func:`_solve_free_detectors` to dispatch psic-family modes that fix multiple sample stages and let the detector arm orient itself entirely from the Bragg condition (and any reference - constraint such as ``alpha_i``). Examples (issue #264): + constraint such as ``incidence``). Examples (issue #264): - ``lifting_detector_eta`` (3 sample fixed, 1 sample + 2 detector free) - revised ``lifting_detector_phi`` / ``lifting_detector_mu`` (after step C of #264 — same shape, different fixed sample stage) - - ``fixed_alpha_i_fixed_chi_fixed_phi`` (2 sample fixed + alpha_i, + - ``fixed_incidence_fixed_chi_fixed_phi`` (2 sample fixed + incidence, 2 sample + 2 detector free) The predicate is intentionally conservative: it requires the @@ -3896,7 +3867,7 @@ def _solve_free_detectors( - ``lifting_detector_eta`` (B4) - revised ``lifting_detector_phi`` / ``lifting_detector_mu`` (C3, C4) - - ``fixed_alpha_i_fixed_chi_fixed_phi`` (B3, with one alpha_i row) + - ``fixed_incidence_fixed_chi_fixed_phi`` (B3, with one incidence row) Variables: every free sample stage plus both detector stages. Equations: 3 from Bragg (``Q_phi(angles) == Q_phi_target``) plus 1 per diff --git a/src/ad_hoc_diffractometer/geometries/schema.json b/src/ad_hoc_diffractometer/geometries/schema.json index 33579aa4..375ed256 100644 --- a/src/ad_hoc_diffractometer/geometries/schema.json +++ b/src/ad_hoc_diffractometer/geometries/schema.json @@ -238,8 +238,7 @@ "type": { "const": "reference" }, "name": { "type": "string", - "description": "Reference-constraint pseudo-angle name. The canonical names are 'psi', 'incidence', 'emergence', 'a_eq_b', 'naz', 'omega'. The legacy names 'alpha_i' and 'beta_out' are accepted for backward compatibility (REMOVE-AT-V1.0) and rewritten to 'incidence' and 'emergence' respectively by the loader, which emits one DeprecationWarning per file (issue #299).", - "enum": ["psi", "incidence", "emergence", "a_eq_b", "naz", "omega", "alpha_i", "beta_out"] + "enum": ["psi", "incidence", "emergence", "specular", "naz", "omega"] }, "value": { "oneOf": [ diff --git a/src/ad_hoc_diffractometer/geometry_loader.py b/src/ad_hoc_diffractometer/geometry_loader.py index 9f2650fc..f6f2152e 100644 --- a/src/ad_hoc_diffractometer/geometry_loader.py +++ b/src/ad_hoc_diffractometer/geometry_loader.py @@ -88,7 +88,6 @@ from .factories import KAPPA_ALPHA_DEFAULT from .kappa import KappaPseudoAngleConvention from .kappa import kappa_axis_from_eulerian -from .mode import _DEPRECATED_REFERENCE_NAME_ALIASES # noqa: PLC2701 from .mode import REQUIRED from .mode import BisectConstraint from .mode import ConstraintSet @@ -679,81 +678,6 @@ def _is_geometry_yaml_text(text: str) -> tuple[bool, Any]: # --------------------------------------------------------------------------- -def _rewrite_legacy_reference_names( - doc: dict[str, Any], - *, - source_label: str, -) -> None: - """REMOVE-AT-V1.0: rewrite legacy reference-constraint names in place. - - Walks the parsed YAML document, finds any occurrence of the - deprecated reference-constraint names (``"alpha_i"`` / ``"beta_out"``) - in mode constraint specs and extras-dict keys, rewrites them to the - canonical names (``"incidence"`` / ``"emergence"``), and emits one - :class:`DeprecationWarning` per file naming the source and listing - the legacy names found. - - Performing the rewrite at file-load granularity (rather than per - constraint, as the :class:`ReferenceConstraint` constructor would - do) collapses what could be N warnings per file (one per legacy - occurrence) into a single warning that names the file. The - downstream :class:`ReferenceConstraint` construction never sees a - legacy name and therefore does not re-warn. - - Operates in-place on ``doc``. A document containing no legacy - names is left untouched and no warning fires. - """ - modes = doc.get("modes") - if not isinstance(modes, dict): - return - found: set[str] = set() - for mode_spec in modes.values(): - if not isinstance(mode_spec, dict): - continue - # Rewrite constraint spec names. - constraints = mode_spec.get("constraints") - if isinstance(constraints, list): - for c in constraints: - if not isinstance(c, dict): - continue - if c.get("type") != "reference": - continue - cname = c.get("name") - if ( - isinstance(cname, str) - and cname in _DEPRECATED_REFERENCE_NAME_ALIASES - ): - found.add(cname) - c["name"] = _DEPRECATED_REFERENCE_NAME_ALIASES[cname] - # Rewrite extras-dict keys. - extras = mode_spec.get("extras") - if isinstance(extras, dict): - legacy_keys = [ - k for k in list(extras) if k in _DEPRECATED_REFERENCE_NAME_ALIASES - ] - for legacy_key in legacy_keys: - canonical_key = _DEPRECATED_REFERENCE_NAME_ALIASES[legacy_key] - found.add(legacy_key) - # Preserve insertion order: replace the legacy key with - # the canonical key at the same position. If the - # canonical key is already present (mixed legacy / - # canonical declaration), drop the legacy entry. - if canonical_key in extras: - del extras[legacy_key] - else: - extras[canonical_key] = extras.pop(legacy_key) - if found: - warnings.warn( - f"{source_label}: reference-constraint names " - f"{sorted(found)!r} are deprecated; YAML rewritten to use " - f"the canonical names " - f"{sorted(_DEPRECATED_REFERENCE_NAME_ALIASES[n] for n in found)!r}." - f" (issue #299)", - DeprecationWarning, - stacklevel=4, - ) - - def _construct_from_doc( doc: dict[str, Any], *, @@ -781,13 +705,6 @@ def _construct_from_doc( _validate_marker(doc) _check_unknown_keys(doc, _TOP_LEVEL_KEYS, "at top level") - # REMOVE-AT-V1.0: rewrite legacy reference-constraint names and - # extras keys in the parsed document. Performed here, once per - # file, so the downstream ReferenceConstraint construction path - # never sees a legacy name and does not emit per-constraint - # deprecation warnings. - _rewrite_legacy_reference_names(doc, source_label=source_label) - if "name" not in doc: raise GeometrySchemaError( f"{source_label}: missing required top-level key 'name'." diff --git a/src/ad_hoc_diffractometer/mode.py b/src/ad_hoc_diffractometer/mode.py index b60417a5..37de8597 100644 --- a/src/ad_hoc_diffractometer/mode.py +++ b/src/ad_hoc_diffractometer/mode.py @@ -40,9 +40,7 @@ polarization axis, etc.) stored on the geometry. The named options are physical pseudo-angles from You (1999) and Lohmeier & Vlieg (1993): ``"psi"``, ``"incidence"``, ``"emergence"``, ``"specular"``, ``"naz"``. -``"alpha_i"`` and ``"beta_out"`` are accepted as deprecated aliases for -``"incidence"`` and ``"emergence"`` respectively (issue #299). At most -one reference constraint is allowed. +At most one reference constraint is allowed. Classes ------- @@ -204,11 +202,6 @@ def __init__( "specular", "naz", "omega", - # REMOVE-AT-V1.0: legacy alias names accepted on input and - # canonicalized to the new names by ReferenceConstraint.__init__. - "alpha_i", - "beta_out", - "a_eq_b", } ) """Valid reference constraint names (physical pseudo-angles). @@ -227,86 +220,9 @@ def __init__( - ``"omega"`` — SPEC ``OMEGA`` pseudo-angle (``Q[6]``): angle between Q and the plane of the chi circle (psic family); see :func:`~ad_hoc_diffractometer.reference.omega_pseudo` - -The legacy names ``"alpha_i"``, ``"beta_out"``, and ``"a_eq_b"`` are also -accepted as deprecated aliases for ``"incidence"``, ``"emergence"``, and -``"specular"`` respectively. Constructing a :class:`ReferenceConstraint` -with a legacy name emits :class:`DeprecationWarning` and canonicalizes to -the new name; the stored ``name`` attribute is always one of the canonical -values listed above. See :data:`_DEPRECATED_REFERENCE_NAME_ALIASES`. -REMOVE-AT-V1.0: the preceding paragraph documents the deprecation -aliases and should be deleted alongside them. -""" - -# REMOVE-AT-V1.0: this entire mapping (and every site that consults it) -# is the deprecation infrastructure for issue #299. -_DEPRECATED_REFERENCE_NAME_ALIASES: dict[str, str] = { - "alpha_i": "incidence", - "beta_out": "emergence", - "a_eq_b": "specular", -} -"""Mapping from deprecated reference-constraint name to its canonical form. - -``"alpha_i"`` / ``"beta_out"`` / ``"a_eq_b"`` are kept as forwarding aliases -so existing user code, saved sessions, and out-of-tree YAML files continue -to work. - -:class:`ReferenceConstraint`, the YAML loader, the forward solver's output -extras, and ``from_dict`` all consult this map. When a legacy name is -supplied, a :class:`DeprecationWarning` is emitted and the canonical name -is used for all downstream storage and comparison. Use the canonical -names (``"incidence"``, ``"emergence"``, ``"specular"``) in new code. -""" - -# REMOVE-AT-V1.0: this mapping (and every site that consults it) is the -# deprecation infrastructure for the issue #299 mode-name renames. -_DEPRECATED_MODE_NAME_ALIASES: dict[str, str] = { - "fixed_alpha_i_vertical": "fixed_incidence_vertical", - "fixed_alpha_i_horizontal": "fixed_incidence_horizontal", - "fixed_alpha_i_fixed_chi_fixed_phi": "fixed_incidence_fixed_chi_fixed_phi", - "fixed_alpha_zaxis": "fixed_incidence_zaxis", - "fixed_beta_out_vertical": "fixed_emergence_vertical", - "fixed_beta_out_horizontal": "fixed_emergence_horizontal", - "fixed_beta_zaxis": "fixed_emergence_zaxis", - "alpha_eq_beta_vertical": "specular_vertical", - "alpha_eq_beta_horizontal": "specular_horizontal", - "alpha_eq_beta_zaxis": "specular_zaxis", -} -"""Mapping from deprecated mode name to its canonical form. - -The legacy mode-name strings are kept as forwarding aliases so existing -user code (e.g. ``g.mode_name = "fixed_alpha_i_vertical"``) and saved -sessions continue to work. :class:`ModeDict`'s read / write / delete / -membership-test methods and :attr:`AdHocDiffractometer.mode_name`'s -setter all consult this map. When a legacy name is supplied, a -:class:`DeprecationWarning` is emitted and the canonical name is used -for all downstream storage and lookup. """ -def _canonicalize_mode_name(name: str, *, operation: str) -> str: - """REMOVE-AT-V1.0: rewrite a legacy mode-name alias to its canonical form. - - Emits :class:`DeprecationWarning` when the rewrite fires. - ``operation`` is one of ``"read"`` / ``"write"`` / ``"delete"`` and - is interpolated into the warning text so the caller sees which - access path triggered it. Returns ``name`` unchanged when no alias - applies. - """ - canonical = _DEPRECATED_MODE_NAME_ALIASES.get(name) - if canonical is None: - return name - import warnings - - warnings.warn( - f"Mode name {name!r} {operation} is deprecated; " - f"use {canonical!r} instead. (issue #299)", - DeprecationWarning, - stacklevel=3, - ) - return canonical - - QAZ: str = "qaz" """Special name for the qaz detector pseudo-angle (You 1999, eq. 18). @@ -941,16 +857,13 @@ class ReferenceConstraint: Azimuthal angle of n̂ about Q (You 1999, eq. 23). ``"incidence"`` Angle of incidence: angle between the incident beam and the surface - plane (perpendicular to n̂). Accepts the deprecated alias - ``"alpha_i"`` (REMOVE-AT-V1.0). + plane (perpendicular to n̂). ``"emergence"`` Angle of emergence: angle between the diffracted beam and the - surface plane. Accepts the deprecated alias ``"beta_out"`` - (REMOVE-AT-V1.0). + surface plane. ``"specular"`` Specular reflection: relational condition ``incidence = emergence``. - ``value`` must be ``True``. Accepts the deprecated alias - ``"a_eq_b"`` (REMOVE-AT-V1.0). + ``value`` must be ``True``. ``"naz"`` Azimuthal angle of n̂ in the lab frame (You 1999). @@ -958,11 +871,7 @@ class ReferenceConstraint: ---------- name : str One of ``"psi"``, ``"incidence"``, ``"emergence"``, ``"specular"``, - ``"naz"``, ``"omega"``. The deprecated aliases ``"alpha_i"``, - ``"beta_out"``, and ``"a_eq_b"`` are also accepted (with a - :class:`DeprecationWarning`) and canonicalized to ``"incidence"``, - ``"emergence"``, and ``"specular"`` respectively before storage. - REMOVE-AT-V1.0: drop the alias clause. + ``"naz"``, ``"omega"``. value : float or bool Target value in degrees (or ``True`` for ``"specular"``). @@ -981,29 +890,10 @@ class ReferenceConstraint: def __init__(self, name: str, value: float | bool) -> None: if name not in REFERENCE_NAMES: - # Compose the user-visible list of valid names from the canonical - # set only; deprecated aliases are accepted on input but should - # not be advertised in the error message for invalid names. - canonical = sorted( - REFERENCE_NAMES - _DEPRECATED_REFERENCE_NAME_ALIASES.keys() - ) raise ValueError( - f"ReferenceConstraint name must be one of {canonical}; got {name!r}." - ) - # REMOVE-AT-V1.0: canonicalize deprecated aliases. Emit a - # DeprecationWarning so callers see the recommended replacement, - # but the construction itself still succeeds. - if name in _DEPRECATED_REFERENCE_NAME_ALIASES: - import warnings - - canonical_name = _DEPRECATED_REFERENCE_NAME_ALIASES[name] - warnings.warn( - f"ReferenceConstraint name {name!r} is deprecated; " - f"use {canonical_name!r} instead. (issue #299)", - DeprecationWarning, - stacklevel=2, + f"ReferenceConstraint name must be one of " + f"{sorted(REFERENCE_NAMES)}; got {name!r}." ) - name = canonical_name if name == "specular" and value is not True: raise ValueError( "ReferenceConstraint('specular', value): value must be True; " @@ -1507,11 +1397,11 @@ def with_constraint_values(self, **updates: float | bool) -> ConstraintSet: ... g.modes["fixed_chi_vertical"].with_constraint_values(chi=45.0) ... ) - Multiple values at once (psic ``fixed_alpha_i_fixed_chi_fixed_phi``): + Multiple values at once (psic ``fixed_incidence_fixed_chi_fixed_phi``): - >>> g.modes["fixed_alpha_i_fixed_chi_fixed_phi"] = ( - ... g.modes["fixed_alpha_i_fixed_chi_fixed_phi"] - ... .with_constraint_values(chi=15.0, phi=30.0, alpha_i=5.0) + >>> g.modes["fixed_incidence_fixed_chi_fixed_phi"] = ( + ... g.modes["fixed_incidence_fixed_chi_fixed_phi"] + ... .with_constraint_values(chi=15.0, phi=30.0, incidence=5.0) ... ) Notes @@ -1544,32 +1434,7 @@ def with_constraint_values(self, **updates: float | bool) -> ConstraintSet: ) name_to_index[cname] = idx - # REMOVE-AT-V1.0: accept the deprecated reference-constraint - # kwargs ``alpha_i`` / ``beta_out`` as aliases for ``incidence`` - # / ``emergence``. The alias only fires when the set actually - # contains the canonical constraint; otherwise the kwarg falls - # through to the regular unknown-key path so a genuine typo is - # not masked by a confusing deprecation warning followed by a - # KeyError. After removal, this whole canonicalization loop - # becomes a single ``canonicalized = updates`` assignment (or - # the variable goes away entirely). - canonicalized: dict[str, float | bool] = {} - for key, new_value in updates.items(): - canonical = _DEPRECATED_REFERENCE_NAME_ALIASES.get(key) - if canonical is not None and canonical in name_to_index: - import warnings - - warnings.warn( - f"with_constraint_values: kwarg {key!r} is deprecated; " - f"use {canonical!r} instead. (issue #299)", - DeprecationWarning, - stacklevel=2, - ) - canonicalized[canonical] = new_value - else: - canonicalized[key] = new_value - - unknown = sorted(k for k in canonicalized if k not in name_to_index) + unknown = sorted(k for k in updates if k not in name_to_index) if unknown: available = sorted(name_to_index) raise KeyError( @@ -1579,7 +1444,7 @@ def with_constraint_values(self, **updates: float | bool) -> ConstraintSet: ) new_constraints: list[AnyConstraint] = list(self._constraints) - for name, new_value in canonicalized.items(): + for name, new_value in updates.items(): idx = name_to_index[name] original = new_constraints[idx] # Each settable-value constraint is constructed with the same @@ -1690,12 +1555,6 @@ class ModeDict: Only :class:`ConstraintSet` instances may be stored. Keys are mode names (str). Iteration follows insertion order. - Deprecated mode-name aliases are canonicalized on every read, write, - delete, and membership test (issue #299). Iteration / ``keys()`` / - ``values()`` / ``items()`` return canonical keys only; a legacy name - accessed via ``__getitem__`` returns the canonical mode but does not - appear under its legacy spelling in iteration. - Parameters ---------- modes : dict[str, ConstraintSet] or None @@ -1728,26 +1587,15 @@ def __setitem__(self, name: str, mode: ConstraintSet) -> None: f"ModeDict values must be ConstraintSet instances; " f"got {type(mode).__name__!r} for key {name!r}." ) - # REMOVE-AT-V1.0: canonicalize legacy mode-name aliases on write. - name = _canonicalize_mode_name(name, operation="write") self._data[name] = mode def __getitem__(self, name: str) -> ConstraintSet: - # REMOVE-AT-V1.0: canonicalize legacy mode-name aliases on read. - name = _canonicalize_mode_name(name, operation="read") return self._data[name] def __delitem__(self, name: str) -> None: - # REMOVE-AT-V1.0: canonicalize legacy mode-name aliases on delete. - name = _canonicalize_mode_name(name, operation="delete") del self._data[name] def __contains__(self, name: object) -> bool: - # REMOVE-AT-V1.0: legacy mode-name aliases are "contained" iff the - # canonical name is present. No warning here — ``in`` tests are - # frequently defensive and per-test warnings would be noisy. - if isinstance(name, str) and name in _DEPRECATED_MODE_NAME_ALIASES: - name = _DEPRECATED_MODE_NAME_ALIASES[name] return name in self._data def __len__(self) -> int: diff --git a/src/ad_hoc_diffractometer/reference.py b/src/ad_hoc_diffractometer/reference.py index e29be383..77c611c9 100644 --- a/src/ad_hoc_diffractometer/reference.py +++ b/src/ad_hoc_diffractometer/reference.py @@ -20,7 +20,6 @@ :func:`emergence_angle` Angle of emergence α_f between the diffracted beam and the sample surface. - Accepts the deprecated alias :func:`exit_angle` (REMOVE-AT-V1.0). :func:`psi_angle` Azimuthal angle ψ of the reference vector n̂ about Q (You 1999, eq. 23). @@ -169,25 +168,6 @@ def emergence_angle( return geometry.emergence(angles=angles) -# REMOVE-AT-V1.0: deprecated alias for the canonical emergence_angle -# function. Emit DeprecationWarning on every call and delegate. -def exit_angle( - geometry: AdHocDiffractometer, - angles: dict[str, float] | None = None, -) -> float: - """Deprecated alias for :func:`emergence_angle`. REMOVE-AT-V1.0.""" - import warnings - - warnings.warn( - "ad_hoc_diffractometer.reference.exit_angle() is deprecated; " - "use ad_hoc_diffractometer.reference.emergence_angle() instead. " - "(issue #299)", - DeprecationWarning, - stacklevel=2, - ) - return emergence_angle(geometry, angles) - - def psi_angle( geometry: AdHocDiffractometer, angles: dict[str, float] | None = None, diff --git a/src/ad_hoc_diffractometer/surface.py b/src/ad_hoc_diffractometer/surface.py index 5d931fd0..1822740f 100644 --- a/src/ad_hoc_diffractometer/surface.py +++ b/src/ad_hoc_diffractometer/surface.py @@ -14,11 +14,11 @@ ---------- incidence(geometry, angles=None) Angle of incidence αᵢ in degrees — angle between the incoming beam - and the sample surface. Accepts the deprecated alias ``alpha_i``. + and the sample surface. emergence(geometry, angles=None) Angle of emergence αf in degrees — angle between the diffracted beam - and the sample surface. Accepts the deprecated alias ``alpha_f``. + and the sample surface. q_components(geometry, angles=None) Decompose Q into Q⊥ (perpendicular to sample surface) and Q‖ @@ -294,43 +294,6 @@ def emergence( return math.degrees(math.asin(abs(sin_af))) -# REMOVE-AT-V1.0: deprecated aliases for the canonical incidence / -# emergence functions. Emit DeprecationWarning on every call and -# delegate to the canonical implementation. -def alpha_i( - geometry: AdHocDiffractometer, - angles: dict[str, float] | None = None, -) -> float: - """Deprecated alias for :func:`incidence`. REMOVE-AT-V1.0.""" - import warnings - - warnings.warn( - "ad_hoc_diffractometer.surface.alpha_i() is deprecated; " - "use ad_hoc_diffractometer.surface.incidence() instead. " - "(issue #299)", - DeprecationWarning, - stacklevel=2, - ) - return incidence(geometry, angles) - - -def alpha_f( - geometry: AdHocDiffractometer, - angles: dict[str, float] | None = None, -) -> float: - """Deprecated alias for :func:`emergence`. REMOVE-AT-V1.0.""" - import warnings - - warnings.warn( - "ad_hoc_diffractometer.surface.alpha_f() is deprecated; " - "use ad_hoc_diffractometer.surface.emergence() instead. " - "(issue #299)", - DeprecationWarning, - stacklevel=2, - ) - return emergence(geometry, angles) - - def q_components( geometry: AdHocDiffractometer, angles: dict[str, float] | None = None, diff --git a/tests/test_diffractometer.py b/tests/test_diffractometer.py index 8d566c31..4ba7759b 100644 --- a/tests/test_diffractometer.py +++ b/tests/test_diffractometer.py @@ -829,129 +829,6 @@ def test_azimuth_wrong_length_raises(): g.azimuth = (0, 1) -# --------------------------------------------------------------------------- -# azimuthal_reference deprecated alias (#298) -# --------------------------------------------------------------------------- -# -# Issue #298 renamed `azimuthal_reference` to `azimuth`. The old name is -# kept as a forwarding alias that emits DeprecationWarning on both read -# and write. The constructor keyword is kept under the same policy. -# `to_dict()` writes the new key `"azimuth"`; `from_dict()` accepts -# either the new or the legacy key for backward compatibility. - - -def test_azimuthal_reference_alias_set_warns(): - """Writing the deprecated name emits DeprecationWarning.""" - from helpers import fourcv - - g = fourcv() - with pytest.warns(DeprecationWarning, match=re.escape("azimuthal_reference")): - g.azimuthal_reference = (0, 0, 1) - - -def test_azimuthal_reference_alias_get_warns(): - """Reading the deprecated name emits DeprecationWarning.""" - from helpers import fourcv - - g = fourcv() - g.azimuth = (0, 0, 1) - with pytest.warns(DeprecationWarning, match=re.escape("azimuthal_reference")): - _ = g.azimuthal_reference - - -def test_azimuthal_reference_alias_shares_storage(): - """The deprecated alias and the canonical name share underlying storage.""" - import warnings - - from helpers import fourcv - - g = fourcv() - g.azimuth = (0, 0, 1) - with warnings.catch_warnings(): - warnings.simplefilter("ignore", DeprecationWarning) - assert g.azimuthal_reference == (0.0, 0.0, 1.0) - g.azimuthal_reference = (1, 0, 0) - assert g.azimuth == (1.0, 0.0, 0.0) - - -def test_azimuthal_reference_constructor_kwarg_warns(): - """The deprecated constructor kwarg emits DeprecationWarning.""" - from helpers import fourcv - - from ad_hoc_diffractometer.diffractometer import AdHocDiffractometer - - template = fourcv() - with pytest.warns(DeprecationWarning, match=re.escape("azimuthal_reference")): - g = AdHocDiffractometer( - name="fourcv", - stages=list(template._stages.values()), - basis=template.basis, - azimuthal_reference=(0, 0, 1), - ) - assert g.azimuth == (0.0, 0.0, 1.0) - - -def test_azimuthal_reference_and_azimuth_conflict_raises(): - """Supplying both kwargs with disagreeing values raises ValueError.""" - from helpers import fourcv - - from ad_hoc_diffractometer.diffractometer import AdHocDiffractometer - - template = fourcv() - with pytest.raises(ValueError, match=re.escape("Cannot specify both")): - AdHocDiffractometer( - name="fourcv", - stages=list(template._stages.values()), - basis=template.basis, - azimuth=(0, 0, 1), - azimuthal_reference=(1, 0, 0), - ) - - -def test_azimuth_and_azimuthal_reference_matching_values_accepted(): - """Supplying both kwargs with matching values warns but does not raise.""" - from helpers import fourcv - - from ad_hoc_diffractometer.diffractometer import AdHocDiffractometer - - template = fourcv() - with pytest.warns(DeprecationWarning, match=re.escape("azimuthal_reference")): - g = AdHocDiffractometer( - name="fourcv", - stages=list(template._stages.values()), - basis=template.basis, - azimuth=(0, 0, 1), - azimuthal_reference=(0, 0, 1), - ) - assert g.azimuth == (0.0, 0.0, 1.0) - - -def test_to_dict_writes_azimuth_key_not_legacy(): - """to_dict() emits the new key "azimuth" and not the legacy key.""" - from helpers import fourcv - - g = fourcv() - g.azimuth = (0, 0, 1) - d = g.to_dict() - assert d["azimuth"] == [0.0, 0.0, 1.0] - assert "azimuthal_reference" not in d - - -def test_from_dict_accepts_legacy_azimuthal_reference_key(): - """from_dict() reads the legacy "azimuthal_reference" key when present.""" - from helpers import fourcv - - from ad_hoc_diffractometer.diffractometer import AdHocDiffractometer - - g = fourcv() - g.azimuth = (0, 0, 1) - d = g.to_dict() - # Simulate a session saved by ad_hoc_diffractometer < v0.12.0 - d["azimuthal_reference"] = d.pop("azimuth") - g2 = AdHocDiffractometer.from_dict(d) - assert g2.azimuth == (0.0, 0.0, 1.0) - - # --------------------------------------------------------------------------- # psi() method (#11) # --------------------------------------------------------------------------- diff --git a/tests/test_forward.py b/tests/test_forward.py index 17994160..4e2d9613 100644 --- a/tests/test_forward.py +++ b/tests/test_forward.py @@ -3172,7 +3172,7 @@ def test_psic_fixed_alpha_i_fixed_chi_fixed_phi_round_trip( alpha_target, context, ): - """Issue #264 B3: 4-D Newton with chi=phi=0 + alpha_i target.""" + """Issue #264 B3: 4-D Newton with chi=phi=0 + incidence target.""" from ad_hoc_diffractometer import REQUIRED from ad_hoc_diffractometer import ConstraintSet from ad_hoc_diffractometer import ReferenceConstraint @@ -3186,20 +3186,20 @@ def test_psic_fixed_alpha_i_fixed_chi_fixed_phi_round_trip( [ SampleConstraint("chi", 0.0), SampleConstraint("phi", 0.0), - ReferenceConstraint("alpha_i", alpha_target), + ReferenceConstraint("incidence", alpha_target), ], computed=["mu", "eta", "nu", "delta"], - extras={"n_hat": REQUIRED, "alpha_i": None, "beta_out": None}, + extras={"n_hat": REQUIRED, "incidence": None, "emergence": None}, ) g.mode_name = "__test_b3" sols = g.forward(h, k, l) - assert len(sols) > 0, f"B3 ({h},{k},{l}) alpha_i={alpha_target}: no solutions" + assert len(sols) > 0, f"B3 ({h},{k},{l}) incidence={alpha_target}: no solutions" for sol in sols: assert sol["chi"] == pytest.approx(0.0, abs=1e-6) assert sol["phi"] == pytest.approx(0.0, abs=1e-6) ai = incidence_angle(g, angles=sol) assert ai == pytest.approx(alpha_target, abs=1e-3), ( - f"B3 ({h},{k},{l}) alpha_i target {alpha_target}: got {ai}" + f"B3 ({h},{k},{l}) incidence target {alpha_target}: got {ai}" ) hkl_back = g.inverse(sol) assert np.allclose(hkl_back, [h, k, l], atol=1e-5) diff --git a/tests/test_geometry_loader.py b/tests/test_geometry_loader.py index 42946274..a273c2d5 100644 --- a/tests/test_geometry_loader.py +++ b/tests/test_geometry_loader.py @@ -1378,152 +1378,3 @@ def test_basis_defensive_det_check_via_monkeypatch(monkeypatch): with pytest.raises(GeometrySchemaError, match="degenerate or non-orthonormal"): _validate_basis(basis) monkeypatch.setattr(np.linalg, "det", real_det) - - -# --------------------------------------------------------------------------- -# REMOVE-AT-V1.0: loader rewriting of legacy reference-constraint names -# (issue #299). -# --------------------------------------------------------------------------- - -_LEGACY_REFERENCE_YAML = """\ -ad_hoc_diffractometer_geometry: - schema_revision: 1 -name: legacy_alias -documentation: A geometry whose YAML still uses the legacy reference names. -basis: BL -stages: - - {name: mu, axis: -transverse, parent: null, role: sample} - - {name: eta, axis: -transverse, parent: mu, role: sample} - - {name: chi, axis: +longitudinal, parent: eta, role: sample} - - {name: phi, axis: -transverse, parent: chi, role: sample} - - {name: nu, axis: +vertical, parent: null, role: detector} - - {name: delta, axis: -transverse, parent: nu, role: detector} -modes: - legacy_incidence: - default: true - constraints: - - {type: sample, stage: mu, value: 0.0} - - {type: sample, stage: eta, value: 0.0} - - {type: sample, stage: chi, value: 0.0} - - {type: reference, name: alpha_i, value: 0.0} - computed: [phi, nu, delta] - extras: - n_hat: REQUIRED - alpha_i: null - beta_out: null -""" - - -def test_loader_rewrites_legacy_reference_names_emits_single_warning(): - """Loading YAML with legacy ``alpha_i`` / ``beta_out`` names emits one - DeprecationWarning per file and produces a geometry with canonical - constraint and extras keys. - - Issue #299. An out-of-tree YAML file written against the v0.11.x - API still uses the deprecated reference-constraint names; the loader - rewrites them in-place and surfaces a single deprecation warning - naming the file rather than per-constraint warnings from - :class:`ReferenceConstraint.__init__`. - """ - import warnings - - with warnings.catch_warnings(record=True) as captured: - warnings.simplefilter("always") - g = load_geometry_file(_LEGACY_REFERENCE_YAML) - - # Exactly one DeprecationWarning fires, from the loader's rewriting - # path; the per-constraint warning from ReferenceConstraint.__init__ - # is suppressed because the loader rewrites the names before - # construction. - dep_warnings = [w for w in captured if issubclass(w.category, DeprecationWarning)] - rewriting_warnings = [w for w in dep_warnings if "YAML rewritten" in str(w.message)] - assert len(rewriting_warnings) == 1, ( - f"expected exactly one loader rewriting DeprecationWarning; got " - f"{len(rewriting_warnings)}: {[str(w.message) for w in rewriting_warnings]}" - ) - msg = str(rewriting_warnings[0].message) - assert "alpha_i" in msg and "beta_out" in msg - assert "incidence" in msg and "emergence" in msg - assert "issue #299" in msg - - # Per-constraint warnings from ReferenceConstraint.__init__ are - # absent (they would fire only if a legacy name reached the - # constructor, which the loader rewrite prevents). - per_constraint_warnings = [ - w for w in dep_warnings if "ReferenceConstraint name" in str(w.message) - ] - assert per_constraint_warnings == [], ( - f"loader rewrite should suppress per-constraint warnings; got " - f"{[str(w.message) for w in per_constraint_warnings]}" - ) - - # The loaded geometry has the canonical constraint name and the - # canonical extras keys (no legacy spellings). - cs = g.modes["legacy_incidence"] - from ad_hoc_diffractometer.mode import ReferenceConstraint - - ref_constraints = [c for c in cs.constraints if isinstance(c, ReferenceConstraint)] - assert len(ref_constraints) == 1 - assert ref_constraints[0].name == "incidence" - assert "incidence" in cs.extras - assert "emergence" in cs.extras - assert "alpha_i" not in cs.extras - assert "beta_out" not in cs.extras - - -def test_loader_drops_legacy_extras_key_when_canonical_already_present(): - """If a YAML extras dict declares both the legacy and the canonical - key (a malformed mixed declaration that nothing in the in-tree - code produces but an out-of-tree author might write), the loader - drops the legacy entry rather than overwriting the canonical one. - The canonical slot's value wins; the legacy slot's value is - discarded. - """ - mixed_yaml = _LEGACY_REFERENCE_YAML.replace( - "alpha_i: null\n beta_out: null", - "alpha_i: null\n incidence: null\n beta_out: null", - ) - import warnings - - with warnings.catch_warnings(record=True) as captured: - warnings.simplefilter("always") - g = load_geometry_file(mixed_yaml) - # Loader still fires the rewriting warning (legacy names were present). - dep_warnings = [ - w - for w in captured - if issubclass(w.category, DeprecationWarning) - and "YAML rewritten" in str(w.message) - ] - assert len(dep_warnings) == 1 - # Canonical key survives; legacy key is gone. - cs = g.modes["legacy_incidence"] - assert "incidence" in cs.extras - assert "alpha_i" not in cs.extras - - -def test_loader_skips_rewriting_when_yaml_is_already_canonical(): - """No DeprecationWarning fires for YAML that already uses the canonical - names — the rewriting path activates only when legacy names are - found. This protects in-tree YAML files (migrated to canonical - names) from triggering spurious deprecation noise at every - ``make_geometry()`` call. - """ - import warnings - - canonical_yaml = _LEGACY_REFERENCE_YAML.replace("alpha_i", "incidence").replace( - "beta_out", "emergence" - ) - with warnings.catch_warnings(record=True) as captured: - warnings.simplefilter("always") - load_geometry_file(canonical_yaml) - - dep_warnings = [ - w - for w in captured - if issubclass(w.category, DeprecationWarning) and "issue #299" in str(w.message) - ] - assert dep_warnings == [], ( - f"canonical YAML must not trigger any issue-#299 deprecation " - f"warnings; got {[str(w.message) for w in dep_warnings]}" - ) diff --git a/tests/test_mode.py b/tests/test_mode.py index ff0f382f..88c549a8 100644 --- a/tests/test_mode.py +++ b/tests/test_mode.py @@ -762,161 +762,6 @@ def test_reference_constraint_construction(name, value, context): assert rc.category == "reference" -# REMOVE-AT-V1.0: this test and its parametrize block exist solely to -# pin the deprecation-alias canonicalization behavior; delete the whole -# function (and its decorator) when the aliases are removed. -@pytest.mark.parametrize( - "legacy_name, canonical_name, value", - [ - pytest.param("alpha_i", "incidence", 5.0, id="alpha_i-to-incidence"), - pytest.param("beta_out", "emergence", 5.0, id="beta_out-to-emergence"), - pytest.param("a_eq_b", "specular", True, id="a_eq_b-to-specular"), - ], -) -def test_reference_constraint_deprecated_name_canonicalized( - legacy_name, canonical_name, value -): - """Issue #299: legacy reference-constraint names are canonicalized. - - Constructing a ReferenceConstraint with a deprecated alias - (``"alpha_i"`` / ``"beta_out"``) emits :class:`DeprecationWarning` - and stores the canonical name (``"incidence"`` / ``"emergence"``). - The constraint behaves identically to one constructed with the - canonical name; this is the contract that lets existing user code - and saved sessions continue to work through the deprecation cycle. - """ - context = pytest.warns( - DeprecationWarning, - match=re.escape( - f"ReferenceConstraint name {legacy_name!r} is deprecated; " - f"use {canonical_name!r} instead. (issue #299)" - ), - ) - with context: - rc = ReferenceConstraint(legacy_name, value) - assert rc.name == canonical_name - assert rc.value == value - # Equal to the constraint built directly with the canonical name. - assert rc == ReferenceConstraint(canonical_name, value) - - -# REMOVE-AT-V1.0: pins the legacy-session round-trip contract — a dict -# saved by ad_hoc_diffractometer <= v0.11.x with a legacy -# reference-constraint name still loads via from_dict, emits one -# DeprecationWarning, and produces a constraint stored under the -# canonical name. -@pytest.mark.parametrize( - "legacy_name, canonical_name, value", - [ - pytest.param("alpha_i", "incidence", 5.0, id="alpha_i-to-incidence"), - pytest.param("beta_out", "emergence", 5.0, id="beta_out-to-emergence"), - pytest.param("a_eq_b", "specular", True, id="a_eq_b-to-specular"), - ], -) -def test_reference_constraint_from_dict_accepts_legacy_name( - legacy_name, canonical_name, value -): - """A saved session with a legacy reference-constraint name still loads. - - ``from_dict`` calls ``__init__``, which canonicalizes the legacy - name and emits one :class:`DeprecationWarning`. The reconstructed - constraint compares equal to one built directly with the canonical - name, so any subsequent ``to_dict`` write produces the canonical - spelling. - """ - legacy_session_dict = { - "type": "ReferenceConstraint", - "name": legacy_name, - "value": value, - } - with pytest.warns( - DeprecationWarning, - match=re.escape( - f"ReferenceConstraint name {legacy_name!r} is deprecated; " - f"use {canonical_name!r} instead. (issue #299)" - ), - ): - rc = ReferenceConstraint.from_dict(legacy_session_dict) - assert rc.name == canonical_name - # A second to_dict round-trip writes the canonical name. - assert rc.to_dict()["name"] == canonical_name - # Equal to the constraint built directly with the canonical name. - assert rc == ReferenceConstraint(canonical_name, value) - - -# REMOVE-AT-V1.0: ModeDict mode-name alias-map deprecation coverage. -@pytest.mark.parametrize( - "legacy_mode_name, canonical_mode_name", - [ - pytest.param( - "fixed_alpha_i_vertical", - "fixed_incidence_vertical", - id="fixed_alpha_i_vertical-to-fixed_incidence_vertical", - ), - pytest.param( - "fixed_beta_out_horizontal", - "fixed_emergence_horizontal", - id="fixed_beta_out_horizontal-to-fixed_emergence_horizontal", - ), - pytest.param( - "alpha_eq_beta_vertical", - "specular_vertical", - id="alpha_eq_beta_vertical-to-specular_vertical", - ), - ], -) -def test_modedict_legacy_mode_name_canonicalized(legacy_mode_name, canonical_mode_name): - """Issue #299: ModeDict canonicalizes legacy mode-name aliases on access. - - A read of a legacy mode-name key emits :class:`DeprecationWarning` - and returns the canonical mode. ``__contains__`` returns ``True`` - for the legacy name (no warning, since defensive ``in`` tests are - common). Iteration / ``keys()`` return the canonical name only. - """ - import ad_hoc_diffractometer as ahd - - g = ahd.make_geometry("psic") - - # __getitem__ rewrites and warns. - with pytest.warns( - DeprecationWarning, - match=re.escape( - f"Mode name {legacy_mode_name!r} read is deprecated; " - f"use {canonical_mode_name!r} instead. (issue #299)" - ), - ): - legacy_mode = g.modes[legacy_mode_name] - canonical_mode = g.modes[canonical_mode_name] - assert legacy_mode is canonical_mode - - # __contains__ accepts the legacy name without a warning. - assert legacy_mode_name in g.modes - assert canonical_mode_name in g.modes - - # Iteration returns canonical names only. - assert canonical_mode_name in list(g.modes) - assert legacy_mode_name not in list(g.modes) - - -def test_geometry_mode_name_setter_canonicalizes_legacy_alias(): - """Setting ``g.mode_name`` with a legacy alias is accepted, warns, and - stores the canonical name so the getter never reports a deprecated - spelling. - """ - import ad_hoc_diffractometer as ahd - - g = ahd.make_geometry("psic") - with pytest.warns( - DeprecationWarning, - match=re.escape( - "Mode name 'fixed_alpha_i_vertical' set is deprecated; " - "use 'fixed_incidence_vertical' instead. (issue #299)" - ), - ): - g.mode_name = "fixed_alpha_i_vertical" - assert g.mode_name == "fixed_incidence_vertical" - - def test_reference_constraint_value_coerced_to_float(): rc = ReferenceConstraint("psi", 90) assert isinstance(rc.value, float) @@ -954,10 +799,10 @@ def test_reference_constraint_to_dict_from_dict_a_eq_b(): @pytest.mark.parametrize( "name, value, surface_normal, expected", [ - # alpha_i/beta_out/specular: implemented when surface_normal is set - pytest.param("alpha_i", 0.0, (0, 0, 1), True, id="alpha_i-with-sn"), - pytest.param("alpha_i", 0.0, None, False, id="alpha_i-no-sn"), - pytest.param("beta_out", 0.0, (0, 0, 1), True, id="beta_out-with-sn"), + # incidence/emergence/specular: implemented when surface_normal is set + pytest.param("incidence", 0.0, (0, 0, 1), True, id="incidence-with-sn"), + pytest.param("incidence", 0.0, None, False, id="incidence-no-sn"), + pytest.param("emergence", 0.0, (0, 0, 1), True, id="emergence-with-sn"), pytest.param("specular", True, (0, 0, 1), True, id="specular-with-sn"), pytest.param("specular", True, None, False, id="specular-no-sn"), # psi/naz: never implemented (no forward solver yet) @@ -1024,7 +869,7 @@ def test_constraint_set_two_reference_raises(): ConstraintSet( [ ReferenceConstraint("psi", 0.0), - ReferenceConstraint("alpha_i", 5.0), + ReferenceConstraint("incidence", 5.0), ] ) @@ -2019,9 +1864,9 @@ def test_sixc_mode_has_bisect(mode_name, expected_has_bisect): ), pytest.param( "fixed_incidence_zaxis", - "alpha_i", + "incidence", None, - id="fixed_incidence_zaxis-alpha_i-output", + id="fixed_incidence_zaxis-incidence-output", ), pytest.param( "fixed_emergence_zaxis", @@ -2031,9 +1876,9 @@ def test_sixc_mode_has_bisect(mode_name, expected_has_bisect): ), pytest.param( "fixed_emergence_zaxis", - "beta_out", + "emergence", None, - id="fixed_emergence_zaxis-beta_out-output", + id="fixed_emergence_zaxis-emergence-output", ), pytest.param("specular_zaxis", "n_hat", "REQUIRED", id="specular_zaxis-n_hat"), ], @@ -2148,11 +1993,13 @@ def test_s2d2_fixed_mu_is_implemented(): "factory, mode_name, extras_key, expected_value", [ pytest.param(zaxis, "zaxis", "n_hat", "REQUIRED", id="zaxis-n_hat"), - pytest.param(zaxis, "zaxis", "alpha_i", None, id="zaxis-alpha_i-output"), - pytest.param(zaxis, "zaxis", "beta_out", None, id="zaxis-beta_out-output"), + pytest.param(zaxis, "zaxis", "incidence", None, id="zaxis-incidence-output"), + pytest.param(zaxis, "zaxis", "emergence", None, id="zaxis-emergence-output"), pytest.param(zaxis, "reflectivity", "n_hat", "REQUIRED", id="zaxis-refl-n_hat"), pytest.param(s2d2, "reflectivity", "n_hat", "REQUIRED", id="s2d2-refl-n_hat"), - pytest.param(s2d2, "reflectivity", "alpha_i", None, id="s2d2-alpha_i-output"), + pytest.param( + s2d2, "reflectivity", "incidence", None, id="s2d2-incidence-output" + ), ], ) def test_zaxis_s2d2_extras_declared(factory, mode_name, extras_key, expected_value): @@ -2631,7 +2478,7 @@ def _b3_constraint_set(): ReferenceConstraint("incidence", 0.0), ], computed=["mu", "eta", "nu", "delta"], - extras={"n_hat": REQUIRED, "alpha_i": None, "beta_out": None}, + extras={"n_hat": REQUIRED, "incidence": None, "emergence": None}, cut_points={"eta": -180.0}, ) @@ -2677,7 +2524,7 @@ def _b3_constraint_set(): pytest.param( # ReferenceConstraint('specular', ...) only accepts True # (the constraint expresses the *boolean* condition - # alpha_i = beta_out; there is no False variant). This + # incidence = emergence; there is no False variant). This # case verifies the bool branch of with_constraint_values # by re-applying the only legal value. lambda: ConstraintSet( @@ -2743,8 +2590,8 @@ def test_with_constraint_values_preserves_computed_extras_cut_points(): assert new.extras is not cs.extras assert new.extras == cs.extras assert new.extras["n_hat"] is REQUIRED - assert new.extras["alpha_i"] is None - assert new.extras["beta_out"] is None + assert new.extras["incidence"] is None + assert new.extras["emergence"] is None # OPTIONAL sentinel identity survives too. cs2 = ConstraintSet( [SampleConstraint("chi", 0.0)], @@ -2868,43 +2715,6 @@ def test_with_constraint_values_round_trips_via_to_dict(): assert restored.to_dict() == original_dict -# REMOVE-AT-V1.0: this test pins the deprecation-alias kwarg behavior -# of with_constraint_values; delete the whole function (and its -# decorator) when the aliases are removed. -@pytest.mark.parametrize( - "legacy_kwarg, canonical_kwarg, value", - [ - pytest.param("alpha_i", "incidence", 5.0, id="alpha_i-to-incidence"), - pytest.param("beta_out", "emergence", 5.0, id="beta_out-to-emergence"), - ], -) -def test_with_constraint_values_accepts_deprecated_alias_kwarg( - legacy_kwarg, canonical_kwarg, value -): - """Issue #299: ``with_constraint_values`` accepts the legacy kwargs. - - Passing ``alpha_i=...`` / ``beta_out=...`` is canonicalized to - ``incidence=...`` / ``emergence=...`` with a - :class:`DeprecationWarning`. The replacement value reaches the - underlying :class:`ReferenceConstraint` exactly as if the canonical - name had been used. - """ - cs = ConstraintSet( - [ReferenceConstraint(canonical_kwarg, 0.0)], - extras={"n_hat": REQUIRED}, - ) - with pytest.warns( - DeprecationWarning, - match=re.escape( - f"with_constraint_values: kwarg {legacy_kwarg!r} is deprecated; " - f"use {canonical_kwarg!r} instead. (issue #299)" - ), - ): - new = cs.with_constraint_values(**{legacy_kwarg: value}) - new_values = {c.name: c.value for c in new.constraints if hasattr(c, "name")} - assert new_values[canonical_kwarg] == value - - # --------------------------------------------------------------------------- # Documentation-placeholder extras warning (issue #294) # --------------------------------------------------------------------------- @@ -2974,13 +2784,13 @@ def test_extras_non_placeholder_keys_never_warn(): cs = ConstraintSet( [SampleConstraint("chi", 0.0)], - extras={"psi": None, "alpha_i": None, "beta_out": None, "omega": None}, + extras={"psi": None, "incidence": None, "emergence": None, "omega": None}, ) with warnings.catch_warnings(record=True) as captured: warnings.simplefilter("always") cs.extras["psi"] = 12.5 - cs.extras["alpha_i"] = 5.0 - cs.extras["beta_out"] = 5.0 + cs.extras["incidence"] = 5.0 + cs.extras["emergence"] = 5.0 cs.extras["omega"] = 0.0 cs.extras["new_key"] = "anything" placeholder_warnings = [ diff --git a/tests/test_reference.py b/tests/test_reference.py index fcd466f4..10342f41 100644 --- a/tests/test_reference.py +++ b/tests/test_reference.py @@ -6,7 +6,6 @@ Covers: - incidence_angle: requires surface_normal; raises when None - emergence_angle: requires surface_normal; raises when None - (also accepts the deprecated alias ``exit_angle``) - psi_angle: requires azimuth; raises when None - naz_angle: requires surface_normal; raises when None; vertical n̂ gives 0 - ReferenceConstraint.is_implemented(): True when reference set, False when None @@ -28,9 +27,6 @@ from ad_hoc_diffractometer import ReferenceConstraint from ad_hoc_diffractometer import ub_identity from ad_hoc_diffractometer.reference import emergence_angle -from ad_hoc_diffractometer.reference import ( # noqa: F401 — deprecation-test import - exit_angle, -) from ad_hoc_diffractometer.reference import incidence_angle from ad_hoc_diffractometer.reference import natural_psi from ad_hoc_diffractometer.reference import naz_angle @@ -107,7 +103,7 @@ def test_emergence_angle_with_surface_normal(): def test_specular_condition_alpha_i_equals_alpha_f(): - """At bisecting with surface normal ⊥ to scattering plane, alpha_i ≈ alpha_f.""" + """At bisecting with surface normal ⊥ to scattering plane, incidence ≈ alpha_f.""" g = _setup_psic() # Surface normal along transverse axis — perpendicular to the scattering plane g.surface_normal = (0, 0, 1) @@ -232,14 +228,16 @@ def test_naz_angle_vertical_normal_returns_zero(): [ # surface-normal constraints: implemented when surface_normal is set pytest.param( - "alpha_i", 0.0, "surface_normal", (0, 0, 1), True, id="alpha_i-with-sn" + "incidence", 0.0, "surface_normal", (0, 0, 1), True, id="incidence-with-sn" + ), + pytest.param( + "incidence", 0.0, "surface_normal", None, False, id="incidence-no-sn" ), - pytest.param("alpha_i", 0.0, "surface_normal", None, False, id="alpha_i-no-sn"), pytest.param( - "beta_out", 0.0, "surface_normal", (0, 0, 1), True, id="beta_out-with-sn" + "emergence", 0.0, "surface_normal", (0, 0, 1), True, id="emergence-with-sn" ), pytest.param( - "beta_out", 0.0, "surface_normal", None, False, id="beta_out-no-sn" + "emergence", 0.0, "surface_normal", None, False, id="emergence-no-sn" ), pytest.param( "specular", True, "surface_normal", (0, 0, 1), True, id="specular-with-sn" @@ -288,15 +286,17 @@ def test_reference_constraint_is_implemented( @pytest.mark.parametrize( "name, value, ref_attr, ref_value, expected", [ - pytest.param("alpha_i", 0.0, "surface_normal", None, False, id="alpha_i-no-sn"), pytest.param( - "alpha_i", 0.0, "surface_normal", (0, 0, 1), True, id="alpha_i-with-sn" + "incidence", 0.0, "surface_normal", None, False, id="incidence-no-sn" ), pytest.param( - "beta_out", 0.0, "surface_normal", None, False, id="beta_out-no-sn" + "incidence", 0.0, "surface_normal", (0, 0, 1), True, id="incidence-with-sn" ), pytest.param( - "beta_out", 0.0, "surface_normal", (0, 0, 1), True, id="beta_out-with-sn" + "emergence", 0.0, "surface_normal", None, False, id="emergence-no-sn" + ), + pytest.param( + "emergence", 0.0, "surface_normal", (0, 0, 1), True, id="emergence-with-sn" ), pytest.param( "specular", True, "surface_normal", None, False, id="specular-no-sn" @@ -485,19 +485,19 @@ def test_surface_mode_returns_solutions(factory, mode_name, h, k, l): # noqa: E @pytest.mark.parametrize( "factory, mode_name, h, k, l", [ - pytest.param(zaxis, "zaxis", 0, 1, 0, id="zaxis-zaxis-alpha_i=0"), + pytest.param(zaxis, "zaxis", 0, 1, 0, id="zaxis-zaxis-incidence=0"), pytest.param( sixc, "fixed_incidence_zaxis", 0, 1, 0, - id="sixc-fixed_alpha-alpha_i=0", + id="sixc-fixed_alpha-incidence=0", ), ], ) def test_surface_alpha_i_fixed_constraint_satisfied(factory, mode_name, h, k, l): # noqa: E741 - """alpha_i modes: incidence angle equals declared target (0°) in all solutions.""" + """incidence modes: incidence angle equals declared target (0°) in all solutions.""" g = _setup_surface(factory) g.mode_name = mode_name solutions = g.forward(h, k, l) @@ -516,12 +516,12 @@ def test_surface_alpha_i_fixed_constraint_satisfied(factory, mode_name, h, k, l) 0, 1, 0, - id="sixc-fixed_beta-beta_out=0", + id="sixc-fixed_beta-emergence=0", ), ], ) def test_surface_beta_out_fixed_constraint_satisfied(factory, mode_name, h, k, l): # noqa: E741 - """beta_out modes: exit angle equals declared target (0°) in all solutions.""" + """emergence modes: exit angle equals declared target (0°) in all solutions.""" g = _setup_surface(factory) g.mode_name = mode_name solutions = g.forward(h, k, l) @@ -536,11 +536,11 @@ def test_surface_beta_out_fixed_constraint_satisfied(factory, mode_name, h, k, l [ pytest.param(zaxis, "reflectivity", 0, 0, 1, id="zaxis-reflectivity"), pytest.param(s2d2, "reflectivity", 0, 1, 0, id="s2d2-reflectivity"), - pytest.param(sixc, "specular_zaxis", 0, 1, 0, id="sixc-alpha_eq_beta"), + pytest.param(sixc, "specular_zaxis", 0, 1, 0, id="sixc-specular"), ], ) def test_surface_a_eq_b_constraint_satisfied(factory, mode_name, h, k, l): # noqa: E741 - """specular modes: alpha_i ≈ beta_out in all solutions.""" + """specular modes: incidence ≈ emergence in all solutions.""" g = _setup_surface(factory) g.mode_name = mode_name solutions = g.forward(h, k, l) @@ -677,8 +677,8 @@ def test_omega_pseudo_independent_of_chi(): "name, expected", [ pytest.param("psi", True, id="psi"), - pytest.param("alpha_i", True, id="alpha_i"), - pytest.param("beta_out", True, id="beta_out"), + pytest.param("incidence", True, id="incidence"), + pytest.param("emergence", True, id="emergence"), pytest.param("specular", True, id="specular"), pytest.param("naz", True, id="naz"), pytest.param("omega", True, id="omega"), @@ -816,24 +816,3 @@ def test_natural_psi_independent_of_motor_state(): for stage in g._stages.values(): # noqa: SLF001 stage.angle = 17.5 assert natural_psi(g, 1, 1, 0) == pytest.approx(baseline, abs=1e-12) - - -# REMOVE-AT-V1.0: deprecation-cycle coverage for the legacy long-form -# wrapper ``reference.exit_angle``. - - -def test_exit_angle_function_deprecated(): - """exit_angle() emits DeprecationWarning and delegates to emergence_angle.""" - g = _setup_psic() - g.surface_normal = (0, 0, 1) - g.mode_name = "bisecting_vertical" - sols = g.forward(1, 0, 0) - assert sols, "fixture should return at least one solution" - with pytest.warns( - DeprecationWarning, - match=r"reference\.exit_angle\(\) is deprecated; " - r"use ad_hoc_diffractometer\.reference\.emergence_angle", - ): - value = exit_angle(g, angles=sols[0]) - expected = emergence_angle(g, angles=sols[0]) - assert value == pytest.approx(expected, abs=1e-12) diff --git a/tests/test_regression_issue_264.py b/tests/test_regression_issue_264.py index da8b31d8..74d3e798 100644 --- a/tests/test_regression_issue_264.py +++ b/tests/test_regression_issue_264.py @@ -306,7 +306,7 @@ def test_fixed_alpha_i_fixed_chi_fixed_phi_routes_to_free_detectors(): is set (otherwise the mode is a stub).""" g = _setup_psic_cubic() cs = g.modes["fixed_incidence_fixed_chi_fixed_phi"] - # Without surface_normal, the alpha_i ReferenceConstraint reports + # Without surface_normal, the incidence ReferenceConstraint reports # is_implemented=False, so the mode is a stub regardless of dispatch. assert cs.is_implemented(g) is False g.surface_normal = (0, 0, 1) @@ -555,7 +555,7 @@ def test_omega_constraint_unimplemented_without_chi_stage(): # --------------------------------------------------------------------------- -# Issue #264 design invariant: B3 produces solutions whose alpha_i matches +# Issue #264 design invariant: B3 produces solutions whose incidence matches # the requested target. # --------------------------------------------------------------------------- @@ -566,16 +566,16 @@ def test_omega_constraint_unimplemented_without_chi_stage(): ) def test_b3_alpha_i_target_satisfied(alpha_target): """B3 ``fixed_incidence_fixed_chi_fixed_phi`` solutions satisfy - alpha_i == target within tolerance.""" + incidence == target within tolerance.""" g = _setup_psic_cubic() g.surface_normal = (0, 0, 1) cs = g.modes["fixed_incidence_fixed_chi_fixed_phi"] - # Override alpha_i target with the parametrized value. + # Override incidence target with the parametrized value. g.modes["__b3_test"] = ConstraintSet( [ SampleConstraint("chi", 0.0), SampleConstraint("phi", 0.0), - ReferenceConstraint("alpha_i", alpha_target), + ReferenceConstraint("incidence", alpha_target), ], computed=cs.computed, extras=dict(cs.extras), @@ -586,5 +586,5 @@ def test_b3_alpha_i_target_satisfied(alpha_target): for sol in sols: ai = incidence_angle(g, angles=sol) assert ai == pytest.approx(alpha_target, abs=1e-3), ( - f"B3 alpha_i target {alpha_target}: got {ai}" + f"B3 incidence target {alpha_target}: got {ai}" ) diff --git a/tests/test_regression_issue_279.py b/tests/test_regression_issue_279.py index 3657fe1f..fe02825d 100644 --- a/tests/test_regression_issue_279.py +++ b/tests/test_regression_issue_279.py @@ -278,7 +278,7 @@ def test_solve_surface_returns_empty_when_only_detector_stage_pinned(): constraints=[ SampleConstraint("omega", 0.0), DetectorConstraint(det_name, 0.0), - ReferenceConstraint("alpha_i", 0.0), + ReferenceConstraint("incidence", 0.0), ], ) diff --git a/tests/test_regression_issue_292.py b/tests/test_regression_issue_292.py index 715d6fd6..17011d1d 100644 --- a/tests/test_regression_issue_292.py +++ b/tests/test_regression_issue_292.py @@ -3,9 +3,9 @@ """ Regression tests for issue #292. -Issue #292 noted that the ``fixed_alpha_i_*``, ``fixed_beta_out_*``, -``alpha_eq_beta_*`` modes (psic, sixc, zaxis, s2d2) — and by extension -every mode declaring an ``alpha_i``, ``beta_out``, ``psi``, or ``omega`` +Issue #292 noted that the ``fixed_incidence_*``, ``fixed_emergence_*``, +``specular_*`` modes (psic, sixc, zaxis, s2d2) — and by extension every +mode declaring an ``incidence``, ``emergence``, ``psi``, or ``omega`` output slot in ``mode.extras`` — left those slots at their YAML default of ``None`` even after a successful ``forward()`` call. This was a latent contract bug: the slots advertised an output the solver never @@ -16,10 +16,10 @@ slot is replaced by a Python list of per-solution float values computed via the corresponding helper in :mod:`ad_hoc_diffractometer.reference`: -* ``alpha_i`` → :func:`~ad_hoc_diffractometer.reference.incidence_angle` -* ``beta_out`` → :func:`~ad_hoc_diffractometer.reference.exit_angle` -* ``psi`` → :func:`~ad_hoc_diffractometer.reference.psi_angle` -* ``omega`` → :func:`~ad_hoc_diffractometer.reference.omega_pseudo` +* ``incidence`` → :func:`~ad_hoc_diffractometer.reference.incidence_angle` +* ``emergence`` → :func:`~ad_hoc_diffractometer.reference.emergence_angle` +* ``psi`` → :func:`~ad_hoc_diffractometer.reference.psi_angle` +* ``omega`` → :func:`~ad_hoc_diffractometer.reference.omega_pseudo` Empty solution lists reset the slot to ``[]``; a missing reference vector leaves the slot at ``None`` (with a debug-level log message). @@ -55,33 +55,32 @@ def _setup_cubic(name, a=4.0): # --------------------------------------------------------------------------- -# B3 surface mode: psic / fixed_incidence_fixed_chi_fixed_phi -# (the only fixed_alpha_i_* mode that is currently implemented for psic). +# B3 surface mode: psic / fixed_incidence_fixed_chi_fixed_phi. # --------------------------------------------------------------------------- @pytest.mark.parametrize( - "h, k, l, alpha_target, context", + "h, k, l, incidence_target, context", [ - pytest.param(0, 1, 1, 0.0, does_not_raise(), id="011-ai0"), - pytest.param(0, 1, 1, 5.0, does_not_raise(), id="011-ai5"), - pytest.param(1, 1, 1, 3.0, does_not_raise(), id="111-ai3"), + pytest.param(0, 1, 1, 0.0, does_not_raise(), id="011-i0"), + pytest.param(0, 1, 1, 5.0, does_not_raise(), id="011-i5"), + pytest.param(1, 1, 1, 3.0, does_not_raise(), id="111-i3"), ], ) -def test_psic_b3_populates_alpha_i_and_beta_out_extras( +def test_psic_b3_populates_incidence_and_emergence_extras( h, k, l, # noqa: E741 - alpha_target, + incidence_target, context, ): - """B3 mode populates ``extras['alpha_i']`` and ``extras['beta_out']``.""" + """B3 mode populates ``extras['incidence']`` and ``extras['emergence']``.""" with context: g = _setup_cubic("psic") g.surface_normal = (0, 0, 1) cs = g.modes["fixed_incidence_fixed_chi_fixed_phi"] # Update the reference-constraint target via the public API: - # rebuild the constraint set with the requested alpha_i value + # rebuild the constraint set with the requested incidence value # (the YAML defaults to 0.0; we want non-zero targets too). from ad_hoc_diffractometer import REQUIRED from ad_hoc_diffractometer import ConstraintSet @@ -92,10 +91,10 @@ def test_psic_b3_populates_alpha_i_and_beta_out_extras( [ SampleConstraint("chi", 0.0), SampleConstraint("phi", 0.0), - ReferenceConstraint("alpha_i", alpha_target), + ReferenceConstraint("incidence", incidence_target), ], computed=cs.computed, - extras={"n_hat": REQUIRED, "alpha_i": None, "beta_out": None}, + extras={"n_hat": REQUIRED, "incidence": None, "emergence": None}, ) g.mode_name = "fixed_incidence_fixed_chi_fixed_phi" @@ -103,19 +102,19 @@ def test_psic_b3_populates_alpha_i_and_beta_out_extras( assert len(sols) > 0 mode = g.modes["fixed_incidence_fixed_chi_fixed_phi"] - assert isinstance(mode.extras["alpha_i"], list) - assert isinstance(mode.extras["beta_out"], list) - assert len(mode.extras["alpha_i"]) == len(sols) - assert len(mode.extras["beta_out"]) == len(sols) + assert isinstance(mode.extras["incidence"], list) + assert isinstance(mode.extras["emergence"], list) + assert len(mode.extras["incidence"]) == len(sols) + assert len(mode.extras["emergence"]) == len(sols) - # Each populated alpha_i value matches the per-solution recomputation + # Each populated incidence value matches the per-solution recomputation # and (per the mode's constraint) equals the target. for ai_stored, bo_stored, sol in zip( - mode.extras["alpha_i"], mode.extras["beta_out"], sols, strict=True + mode.extras["incidence"], mode.extras["emergence"], sols, strict=True ): assert ai_stored == pytest.approx(incidence_angle(g, angles=sol), abs=1e-8) assert bo_stored == pytest.approx(emergence_angle(g, angles=sol), abs=1e-8) - assert ai_stored == pytest.approx(alpha_target, abs=1e-3) + assert ai_stored == pytest.approx(incidence_target, abs=1e-3) # --------------------------------------------------------------------------- @@ -287,7 +286,7 @@ def test_modes_without_output_slots_are_untouched(): sols = g.forward(0, 1, 1) assert len(sols) > 0 extras = g.modes["bisecting_vertical"].extras - for key in ("alpha_i", "beta_out", "psi", "omega"): + for key in ("incidence", "emergence", "psi", "omega"): assert key not in extras, ( f"populate hook must not add {key!r} to a mode whose YAML did not " f"declare it; bisecting_vertical extras: {sorted(extras)}" @@ -331,65 +330,3 @@ def _explode(*_args, **_kwargs): "leaving mode.extras['omega'] as None" in rec.getMessage() for rec in caplog.records ), "expected a debug log record naming the soft-failed slot" - - -# --------------------------------------------------------------------------- -# REMOVE-AT-V1.0: deprecation back-fill for issue #299 alias keys. -# --------------------------------------------------------------------------- - - -def test_populate_extras_back_fills_legacy_alias_slots(): - """Canonical-slot population back-fills the legacy alias slots. - - Issue #299 renamed the surface-frame output slots ``"alpha_i"`` / - ``"beta_out"`` to ``"incidence"`` / ``"emergence"``. An in-tree - YAML mode after migration declares only the canonical slot, but - out-of-tree callers that read ``result.extras["alpha_i"]`` in - v0.11.x and earlier must continue to see the populated value - through the deprecation cycle. ``_populate_output_extras`` writes - the same per-solution list into the legacy slot whenever the - canonical slot is declared, so the legacy read path keeps working - even after the YAML is migrated. - - Hand-build a mode declaring only the canonical slots so this test - exercises the back-fill in isolation (the in-tree YAML still - declares legacy slots at the Step 1+2 boundary, and the legacy - slot is populated by the main computer-dict loop in that case). - """ - from ad_hoc_diffractometer.mode import REQUIRED - from ad_hoc_diffractometer.mode import ConstraintSet - from ad_hoc_diffractometer.mode import ReferenceConstraint - from ad_hoc_diffractometer.mode import SampleConstraint - - g = _setup_cubic("psic") - # Build a B3-style ConstraintSet that declares only the canonical - # extras slots — no ``alpha_i`` / ``beta_out`` keys. The back-fill - # is the only path that can put values into those legacy slots. - g.modes["__back_fill_probe"] = ConstraintSet( - [ - SampleConstraint("chi", 0.0), - SampleConstraint("phi", 0.0), - ReferenceConstraint("incidence", 0.0), - ], - computed=["mu", "eta", "nu", "delta"], - extras={ - "n_hat": REQUIRED, - "incidence": None, - "emergence": None, - }, - cut_points={"eta": -180.0}, - ) - g.surface_normal = (0, 0, 1) - g.mode_name = "__back_fill_probe" - sols = g.forward(0, 1, 1) - assert len(sols) > 0 - - extras = g.modes["__back_fill_probe"].extras - # Canonical slots populated as before. - assert isinstance(extras["incidence"], list) - assert isinstance(extras["emergence"], list) - assert len(extras["incidence"]) == len(sols) - # Legacy slots back-filled with the same lists (identity, not just - # equality — the back-fill aliases the canonical list object). - assert extras["alpha_i"] is extras["incidence"] - assert extras["beta_out"] is extras["emergence"] diff --git a/tests/test_surface.py b/tests/test_surface.py index 7c2d7b0e..bb7e4bb7 100644 --- a/tests/test_surface.py +++ b/tests/test_surface.py @@ -8,9 +8,9 @@ - surface_normal fallback to azimuth - _surface_vectors precondition errors (no wavelength, no UB, no normal, unknown stage name) - - alpha_i: matches motor angle for canonical geometries (s2d2, zaxis) - - alpha_f: matches out-of-plane detector angle (s2d2, zaxis) - - alpha_i and alpha_f are symmetric: same at ai=af angles + - incidence: matches motor angle for canonical geometries (s2d2, zaxis) + - emergence: matches out-of-plane detector angle (s2d2, zaxis) + - incidence and emergence are symmetric: same at ai=af angles - q_components: Q_perp, Q_par, Q_perp_signed, Q_total - Q_perp ≥ 0, Q_par ≥ 0 always - Q_total = sqrt(Q_perp^2 + Q_par^2) @@ -18,9 +18,9 @@ - is_specular: True when ai ≈ af, False otherwise - is_evanescent: True when ai < crit, False when ai ≥ crit, error when crit=None - Serialisation round-trip: surface_normal in to_dict/from_dict - - Standalone functions (alpha_i, alpha_f, q_components, is_specular, + - Standalone functions (incidence, emergence, q_components, is_specular, is_evanescent) imported from top-level - - geometry.alpha_i/alpha_f/q_components/is_specular/is_evanescent methods + - geometry.incidence/emergence/q_components/is_specular/is_evanescent methods - Default angles (None) uses current stage angles - psic surface: surface_normal set, UB=identity, verify ai/af/psi combination """ @@ -37,12 +37,6 @@ import ad_hoc_diffractometer as ahd from ad_hoc_diffractometer import ub_identity from ad_hoc_diffractometer.surface import _surface_vectors -from ad_hoc_diffractometer.surface import ( # noqa: F401 — deprecation-test imports - alpha_f, -) -from ad_hoc_diffractometer.surface import ( # noqa: F401 — deprecation-test imports - alpha_i, -) from ad_hoc_diffractometer.surface import emergence from ad_hoc_diffractometer.surface import incidence from ad_hoc_diffractometer.surface import is_evanescent @@ -237,7 +231,7 @@ def test_surface_normal_fallback_to_azimuth(): # --------------------------------------------------------------------------- -# alpha_i — s2d2 canonical: alpha_i = mu (surface_normal along +z, a=1) +# incidence — s2d2 canonical: incidence = mu (surface_normal along +z, a=1) # --------------------------------------------------------------------------- @@ -252,8 +246,8 @@ def test_surface_normal_fallback_to_azimuth(): pytest.param(90.0, 90.0, does_not_raise(), id="mu=90"), ], ) -def test_alpha_i_s2d2_equals_mu(mu, expected_ai, context): - """In s2d2 with surface_normal=(0,0,1), alpha_i = mu exactly.""" +def test_incidence_s2d2_equals_mu(mu, expected_ai, context): + """In s2d2 with surface_normal=(0,0,1), incidence = mu exactly.""" g = _make_s2d2() with context: ai = g.incidence({"mu": mu, "Z": 0.0, "nu": 0.0, "delta": 0.0}) @@ -261,7 +255,7 @@ def test_alpha_i_s2d2_equals_mu(mu, expected_ai, context): # --------------------------------------------------------------------------- -# alpha_i — zaxis canonical: alpha_i = alpha (surface_normal along +z) +# incidence — zaxis canonical: incidence = alpha (surface_normal along +z) # --------------------------------------------------------------------------- @@ -274,8 +268,8 @@ def test_alpha_i_s2d2_equals_mu(mu, expected_ai, context): pytest.param(20.0, 20.0, does_not_raise(), id="alpha=20"), ], ) -def test_alpha_i_zaxis_equals_alpha(alpha_val, expected_ai, context): - """In zaxis with surface_normal=(0,0,1), alpha_i = alpha exactly.""" +def test_incidence_zaxis_equals_alpha(alpha_val, expected_ai, context): + """In zaxis with surface_normal=(0,0,1), incidence = alpha exactly.""" g = _make_zaxis() with context: ai = g.incidence({"alpha": alpha_val, "Z": 0.0, "delta": 10.0, "gamma": 0.0}) @@ -283,7 +277,7 @@ def test_alpha_i_zaxis_equals_alpha(alpha_val, expected_ai, context): # --------------------------------------------------------------------------- -# alpha_f — s2d2: alpha_f = nu when delta = 0 +# emergence — s2d2: emergence = nu when delta = 0 # --------------------------------------------------------------------------- @@ -296,16 +290,16 @@ def test_alpha_i_zaxis_equals_alpha(alpha_val, expected_ai, context): pytest.param(20.0, 20.0, does_not_raise(), id="nu=20"), ], ) -def test_alpha_f_s2d2_equals_nu_at_delta_zero(nu, expected_af, context): - """In s2d2 with delta=0 and surface_normal=(0,0,1), alpha_f = nu exactly.""" +def test_emergence_s2d2_equals_nu_at_delta_zero(nu, expected_af, context): + """In s2d2 with delta=0 and surface_normal=(0,0,1), emergence = nu exactly.""" g = _make_s2d2() with context: af = g.emergence({"mu": 0.0, "Z": 0.0, "nu": nu, "delta": 0.0}) assert af == pytest.approx(expected_af, abs=1e-6) -def test_alpha_f_s2d2_zero_at_any_in_plane_delta(): - """In s2d2 with nu=0, alpha_f = 0 for any delta (in-plane only).""" +def test_emergence_s2d2_zero_at_any_in_plane_delta(): + """In s2d2 with nu=0, emergence = 0 for any delta (in-plane only).""" g = _make_s2d2() for delta in [0.0, 10.0, 20.0, 45.0]: af = g.emergence({"mu": 0.0, "Z": 0.0, "nu": 0.0, "delta": delta}) @@ -313,7 +307,7 @@ def test_alpha_f_s2d2_zero_at_any_in_plane_delta(): # --------------------------------------------------------------------------- -# alpha_f — zaxis: matches geometric formula +# emergence — zaxis: matches geometric formula # --------------------------------------------------------------------------- @@ -335,7 +329,7 @@ def test_alpha_f_s2d2_zero_at_any_in_plane_delta(): # With the surface normal along +transverse (= +z in YOU # basis, set by ``surface_normal = (0, 0, 1)``), the # out-of-plane projection is simply ``sin γ`` — i.e. - # ``alpha_f = γ`` independent of delta. The earlier formula + # ``emergence = γ`` independent of delta. The earlier formula # ``arcsin(cos δ · sin γ)`` was a baked-in expectation under # the pre-#280 (inner-leftmost) composition where # ``D = R(gamma) @ R(delta)``. @@ -355,8 +349,8 @@ def test_alpha_f_s2d2_zero_at_any_in_plane_delta(): ), ], ) -def test_alpha_f_zaxis_formula(delta, gamma, expected_af, context): - """alpha_f for zaxis under the standard BL1967 detector composition.""" +def test_emergence_zaxis_formula(delta, gamma, expected_af, context): + """emergence for zaxis under the standard BL1967 detector composition.""" g = _make_zaxis() with context: af = g.emergence({"alpha": 0.0, "Z": 0.0, "delta": delta, "gamma": gamma}) @@ -364,20 +358,20 @@ def test_alpha_f_zaxis_formula(delta, gamma, expected_af, context): # --------------------------------------------------------------------------- -# alpha_i always in [0°, 90°] +# incidence always in [0°, 90°] # --------------------------------------------------------------------------- -def test_alpha_i_always_nonnegative(): - """alpha_i is always in [0°, 90°] regardless of sign of mu.""" +def test_incidence_always_nonnegative(): + """incidence is always in [0°, 90°] regardless of sign of mu.""" g = _make_s2d2() for mu in [-20.0, -10.0, -5.0, 0.0, 5.0, 10.0, 20.0]: ai = g.incidence({"mu": mu, "Z": 0.0, "nu": 0.0, "delta": 0.0}) assert 0.0 <= ai <= 90.0 -def test_alpha_f_always_nonnegative(): - """alpha_f is always in [0°, 90°].""" +def test_emergence_always_nonnegative(): + """emergence is always in [0°, 90°].""" g = _make_s2d2() for nu in [-20.0, -10.0, 0.0, 10.0, 20.0]: af = g.emergence({"mu": 0.0, "Z": 0.0, "nu": nu, "delta": 0.0}) @@ -459,8 +453,8 @@ def test_is_specular_true_when_ai_equals_af(): is_specular returns True when ai = af. In s2d2 with surface_normal=(0,0,1) and a=1, the specular condition is: - - alpha_i = mu (verified separately) - - alpha_f = |nu - mu| when delta=0 (rotation of surface normal and detector + - incidence = mu (verified separately) + - emergence = |nu - mu| when delta=0 (rotation of surface normal and detector both about the same +x axis means their relative angle is nu - mu) Therefore specular (ai = af) requires nu = 2*mu. """ @@ -533,8 +527,8 @@ def test_is_evanescent_raises_without_critical_angle(): # --------------------------------------------------------------------------- -def test_psic_alpha_i_from_mu(): - """In psic with surface_normal=(0,0,1), alpha_i = mu (rotation about +x).""" +def test_psic_incidence_from_mu(): + """In psic with surface_normal=(0,0,1), incidence = mu (rotation about +x).""" g = _make_psic() for mu in [0.0, 2.0, 5.0, 10.0]: ai = g.incidence( @@ -543,8 +537,8 @@ def test_psic_alpha_i_from_mu(): assert ai == pytest.approx(mu, abs=1e-6) -def test_psic_alpha_f_from_nu(): - """In psic with surface_normal=(0,0,1), alpha_f = nu when delta=0.""" +def test_psic_emergence_from_nu(): + """In psic with surface_normal=(0,0,1), emergence = nu when delta=0.""" g = _make_psic() for nu in [0.0, 2.0, 5.0, 10.0]: af = g.emergence( @@ -584,53 +578,6 @@ def test_standalone_emergence(): assert emergence(g, angles) == pytest.approx(5.0, abs=1e-6) -# REMOVE-AT-V1.0: deprecation-cycle coverage for the legacy method -# names ``alpha_i`` / ``alpha_f`` on AdHocDiffractometer, and the -# legacy standalone functions ``surface.alpha_i`` / ``surface.alpha_f``. - - -def test_geometry_alpha_i_method_deprecated(): - g = _make_s2d2() - with pytest.warns( - DeprecationWarning, - match=r"AdHocDiffractometer\.alpha_i\(\) is deprecated; use \.incidence", - ): - ai = g.alpha_i({"mu": 5.0, "Z": 0.0, "nu": 0.0, "delta": 0.0}) - assert ai == pytest.approx(5.0, abs=1e-6) - - -def test_geometry_alpha_f_method_deprecated(): - g = _make_s2d2() - with pytest.warns( - DeprecationWarning, - match=r"AdHocDiffractometer\.alpha_f\(\) is deprecated; use \.emergence", - ): - af = g.alpha_f({"mu": 0.0, "Z": 0.0, "nu": 5.0, "delta": 0.0}) - assert af == pytest.approx(5.0, abs=1e-6) - - -def test_surface_alpha_i_function_deprecated(): - g = _make_s2d2() - angles = {"mu": 5.0, "Z": 0.0, "nu": 0.0, "delta": 0.0} - with pytest.warns( - DeprecationWarning, - match=r"surface\.alpha_i\(\) is deprecated; use ad_hoc_diffractometer\.surface\.incidence", - ): - ai = alpha_i(g, angles) - assert ai == pytest.approx(5.0, abs=1e-6) - - -def test_surface_alpha_f_function_deprecated(): - g = _make_s2d2() - angles = {"mu": 0.0, "Z": 0.0, "nu": 5.0, "delta": 0.0} - with pytest.warns( - DeprecationWarning, - match=r"surface\.alpha_f\(\) is deprecated; use ad_hoc_diffractometer\.surface\.emergence", - ): - af = alpha_f(g, angles) - assert af == pytest.approx(5.0, abs=1e-6) - - def test_standalone_q_components(): g = _make_s2d2() angles = {"mu": 5.0, "Z": 0.0, "nu": 5.0, "delta": 10.0} From 3e3c23f78ce381cc8dfeefe47a0e43536d599d19 Mon Sep 17 00:00:00 2001 From: Pete Jemian Date: Thu, 18 Jun 2026 22:09:11 -0500 Subject: [PATCH 7/7] docs(#299) update CHANGES.md unreleased section Replace the commented-out PR #298 unreleased block (which described the now-deleted azimuthal_reference deprecation cycle) with a live unreleased section listing the actual renames landed since v0.11.1: constraints (incidence, emergence, specular), modes (fixed_incidence_*, fixed_emergence_*, specular_*), Python methods (incidence, emergence), the long-form wrapper (emergence_angle), and azimuth (#298). Adds one bullet recording the pre-v1.0 no-deprecation policy adoption. Sorted alphabetically per the AGENTS.md changelog rule. Contributed by: OpenCode (argo/claudeopus47) --- CHANGES.md | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 68522094..5c4216a0 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -5,24 +5,21 @@ Issues](https://github.com/BCDA-APS/ad_hoc_diffractometer/issues) for the full issue tracker. The initial project development roadmap is documented here: [roadmap](https://github.com/BCDA-APS/ad_hoc_diffractometer/blob/main/roadmap.md). - ## Release v0.11.1