# This file was generated by the "yardl" tool. DO NOT EDIT.

# pyright: reportUnusedImport=false
# pyright: reportUnknownArgumentType=false
# pyright: reportUnknownMemberType=false
# pyright: reportUnknownVariableType=false

import datetime
import enum
import types
import typing

import numpy as np
import numpy.typing as npt

from . import yardl_types as yardl
from . import _dtypes


Shape = typing.TypeVar("Shape")
Shape_NP = typing.TypeVar("Shape_NP", bound=np.generic)
T = typing.TypeVar("T")
T_NP = typing.TypeVar("T_NP", bound=np.generic)


SGID = yardl.UInt32
"""Symmetry Group Identifier Type
Most PET scanners have some kind of geometric symmetry, e.g. rotation over a full
module, or translation along the axis of the scanner. Module-pairs that are related
by such a symmetry often have the same geometric detection efficiencies. PETSIRD
calls this a "symmetry group" (SG). Each SG had a unique identifier (SGID).
SGIDs are used to efficiently encode module-pair efficiencies etc.
"""


DetectionBinEfficiencies = npt.NDArray[np.float32]
"""Detection efficiencies for every DetectionBin if a particular type of module
Constraint: size == number of all possible unique DetectionBins
"""


class ModulePairEfficiencies:
    """Detection efficiency for two detection bins in a pair of modules.
    This is one component (often called "geometric") of the detection efficiency model.
    Note that "detection bin" includes energy windows (if any) for the modules.
    """

    values: npt.NDArray[np.float32]
    """Detection efficiency for a pair of detection bins
    detectionBin1 and detectionBin2 run from 0 up to the number of detections_bins in each module,
    """

    sgid: SGID
    """Symmetry Group Identifier (SGID)
    This should be a number between 0 and numberOfSGIDs-1
    """


    def __init__(self, *,
        values: typing.Optional[npt.NDArray[np.float32]] = None,
        sgid: SGID = 0,
    ):
        self.values = values if values is not None else np.zeros((0, 0), dtype=np.dtype(np.float32))
        self.sgid = sgid

    def __eq__(self, other: object) -> bool:
        return (
            isinstance(other, ModulePairEfficiencies)
            and yardl.structural_equal(self.values, other.values)
            and self.sgid == other.sgid
        )

    def __str__(self) -> str:
        return f"ModulePairEfficiencies(values={self.values}, sgid={self.sgid})"

    def __repr__(self) -> str:
        return f"ModulePairEfficiencies(values={repr(self.values)}, sgid={repr(self.sgid)})"


ModulePairSGIDLUT = npt.NDArray[np.int32]
"""Lookup table for SGIDs
For every module pair (of specific types), give the SGID. If -1, the module-pair is not in coincidence.
Values run from -1 ... (numberOfSGIDs-1)
"""


ModulePairEfficienciesVector = list[ModulePairEfficiencies]
"""List of ModulePairEfficiencies, one for each SGID"""


class DetectionEfficiencies:
    """Component-based information on detection efficiencies
    This encodes a simple model for the detection efficiency of (true) coincidences
    consisting of the product of the efficiency of the two detection bins
    and a (geometric) component determined by their location in the two modules.
    The former are stored in detectionBinEfficiencies, and the latter in modulePairEfficienciesVector
    (a list of ModulePairEfficiencies, each entry corresponding to a module pair).

    To save memory, the modulePairEfficienciesVector contains only one element for each SGID.
    The SGID for a module-pair can be found in modulePairSGIDLUT.

    Finding the total detection efficiency therefore follows these steps in pseudo-code
    0. obtain module-types
    1. find module_index for each detection_bin
    2. find detection_bin "inside" each module
    3. SGID = modulePairSGIDLUT[type_of_module1][type_of_module2][module_index1, module_index2]
    4. if (SGID < 0) return 0
    5. module_pair_efficiencies = modulePairEfficienciesVector[type_of_module1][type_of_module2][SGID]
    6. return calibrationFactor
          * detectionBinEfficiencies[type_of_module1](detection_bin1) * detectionBinEfficiencies[type_of_module2](detection_bin2)
          * module_pair_efficiencies[detection_bin_in_module1, detection_bin_in_module2]

    If either of the components is not present, its value is considered to be 1.

    The calibrationFactor is defined in terms of measured/expected "true unscattered" coincidences.
    Moreover, it is (in principle) defined in terms of an idealised measurement of large uniform source.
    Therefore, "expected" counts should take into account the
    - activity (as estimated via a dose calibrator)
    - positron emission branching fraction
    - detectionBinEfficiencies and modulePairEfficiencies
    - the line integral (in millimeter) through the source
    This definition has the effect that for a given pair of detection_bins, we should have
      mean_precorrected_true_coincidences =
        detection_effiency * line_integral(number_of_positron_annihilations_in_source_as_estimated_from_dose_calibrator)
      (with the precorrection taking into scatter and attenuation)

    Note that computing a detection efficiency for a triple coincidence is left to the user.
    """

    method_description: str
    """Text field describing how these efficiencies were determined"""

    calibration_factor: yardl.Float32
    """Calibration factor for "absolute" efficiency"""

    detection_bin_efficiencies: typing.Optional[list[DetectionBinEfficiencies]]
    """List of detection efficiencies for every detection bin (one for each module-type).
    Constraint: size(detectionBinEfficiencies) == ScannerGeometry.numberOfModuleTypes()
    """

    module_pair_sgidlut: typing.Optional[list[list[ModulePairSGIDLUT]]]
    """Nested list of lookup tables for SGIDs, one for each module-type pair.
    Also indicates if coincidences between a module-pair are recorded.
    """

    module_pair_efficiencies_vectors: typing.Optional[list[list[ModulePairEfficienciesVector]]]
    """Nested list of all modulePairEfficienciesVectors (one for each module-type pair)
    Constraint: size(modulePairEfficienciesVectors[type_of_module1][type_of_module2]) == max(modulePairSGIDLUT[type_of_module1][type_of_module2]) + 1
    """


    def __init__(self, *,
        method_description: str = "",
        calibration_factor: yardl.Float32 = 0.0,
        detection_bin_efficiencies: typing.Optional[list[DetectionBinEfficiencies]] = None,
        module_pair_sgidlut: typing.Optional[list[list[ModulePairSGIDLUT]]] = None,
        module_pair_efficiencies_vectors: typing.Optional[list[list[ModulePairEfficienciesVector]]] = None,
    ):
        self.method_description = method_description
        self.calibration_factor = calibration_factor
        self.detection_bin_efficiencies = detection_bin_efficiencies
        self.module_pair_sgidlut = module_pair_sgidlut
        self.module_pair_efficiencies_vectors = module_pair_efficiencies_vectors

    def __eq__(self, other: object) -> bool:
        return (
            isinstance(other, DetectionEfficiencies)
            and self.method_description == other.method_description
            and self.calibration_factor == other.calibration_factor
            and (other.detection_bin_efficiencies is None if self.detection_bin_efficiencies is None else (other.detection_bin_efficiencies is not None and len(self.detection_bin_efficiencies) == len(other.detection_bin_efficiencies) and all(yardl.structural_equal(a, b) for a, b in zip(self.detection_bin_efficiencies, other.detection_bin_efficiencies))))
            and (other.module_pair_sgidlut is None if self.module_pair_sgidlut is None else (other.module_pair_sgidlut is not None and len(self.module_pair_sgidlut) == len(other.module_pair_sgidlut) and all(len(a) == len(b) and all(yardl.structural_equal(a, b) for a, b in zip(a, b)) for a, b in zip(self.module_pair_sgidlut, other.module_pair_sgidlut))))
            and (other.module_pair_efficiencies_vectors is None if self.module_pair_efficiencies_vectors is None else (other.module_pair_efficiencies_vectors is not None and len(self.module_pair_efficiencies_vectors) == len(other.module_pair_efficiencies_vectors) and all(len(a) == len(b) and all(len(a) == len(b) and all(a == b for a, b in zip(a, b)) for a, b in zip(a, b)) for a, b in zip(self.module_pair_efficiencies_vectors, other.module_pair_efficiencies_vectors))))
        )

    def __str__(self) -> str:
        return f"DetectionEfficiencies(methodDescription={self.method_description}, calibrationFactor={self.calibration_factor}, detectionBinEfficiencies={self.detection_bin_efficiencies}, modulePairSGIDLUT={self.module_pair_sgidlut}, modulePairEfficienciesVectors={self.module_pair_efficiencies_vectors})"

    def __repr__(self) -> str:
        return f"DetectionEfficiencies(methodDescription={repr(self.method_description)}, calibrationFactor={repr(self.calibration_factor)}, detectionBinEfficiencies={repr(self.detection_bin_efficiencies)}, modulePairSGIDLUT={repr(self.module_pair_sgidlut)}, modulePairEfficienciesVectors={repr(self.module_pair_efficiencies_vectors)})"


SinglesAliveTimeFractions = npt.NDArray[np.float32]
"""Type for alive-time fractions for singles
See AliveTimeFractions
Constraint: size == number of all possible DetectionBins (for a specific type_of_module)
"""


ModuleCoincidenceAliveTimeFractions = npt.NDArray[np.float32]
"""Type for coincidence alive time fractions array for 2 modules in coincidence
See AliveTimeFractions
"""


