Module ctsimu.scenario.drift

Drift structure for a Parameter.

Expand source code
# -*- coding: UTF-8 -*-
"""
Drift structure for a `ctsimu.scenario.parameter.Parameter`.
"""

import numbers
import math
import numpy
from ..helpers import *

class Drift:
        """Drift structure for a parameter.
        Drifts typically belong to a `ctsimu.scenario.parameter.Parameter` object,
        which keeps a list of possible parameter drifts.

        Attributes
        ----------
        trajectory : list
                List of drift values. If the number of drift values does not
                match the number of frames in the CT scan, a linear interpolation
                between the drift values can be used to calculate the drift value
                for a specific frame.

        native_unit : str
                The drift's native unit. Should match the native unit of the
                `ctsimu.scenario.parameter.Parameter` object to which this drift belongs.
                Possible values: `None`, `"mm"`, `"rad"`, `"deg"`, `"s"`, `"mA"`, `"kV"`, `"g/cm^3"`, `"lp/mm"`, `"deg/s"`, `"C"`, `"bool"`, `"string"`.

        known_to_reconstruction : bool
                Should this parameter drift be considered by the reconstruction software?
                This attribute is obeyed when calculating projection matrices.

        interpolation : bool
                Does a linear interpolation take place between the components of the
                `trajectory` list if the number of trajectory components does not match
                the number of frames in the CT scan?
        """

        def __init__(self, native_unit:str, preferred_unit:str=None, _root=None):
                """A new drift object must be assigned a valid native unit
                to enable the JSON parser to convert the drift values from the
                JSON file, if necessary.

                Parameters
                ----------
                native_unit : str
                        The drift's native unit. Should match the native unit of the
                        `ctsimu.scenario.parameter.Parameter` object to which this drift belongs.
                        Possible values: `None`, `"mm"`, `"rad"`, `"deg"`, `"s"`, `"mA"`, `"kV"`, `"g/cm^3"`, `"lp/mm"`, `"deg/s"`, `"C"`, `"bool"`, `"string"`.

                preferred_unit : str
                        Preferred unit to represent these drift values in the JSON file.
                        If set to `None`, the native unit will be used.
                """
                self._root = _root  # root scenario object

                self.value = []
                self.native_unit = native_unit

                if preferred_unit is not None:
                        self.preferred_unit = preferred_unit
                else:
                        self.preferred_unit = native_unit

                self.known_to_reconstruction = True
                self.interpolation = True

                self.reset()

        def reset(self):
                """Clear the list of drift values, set `known_to_reconstruction` to `True`."""
                self.known_to_reconstruction = True
                self.value = []

        def json_dict(self) -> dict:
                """Create a CTSimU JSON dictionary for this drift.

                Returns
                -------
                json_dict : dict
                """
                jd = dict()
                if len(self.value) > 0:
                        jd["value"] = self.value
                else:
                        jd["value"] = None

                if self.native_unit not in native_units_to_omit_in_json_file:
                        unit = self.native_unit
                        if self.preferred_unit is not None:
                                unit = self.preferred_unit

                                # Convert value to preferred unit:
                                conversion_factor = convert_to_native_unit(given_unit=self.preferred_unit, native_unit=self.native_unit, value=1)

                                if conversion_factor != 0:
                                        jd["value"] = list()
                                        for val in self.value:
                                                jd["value"].append(val/conversion_factor)

                        jd["unit"] = unit

                jd["known_to_reconstruction"] = self.known_to_reconstruction
                return jd

        def set_known_to_reconstruction(self, known:bool):
                """Set the `known_to_reconstruction` property.

                Parameters
                ----------
                known : bool
                        Should the reconstruction software take this drift into account?
                """
                self.known_to_reconstruction = known

        def set_interpolation(self, intpol:bool):
                """Set the `interpolation` property. Only necessary for
                number-type parameters. String parameters cannot run an interpolation.

                Parameters
                ----------
                intpol : bool
                        `True` if the drift value should be interpolated if the number of
                        drift components does not match the number of frames in the CT scan.
                        `False` if no interpolation should take place: the last drift value
                        that would match the given frame number is taken.
                """
                self.interpolation = intpol

        def set_native_unit(self, native_unit:str):
                """Set the drifts's native unit.

                Parameters
                ----------
                native_unit : str
                        New native unit for the drift.
                        Possible values: `None`, `"mm"`, `"rad"`, `"deg"`, `"s"`, `"mA"`, `"kV"`, `"g/cm^3"`, `"lp/mm"`, `"deg/s"`, `"C"`, `"bool"`, `"string"`.
                """
                if is_valid_native_unit(native_unit):
                        self.native_unit = native_unit

                        if native_unit == "string":
                                self.interpolation = False

        def add_drift_component(self, value):
                """Add a drift component to the list of drift values.

                Parameters
                ----------
                value : float or str or bool
                        Drift value. For number-type drifts (`float`), this is given as an
                        absolute deviation from the parameter's standard value.
                        For string-type drifts (`str`), this is the string that should replace
                        the parameter's standard value.
                """
                self.value.append(value)

        def set_from_json(self, json_drift_object:dict) -> bool:
                """Set the drift from a given CTSimU drift structure.
                The proper `native_unit` must be set up correctly before
                running this function.

                Parameters
                ----------
                json_drift_object : dict
                        A CTSimU drift structure, imported from a JSON structure.

                Returns
                -------
                success : bool
                        `True` on success, `False` if an error occurred.
                """
                self.reset()

                # Get JSON unit:
                if json_exists_and_not_null(json_drift_object, ["unit"]):
                        self.preferred_unit = get_value(
                                dictionary=json_drift_object,
                                keys=["unit"],
                                fail_value=self.native_unit
                        )

                # Known to reconstruction
                if json_exists_and_not_null(json_drift_object, ["known_to_reconstruction"]):
                        self.set_known_to_reconstruction(
                                get_value_in_native_unit(
                                        native_unit="bool",
                                        dictionary=json_drift_object,
                                        keys=["known_to_reconstruction"],
                                        fail_value=True
                                )
                        )

                # Get drift values:
                if json_exists_and_not_null(json_drift_object, ["value"]):
                        value = json_extract(json_drift_object, ["value"])
                        if isinstance(value, list):
                                # Drift value is an array.
                                for v in value:
                                        self.add_drift_component(
                                                convert_to_native_unit(self.preferred_unit, self.native_unit, v)
                                        )

                                return True
                        elif isinstance(value, numbers.Number):
                                # Interpret value as a single number:
                                self.add_drift_component(
                                        convert_to_native_unit(self.preferred_unit, self.native_unit, value)
                                )
                                return True
                elif json_exists_and_not_null(json_drift_object, ["file"]):
                        filename = json_extract(json_drift_object, ["file"])
                        if isinstance(filename, str):
                                # Read drift values from a file
                                if self._root is not None:
                                        # Get path relative to JSON file:
                                        rel_filename = self._root.path_of_external_file(filename)
                                else:
                                        rel_filename = filename

                                values = read_csv_file(rel_filename)

                                firstColumn = values[0]
                                for v in firstColumn:
                                        self.add_drift_component(float(v))

                                return True

                return False

        def get_value_for_frame(self, frame:float, n_frames:int) -> float | str | bool:
                """Return a drift value for the given `frame` number,
                assuming a total number of `n_frames` in the CT scan.
                If interpolation is activated, linear interpolation will
                take place between drift values, but also for frame numbers
                outside of the expected range: (frame < 0) and (frame >= n_frames).
                Note that the frame number starts at 0.

                Parameters
                ----------
                frame : float
                        Current frame number.

                n_frames : int
                        Total number of frames in scan.

                Returns
                -------
                value : float or str or bool
                        The drift's value for the given `frame` number.
                """

                nTrajectoryPoints = len(self.value)
                if nTrajectoryPoints is not None:
                        if nTrajectoryPoints > 1:
                                if n_frames > 1:
                                        # We know that we have at least two drift values, so we are
                                        # on the safe side for linear interpolations.
                                        # We also know that we have at least two frames in the scan,
                                        # and can therefore map our "scan progress" (the current
                                        # frame number) to the array of drift values.

                                        lastFrameNr = n_frames - 1 # frames start counting at 0

                                        # Frame progress on a scale between 0 and 1.
                                        # 0: first frame (start), 1: last frame (finish).
                                        progress = float(frame) / float(lastFrameNr)

                                        # Calculate the theoretical array index to get the drift
                                        # value for the current frame from the array of drift values:
                                        lastTrajectoryIndex = int(nTrajectoryPoints - 1)
                                        trajectoryIndex = progress * float(lastTrajectoryIndex)

                                        if (progress >= 0) and (progress <= 1):
                                                # We are inside the array of drift values.
                                                leftIndex = int(math.floor(trajectoryIndex))

                                                if float(leftIndex) == float(trajectoryIndex):
                                                        # We are exactly at one trajectory point;
                                                        # no need for interpolation.
                                                        return self.value[leftIndex]

                                                if self.interpolation is True:
                                                        # Linear interpolation:
                                                        rightIndex = int(leftIndex + 1)

                                                        # We return a weighted average of the two drift
                                                        # values where the current frame is "in between".
                                                        # Weight for the right bin is frac(trajectoryIndex).
                                                        rightWeight = float(trajectoryIndex) - float(math.floor(trajectoryIndex))

                                                        # Weight for the left bin is 1-rightWeight.
                                                        leftWeight = 1.0 - rightWeight

                                                        return leftWeight*self.value[leftIndex] \
                                                                + rightWeight*self.value[rightIndex]
                                                else:
                                                        # No interpolation.
                                                        # Return the value at the last drift value index
                                                        # that would apply to this frame position.
                                                        return self.value[leftIndex]
                                        else:
                                                # Linear interpolation beyond provided trajectory data.
                                                if progress > 1.0:
                                                        # We are beyond the expected last frame.
                                                        if self.interpolation is True:
                                                                # Linear interpolation beyond last two drift values:
                                                                trajectoryValue0 = self.value[int(lastTrajectoryIndex-1)]
                                                                trajectoryValue1 = self.value[int(lastTrajectoryIndex)]
                                                                offsetValue = trajectoryValue1

                                                                # We assume a linear interpolation function beyond the two
                                                                # last drift values. Taking the last frame as the zero point
                                                                # (i.e., the starting point) of this linear interpolation,
                                                                # the frame's position on the x axis would be:
                                                                xTraj = trajectoryIndex - float(lastTrajectoryIndex)
                                                        else:
                                                                # No interpolation. Return last trajectory value:
                                                                return self.value[lastTrajectoryIndex]
                                                else:
                                                        # We are before the first frame (i.e., before frame 0).
                                                        if self.interpolation is True:
                                                                trajectoryValue0 = self.value[0]
                                                                trajectoryValue1 = self.value[1]
                                                                offsetValue = trajectoryValue0

                                                                # We assume a linear interpolation function beyond the two
                                                                # last drift values. Taking the last frame as the zero point
                                                                # (i.e., the starting point) of this linear interpolation,
                                                                # the frame's position on the x axis would be:
                                                                xTraj = trajectoryIndex # is negative in this case
                                                        else:
                                                                # No interpolation. Return first trajectory value:
                                                                return self.value[0]

                                                # m = the slope of our linear interpolation function.
                                                # We are on the axis of trajectory drift values:
                                                # Our step size in x direction is 1 (trajectoryValue1 and
                                                # trajectoryValue0 are one trajectory step apart). Not
                                                # to be confused with the frame number.
                                                m = float(trajectoryValue1 - trajectoryValue0)
                                                driftValue = m * xTraj + offsetValue

                                                return driftValue
                                else:
                                        # n_frames <= 1
                                        # If "scan" only has 1 or 0 frames,
                                        # simply return the first trajectory value.
                                        return self.value[0]
                        else:
                                # trajectory points <= 1:
                                if nTrajectoryPoints > 0:
                                        # Simply check if the trajectory consists of at least
                                        # 1 point and return this value:
                                        return self.value[0]

                # Drifts are per-frame absolute deviations from the standard value,
                # so 0 is a sane default value for no drift components:
                return 0

