Source code for ts_ids_components.xrd.method

"""
Method components (scan and optics parameters) for XRD IDSs.

The :py:class:`XrdScan` component holds the scan and optics parameters common
to an XRD measurement, including the 2-theta scan range, step size, detector,
and X-ray source settings. See the "Vendor value normalization" note on
:py:class:`XrdGeometryMode` for vendor-specific translation rules required
before raw values can be assigned to the fields below.
"""

from enum import Enum

from ts_ids_core.annotations import NullableString, Required, UUIDPrimaryKey
from ts_ids_core.base.ids_element import IdsElement
from ts_ids_core.base.ids_field import IdsField
from ts_ids_core.schema import RawValueUnit


[docs] class XrdGeometryMode(str, Enum): """ Standardized scan mode for an XRD measurement -- a bare Reflection/Transmission/Capillary split isn't specific enough, since e.g. :py:attr:`COUPLED_REFLECTION` and :py:attr:`DETECTOR_SCAN` are both reflection-geometry measurements but are different scan modes with different physics (and different absorption-correction requirements). Vendor value normalization --------------------------- Each vendor reports scan mode using a different raw value, which must be translated to one of these values in the task script (not passed through verbatim): * :py:attr:`COUPLED_REFLECTION`: * Bruker: the ``DataRoute/ScanInformation`` element's ``ScanName``/ ``VisibleName`` attributes, e.g. ``ScanName="LockedCoupled"``, ``VisibleName="Coupled TwoTheta/Theta"``. * Rigaku: the raw ``AttachmentHead`` element's ``SelectedUnit`` attribute, e.g. ``"ASC10_Reflection"``. * Malvern Panalytical: the ``<scan>`` element's ``scanAxis`` attribute, e.g. ``scanAxis="Gonio"``. * :py:attr:`TRANSMISSION_CAPILLARY`: * Rigaku: the same ``AttachmentHead`` element's ``SelectedUnit`` attribute as above, e.g. ``"Capillary"``. * Malvern Panalytical: the ``<scan>`` element's ``scanAxis`` attribute, e.g. ``scanAxis="2Theta"``. * :py:attr:`TRANSMISSION_FLAT_SHEET`: * Malvern Panalytical: the ``<scan>`` element's ``scanAxis`` attribute, e.g. ``scanAxis="2Theta-Omega"``. The remaining modes (:py:attr:`DETECTOR_SCAN`, :py:attr:`GRAZING_INCIDENCE`, :py:attr:`X_RAY_REFLECTIVITY`, :py:attr:`ROCKING_CURVE`, :py:attr:`PHI_SCAN`, :py:attr:`PSI_SCAN`, :py:attr:`RECIPROCAL_SPACE_MAPPING`, :py:attr:`GISAXS`) have no raw-field mapping documented here yet. Only :py:attr:`COUPLED_REFLECTION`, :py:attr:`DETECTOR_SCAN`, :py:attr:`GRAZING_INCIDENCE`, :py:attr:`X_RAY_REFLECTIVITY`, :py:attr:`TRANSMISSION_CAPILLARY`, and :py:attr:`TRANSMISSION_FLAT_SHEET` vary 2-theta and are supported by :py:class:`TwoThetaScanDatacube <ts_ids_components.xrd.datacube.TwoThetaScanDatacube>`; the remaining modes vary a different primary axis (omega, phi, psi, or a reciprocal-space grid) and have no datacube component defined here -- see :py:mod:`ts_ids_components.xrd.datacube` for why. """ COUPLED_REFLECTION = "Coupled Reflection (Bragg-Brentano)" DETECTOR_SCAN = "Detector Scan (Uncoupled)" GRAZING_INCIDENCE = "Grazing Incidence (GIXRD)" X_RAY_REFLECTIVITY = "X-ray Reflectivity (XRR)" TRANSMISSION_CAPILLARY = "Transmission (Capillary / Debye-Scherrer)" TRANSMISSION_FLAT_SHEET = "Transmission (Flat-Sheet Coupled)" ROCKING_CURVE = "Rocking Curve (ω-Scan)" PHI_SCAN = "Phi Scan (ϕ-Scan)" PSI_SCAN = "Psi Scan (ψ-Scan)" RECIPROCAL_SPACE_MAPPING = "Reciprocal Space Mapping (RSM)" GISAXS = "Grazing-Incidence Small-Angle X-ray Scattering (GISAXS)"
class _RawAndStandardizedValue(IdsElement): """ Base for a field bundling a raw value exactly as reported by the instrument alongside a value translated to a standardized vocabulary -- mirroring how :py:class:`~ts_ids_core.schema.value_unit.RawValueUnit` bundles a raw string directly alongside its parsed value, so the original raw string is never lost even when it isn't one of the standardized values (e.g. a genuinely new mode). Subclasses restate both fields with their own `description`/`example_values`. """ value: Required[NullableString] = IdsField( description="Standardized value, translated from `raw_value`." ) raw_value: Required[NullableString] = IdsField( description="The raw, untransformed value from the primary data." )
[docs] class XrdGeometry(_RawAndStandardizedValue): """ Scan mode/geometry for an XRD measurement. See :py:class:`XrdGeometryMode` for the per-vendor raw-file lookup and translation rules used to populate `value` from `raw_value`. """ value: Required[NullableString] = IdsField( description="Standardized scan mode, translated from `raw_value`.", json_schema_extra={"example_values": [mode.value for mode in XrdGeometryMode]}, ) raw_value: Required[NullableString] = IdsField( description="The raw, untransformed scan mode/geometry value from the primary data." )
[docs] class XrdMeasurementStatusMode(str, Enum): """ Standardized measurement lifecycle state, matching Malvern Panalytical XRDML's own ``statusType`` enumeration (confirmed against ``XRDMeasurement21.xsd``): ``"Completed"`` ("the data has been measured as indicated in the measurement program"), ``"Aborted"`` ("the operator aborted the measurement"), and ``"Not finished"`` ("the measurement was not completed"; also covers files left in an intermediate state by e.g. a power failure). """ COMPLETED = "completed" ABORTED = "aborted" NOT_FINISHED = "not finished"
[docs] class XrdMeasurementStatus(_RawAndStandardizedValue): """ Measurement lifecycle state for an XRD scan. Bruker's raw ``<MeasurementStatus>`` value ``"Measured"`` (from ``RawData0.xml``) normalizes to :py:attr:`XrdMeasurementStatusMode.COMPLETED`; Panalytical XRDML's own ``status`` attribute values pass straight through (lowercased) to the matching `XrdMeasurementStatusMode` member. """ value: Required[NullableString] = IdsField( description="Standardized measurement lifecycle state, translated from `raw_value`.", json_schema_extra={ "example_values": [mode.value for mode in XrdMeasurementStatusMode] }, ) raw_value: Required[NullableString] = IdsField( description=( "The raw, untransformed measurement lifecycle state from the " "primary data." ) )
[docs] class RawValueRawUnit(RawValueUnit): """A `RawValueUnit` that also preserves the unit exactly as reported before normalization (e.g. Bruker/Empyrean's own `Unit`/`unit` attributes, Rigaku's sibling `<XUnit>` elements), independent of whether the parser's normalization to `unit` was correct -- the same rationale that already justifies `raw_value` on `RawValueUnit` applies equally to units: a genuinely different raw string (e.g. Bruker's `"Å"` vs. Empyrean's `"Angstrom"` for the same physical unit) can be mis-normalized or unexpected just as easily as a value can. `unit` is deliberately left as a plain nullable string, not a `Literal` -- the platform convention is that the task script, not the IDS schema, decides what unit string gets written, so a future instrument or configuration reporting a different unit for the same physical quantity doesn't require an IDS schema change. Do not subclass this to fix `unit` to a `Literal` -- that would reintroduce exactly the constraint this design avoids. """ raw_unit: NullableString = IdsField( description="The raw, untransformed unit string from the primary data." )
[docs] class Anode(IdsElement): """ X-ray tube anode (target) material and its characteristic emission wavelengths. The anode material fixes which wavelengths the tube radiates (e.g. a copper anode always radiates Cu K-alpha1/K-alpha2/K-beta), so these are grouped on one class rather than split across sibling fields on `XraySource`. Every vendor reports `k_alpha1`/`k_alpha2`/`k_beta` as explicit values (Bruker's ``WaveLengthAlpha1``/``WaveLengthAlpha2``/``WaveLengthBeta``, Rigaku's ``WavelengthKalpha1``/``WavelengthKalpha2``/``WavelengthKbeta``, and Empyrean's ``usedWavelength/kAlpha1``/``kAlpha2``/``kBeta``). Each uses `RawValueRawUnit` rather than a plain float so the parser always has somewhere to put the raw value/unit strings it read. `k_alpha2_over_k_alpha1_ratio` is reported less consistently: Bruker and Empyrean report it (``WaveLengthRatio``, ``ratioKAlpha2KAlpha1``), but Rigaku doesn't report an equivalent value. """ material: NullableString = IdsField( description="Anode (target) material of the X-ray tube, e.g. 'Cu' for copper." ) k_alpha1: RawValueRawUnit = IdsField( description=( "Wavelength of the K-alpha1 emission line -- the stronger of the " "two K-alpha lines emitted by this anode material." ) ) k_alpha2: RawValueRawUnit = IdsField( description=( "Wavelength of the K-alpha2 emission line -- the weaker of the " "two K-alpha lines emitted by this anode material." ) ) k_alpha2_over_k_alpha1_ratio: RawValueRawUnit = IdsField( description="Relative intensity of the K-alpha2 line to the K-alpha1 line." ) k_beta: RawValueRawUnit = IdsField( description=( "Wavelength of the K-beta emission line, less intense than " "K-alpha and used less often as the primary analysis line." ) )
[docs] class XrdEmissionLine(str, Enum): """ Which of an anode's characteristic emission lines (see `Anode`) a scan's 2-theta axis is reduced against for angle-to-d-spacing conversion (Bragg's law). :py:attr:`K_ALPHA_WEIGHTED` is the intensity-weighted average of the K-alpha1/K-alpha2 doublet, used when the two lines aren't resolved separately (e.g. no monochromator isolating K-alpha1 alone). """ K_ALPHA1 = "k_alpha1" K_ALPHA_WEIGHTED = "k_alpha_weighted" K_ALPHA2 = "k_alpha2" K_BETA = "k_beta"
[docs] class UsedWavelength(_RawAndStandardizedValue): """ Which emission line a scan's 2-theta axis is reduced against, and the corresponding numeric wavelength. Malvern Panalytical Empyrean reports this explicitly and specifically (``usedWavelength``'s ``intended`` attribute, e.g. ``"K-Alpha 1"`` or ``"K-Alpha"`` for the weighted average); Rigaku also reports it explicitly but more coarsely (``WaveType``, e.g. ``"Ka"``, which doesn't distinguish K-alpha1 from the weighted average). Bruker's raw files carry no equivalent field at all -- only the anode's physical emission-line constants (`Anode`), with nothing indicating which one a given scan's reduction actually used; a Bruker task script should leave this unset rather than guess. `wavelength` is carried alongside `value`/`raw_value` rather than looked up on `Anode`, since the K-alpha-weighted case is not any single line stored there. """ value: Required[NullableString] = IdsField( description="Standardized emission line, translated from `raw_value`.", json_schema_extra={"example_values": [line.value for line in XrdEmissionLine]}, ) raw_value: Required[NullableString] = IdsField( description=( "The raw, untransformed used-wavelength indicator from the " "primary data." ) ) wavelength: RawValueRawUnit = IdsField( description=( "Numeric wavelength corresponding to `value`, used for " "2-theta-to-d-spacing conversion." ) )
[docs] class XraySource(IdsElement): """X-ray tube settings common across XRD vendors.""" anode: Anode = IdsField( description="Anode (target) material and its characteristic emission wavelengths." ) voltage: RawValueRawUnit = IdsField( description="Generator voltage applied to the X-ray tube." ) current: RawValueRawUnit = IdsField( description="Generator current applied to the X-ray tube." ) used_wavelength: UsedWavelength = IdsField( description=( "Which anode emission line this scan's 2-theta axis is reduced " "against. Not reported by every vendor -- see `UsedWavelength`." ) )
[docs] class Detector(IdsElement): """X-ray detector used to measure diffracted intensity.""" name: NullableString = IdsField(description="Name or model of the detector.")
[docs] class XrdScan(IdsElement): """ Scan and optics parameters common to an XRD measurement, shared across vendors. """ pk: UUIDPrimaryKey = IdsField( description="Primary key for this scan, referenced by datacubes produced by it." ) geometry: XrdGeometry = IdsField( description="Sample measurement geometry used for this scan." ) measurement_status: XrdMeasurementStatus = IdsField( description="Lifecycle state of this scan's measurement." ) two_theta_start: RawValueRawUnit = IdsField( description="Start of the 2-theta scan range." ) two_theta_stop: RawValueRawUnit = IdsField( description="End of the 2-theta scan range." ) step_size: RawValueRawUnit = IdsField( description="Angular increment between consecutive 2-theta scan points." ) time_per_step: RawValueRawUnit = IdsField( description="Integration (counting) time at each scan step." ) speed: RawValueRawUnit = IdsField( description=( "Scan speed, typically in degrees per minute. Only reported by some " "vendors; for others this is derivable from step_size and " "time_per_step. No QUDT unit for degrees-per-minute has been " "confirmed." ) ) duration: RawValueRawUnit = IdsField(description="Total duration of the scan.") measurement_program: NullableString = IdsField( description=( "Name or path of the instrument method/program file used to run " "this scan (e.g. Bruker's BsmlFileName, Rigaku's PackageName). " "Not reported as a discrete field by every vendor -- e.g. " "Empyrean only carries an equivalent value embedded in a " "free-text comment field -- so this should be left unset where " "no discrete field exists in the raw data." ) ) detector: Detector = IdsField(description="Detector used for this scan.") x_ray_source: XraySource = IdsField( description="X-ray tube settings used for this scan." )