class AliveTimeFractions:
    """Type for alive-time fraction information
    (1 means no dead-time, 0 means no detected counts)
    A component-based model is used, where the total alive-time for a pair of DetectionBins
    is computed as the product of the (singles) alive-time of each detector times the
    coincidence-alive-time of the corresponding module-pair, i.e. conceptually
    aliveTimeFraction((type_of_module1, detectionBin1), (type_of_module2, detectionBin2)) =
      singlesAliveTimeFractions[type_of_module1](detectionBin1) *
      singlesAliveTimeFractions[type_of_module2](detectionBin2) *
      moduleCoincidenceAliveTimeFractions[type_of_module1][type_of_module2][module(detectionBin1), module(detectionBin2)]
    """

    singles_alive_time_fractions: list[SinglesAliveTimeFractions]
    """List of singles alive-time fractions (one for each type of module)"""

    module_coincidence_alive_time_fractions: typing.Optional[list[list[ModuleCoincidenceAliveTimeFractions]]]
    """Nested list of all-time fractions for 2 modules in coincidence.
    Constraint: size(moduleCoincidenceAliveTimeFractions[type_of_module1][type_of_module2], 0) == total number of modules of type 1
    Constraint: size(moduleCoincidenceAliveTimeFractions[type_of_module1][type_of_module2], 1) == total number of modules of types 2
    Constraint: moduleCoincidenceAliveTimeFractions[type_of_module1][type_of_module2][mod1, mod2] ==
      moduleCoincidenceAliveTimeFractions[type_of_module2][type_of_module1][mod2, mod1]
    """


    def __init__(self, *,
        singles_alive_time_fractions: typing.Optional[list[SinglesAliveTimeFractions]] = None,
        module_coincidence_alive_time_fractions: typing.Optional[list[list[ModuleCoincidenceAliveTimeFractions]]] = None,
    ):
        self.singles_alive_time_fractions = singles_alive_time_fractions if singles_alive_time_fractions is not None else []
        self.module_coincidence_alive_time_fractions = module_coincidence_alive_time_fractions

    def __eq__(self, other: object) -> bool:
        return (
            isinstance(other, AliveTimeFractions)
            and len(self.singles_alive_time_fractions) == len(other.singles_alive_time_fractions) and all(yardl.structural_equal(a, b) for a, b in zip(self.singles_alive_time_fractions, other.singles_alive_time_fractions))
            and (other.module_coincidence_alive_time_fractions is None if self.module_coincidence_alive_time_fractions is None else (other.module_coincidence_alive_time_fractions is not None and len(self.module_coincidence_alive_time_fractions) == len(other.module_coincidence_alive_time_fractions) and all(len(a) == len(b) and all(yardl.structural_equal(a, b) for a, b in zip(a, b)) for a, b in zip(self.module_coincidence_alive_time_fractions, other.module_coincidence_alive_time_fractions))))
        )

    def __str__(self) -> str:
        return f"AliveTimeFractions(singlesAliveTimeFractions={self.singles_alive_time_fractions}, moduleCoincidenceAliveTimeFractions={self.module_coincidence_alive_time_fractions})"

    def __repr__(self) -> str:
        return f"AliveTimeFractions(singlesAliveTimeFractions={repr(self.singles_alive_time_fractions)}, moduleCoincidenceAliveTimeFractions={repr(self.module_coincidence_alive_time_fractions)})"


class SolidVolume(typing.Generic[Shape]):
    """A shape filled with a uniform material"""

    shape: Shape
    material_id: yardl.UInt32
    """identifier referring to `ScannerInformation.bulkMaterials` list"""


    def __init__(self, *,
        shape: Shape,
        material_id: yardl.UInt32 = 0,
    ):
        self.shape = shape
        self.material_id = material_id

    def __eq__(self, other: object) -> bool:
        return (
            isinstance(other, SolidVolume)
            and yardl.structural_equal(self.shape, other.shape)
            and self.material_id == other.material_id
        )

    def __str__(self) -> str:
        return f"SolidVolume(shape={self.shape}, materialId={self.material_id})"

    def __repr__(self) -> str:
        return f"SolidVolume(shape={repr(self.shape)}, materialId={repr(self.material_id)})"


class Coordinate:
    """3D coordinates (in mm)"""

    c: npt.NDArray[np.float32]

    def __init__(self, *,
        c: typing.Optional[npt.NDArray[np.float32]] = None,
    ):
        self.c = c if c is not None else np.zeros((3,), dtype=np.dtype(np.float32))

    def __eq__(self, other: object) -> bool:
        return (
            isinstance(other, Coordinate)
            and yardl.structural_equal(self.c, other.c)
        )

    def __str__(self) -> str:
        return f"Coordinate(c={self.c})"

    def __repr__(self) -> str:
        return f"Coordinate(c={repr(self.c)})"


class BoxShape:
    """A box-shape specified by 8 corners (e.g. cuboid, wedge, etc.)
    TODO need to think about a clear definition of planes
    We do not want to have to check about intersection planes
    Potential mechanisms:
    - lexicographical ordering of corner coordinates?
    - first 4 coordinates give first plane, 5th and 6th need to define plane with first 2, etc.
    """

    corners: list[Coordinate]

    def __init__(self, *,
        corners: typing.Optional[list[Coordinate]] = None,
    ):
        self.corners = corners if corners is not None else [Coordinate() for _ in range(8)]

    def __eq__(self, other: object) -> bool:
        return (
            isinstance(other, BoxShape)
            and len(self.corners) == len(other.corners) and all(a == b for a, b in zip(self.corners, other.corners))
        )

    def __str__(self) -> str:
        return f"BoxShape(corners={self.corners})"

    def __repr__(self) -> str:
        return f"BoxShape(corners={repr(self.corners)})"


BoxSolidVolume = SolidVolume[BoxShape]

class AnnulusShape:
    """Annulus of certain thickness centered at [0,0,0] and oriented along the [0,0,1] axis
    in radians. An angle of 0 corresponds to the [1,0,0] axis, Pi/2 corresponds to the [0,1,0] axis.
    """

    inner_radius: yardl.Float32
    """inner radius (in mm)"""

    outer_radius: yardl.Float32
    """outer radius (in mm)"""

    thickness: yardl.Float32
    """thickness of the annulus, i.e. length along the axis (in mm)"""

    angular_range: list[yardl.Float32]
    """start-stop angle (in radians)"""


    def __init__(self, *,
        inner_radius: yardl.Float32 = 0.0,
        outer_radius: yardl.Float32 = 0.0,
        thickness: yardl.Float32 = 0.0,
        angular_range: typing.Optional[list[yardl.Float32]] = None,
    ):
        self.inner_radius = inner_radius
        self.outer_radius = outer_radius
        self.thickness = thickness
        self.angular_range = angular_range if angular_range is not None else [0.0] * 2

    def __eq__(self, other: object) -> bool:
        return (
            isinstance(other, AnnulusShape)
            and self.inner_radius == other.inner_radius
            and self.outer_radius == other.outer_radius
            and self.thickness == other.thickness
            and self.angular_range == other.angular_range
        )

    def __str__(self) -> str:
        return f"AnnulusShape(innerRadius={self.inner_radius}, outerRadius={self.outer_radius}, thickness={self.thickness}, angularRange={self.angular_range})"

    def __repr__(self) -> str:
        return f"AnnulusShape(innerRadius={repr(self.inner_radius)}, outerRadius={repr(self.outer_radius)}, thickness={repr(self.thickness)}, angularRange={repr(self.angular_range)})"


_T = typing.TypeVar('_T')

class GeometricShape:
    BoxShape: typing.ClassVar[type["GeometricShapeUnionCase[BoxShape]"]]
    AnnulusShape: typing.ClassVar[type["GeometricShapeUnionCase[AnnulusShape]"]]

class GeometricShapeUnionCase(GeometricShape, yardl.UnionCase[_T]):
    pass

GeometricShape.BoxShape = type("GeometricShape.BoxShape", (GeometricShapeUnionCase,), {"index": 0, "tag": "BoxShape"})
GeometricShape.AnnulusShape = type("GeometricShape.AnnulusShape", (GeometricShapeUnionCase,), {"index": 1, "tag": "AnnulusShape"})
del GeometricShapeUnionCase

GenericSolidVolume = SolidVolume[GeometricShape]

class RigidTransformation:
    """Rigid transformation, encoded via homogenous transformation
    transformed_coord = matrix * [c, 1] (where [c,1] is a column vector)
    with `c` of type `Coordinate`
    """

    matrix: npt.NDArray[np.float32]

    def __init__(self, *,
        matrix: typing.Optional[npt.NDArray[np.float32]] = None,
    ):
        self.matrix = matrix if matrix is not None else np.zeros((3, 4,), dtype=np.dtype(np.float32))

    def __eq__(self, other: object) -> bool:
        return (
            isinstance(other, RigidTransformation)
            and yardl.structural_equal(self.matrix, other.matrix)
        )

    def __str__(self) -> str:
        return f"RigidTransformation(matrix={self.matrix})"

    def __repr__(self) -> str:
        return f"RigidTransformation(matrix={repr(self.matrix)})"


class ReplicatedObject(typing.Generic[T]):
    """A list of identical objects at different locations"""

    object: T
    transforms: list[RigidTransformation]
    """list of transforms
    constraint: length >= 1
    """


    def __init__(self, *,
        object: T,
        transforms: typing.Optional[list[RigidTransformation]] = None,
    ):
        self.object = object
        self.transforms = transforms if transforms is not None else []

    def number_of_objects(self) -> yardl.Size:
        return len(self.transforms)

    def __eq__(self, other: object) -> bool:
        return (
            isinstance(other, ReplicatedObject)
            and yardl.structural_equal(self.object, other.object)
            and len(self.transforms) == len(other.transforms) and all(a == b for a, b in zip(self.transforms, other.transforms))
        )

    def __str__(self) -> str:
        return f"ReplicatedObject(object={self.object}, transforms={self.transforms})"

    def __repr__(self) -> str:
        return f"ReplicatedObject(object={repr(self.object)}, transforms={repr(self.transforms)})"


ReplicatedBoxSolidVolume = ReplicatedObject[BoxSolidVolume]
"""A list of identical SolidVolumes<BoxShape> at different locations"""


ReplicatedGenericSolidVolume = ReplicatedObject[GenericSolidVolume]
"""A list of identical SolidVolumes<GeometricShape> at different locations"""


class DetectorModule:
    """Top-level detector structure, consisting of a group of (elementary) detecting elements (or "crystals")
    Note: all "crystals" have to be identical, aside from location. This includes properties such as
    shape, energy and timing resolution.
    This implies that for scanners with different types of detecting elements (e.g. phoswich detectors),
    different DetectorModules have to be used (see ScannerGeometry).
    """

    detecting_elements: ReplicatedBoxSolidVolume
    non_detecting_elements: list[ReplicatedGenericSolidVolume]
    """optional list describing shielding/optical reflectors etc"""


    def __init__(self, *,
        detecting_elements: typing.Optional[ReplicatedBoxSolidVolume] = None,
        non_detecting_elements: typing.Optional[list[ReplicatedGenericSolidVolume]] = None,
    ):
        self.detecting_elements = detecting_elements if detecting_elements is not None else ReplicatedObject(object=SolidVolume(shape=BoxShape()))
        self.non_detecting_elements = non_detecting_elements if non_detecting_elements is not None else []

    def __eq__(self, other: object) -> bool:
        return (
            isinstance(other, DetectorModule)
            and self.detecting_elements == other.detecting_elements
            and len(self.non_detecting_elements) == len(other.non_detecting_elements) and all(a == b for a, b in zip(self.non_detecting_elements, other.non_detecting_elements))
        )

    def __str__(self) -> str:
        return f"DetectorModule(detectingElements={self.detecting_elements}, nonDetectingElements={self.non_detecting_elements})"

    def __repr__(self) -> str:
        return f"DetectorModule(detectingElements={repr(self.detecting_elements)}, nonDetectingElements={repr(self.non_detecting_elements)})"


ReplicatedDetectorModule = ReplicatedObject[DetectorModule]
"""A list of identical modules at different locations"""


