Skip to content

Commit

Permalink
Refactored (base) classes from init.py into separate files
Browse files Browse the repository at this point in the history
  • Loading branch information
TomTomRixRix committed Jul 19, 2024
1 parent 5a6aa6b commit 812b462
Show file tree
Hide file tree
Showing 24 changed files with 1,087 additions and 1,034 deletions.
30 changes: 1 addition & 29 deletions simpa/core/__init__.py
Original file line number Diff line number Diff line change
@@ -1,32 +1,4 @@
# SPDX-FileCopyrightText: 2021 Division of Intelligent Medical Systems, DKFZ
# SPDX-FileCopyrightText: 2021 Janek Groehl
# SPDX-License-Identifier: MIT
from abc import abstractmethod

from simpa.core.device_digital_twins import DigitalDeviceTwinBase
from simpa.log import Logger
from simpa.utils import Settings
from simpa.utils.processing_device import get_processing_device

class PipelineModule:
"""
Defines a pipeline module (either simulation or processing module) that implements a run method and can be called by running the pipeline's simulate method.
"""
def __init__(self, global_settings: Settings):
"""
:param global_settings: The SIMPA settings dictionary
:type global_settings: Settings
"""
self.logger = Logger()
self.global_settings = global_settings
self.torch_device = get_processing_device(self.global_settings)

@abstractmethod
def run(self, digital_device_twin: DigitalDeviceTwinBase):
"""
Executes the respective simulation module
:param digital_device_twin: The digital twin that can be used by the digital device_twin.
"""
pass

from .pipeline_module import PipelineModule
170 changes: 19 additions & 151 deletions simpa/core/device_digital_twins/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,154 +2,22 @@
# SPDX-FileCopyrightText: 2021 Janek Groehl
# SPDX-License-Identifier: MIT

from abc import abstractmethod
from simpa.log import Logger
import numpy as np
import hashlib
import uuid
from simpa.utils.serializer import SerializableSIMPAClass
from simpa.utils.calculate import are_equal


class DigitalDeviceTwinBase(SerializableSIMPAClass):
"""
This class represents a device that can be used for illumination, detection or a combined photoacoustic device
which has representations of both.
"""

def __init__(self, device_position_mm=None, field_of_view_extent_mm=None):
"""
:param device_position_mm: Each device has an internal position which serves as origin for internal \
representations of e.g. detector element positions or illuminator positions.
:type device_position_mm: ndarray
:param field_of_view_extent_mm: Field of view which is defined as a numpy array of the shape \
[xs, xe, ys, ye, zs, ze], where x, y, and z denote the coordinate axes and s and e denote the start and end \
positions.
:type field_of_view_extent_mm: ndarray
"""
if device_position_mm is None:
self.device_position_mm = np.array([0, 0, 0])
else:
self.device_position_mm = device_position_mm

if field_of_view_extent_mm is None:
self.field_of_view_extent_mm = np.asarray([-10, 10, -10, 10, -10, 10])
else:
self.field_of_view_extent_mm = field_of_view_extent_mm

self.logger = Logger()

def __eq__(self, other):
"""
Checks each key, value pair in the devices.
"""
if isinstance(other, DigitalDeviceTwinBase):
if self.__dict__.keys() != other.__dict__.keys():
return False
for self_key, self_value in self.__dict__.items():
other_value = other.__dict__[self_key]
if not are_equal(self_value, other_value):
return False
return True
return False

@abstractmethod
def check_settings_prerequisites(self, global_settings) -> bool:
"""
It might be that certain device geometries need a certain dimensionality of the simulated PAI volume, or that
it requires the existence of certain Tags in the global global_settings.
To this end, a PAI device should use this method to inform the user about a mismatch of the desired device and
throw a ValueError if that is the case.
:param global_settings: Settings for the entire simulation pipeline.
:type global_settings: Settings
:raises ValueError: raises a value error if the prerequisites are not matched.
:returns: True if the prerequisites are met, False if they are not met, but no exception has been raised.
:rtype: bool
"""
pass

@abstractmethod
def update_settings_for_use_of_model_based_volume_creator(self, global_settings):
"""
This method can be overwritten by a PA device if the device poses special constraints to the
volume that should be considered by the model-based volume creator.
:param global_settings: Settings for the entire simulation pipeline.
:type global_settings: Settings
"""
pass

def get_field_of_view_mm(self) -> np.ndarray:
"""
Returns the absolute field of view in mm where the probe position is already
accounted for.
It is defined as a numpy array of the shape [xs, xe, ys, ye, zs, ze],
where x, y, and z denote the coordinate axes and s and e denote the start and end
positions.
:return: Absolute field of view in mm where the probe position is already accounted for.
:rtype: ndarray
"""
position = self.device_position_mm
field_of_view_extent = self.field_of_view_extent_mm