Classes

class Drift (native_unit: str, preferred_unit: str = None)

Drift structure for a parameter. Drifts typically belong to a Parameter object, which keeps a list of possible parameter drifts.

Attributes

trajectory : list
List of drift values. If the number of drift values does not match the number of frames in the CT scan, a linear interpolation between the drift values can be used to calculate the drift value for a specific frame.
native_unit : str
The drift's native unit. Should match the native unit of the Parameter object to which this drift belongs. Possible values: None, "mm", "rad", "deg", "s", "mA", "kV", "g/cm^3", "lp/mm", "deg/s", "C", "bool", "string".
known_to_reconstruction : bool
Should this parameter drift be considered by the reconstruction software? This attribute is obeyed when calculating projection matrices.
interpolation : bool
Does a linear interpolation take place between the components of the trajectory list if the number of trajectory components does not match the number of frames in the CT scan?

A new drift object must be assigned a valid native unit to enable the JSON parser to convert the drift values from the JSON file, if necessary.

Parameters

native_unit : str
The drift's native unit. Should match the native unit of the Parameter object to which this drift belongs. Possible values: None, "mm", "rad", "deg", "s", "mA", "kV", "g/cm^3", "lp/mm", "deg/s", "C", "bool", "string".
preferred_unit : str
Preferred unit to represent these drift values in the JSON file. If set to None, the native unit will be used.
Expand source code
class Drift:
        """Drift structure for a parameter.
        Drifts typically belong to a `ctsimu.scenario.parameter.Parameter` object,
        which keeps a list of possible parameter drifts.

        Attributes
        ----------
        trajectory : list
                List of drift values. If the number of drift values does not
                match the number of frames in the CT scan, a linear interpolation
                between the drift values can be used to calculate the drift value
                for a specific frame.

        native_unit : str
                The drift's native unit. Should match the native unit of the
                `ctsimu.scenario.parameter.Parameter` object to which this drift belongs.
                Possible values: `None`, `"mm"`, `"rad"`, `"deg"`, `"s"`, `"mA"`, `"kV"`, `"g/cm^3"`, `"lp/mm"`, `"deg/s"`, `"C"`, `"bool"`, `"string"`.

        known_to_reconstruction : bool
                Should this parameter drift be considered by the reconstruction software?
                This attribute is obeyed when calculating projection matrices.

        interpolation : bool
                Does a linear interpolation take place between the components of the
                `trajectory` list if the number of trajectory components does not match
                the number of frames in the CT scan?
        """

        def __init__(self, native_unit:str, preferred_unit:str=None, _root=None):
                """A new drift object must be assigned a valid native unit
                to enable the JSON parser to convert the drift values from the
                JSON file, if necessary.

                Parameters
                ----------
                native_unit : str
                        The drift's native unit. Should match the native unit of the
                        `ctsimu.scenario.parameter.Parameter` object to which this drift belongs.
                        Possible values: `None`, `"mm"`, `"rad"`, `"deg"`, `"s"`, `"mA"`, `"kV"`, `"g/cm^3"`, `"lp/mm"`, `"deg/s"`, `"C"`, `"bool"`, `"string"`.

                preferred_unit : str
                        Preferred unit to represent these drift values in the JSON file.
                        If set to `None`, the native unit will be used.
                """
                self._root = _root  # root scenario object

                self.value = []
                self.native_unit = native_unit

                if preferred_unit is not None:
                        self.preferred_unit = preferred_unit
                else:
                        self.preferred_unit = native_unit

                self.known_to_reconstruction = True
                self.interpolation = True

                self.reset()

        def reset(self):
                """Clear the list of drift values, set `known_to_reconstruction` to `True`."""
                self.known_to_reconstruction = True
                self.value = []

        def json_dict(self) -> dict:
                """Create a CTSimU JSON dictionary for this drift.

                Returns
                -------
                json_dict : dict
                """
                jd = dict()
                if len(self.value) > 0:
                        jd["value"] = self.value
                else:
                        jd["value"] = None

                if self.native_unit not in native_units_to_omit_in_json_file:
                        unit = self.native_unit
                        if self.preferred_unit is not None:
                                unit = self.preferred_unit

                                # Convert value to preferred unit:
                                conversion_factor = convert_to_native_unit(given_unit=self.preferred_unit, native_unit=self.native_unit, value=1)

                                if conversion_factor != 0:
                                        jd["value"] = list()
                                        for val in self.value:
                                                jd["value"].append(val/conversion_factor)

                        jd["unit"] = unit

                jd["known_to_reconstruction"] = self.known_to_reconstruction
                return jd

        def set_known_to_reconstruction(self, known:bool):
                """Set the `known_to_reconstruction` property.

                Parameters
                ----------
                known : bool
                        Should the reconstruction software take this drift into account?
                """
                self.known_to_reconstruction = known

        def set_interpolation(self, intpol:bool):
                """Set the `interpolation` property. Only necessary for
                number-type parameters. String parameters cannot run an interpolation.

                Parameters
                ----------
                intpol : bool
                        `True` if the drift value should be interpolated if the number of
                        drift components does not match the number of frames in the CT scan.
                        `False` if no interpolation should take place: the last drift value
                        that would match the given frame number is taken.
                """
                self.interpolation = intpol

        def set_native_unit(self, native_unit:str):
                """Set the drifts's native unit.

                Parameters
                ----------
                native_unit : str
                        New native unit for the drift.
                        Possible values: `None`, `"mm"`, `"rad"`, `"deg"`, `"s"`, `"mA"`, `"kV"`, `"g/cm^3"`, `"lp/mm"`, `"deg/s"`, `"C"`, `"bool"`, `"string"`.
                """
                if is_valid_native_unit(native_unit):
                        self.native_unit = native_unit

                        if native_unit == "string":
                                self.interpolation = False

        def add_drift_component(self, value):
                """Add a drift component to the list of drift values.

                Parameters
                ----------
                value : float or str or bool
                        Drift value. For number-type drifts (`float`), this is given as an
                        absolute deviation from the parameter's standard value.
                        For string-type drifts (`str`), this is the string that should replace
                        the parameter's standard value.
                """
                self.value.append(value)

        def set_from_json(self, json_drift_object:dict) -> bool:
                """Set the drift from a given CTSimU drift structure.
                The proper `native_unit` must be set up correctly before
                running this function.

                Parameters
                ----------
                json_drift_object : dict
                        A CTSimU drift structure, imported from a JSON structure.

                Returns
                -------
                success : bool
                        `True` on success, `False` if an error occurred.
                """
                self.reset()

                # Get JSON unit:
                if json_exists_and_not_null(json_drift_object, ["unit"]):
                        self.preferred_unit = get_value(
                                dictionary=json_drift_object,
                                keys=["unit"],
                                fail_value=self.native_unit
                        )

                # Known to reconstruction
                if json_exists_and_not_null(json_drift_object, ["known_to_reconstruction"]):
                        self.set_known_to_reconstruction(
                                get_value_in_native_unit(
                                        native_unit="bool",
                                        dictionary=json_drift_object,
                                        keys=["known_to_reconstruction"],
                                        fail_value=True
                                )
                        )

                # Get drift values:
                if json_exists_and_not_null(json_drift_object, ["value"]):
                        value = json_extract(json_drift_object, ["value"])
                        if isinstance(value, list):
                                # Drift value is an array.
                                for v in value:
                                        self.add_drift_component(
                                                convert_to_native_unit(self.preferred_unit, self.native_unit, v)
                                        )

                                return True
                        elif isinstance(value, numbers.Number):
                                # Interpret value as a single number:
                                self.add_drift_component(
                                        convert_to_native_unit(self.preferred_unit, self.native_unit, value)
                                )
                                return True
                elif json_exists_and_not_null(json_drift_object, ["file"]):
                        filename = json_extract(json_drift_object, ["file"])
                        if isinstance(filename, str):
                                # Read drift values from a file
                                if self._root is not None:
                                        # Get path relative to JSON file:
                                        rel_filename = self._root.path_of_external_file(filename)
                                else:
                                        rel_filename = filename

                                values = read_csv_file(rel_filename)

                                firstColumn = values[0]
                                for v in firstColumn:
                                        self.add_drift_component(float(v))

                                return True

                return False

        def get_value_for_frame(self, frame:float, n_frames:int) -> float | str | bool:
                """Return a drift value for the given `frame` number,
                assuming a total number of `n_frames` in the CT scan.
                If interpolation is activated, linear interpolation will
                take place between drift values, but also for frame numbers
                outside of the expected range: (frame < 0) and (frame >= n_frames).
                Note that the frame number starts at 0.

                Parameters
                ----------
                frame : float
                        Current frame number.

                n_frames : int
                        Total number of frames in scan.

                Returns
                -------
                value : float or str or bool
                        The drift's value for the given `frame` number.
                """

                nTrajectoryPoints = len(self.value)
                if nTrajectoryPoints is not None:
                        if nTrajectoryPoints > 1:
                                if n_frames > 1:
                                        # We know that we have at least two drift values, so we are
                                        # on the safe side for linear interpolations.
                                        # We also know that we have at least two frames in the scan,
                                        # and can therefore map our "scan progress" (the current
                                        # frame number) to the array of drift values.

                                        lastFrameNr = n_frames - 1 # frames start counting at 0

                                        # Frame progress on a scale between 0 and 1.
                                        # 0: first frame (start), 1: last frame (finish).
                                        progress = float(frame) / float(lastFrameNr)

                                        # Calculate the theoretical array index to get the drift
                                        # value for the current frame from the array of drift values:
                                        lastTrajectoryIndex = int(nTrajectoryPoints - 1)
                                        trajectoryIndex = progress * float(lastTrajectoryIndex)

                                        if (progress >= 0) and (progress <= 1):
                                                # We are inside the array of drift values.
                                                leftIndex = int(math.floor(trajectoryIndex))

                                                if float(leftIndex) == float(trajectoryIndex):
                                                        # We are exactly at one trajectory point;
                                                        # no need for interpolation.
                                                        return self.value[leftIndex]

                                                if self.interpolation is True:
                                                        # Linear interpolation:
                                                        rightIndex = int(leftIndex + 1)

                                                        # We return a weighted average of the two drift
                                                        # values where the current frame is "in between".
                                                        # Weight for the right bin is frac(trajectoryIndex).
                                                        rightWeight = float(trajectoryIndex) - float(math.floor(trajectoryIndex))

                                                        # Weight for the left bin is 1-rightWeight.
                                                        leftWeight = 1.0 - rightWeight

                                                        return leftWeight*self.value[leftIndex] \
                                                                + rightWeight*self.value[rightIndex]
                                                else:
                                                        # No interpolation.
                                                        # Return the value at the last drift value index
                                                        # that would apply to this frame position.
                                                        return self.value[leftIndex]
                                        else:
                                                # Linear interpolation beyond provided trajectory data.
                                                if progress > 1.0:
                                                        # We are beyond the expected last frame.
                                                        if self.interpolation is True:
                                                                # Linear interpolation beyond last two drift values:
                                                                trajectoryValue0 = self.value[int(lastTrajectoryIndex-1)]
                                                                trajectoryValue1 = self.value[int(lastTrajectoryIndex)]
                                                                offsetValue = trajectoryValue1

                                                                # We assume a linear interpolation function beyond the two
                                                                # last drift values. Taking the last frame as the zero point
                                                                # (i.e., the starting point) of this linear interpolation,
                                                                # the frame's position on the x axis would be:
                                                                xTraj = trajectoryIndex - float(lastTrajectoryIndex)
                                                        else:
                                                                # No interpolation. Return last trajectory value:
                                                                return self.value[lastTrajectoryIndex]
                                                else:
                                                        # We are before the first frame (i.e., before frame 0).
                                                        if self.interpolation is True:
                                                                trajectoryValue0 = self.value[0]
                                                                trajectoryValue1 = self.value[1]
                                                                offsetValue = trajectoryValue0

                                                                # We assume a linear interpolation function beyond the two
                                                                # last drift values. Taking the last frame as the zero point
                                                                # (i.e., the starting point) of this linear interpolation,
                                                                # the frame's position on the x axis would be:
                                                                xTraj = trajectoryIndex # is negative in this case
                                                        else:
                                                                # No interpolation. Return first trajectory value:
                                                                return self.value[0]

                                                # m = the slope of our linear interpolation function.
                                                # We are on the axis of trajectory drift values:
                                                # Our step size in x direction is 1 (trajectoryValue1 and
                                                # trajectoryValue0 are one trajectory step apart). Not
                                                # to be confused with the frame number.
                                                m = float(trajectoryValue1 - trajectoryValue0)
                                                driftValue = m * xTraj + offsetValue

                                                return driftValue
                                else:
                                        # n_frames <= 1
                                        # If "scan" only has 1 or 0 frames,
                                        # simply return the first trajectory value.
                                        return self.value[0]
                        else:
                                # trajectory points <= 1:
                                if nTrajectoryPoints > 0:
                                        # Simply check if the trajectory consists of at least
                                        # 1 point and return this value:
                                        return self.value[0]

                # Drifts are per-frame absolute deviations from the standard value,
                # so 0 is a sane default value for no drift components:
                return 0