class ScannerGeometry:
    """Full definition of the geometry of the scanner, consisting of
    one of more types of modules replicated in space and (optional) other structures (e.g. side-shielding)
    """

    replicated_modules: list[ReplicatedDetectorModule]
    """list of different types of replicated modules
    While most current clinical scanners have only one type of DetectorModule, some
    would have different types, e.g. for scanners with phoswich detector, high resolution inserts, etc.
    Constraint: numberOfModuleTypes > 0
    """

    non_detecting_volumes: typing.Optional[list[GenericSolidVolume]]
    """shielding etc"""


    def __init__(self, *,
        replicated_modules: typing.Optional[list[ReplicatedDetectorModule]] = None,
        non_detecting_volumes: typing.Optional[list[GenericSolidVolume]] = None,
    ):
        self.replicated_modules = replicated_modules if replicated_modules is not None else []
        self.non_detecting_volumes = non_detecting_volumes

    def number_of_module_types(self) -> yardl.Size:
        return len(self.replicated_modules)

    def number_of_replicated_modules(self) -> yardl.Size:
        """Warning: the following function will be removed in the near future (TODO)"""

        return len(self.replicated_modules)

    def __eq__(self, other: object) -> bool:
        return (
            isinstance(other, ScannerGeometry)
            and len(self.replicated_modules) == len(other.replicated_modules) and all(a == b for a, b in zip(self.replicated_modules, other.replicated_modules))
            and (other.non_detecting_volumes is None if self.non_detecting_volumes is None else (other.non_detecting_volumes is not None and len(self.non_detecting_volumes) == len(other.non_detecting_volumes) and all(a == b for a, b in zip(self.non_detecting_volumes, other.non_detecting_volumes))))
        )

    def __str__(self) -> str:
        return f"ScannerGeometry(replicatedModules={self.replicated_modules}, nonDetectingVolumes={self.non_detecting_volumes})"

    def __repr__(self) -> str:
        return f"ScannerGeometry(replicatedModules={repr(self.replicated_modules)}, nonDetectingVolumes={repr(self.non_detecting_volumes)})"


TypeOfModule = yardl.UInt32
"""This is the type for the index over "module-types".
It is an unsigned integer that runs over all the types starting from 0.

This index runs over ScannerGeometry.replicated_modules
"""


TypeOfModulePair = list[TypeOfModule]
"""This is a pair of 2 "module-types"."""


class ExpandedDetectionBin:
    """type for a "single" detection (of a particular type of module), expanded into
    indices into the `ScannerGeometry`
    """

    module_index: yardl.UInt32
    """index (starting from 0) into ScannerGeometry.replicatedModules.transforms"""

    element_index: yardl.UInt32
    """index (starting from 0) into DetectorModule.detectingElements.transforms"""

    energy_index: yardl.UInt32
    """index (starting from 0) for the energy window
    See the ScannerInformation.eventEnergyBinEdges field
    """


    def __init__(self, *,
        module_index: yardl.UInt32 = 0,
        element_index: yardl.UInt32 = 0,
        energy_index: yardl.UInt32 = 0,
    ):
        self.module_index = module_index
        self.element_index = element_index
        self.energy_index = energy_index

    def __eq__(self, other: object) -> bool:
        return (
            isinstance(other, ExpandedDetectionBin)
            and self.module_index == other.module_index
            and self.element_index == other.element_index
            and self.energy_index == other.energy_index
        )

    def __str__(self) -> str:
        return f"ExpandedDetectionBin(moduleIndex={self.module_index}, elementIndex={self.element_index}, energyIndex={self.energy_index})"

    def __repr__(self) -> str:
        return f"ExpandedDetectionBin(moduleIndex={repr(self.module_index)}, elementIndex={repr(self.element_index)}, energyIndex={repr(self.energy_index)})"


DetectionBin = yardl.UInt32
"""type for a "single" detection (of a particular type of module), used in `CoincidenceEvent` and `TripleEvent`.
This a single number (starting from 0), encoding the indices in `ExpandedDetectionBin`
into a single number.
For a particular type of module, the number of modules, (detecting) elements in each module and
energy windows is fixed. Given an `ExpandedDetectionBin`, a `DetectionBin` is computed as
   energyIndex + (elementIndex + moduleIndex * numberOfElementsIndices) * numberOfModules
TODO is uint big enough?
"""


class SingleEvent:
    """All information about a single event specified as indices (i.e. discretized)."""

    detection_bin: DetectionBin
    """identifiers of detecting bin (see doc for DetectionBin)"""

    time_offset_in_time_block: yardl.UInt32
    """arrival time w.r.t. start of the time-block (in ps) (see EventTimeBlock)."""


    def __init__(self, *,
        detection_bin: DetectionBin = 0,
        time_offset_in_time_block: yardl.UInt32 = 0,
    ):
        self.detection_bin = detection_bin
        self.time_offset_in_time_block = time_offset_in_time_block

    def __eq__(self, other: object) -> bool:
        return (
            isinstance(other, SingleEvent)
            and self.detection_bin == other.detection_bin
            and self.time_offset_in_time_block == other.time_offset_in_time_block
        )

    def __str__(self) -> str:
        return f"SingleEvent(detectionBin={self.detection_bin}, timeOffsetInTimeBlock={self.time_offset_in_time_block})"

    def __repr__(self) -> str:
        return f"SingleEvent(detectionBin={repr(self.detection_bin)}, timeOffsetInTimeBlock={repr(self.time_offset_in_time_block)})"


class CoincidenceEvent:
    """All information about a coincidence event specified as indices (i.e. discretized)."""

    detection_bins: list[DetectionBin]
    """identifiers of the two detecting bins (see doc for DetectionBin)"""

    tof_idx: yardl.UInt32
    """an index into the tofBinEdges field in the ScannerInformation"""


    def __init__(self, *,
        detection_bins: typing.Optional[list[DetectionBin]] = None,
        tof_idx: yardl.UInt32 = 0,
    ):
        self.detection_bins = detection_bins if detection_bins is not None else [0] * 2
        self.tof_idx = tof_idx

    def __eq__(self, other: object) -> bool:
        return (
            isinstance(other, CoincidenceEvent)
            and self.detection_bins == other.detection_bins
            and self.tof_idx == other.tof_idx
        )

    def __str__(self) -> str:
        return f"CoincidenceEvent(detectionBins={self.detection_bins}, tofIdx={self.tof_idx})"

    def __repr__(self) -> str:
        return f"CoincidenceEvent(detectionBins={repr(self.detection_bins)}, tofIdx={repr(self.tof_idx)})"


class TripleEvent:
    """All information about a triple coincidence event specified as identifiers (i.e. discretized)."""

    detection_bins: list[DetectionBin]
    """identifiers of the three detecting bins (see doc for DetectionBin)
    Note that it is possible that 2 "detecting elements" (i.e. module/element in the ExpandedDetectionBin)
    are equal (e.g. for inter-crystal Compton events)
    """

    tof_indices: list[yardl.UInt32]
    """timing differences w.r.t. first event, stored as
    indices into the tofBinEdges field in the ScannerInformation
    Note: only 2, corresponding to the arrival time differences of the second and third detectionBins
    listed w.r.t. the first detectionBin
    """


    def __init__(self, *,
        detection_bins: typing.Optional[list[DetectionBin]] = None,
        tof_indices: typing.Optional[list[yardl.UInt32]] = None,
    ):
        self.detection_bins = detection_bins if detection_bins is not None else [0] * 3
        self.tof_indices = tof_indices if tof_indices is not None else [0] * 2

    def __eq__(self, other: object) -> bool:
        return (
            isinstance(other, TripleEvent)
            and self.detection_bins == other.detection_bins
            and self.tof_indices == other.tof_indices
        )

    def __str__(self) -> str:
        return f"TripleEvent(detectionBins={self.detection_bins}, tofIndices={self.tof_indices})"

    def __repr__(self) -> str:
        return f"TripleEvent(detectionBins={repr(self.detection_bins)}, tofIndices={repr(self.tof_indices)})"


class QuadrupleEvent:
    """All information about a quadruple coincidence event specified as indices (i.e. discretized)."""

    detection_bins: list[DetectionBin]
    """identifiers of the four detecting bins (see doc for DetectionBin)"""

    tof_indices: list[yardl.UInt32]
    """timing differences w.r.t. first event, stored as
    indices into the tofBinEdges field in the ScannerInformation.
    Note: only 3, see documentation for TripleEvent
    """


    def __init__(self, *,
        detection_bins: typing.Optional[list[DetectionBin]] = None,
        tof_indices: typing.Optional[list[yardl.UInt32]] = None,
    ):
        self.detection_bins = detection_bins if detection_bins is not None else [0] * 4
        self.tof_indices = tof_indices if tof_indices is not None else [0] * 3

    def __eq__(self, other: object) -> bool:
        return (
            isinstance(other, QuadrupleEvent)
            and self.detection_bins == other.detection_bins
            and self.tof_indices == other.tof_indices
        )

    def __str__(self) -> str:
        return f"QuadrupleEvent(detectionBins={self.detection_bins}, tofIndices={self.tof_indices})"

    def __repr__(self) -> str:
        return f"QuadrupleEvent(detectionBins={repr(self.detection_bins)}, tofIndices={repr(self.tof_indices)})"


class Subject:
    name: typing.Optional[str]
    id: str

    def __init__(self, *,
        name: typing.Optional[str] = None,
        id: str = "",
    ):
        self.name = name
        self.id = id

    def __eq__(self, other: object) -> bool:
        return (
            isinstance(other, Subject)
            and self.name == other.name
            and self.id == other.id
        )

    def __str__(self) -> str:
        return f"Subject(name={self.name}, id={self.id})"

    def __repr__(self) -> str:
        return f"Subject(name={repr(self.name)}, id={repr(self.id)})"


class Institution:
    name: str
    address: str

    def __init__(self, *,
        name: str = "",
        address: str = "",
    ):
        self.name = name
        self.address = address

    def __eq__(self, other: object) -> bool:
        return (
            isinstance(other, Institution)
            and self.name == other.name
            and self.address == other.address
        )

    def __str__(self) -> str:
        return f"Institution(name={self.name}, address={self.address})"

    def __repr__(self) -> str:
        return f"Institution(name={repr(self.name)}, address={repr(self.address)})"


class ExternalSignalTypeEnum(yardl.OutOfRangeEnum):
    ECG_TRACE = 0
    ECG_TRIGGER = 1
    RESP_TRACE = 2
    RESP_TRIGGER = 3
    OTHER_MOTION_SIGNAL = 4
    OTHER_MOTION_TRIGGER = 5
    EXTERNAL_SYNC = 6
    MR_PULSE_START = 7
    OTHER = 8
    """other options, to be listed in the future"""


