From fe43c35849016e60440dc5d959328438e8f3f370 Mon Sep 17 00:00:00 2001 From: Pete Jemian Date: Mon, 15 Jun 2026 18:12:27 -0500 Subject: [PATCH 1/3] refactor(#298) rename azimuthal_reference -> azimuth (deprecate alias) Add 'azimuth' as the canonical property name and constructor kwarg on AdHocDiffractometer. The old 'azimuthal_reference' name is retained as a forwarding alias that emits DeprecationWarning on every read, write, and constructor-kwarg use; the deprecation message names v0.12.0 and points callers at 'azimuth'. Internal storage is renamed _azimuthal_reference -> _azimuth. All in-tree call sites in src/ (reference.py, forward.py, surface.py, mode.py, benchmark.py) and their docstrings, error messages, and log text migrate to the new name; the internal helper _require_azimuthal_reference is renamed _require_azimuth. to_dict() writes the new key 'azimuth'. from_dict() accepts either 'azimuth' (new) or 'azimuthal_reference' (legacy) so sessions saved by ad_hoc_diffractometer <= v0.11.x keep loading unchanged. required_reference_vector now returns 'azimuth' instead of 'azimuthal_reference' for psi / naz reference constraints. This is the one strict behaviour change in the PR. Test suite: 118 references across nine files bulk-renamed. A new TestAzimuthDeprecation block in tests/test_diffractometer.py adds seven tests covering deprecation warnings on the property getter, setter, and constructor kwarg; alias-shares-storage; both-kwargs disagreement raises; to_dict emits the new key; from_dict reads the legacy key. Contributed by: OpenCode (argo/claudeopus47) --- CHANGES.md | 13 ++ src/ad_hoc_diffractometer/benchmark.py | 8 +- src/ad_hoc_diffractometer/diffractometer.py | 127 ++++++++---- src/ad_hoc_diffractometer/forward.py | 12 +- src/ad_hoc_diffractometer/mode.py | 14 +- src/ad_hoc_diffractometer/reference.py | 30 +-- src/ad_hoc_diffractometer/rotation.py | 2 +- src/ad_hoc_diffractometer/stage.py | 2 +- src/ad_hoc_diffractometer/surface.py | 14 +- tests/test_benchmark.py | 6 +- tests/test_diffractometer.py | 215 +++++++++++++++----- tests/test_forward.py | 4 +- tests/test_mode.py | 2 +- tests/test_reference.py | 82 ++++---- tests/test_regression_issue_264.py | 4 +- tests/test_regression_issue_278.py | 6 +- tests/test_regression_issue_292.py | 4 +- tests/test_surface.py | 12 +- 18 files changed, 362 insertions(+), 195 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 0bbfc5d9..68522094 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -9,6 +9,19 @@ issue tracker. The initial project development roadmap is documented here: ## Unreleased Note any unreleased items inside the comment here. Not visible until release. + +### Added + +- `azimuth` property and constructor kwarg as canonical name. (#298) + +### Changed + +- `required_reference_vector` returns `"azimuth"` instead of `"azimuthal_reference"`. (#298) + +### Deprecated + +- `azimuthal_reference` property and kwarg; use `azimuth` instead. (#298) + --> ## Release v0.11.1 diff --git a/src/ad_hoc_diffractometer/benchmark.py b/src/ad_hoc_diffractometer/benchmark.py index 00e7673a..cc2824af 100644 --- a/src/ad_hoc_diffractometer/benchmark.py +++ b/src/ad_hoc_diffractometer/benchmark.py @@ -97,7 +97,7 @@ def _prepare_mode(geometry, mode_name: str) -> None: Sets ``mode_name`` on the geometry and provides the minimum setup required for modes that need extra configuration: - - fixed_psi modes: sets ``azimuthal_reference`` + - 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): @@ -117,11 +117,11 @@ def _prepare_mode(geometry, mode_name: str) -> None: if key in cs.extras and cs.extras[key] is REQUIRED: cs.extras[key] = default - # Reference-vector modes: set azimuthal_reference if needed + # Reference-vector modes: set azimuth if needed for c in cs._constraints: cname = getattr(c, "_name", getattr(c, "name", "")) - if cname == "psi" and geometry.azimuthal_reference is None: - geometry.azimuthal_reference = (0, 0, 1) + if cname == "psi" and geometry.azimuth is None: + geometry.azimuth = (0, 0, 1) # Surface modes: set surface_normal if needed for c in cs._constraints: diff --git a/src/ad_hoc_diffractometer/diffractometer.py b/src/ad_hoc_diffractometer/diffractometer.py index 1711ca9a..68a63076 100644 --- a/src/ad_hoc_diffractometer/diffractometer.py +++ b/src/ad_hoc_diffractometer/diffractometer.py @@ -9,6 +9,7 @@ import builtins import logging +import warnings import numpy as np @@ -69,10 +70,14 @@ class AdHocDiffractometer: kappa4ch, kappa6c). None for non-kappa geometries. Set by the kappa demo geometries; not intended to be changed after construction. - azimuthal_reference : tuple of float or None, optional + azimuth : tuple of float or None, optional 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` @@ -104,6 +109,7 @@ def __init__( wavelength: float | None = None, 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, @@ -116,7 +122,26 @@ 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 - self.azimuthal_reference = azimuthal_reference # validated via property setter + # 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 self._detector_tilt: float | None = None @@ -535,7 +560,7 @@ def kappa_pseudo_angle_convention(self, value) -> None: self._kappa_pseudo_angle_convention = value @property - def azimuthal_reference(self) -> tuple[float, float, float] | None: + def azimuth(self) -> tuple[float, float, float] | None: """ Azimuthal reference vector as Miller indices (h, k, l), or ``None``. @@ -565,30 +590,62 @@ def azimuthal_reference(self) -> tuple[float, float, float] | None: Examples -------- >>> g = psic() - >>> g.azimuthal_reference = (0, 0, 1) # c-axis surface normal - >>> g.azimuthal_reference + >>> g.azimuth = (0, 0, 1) # c-axis surface normal + >>> g.azimuth (0.0, 0.0, 1.0) - >>> g.azimuthal_reference = None # clear reference + >>> g.azimuth = None # clear reference """ - return self._azimuthal_reference + return self._azimuth - @azimuthal_reference.setter - def azimuthal_reference(self, value: tuple[float, float, float] | None) -> None: + @azimuth.setter + def azimuth(self, value: tuple[float, float, float] | None) -> None: if value is None: - self._azimuthal_reference = None + self._azimuth = None return try: h, k, l = (float(x) for x in value) # noqa: E741 except (TypeError, ValueError) as exc: raise ValueError( - "azimuthal_reference must be a length-3 sequence of numbers " + "azimuth must be a length-3 sequence of numbers " f"or None; got {value!r}." ) from exc if h == 0.0 and k == 0.0 and l == 0.0: raise ValueError( - "azimuthal_reference must be a non-zero vector; (0, 0, 0) is not allowed." + "azimuth must be a non-zero vector; (0, 0, 0) is not allowed." ) - self._azimuthal_reference = (h, k, l) + 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 @@ -1041,7 +1098,7 @@ def psi(self, angles: dict[str, float] | None = None) -> float: ValueError If ``self.sample.UB`` is None. ValueError - If ``self.azimuthal_reference`` is None (no reference set). + If ``self.azimuth`` is None (no reference set). ValueError If the reference vector is parallel to Q (ψ is undefined). @@ -1061,7 +1118,7 @@ def psi(self, angles: dict[str, float] | None = None) -> float: >>> import ad_hoc_diffractometer as ahd >>> g = ahd.fourcv() >>> g.wavelength = 1.5406 - >>> g.azimuthal_reference = (0, 0, 1) + >>> g.azimuth = (0, 0, 1) >>> # ... set UB, move motors ... >>> psi = g.psi() @@ -1078,10 +1135,10 @@ def psi(self, angles: dict[str, float] | None = None) -> float: "psi() requires a UB matrix on the active sample. " "Call ub_identity() or ub_from_two_reflections_bl1967() first." ) - if self.azimuthal_reference is None: + if self.azimuth is None: raise ValueError( - "psi() requires azimuthal_reference to be set on the geometry. " - "Set geometry.azimuthal_reference = (h, k, l) first." + "psi() requires azimuth to be set on the geometry. " + "Set geometry.azimuth = (h, k, l) first." ) if angles is None: @@ -1096,7 +1153,7 @@ def psi(self, angles: dict[str, float] | None = None) -> float: Q_hat = Q_phi / Q_mag # Reference direction in phi frame - n_hkl = np.asarray(self.azimuthal_reference, dtype=float) + n_hkl = np.asarray(self.azimuth, dtype=float) n_phi = self.sample.UB @ n_hkl n_mag = np.linalg.norm(n_phi) if n_mag < 1e-14: @@ -1153,7 +1210,7 @@ def surface_normal(self) -> tuple[float, float, float] | None: :meth:`is_specular`, :meth:`is_evanescent`). When ``None``, the surface calculations fall back to - :attr:`azimuthal_reference` if that is set. + :attr:`azimuth` if that is set. Setting this to ``None`` clears the surface normal. Setting to a non-zero (h, k, l) 3-tuple stores it. @@ -1213,8 +1270,8 @@ def required_reference_vector(self) -> str | None: ``"alpha_i"`` :attr:`surface_normal` ``"beta_out"`` :attr:`surface_normal` ``"a_eq_b"`` :attr:`surface_normal` - ``"psi"`` :attr:`azimuthal_reference` - ``"naz"`` :attr:`azimuthal_reference` + ``"psi"`` :attr:`azimuth` + ``"naz"`` :attr:`azimuth` ``"omega"`` (none) ===================================== ================================= @@ -1224,7 +1281,7 @@ def required_reference_vector(self) -> str | None: Returns ------- str or None - One of ``"surface_normal"``, ``"azimuthal_reference"``, or + One of ``"surface_normal"``, ``"azimuth"``, or ``None``. ``None`` is returned when the active mode either has no :class:`~ad_hoc_diffractometer.mode.ReferenceConstraint`, uses a ReferenceConstraint that needs no vector (``"omega"``), @@ -1240,11 +1297,11 @@ def required_reference_vector(self) -> str | None: 'surface_normal' >>> setattr(g, g.required_reference_vector, (0, 0, 1)) - Azimuthal mode → ``azimuthal_reference``: + Azimuthal mode → ``azimuth``: >>> g.mode_name = "fixed_psi_vertical" >>> g.required_reference_vector - 'azimuthal_reference' + 'azimuth' OMEGA pseudo-angle mode → no vector required: @@ -1255,7 +1312,7 @@ def required_reference_vector(self) -> str | None: See Also -------- surface_normal : The attribute set for surface-type reference constraints. - azimuthal_reference : The attribute set for psi/naz reference constraints. + azimuth : The attribute set for psi/naz reference constraints. """ from .mode import ReferenceConstraint @@ -1268,7 +1325,7 @@ def required_reference_vector(self) -> str | None: if rc.name in {"alpha_i", "beta_out", "a_eq_b"}: return "surface_normal" if rc.name in {"psi", "naz"}: - return "azimuthal_reference" + return "azimuth" # "omega" pseudo-angle requires no reference vector. return None @@ -1394,7 +1451,7 @@ def alpha_i(self, angles: dict[str, float] | None = None) -> float: αᵢ is the angle between the incoming beam and the sample surface. Requires ``wavelength``, ``sample.UB``, and ``surface_normal`` - (or ``azimuthal_reference``) to be set. + (or ``azimuth``) to be set. Parameters ---------- @@ -1429,7 +1486,7 @@ def alpha_f(self, angles: dict[str, float] | None = None) -> float: αf is the angle between the diffracted beam and the sample surface. Requires ``wavelength``, ``sample.UB``, and ``surface_normal`` - (or ``azimuthal_reference``) to be set. + (or ``azimuth``) to be set. Parameters ---------- @@ -1815,7 +1872,7 @@ def _ang(u, v): lines.append("") # Azimuthal reference - az_ref = self.azimuthal_reference + az_ref = self.azimuth if az_ref is not None: h, k, l = az_ref # noqa: E741 lines.append( @@ -2012,7 +2069,7 @@ def to_dict(self) -> dict: Wavelength in Å (float or None). ``"kappa_alpha_deg"`` Kappa tilt angle in degrees (float or None). - ``"azimuthal_reference"`` + ``"azimuth"`` Miller indices [h, k, l] (list of 3 float, or None). ``"basis"`` Dict mapping physical direction names to [x, y, z] lists. @@ -2067,11 +2124,7 @@ def to_dict(self) -> dict: "description": self.description, "wavelength": self._wavelength, "kappa_alpha_deg": self._kappa_alpha_deg, - "azimuthal_reference": ( - list(self._azimuthal_reference) - if self._azimuthal_reference is not None - else None - ), + "azimuth": (list(self._azimuth) if self._azimuth is not None else None), "surface_normal": ( list(self._surface_normal) if self._surface_normal is not None else None ), @@ -2163,7 +2216,7 @@ def from_dict(cls, d: dict) -> "AdHocDiffractometer": description=d.get("description", ""), wavelength=d.get("wavelength"), kappa_alpha_deg=d.get("kappa_alpha_deg"), - azimuthal_reference=d.get("azimuthal_reference"), + azimuth=d.get("azimuth", d.get("azimuthal_reference")), 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 59d044a9..c23837a6 100644 --- a/src/ad_hoc_diffractometer/forward.py +++ b/src/ad_hoc_diffractometer/forward.py @@ -529,7 +529,7 @@ def _populate_output_extras( values.append(float(compute(geometry, angles=angles))) except Exception as exc: # noqa: BLE001 # Underlying call raised (e.g. missing surface_normal / - # azimuthal_reference, or psi undefined when Q ∥ n_ref). + # azimuth, or psi undefined when Q ∥ n_ref). # Leave the slot unpopulated and record the cause for a # single debug log below. We break immediately so a # later good solution does not mask the failure. @@ -1765,7 +1765,7 @@ def _compute_natural_psi( Parameters ---------- geometry : AdHocDiffractometer - Must have ``sample.UB`` and ``azimuthal_reference`` set. + Must have ``sample.UB`` and ``azimuth`` set. Q_phi : numpy.ndarray, shape (3,) Target scattering vector in the phi frame (``UB @ hkl``). @@ -1775,7 +1775,7 @@ def _compute_natural_psi( ψ in degrees (−180°, +180°], or ``None`` when ψ is undefined (Q ∥ incident beam, or reference vector ∥ Q). """ - n_hkl = geometry.azimuthal_reference + n_hkl = geometry.azimuth if n_hkl is None: return None # pragma: no cover @@ -1813,7 +1813,7 @@ def _is_psi_mode(geometry: AdHocDiffractometer, mode) -> bool: """Return True when the mode contains a psi ReferenceConstraint and the reference is set.""" from .mode import ReferenceConstraint - if geometry.azimuthal_reference is None: + if geometry.azimuth is None: return False return any( isinstance(c, ReferenceConstraint) and c.name == "psi" for c in mode.constraints @@ -1878,9 +1878,9 @@ def _solve_psi_mode( warnings.warn( f"forward(): ψ is undefined for this reflection in geometry " f"{geometry.name!r} — Q is parallel to " - f"azimuthal_reference={geometry.azimuthal_reference} (or to the " + f"azimuth={geometry.azimuth} (or to the " f"incident beam). Choose a different reflection or change " - f"geometry.azimuthal_reference. Returning [].", + f"geometry.azimuth. Returning [].", UserWarning, stacklevel=5, ) diff --git a/src/ad_hoc_diffractometer/mode.py b/src/ad_hoc_diffractometer/mode.py index 78ee922c..12b45955 100644 --- a/src/ad_hoc_diffractometer/mode.py +++ b/src/ad_hoc_diffractometer/mode.py @@ -268,7 +268,7 @@ def __init__( placeholder. The corresponding value lives on the diffractometer under :attr:`~ad_hoc_diffractometer.diffractometer.AdHocDiffractometer.surface_normal` or -:attr:`~ad_hoc_diffractometer.diffractometer.AdHocDiffractometer.azimuthal_reference`, +:attr:`~ad_hoc_diffractometer.diffractometer.AdHocDiffractometer.azimuth`, chosen by the mode's :class:`ReferenceConstraint`; the ``extras`` entry is just a hint that documents the requirement. @@ -316,7 +316,7 @@ def __setitem__(self, key: str, value: Any) -> None: 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"'g.azimuthal_reference = (h, k, l)' for " + f"'g.azimuth = (h, k, l)' for " f"psi / naz constraints. See " f"AdHocDiffractometer.required_reference_vector to " f"discover which attribute the active mode needs. " @@ -840,7 +840,7 @@ class ReferenceConstraint: Constrains a physical pseudo-angle between Q and the reference vector n̂. The reference vector n̂ must be stored on the geometry as - ``geometry.surface_normal`` (preferred) or ``geometry.azimuthal_reference`` + ``geometry.surface_normal`` (preferred) or ``geometry.azimuth`` before calling ``forward()``. Valid names (from You 1999 and Lohmeier & Vlieg 1993): @@ -936,7 +936,7 @@ def has_reference_vector(self, geometry: AdHocDiffractometer) -> bool: Return True when the required reference vector is set on the geometry. For ``"psi"`` and ``"naz"``: requires - :attr:`~geometry.AdHocDiffractometer.azimuthal_reference` to be set. + :attr:`~geometry.AdHocDiffractometer.azimuth` to be set. For ``"alpha_i"``, ``"beta_out"``, and ``"a_eq_b"``: requires :attr:`~geometry.AdHocDiffractometer.surface_normal` to be set. @@ -953,7 +953,7 @@ def has_reference_vector(self, geometry: AdHocDiffractometer) -> bool: if self._name == "omega": return True if self._name in {"psi", "naz"}: - return geometry.azimuthal_reference is not None + return geometry.azimuth is not None return geometry.surface_normal is not None def is_implemented(self, geometry: AdHocDiffractometer) -> bool: @@ -966,7 +966,7 @@ def is_implemented(self, geometry: AdHocDiffractometer) -> bool: - ``"alpha_i"`` — requires :attr:`~geometry.AdHocDiffractometer.surface_normal` - ``"beta_out"`` — requires :attr:`~geometry.AdHocDiffractometer.surface_normal` - ``"a_eq_b"`` — requires :attr:`~geometry.AdHocDiffractometer.surface_normal` - - ``"psi"`` — requires :attr:`~geometry.AdHocDiffractometer.azimuthal_reference`. + - ``"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 every Bragg solution. The solver computes the natural ψ from UB and @@ -985,7 +985,7 @@ def is_implemented(self, geometry: AdHocDiffractometer) -> bool: if self._name == "naz": return False if self._name == "psi": - return geometry.azimuthal_reference is not None + return geometry.azimuth is not None if self._name == "omega": # Implemented for any geometry with a chi sample stage. return any(s.name == "chi" for s in geometry.sample_stages) diff --git a/src/ad_hoc_diffractometer/reference.py b/src/ad_hoc_diffractometer/reference.py index 3c89da94..c9c91c9d 100644 --- a/src/ad_hoc_diffractometer/reference.py +++ b/src/ad_hoc_diffractometer/reference.py @@ -9,7 +9,7 @@ SPEC ``OMEGA`` pseudo-angle (angle between Q and the chi-circle plane). These functions require the geometry's :attr:`surface_normal` or -:attr:`azimuthal_reference` to be set before calling, **except** for +:attr:`azimuth` to be set before calling, **except** for :func:`omega_pseudo`, which is a pure motor-frame quantity and needs neither. @@ -77,14 +77,14 @@ def _require_surface_normal(geometry: AdHocDiffractometer) -> None: ) -def _require_azimuthal_reference(geometry: AdHocDiffractometer) -> None: - """Raise ValueError if azimuthal_reference is not set on the geometry.""" - ar = geometry.azimuthal_reference +def _require_azimuth(geometry: AdHocDiffractometer) -> None: + """Raise ValueError if azimuth is not set on the geometry.""" + ar = geometry.azimuth if ar is None: raise ValueError( - f"geometry '{geometry.name}': azimuthal_reference must be set before " + f"geometry '{geometry.name}': azimuth must be set before " "computing the ψ angle. " - "Set g.azimuthal_reference = (h, k, l) with the reference direction " + "Set g.azimuth = (h, k, l) with the reference direction " "expressed as Miller indices." ) @@ -180,7 +180,7 @@ def psi_angle( (also projected onto that plane). ψ = 0 when the reference vector lies in the scattering plane on the same side as the incident beam. - Requires :attr:`~geometry.AdHocDiffractometer.azimuthal_reference` + Requires :attr:`~geometry.AdHocDiffractometer.azimuth` to be set. Parameters @@ -199,7 +199,7 @@ def psi_angle( Raises ------ ValueError - If ``geometry.azimuthal_reference`` is ``None``. + If ``geometry.azimuth`` is ``None``. ValueError If the reference vector is parallel to Q (ψ is undefined). @@ -207,7 +207,7 @@ def psi_angle( ---------- * You (1999), eq. 23. """ - _require_azimuthal_reference(geometry) + _require_azimuth(geometry) return geometry.psi(angles=angles) @@ -223,7 +223,7 @@ def natural_psi( The azimuthal angle ψ is a pure phi-frame quantity: for a fixed crystal orientation (UB matrix) and a fixed reflection (h, k, l), ψ depends only on ``Q_phi = UB @ (h, k, l)`` and the azimuthal - reference vector ``n_phi = UB @ azimuthal_reference``. **No motor + reference vector ``n_phi = UB @ azimuth``. **No motor angles enter the calculation** — every motor configuration that brings (h, k, l) into Bragg condition produces the *same* ψ. @@ -234,14 +234,14 @@ def natural_psi( must I request to make this reflection reachable?" before calling :meth:`~diffractometer.AdHocDiffractometer.forward`. - Requires :attr:`~geometry.AdHocDiffractometer.azimuthal_reference` + Requires :attr:`~geometry.AdHocDiffractometer.azimuth` and :attr:`~sample.Sample.UB` to be set on the geometry. Parameters ---------- geometry : AdHocDiffractometer The diffractometer instance. ``geometry.sample.UB`` and - ``geometry.azimuthal_reference`` must be set. + ``geometry.azimuth`` must be set. h, k, l : float Miller indices of the reflection. @@ -257,7 +257,7 @@ def natural_psi( Raises ------ ValueError - If ``geometry.azimuthal_reference`` is ``None``. + If ``geometry.azimuth`` is ``None``. See Also -------- @@ -268,7 +268,7 @@ def natural_psi( ---------- * You (1999), eq. 23. """ - _require_azimuthal_reference(geometry) + _require_azimuth(geometry) # Local import to avoid module-load ordering issues with forward.py. from .forward import _compute_natural_psi @@ -486,7 +486,7 @@ def omega_pseudo( ----- Unlike :func:`incidence_angle`, :func:`exit_angle`, :func:`psi_angle`, and :func:`naz_angle`, this function does **not** require any - reference vector (``surface_normal`` / ``azimuthal_reference``) to be + 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/rotation.py b/src/ad_hoc_diffractometer/rotation.py index 1a91cafc..7d9984c9 100644 --- a/src/ad_hoc_diffractometer/rotation.py +++ b/src/ad_hoc_diffractometer/rotation.py @@ -40,7 +40,7 @@ def rotation_matrix(axis: np.ndarray, angle_deg: float) -> np.ndarray: of how each stage is wired in its YAML definition. Do not confuse it with ``n̂`` (``n_hat`` in mode ``extras``, :attr:`~ad_hoc_diffractometer.diffractometer.AdHocDiffractometer.surface_normal`, - :attr:`~ad_hoc_diffractometer.diffractometer.AdHocDiffractometer.azimuthal_reference`), + :attr:`~ad_hoc_diffractometer.diffractometer.AdHocDiffractometer.azimuth`), which is the user-facing **reference vector** consumed by :class:`~ad_hoc_diffractometer.mode.ReferenceConstraint` modes. See the :ref:`glossary` entries for "n̂ (reference vector)" and diff --git a/src/ad_hoc_diffractometer/stage.py b/src/ad_hoc_diffractometer/stage.py index eb68fa81..3dec22b8 100644 --- a/src/ad_hoc_diffractometer/stage.py +++ b/src/ad_hoc_diffractometer/stage.py @@ -40,7 +40,7 @@ class Stage: stage definition. It is **not** the same as ``n̂`` (``n_hat`` in mode ``extras``, :attr:`~ad_hoc_diffractometer.diffractometer.AdHocDiffractometer.surface_normal`, - :attr:`~ad_hoc_diffractometer.diffractometer.AdHocDiffractometer.azimuthal_reference`), + :attr:`~ad_hoc_diffractometer.diffractometer.AdHocDiffractometer.azimuth`), which is the user-facing reference vector consumed by :class:`~ad_hoc_diffractometer.mode.ReferenceConstraint` modes. See the :ref:`glossary` for the disambiguation. diff --git a/src/ad_hoc_diffractometer/surface.py b/src/ad_hoc_diffractometer/surface.py index fbec5fa6..fceb1d40 100644 --- a/src/ad_hoc_diffractometer/surface.py +++ b/src/ad_hoc_diffractometer/surface.py @@ -36,7 +36,7 @@ The sample surface normal is specified as Miller indices ``(h, k, l)`` via the geometry's ``surface_normal`` property (added to ``AdHocDiffractometer``). If ``surface_normal`` is ``None`` the -``azimuthal_reference`` is used as a fallback. +``azimuth`` is used as a fallback. The surface normal in the lab frame is computed as:: @@ -119,7 +119,7 @@ def _surface_vectors( ValueError If ``geometry.wavelength`` is None. ValueError - If ``geometry.surface_normal`` and ``geometry.azimuthal_reference`` + If ``geometry.surface_normal`` and ``geometry.azimuth`` are both None (no surface normal available). ValueError If ``geometry.sample.UB`` is None. @@ -129,11 +129,11 @@ def _surface_vectors( n_hkl = geometry.surface_normal if n_hkl is None: - n_hkl = geometry.azimuthal_reference + n_hkl = geometry.azimuth if n_hkl is None: raise ValueError( "surface calculations require geometry.surface_normal " - "(or geometry.azimuthal_reference as a fallback) to be set." + "(or geometry.azimuth as a fallback) to be set." ) if geometry.sample.UB is None: @@ -219,7 +219,7 @@ def alpha_i( ---------- geometry : AdHocDiffractometer Must have ``wavelength``, ``sample.UB``, and ``surface_normal`` - (or ``azimuthal_reference``) set. + (or ``azimuth``) set. angles : dict[str, float] or None Motor angles in degrees. If ``None``, current stage angles are used. @@ -270,7 +270,7 @@ def alpha_f( ---------- geometry : AdHocDiffractometer Must have ``wavelength``, ``sample.UB``, and ``surface_normal`` - (or ``azimuthal_reference``) set. + (or ``azimuth``) set. angles : dict[str, float] or None Motor angles in degrees. If ``None``, current stage angles are used. @@ -317,7 +317,7 @@ def q_components( ---------- geometry : AdHocDiffractometer Must have ``wavelength``, ``sample.UB``, and ``surface_normal`` - (or ``azimuthal_reference``) set. + (or ``azimuth``) set. angles : dict[str, float] or None Motor angles in degrees. If ``None``, current stage angles are used. diff --git a/tests/test_benchmark.py b/tests/test_benchmark.py index 7fe4bb3c..fd4a2279 100644 --- a/tests/test_benchmark.py +++ b/tests/test_benchmark.py @@ -111,11 +111,11 @@ def test_double_diffraction_extras(self, geometry_name, mode_name, context): assert isinstance(cs.extras[key], float) def test_fixed_psi_sets_reference(self): - """fixed_psi modes get an azimuthal_reference if not set.""" + """fixed_psi modes get an azimuth if not set.""" g = _setup_geometry("fourcv") - assert g.azimuthal_reference is None + assert g.azimuth is None _prepare_mode(g, "fixed_psi") - assert g.azimuthal_reference is not None + assert g.azimuth is not None @pytest.mark.parametrize( "geometry_name, mode_name", diff --git a/tests/test_diffractometer.py b/tests/test_diffractometer.py index 015533f6..44469e90 100644 --- a/tests/test_diffractometer.py +++ b/tests/test_diffractometer.py @@ -10,7 +10,7 @@ - set_angle() and stage() - sample_rotation_matrix() and detector_rotation_matrix() - check_limits() - - azimuthal_reference property: storage, validation, default None + - azimuth property: storage, validation, default None - psi() method: psi=0 when n in scattering plane, psi=90 when n perp, uses current angles when called with no args, error cases - wh(print=False): str output, content, graceful fallbacks, stdout control @@ -753,80 +753,185 @@ def test_inverse_unknown_stage_raises(): # --------------------------------------------------------------------------- -# azimuthal_reference property (#11) +# azimuth property (#11) # --------------------------------------------------------------------------- -def test_azimuthal_reference_default_is_none(): - """azimuthal_reference is None by default.""" +def test_azimuth_default_is_none(): + """azimuth is None by default.""" from helpers import fourcv - assert fourcv().azimuthal_reference is None + assert fourcv().azimuth is None -def test_azimuthal_reference_set_tuple(): +def test_azimuth_set_tuple(): """Setting to a 3-tuple stores it as (float, float, float).""" from helpers import fourcv g = fourcv() - g.azimuthal_reference = (0, 0, 1) - assert g.azimuthal_reference == (0.0, 0.0, 1.0) + g.azimuth = (0, 0, 1) + assert g.azimuth == (0.0, 0.0, 1.0) -def test_azimuthal_reference_set_list(): +def test_azimuth_set_list(): """Setting to a list of 3 numbers works (converts to tuple of floats).""" from helpers import psic g = psic() - g.azimuthal_reference = [1, 1, 0] - assert g.azimuthal_reference == (1.0, 1.0, 0.0) + g.azimuth = [1, 1, 0] + assert g.azimuth == (1.0, 1.0, 0.0) -def test_azimuthal_reference_clear_with_none(): +def test_azimuth_clear_with_none(): """Setting to None clears the reference.""" from helpers import fourcv g = fourcv() - g.azimuthal_reference = (0, 0, 1) - g.azimuthal_reference = None - assert g.azimuthal_reference is None + g.azimuth = (0, 0, 1) + g.azimuth = None + assert g.azimuth is None -def test_azimuthal_reference_constructor(): - """azimuthal_reference can be set at construction via keyword argument.""" +def test_azimuth_constructor(): + """azimuth can be set at construction via keyword argument.""" from helpers import fourcv # Use fourcv factory result and check the property works after setting g = fourcv() - g.azimuthal_reference = (0, 1, 0) - assert g.azimuthal_reference == (0.0, 1.0, 0.0) + g.azimuth = (0, 1, 0) + assert g.azimuth == (0.0, 1.0, 0.0) -def test_azimuthal_reference_zero_vector_raises(): +def test_azimuth_zero_vector_raises(): """Setting to (0, 0, 0) raises ValueError.""" from helpers import fourcv g = fourcv() with pytest.raises(ValueError, match=re.escape("non-zero")): - g.azimuthal_reference = (0, 0, 0) + g.azimuth = (0, 0, 0) -def test_azimuthal_reference_bad_type_raises(): +def test_azimuth_bad_type_raises(): """Setting to a non-sequence raises ValueError.""" from helpers import fourcv g = fourcv() with pytest.raises(ValueError, match=re.escape("length-3 sequence")): - g.azimuthal_reference = 42 + g.azimuth = 42 -def test_azimuthal_reference_wrong_length_raises(): +def test_azimuth_wrong_length_raises(): """Setting to a 2-element tuple raises ValueError.""" from helpers import fourcv g = fourcv() with pytest.raises(ValueError): - g.azimuthal_reference = (0, 1) + 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_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) # --------------------------------------------------------------------------- @@ -842,7 +947,7 @@ def test_azimuthal_reference_wrong_length_raises(): def _fourcv_identity(): - """fourcv with B=I (a=1), lambda=2pi, UB=I, azimuthal_reference=(1,0,0). + """fourcv with B=I (a=1), lambda=2pi, UB=I, azimuth=(1,0,0). With the corrected fourcv (vertical scattering plane), omega and ttheta rotate about the transverse (-x) axis. At chi=0, phi=0, omega=30, ttheta=60 @@ -858,7 +963,7 @@ def _fourcv_identity(): g.wavelength = 2 * _math.pi g.sample.lattice = Lattice(a=1.0) ub_identity(g.sample) - g.azimuthal_reference = ( + g.azimuth = ( 1, 0, 0, @@ -881,12 +986,12 @@ def test_psi_n_perpendicular_to_scattering_plane_is_90(): Under issue #280 ub_identity, ``UB @ (0, 0, 1) = U[:, 2] · |b3*|`` is physically along ``+transverse`` (= +x in fourcv-BL basis), which is perpendicular to the vertical scattering plane. Pre-#280 the same - physical configuration was selected by ``azimuthal_reference = + physical configuration was selected by ``azimuth = (1, 0, 0)`` because the basis-relative ``U = I`` put the crystal a-axis along ``+x``. """ g = _fourcv_identity() - g.azimuthal_reference = (0, 0, 1) # → n_phi along +transverse + g.azimuth = (0, 0, 1) # → n_phi along +transverse angles = {"omega": 30.0, "chi": 0.0, "phi": 0.0, "ttheta": 60.0} psi = g.psi(angles) assert abs(psi - 90.0) < 1e-8 @@ -901,13 +1006,13 @@ def test_psi_n_in_scattering_plane_is_0(): is parallel to the incident beam. By the BL1967 psi convention, a reference parallel to the incident beam gives psi = 0. - The pre-#280 test used ``azimuthal_reference = (0, 1, 0)`` for the + The pre-#280 test used ``azimuth = (0, 1, 0)`` for the same physical configuration because the basis-relative ``U = I`` aligned the crystal b-axis along the basis-y direction. Under the new ub_identity the crystal a-axis is the one along the beam. """ g = _fourcv_identity() - g.azimuthal_reference = (1, 0, 0) # → n_phi along +longitudinal + g.azimuth = (1, 0, 0) # → n_phi along +longitudinal angles = {"omega": 30.0, "chi": 0.0, "phi": 0.0, "ttheta": 60.0} psi = g.psi(angles) assert abs(psi - 0.0) < 1e-8 @@ -923,7 +1028,7 @@ def test_psi_uses_current_angles_when_none_passed(): # Under issue #280 ub_identity, (0,0,1) places the reference # physically along +transverse — perpendicular to the vertical # scattering plane and to Q at these angles. - g.azimuthal_reference = (0, 0, 1) + g.azimuth = (0, 0, 1) psi_implicit = g.psi() psi_explicit = g.psi({"omega": 30.0, "chi": 0.0, "phi": 0.0, "ttheta": 60.0}) assert abs(psi_implicit - psi_explicit) < 1e-10 @@ -948,10 +1053,10 @@ def test_psi_range_within_180(): def test_psi_no_reference_raises(): - """psi() raises ValueError when azimuthal_reference is None.""" + """psi() raises ValueError when azimuth is None.""" g = _fourcv_identity() - g.azimuthal_reference = None - with pytest.raises(ValueError, match=re.escape("azimuthal_reference")): + g.azimuth = None + with pytest.raises(ValueError, match=re.escape("azimuth")): g.psi({"omega": 30.0, "chi": 0.0, "phi": 0.0, "ttheta": 60.0}) @@ -980,7 +1085,7 @@ def test_psi_n_parallel_to_q_raises(): Choose a Miller index whose ``UB @ hkl`` is parallel to that Q. Under the new ub_identity the simplest such case is to set - ``azimuthal_reference`` to the same physical direction as Q at the + ``azimuth`` to the same physical direction as Q at the chosen motor configuration. We pick the hkl that places n_phi exactly where Q lands for these angles. """ @@ -994,7 +1099,7 @@ def test_psi_n_parallel_to_q_raises(): Q_phi = g.inverse(angles) # Q_phi here actually returns the hkl tuple; use it directly as # the parallel reference. - g.azimuthal_reference = tuple(float(v) for v in Q_phi) + g.azimuth = tuple(float(v) for v in Q_phi) with pytest.raises(ValueError, match=re.escape("parallel to Q")): g.psi(angles) @@ -1020,20 +1125,20 @@ def test_psi_beam_parallel_to_q_raises(): # branch. Under the new ub_identity, ``(0, 0, 1)`` places n_phi # along physical +transverse (= +x in fourcv-BL), which is # perpendicular to the beam direction. - g.azimuthal_reference = (0, 0, 1) + g.azimuth = (0, 0, 1) with pytest.raises( ValueError, match=re.escape("incident beam direction is parallel to Q") ): g.psi(angles) -def test_psi_pa_shows_azimuthal_reference(): +def test_psi_pa_shows_azimuth(): """pa property output includes the azimuthal reference hkl when set.""" from helpers import fourcv g = fourcv() g.wavelength = 1.5406 - g.azimuthal_reference = (0, 0, 1) + g.azimuth = (0, 0, 1) assert "Azimuthal Reference" in g.pa(print=False) assert "0 0 1" in g.pa(print=False) @@ -1434,7 +1539,7 @@ def test_psi_q_zero_raises(angles, match): g = fourcv() g.wavelength = 2 * _math.pi ub_identity(g.sample) - g.azimuthal_reference = (0, 0, 1) + g.azimuth = (0, 0, 1) with pytest.raises(ValueError, match=match): g.psi(angles) @@ -1450,7 +1555,7 @@ def test_psi_n_maps_to_zero_raises(): g.sample.lattice = Lattice(a=1.0) g.sample.UB = np.zeros((3, 3)) g.sample.U = np.zeros((3, 3)) - g.azimuthal_reference = (0, 0, 1) + g.azimuth = (0, 0, 1) with pytest.raises(ValueError, match="zero in the phi frame"): g.psi({"omega": 30.0, "chi": 0.0, "phi": 0.0, "ttheta": 60.0}) @@ -1526,7 +1631,7 @@ def test_wh_logs_debug_when_psi_unavailable(caplog): g = fourcv() g.wavelength = 1.5406 ub_identity(g.sample) - # No azimuthal_reference → psi() raises → logger.debug fires + # No azimuth → psi() raises → logger.debug fires with caplog.at_level(logging.DEBUG, logger="ad_hoc_diffractometer.diffractometer"): g.wh(print=False) @@ -1914,7 +2019,7 @@ def _sapphire_fourcv(): g = fourcv() g.wavelength = 1.549802558 - g.azimuthal_reference = (0, 0, 1) + g.azimuth = (0, 0, 1) g.add_sample("sapphire", Lattice(a=4.785, c=12.991, gamma=120)) g.sample = "sapphire" g.add_reflection( @@ -1974,10 +2079,10 @@ def test_geometry_to_dict_top_level(key, expected, context): assert _sapphire_fourcv().to_dict()[key] == pytest.approx(expected) -def test_geometry_to_dict_azimuthal_reference(): +def test_geometry_to_dict_azimuth(): """to_dict() stores the azimuthal reference vector.""" d = _sapphire_fourcv().to_dict() - assert d["azimuthal_reference"] == pytest.approx([0.0, 0.0, 1.0]) + assert d["azimuth"] == pytest.approx([0.0, 0.0, 1.0]) def test_geometry_to_dict_stages(): @@ -2008,8 +2113,8 @@ def test_geometry_to_dict_stage_angle_preserved(): id="wavelength", ), pytest.param( - "azimuthal_reference", - lambda o, r: r.azimuthal_reference == pytest.approx(o.azimuthal_reference), + "azimuth", + lambda o, r: r.azimuth == pytest.approx(o.azimuth), does_not_raise(), id="azimuthal-ref", ), @@ -2101,7 +2206,7 @@ def test_geometry_json_roundtrip(): pytest.param( "no-azimuthal-ref", lambda: _no_azref_fourcv(), - lambda o, r: r.azimuthal_reference is None, + lambda o, r: r.azimuth is None, does_not_raise(), id="no-azimuthal-ref", ), @@ -2210,14 +2315,14 @@ def test_geometry_to_dict_version_unknown_on_metadata_error(): pytest.param( "psic", "fixed_psi_vertical", - "azimuthal_reference", - id="psic-fixed_psi_vertical-azimuthal_reference", + "azimuth", + id="psic-fixed_psi_vertical-azimuth", ), pytest.param( "psic", "fixed_psi_horizontal", - "azimuthal_reference", - id="psic-fixed_psi_horizontal-azimuthal_reference", + "azimuth", + id="psic-fixed_psi_horizontal-azimuth", ), pytest.param( "psic", @@ -2246,8 +2351,8 @@ def test_geometry_to_dict_version_unknown_on_metadata_error(): pytest.param( "fourcv", "fixed_psi", - "azimuthal_reference", - id="fourcv-fixed_psi-azimuthal_reference", + "azimuth", + id="fourcv-fixed_psi-azimuth", ), ], ) diff --git a/tests/test_forward.py b/tests/test_forward.py index 41c837e6..04dd2ad0 100644 --- a/tests/test_forward.py +++ b/tests/test_forward.py @@ -1826,9 +1826,9 @@ def test_kappa6c_lifting_detector_qaz_filters_by_detector_limits(): def _setup_psi(factory, ref=(0, 0, 1), a=4.0): - """Return a geometry with wavelength, UB=B, and azimuthal_reference set.""" + """Return a geometry with wavelength, UB=B, and azimuth set.""" g = _setup_cubic(factory, a=a) - g.azimuthal_reference = ref + g.azimuth = ref return g diff --git a/tests/test_mode.py b/tests/test_mode.py index 0095b020..4734bd8a 100644 --- a/tests/test_mode.py +++ b/tests/test_mode.py @@ -2724,7 +2724,7 @@ def test_extras_n_hat_assignment_warns_pointing_at_geometry_attribute(): assert len(captured) == 1 msg = str(captured[0].message) assert "g.surface_normal" in msg - assert "g.azimuthal_reference" in msg + assert "g.azimuth" in msg assert "required_reference_vector" in msg diff --git a/tests/test_reference.py b/tests/test_reference.py index 6d3ab98f..0606175e 100644 --- a/tests/test_reference.py +++ b/tests/test_reference.py @@ -6,10 +6,10 @@ Covers: - incidence_angle: requires surface_normal; raises when None - exit_angle: requires surface_normal; raises when None - - psi_angle: requires azimuthal_reference; raises when None + - 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 - - Serialisation round-trip with surface_normal and azimuthal_reference set + - Serialisation round-trip with surface_normal and azimuth set - Smoke tests: reasonable output range for known geometry configurations """ @@ -121,16 +121,16 @@ def test_specular_condition_alpha_i_equals_alpha_f(): # --------------------------------------------------------------------------- -def test_psi_angle_raises_without_azimuthal_reference(): - """psi_angle raises ValueError when azimuthal_reference is None.""" +def test_psi_angle_raises_without_azimuth(): + """psi_angle raises ValueError when azimuth is None.""" g = _setup_psic() - assert g.azimuthal_reference is None - with pytest.raises(ValueError, match=re.escape("azimuthal_reference must be set")): + assert g.azimuth is None + with pytest.raises(ValueError, match=re.escape("azimuth must be set")): psi_angle(g) -def test_psi_angle_with_azimuthal_reference(): - """psi_angle returns a float in (-180, 180] when azimuthal_reference is set. +def test_psi_angle_with_azimuth(): + """psi_angle returns a float in (-180, 180] when azimuth is set. Uses ``(0, 1, 0)`` instead of ``(1, 0, 0)``: under issue #280 ub_identity the crystal a* axis is along the beam, so Q_phi(1,0,0) @@ -138,7 +138,7 @@ def test_psi_angle_with_azimuthal_reference(): produces a Q_phi off the beam axis. """ g = _setup_psic() - g.azimuthal_reference = (0, 0, 1) + g.azimuth = (0, 0, 1) g.mode_name = "bisecting_vertical" sols = g.forward(0, 1, 0) for s in sols: @@ -151,10 +151,10 @@ def test_psi_angle_uses_current_angles_when_none(): """psi_angle uses current stage angles when angles=None. Uses ``(0, 1, 0)`` for the same reason as - :func:`test_psi_angle_with_azimuthal_reference`. + :func:`test_psi_angle_with_azimuth`. """ g = _setup_psic() - g.azimuthal_reference = (0, 0, 1) + g.azimuth = (0, 0, 1) g.mode_name = "bisecting_vertical" sols = g.forward(0, 1, 0) s = sols[0] @@ -241,11 +241,11 @@ def test_naz_angle_vertical_normal_returns_zero(): "a_eq_b", True, "surface_normal", (0, 0, 1), True, id="a_eq_b-with-sn" ), pytest.param("a_eq_b", True, "surface_normal", None, False, id="a_eq_b-no-sn"), - # psi: implemented when azimuthal_reference is set + # psi: implemented when azimuth is set pytest.param( "psi", 0.0, - "azimuthal_reference", + "azimuth", (0, 0, 1), True, id="psi-with-azref", @@ -253,7 +253,7 @@ def test_naz_angle_vertical_normal_returns_zero(): pytest.param( "psi", 0.0, - "azimuthal_reference", + "azimuth", None, False, id="psi-no-azref", @@ -262,7 +262,7 @@ def test_naz_angle_vertical_normal_returns_zero(): pytest.param( "naz", 0.0, - "azimuthal_reference", + "azimuth", (0, 0, 1), False, id="naz-not-implemented", @@ -296,14 +296,10 @@ def test_reference_constraint_is_implemented( pytest.param( "a_eq_b", True, "surface_normal", (0, 0, 1), True, id="a_eq_b-with-sn" ), - pytest.param("psi", 0.0, "azimuthal_reference", None, False, id="psi-no-ar"), - pytest.param( - "psi", 0.0, "azimuthal_reference", (0, 0, 1), True, id="psi-with-ar" - ), - pytest.param("naz", 0.0, "azimuthal_reference", None, False, id="naz-no-ar"), - pytest.param( - "naz", 0.0, "azimuthal_reference", (0, 0, 1), True, id="naz-with-ar" - ), + 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"), + pytest.param("naz", 0.0, "azimuth", None, False, id="naz-no-ar"), + pytest.param("naz", 0.0, "azimuth", (0, 0, 1), True, id="naz-with-ar"), ], ) def test_reference_constraint_has_reference_vector( @@ -334,17 +330,17 @@ def test_surface_normal_round_trip(): assert g2.surface_normal == (0.0, 0.0, 1.0) -def test_azimuthal_reference_round_trip(): - """azimuthal_reference survives to_dict / from_dict round-trip.""" +def test_azimuth_round_trip(): + """azimuth survives to_dict / from_dict round-trip.""" import json g = _setup_psic() - g.azimuthal_reference = (1, 0, 0) + g.azimuth = (1, 0, 0) d = g.to_dict() assert json.dumps(d) - assert d["azimuthal_reference"] == [1.0, 0.0, 0.0] + assert d["azimuth"] == [1.0, 0.0, 0.0] g2 = AdHocDiffractometer.from_dict(d) - assert g2.azimuthal_reference == (1.0, 0.0, 0.0) + assert g2.azimuth == (1.0, 0.0, 0.0) def test_surface_normal_none_serialisation(): @@ -363,14 +359,14 @@ def test_surface_normal_none_serialisation(): def test_fixed_psi_implemented_with_azref(): - """fixed_psi is_implemented=True when azimuthal_reference is set. + """fixed_psi is_implemented=True when azimuth is set. - With azimuthal_reference set, the fixed_psi forward solver is available. + With azimuth set, the fixed_psi forward solver is available. It acts as a validation filter: it returns bisecting solutions only when the natural psi for (h,k,l) matches the stored target. """ g = _setup_psic() - g.azimuthal_reference = (0, 0, 1) + g.azimuth = (0, 0, 1) g.mode_name = "fixed_psi_vertical" cs = g.modes["fixed_psi_vertical"] # is_implemented is True — solver available @@ -387,9 +383,9 @@ def test_fixed_psi_implemented_with_azref(): def test_fixed_psi_not_implemented_without_azref(): - """fixed_psi is_implemented=False when azimuthal_reference is not set.""" + """fixed_psi is_implemented=False when azimuth is not set.""" g = _setup_psic() - assert g.azimuthal_reference is None + assert g.azimuth is None g.mode_name = "fixed_psi_vertical" cs = g.modes["fixed_psi_vertical"] assert cs.is_implemented(g) is False @@ -580,10 +576,10 @@ def test_omega_pseudo_requires_chi_stage(): def test_omega_pseudo_does_not_require_surface_normal(): - """omega_pseudo works with no surface_normal or azimuthal_reference.""" + """omega_pseudo works with no surface_normal or azimuth.""" g = _setup_psic() assert g.surface_normal is None - assert g.azimuthal_reference is None + assert g.azimuth is None g.mode_name = "bisecting_vertical" sols = g.forward(1, 0, 0) for s in sols: @@ -725,13 +721,13 @@ def test_reference_constraint_omega_serialization_round_trip(): # --------------------------------------------------------------------------- -def test_natural_psi_raises_without_azimuthal_reference(): - """natural_psi raises ValueError when azimuthal_reference is None.""" +def test_natural_psi_raises_without_azimuth(): + """natural_psi raises ValueError when azimuth is None.""" g = _setup_psic() - # azimuthal_reference defaults to None + # azimuth defaults to None with pytest.raises( ValueError, - match=re.escape("azimuthal_reference must be set"), + match=re.escape("azimuth must be set"), ): natural_psi(g, 1, 0, 0) @@ -755,7 +751,7 @@ def test_natural_psi_matches_expected_values(h, k, l, expected, context): # noq """natural_psi returns the expected motor-angle-independent ψ value.""" with context: g = _setup_psic() - g.azimuthal_reference = (0, 0, 1) + g.azimuth = (0, 0, 1) result = natural_psi(g, h, k, l) assert result == pytest.approx(expected, abs=1e-6) @@ -778,7 +774,7 @@ def test_natural_psi_returns_none_when_undefined(h, k, l, context): # noqa: E74 """ with context: g = _setup_psic() - g.azimuthal_reference = (0, 0, 1) + g.azimuth = (0, 0, 1) assert natural_psi(g, h, k, l) is None @@ -790,7 +786,7 @@ def test_natural_psi_equals_psi_angle_at_bisecting_solution(): satisfies Bragg gives the same ψ. """ g = _setup_psic() - g.azimuthal_reference = (0, 0, 1) + g.azimuth = (0, 0, 1) g.mode_name = "bisecting_vertical" sols = g.forward(1, 1, 0) assert sols, "bisecting_vertical should return at least one solution for (1,1,0)" @@ -802,7 +798,7 @@ def test_natural_psi_equals_psi_angle_at_bisecting_solution(): def test_natural_psi_independent_of_motor_state(): """natural_psi depends only on UB and (h, k, l), not on stage angles.""" g = _setup_psic() - g.azimuthal_reference = (0, 0, 1) + g.azimuth = (0, 0, 1) baseline = natural_psi(g, 1, 1, 0) # Move every stage to an arbitrary non-zero angle. for stage in g._stages.values(): # noqa: SLF001 diff --git a/tests/test_regression_issue_264.py b/tests/test_regression_issue_264.py index 977b3f00..178ac5ff 100644 --- a/tests/test_regression_issue_264.py +++ b/tests/test_regression_issue_264.py @@ -467,7 +467,7 @@ def test_revised_fixed_psi_round_trip( from ad_hoc_diffractometer.reference import psi_angle g = _setup_psic_cubic() - g.azimuthal_reference = (0, 0, 1) + g.azimuth = (0, 0, 1) natural = _natural_psi(g, h, k, l) assert natural is not None, ( f"{mode_name} ({h},{k},{l}): natural psi is undefined for this hkl" @@ -510,7 +510,7 @@ def test_revised_fixed_psi_wrong_target_returns_empty(): psi is undefined). """ g = _setup_psic_cubic() - g.azimuthal_reference = (0, 0, 1) + g.azimuth = (0, 0, 1) natural = _natural_psi(g, 0, 1, 0) assert natural is not None diff --git a/tests/test_regression_issue_278.py b/tests/test_regression_issue_278.py index 7b915cce..c536d0e7 100644 --- a/tests/test_regression_issue_278.py +++ b/tests/test_regression_issue_278.py @@ -67,7 +67,7 @@ def _setup_psic_cubic(): g.wavelength = WAVELENGTH g.sample.lattice = ahd.Lattice(a=4.0) ub_identity(g.sample) - g.azimuthal_reference = (0, 0, 1) + g.azimuth = (0, 0, 1) g.surface_normal = (0, 0, 1) return g @@ -169,7 +169,7 @@ def test_fixed_psi_undefined_warns_and_returns_empty( """When ψ is undefined for the reflection (Q ∥ reference), ``forward()`` warns and returns ``[]``. - The warning message must mention the azimuthal_reference so the + The warning message must mention the azimuth so the user can choose a different reference direction if desired. """ with context: @@ -255,7 +255,7 @@ def test_issue_278_sapphire_reproducer_warns(h, k, l, context): # noqa: E741 a=4.758, b=4.758, c=12.991, alpha=90, beta=90, gamma=120 ) ub_identity(g.sample) - g.azimuthal_reference = (0, 0, 1) + g.azimuth = (0, 0, 1) g.surface_normal = (0, 0, 1) g.mode_name = "fixed_psi_horizontal" diff --git a/tests/test_regression_issue_292.py b/tests/test_regression_issue_292.py index 4abcde9a..155bcfa8 100644 --- a/tests/test_regression_issue_292.py +++ b/tests/test_regression_issue_292.py @@ -190,7 +190,7 @@ def test_fourcv_fixed_psi_populates_psi_extra( from ad_hoc_diffractometer.forward import _compute_natural_psi g = _setup_cubic("fourcv") - g.azimuthal_reference = ref + g.azimuth = ref Q_phi = g.sample.UB @ np.array([h, k, l], dtype=float) natural = _compute_natural_psi(g, Q_phi) assert natural is not None @@ -231,7 +231,7 @@ def test_extras_reset_when_forward_returns_no_solutions(): from ad_hoc_diffractometer.forward import _compute_natural_psi g = _setup_cubic("fourcv") - g.azimuthal_reference = (0, 0, 1) + g.azimuth = (0, 0, 1) # Choose a psi target that differs from the natural psi by 45° so the # validation filter rejects every solution and returns []. diff --git a/tests/test_surface.py b/tests/test_surface.py index 7b4618f3..fc0c5cb3 100644 --- a/tests/test_surface.py +++ b/tests/test_surface.py @@ -5,7 +5,7 @@ Covers: - surface_normal property: set/get/clear, validation errors - - surface_normal fallback to azimuthal_reference + - 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) @@ -204,7 +204,7 @@ def _no_surface_normal(): g.wavelength = WAVELENGTH g.sample.lattice = ahd.Lattice(a=1.0) ub_identity(g.sample) - # surface_normal and azimuthal_reference both None + # surface_normal and azimuth both None return g @@ -216,15 +216,15 @@ def _no_ub(): # --------------------------------------------------------------------------- -# surface_normal falls back to azimuthal_reference +# surface_normal falls back to azimuth # --------------------------------------------------------------------------- -def test_surface_normal_fallback_to_azimuthal_reference(): - """When surface_normal is None, azimuthal_reference is used.""" +def test_surface_normal_fallback_to_azimuth(): + """When surface_normal is None, azimuth is used.""" g = _make_s2d2() g.surface_normal = None - g.azimuthal_reference = (0, 0, 1) + 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}) assert pytest.approx(ai, abs=1e-6) == 5.0 From d3e4f2393a3b0dca2347aa2f0262f0f5092ea6af Mon Sep 17 00:00:00 2001 From: Pete Jemian Date: Mon, 15 Jun 2026 18:12:43 -0500 Subject: [PATCH 2/3] docs(#298) migrate documentation to 'azimuth' name Update all documentation sources (glossary, concepts, surface how-to, modes / forward / constraints how-tos, geometry pages, declarative schema reference, and the fourcv alignment notebook) to use the new canonical 'azimuth' attribute name in prose, code examples, recipes, and the per-geometry Extras tables. Add deprecation notes in three places where a user is likely to look: - glossary.md: the 'Azimuthal reference vector' entry now mentions the v0.11.x-and-earlier 'azimuthal_reference' name as a deprecated forwarding alias. - howto/surface.md (quick-reference section): a {note} admonition after the recipe table announces the rename, the deprecation warnings, and the required_reference_vector return-value change. - howto/surface.md (serialization section): a one-line note that from_dict() still accepts the legacy 'azimuthal_reference' key for backward compatibility with saved sessions. Contributed by: OpenCode (argo/claudeopus47) --- docs/source/concepts.md | 6 +-- docs/source/geometries/fourch.md | 4 +- docs/source/geometries/fourcv.md | 4 +- docs/source/geometries/kappa4ch.md | 4 +- docs/source/geometries/kappa4cv.md | 4 +- docs/source/geometries/kappa6c.md | 8 ++-- docs/source/geometries/psic.md | 6 +-- docs/source/glossary.md | 15 ++++--- docs/source/howto/constraints.md | 8 ++-- docs/source/howto/forward.md | 6 +-- .../source/howto/fourcv_alignment_howto.ipynb | 6 +-- docs/source/howto/modes.md | 6 +-- docs/source/howto/surface.md | 41 ++++++++++++------- .../reference/declarative_geometry_schema.md | 2 +- 14 files changed, 68 insertions(+), 52 deletions(-) diff --git a/docs/source/concepts.md b/docs/source/concepts.md index 062387ec..e5b57b3d 100644 --- a/docs/source/concepts.md +++ b/docs/source/concepts.md @@ -96,7 +96,7 @@ the geometry's basis dict. ``n_axis`` here is the **per-stage rotation axis** vector — internal to the stage definition. It is **not** the same as ``n̂`` (written as the ``n_hat`` key in mode ``extras``, or as -``g.surface_normal`` / ``g.azimuthal_reference`` on the geometry), +``g.surface_normal`` / ``g.azimuth`` on the geometry), which is the *user-facing reference vector* required by surface and azimuthal modes. See the {ref}`glossary ` entries for "n̂ (reference vector)" and "Stage rotation axis" for the full @@ -527,12 +527,12 @@ vectors may be set: - **`surface_normal`** — direction perpendicular to the sample surface; used by incidence/exit angle functions and surface diffraction modes. -- **`azimuthal_reference`** — direction defining ψ = 0; used by +- **`azimuth`** — direction defining ψ = 0; used by `psi_angle` and `fixed_psi_*` modes. ```python g.surface_normal = (0, 0, 1) # (001)-cut sample -g.azimuthal_reference = (1, 0, 0) +g.azimuth = (1, 0, 0) ``` Vectors are stored as Miller indices and converted to the lab frame diff --git a/docs/source/geometries/fourch.md b/docs/source/geometries/fourch.md index e78e0127..10d6b505 100644 --- a/docs/source/geometries/fourch.md +++ b/docs/source/geometries/fourch.md @@ -114,13 +114,13 @@ Override at run time with `g.modes["fixed_omega"].with_constraint_values(omega=. {class}`~ad_hoc_diffractometer.mode.ReferenceConstraint`: azimuthal angle ψ validation filter. -Set ``g.azimuthal_reference = (h, k, l)`` before calling ``forward()``. +Set ``g.azimuth = (h, k, l)`` before calling ``forward()``. Returns bisecting solutions only when the natural ψ for (h,k,l) matches the stored target. See {doc}`../howto/surface`. | | | |---|---| -| **Extras (input)** | n̂ → set `g.azimuthal_reference = (h, k, l)`; ψ target via `with_constraint_values(psi=...)` | +| **Extras (input)** | n̂ → set `g.azimuth = (h, k, l)`; ψ target via `with_constraint_values(psi=...)` | | **Extras (output)** | psi (computed azimuth) | ### `double_diffraction` diff --git a/docs/source/geometries/fourcv.md b/docs/source/geometries/fourcv.md index 69e63a7f..70d94521 100644 --- a/docs/source/geometries/fourcv.md +++ b/docs/source/geometries/fourcv.md @@ -114,13 +114,13 @@ Override at run time with `g.modes["fixed_omega"].with_constraint_values(omega=. {class}`~ad_hoc_diffractometer.mode.ReferenceConstraint`: azimuthal angle ψ validation filter. -Set ``g.azimuthal_reference = (h, k, l)`` before calling ``forward()``. +Set ``g.azimuth = (h, k, l)`` before calling ``forward()``. Returns bisecting solutions only when the natural ψ for (h,k,l) matches the stored target. See {doc}`../howto/surface`. | | | |---|---| -| **Extras (input)** | n̂ → set `g.azimuthal_reference = (h, k, l)`; ψ target via `with_constraint_values(psi=...)` | +| **Extras (input)** | n̂ → set `g.azimuth = (h, k, l)`; ψ target via `with_constraint_values(psi=...)` | | **Extras (output)** | psi (computed azimuth) | ### `double_diffraction` diff --git a/docs/source/geometries/kappa4ch.md b/docs/source/geometries/kappa4ch.md index 18760ec7..e7e333fc 100644 --- a/docs/source/geometries/kappa4ch.md +++ b/docs/source/geometries/kappa4ch.md @@ -175,14 +175,14 @@ Override at run time with `g.modes["fixed_phi"].with_constraint_values(phi=...)` {class}`~ad_hoc_diffractometer.mode.ReferenceConstraint`: azimuthal angle ψ validation filter. -Set ``g.azimuthal_reference = (h, k, l)`` before calling ``forward()``. +Set ``g.azimuth = (h, k, l)`` before calling ``forward()``. Returns bisecting solutions only when the natural ψ for (h,k,l) matches the stored target. See {doc}`../howto/surface`. Override the ψ target at run time with `g.modes["fixed_psi"].with_constraint_values(psi=...)` — see {doc}`../howto/constraints`. | | | |---|---| -| **Extras (input)** | n̂ → set `g.azimuthal_reference = (h, k, l)`; ψ target via `with_constraint_values(psi=...)` | +| **Extras (input)** | n̂ → set `g.azimuth = (h, k, l)`; ψ target via `with_constraint_values(psi=...)` | | **Extras (output)** | psi (computed azimuth) | ## API reference diff --git a/docs/source/geometries/kappa4cv.md b/docs/source/geometries/kappa4cv.md index d516ba49..975812f1 100644 --- a/docs/source/geometries/kappa4cv.md +++ b/docs/source/geometries/kappa4cv.md @@ -185,7 +185,7 @@ Override at run time with `g.modes["fixed_phi"].with_constraint_values(phi=...)` {class}`~ad_hoc_diffractometer.mode.ReferenceConstraint`: azimuthal angle ψ validation filter. -Set ``g.azimuthal_reference = (h, k, l)`` before calling ``forward()``. +Set ``g.azimuth = (h, k, l)`` before calling ``forward()``. Returns bisecting solutions only when the natural ψ for (h,k,l) matches the stored target. See {doc}`../howto/surface`. Override the ψ target at run time with `g.modes["fixed_psi"].with_constraint_values(psi=...)` — see {doc}`../howto/constraints`. @@ -193,7 +193,7 @@ Override the ψ target at run time with `g.modes["fixed_psi"].with_constraint_va | | | |---|---| | **Computed** | komega, kappa, kphi, ttheta | -| **Extras (input)** | n̂ → set `g.azimuthal_reference = (h, k, l)`; ψ target via `with_constraint_values(psi=...)` | +| **Extras (input)** | n̂ → set `g.azimuth = (h, k, l)`; ψ target via `with_constraint_values(psi=...)` | | **Extras (output)** | psi (computed azimuth) | ### `double_diffraction` diff --git a/docs/source/geometries/kappa6c.md b/docs/source/geometries/kappa6c.md index f06d2b2a..ae31fde5 100644 --- a/docs/source/geometries/kappa6c.md +++ b/docs/source/geometries/kappa6c.md @@ -173,7 +173,7 @@ Override at run time with `g.modes["fixed_nu"].with_constraint_values(nu=...)` ### `fixed_psi_vertical` Vertical bisecting with azimuthal angle ψ validation. -Set ``g.azimuthal_reference = (h, k, l)`` before calling ``forward()``. +Set ``g.azimuth = (h, k, l)`` before calling ``forward()``. The solver returns bisecting solutions only when the natural ψ for the requested (h,k,l) matches the stored target. See {doc}`../howto/surface`. Override the ψ target at run time with `g.modes["fixed_psi_vertical"].with_constraint_values(psi=...)` — see {doc}`../howto/constraints`. @@ -182,7 +182,7 @@ Override the ψ target at run time with `g.modes["fixed_psi_vertical"].with_cons |---|---| | **Computed** | komega, kappa, kphi, delta | | **Constant during** `forward()` | mu = 0, nu = 0 | -| **Extras (input)** | n̂ → set `g.azimuthal_reference = (h, k, l)`; ψ target via `with_constraint_values(psi=...)` | +| **Extras (input)** | n̂ → set `g.azimuth = (h, k, l)`; ψ target via `with_constraint_values(psi=...)` | | **Extras (output)** | psi (computed azimuth) | ### `double_diffraction_vertical` @@ -245,14 +245,14 @@ Override at run time with `g.modes["fixed_delta"].with_constraint_values(delta=. Horizontal bisecting with azimuthal angle ψ validation. Symmetric with `fixed_psi_vertical` in the horizontal plane. -Set ``g.azimuthal_reference = (h, k, l)`` before calling ``forward()``. +Set ``g.azimuth = (h, k, l)`` before calling ``forward()``. Override the ψ target at run time with `g.modes["fixed_psi_horizontal"].with_constraint_values(psi=...)` — see {doc}`../howto/constraints`. | | | |---|---| | **Computed** | mu, kappa, kphi, nu | | **Constant during** `forward()` | komega = 0, delta = 0 | -| **Extras (input)** | n̂ → set `g.azimuthal_reference = (h, k, l)`; ψ target via `with_constraint_values(psi=...)` | +| **Extras (input)** | n̂ → set `g.azimuth = (h, k, l)`; ψ target via `with_constraint_values(psi=...)` | | **Extras (output)** | psi (computed azimuth) | ### `double_diffraction_horizontal` diff --git a/docs/source/geometries/psic.md b/docs/source/geometries/psic.md index 8de743cd..d7843d95 100644 --- a/docs/source/geometries/psic.md +++ b/docs/source/geometries/psic.md @@ -150,7 +150,7 @@ Set ``g.surface_normal = (h, k, l)`` before calling ``forward()``. Issue #264 revision. Vertical scattering plane (`nu = 0`) with `mu` fixed at the user-specified value (default 0) and azimuthal angle ψ -validation. Set ``g.azimuthal_reference = (h, k, l)`` before calling +validation. Set ``g.azimuth = (h, k, l)`` before calling ``forward()``. Override the mu pin or the psi target at run time with `g.modes["fixed_psi_vertical"].with_constraint_values(mu=..., psi=...)` — see {doc}`../howto/constraints`. The previous bisect(`eta`, `delta`) constraint was @@ -164,7 +164,7 @@ validated ψ. See {doc}`../howto/surface`. |---|---| | **Computed** | eta, chi, phi, delta | | **Constant during** `forward()` | nu = 0, mu = constraint value, ψ = target | -| **Extras (input)** | n̂ → set `g.azimuthal_reference = (h, k, l)`; ψ target via `with_constraint_values(psi=...)` | +| **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` @@ -349,7 +349,7 @@ Override the eta pin or the psi target at run time with `g.modes["fixed_psi_hori |---|---| | **Computed** | mu, chi, phi, nu | | **Constant during** `forward()` | delta = 0, eta = constraint value, ψ = target | -| **Extras (input)** | n̂ → set `g.azimuthal_reference = (h, k, l)`; ψ target via `with_constraint_values(psi=...)` | +| **Extras (input)** | n̂ → set `g.azimuth = (h, k, l)`; ψ target via `with_constraint_values(psi=...)` | | **Extras (output)** | psi | ### `fixed_omega_horizontal` diff --git a/docs/source/glossary.md b/docs/source/glossary.md index aff191b9..58e5686b 100644 --- a/docs/source/glossary.md +++ b/docs/source/glossary.md @@ -19,15 +19,18 @@ Azimuthal reference vector The reciprocal-space direction (Miller indices ``(h, k, l)``) about which the azimuthal angle **ψ** is measured. Stored on the geometry as - {attr}`~ad_hoc_diffractometer.diffractometer.AdHocDiffractometer.azimuthal_reference` + {attr}`~ad_hoc_diffractometer.diffractometer.AdHocDiffractometer.azimuth` and consumed by the ``"psi"`` and ``"naz"`` {class}`~ad_hoc_diffractometer.mode.ReferenceConstraint` modes. - Set with ``g.azimuthal_reference = (h, k, l)`` (a length-3 sequence + Set with ``g.azimuth = (h, k, l)`` (a length-3 sequence of numbers; ``(0, 0, 0)`` is rejected). Default is ``None``. In 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`. + 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. B matrix The matrix that encodes the reciprocal lattice and maps Miller indices @@ -218,14 +221,14 @@ n̂ (reference vector) ``g.surface_normal = (h, k, l)``. * - On the geometry, when used by ``"psi"`` or ``"naz"`` reference constraints - - {attr}`~ad_hoc_diffractometer.diffractometer.AdHocDiffractometer.azimuthal_reference` + - {attr}`~ad_hoc_diffractometer.diffractometer.AdHocDiffractometer.azimuth` - The actual stored vector. Set via - ``g.azimuthal_reference = (h, k, l)``. + ``g.azimuth = (h, k, l)``. **Which attribute does the active mode need?** Use {attr}`~ad_hoc_diffractometer.diffractometer.AdHocDiffractometer.required_reference_vector` to ask the geometry directly — it returns - ``"surface_normal"``, ``"azimuthal_reference"``, or ``None`` based + ``"surface_normal"``, ``"azimuth"``, or ``None`` based on the active mode's reference constraint. **Not to be confused with** the {term}`Stage rotation axis` (the diff --git a/docs/source/howto/constraints.md b/docs/source/howto/constraints.md index 907b3a5a..c0819516 100644 --- a/docs/source/howto/constraints.md +++ b/docs/source/howto/constraints.md @@ -309,7 +309,7 @@ print(cs.extras) Modes whose constraint patterns do not yet have a solver implementation return `False` from `is_implemented()` and raise `NotImplementedError` when `forward()` is called. Some modes require a prerequisite to be -set on the geometry (e.g. ``azimuthal_reference`` for psi modes, +set on the geometry (e.g. ``azimuth`` for psi modes, ``surface_normal`` for surface modes) — they are considered stubs until the prerequisite is met: @@ -317,11 +317,11 @@ the prerequisite is met: g = ahd.make_geometry("fourcv") g.mode_name = "fixed_psi" -# Without azimuthal_reference: not implemented +# Without azimuth: not implemented print(g.modes["fixed_psi"].is_implemented(g)) # False -# With azimuthal_reference: implemented -g.azimuthal_reference = (0, 0, 1) +# With azimuth: implemented +g.azimuth = (0, 0, 1) print(g.modes["fixed_psi"].is_implemented(g)) # True ``` diff --git a/docs/source/howto/forward.md b/docs/source/howto/forward.md index 49e445b6..70ffffee 100644 --- a/docs/source/howto/forward.md +++ b/docs/source/howto/forward.md @@ -162,7 +162,7 @@ Attributes: `solution_index`, `constraint_repr`, `residual`, `tolerance`. ### NotImplementedError Raised when the active mode is `None` or its `is_implemented(geometry)` -returns `False` (e.g. a prerequisite like ``azimuthal_reference`` or +returns `False` (e.g. a prerequisite like ``azimuth`` or ``surface_normal`` is not set): ```python @@ -172,9 +172,9 @@ try: except NotImplementedError as e: print(e) -# fixed_psi requires azimuthal_reference to be set +# fixed_psi requires azimuth to be set g.mode_name = "fixed_psi" -g.azimuthal_reference = None +g.azimuth = None try: g.forward(1, 0, 0) except NotImplementedError as e: diff --git a/docs/source/howto/fourcv_alignment_howto.ipynb b/docs/source/howto/fourcv_alignment_howto.ipynb index f1aeb84d..316feaad 100644 --- a/docs/source/howto/fourcv_alignment_howto.ipynb +++ b/docs/source/howto/fourcv_alignment_howto.ipynb @@ -909,7 +909,7 @@ ], "source": [ "# Set the azimuthal reference to the c-axis (conventional for surface work)\n", - "g.azimuthal_reference = (0, 0, 1)\n", + "g.azimuth = (0, 0, 1)\n", "\n", "# pa() prints the full SPEC-style diffractometer status\n", "g.pa(print=True)" @@ -1022,7 +1022,7 @@ "- **Refine lattice constants** from several measured 2θ values with\n", " `ahd.refine_lattice_bl1967()` or `ahd.refine_lattice_simplex()`.\n", "- **Compute the azimuthal angle ψ** with `g.psi()` once the UB matrix\n", - " is fully refined (requires `g.azimuthal_reference` to be set).\n", + " is fully refined (requires `g.azimuth` to be set).\n", "- **Save and restore the alignment** with `g.to_dict()` / `g.from_dict()`,\n", " which serialises the full state (lattice, reflections, UB matrix,\n", " wavelength, modes, cut-points) to a JSON-compatible dict." @@ -1075,7 +1075,7 @@ "output_type": "stream", "text": [ "Keys in saved alignment state:\n", - "['name', 'description', 'wavelength', 'kappa_alpha_deg', 'azimuthal_reference', 'surface_normal', 'detector_distance', 'detector_tilt', 'detector_offset', 'inclination_matrix', 'basis', 'stages', 'active_sample', 'samples', 'modes', 'mode_name', 'cut_points']\n", + "['name', 'description', 'wavelength', 'kappa_alpha_deg', 'azimuth', 'surface_normal', 'detector_distance', 'detector_tilt', 'detector_offset', 'inclination_matrix', 'basis', 'stages', 'active_sample', 'samples', 'modes', 'mode_name', 'cut_points']\n", "\n", "JSON-serialisable: True\n", "\n", diff --git a/docs/source/howto/modes.md b/docs/source/howto/modes.md index 3008b6c3..d47139bb 100644 --- a/docs/source/howto/modes.md +++ b/docs/source/howto/modes.md @@ -139,13 +139,13 @@ print(cs.is_implemented(g)) ## Check if a mode is implemented Some modes require a prerequisite on the geometry. For example, -`fixed_psi` requires ``g.azimuthal_reference`` to be set: +`fixed_psi` requires ``g.azimuth`` to be set: ```python g.mode_name = "fixed_psi" -print(g.modes["fixed_psi"].is_implemented(g)) # False (no azimuthal_reference) +print(g.modes["fixed_psi"].is_implemented(g)) # False (no azimuth) -g.azimuthal_reference = (0, 0, 1) +g.azimuth = (0, 0, 1) print(g.modes["fixed_psi"].is_implemented(g)) # True # forward() raises NotImplementedError for unimplemented modes diff --git a/docs/source/howto/surface.md b/docs/source/howto/surface.md index 2540f1ef..a16f1565 100644 --- a/docs/source/howto/surface.md +++ b/docs/source/howto/surface.md @@ -15,7 +15,7 @@ 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)` | -| `psi`, `naz` | `azimuthal_reference` | `g.azimuthal_reference = (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: @@ -27,7 +27,16 @@ setattr(g, attr, (0, 0, 1)) # equivalent to g.surface_normal = ... ``` `required_reference_vector` returns `'surface_normal'`, -`'azimuthal_reference'`, or `None`. +`'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 @@ -52,14 +61,14 @@ and inspecting the active mode shows The `n_hat` key is a **documentation placeholder**, not a settable input slot — the actual vector lives on the geometry under -`surface_normal` or `azimuthal_reference` (see the table above). +`surface_normal` or `azimuth` (see the table above). ```python # WRONG — n_hat in extras is silently ignored by forward(): g.modes["fixed_psi_vertical"].extras["n_hat"] = (0, 0, 1) # RIGHT — set the geometry attribute the constraint reads: -g.azimuthal_reference = (0, 0, 1) +g.azimuth = (0, 0, 1) ``` Since issue #294 the package emits a `UserWarning` when the first @@ -78,7 +87,7 @@ 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`). -- **`azimuthal_reference`** — the direction used to define ψ = 0, used by +- **`azimuth`** — the direction used to define ψ = 0, used by `psi_angle` and `fixed_psi_*` modes. They may be the same vector (e.g. the surface normal is also the azimuthal @@ -118,11 +127,11 @@ The setter accepts any three-element sequence of numbers and raises ```python # Azimuthal reference along the a-axis: (1, 0, 0) -g.azimuthal_reference = (1, 0, 0) -print(g.azimuthal_reference) # (1.0, 0.0, 0.0) +g.azimuth = (1, 0, 0) +print(g.azimuth) # (1.0, 0.0, 0.0) # Same vector as surface normal for a (001) surface -g.azimuthal_reference = (0, 0, 1) +g.azimuth = (0, 0, 1) ``` ## Compute incidence and exit angles @@ -155,7 +164,7 @@ ai = incidence_angle(g) # uses current stage angles ```python from ad_hoc_diffractometer import psi_angle -g.azimuthal_reference = (0, 0, 1) +g.azimuth = (0, 0, 1) g.mode_name = "bisecting_vertical" solutions = g.forward(1, 0, 0) @@ -209,7 +218,7 @@ for sol in solutions: ## Serialization -`surface_normal` and `azimuthal_reference` are serialized in `to_dict()` +`surface_normal` and `azimuth` are serialized in `to_dict()` and restored by `from_dict()`: ```python @@ -217,25 +226,29 @@ import json from ad_hoc_diffractometer import AdHocDiffractometer g.surface_normal = (0, 0, 1) -g.azimuthal_reference = (1, 0, 0) +g.azimuth = (1, 0, 0) d = g.to_dict() print(d["surface_normal"]) # [0.0, 0.0, 1.0] -print(d["azimuthal_reference"]) # [1.0, 0.0, 0.0] +print(d["azimuth"]) # [1.0, 0.0, 0.0] 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 vector to be set on the geometry. ``fixed_psi_*`` modes require -``azimuthal_reference``; surface modes (``zaxis``, ``reflectivity``) require +``azimuth``; surface modes (``zaxis``, ``reflectivity``) require ``surface_normal``. ```python -g.azimuthal_reference = (0, 0, 1) +g.azimuth = (0, 0, 1) g.mode_name = "fixed_psi_vertical" cs = g.modes["fixed_psi_vertical"] diff --git a/docs/source/reference/declarative_geometry_schema.md b/docs/source/reference/declarative_geometry_schema.md index ca27a33e..658ff012 100644 --- a/docs/source/reference/declarative_geometry_schema.md +++ b/docs/source/reference/declarative_geometry_schema.md @@ -246,7 +246,7 @@ right-handed rotation about ``n_axis``; ``-n_axis`` is left-handed. Here ``n_axis`` is the stage's **rotation-axis vector** (an internal property of the stage definition). It is **not** the same as ``n̂`` (``n_hat`` in mode ``extras``, ``surface_normal``, or -``azimuthal_reference``), which is the user-facing **reference +``azimuth``), which is the user-facing **reference vector** required by surface and ψ modes — see {ref}`glossary` for the disambiguation. The ``rotation: left/right`` shorthand discussed in early issue drafts is **not** part of the schema — use signed-axis From 2e429232c87a13ac24d2eb2f089c9154d569df4c Mon Sep 17 00:00:00 2001 From: Pete Jemian Date: Mon, 15 Jun 2026 18:31:17 -0500 Subject: [PATCH 3/3] test(#298) cover the matching-kwargs branch in __init__ CI on the PR flagged branch coverage 142->144 in diffractometer.py: the path where both 'azimuth' and 'azimuthal_reference' are supplied with matching values (no conflict-raise, deprecation warning emitted, azimuth already set so the fallback assignment is skipped). Add test_azimuth_and_azimuthal_reference_matching_values_accepted to cover that branch and restore 100% coverage. Contributed by: OpenCode (argo/claudeopus47) --- tests/test_diffractometer.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/test_diffractometer.py b/tests/test_diffractometer.py index 44469e90..365473b8 100644 --- a/tests/test_diffractometer.py +++ b/tests/test_diffractometer.py @@ -908,6 +908,24 @@ def test_azimuthal_reference_and_azimuth_conflict_raises(): ) +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