Methods

def add_drift_component(self, value)

Add a drift component to the list of drift values.

Parameters

value : float or str or bool
Drift value. For number-type drifts (float), this is given as an absolute deviation from the parameter's standard value. For string-type drifts (str), this is the string that should replace the parameter's standard value.
Expand source code
def add_drift_component(self, value):
        """Add a drift component to the list of drift values.

        Parameters
        ----------
        value : float or str or bool
                Drift value. For number-type drifts (`float`), this is given as an
                absolute deviation from the parameter's standard value.
                For string-type drifts (`str`), this is the string that should replace
                the parameter's standard value.
        """
        self.value.append(value)
def get_value_for_frame(self, frame: float, n_frames: int) ‑> float | str | bool

Return a drift value for the given frame number, assuming a total number of n_frames in the CT scan. If interpolation is activated, linear interpolation will take place between drift values, but also for frame numbers outside of the expected range: (frame < 0) and (frame >= n_frames). Note that the frame number starts at 0.

Parameters

frame : float
Current frame number.
n_frames : int
Total number of frames in scan.

Returns

value : float or str or bool
The drift's value for the given frame number.
Expand source code
def get_value_for_frame(self, frame:float, n_frames:int) -> float | str | bool:
        """Return a drift value for the given `frame` number,
        assuming a total number of `n_frames` in the CT scan.
        If interpolation is activated, linear interpolation will
        take place between drift values, but also for frame numbers
        outside of the expected range: (frame < 0) and (frame >= n_frames).
        Note that the frame number starts at 0.

        Parameters
        ----------
        frame : float
                Current frame number.

        n_frames : int
                Total number of frames in scan.

        Returns
        -------
        value : float or str or bool
                The drift's value for the given `frame` number.
        """

        nTrajectoryPoints = len(self.value)
        if nTrajectoryPoints is not None:
                if nTrajectoryPoints > 1:
                        if n_frames > 1:
                                # We know that we have at least two drift values, so we are
                                # on the safe side for linear interpolations.
                                # We also know that we have at least two frames in the scan,
                                # and can therefore map our "scan progress" (the current
                                # frame number) to the array of drift values.

                                lastFrameNr = n_frames - 1 # frames start counting at 0

                                # Frame progress on a scale between 0 and 1.
                                # 0: first frame (start), 1: last frame (finish).
                                progress = float(frame) / float(lastFrameNr)

                                # Calculate the theoretical array index to get the drift
                                # value for the current frame from the array of drift values:
                                lastTrajectoryIndex = int(nTrajectoryPoints - 1)
                                trajectoryIndex = progress * float(lastTrajectoryIndex)

                                if (progress >= 0) and (progress <= 1):
                                        # We are inside the array of drift values.
                                        leftIndex = int(math.floor(trajectoryIndex))

                                        if float(leftIndex) == float(trajectoryIndex):
                                                # We are exactly at one trajectory point;
                                                # no need for interpolation.
                                                return self.value[leftIndex]

                                        if self.interpolation is True:
                                                # Linear interpolation:
                                                rightIndex = int(leftIndex + 1)

                                                # We return a weighted average of the two drift
                                                # values where the current frame is "in between".
                                                # Weight for the right bin is frac(trajectoryIndex).
                                                rightWeight = float(trajectoryIndex) - float(math.floor(trajectoryIndex))

                                                # Weight for the left bin is 1-rightWeight.
                                                leftWeight = 1.0 - rightWeight

                                                return leftWeight*self.value[leftIndex] \
                                                        + rightWeight*self.value[rightIndex]
                                        else:
                                                # No interpolation.
                                                # Return the value at the last drift value index
                                                # that would apply to this frame position.
                                                return self.value[leftIndex]
                                else:
                                        # Linear interpolation beyond provided trajectory data.
                                        if progress > 1.0:
                                                # We are beyond the expected last frame.
                                                if self.interpolation is True:
                                                        # Linear interpolation beyond last two drift values:
                                                        trajectoryValue0 = self.value[int(lastTrajectoryIndex-1)]
                                                        trajectoryValue1 = self.value[int(lastTrajectoryIndex)]
                                                        offsetValue = trajectoryValue1

                                                        # We assume a linear interpolation function beyond the two
                                                        # last drift values. Taking the last frame as the zero point
                                                        # (i.e., the starting point) of this linear interpolation,
                                                        # the frame's position on the x axis would be:
                                                        xTraj = trajectoryIndex - float(lastTrajectoryIndex)
                                                else:
                                                        # No interpolation. Return last trajectory value:
                                                        return self.value[lastTrajectoryIndex]
                                        else:
                                                # We are before the first frame (i.e., before frame 0).
                                                if self.interpolation is True:
                                                        trajectoryValue0 = self.value[0]
                                                        trajectoryValue1 = self.value[1]
                                                        offsetValue = trajectoryValue0

                                                        # We assume a linear interpolation function beyond the two
                                                        # last drift values. Taking the last frame as the zero point
                                                        # (i.e., the starting point) of this linear interpolation,
                                                        # the frame's position on the x axis would be:
                                                        xTraj = trajectoryIndex # is negative in this case
                                                else:
                                                        # No interpolation. Return first trajectory value:
                                                        return self.value[0]

                                        # m = the slope of our linear interpolation function.
                                        # We are on the axis of trajectory drift values:
                                        # Our step size in x direction is 1 (trajectoryValue1 and
                                        # trajectoryValue0 are one trajectory step apart). Not
                                        # to be confused with the frame number.
                                        m = float(trajectoryValue1 - trajectoryValue0)
                                        driftValue = m * xTraj + offsetValue

                                        return driftValue
                        else:
                                # n_frames <= 1
                                # If "scan" only has 1 or 0 frames,
                                # simply return the first trajectory value.
                                return self.value[0]
                else:
                        # trajectory points <= 1:
                        if nTrajectoryPoints > 0:
                                # Simply check if the trajectory consists of at least
                                # 1 point and return this value:
                                return self.value[0]

        # Drifts are per-frame absolute deviations from the standard value,
        # so 0 is a sane default value for no drift components:
        return 0