class ExternalSignal:
    type: ExternalSignalTypeEnum
    description: str
    id: yardl.UInt32

    def __init__(self, *,
        type: ExternalSignalTypeEnum = ExternalSignalTypeEnum.ECG_TRACE,
        description: str = "",
        id: yardl.UInt32 = 0,
    ):
        self.type = type
        self.description = description
        self.id = id

    def __eq__(self, other: object) -> bool:
        return (
            isinstance(other, ExternalSignal)
            and self.type == other.type
            and self.description == other.description
            and self.id == other.id
        )

    def __str__(self) -> str:
        return f"ExternalSignal(type={self.type}, description={self.description}, id={self.id})"

    def __repr__(self) -> str:
        return f"ExternalSignal(type={repr(self.type)}, description={repr(self.description)}, id={repr(self.id)})"


class ExamInformation:
    """Items describing the exam (incomplete)"""

    subject: Subject
    institution: Institution
    protocol: typing.Optional[str]
    start_of_acquisition: typing.Optional[yardl.DateTime]
    external_signals: list[ExternalSignal]
    """A list of all possible signals that are recorded in the stream.
    Actual values will be in recorded as ExternalSignalTimeBlock
    """


    def __init__(self, *,
        subject: typing.Optional[Subject] = None,
        institution: typing.Optional[Institution] = None,
        protocol: typing.Optional[str] = None,
        start_of_acquisition: typing.Optional[yardl.DateTime] = None,
        external_signals: typing.Optional[list[ExternalSignal]] = None,
    ):
        self.subject = subject if subject is not None else Subject()
        self.institution = institution if institution is not None else Institution()
        self.protocol = protocol
        self.start_of_acquisition = start_of_acquisition
        self.external_signals = external_signals if external_signals is not None else []

    def __eq__(self, other: object) -> bool:
        return (
            isinstance(other, ExamInformation)
            and self.subject == other.subject
            and self.institution == other.institution
            and self.protocol == other.protocol
            and self.start_of_acquisition == other.start_of_acquisition
            and self.external_signals == other.external_signals
        )

    def __str__(self) -> str:
        return f"ExamInformation(subject={self.subject}, institution={self.institution}, protocol={self.protocol}, startOfAcquisition={self.start_of_acquisition}, externalSignals={self.external_signals})"

    def __repr__(self) -> str:
        return f"ExamInformation(subject={repr(self.subject)}, institution={repr(self.institution)}, protocol={repr(self.protocol)}, startOfAcquisition={repr(self.start_of_acquisition)}, externalSignals={repr(self.external_signals)})"


class Direction:
    """3D direction vector (normalized to 1)"""

    c: npt.NDArray[np.float32]

    def __init__(self, *,
        c: typing.Optional[npt.NDArray[np.float32]] = None,
    ):
        self.c = c if c is not None else np.zeros((3,), dtype=np.dtype(np.float32))

    def __eq__(self, other: object) -> bool:
        return (
            isinstance(other, Direction)
            and yardl.structural_equal(self.c, other.c)
        )

    def __str__(self) -> str:
        return f"Direction(c={self.c})"

    def __repr__(self) -> str:
        return f"Direction(c={repr(self.c)})"


class DirectionMatrix:
    """Orthonormal matrix
    direction_of_first_axis = matrix * [1, 0 ,0] (as a column vector)
    """

    matrix: npt.NDArray[np.float32]

    def __init__(self, *,
        matrix: typing.Optional[npt.NDArray[np.float32]] = None,
    ):
        self.matrix = matrix if matrix is not None else np.zeros((3, 3,), dtype=np.dtype(np.float32))

    def __eq__(self, other: object) -> bool:
        return (
            isinstance(other, DirectionMatrix)
            and yardl.structural_equal(self.matrix, other.matrix)
        )

    def __str__(self) -> str:
        return f"DirectionMatrix(matrix={self.matrix})"

    def __repr__(self) -> str:
        return f"DirectionMatrix(matrix={repr(self.matrix)})"


class Atom:
    """Atom definition in terms of Z and A"""

    mass_number: yardl.UInt32
    """A"""

    atomic_number: yardl.UInt32
    """Z"""


    def __init__(self, *,
        mass_number: yardl.UInt32 = 0,
        atomic_number: yardl.UInt32 = 0,
    ):
        self.mass_number = mass_number
        self.atomic_number = atomic_number

    def __eq__(self, other: object) -> bool:
        return (
            isinstance(other, Atom)
            and self.mass_number == other.mass_number
            and self.atomic_number == other.atomic_number
        )

    def __str__(self) -> str:
        return f"Atom(massNumber={self.mass_number}, atomicNumber={self.atomic_number})"

    def __repr__(self) -> str:
        return f"Atom(massNumber={repr(self.mass_number)}, atomicNumber={repr(self.atomic_number)})"


class BulkMaterial:
    """Specification of materials used in the scanner.
    TODO agree with vendors if this information can be supplied and to what accuracy
    Ideally this list should be reasonably accurate to be useful for Monte Carlo simulations, but can be approximate.
    """

    id: yardl.UInt32
    """unique id that can be used to refer to the material in voxelised maps etc"""

    name: str
    """informative string, not standardised.
    Expected examples:
    detecting: BGO, LSO, LYSO, LaBr, GAGG, plastic
    non-detecting: tungsten, lead
    """

    density: yardl.Float32
    """density of the material
    Units: g/cc
    """

    atoms: list[Atom]
    """List of atoms"""

    mass_fractions: list[yardl.Float32]
    """List of massFractions for the atoms.
    constraint: sum of massFractions should be 1
    constraint:  size(atoms) == size(massFractions)
    """


    def __init__(self, *,
        id: yardl.UInt32 = 0,
        name: str = "",
        density: yardl.Float32 = 0.0,
        atoms: typing.Optional[list[Atom]] = None,
        mass_fractions: typing.Optional[list[yardl.Float32]] = None,
    ):
        self.id = id
        self.name = name
        self.density = density
        self.atoms = atoms if atoms is not None else []
        self.mass_fractions = mass_fractions if mass_fractions is not None else []

    def __eq__(self, other: object) -> bool:
        return (
            isinstance(other, BulkMaterial)
            and self.id == other.id
            and self.name == other.name
            and self.density == other.density
            and self.atoms == other.atoms
            and self.mass_fractions == other.mass_fractions
        )

    def __str__(self) -> str:
        return f"BulkMaterial(id={self.id}, name={self.name}, density={self.density}, atoms={self.atoms}, massFractions={self.mass_fractions})"

    def __repr__(self) -> str:
        return f"BulkMaterial(id={repr(self.id)}, name={repr(self.name)}, density={repr(self.density)}, atoms={repr(self.atoms)}, massFractions={repr(self.mass_fractions)})"


class BinEdges:
    """Type definition for histogram bins specified in terms of edges
    Note that specifying edges allows having non-equal bin sizes.
    Edges have to be given from first (smallest) to last (largest).
    There is one more edge than the number of bins.
    """

    edges: npt.NDArray[np.float32]

    def __init__(self, *,
        edges: typing.Optional[npt.NDArray[np.float32]] = None,
    ):
        self.edges = edges if edges is not None else np.zeros((0), dtype=np.dtype(np.float32))

    def number_of_bins(self) -> yardl.Size:
        return self.edges.size - 1

    def __eq__(self, other: object) -> bool:
        return (
            isinstance(other, BinEdges)
            and yardl.structural_equal(self.edges, other.edges)
        )

    def __str__(self) -> str:
        return f"BinEdges(edges={self.edges})"

    def __repr__(self) -> str:
        return f"BinEdges(edges={repr(self.edges)})"


class SinglesHistogramLevelType(yardl.OutOfRangeEnum):
    """Type definition for how single histograms are stored.
    Many scanners are able to store singles accumulated in time intervals, i.e.
    histogrammed (as opposed to a list of singles with their corresponding DetectionBin).
    However, some scanners do not store this information at the smallest detector resolution.
    Therefore, PETSIRD currently supports a total singles count for each module as well.
    See SinglesHistogramTimeBlock.
    """

    NONE = 0
    """singles histograms are not stored at all"""

    MODULE = 1
    """singles counts per module"""

    ALL = 2
    """singles counts for each DetectionBin"""


class SingleEventPolicy(yardl.OutOfRangeEnum):
    """Type definition for how to encode how the scanner records single events (if it does)."""

    ALL = 0
    """store all single events"""

    REJECT_IF_NOT_IN_COINCIDENCE = 1
    """only store those that are in coincidence with another single"""

    OTHER = 2
    """other options, to be listed in the future"""


class CoincidencePolicy(yardl.OutOfRangeEnum):
    """Type definition for how to encode how the scanner handles multiple coincidences when recording the prompts.
    Due to various effects (such as high count rate, prompt gammas), it is possible that multiple single
    events are detected within the coincidence window. This type encodes some different ways
    that this multiple events are handled, and recorded in the EventTimeBlock.promptEvents field.
    """

    REJECT_HIGHER_MULTIPLES = 0
    """multiples (more than 2 events in the coincidence window) will be rejected (i.e., not stored in the EventTimeBlock.promptEvents field)"""

    MULTIPLES_AS_ALL_COINCIDENCES = 1
    """multiples will be stored as a sequence of all pairs, e.g. a triple leads to 3 pairs"""

    MULTIPLES_AS_SEQUENTIAL_COINCIDENCES = 2
    """multiples will be stored as a sequence of pairs, e.g. a triple leads to 2 pairs
    (single1 and single2, single2 and single3)
    """

    OTHER = 3
    """other options, to be listed in the future"""


class TripleEventPolicy(yardl.OutOfRangeEnum):
    """Type definition for how to encode how the scanner handles higher order coincidences when recording the triples."""

    REJECT_HIGHER_MULTIPLES = 0
    """quadruples etc are not stored in the EventTimeBlock.tripleEvents field"""

    OTHER = 1
    """other options, to be listed in the future"""


class QuadrupleEventPolicy(yardl.OutOfRangeEnum):
    """Type definition for how to encode how the scanner handles higher order coincidences when recording the quadruples."""

    REJECT_HIGHER_MULTIPLES = 0
    """quadruples etc are not stored in the EventTimeBlock.quadrupleEvents field"""

    OTHER = 1
    """other options, to be listed in the future"""