field_of_view = np.asarray([position[0] + field_of_view_extent[0],
position[0] + field_of_view_extent[1],
position[1] + field_of_view_extent[2],
position[1] + field_of_view_extent[3],
position[2] + field_of_view_extent[4],
position[2] + field_of_view_extent[5]
])
if min(field_of_view) < 0:
self.logger.warning(f"The field of view of the chosen device is not fully within the simulated volume, "
f"field of view is: {field_of_view}")
field_of_view[field_of_view < 0] = 0

return field_of_view

def generate_uuid(self):
"""
Generates a universally unique identifier (uuid) for each device.
:return:
"""
class_dict = self.__dict__
m = hashlib.md5()
m.update(str(class_dict).encode('utf-8'))
return str(uuid.UUID(m.hexdigest()))

def serialize(self) -> dict:
serialized_device = self.__dict__
return {"DigitalDeviceTwinBase": serialized_device}

@staticmethod
def deserialize(dictionary_to_deserialize):
deserialized_device = DigitalDeviceTwinBase(
device_position_mm=dictionary_to_deserialize["device_position_mm"],
field_of_view_extent_mm=dictionary_to_deserialize["field_of_view_extent_mm"])
return deserialized_device


"""
It is important to have these relative imports after the definition of the DigitalDeviceTwinBase class to avoid circular imports triggered by imported child classes
"""
from .pa_devices import PhotoacousticDevice # nopep8
from simpa.core.device_digital_twins.detection_geometries import DetectionGeometryBase # nopep8
from simpa.core.device_digital_twins.illumination_geometries import IlluminationGeometryBase # nopep8
from .detection_geometries.curved_array import CurvedArrayDetectionGeometry # nopep8
from .detection_geometries.linear_array import LinearArrayDetectionGeometry # nopep8
from .detection_geometries.planar_array import PlanarArrayDetectionGeometry # nopep8
from .illumination_geometries.slit_illumination import SlitIlluminationGeometry # nopep8
from .illumination_geometries.gaussian_beam_illumination import GaussianBeamIlluminationGeometry # nopep8
from .illumination_geometries.pencil_array_illumination import PencilArrayIlluminationGeometry # nopep8
from .illumination_geometries.pencil_beam_illumination import PencilBeamIlluminationGeometry # nopep8
from .illumination_geometries.disk_illumination import DiskIlluminationGeometry # nopep8
from .illumination_geometries.rectangle_illumination import RectangleIlluminationGeometry # nopep8
from .illumination_geometries.ring_illumination import RingIlluminationGeometry # nopep8
from .illumination_geometries.ithera_msot_acuity_illumination import MSOTAcuityIlluminationGeometry # nopep8
from .illumination_geometries.ithera_msot_invision_illumination import MSOTInVisionIlluminationGeometry # nopep8
from .pa_devices.ithera_msot_invision import InVision256TF # nopep8
from .pa_devices.ithera_msot_acuity import MSOTAcuityEcho # nopep8
from .pa_devices.ithera_rsom import RSOMExplorerP50 # nopep8
from .digital_device_twin_base import DigitalDeviceTwinBase
from .pa_devices import PhotoacousticDevice
from .detection_geometries import DetectionGeometryBase
from .detection_geometries.curved_array import CurvedArrayDetectionGeometry
from .detection_geometries.linear_array import LinearArrayDetectionGeometry
from .detection_geometries.planar_array import PlanarArrayDetectionGeometry
from .illumination_geometries import IlluminationGeometryBase
from .illumination_geometries.slit_illumination import SlitIlluminationGeometry
from .illumination_geometries.gaussian_beam_illumination import GaussianBeamIlluminationGeometry
from .illumination_geometries.pencil_array_illumination import PencilArrayIlluminationGeometry
from .illumination_geometries.pencil_beam_illumination import PencilBeamIlluminationGeometry
from .illumination_geometries.disk_illumination import DiskIlluminationGeometry
from .illumination_geometries.rectangle_illumination import RectangleIlluminationGeometry
from .illumination_geometries.ring_illumination import RingIlluminationGeometry
from .illumination_geometries.ithera_msot_acuity_illumination import MSOTAcuityIlluminationGeometry
from .illumination_geometries.ithera_msot_invision_illumination import MSOTInVisionIlluminationGeometry
from .pa_devices.ithera_msot_invision import InVision256TF
from .pa_devices.ithera_msot_acuity import MSOTAcuityEcho
from .pa_devices.ithera_rsom import RSOMExplorerP50
133 changes: 1 addition & 132 deletions simpa/core/device_digital_twins/detection_geometries/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,135 +2,4 @@
# SPDX-FileCopyrightText: 2021 Janek Groehl
# SPDX-License-Identifier: MIT

from abc import abstractmethod
from simpa.core.device_digital_twins import DigitalDeviceTwinBase
import numpy as np