def json_dict(self) ‑> dict

Create a CTSimU JSON dictionary for this drift.

Returns

json_dict : dict
 
Expand source code
def json_dict(self) -> dict:
        """Create a CTSimU JSON dictionary for this drift.

        Returns
        -------
        json_dict : dict
        """
        jd = dict()
        if len(self.value) > 0:
                jd["value"] = self.value
        else:
                jd["value"] = None

        if self.native_unit not in native_units_to_omit_in_json_file:
                unit = self.native_unit
                if self.preferred_unit is not None:
                        unit = self.preferred_unit

                        # Convert value to preferred unit:
                        conversion_factor = convert_to_native_unit(given_unit=self.preferred_unit, native_unit=self.native_unit, value=1)

                        if conversion_factor != 0:
                                jd["value"] = list()
                                for val in self.value:
                                        jd["value"].append(val/conversion_factor)

                jd["unit"] = unit

        jd["known_to_reconstruction"] = self.known_to_reconstruction
        return jd
def reset(self)

Clear the list of drift values, set known_to_reconstruction to True.

Expand source code
def reset(self):
        """Clear the list of drift values, set `known_to_reconstruction` to `True`."""
        self.known_to_reconstruction = True
        self.value = []
def set_from_json(self, json_drift_object: dict) ‑> bool