class ScannerInformation:
    model_name: str
    scanner_geometry: ScannerGeometry
    """Geometric information for all detecting elements
    All coordinates are in the PET gantry coordinate system.
    """

    bulk_materials: list[BulkMaterial]
    """List of materials present in the scanner geometry. The `material_id`s there will refer to the
    identifiers in this list below.
    """

    gantry_alignment: typing.Optional[RigidTransformation]
    """Fixed transformation to reference location for this scanner.
    This field can be used to encode alignment with the CT or MRI gantry for instance.
    The transformation should convert from the PET gantry coordinate system to the reference.
    An empty field implies the identity transformation.
    """

    tof_bin_edges: list[list[BinEdges]]
    """Edge information for TOF bins in mm

    This is a nested vector with elements of type BinEdges, i.e. it specifies the edges for every module-pair
    (actually pair of module-types).
    To get the tof bin edges for 2 types of modules, use
     tofBinEdges[typeOfModule1][typeOfModule2]

    0 corresponds to the same arrival time. Negative numbers indicate that the first detector detected first.
    For instance, a coincidence event is stored as 2 detectionBins, denoting the arrival time at the first
    detecting element t1 and the arrival time at the second detecting element t2, we store (t1-t2)*c/2.
    Note: for non-TOF scanners (or module-types), this defines the coincidence window.
    TODO: this currently assumes equal size for each TOF bin, but some scanners "stretch" TOF bins depending on length of LOR
    Constraint: this 2D array has to be symmetric.
    Constraint: size(tofBinEdges) == size(ScannerGeometry.replicatedModules)
    Constraint: size(tofBinEdges[*]) == size(ScannerGeometry.replicatedModules)
    """

    tof_resolution: list[list[yardl.Float32]]
    """TOF resolution aka CTR (as FWHM) in mm
    To get the Coincidence Timing Resolution (CTR) in mm for 2 types of modules, use
     tofResolution[typeOfModule1][typeOfModule2]
    Note that this specifies the CTR "before" tof-binning

    Constraint: this 2D array has to be symmetric.
    Constraint: size(tofResolution) == size(ScannerGeometry.replicatedModules)
    Constraint: size(tofResolution[*]) == size(ScannerGeometry.replicatedModules)
    """

    event_energy_bin_edges: list[BinEdges]
    """Edge information (in keV) for energy windows used for coincidences, triples etc

    This is a list of values for every module type.
    Constraint: size(eventEnergyBinEdges) == size(ScannerGeometry.replicatedModules)
    """

    energy_resolution_at_511: list[yardl.Float32]
    """FWHM of photopeak for incoming gamma of 511 keV, expressed as a ratio w.r.t. 511

    This is a list of values for every module type.
    Constraint: size(energyResolutionAt511) == size(ScannerGeometry.replicatedModules)
    """

    singles_histogram_level: SinglesHistogramLevelType
    """level at which the singles histograms are stored"""

    singles_histogram_energy_bin_edges: list[BinEdges]
    """Edge information (in keV) for energy windows used for singles histograms

    This is a list of values for every module type.
    In many cases, this could be the same as eventEnergyBinEdges, but is an independent field
    for flexibility.
    Constraint: (singlesHistogramLevel != SinglesHistogramLevelType.none
                 && size(singlesHistogramEnergyBinEdges) >=  size(ScannerGeometry.replicatedModules)) ||
                (singlesHistogramLevel == SinglesHistogramLevelType.none && size(singlesHistogramEnergyBinEdges) == 0)
    """

    single_event_policy: SingleEventPolicy
    """Encode how the scanner handles single events in the EventTimeBlock.singleEvents field"""

    prompt_event_policy: CoincidencePolicy
    """Encode how the scanner handles multiple coincidences in the EventTimeBlock.promptEvents field"""

    delayed_event_policy: CoincidencePolicy
    """Encode how the scanner handles multiple coincidences in the EventTimeBlock.delayedEvents field
    It is expected that this will have value rejectHigherMultiples, but it is not guaranteed.
    """

    triple_event_policy: TripleEventPolicy
    """Encode how the scanner handles higher order coincidences in the EventTimeBlock.tripleEvents field
    This field is ignored if tripleEventsAreStored == false.
    """

    quadruple_event_policy: QuadrupleEventPolicy
    """Encode how the scanner handles higher order coincidences in the EventTimeBlock.quadrupleEvents field
    This field is ignored if quadrupleEventsAreStored == false.
    """

    single_events_are_stored: bool
    """a flag to indicate of single events are recorded in the stream (see EventTimeBlock)
    Note that this is separate from the singlesHistogramLevel, where singles are histogrammed in a time-block.
    """

    prompt_events_are_stored: bool
    """a flag to indicate of prompt coincidences are recorded in the stream (see EventTimeBlock)"""

    delayed_events_are_stored: bool
    """a flag to indicate of delayed coincidences are recorded in the stream (see EventTimeBlock)"""

    triple_events_are_stored: bool
    """a flag to indicate of triple events are recorded in the stream (see EventTimeBlock)"""

    quadruple_events_are_stored: bool
    """a flag to indicate of quadruple coincidences are recorded in the stream (see EventTimeBlock)"""

    detection_efficiencies: DetectionEfficiencies
    """coincidence detection efficiencies"""


    def __init__(self, *,
        model_name: str = "",
        scanner_geometry: typing.Optional[ScannerGeometry] = None,
        bulk_materials: typing.Optional[list[BulkMaterial]] = None,
        gantry_alignment: typing.Optional[RigidTransformation] = None,
        tof_bin_edges: typing.Optional[list[list[BinEdges]]] = None,
        tof_resolution: typing.Optional[list[list[yardl.Float32]]] = None,
        event_energy_bin_edges: typing.Optional[list[BinEdges]] = None,
        energy_resolution_at_511: typing.Optional[list[yardl.Float32]] = None,
        singles_histogram_level: SinglesHistogramLevelType = SinglesHistogramLevelType.NONE,
        singles_histogram_energy_bin_edges: typing.Optional[list[BinEdges]] = None,
        single_event_policy: SingleEventPolicy = SingleEventPolicy.ALL,
        prompt_event_policy: CoincidencePolicy = CoincidencePolicy.REJECT_HIGHER_MULTIPLES,
        delayed_event_policy: CoincidencePolicy = CoincidencePolicy.REJECT_HIGHER_MULTIPLES,
        triple_event_policy: TripleEventPolicy = TripleEventPolicy.REJECT_HIGHER_MULTIPLES,
        quadruple_event_policy: QuadrupleEventPolicy = QuadrupleEventPolicy.REJECT_HIGHER_MULTIPLES,
        single_events_are_stored: bool = False,
        prompt_events_are_stored: bool = False,
        delayed_events_are_stored: bool = False,
        triple_events_are_stored: bool = False,
        quadruple_events_are_stored: bool = False,
        detection_efficiencies: typing.Optional[DetectionEfficiencies] = None,
    ):
        self.model_name = model_name
        self.scanner_geometry = scanner_geometry if scanner_geometry is not None else ScannerGeometry()
        self.bulk_materials = bulk_materials if bulk_materials is not None else []
        self.gantry_alignment = gantry_alignment
        self.tof_bin_edges = tof_bin_edges if tof_bin_edges is not None else []
        self.tof_resolution = tof_resolution if tof_resolution is not None else []
        self.event_energy_bin_edges = event_energy_bin_edges if event_energy_bin_edges is not None else []
        self.energy_resolution_at_511 = energy_resolution_at_511 if energy_resolution_at_511 is not None else []
        self.singles_histogram_level = singles_histogram_level
        self.singles_histogram_energy_bin_edges = singles_histogram_energy_bin_edges if singles_histogram_energy_bin_edges is not None else []
        self.single_event_policy = single_event_policy
        self.prompt_event_policy = prompt_event_policy
        self.delayed_event_policy = delayed_event_policy
        self.triple_event_policy = triple_event_policy
        self.quadruple_event_policy = quadruple_event_policy
        self.single_events_are_stored = single_events_are_stored
        self.prompt_events_are_stored = prompt_events_are_stored
        self.delayed_events_are_stored = delayed_events_are_stored
        self.triple_events_are_stored = triple_events_are_stored
        self.quadruple_events_are_stored = quadruple_events_are_stored
        self.detection_efficiencies = detection_efficiencies if detection_efficiencies is not None else DetectionEfficiencies()

    def __eq__(self, other: object) -> bool:
        return (
            isinstance(other, ScannerInformation)
            and self.model_name == other.model_name
            and self.scanner_geometry == other.scanner_geometry
            and self.bulk_materials == other.bulk_materials
            and (other.gantry_alignment is None if self.gantry_alignment is None else (other.gantry_alignment is not None and self.gantry_alignment == other.gantry_alignment))
            and len(self.tof_bin_edges) == len(other.tof_bin_edges) and all(len(a) == len(b) and all(a == b for a, b in zip(a, b)) for a, b in zip(self.tof_bin_edges, other.tof_bin_edges))
            and self.tof_resolution == other.tof_resolution
            and len(self.event_energy_bin_edges) == len(other.event_energy_bin_edges) and all(a == b for a, b in zip(self.event_energy_bin_edges, other.event_energy_bin_edges))
            and self.energy_resolution_at_511 == other.energy_resolution_at_511
            and self.singles_histogram_level == other.singles_histogram_level
            and len(self.singles_histogram_energy_bin_edges) == len(other.singles_histogram_energy_bin_edges) and all(a == b for a, b in zip(self.singles_histogram_energy_bin_edges, other.singles_histogram_energy_bin_edges))
            and self.single_event_policy == other.single_event_policy
            and self.prompt_event_policy == other.prompt_event_policy
            and self.delayed_event_policy == other.delayed_event_policy
            and self.triple_event_policy == other.triple_event_policy
            and self.quadruple_event_policy == other.quadruple_event_policy
            and self.single_events_are_stored == other.single_events_are_stored
            and self.prompt_events_are_stored == other.prompt_events_are_stored
            and self.delayed_events_are_stored == other.delayed_events_are_stored
            and self.triple_events_are_stored == other.triple_events_are_stored
            and self.quadruple_events_are_stored == other.quadruple_events_are_stored
            and self.detection_efficiencies == other.detection_efficiencies
        )

    def __str__(self) -> str:
        return f"ScannerInformation(modelName={self.model_name}, scannerGeometry={self.scanner_geometry}, bulkMaterials={self.bulk_materials}, gantryAlignment={self.gantry_alignment}, tofBinEdges={self.tof_bin_edges}, tofResolution={self.tof_resolution}, eventEnergyBinEdges={self.event_energy_bin_edges}, energyResolutionAt511={self.energy_resolution_at_511}, singlesHistogramLevel={self.singles_histogram_level}, singlesHistogramEnergyBinEdges={self.singles_histogram_energy_bin_edges}, singleEventPolicy={self.single_event_policy}, promptEventPolicy={self.prompt_event_policy}, delayedEventPolicy={self.delayed_event_policy}, tripleEventPolicy={self.triple_event_policy}, quadrupleEventPolicy={self.quadruple_event_policy}, singleEventsAreStored={self.single_events_are_stored}, promptEventsAreStored={self.prompt_events_are_stored}, delayedEventsAreStored={self.delayed_events_are_stored}, tripleEventsAreStored={self.triple_events_are_stored}, quadrupleEventsAreStored={self.quadruple_events_are_stored}, detectionEfficiencies={self.detection_efficiencies})"

    def __repr__(self) -> str:
        return f"ScannerInformation(modelName={repr(self.model_name)}, scannerGeometry={repr(self.scanner_geometry)}, bulkMaterials={repr(self.bulk_materials)}, gantryAlignment={repr(self.gantry_alignment)}, tofBinEdges={repr(self.tof_bin_edges)}, tofResolution={repr(self.tof_resolution)}, eventEnergyBinEdges={repr(self.event_energy_bin_edges)}, energyResolutionAt511={repr(self.energy_resolution_at_511)}, singlesHistogramLevel={repr(self.singles_histogram_level)}, singlesHistogramEnergyBinEdges={repr(self.singles_histogram_energy_bin_edges)}, singleEventPolicy={repr(self.single_event_policy)}, promptEventPolicy={repr(self.prompt_event_policy)}, delayedEventPolicy={repr(self.delayed_event_policy)}, tripleEventPolicy={repr(self.triple_event_policy)}, quadrupleEventPolicy={repr(self.quadruple_event_policy)}, singleEventsAreStored={repr(self.single_events_are_stored)}, promptEventsAreStored={repr(self.prompt_events_are_stored)}, delayedEventsAreStored={repr(self.delayed_events_are_stored)}, tripleEventsAreStored={repr(self.triple_events_are_stored)}, quadrupleEventsAreStored={repr(self.quadruple_events_are_stored)}, detectionEfficiencies={repr(self.detection_efficiencies)})"


