"""
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."
)