class DetectionGeometryBase(DigitalDeviceTwinBase):
"""
This class is the base class for representing all detector geometries.
"""

def __init__(self, number_detector_elements, detector_element_width_mm,
detector_element_length_mm, center_frequency_hz, bandwidth_percent,
sampling_frequency_mhz, device_position_mm: np.ndarray = None,
field_of_view_extent_mm: np.ndarray = None):
"""
:param number_detector_elements: Total number of detector elements.
:type number_detector_elements: int
:param detector_element_width_mm: In-plane width of one detector element (pitch - distance between two
elements) in mm.
:type detector_element_width_mm: int, float
:param detector_element_length_mm: Out-of-plane length of one detector element in mm.
:type detector_element_length_mm: int, float
:param center_frequency_hz: Center frequency of the detector with approximately gaussian frequency response in
Hz.
:type center_frequency_hz: int, float
:param bandwidth_percent: Full width at half maximum in percent of the center frequency.
:type bandwidth_percent: int, float
:param sampling_frequency_mhz: Sampling frequency of the detector in MHz.
:type sampling_frequency_mhz: int, float
:param device_position_mm: Each device has an internal position which serves as origin for internal \
representations of detector positions.
:type device_position_mm: ndarray
:param field_of_view_extent_mm: Field of view which is defined as a numpy array of the shape \
[xs, xe, ys, ye, zs, ze], where x, y, and z denote the coordinate axes and s and e denote the start and end \
positions.
:type field_of_view_extent_mm: ndarray
"""
super(DetectionGeometryBase, self).__init__(device_position_mm=device_position_mm,
field_of_view_extent_mm=field_of_view_extent_mm)
self.number_detector_elements = number_detector_elements
self.detector_element_width_mm = detector_element_width_mm
self.detector_element_length_mm = detector_element_length_mm
self.center_frequency_Hz = center_frequency_hz
self.bandwidth_percent = bandwidth_percent
self.sampling_frequency_MHz = sampling_frequency_mhz

@abstractmethod
def get_detector_element_positions_base_mm(self) -> np.ndarray:
"""
Defines the abstract positions of the detection elements in an arbitrary coordinate system.
Typically, the center of the field of view is defined as the origin.
To obtain the positions in an interpretable coordinate system, please use the other method::
get_detector_element_positions_accounting_for_device_position_mm
:returns: A numpy array containing the position vectors of the detection elements.
"""
pass

def get_detector_element_positions_accounting_for_device_position_mm(self) -> np.ndarray:
"""
Similar to::
get_detector_element_positions_base_mm
This method returns the absolute positions of the detection elements relative to the device
position in the imaged volume, where the device position is defined by the following tag::
Tags.DIGITAL_DEVICE_POSITION
:returns: A numpy array containing the coordinates of the detection elements
"""
abstract_element_positions = self.get_detector_element_positions_base_mm()
device_position = self.device_position_mm
return np.add(abstract_element_positions, device_position)

def get_detector_element_positions_accounting_for_field_of_view(self) -> np.ndarray:
"""
Similar to::
get_detector_element_positions_base_mm
This method returns the absolute positions of the detection elements relative to the device
position in the imaged volume, where the device position is defined by the following tag::
Tags.DIGITAL_DEVICE_POSITION
:returns: A numpy array containing the coordinates of the detection elements
"""
abstract_element_positions = np.copy(self.get_detector_element_positions_base_mm())
field_of_view = self.field_of_view_extent_mm
x_half = (field_of_view[1] - field_of_view[0]) / 2
y_half = (field_of_view[3] - field_of_view[2]) / 2
if np.abs(x_half) < 1e-10:
abstract_element_positions[:, 0] = 0
if np.abs(y_half) < 1e-10:
abstract_element_positions[:, 1] = 0

abstract_element_positions[:, 0] += x_half
abstract_element_positions[:, 1] += y_half
abstract_element_positions[:, 2] += field_of_view[4]
return abstract_element_positions

@abstractmethod
def get_detector_element_orientations(self) -> np.ndarray:
"""
This method yields a normalised orientation vector for each detection element. The length of
this vector is the same as the one obtained via the position methods::
get_detector_element_positions_base_mm
get_detector_element_positions_accounting_for_device_position_mm
:returns: a numpy array that contains normalised orientation vectors for each detection element
"""
pass

def serialize(self) -> dict:
serialized_device = self.__dict__
return {DetectionGeometryBase: serialized_device}

@staticmethod
def deserialize(dictionary_to_deserialize):
deserialized_device = DetectionGeometryBase()
for key, value in dictionary_to_deserialize.items():
deserialized_device.__dict__[key] = value
return deserialized_device
from .detection_geometry_base import DetectionGeometryBase
Loading

0 comments on commit 812b462

Please sign in to comment.