class Header:
    scanner: ScannerInformation
    exam: typing.Optional[ExamInformation]

    def __init__(self, *,
        scanner: typing.Optional[ScannerInformation] = None,
        exam: typing.Optional[ExamInformation] = None,
    ):
        self.scanner = scanner if scanner is not None else ScannerInformation()
        self.exam = exam

    def __eq__(self, other: object) -> bool:
        return (
            isinstance(other, Header)
            and self.scanner == other.scanner
            and self.exam == other.exam
        )

    def __str__(self) -> str:
        return f"Header(scanner={self.scanner}, exam={self.exam})"

    def __repr__(self) -> str:
        return f"Header(scanner={repr(self.scanner)}, exam={repr(self.exam)})"


class TimeInterval:
    """Time interval in milliseconds since start of acquisition"""

    start: yardl.UInt32
    stop: yardl.UInt32

    def __init__(self, *,
        start: yardl.UInt32 = 0,
        stop: yardl.UInt32 = 0,
    ):
        self.start = start
        self.stop = stop

    def __eq__(self, other: object) -> bool:
        return (
            isinstance(other, TimeInterval)
            and self.start == other.start
            and self.stop == other.stop
        )

    def __str__(self) -> str:
        return f"TimeInterval(start={self.start}, stop={self.stop})"

    def __repr__(self) -> str:
        return f"TimeInterval(start={repr(self.start)}, stop={repr(self.stop)})"


ListOfSingleEvents = list[SingleEvent]
"""Type definition for a list of single events (for a module of a specific type)"""


ListOfCoincidenceEvents = list[CoincidenceEvent]
"""Type definition for a list of coincidences, where all coincidences are between 2 modules of specific types"""


ListOfTripleEvents = list[TripleEvent]
"""Type definition for a list of triples, where all triples are between 3 modules of specific types"""


ListOfQuadrupleEvents = list[TripleEvent]
"""Type definition for a list of triples, where all triples are between 3 modules of specific types"""


class EventTimeBlock:
    """time-block that stores the detected (gamma photon) events as lists.
    An event time block optionally stores
    - singles
    - "prompt" coincidences (i.e. 2 photons detected within the coincidence window),
    - "delayed"coincidences (i.e. 2 photons with arrival time within the coincidence window after subtracting a
    (scanner dependent) offset for the second event)
    - triples
    - quadruples
    Note: PETSIRD currently does not allow storing higher order coincidences. These are very unlikely to occur in practice.
    It is expected that most scanners will only store prompt and delayed coincidences. If singles are stored, it is expected
    that coincidences are not stored, but this is not guaranteed.

    See also  the "policy" fields in ScannerInformation.
    For instance, if coincidences, triples and quadruples are stored, it is likely that ScannerInformation.promptEventPolicy = rejectHigherMultiples,
    ScannerInformation.tripleEventPolicy = rejectHigherMultiples, and ScannerInformation.quadrupleEventPolicy = rejectHigherMultiples as well,
    but this is not guaranteed.
    """

    time_interval: TimeInterval
    """time interval for this time block
    We suggest that for each coincidence/multiple event, the earliest time of arrival
    of a photon is used for deciding which time block the event is entered in.
    However, the impact of this choice is negligible and therefore can be vendor dependent.
    WARNING: if singleEvents are recorded, the time interval currently has to be smaller
    than 2^32 picoseconds due to how time is stored, see SingleEvent.
    """

    single_events: list[ListOfSingleEvents]
    """nested vector with lists of singles
    If ScannerInformation.singleEventsAreStored is False, the vector should have
    size 0. Otherwise, there is one separate list for every module type.
    To get the list of singles for a type of module where the `detectionBin`
    is in a module of the first type, use
     singleEvents[typeOfModule]
    Constraint: (size(singleEvents) > 0) == ScannerInformation.singleEventsAreStored
    Constraint: (size(singleEvents) == 0) || (size(singleEvents) == ScannerInformation.numberOfModuleTypes)
    """

    prompt_events: list[list[ListOfCoincidenceEvents]]
    """nested vector with lists of prompts in this time block
    If ScannerInformation.promptEventsAreStored is False, the vector should have
    size 0. If not, there is one separate list for every pair of module types.
    To get the list of prompts for 2 types of modules where the first `detectionBin`
    given in the `CoincidenceEvent` is in a module of the first type, use
     promptEvents[typeOfModule1][typeOfModule2]
    Note that the ListOfCoincidenceEvents could be empty for a particular module-pair.
    Note that in principle, this nested vector could be made "upper diagonal",
    but this is currently not enforced. Therefore, to check all coincidences between two
    different types of modules, a user will have to check two lists, i.e. also
     promptEvents[typeOfModule2][typeOfModule1]
    Constraint: (size(promptEvents) > 0) == ScannerInformation.promptEventsAreStored
    Constraint: (size(promptEvents) == 0) || (size(promptsEvents) == ScannerInformation.numberOfModuleTypes)
    Constraint: (size(promptEvents) == 0) || (size(promptsEvents[*]) == ScannerInformation.numberOfModuleTypes)
    """

    delayed_events: list[list[ListOfCoincidenceEvents]]
    """nested vector with lists of delayed coincidences in this time block
    To get the list of delayeds for 2 types of modules, use
     delayedEvents[typeOfModule1][typeOfModule2]
    See promptEvents for more information.
    Constraint: (size(delayedEvents) > 0) == ScannerInformation.delayedCoincidencesAreStored
    Constraint: (size(delayedEvents) == 0) || (size(delayedEvents) == ScannerInformation.numberOfModuleTypes)
    Constraint: (size(delayedEvents) == 0) || (size(delayedEvents[*]) == ScannerInformation.numberOfModuleTypes)
    """

    triple_events: list[list[list[ListOfTripleEvents]]]
    """nested vector with lists of triple coincidences in this time block
    To get the list of triples for 3 types of modules, use
     tripleEvents[typeOfModule1][typeOfModule2][typeOfModule3]
    See promptEvents for more information.
    Constraint: (size(tripleEvents) > 0) == ScannerInformation.tripleEventsAreStored
    Constraint: (size(tripleEvents) == 0) || (size(tripleEvents) == ScannerInformation.numberOfModuleTypes)
    Constraint: (size(tripleEvents) == 0) || (size(tripleEvents[*]) == ScannerInformation.numberOfModuleTypes)
    Constraint: (size(tripleEvents) == 0) || (size(tripleEvents[*][*]) == ScannerInformation.numberOfModuleTypes)
    """

    quadruple_events: list[list[list[list[ListOfQuadrupleEvents]]]]
    """nested vector with lists of quadruple coincidences in this time block
    To get the list of quadruples for 4 types of modules, use
     quadrupleEvents[typeOfModule1][typeOfModule2][typeOfModule3][typeOfModule4]
    See promptEvents for more information.
    Constraint: (size(quadrupleEvents) > 0) == ScannerInformation.quadrupleEventsAreStored
    Constraint: (size(quadrupleEvents) == 0) || (size(quadrupleEvents) == ScannerInformation.numberOfModuleTypes)
    Constraint: (size(quadrupleEvents) == 0) || (size(quadrupleEvents[*]) == ScannerInformation.numberOfModuleTypes)
    Constraint: (size(quadrupleEvents) == 0) || (size(quadrupleEvents[*][*]) == ScannerInformation.numberOfModuleTypes)
    Constraint: (size(quadrupleEvents) == 0) || (size(quadrupleEvents[*][*][*]) == ScannerInformation.numberOfModuleTypes)
    """


    def __init__(self, *,
        time_interval: typing.Optional[TimeInterval] = None,
        single_events: typing.Optional[list[ListOfSingleEvents]] = None,
        prompt_events: typing.Optional[list[list[ListOfCoincidenceEvents]]] = None,
        delayed_events: typing.Optional[list[list[ListOfCoincidenceEvents]]] = None,
        triple_events: typing.Optional[list[list[list[ListOfTripleEvents]]]] = None,
        quadruple_events: typing.Optional[list[list[list[list[ListOfQuadrupleEvents]]]]] = None,
    ):
        self.time_interval = time_interval if time_interval is not None else TimeInterval()
        self.single_events = single_events if single_events is not None else []
        self.prompt_events = prompt_events if prompt_events is not None else []
        self.delayed_events = delayed_events if delayed_events is not None else []
        self.triple_events = triple_events if triple_events is not None else []
        self.quadruple_events = quadruple_events if quadruple_events is not None else []

    def __eq__(self, other: object) -> bool:
        return (
            isinstance(other, EventTimeBlock)
            and self.time_interval == other.time_interval
            and self.single_events == other.single_events
            and self.prompt_events == other.prompt_events
            and self.delayed_events == other.delayed_events
            and self.triple_events == other.triple_events
            and self.quadruple_events == other.quadruple_events
        )

    def __str__(self) -> str:
        return f"EventTimeBlock(timeInterval={self.time_interval}, singleEvents={self.single_events}, promptEvents={self.prompt_events}, delayedEvents={self.delayed_events}, tripleEvents={self.triple_events}, quadrupleEvents={self.quadruple_events})"

    def __repr__(self) -> str:
        return f"EventTimeBlock(timeInterval={repr(self.time_interval)}, singleEvents={repr(self.single_events)}, promptEvents={repr(self.prompt_events)}, delayedEvents={repr(self.delayed_events)}, tripleEvents={repr(self.triple_events)}, quadrupleEvents={repr(self.quadruple_events)})"


