Source code for ts_ids_components.xrd.datacube_raw

"""
Raw two-theta scan datacube preservation for XRD IDSs.

:py:class:`TwoThetaScanDatacube <ts_ids_components.xrd.datacube.TwoThetaScanDatacube>`
stores parsed, structured per-point data -- but converting a raw string to a
float is lossy by construction: ``float("40.0000") == 40.0``, silently
dropping trailing zeros that reflect the instrument's actual reported
precision, not incidental formatting. :py:class:`RawTwoThetaScanDatacube` is
an exact structural replica of `TwoThetaScanDatacube` -- the same one
``two_theta`` dimension plus per-vendor named measures -- built from
:py:class:`RawXrdDimension`/:py:class:`RawXrdMeasure` instead of
:py:class:`Dimension <ts_ids_core.schema.dimension.Dimension>`/
:py:class:`XrdMeasure <ts_ids_components.xrd.datacube.XrdMeasure>`: every
``scale``/``value`` entry is the exact original raw string (not a parsed
float), and every ``unit`` is the exact original raw unit string (not the
canonicalized unit name used on the parsed side), so the original can always
be recovered independent of parser correctness, point-for-point and
axis-for-axis.

Like `TwoThetaScanDatacube`, each entry also carries its own ``fk_method``/
``fk_run``/``fk_system``/``fk_sample`` rather than relying on matching list
order with ``datacubes``, since matching by list order is fragile the
moment a file has more than one scan. On top of that,
:py:class:`RawTwoThetaScanDatacube` carries an ``fk_datacube`` pointing
directly at the `TwoThetaScanDatacube` it was parsed from, so translating
between a parsed datacube and its raw counterpart is a direct lookup rather
than a join on the shared ``(fk_method, fk_run)`` pair -- the same reasoning
that motivates bundling a raw string directly alongside its parsed value on
:py:class:`~ts_ids_core.schema.value_unit.RawValueUnit`. Consuming IDSs
declare ``datacubes_raw: List[RawTwoThetaScanDatacube]`` as a top-level
array; each vendor's own subclass restates ``measures`` with the same total
``fixed_length`` as its corresponding `TwoThetaScanDatacube` subclass.
"""

from typing import List

from pydantic import model_validator
from ts_ids_core.annotations import (
    NullableString,
    Required,
    UUIDForeignKey,
    fixed_length,
)
from ts_ids_core.base.ids_element import IdsElement
from ts_ids_core.base.ids_field import IdsField
from typing_extensions import Annotated

from ts_ids_components.xrd.datacube import (
    _require_two_theta_dimension_and_intensity_measure,
)


[docs] class RawXrdDimension(IdsElement): """ A dimension of a :py:class:`RawTwoThetaScanDatacube`. Structurally identical to :py:class:`Dimension <ts_ids_core.schema.dimension.Dimension>` (name, unit, scale), except `scale` holds the exact original raw strings a datacube's dimension was parsed from (not parsed floats), and `unit` holds the exact original raw unit string (not the canonicalized unit name used on `TwoThetaScanDatacube <ts_ids_components.xrd.datacube.TwoThetaScanDatacube>`'s own dimension). """ name: Required[NullableString] = IdsField( description=( "Name of the dimension, matching the corresponding `TwoThetaScanDatacube` " "dimension by name (e.g. 'two_theta')." ) ) unit: Required[NullableString] = IdsField( description="Unit exactly as it appears in the raw file, unconverted." ) scale: Required[List[NullableString]] = IdsField( description=( "Dimension scale values exactly as they appear in the raw file, " "unconverted." ) )
[docs] class RawXrdMeasure(IdsElement): """ A measure of a :py:class:`RawTwoThetaScanDatacube`. Structurally identical to :py:class:`XrdMeasure <ts_ids_components.xrd.datacube.XrdMeasure>` (name, unit, value), except `value` holds the exact original raw strings a measure was parsed from (not parsed floats), and `unit` holds the exact original raw unit string (not the canonicalized unit name used on `XrdMeasure`). """ name: Required[NullableString] = IdsField( description=( "Name of the measure, matching the corresponding `XrdMeasure` by " "name (e.g. 'intensity')." ) ) unit: Required[NullableString] = IdsField( description="Unit exactly as it appears in the raw file, unconverted." ) value: Required[List[NullableString]] = IdsField( description=( "Per-point measure values exactly as they appear in the raw " "file, unconverted." ) )
[docs] class RawTwoThetaScanDatacube(IdsElement): """ An exact structural replica of `TwoThetaScanDatacube <ts_ids_components.xrd.datacube.TwoThetaScanDatacube>` -- the same one `two_theta` dimension plus per-vendor named measures -- built from :py:class:`RawXrdDimension`/:py:class:`RawXrdMeasure` instead of :py:class:`Dimension <ts_ids_core.schema.dimension.Dimension>`/ `XrdMeasure`, so every `scale`/`value` entry is the exact original raw string (not a parsed float) and every `unit` is the exact original raw unit string (not the canonicalized unit name). See the module docstring above for why this exists. Linked to its source scan/run/system/sample by `fk_method`/`fk_run`/`fk_system`/`fk_sample` (rather than by matching list order with `datacubes`), and directly to its parsed counterpart by `fk_datacube`. """ fk_method: UUIDForeignKey = IdsField( primary_key="/properties/methods/items/properties/pk", description="Foreign key to the scan (in `methods`) this raw data was parsed from.", ) fk_run: UUIDForeignKey = IdsField( primary_key="/properties/runs/items/properties/pk", description="Foreign key to the run (in `runs`) this raw data was parsed from.", ) fk_system: UUIDForeignKey = IdsField( primary_key="/properties/systems/items/properties/pk", description="Foreign key to the system (in `systems`) this raw data was parsed from.", ) fk_sample: UUIDForeignKey = IdsField( primary_key="/properties/samples/items/properties/pk", description="Foreign key to the sample (in `samples`) this raw data was measured on.", ) fk_datacube: UUIDForeignKey = IdsField( primary_key="/properties/datacubes/items/properties/pk", description="Foreign key to the parsed datacube (in `datacubes`) this raw data was parsed from.", ) name: Required[str] description: NullableString = IdsField(default=None) dimensions: Required[Annotated[List[RawXrdDimension], fixed_length(1)]] measures: Required[Annotated[List[RawXrdMeasure], fixed_length(1)]]
[docs] @model_validator(mode="after") def two_theta_dimension_and_intensity_measure_present( self, ) -> "RawTwoThetaScanDatacube": """Mirrors `TwoThetaScanDatacube`'s own validator of the same name: the one dimension must be `two_theta`, and `intensity` must be present as a measure, by name.""" _require_two_theta_dimension_and_intensity_measure( self.dimensions[0].name, [measure.name for measure in self.measures], type(self).__name__, ) return self
[docs] @model_validator(mode="after") def consistent_scale_and_value_lengths(self) -> "RawTwoThetaScanDatacube": """Mirrors `ts_ids_core.schema.DataCube.consistent_number_of_dimensions`, which `RawTwoThetaScanDatacube` doesn't inherit (it isn't a `DataCube` subclass, since its fields are string-typed rather than float-typed): every measure's `value` must have as many entries as the dimension's `scale`, so a raw parser bug producing a truncated or misaligned per-point array is caught at validation time rather than silently stored as valid IDS data.""" expected_length = len(self.dimensions[0].scale) for measure in self.measures: if len(measure.value) != expected_length: raise ValueError( f"{type(self).__name__} measure '{measure.name}' has " f"{len(measure.value)} value(s), expected {expected_length} " "to match dimensions[0].scale" ) return self