Set the drift from a given CTSimU drift structure. The proper native_unit must be set up correctly before running this function.

Parameters

json_drift_object : dict
A CTSimU drift structure, imported from a JSON structure.

Returns

success : bool
True on success, False if an error occurred.
Expand source code
def set_from_json(self, json_drift_object:dict) -> bool:
        """Set the drift from a given CTSimU drift structure.
        The proper `native_unit` must be set up correctly before
        running this function.

        Parameters
        ----------
        json_drift_object : dict
                A CTSimU drift structure, imported from a JSON structure.

        Returns
        -------
        success : bool
                `True` on success, `False` if an error occurred.
        """
        self.reset()

        # Get JSON unit:
        if json_exists_and_not_null(json_drift_object, ["unit"]):
                self.preferred_unit = get_value(
                        dictionary=json_drift_object,
                        keys=["unit"],
                        fail_value=self.native_unit
                )

        # Known to reconstruction
        if json_exists_and_not_null(json_drift_object, ["known_to_reconstruction"]):
                self.set_known_to_reconstruction(
                        get_value_in_native_unit(
                                native_unit="bool",
                                dictionary=json_drift_object,
                                keys=["known_to_reconstruction"],
                                fail_value=True
                        )
                )

        # Get drift values:
        if json_exists_and_not_null(json_drift_object, ["value"]):
                value = json_extract(json_drift_object, ["value"])
                if isinstance(value, list):
                        # Drift value is an array.
                        for v in value:
                                self.add_drift_component(
                                        convert_to_native_unit(self.preferred_unit, self.native_unit, v)
                                )

                        return True
                elif isinstance(value, numbers.Number):
                        # Interpret value as a single number:
                        self.add_drift_component(
                                convert_to_native_unit(self.preferred_unit, self.native_unit, value)
                        )
                        return True
        elif json_exists_and_not_null(json_drift_object, ["file"]):
                filename = json_extract(json_drift_object, ["file"])
                if isinstance(filename, str):
                        # Read drift values from a file
                        if self._root is not None:
                                # Get path relative to JSON file:
                                rel_filename = self._root.path_of_external_file(filename)
                        else:
                                rel_filename = filename

                        values = read_csv_file(rel_filename)

                        firstColumn = values[0]
                        for v in firstColumn:
                                self.add_drift_component(float(v))

                        return True

        return False