class ExternalSignalTimeBlock:
    """A time block containing the values for a particular external signal.
    See ExamInformation.externalSignals for the list of possible signals.
    """

    time_interval: TimeInterval
    """time interval for this time block
    Note: for triggers, the time interval should be very short (and can be zero).
    """

    signal_id: yardl.UInt32
    """refer to ExternalSignal.id
    Constraint: value has to be listed in ExamInformation.externalSignals.
    """

    signal_values: list[yardl.Float32]
    """Note for triggers, this field is to be ignored"""


    def __init__(self, *,
        time_interval: typing.Optional[TimeInterval] = None,
        signal_id: yardl.UInt32 = 0,
        signal_values: typing.Optional[list[yardl.Float32]] = None,
    ):
        self.time_interval = time_interval if time_interval is not None else TimeInterval()
        self.signal_id = signal_id
        self.signal_values = signal_values if signal_values is not None else []

    def __eq__(self, other: object) -> bool:
        return (
            isinstance(other, ExternalSignalTimeBlock)
            and self.time_interval == other.time_interval
            and self.signal_id == other.signal_id
            and self.signal_values == other.signal_values
        )

    def __str__(self) -> str:
        return f"ExternalSignalTimeBlock(timeInterval={self.time_interval}, signalID={self.signal_id}, signalValues={self.signal_values})"

    def __repr__(self) -> str:
        return f"ExternalSignalTimeBlock(timeInterval={repr(self.time_interval)}, signalID={repr(self.signal_id)}, signalValues={repr(self.signal_values)})"


class BedMovementTimeBlock:
    time_interval: TimeInterval
    """time interval for this time block"""

    transform: RigidTransformation

    def __init__(self, *,
        time_interval: typing.Optional[TimeInterval] = None,
        transform: typing.Optional[RigidTransformation] = None,
    ):
        self.time_interval = time_interval if time_interval is not None else TimeInterval()
        self.transform = transform if transform is not None else RigidTransformation()

    def __eq__(self, other: object) -> bool:
        return (
            isinstance(other, BedMovementTimeBlock)
            and self.time_interval == other.time_interval
            and self.transform == other.transform
        )

    def __str__(self) -> str:
        return f"BedMovementTimeBlock(timeInterval={self.time_interval}, transform={self.transform})"

    def __repr__(self) -> str:
        return f"BedMovementTimeBlock(timeInterval={repr(self.time_interval)}, transform={repr(self.transform)})"


class GantryMovementTimeBlock:
    time_interval: TimeInterval
    """time interval for this time block"""

    transform: RigidTransformation

    def __init__(self, *,
        time_interval: typing.Optional[TimeInterval] = None,
        transform: typing.Optional[RigidTransformation] = None,
    ):
        self.time_interval = time_interval if time_interval is not None else TimeInterval()
        self.transform = transform if transform is not None else RigidTransformation()

    def __eq__(self, other: object) -> bool:
        return (
            isinstance(other, GantryMovementTimeBlock)
            and self.time_interval == other.time_interval
            and self.transform == other.transform
        )

    def __str__(self) -> str:
        return f"GantryMovementTimeBlock(timeInterval={self.time_interval}, transform={self.transform})"

    def __repr__(self) -> str:
        return f"GantryMovementTimeBlock(timeInterval={repr(self.time_interval)}, transform={repr(self.transform)})"


class DeadTimeTimeBlock:
    """A time block that stores the dead-time information for the given interval
    Dead-time is encoded as "alive_time_fraction", i.e. 1 - dead_time_fraction
    Constraint: the time covered by all event time blocks has to be covered by dead-time time blocks
    """

    time_interval: TimeInterval
    """time interval for this time block"""

    alive_time_fractions: AliveTimeFractions
    """structure containing the alive-times for this time block"""


    def __init__(self, *,
        time_interval: typing.Optional[TimeInterval] = None,
        alive_time_fractions: typing.Optional[AliveTimeFractions] = None,
    ):
        self.time_interval = time_interval if time_interval is not None else TimeInterval()
        self.alive_time_fractions = alive_time_fractions if alive_time_fractions is not None else AliveTimeFractions()

    def __eq__(self, other: object) -> bool:
        return (
            isinstance(other, DeadTimeTimeBlock)
            and self.time_interval == other.time_interval
            and self.alive_time_fractions == other.alive_time_fractions
        )

    def __str__(self) -> str:
        return f"DeadTimeTimeBlock(timeInterval={self.time_interval}, aliveTimeFractions={self.alive_time_fractions})"

    def __repr__(self) -> str:
        return f"DeadTimeTimeBlock(timeInterval={repr(self.time_interval)}, aliveTimeFractions={repr(self.alive_time_fractions)})"


SinglesHistogram = npt.NDArray[np.uint64]
"""Type for the histogram of singles of one type of module
See SinglesHistogramTimeBlock
"""


class SinglesHistogramTimeBlock:
    """A time block that stores a singles histogram.
    Note that all detected singles (within the ScannerInformation.singlesHistogramEnergyBinEdges)
    should be stored. This is in contrast to the EventTimeBlock.singleEvents, where optionally
    only singles that are in coincidence are recorded (see SingleEventPolicy).

    See also SinglesHistogramLevelType for information.

    Constraint: if ScannerInformation.singlesHistogramLevel == SinglesHistogramLevelType.none, there should
        be no time blocks of this type in the stream.
        Otherwise, the union of SinglesHistogramTimeBlock.timeInterval should be at least as large as the union of all EventTimeBlocks.timeInterval
    """

    time_interval: TimeInterval
    singles_histograms: list[SinglesHistogram]
    """A list of histograms of all singles (after energy discrimination) occuring in the timeInterval.
    The dimensions correspond to positional information (i.e. moduleIdx or detElIdx, see below)
    and energy index. The latter index refers to ScannerInformation.singlesHistogramEnergyBinEdges.
    Note that in future, we might consider also storing singles before energy discrimination,
    but this is currently not supported.

    The histograms are stored per module-type.

    If ScannerInformation.singlesHistogramLevel == SinglesHistogramLevelType.module, the
    singlesDetectionBin runs over all modules (of a particular type) and all singles energy windows.
    If ScannerInformation.singlesHistogramLevel == SinglesHistogramLevelType.all, the
    singlesDetectionBin runs over all detectionBins (of a particular module type) and all singles energy windows.

    Constraint: (singlesHistogramLevel == module && size(singlesHistogram[type_of_module]) == total_number_of_modules_of_this_type * number_of_singles_energy_windows_of_this_type) ||
                (singlesHistogramLevel == all && size(singlesHistogram[type_of_module]) == total number of singles detection bins_in_all_modules_of_that_type)
    """


    def __init__(self, *,
        time_interval: typing.Optional[TimeInterval] = None,
        singles_histograms: typing.Optional[list[SinglesHistogram]] = None,
    ):
        self.time_interval = time_interval if time_interval is not None else TimeInterval()
        self.singles_histograms = singles_histograms if singles_histograms is not None else []

    def __eq__(self, other: object) -> bool:
        return (
            isinstance(other, SinglesHistogramTimeBlock)
            and self.time_interval == other.time_interval
            and len(self.singles_histograms) == len(other.singles_histograms) and all(yardl.structural_equal(a, b) for a, b in zip(self.singles_histograms, other.singles_histograms))
        )

    def __str__(self) -> str:
        return f"SinglesHistogramTimeBlock(timeInterval={self.time_interval}, singlesHistograms={self.singles_histograms})"

    def __repr__(self) -> str:
        return f"SinglesHistogramTimeBlock(timeInterval={repr(self.time_interval)}, singlesHistograms={repr(self.singles_histograms)})"


class TimeBlock:
    EventTimeBlock: typing.ClassVar[type["TimeBlockUnionCase[EventTimeBlock]"]]
    ExternalSignalTimeBlock: typing.ClassVar[type["TimeBlockUnionCase[ExternalSignalTimeBlock]"]]
    BedMovementTimeBlock: typing.ClassVar[type["TimeBlockUnionCase[BedMovementTimeBlock]"]]
    GantryMovementTimeBlock: typing.ClassVar[type["TimeBlockUnionCase[GantryMovementTimeBlock]"]]
    DeadTimeTimeBlock: typing.ClassVar[type["TimeBlockUnionCase[DeadTimeTimeBlock]"]]
    SinglesHistogramTimeBlock: typing.ClassVar[type["TimeBlockUnionCase[SinglesHistogramTimeBlock]"]]

class TimeBlockUnionCase(TimeBlock, yardl.UnionCase[_T]):
    pass

TimeBlock.EventTimeBlock = type("TimeBlock.EventTimeBlock", (TimeBlockUnionCase,), {"index": 0, "tag": "EventTimeBlock"})
TimeBlock.ExternalSignalTimeBlock = type("TimeBlock.ExternalSignalTimeBlock", (TimeBlockUnionCase,), {"index": 1, "tag": "ExternalSignalTimeBlock"})
TimeBlock.BedMovementTimeBlock = type("TimeBlock.BedMovementTimeBlock", (TimeBlockUnionCase,), {"index": 2, "tag": "BedMovementTimeBlock"})
TimeBlock.GantryMovementTimeBlock = type("TimeBlock.GantryMovementTimeBlock", (TimeBlockUnionCase,), {"index": 3, "tag": "GantryMovementTimeBlock"})
TimeBlock.DeadTimeTimeBlock = type("TimeBlock.DeadTimeTimeBlock", (TimeBlockUnionCase,), {"index": 4, "tag": "DeadTimeTimeBlock"})
TimeBlock.SinglesHistogramTimeBlock = type("TimeBlock.SinglesHistogramTimeBlock", (TimeBlockUnionCase,), {"index": 5, "tag": "SinglesHistogramTimeBlock"})
del TimeBlockUnionCase

class TimeFrameInformation:
    """A sequence of time intervals (could be consecutive)"""

    time_frames: list[TimeInterval]

    def __init__(self, *,
        time_frames: typing.Optional[list[TimeInterval]] = None,
    ):
        self.time_frames = time_frames if time_frames is not None else []

    def number_of_time_frames(self) -> yardl.Size:
        return len(self.time_frames)

    def __eq__(self, other: object) -> bool:
        return (
            isinstance(other, TimeFrameInformation)
            and self.time_frames == other.time_frames
        )

    def __str__(self) -> str:
        return f"TimeFrameInformation(timeFrames={self.time_frames})"

    def __repr__(self) -> str:
        return f"TimeFrameInformation(timeFrames={repr(self.time_frames)})"


