Coverage for nexusLIMS/schemas/metadata.py: 100%
144 statements
« prev ^ index » next coverage.py v7.11.3, created at 2026-03-24 05:23 +0000
« prev ^ index » next coverage.py v7.11.3, created at 2026-03-24 05:23 +0000
1"""
2Type-specific metadata schemas for NexusLIMS extractor plugins.
4This module defines Pydantic models for validating metadata extracted from different
5types of microscopy data (Image, Spectrum, SpectrumImage, Diffraction). Each schema
6uses Pint Quantity objects for physical measurements, EM Glossary field names, and
7supports flexible extension fields.
9The schemas follow a hierarchical structure:
10- :class:`NexusMetadata` - Base schema with common fields
11- :class:`ImageMetadata` - SEM/TEM/STEM image data
12- :class:`SpectrumMetadata` - EDS/EELS spectral data
13- :class:`SpectrumImageMetadata` - Hyperspectral data (inherits from both)
14- :class:`DiffractionMetadata` - Diffraction pattern data
16**Key Features:**
17- Pint Quantity fields for machine-actionable units
18- EM Glossary v2.0.0 terminology (see :doc:`/dev_guide/em_glossary_reference`)
19- Automatic unit normalization to preferred units
20- Flexible extensions section for instrument-specific metadata
21- Strict validation of core fields
23Examples
24--------
25Validate SEM image metadata:
27>>> from nexusLIMS.schemas.metadata import ImageMetadata
28>>> from nexusLIMS.schemas.units import ureg
29>>>
30>>> meta = ImageMetadata(
31... creation_time="2024-01-15T10:30:00-05:00",
32... data_type="SEM_Imaging",
33... dataset_type="Image",
34... acceleration_voltage=ureg.Quantity(10, "kilovolt"),
35... working_distance=ureg.Quantity(10, "millimeter"),
36... beam_current=ureg.Quantity(100, "picoampere"),
37... )
38>>> print(meta.acceleration_voltage)
3910.0 kilovolt
41Validate spectrum metadata:
43>>> from nexusLIMS.schemas.metadata import SpectrumMetadata
44>>>
45>>> spec_meta = SpectrumMetadata(
46... creation_time="2024-01-15T10:30:00-05:00",
47... data_type="EDS_Spectrum",
48... dataset_type="Spectrum",
49... acquisition_time=ureg.Quantity(30, "second"),
50... live_time=ureg.Quantity(28.5, "second"),
51... )
53Use extensions for instrument-specific fields:
55>>> meta_with_ext = ImageMetadata(
56... creation_time="2024-01-15T10:30:00-05:00",
57... data_type="SEM_Imaging",
58... dataset_type="Image",
59... extensions={
60... "facility": "Nexus Microscopy Center",
61... "detector_brightness": 50.0,
62... "scan_speed": 6,
63... }
64... )
66For detailed documentation on the metadata schema system, see:
68- :doc:`/dev_guide/nexuslims_internal_schema` - Schema architecture,
69 migration guide, and usage examples
70- :doc:`/dev_guide/em_glossary_reference` - Complete EM Glossary field
71 reference and mapping tables
72"""
74import logging
75from datetime import datetime
76from typing import Any, Dict, Literal
78from pydantic import BaseModel, Field, field_validator, model_validator
80from nexusLIMS.schemas import em_glossary
81from nexusLIMS.schemas.pint_types import PintQuantity
83_logger = logging.getLogger(__name__)
86def emg_field(
87 field_name: str,
88 default: Any = None,
89 *,
90 description: str | None = None,
91 **kwargs: Any,
92) -> Any:
93 """
94 Create a Pydantic Field with EM Glossary metadata.
96 This helper automatically adds EM Glossary semantic annotations to field
97 definitions, including EMG ID, URI, and label. It pulls metadata from the
98 :mod:`~nexusLIMS.schemas.em_glossary` module to maintain a single source
99 of truth.
101 Parameters
102 ----------
103 field_name : str
104 Internal field name (e.g., "acceleration_voltage"). Used to look up
105 EMG metadata from :mod:`~nexusLIMS.schemas.em_glossary` module.
107 default : Any, optional
108 Default value for the field. Use `...` for required fields, `None`
109 for optional fields.
111 description : str, optional
112 Field description. If not provided, uses description from
113 :mod:`~nexusLIMS.schemas.em_glossary` module.
115 **kwargs : Any
116 Additional keyword arguments passed to :func:`pydantic.fields.Field`, such as:
117 - `alias`: Override display name (default from em_glossary)
118 - `gt`, `ge`, `lt`, `le`: Numeric constraints
119 - `examples`: Example values for documentation
120 - `json_schema_extra`: Additional JSON schema metadata (merged with EMG data)
122 Returns
123 -------
124 :class:`pydantic.fields.FieldInfo`
125 Configured Pydantic field with EMG metadata
127 Examples
128 --------
129 Create a field with automatic EMG metadata:
131 >>> from nexusLIMS.schemas.metadata import emg_field
132 >>> from nexusLIMS.schemas.pint_types import PintQuantity
133 >>>
134 >>> class MySchema(BaseModel):
135 ... dwell_time: PintQuantity | None = emg_field("dwell_time")
137 The field automatically gets:
138 - `alias`: "Dwell Time" (display name)
139 - `description`: "Time period during which the beam remains at one position."
140 - `json_schema_extra`: `{"emg_id": "EMG_00000015", "emg_uri": "...", ...}`
142 Override description:
144 >>> acceleration_voltage: PintQuantity | None = emg_field(
145 ... "dwell_time",
146 ... description="Custom description",
147 ... )
149 Add additional JSON schema metadata:
151 >>> beam_current: PintQuantity | None = emg_field(
152 ... "beam_current",
153 ... json_schema_extra={"units": "second", "typical_range": "1e-3 to 1"},
154 ... )
156 Notes
157 -----
158 - Fields without EMG mappings still get display names and descriptions
159 - EMG metadata is only added if the field has a valid EMG ID
160 - The alias (display name) comes from em_glossary for consistency
161 - All EMG metadata is stored in json_schema_extra for JSON schema export
162 """
163 # Get EMG metadata
164 emg_id = em_glossary.get_emg_id(field_name)
165 emg_uri = em_glossary.get_emg_uri(field_name)
166 display_name = kwargs.pop("alias", None) or em_glossary.get_display_name(field_name)
167 field_description = description or em_glossary.get_description(field_name)
169 # Build json_schema_extra with EMG metadata
170 extra = kwargs.pop("json_schema_extra", {})
171 if emg_id:
172 emg_label = em_glossary.get_emg_label(emg_id)
173 extra.update(
174 {
175 "emg_id": emg_id,
176 "emg_uri": emg_uri,
177 "emg_label": emg_label,
178 }
179 )
181 return Field(
182 default,
183 alias=display_name,
184 description=field_description,
185 json_schema_extra=extra if extra else None,
186 **kwargs,
187 )
190class ExtractionDetails(BaseModel):
191 """
192 Metadata about the NexusLIMS extraction process.
194 Records when metadata was extracted, which extractor module was used,
195 and the NexusLIMS version.
196 """
198 date: str = Field(
199 ...,
200 alias="Date",
201 description="ISO-8601 timestamp when extraction occurred",
202 )
203 """ISO-8601 formatted timestamp with timezone indicating when the
204 metadata extraction occurred."""
206 module: str = Field(
207 ...,
208 alias="Module",
209 description="Extractor module name",
210 )
211 """Fully qualified Python module name of the extractor that processed
212 this file. Examples: `'nexusLIMS.extractors.plugins.digital_micrograph'`,
213 `'nexusLIMS.extractors.plugins.quanta_tif'`"""
215 version: str = Field(
216 ...,
217 alias="Version",
218 description="NexusLIMS version",
219 )
220 """NexusLIMS version string used for extraction. Example: `'1.2.3'`"""
222 extractor_warnings: str | None = Field(
223 None,
224 alias="Extractor Warnings",
225 description="Warning or error messages from the extraction process",
226 )
227 """Warning or error messages from the extraction process"""
229 model_config: dict = {
230 "populate_by_name": True,
231 }
232 """
233 Pydantic model configuration:
235 - ``populate_by_name: True`` -- Accept both Python field names and JSON aliases
236 """
239class StagePosition(BaseModel):
240 """
241 Stage position with coordinates and tilt angles.
243 Represents the physical position and orientation of the microscope stage.
244 All fields use Pint Quantity objects with appropriate units and are optional
245 to accommodate different stage configurations.
247 Examples
248 --------
249 >>> from nexusLIMS.schemas.metadata import StagePosition
250 >>> from nexusLIMS.schemas.units import ureg
251 >>>
252 >>> pos = StagePosition(
253 ... x=ureg.Quantity(100, "um"),
254 ... y=ureg.Quantity(200, "um"),
255 ... z=ureg.Quantity(5, "mm"),
256 ... tilt_alpha=ureg.Quantity(10, "degree"),
257 ... )
258 >>> print(pos.x)
259 100 micrometer
261 Notes
262 -----
263 Some microscopes may not have all degrees of freedom. Single-tilt stages
264 will only have tilt_alpha, while dual-tilt stages (e.g., tomography holders)
265 will have both tilt_alpha and tilt_beta.
266 """
268 x: PintQuantity | None = Field(
269 None,
270 description="Stage X coordinate",
271 )
272 """Stage X coordinate. Preferred unit: micrometer (µm)"""
274 y: PintQuantity | None = Field(
275 None,
276 description="Stage Y coordinate",
277 )
278 """Stage Y coordinate. Preferred unit: micrometer (µm)"""
280 z: PintQuantity | None = Field(
281 None,
282 description="Stage Z coordinate (height)",
283 )
284 """Stage Z coordinate (height). Preferred unit: millimeter (mm)"""
286 rotation: PintQuantity | None = Field(
287 None,
288 description="Stage rotation angle around Z axis",
289 )
290 """Stage rotation angle around Z axis. Preferred unit: degree (°)"""
292 tilt_alpha: PintQuantity | None = Field(
293 None,
294 description="Tilt angle along primary tilt axis (alpha)",
295 )
296 """
297 Tilt angle along the stage's primary tilt axis (alpha).
298 Preferred unit: degree (°)
299 """
301 tilt_beta: PintQuantity | None = Field(
302 None,
303 description="Tilt angle along secondary tilt axis (beta), if capable",
304 )
305 """
306 Tilt angle along the stage's secondary tilt axis (beta), if the stage
307 is capable of dual-axis tilting. Preferred unit: degree (°)
308 """
310 model_config = {
311 "extra": "allow", # Allow additional vendor-specific coordinates
312 }
313 """
314 Pydantic model configuration:
316 - ``extra: "allow"`` -- Allow additional vendor-specific stage positions
317 """
320class NexusMetadata(BaseModel):
321 """
322 Base schema for all NexusLIMS metadata.
324 This is the foundation schema that all type-specific schemas inherit from.
325 It defines the required fields common to all dataset types and provides
326 the extension mechanism for instrument-specific metadata.
328 Notes
329 -----
330 The extensions section allows arbitrary metadata while maintaining strict
331 validation on core fields. This hybrid approach ensures:
333 - Core fields are consistent and validated
334 - Instrument-specific metadata is preserved
335 - No data loss during extraction
337 Extensions should use descriptive key names and avoid conflicts with core
338 field names.
339 """
341 # Required fields (common to all types)
342 creation_time: str = Field(
343 ...,
344 alias="Creation Time",
345 description="ISO-8601 timestamp with timezone",
346 )
347 """
348 ISO-8601 formatted timestamp with timezone indicating when the data
349 was acquired. Must include timezone offset (+00:00, -05:00) or 'Z'.
350 Examples: "2024-01-15T10:30:00-05:00", "2024-01-15T15:30:00Z"
351 """
353 data_type: str = Field(
354 ...,
355 alias="Data Type",
356 description="Human-readable data type description",
357 )
358 """
359 Human-readable description of the data type using underscore-separated
360 components. Examples: "STEM_Imaging", "TEM_EDS", "SEM_Imaging"
361 """
363 dataset_type: Literal[
364 "Image",
365 "Spectrum",
366 "SpectrumImage",
367 "Diffraction",
368 "Misc",
369 "Unknown",
370 ] = Field(
371 ...,
372 alias="DatasetType",
373 description="Schema-defined dataset category",
374 )
375 """
376 Schema-defined category matching the Nexus Experiment XML schema type
377 attribute.
378 """
380 # Common optional fields
381 data_dimensions: str | None = Field(
382 None,
383 alias="Data Dimensions",
384 description="String representation of data shape",
385 )
386 """
387 String representation of data shape as a tuple.
388 Examples: "(1024, 1024)", "(2048,)", "(12, 1024, 1024)"
389 """
391 instrument_id: str | None = Field(
392 None,
393 alias="Instrument ID",
394 description="NexusLIMS persistent instrument identifier",
395 )
396 """
397 NexusLIMS persistent identifier for the instrument.
398 Examples: "FEI-Titan-TEM-635816", "Quanta-FEG-650-SEM-555555"
399 """
401 warnings: list[str | list[str]] = Field(
402 default_factory=list,
403 description="Field names flagged as unreliable",
404 )
405 """
406 Field names flagged as unreliable. These are marked with warning="true"
407 in the XML output.
408 """
410 nexuslims_extraction: ExtractionDetails | None = Field(
411 None,
412 alias="NexusLIMS Extraction",
413 description="NexusLIMS extraction metadata (date, module, version, warnings)",
414 )
415 """
416 NexusLIMS extraction metadata containing date, module, and version
417 information about when and how the metadata was extracted.
418 """
420 extensions: Dict[str, Any] = Field(
421 default_factory=dict,
422 description="Instrument-specific metadata extensions",
423 )
424 """
425 Flexible container for instrument-specific metadata that doesn't fit
426 the core schema. Use this for vendor-specific fields, facility metadata,
427 or experimental parameters not covered by EM Glossary.
428 """
430 # Configuration
431 model_config = {
432 "populate_by_name": True, # Accept both Python names and aliases
433 "extra": "forbid", # Force use of extensions for extra fields
434 }
435 """
436 Pydantic model configuration:
438 - ``populate_by_name: True`` -- Accept both Python field names and JSON aliases
439 - ``extra: "forbid"`` -- Forbid extra fields (forces use of extensions dict
440 for additional data)
441 """
443 @field_validator("creation_time")
444 @classmethod
445 def validate_iso_timestamp(cls, v: str) -> str:
446 """Validate ISO-8601 timestamp with timezone."""
447 try:
448 datetime.fromisoformat(v)
449 except ValueError as e:
450 msg = f"Invalid ISO-8601 timestamp format: {v}"
451 raise ValueError(msg) from e
453 # Require timezone information
454 min_dashes_for_tz = 3
455 if not ("+" in v or v.endswith("Z") or v.count("-") >= min_dashes_for_tz):
456 msg = (
457 f"Timestamp must include timezone: {v}. "
458 f"Use format like '2024-01-15T10:30:00-05:00' or '...Z'"
459 )
460 raise ValueError(msg)
462 return v
464 @field_validator("data_type")
465 @classmethod
466 def validate_data_type_not_empty(cls, v: str) -> str:
467 """Validate ``data_type`` is not empty."""
468 if not v or not v.strip():
469 msg = "Data Type cannot be empty"
470 raise ValueError(msg)
471 return v
474class ImageMetadata(NexusMetadata):
475 """
476 Schema for image dataset metadata (SEM, TEM, STEM, FIB, HIM).
478 Extends :class:`NexusMetadata` with fields specific to 2D image acquisition.
479 Uses Pint Quantity objects for all physical measurements.
481 Examples
482 --------
483 >>> from nexusLIMS.schemas.metadata import ImageMetadata
484 >>> from nexusLIMS.schemas.units import ureg
485 >>>
486 >>> meta = ImageMetadata(
487 ... creation_time="2024-01-15T10:30:00-05:00",
488 ... data_type="SEM_Imaging",
489 ... dataset_type="Image",
490 ... acceleration_voltage=ureg.Quantity(15, "kV"),
491 ... working_distance=ureg.Quantity(10.5, "mm"),
492 ... beam_current=ureg.Quantity(50, "pA"),
493 ... magnification=5000.0,
494 ... )
495 """
497 dataset_type: Literal["Image"] = Field(
498 "Image",
499 alias="DatasetType",
500 description="Must be 'Image' for this schema",
501 )
503 # Image-specific fields (using EM Glossary names)
504 acceleration_voltage: PintQuantity | None = emg_field("acceleration_voltage")
505 """
506 Accelerating voltage of the electron/ion beam.
507 Preferred unit: kilovolt (kV). EM Glossary: EMG_00000004
508 """
510 working_distance: PintQuantity | None = emg_field("working_distance")
511 """
512 Distance between final lens and sample surface.
513 Preferred unit: millimeter (mm). EM Glossary: EMG_00000050
514 """
516 beam_current: PintQuantity | None = emg_field("beam_current")
517 """
518 Electron beam current.
519 Preferred unit: picoampere (pA). EM Glossary: EMG_00000006
520 """
522 emission_current: PintQuantity | None = emg_field("emission_current")
523 """
524 Emission current from electron source.
525 Preferred unit: microampere (µA). EM Glossary: EMG_00000025
526 """
528 dwell_time: PintQuantity | None = emg_field("dwell_time")
529 """
530 Time the beam dwells on each pixel during scanning.
531 Preferred unit: microsecond (µs). EM Glossary: EMG_00000015
532 """
534 magnification: float | None = emg_field("magnification")
535 """Nominal magnification (dimensionless)."""
537 horizontal_field_width: PintQuantity | None = emg_field("horizontal_field_width")
538 """Width of the scanned area. Preferred unit: micrometer (µm)"""
540 vertical_field_width: PintQuantity | None = emg_field("vertical_field_width")
541 """Height of the scanned area. Preferred unit: micrometer (µm)"""
543 pixel_width: PintQuantity | None = emg_field("pixel_width")
544 """Physical width of a single pixel. Preferred unit: nanometer (nm)"""
546 pixel_height: PintQuantity | None = emg_field("pixel_height")
547 """Physical height of a single pixel. Preferred unit: nanometer (nm)"""
549 scan_rotation: PintQuantity | None = emg_field("scan_rotation")
550 """Rotation angle of the scan frame. Preferred unit: degree (°)"""
552 detector_type: str | None = emg_field("detector_type")
553 """Type or name of detector used. Examples: "ETD", "InLens", "HAADF", "BF" """
555 acquisition_device: str | None = emg_field("acquisition_device")
556 """
557 Name of the acquisition device or camera.
558 Examples: "BM-UltraScan", "K2 Summit"
559 """
561 stage_position: StagePosition | None = Field(
562 None,
563 description="Stage coordinates and tilt angles",
564 )
565 """
566 Stage coordinates and tilt angles. See :class:`StagePosition` for details.
567 Preferred units: x/y in µm, z in mm, angles in degrees
568 """
571class SpectrumMetadata(NexusMetadata):
572 """
573 Schema for spectrum dataset metadata (EDS, EELS, etc.).
575 Extends :class:`NexusMetadata` with fields specific to spectral data acquisition.
577 Examples
578 --------
579 >>> from nexusLIMS.schemas.metadata import SpectrumMetadata
580 >>> from nexusLIMS.schemas.units import ureg
581 >>>
582 >>> meta = SpectrumMetadata(
583 ... creation_time="2024-01-15T10:30:00-05:00",
584 ... data_type="EDS_Spectrum",
585 ... dataset_type="Spectrum",
586 ... acquisition_time=ureg.Quantity(30, "s"),
587 ... live_time=ureg.Quantity(28.5, "s"),
588 ... channel_size=ureg.Quantity(10, "eV"),
589 ... )
590 """
592 dataset_type: Literal["Spectrum"] = Field(
593 "Spectrum",
594 alias="DatasetType",
595 description="Must be 'Spectrum' for this schema",
596 )
598 # Spectrum-specific fields
599 acquisition_time: PintQuantity | None = emg_field("acquisition_time")
600 """
601 Total time for spectrum acquisition.
602 Preferred unit: second (s). EM Glossary: EMG_00000055
603 """
605 live_time: PintQuantity | None = emg_field("live_time")
606 """Live time excluding dead time. Preferred unit: second (s)"""
608 detector_energy_resolution: PintQuantity | None = emg_field(
609 "detector_energy_resolution"
610 )
611 """Energy resolution of the detector. Preferred unit: electronvolt (eV)"""
613 channel_size: PintQuantity | None = emg_field("channel_size")
614 """Energy width of each channel. Preferred unit: electronvolt (eV)"""
616 starting_energy: PintQuantity | None = emg_field("starting_energy")
617 """Starting energy of the spectrum. Preferred unit: kiloelectronvolt (keV)"""
619 azimuthal_angle: PintQuantity | None = emg_field("azimuthal_angle")
620 """Azimuthal angle of the detector. Preferred unit: degree (°)"""
622 elevation_angle: PintQuantity | None = emg_field("elevation_angle")
623 """Elevation angle of the detector. Preferred unit: degree (°)"""
625 takeoff_angle: PintQuantity | None = emg_field("takeoff_angle")
626 """X-ray takeoff angle. Preferred unit: degree (°)"""
628 elements: list[str] | None = Field(
629 None,
630 description="Detected elements",
631 )
632 """Detected elements (e.g., ["Fe", "Cr", "Ni"])"""
635class SpectrumImageMetadata(ImageMetadata, SpectrumMetadata):
636 """
637 Schema for spectrum image (hyperspectral) dataset metadata.
639 Combines fields from both :class:`ImageMetadata` and :class:`SpectrumMetadata`
640 since spectrum images have both spatial and spectral dimensions. Inherits all
641 fields from both parent classes.
643 Examples
644 --------
645 >>> from nexusLIMS.schemas.metadata import SpectrumImageMetadata
646 >>> from nexusLIMS.schemas.units import ureg
647 >>>
648 >>> meta = SpectrumImageMetadata(
649 ... creation_time="2024-01-15T10:30:00-05:00",
650 ... data_type="STEM_EDS_SpectrumImage",
651 ... dataset_type="SpectrumImage",
652 ... acceleration_voltage=ureg.Quantity(200, "kV"), # Image field
653 ... acquisition_time=ureg.Quantity(1200, "s"), # Spectrum field
654 ... pixel_time=ureg.Quantity(0.5, "s"), # SpectrumImage specific
655 ... )
656 """
658 dataset_type: Literal["SpectrumImage"] = Field(
659 "SpectrumImage",
660 alias="DatasetType",
661 description="Must be 'SpectrumImage' for this schema",
662 )
664 # SpectrumImage-specific fields
665 pixel_time: PintQuantity | None = Field(
666 None,
667 description="Time per pixel for spectrum acquisition",
668 )
669 """Time spent acquiring spectrum at each pixel. Preferred unit: second (s)"""
671 scan_mode: str | None = Field(
672 None,
673 description="Scanning mode (raster, serpentine, etc.)",
674 )
675 """
676 Scanning mode used for acquisition.
677 Examples: "raster", "serpentine", "fly-back"
678 """
680 @model_validator(mode="after")
681 def validate_spectrum_image_fields(self) -> "SpectrumImageMetadata":
682 """Ensure SpectrumImage has both image and spectrum metadata."""
683 # Just a placeholder - could add validation logic here if needed
684 return self
687class DiffractionMetadata(NexusMetadata):
688 """
689 Schema for diffraction pattern dataset metadata (TEM, EBSD, etc.).
691 Extends :class:`NexusMetadata` with fields specific to diffraction data.
693 Examples
694 --------
695 >>> from nexusLIMS.schemas.metadata import DiffractionMetadata
696 >>> from nexusLIMS.schemas.units import ureg
697 >>>
698 >>> meta = DiffractionMetadata(
699 ... creation_time="2024-01-15T10:30:00-05:00",
700 ... data_type="TEM_Diffraction",
701 ... dataset_type="Diffraction",
702 ... camera_length=ureg.Quantity(200, "mm"),
703 ... convergence_angle=ureg.Quantity(0.5, "mrad"),
704 ... acceleration_voltage=ureg.Quantity(200, "kV"),
705 ... )
706 """
708 dataset_type: Literal["Diffraction"] = Field(
709 "Diffraction",
710 alias="DatasetType",
711 description="Must be 'Diffraction' for this schema",
712 )
714 # Diffraction-specific fields
715 camera_length: PintQuantity | None = emg_field("camera_length")
716 """
717 Camera length for diffraction pattern.
718 Preferred unit: millimeter (mm). EM Glossary: EMG_00000008
719 """
721 convergence_angle: PintQuantity | None = emg_field("convergence_angle")
722 """
723 Convergence angle of the electron beam.
724 Preferred unit: milliradian (mrad). EM Glossary: EMG_00000010
725 """
727 acceleration_voltage: PintQuantity | None = emg_field("acceleration_voltage")
728 """
729 Accelerating voltage (also relevant for diffraction).
730 Preferred unit: kilovolt (kV). EM Glossary: EMG_00000004
731 """
733 acquisition_device: str | None = emg_field("acquisition_device")
734 """Name of the detector/camera used."""