def set_interpolation(self, intpol: bool)

Set the interpolation property. Only necessary for number-type parameters. String parameters cannot run an interpolation.

Parameters

intpol : bool
True if the drift value should be interpolated if the number of drift components does not match the number of frames in the CT scan. False if no interpolation should take place: the last drift value that would match the given frame number is taken.
Expand source code
def set_interpolation(self, intpol:bool):
        """Set the `interpolation` property. Only necessary for
        number-type parameters. String parameters cannot run an interpolation.

        Parameters
        ----------
        intpol : bool
                `True` if the drift value should be interpolated if the number of
                drift components does not match the number of frames in the CT scan.
                `False` if no interpolation should take place: the last drift value
                that would match the given frame number is taken.
        """
        self.interpolation = intpol
def set_known_to_reconstruction(self, known: bool)

Set the known_to_reconstruction property.

Parameters

known : bool
Should the reconstruction software take this drift into account?
Expand source code
def set_known_to_reconstruction(self, known:bool):
        """Set the `known_to_reconstruction` property.

        Parameters
        ----------
        known : bool
                Should the reconstruction software take this drift into account?
        """
        self.known_to_reconstruction = known
def set_native_unit(self, native_unit: str)

Set the drifts's native unit.

Parameters

native_unit : str
New native unit for the drift. Possible values: None, "mm", "rad", "deg", "s", "mA", "kV", "g/cm^3", "lp/mm", "deg/s", "C", "bool", "string".
Expand source code
def set_native_unit(self, native_unit:str):
        """Set the drifts's native unit.

        Parameters
        ----------
        native_unit : str
                New native unit for the drift.
                Possible values: `None`, `"mm"`, `"rad"`, `"deg"`, `"s"`, `"mA"`, `"kV"`, `"g/cm^3"`, `"lp/mm"`, `"deg/s"`, `"C"`, `"bool"`, `"string"`.
        """
        if is_valid_native_unit(native_unit):
                self.native_unit = native_unit

                if native_unit == "string":
                        self.interpolation = False