def _mk_get_dtype():
    dtype_map: dict[typing.Union[type, types.GenericAlias], typing.Union[np.dtype[typing.Any], typing.Callable[[tuple[type, ...]], np.dtype[typing.Any]]]] = {}
    get_dtype = _dtypes.make_get_dtype_func(dtype_map)

    dtype_map.setdefault(SGID, np.dtype(np.uint32))
    dtype_map.setdefault(ModulePairEfficiencies, np.dtype([('values', np.dtype(np.object_)), ('sgid', np.dtype(np.uint32))], align=True))
    dtype_map.setdefault(DetectionEfficiencies, np.dtype([('method_description', np.dtype(np.object_)), ('calibration_factor', np.dtype(np.float32)), ('detection_bin_efficiencies', np.dtype([('has_value', np.dtype(np.bool_)), ('value', np.dtype(np.object_))], align=True)), ('module_pair_sgidlut', np.dtype([('has_value', np.dtype(np.bool_)), ('value', np.dtype(np.object_))], align=True)), ('module_pair_efficiencies_vectors', np.dtype([('has_value', np.dtype(np.bool_)), ('value', np.dtype(np.object_))], align=True))], align=True))
    dtype_map.setdefault(AliveTimeFractions, np.dtype([('singles_alive_time_fractions', np.dtype(np.object_)), ('module_coincidence_alive_time_fractions', np.dtype([('has_value', np.dtype(np.bool_)), ('value', np.dtype(np.object_))], align=True))], align=True))
    dtype_map.setdefault(SolidVolume, lambda type_args: np.dtype([('shape', get_dtype(type_args[0])), ('material_id', np.dtype(np.uint32))], align=True))
    dtype_map.setdefault(Coordinate, np.dtype([('c', np.dtype(np.float32), (3,))], align=True))
    dtype_map.setdefault(BoxShape, np.dtype([('corners', get_dtype(Coordinate), (8,))], align=True))
    dtype_map.setdefault(BoxSolidVolume, get_dtype(types.GenericAlias(SolidVolume, (BoxShape,))))
    dtype_map.setdefault(AnnulusShape, np.dtype([('inner_radius', np.dtype(np.float32)), ('outer_radius', np.dtype(np.float32)), ('thickness', np.dtype(np.float32)), ('angular_range', np.dtype(np.float32), (2,))], align=True))
    dtype_map.setdefault(GeometricShape, np.dtype(np.object_))
    dtype_map.setdefault(GenericSolidVolume, get_dtype(types.GenericAlias(SolidVolume, (GeometricShape,))))
    dtype_map.setdefault(RigidTransformation, np.dtype([('matrix', np.dtype(np.float32), (3, 4,))], align=True))
    dtype_map.setdefault(ReplicatedObject, lambda type_args: np.dtype([('object', get_dtype(type_args[0])), ('transforms', np.dtype(np.object_))], align=True))
    dtype_map.setdefault(ReplicatedBoxSolidVolume, get_dtype(types.GenericAlias(ReplicatedObject, (BoxSolidVolume,))))
    dtype_map.setdefault(ReplicatedGenericSolidVolume, get_dtype(types.GenericAlias(ReplicatedObject, (GenericSolidVolume,))))
    dtype_map.setdefault(DetectorModule, np.dtype([('detecting_elements', get_dtype(types.GenericAlias(ReplicatedObject, (BoxSolidVolume,)))), ('non_detecting_elements', np.dtype(np.object_))], align=True))
    dtype_map.setdefault(ReplicatedDetectorModule, get_dtype(types.GenericAlias(ReplicatedObject, (DetectorModule,))))
    dtype_map.setdefault(ScannerGeometry, np.dtype([('replicated_modules', np.dtype(np.object_)), ('non_detecting_volumes', np.dtype([('has_value', np.dtype(np.bool_)), ('value', np.dtype(np.object_))], align=True))], align=True))
    dtype_map.setdefault(TypeOfModule, np.dtype(np.uint32))
    dtype_map.setdefault(ExpandedDetectionBin, np.dtype([('module_index', np.dtype(np.uint32)), ('element_index', np.dtype(np.uint32)), ('energy_index', np.dtype(np.uint32))], align=True))
    dtype_map.setdefault(DetectionBin, np.dtype(np.uint32))
    dtype_map.setdefault(SingleEvent, np.dtype([('detection_bin', np.dtype(np.uint32)), ('time_offset_in_time_block', np.dtype(np.uint32))], align=True))
    dtype_map.setdefault(CoincidenceEvent, np.dtype([('detection_bins', np.dtype(np.uint32), (2,)), ('tof_idx', np.dtype(np.uint32))], align=True))
    dtype_map.setdefault(TripleEvent, np.dtype([('detection_bins', np.dtype(np.uint32), (3,)), ('tof_indices', np.dtype(np.uint32), (2,))], align=True))
    dtype_map.setdefault(QuadrupleEvent, np.dtype([('detection_bins', np.dtype(np.uint32), (4,)), ('tof_indices', np.dtype(np.uint32), (3,))], align=True))
    dtype_map.setdefault(Subject, np.dtype([('name', np.dtype([('has_value', np.dtype(np.bool_)), ('value', np.dtype(np.object_))], align=True)), ('id', np.dtype(np.object_))], align=True))
    dtype_map.setdefault(Institution, np.dtype([('name', np.dtype(np.object_)), ('address', np.dtype(np.object_))], align=True))
    dtype_map.setdefault(ExternalSignalTypeEnum, np.dtype(np.int32))
    dtype_map.setdefault(ExternalSignal, np.dtype([('type', get_dtype(ExternalSignalTypeEnum)), ('description', np.dtype(np.object_)), ('id', np.dtype(np.uint32))], align=True))
    dtype_map.setdefault(ExamInformation, np.dtype([('subject', get_dtype(Subject)), ('institution', get_dtype(Institution)), ('protocol', np.dtype([('has_value', np.dtype(np.bool_)), ('value', np.dtype(np.object_))], align=True)), ('start_of_acquisition', np.dtype([('has_value', np.dtype(np.bool_)), ('value', np.dtype(np.datetime64))], align=True)), ('external_signals', np.dtype(np.object_))], align=True))
    dtype_map.setdefault(Direction, np.dtype([('c', np.dtype(np.float32), (3,))], align=True))
    dtype_map.setdefault(DirectionMatrix, np.dtype([('matrix', np.dtype(np.float32), (3, 3,))], align=True))
    dtype_map.setdefault(Atom, np.dtype([('mass_number', np.dtype(np.uint32)), ('atomic_number', np.dtype(np.uint32))], align=True))
    dtype_map.setdefault(BulkMaterial, np.dtype([('id', np.dtype(np.uint32)), ('name', np.dtype(np.object_)), ('density', np.dtype(np.float32)), ('atoms', np.dtype(np.object_)), ('mass_fractions', np.dtype(np.object_))], align=True))
    dtype_map.setdefault(BinEdges, np.dtype([('edges', np.dtype(np.object_))], align=True))
    dtype_map.setdefault(SinglesHistogramLevelType, np.dtype(np.int32))
    dtype_map.setdefault(SingleEventPolicy, np.dtype(np.int32))
    dtype_map.setdefault(CoincidencePolicy, np.dtype(np.int32))
    dtype_map.setdefault(TripleEventPolicy, np.dtype(np.int32))
    dtype_map.setdefault(QuadrupleEventPolicy, np.dtype(np.int32))
    dtype_map.setdefault(ScannerInformation, np.dtype([('model_name', np.dtype(np.object_)), ('scanner_geometry', get_dtype(ScannerGeometry)), ('bulk_materials', np.dtype(np.object_)), ('gantry_alignment', np.dtype([('has_value', np.dtype(np.bool_)), ('value', get_dtype(RigidTransformation))], align=True)), ('tof_bin_edges', np.dtype(np.object_)), ('tof_resolution', np.dtype(np.object_)), ('event_energy_bin_edges', np.dtype(np.object_)), ('energy_resolution_at_511', np.dtype(np.object_)), ('singles_histogram_level', get_dtype(SinglesHistogramLevelType)), ('singles_histogram_energy_bin_edges', np.dtype(np.object_)), ('single_event_policy', get_dtype(SingleEventPolicy)), ('prompt_event_policy', get_dtype(CoincidencePolicy)), ('delayed_event_policy', get_dtype(CoincidencePolicy)), ('triple_event_policy', get_dtype(TripleEventPolicy)), ('quadruple_event_policy', get_dtype(QuadrupleEventPolicy)), ('single_events_are_stored', np.dtype(np.bool_)), ('prompt_events_are_stored', np.dtype(np.bool_)), ('delayed_events_are_stored', np.dtype(np.bool_)), ('triple_events_are_stored', np.dtype(np.bool_)), ('quadruple_events_are_stored', np.dtype(np.bool_)), ('detection_efficiencies', get_dtype(DetectionEfficiencies))], align=True))
    dtype_map.setdefault(Header, np.dtype([('scanner', get_dtype(ScannerInformation)), ('exam', np.dtype([('has_value', np.dtype(np.bool_)), ('value', get_dtype(ExamInformation))], align=True))], align=True))
    dtype_map.setdefault(TimeInterval, np.dtype([('start', np.dtype(np.uint32)), ('stop', np.dtype(np.uint32))], align=True))
    dtype_map.setdefault(EventTimeBlock, np.dtype([('time_interval', get_dtype(TimeInterval)), ('single_events', np.dtype(np.object_)), ('prompt_events', np.dtype(np.object_)), ('delayed_events', np.dtype(np.object_)), ('triple_events', np.dtype(np.object_)), ('quadruple_events', np.dtype(np.object_))], align=True))
    dtype_map.setdefault(ExternalSignalTimeBlock, np.dtype([('time_interval', get_dtype(TimeInterval)), ('signal_id', np.dtype(np.uint32)), ('signal_values', np.dtype(np.object_))], align=True))
    dtype_map.setdefault(BedMovementTimeBlock, np.dtype([('time_interval', get_dtype(TimeInterval)), ('transform', get_dtype(RigidTransformation))], align=True))
    dtype_map.setdefault(GantryMovementTimeBlock, np.dtype([('time_interval', get_dtype(TimeInterval)), ('transform', get_dtype(RigidTransformation))], align=True))
    dtype_map.setdefault(DeadTimeTimeBlock, np.dtype([('time_interval', get_dtype(TimeInterval)), ('alive_time_fractions', get_dtype(AliveTimeFractions))], align=True))
    dtype_map.setdefault(SinglesHistogramTimeBlock, np.dtype([('time_interval', get_dtype(TimeInterval)), ('singles_histograms', np.dtype(np.object_))], align=True))
    dtype_map.setdefault(TimeBlock, np.dtype(np.object_))
    dtype_map.setdefault(TimeFrameInformation, np.dtype([('time_frames', np.dtype(np.object_))], align=True))

    return get_dtype

get_dtype = _mk_get_dtype()

