From 89822475a3e129540f5e140748a302a6ee53f7c2 Mon Sep 17 00:00:00 2001 From: Kris Dreher Date: Fri, 8 Mar 2024 13:44:33 +0100 Subject: [PATCH 01/34] Initial implementation of adding refractive index to property maps --- .../volume_creation_module/__init__.py | 3 +- simpa/utils/calculate.py | 4 +- simpa/utils/constants.py | 35 ++++++++++++++++ simpa/utils/dict_path_manager.py | 41 +++++-------------- simpa/utils/libraries/molecule_library.py | 30 ++++++++++---- .../__init__.py | 3 ++ simpa/utils/libraries/spectrum_library.py | 15 ++++++- simpa/utils/tags.py | 6 +++ simpa/utils/tissue_properties.py | 16 +------- .../matplotlib_data_visualisation.py | 7 ++++ simpa_examples/minimal_optical_simulation.py | 3 +- 11 files changed, 103 insertions(+), 60 deletions(-) create mode 100644 simpa/utils/libraries/refractive_index_spectrum_data/__init__.py diff --git a/simpa/core/simulation_modules/volume_creation_module/__init__.py b/simpa/core/simulation_modules/volume_creation_module/__init__.py index 991c7669..2b5cd4a1 100644 --- a/simpa/core/simulation_modules/volume_creation_module/__init__.py +++ b/simpa/core/simulation_modules/volume_creation_module/__init__.py @@ -13,6 +13,7 @@ from simpa.io_handling import save_data_field from simpa.utils.quality_assurance.data_sanity_testing import assert_equal_shapes, assert_array_well_defined from simpa.utils.processing_device import get_processing_device +from simpa.utils.constants import wavelength_independent_properties class VolumeCreatorModuleBase(SimulationModule): @@ -39,7 +40,7 @@ def create_empty_volumes(self): for key in TissueProperties.property_tags: # Create wavelength-independent properties only in the first wavelength run - if key in TissueProperties.wavelength_independent_properties and wavelength != first_wavelength: + if key in wavelength_independent_properties and wavelength != first_wavelength: continue volumes[key] = torch.zeros(sizes, dtype=torch.float, device=self.torch_device) diff --git a/simpa/utils/calculate.py b/simpa/utils/calculate.py index a2d60123..3533f4bd 100644 --- a/simpa/utils/calculate.py +++ b/simpa/utils/calculate.py @@ -15,9 +15,9 @@ def calculate_oxygenation(molecule_list): hbO2 = None for molecule in molecule_list: - if molecule.spectrum.spectrum_name == "Deoxyhemoglobin": + if molecule.absorption_spectrum.spectrum_name == "Deoxyhemoglobin": hb = molecule.volume_fraction - if molecule.spectrum.spectrum_name == "Oxyhemoglobin": + if molecule.absorption_spectrum.spectrum_name == "Oxyhemoglobin": hbO2 = molecule.volume_fraction if hb is None and hbO2 is None: diff --git a/simpa/utils/constants.py b/simpa/utils/constants.py index 8e8520e8..d527cf65 100644 --- a/simpa/utils/constants.py +++ b/simpa/utils/constants.py @@ -2,6 +2,8 @@ # SPDX-FileCopyrightText: 2021 Janek Groehl # SPDX-License-Identifier: MIT +from simpa.utils.tags import Tags + EPS = 1e-20 """ Defines the smallest increment that should be considered by SIMPA. @@ -27,3 +29,36 @@ class SegmentationClasses: MEDIPRENE = 11 SOFT_TISSUE = 12 LYMPH_NODE = 13 + + +wavelength_dependent_properties = [Tags.DATA_FIELD_ABSORPTION_PER_CM, + Tags.DATA_FIELD_SCATTERING_PER_CM, + Tags.DATA_FIELD_ANISOTROPY, + Tags.DATA_FIELD_REFRACTIVE_INDEX] + +wavelength_independent_properties = [Tags.DATA_FIELD_OXYGENATION, + Tags.DATA_FIELD_SEGMENTATION, + Tags.DATA_FIELD_GRUNEISEN_PARAMETER, + Tags.DATA_FIELD_SPEED_OF_SOUND, + Tags.DATA_FIELD_DENSITY, + Tags.DATA_FIELD_ALPHA_COEFF] + +toolkit_properties = [Tags.KWAVE_PROPERTY_SENSOR_MASK, + Tags.KWAVE_PROPERTY_DIRECTIVITY_ANGLE] + +simulation_output = [Tags.DATA_FIELD_FLUENCE, + Tags.DATA_FIELD_INITIAL_PRESSURE, + Tags.OPTICAL_MODEL_UNITS, + Tags.DATA_FIELD_TIME_SERIES_DATA, + Tags.DATA_FIELD_RECONSTRUCTED_DATA, + Tags.DATA_FIELD_DIFFUSE_REFLECTANCE, + Tags.DATA_FIELD_DIFFUSE_REFLECTANCE_POS, + Tags.DATA_FIELD_PHOTON_EXIT_POS, + Tags.DATA_FIELD_PHOTON_EXIT_DIR] + +simulation_output_fields = [Tags.OPTICAL_MODEL_OUTPUT_NAME, + Tags.SIMULATION_PROPERTIES] + +wavelength_dependent_image_processing_output = [Tags.ITERATIVE_qPAI_RESULT] + +wavelength_independent_image_processing_output = [Tags.LINEAR_UNMIXING_RESULT] diff --git a/simpa/utils/dict_path_manager.py b/simpa/utils/dict_path_manager.py index 86d17a62..5fcc5da6 100644 --- a/simpa/utils/dict_path_manager.py +++ b/simpa/utils/dict_path_manager.py @@ -3,6 +3,13 @@ # SPDX-License-Identifier: MIT from simpa.utils import Tags +from simpa.utils.constants import (wavelength_dependent_properties, + wavelength_independent_properties, + wavelength_dependent_image_processing_output, + wavelength_independent_image_processing_output, + simulation_output, + simulation_output_fields, + toolkit_properties) def generate_dict_path(data_field, wavelength: (int, float) = None) -> str: @@ -14,39 +21,11 @@ def generate_dict_path(data_field, wavelength: (int, float) = None) -> str: :return: String which defines the path to the data_field. """ + all_wavelength_independent_properties = wavelength_independent_properties + toolkit_properties + if data_field in [Tags.SIMULATIONS, Tags.SETTINGS, Tags.DIGITAL_DEVICE, Tags.SIMULATION_PIPELINE]: return "/" + data_field + "/" - wavelength_dependent_properties = [Tags.DATA_FIELD_ABSORPTION_PER_CM, - Tags.DATA_FIELD_SCATTERING_PER_CM, - Tags.DATA_FIELD_ANISOTROPY] - - wavelength_independent_properties = [Tags.DATA_FIELD_OXYGENATION, - Tags.DATA_FIELD_SEGMENTATION, - Tags.DATA_FIELD_GRUNEISEN_PARAMETER, - Tags.DATA_FIELD_SPEED_OF_SOUND, - Tags.DATA_FIELD_DENSITY, - Tags.DATA_FIELD_ALPHA_COEFF, - Tags.KWAVE_PROPERTY_SENSOR_MASK, - Tags.KWAVE_PROPERTY_DIRECTIVITY_ANGLE] - - simulation_output = [Tags.DATA_FIELD_FLUENCE, - Tags.DATA_FIELD_INITIAL_PRESSURE, - Tags.OPTICAL_MODEL_UNITS, - Tags.DATA_FIELD_TIME_SERIES_DATA, - Tags.DATA_FIELD_RECONSTRUCTED_DATA, - Tags.DATA_FIELD_DIFFUSE_REFLECTANCE, - Tags.DATA_FIELD_DIFFUSE_REFLECTANCE_POS, - Tags.DATA_FIELD_PHOTON_EXIT_POS, - Tags.DATA_FIELD_PHOTON_EXIT_DIR] - - simulation_output_fields = [Tags.OPTICAL_MODEL_OUTPUT_NAME, - Tags.SIMULATION_PROPERTIES] - - wavelength_dependent_image_processing_output = [Tags.ITERATIVE_qPAI_RESULT] - - wavelength_independent_image_processing_output = [Tags.LINEAR_UNMIXING_RESULT] - if wavelength is not None: wl = "/{}/".format(wavelength) @@ -69,7 +48,7 @@ def generate_dict_path(data_field, wavelength: (int, float) = None) -> str: else: dict_path = "/" + Tags.SIMULATIONS + "/" + data_field - elif data_field in wavelength_independent_properties: + elif data_field in all_wavelength_independent_properties: dict_path = "/" + Tags.SIMULATIONS + "/" + Tags.SIMULATION_PROPERTIES + "/" + data_field + "/" elif data_field in simulation_output_fields: dict_path = "/" + Tags.SIMULATIONS + "/" + data_field + "/" diff --git a/simpa/utils/libraries/molecule_library.py b/simpa/utils/libraries/molecule_library.py index 35742c67..70fd2193 100644 --- a/simpa/utils/libraries/molecule_library.py +++ b/simpa/utils/libraries/molecule_library.py @@ -7,11 +7,11 @@ from simpa.utils.tissue_properties import TissueProperties from simpa.utils.libraries.literature_values import OpticalTissueProperties, StandardProperties -from simpa.utils.libraries.spectrum_library import AnisotropySpectrumLibrary, ScatteringSpectrumLibrary +from simpa.utils.libraries.spectrum_library import (AnisotropySpectrumLibrary, ScatteringSpectrumLibrary, + RefractiveIndexSpectrumLibrary, AbsorptionSpectrumLibrary) from simpa.utils import Spectrum from simpa.utils.calculate import calculate_oxygenation, calculate_gruneisen_parameter_from_temperature from simpa.utils.serializer import SerializableSIMPAClass -from simpa.utils.libraries.spectrum_library import AbsorptionSpectrumLibrary class MolecularComposition(SerializableSIMPAClass, list): @@ -55,10 +55,11 @@ def get_properties_for_wavelength(self, wavelength) -> TissueProperties: self.internal_properties[Tags.DATA_FIELD_ABSORPTION_PER_CM] = 0 self.internal_properties[Tags.DATA_FIELD_SCATTERING_PER_CM] = 0 self.internal_properties[Tags.DATA_FIELD_ANISOTROPY] = 0 + self.internal_properties[Tags.DATA_FIELD_REFRACTIVE_INDEX] = 0 for molecule in self: self.internal_properties[Tags.DATA_FIELD_ABSORPTION_PER_CM] += \ - (molecule.volume_fraction * molecule.spectrum.get_value_for_wavelength(wavelength)) + (molecule.volume_fraction * molecule.absorption_spectrum.get_value_for_wavelength(wavelength)) self.internal_properties[Tags.DATA_FIELD_SCATTERING_PER_CM] += \ (molecule.volume_fraction * (molecule.scattering_spectrum.get_value_for_wavelength(wavelength))) @@ -66,6 +67,9 @@ def get_properties_for_wavelength(self, wavelength) -> TissueProperties: self.internal_properties[Tags.DATA_FIELD_ANISOTROPY] += \ molecule.volume_fraction * molecule.anisotropy_spectrum.get_value_for_wavelength(wavelength) + self.internal_properties[Tags.DATA_FIELD_REFRACTIVE_INDEX] += \ + molecule.volume_fraction * molecule.refractive_index.get_value_for_wavelength(wavelength) + return self.internal_properties def serialize(self) -> dict: @@ -90,7 +94,9 @@ def __init__(self, name: str = None, absorption_spectrum: Spectrum = None, volume_fraction: float = None, scattering_spectrum: Spectrum = None, - anisotropy_spectrum: Spectrum = None, gruneisen_parameter: float = None, + anisotropy_spectrum: Spectrum = None, + refractive_index: Spectrum = None, + gruneisen_parameter: float = None, density: float = None, speed_of_sound: float = None, alpha_coefficient: float = None): """ @@ -99,6 +105,7 @@ def __init__(self, name: str = None, :param volume_fraction: float :param scattering_spectrum: Spectrum :param anisotropy_spectrum: Spectrum + :param refractive_index: Spectrum :param gruneisen_parameter: float :param density: float :param speed_of_sound: float @@ -120,7 +127,7 @@ def __init__(self, name: str = None, if not isinstance(absorption_spectrum, Spectrum): raise TypeError(f"The given spectrum was not of type AbsorptionSpectrum! Instead: " f"{type(absorption_spectrum)} and reads: {absorption_spectrum}") - self.spectrum = absorption_spectrum + self.absorption_spectrum = absorption_spectrum if volume_fraction is None: volume_fraction = 0.0 @@ -136,11 +143,17 @@ def __init__(self, name: str = None, self.scattering_spectrum = scattering_spectrum if anisotropy_spectrum is None: - anisotropy_spectrum = 0.0 + anisotropy_spectrum = AnisotropySpectrumLibrary.CONSTANT_ANISOTROPY_ARBITRARY(1) if not isinstance(anisotropy_spectrum, Spectrum): raise TypeError(f"The given anisotropy was not of type Spectrum instead of {type(anisotropy_spectrum)}!") self.anisotropy_spectrum = anisotropy_spectrum + if refractive_index is None: + refractive_index = RefractiveIndexSpectrumLibrary.CONSTANT_REFRACTOR_ARBITRARY(1) + if not isinstance(refractive_index, Spectrum): + raise TypeError(f"The refractive index was not of type Spectrum instead of {type(refractive_index)}!") + self.refractive_index = refractive_index + if gruneisen_parameter is None: gruneisen_parameter = calculate_gruneisen_parameter_from_temperature( StandardProperties.BODY_TEMPERATURE_CELCIUS) @@ -172,9 +185,10 @@ def __init__(self, name: str = None, def __eq__(self, other): if isinstance(other, Molecule): return (self.name == other.name and - self.spectrum == other.spectrum and + self.absorption_spectrum == other.absorption_spectrum and self.volume_fraction == other.volume_fraction and self.scattering_spectrum == other.scattering_spectrum and + self.refractive_index == other.refractive_index and self.alpha_coefficient == other.alpha_coefficient and self.speed_of_sound == other.speed_of_sound and self.gruneisen_parameter == other.gruneisen_parameter and @@ -191,7 +205,7 @@ def serialize(self): @staticmethod def deserialize(dictionary_to_deserialize: dict): deserialized_molecule = Molecule(name=dictionary_to_deserialize["name"], - absorption_spectrum=dictionary_to_deserialize["spectrum"], + absorption_spectrum=dictionary_to_deserialize["absorption_spectrum"], volume_fraction=dictionary_to_deserialize["volume_fraction"], scattering_spectrum=dictionary_to_deserialize["scattering_spectrum"], alpha_coefficient=dictionary_to_deserialize["alpha_coefficient"], diff --git a/simpa/utils/libraries/refractive_index_spectrum_data/__init__.py b/simpa/utils/libraries/refractive_index_spectrum_data/__init__.py new file mode 100644 index 00000000..89cc8954 --- /dev/null +++ b/simpa/utils/libraries/refractive_index_spectrum_data/__init__.py @@ -0,0 +1,3 @@ +# SPDX-FileCopyrightText: 2021 Division of Intelligent Medical Systems, DKFZ +# SPDX-FileCopyrightText: 2021 Janek Groehl +# SPDX-License-Identifier: MIT diff --git a/simpa/utils/libraries/spectrum_library.py b/simpa/utils/libraries/spectrum_library.py index f89c32ca..b13099dd 100644 --- a/simpa/utils/libraries/spectrum_library.py +++ b/simpa/utils/libraries/spectrum_library.py @@ -33,7 +33,7 @@ def __init__(self, spectrum_name: str, wavelengths: np.ndarray, values: np.ndarr str(np.shape(wavelengths)) + " vs " + str(np.shape(values))) new_wavelengths = np.arange(self.min_wavelength, self.max_wavelength+1, 1) - self.new_absorptions = np.interp(new_wavelengths, self.wavelengths, self.values) + self.new_values = np.interp(new_wavelengths, self.wavelengths, self.values) def get_value_over_wavelength(self): """ @@ -47,7 +47,7 @@ def get_value_for_wavelength(self, wavelength: int) -> float: Must be an integer value between the minimum and maximum wavelength. :return: the best matching linearly interpolated absorption value for the given wavelength. """ - return self.new_absorptions[wavelength-self.min_wavelength] + return self.new_values[wavelength-self.min_wavelength] def __eq__(self, other): if isinstance(other, Spectrum): @@ -149,6 +149,17 @@ def CONSTANT_ABSORBER_ARBITRARY(absorption_coefficient: float = 1): np.asarray([absorption_coefficient, absorption_coefficient])) +class RefractiveIndexSpectrumLibrary(SpectraLibrary): + + def __init__(self, additional_folder_path: str = None): + super(RefractiveIndexSpectrumLibrary, self).__init__("refractive_index_spectra_data", additional_folder_path) + + @staticmethod + def CONSTANT_REFRACTOR_ARBITRARY(refractive_index: float = 1): + return Spectrum("Constant Refractor (arb)", np.asarray([450, 1000]), + np.asarray([refractive_index, refractive_index])) + + def get_simpa_internal_absorption_spectra_by_names(absorption_spectrum_names: list): lib = AbsorptionSpectrumLibrary() spectra = [] diff --git a/simpa/utils/tags.py b/simpa/utils/tags.py index 49c89288..56175993 100644 --- a/simpa/utils/tags.py +++ b/simpa/utils/tags.py @@ -859,6 +859,12 @@ class Tags: Usage: SIMPA package, naming convention """ + DATA_FIELD_REFRACTIVE_INDEX = "n" + """ + Refractive index of the generated volume/structure.\n + Usage: SIMPA package, naming convention + """ + DATA_FIELD_OXYGENATION = "oxy" """ Oxygenation of the generated volume/structure.\n diff --git a/simpa/utils/tissue_properties.py b/simpa/utils/tissue_properties.py index 2496a117..b21d28ad 100644 --- a/simpa/utils/tissue_properties.py +++ b/simpa/utils/tissue_properties.py @@ -3,25 +3,11 @@ # SPDX-License-Identifier: MIT from simpa.utils import Tags +from simpa.utils.constants import wavelength_independent_properties, wavelength_dependent_properties class TissueProperties(dict): - wavelength_dependent_properties = [ - Tags.DATA_FIELD_ABSORPTION_PER_CM, - Tags.DATA_FIELD_SCATTERING_PER_CM, - Tags.DATA_FIELD_ANISOTROPY - ] - - wavelength_independent_properties = [ - Tags.DATA_FIELD_GRUNEISEN_PARAMETER, - Tags.DATA_FIELD_SEGMENTATION, - Tags.DATA_FIELD_OXYGENATION, - Tags.DATA_FIELD_DENSITY, - Tags.DATA_FIELD_SPEED_OF_SOUND, - Tags.DATA_FIELD_ALPHA_COEFF - ] - property_tags = wavelength_dependent_properties + wavelength_independent_properties def __init__(self): diff --git a/simpa/visualisation/matplotlib_data_visualisation.py b/simpa/visualisation/matplotlib_data_visualisation.py index a1fb7e12..4f9f2472 100644 --- a/simpa/visualisation/matplotlib_data_visualisation.py +++ b/simpa/visualisation/matplotlib_data_visualisation.py @@ -20,6 +20,7 @@ def visualise_data(wavelength: int = None, show_absorption=False, show_scattering=False, show_anisotropy=False, + show_refractive_index=False, show_speed_of_sound=False, show_tissue_density=False, show_fluence=False, @@ -62,6 +63,7 @@ def visualise_data(wavelength: int = None, absorption = get_data_field_from_simpa_output(file, Tags.DATA_FIELD_ABSORPTION_PER_CM, wavelength) scattering = get_data_field_from_simpa_output(file, Tags.DATA_FIELD_SCATTERING_PER_CM, wavelength) anisotropy = get_data_field_from_simpa_output(file, Tags.DATA_FIELD_ANISOTROPY, wavelength) + refractive_index = get_data_field_from_simpa_output(file, Tags.DATA_FIELD_REFRACTIVE_INDEX, wavelength) segmentation_map = get_data_field_from_simpa_output(file, Tags.DATA_FIELD_SEGMENTATION) speed_of_sound = get_data_field_from_simpa_output(file, Tags.DATA_FIELD_SPEED_OF_SOUND) density = get_data_field_from_simpa_output(file, Tags.DATA_FIELD_DENSITY) @@ -163,6 +165,11 @@ def visualise_data(wavelength: int = None, data_item_names.append("Anisotropy") cmaps.append("gray") logscales.append(True and log_scale) + if refractive_index is not None and show_refractive_index: + data_to_show.append(refractive_index) + data_item_names.append("Refractive Index") + cmaps.append("gray") + logscales.append(True and log_scale) if speed_of_sound is not None and show_speed_of_sound: data_to_show.append(speed_of_sound) data_item_names.append("Speed of Sound") diff --git a/simpa_examples/minimal_optical_simulation.py b/simpa_examples/minimal_optical_simulation.py index 2367e24b..7676f496 100644 --- a/simpa_examples/minimal_optical_simulation.py +++ b/simpa_examples/minimal_optical_simulation.py @@ -157,4 +157,5 @@ def __init__(self): show_initial_pressure=True, show_absorption=True, show_diffuse_reflectance=SAVE_REFLECTANCE, - log_scale=True) + show_refractive_index=True, + log_scale=False) From c2c75ea4235104ab3cb76e6e5cbdc1c1ecaa8963 Mon Sep 17 00:00:00 2001 From: Kris Dreher Date: Fri, 8 Mar 2024 13:46:18 +0100 Subject: [PATCH 02/34] Reverted minimal_optical_example --- simpa_examples/minimal_optical_simulation.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/simpa_examples/minimal_optical_simulation.py b/simpa_examples/minimal_optical_simulation.py index 7676f496..2367e24b 100644 --- a/simpa_examples/minimal_optical_simulation.py +++ b/simpa_examples/minimal_optical_simulation.py @@ -157,5 +157,4 @@ def __init__(self): show_initial_pressure=True, show_absorption=True, show_diffuse_reflectance=SAVE_REFLECTANCE, - show_refractive_index=True, - log_scale=False) + log_scale=True) From cf41374333a932e8386045db5ceaa4a8fc941c6e Mon Sep 17 00:00:00 2001 From: Kris Dreher Date: Fri, 8 Mar 2024 15:11:17 +0100 Subject: [PATCH 03/34] Added literature sources for the refractive indices of all molecules --- simpa/utils/libraries/molecule_library.py | 23 +++++++++++++- .../Deoxyhemoglobin.npz | Bin 0 -> 1704 bytes .../Heavy_Water.npz | Bin 0 -> 2136 bytes .../Oxyhemoglobin.npz | Bin 0 -> 1576 bytes .../refractive_index_spectra_data/Water.npz | Bin 0 -> 2136 bytes .../refractive_index_spectra_data/__init__.py | 28 ++++++++++++++++++ .../__init__.py | 3 -- 7 files changed, 50 insertions(+), 4 deletions(-) create mode 100644 simpa/utils/libraries/refractive_index_spectra_data/Deoxyhemoglobin.npz create mode 100644 simpa/utils/libraries/refractive_index_spectra_data/Heavy_Water.npz create mode 100644 simpa/utils/libraries/refractive_index_spectra_data/Oxyhemoglobin.npz create mode 100644 simpa/utils/libraries/refractive_index_spectra_data/Water.npz create mode 100644 simpa/utils/libraries/refractive_index_spectra_data/__init__.py delete mode 100644 simpa/utils/libraries/refractive_index_spectrum_data/__init__.py diff --git a/simpa/utils/libraries/molecule_library.py b/simpa/utils/libraries/molecule_library.py index 70fd2193..3c813e43 100644 --- a/simpa/utils/libraries/molecule_library.py +++ b/simpa/utils/libraries/molecule_library.py @@ -228,6 +228,7 @@ def water(volume_fraction: float = 1.0): StandardProperties.WATER_MUS), anisotropy_spectrum=AnisotropySpectrumLibrary.CONSTANT_ANISOTROPY_ARBITRARY( StandardProperties.WATER_G), + refractive_index=RefractiveIndexSpectrumLibrary().get_spectrum_by_name("Water"), density=StandardProperties.DENSITY_WATER, speed_of_sound=StandardProperties.SPEED_OF_SOUND_WATER, alpha_coefficient=StandardProperties.ALPHA_COEFF_WATER @@ -241,6 +242,7 @@ def oxyhemoglobin(volume_fraction: float = 1.0): scattering_spectrum=ScatteringSpectrumLibrary().get_spectrum_by_name("blood_scattering"), anisotropy_spectrum=AnisotropySpectrumLibrary.CONSTANT_ANISOTROPY_ARBITRARY( OpticalTissueProperties.BLOOD_ANISOTROPY), + refractive_index=RefractiveIndexSpectrumLibrary().get_spectrum_by_name("Oxyhemoglobin"), density=StandardProperties.DENSITY_BLOOD, speed_of_sound=StandardProperties.SPEED_OF_SOUND_BLOOD, alpha_coefficient=StandardProperties.ALPHA_COEFF_BLOOD @@ -250,10 +252,12 @@ def oxyhemoglobin(volume_fraction: float = 1.0): def deoxyhemoglobin(volume_fraction: float = 1.0): return Molecule(name="deoxyhemoglobin", absorption_spectrum=AbsorptionSpectrumLibrary().get_spectrum_by_name("Deoxyhemoglobin"), + volume_fraction=volume_fraction, scattering_spectrum=ScatteringSpectrumLibrary().get_spectrum_by_name("blood_scattering"), anisotropy_spectrum=AnisotropySpectrumLibrary.CONSTANT_ANISOTROPY_ARBITRARY( OpticalTissueProperties.BLOOD_ANISOTROPY), + refractive_index=RefractiveIndexSpectrumLibrary().get_spectrum_by_name("Deoxyhemoglobin"), density=StandardProperties.DENSITY_BLOOD, speed_of_sound=StandardProperties.SPEED_OF_SOUND_BLOOD, alpha_coefficient=StandardProperties.ALPHA_COEFF_BLOOD @@ -268,6 +272,8 @@ def melanin(volume_fraction: float = 1.0): "epidermis", OpticalTissueProperties.MUS500_EPIDERMIS, OpticalTissueProperties.FRAY_EPIDERMIS, OpticalTissueProperties.BMIE_EPIDERMIS), anisotropy_spectrum=AnisotropySpectrumLibrary().get_spectrum_by_name("Epidermis_Anisotropy"), + # for n: DOI:10.1371/journal.pone.0150268 + refractive_index=RefractiveIndexSpectrumLibrary.CONSTANT_REFRACTOR_ARBITRARY(1.36), density=StandardProperties.DENSITY_SKIN, speed_of_sound=StandardProperties.SPEED_OF_SOUND_SKIN, alpha_coefficient=StandardProperties.ALPHA_COEFF_SKIN @@ -281,6 +287,8 @@ def fat(volume_fraction: float = 1.0): scattering_spectrum=ScatteringSpectrumLibrary().get_spectrum_by_name("fat_scattering"), anisotropy_spectrum=AnisotropySpectrumLibrary.CONSTANT_ANISOTROPY_ARBITRARY( OpticalTissueProperties.STANDARD_ANISOTROPY), + # for n: DOI:10.1371/journal.pone.0150268 + refractive_index=RefractiveIndexSpectrumLibrary.CONSTANT_REFRACTOR_ARBITRARY(1.46), density=StandardProperties.DENSITY_FAT, speed_of_sound=StandardProperties.SPEED_OF_SOUND_FAT, alpha_coefficient=StandardProperties.ALPHA_COEFF_FAT @@ -289,13 +297,14 @@ def fat(volume_fraction: float = 1.0): # Scatterers @staticmethod def constant_scatterer(scattering_coefficient: float = 100.0, anisotropy: float = 0.9, - volume_fraction: float = 1.0): + refractive_index: float = 1.329, volume_fraction: float = 1.0): return Molecule(name="constant_scatterer", absorption_spectrum=AbsorptionSpectrumLibrary().CONSTANT_ABSORBER_ARBITRARY(1e-20), volume_fraction=volume_fraction, scattering_spectrum=ScatteringSpectrumLibrary.CONSTANT_SCATTERING_ARBITRARY( scattering_coefficient), anisotropy_spectrum=AnisotropySpectrumLibrary.CONSTANT_ANISOTROPY_ARBITRARY(anisotropy), + refractive_index=RefractiveIndexSpectrumLibrary.CONSTANT_REFRACTOR_ARBITRARY(refractive_index), density=StandardProperties.DENSITY_GENERIC, speed_of_sound=StandardProperties.SPEED_OF_SOUND_GENERIC, alpha_coefficient=StandardProperties.ALPHA_COEFF_GENERIC @@ -309,6 +318,8 @@ def soft_tissue_scatterer(volume_fraction: float = 1.0): scattering_spectrum=ScatteringSpectrumLibrary().get_spectrum_by_name("background_scattering"), anisotropy_spectrum=AnisotropySpectrumLibrary.CONSTANT_ANISOTROPY_ARBITRARY( OpticalTissueProperties.STANDARD_ANISOTROPY), + # for n: DOI:10.1371/journal.pone.0150268 + refractive_index=RefractiveIndexSpectrumLibrary.CONSTANT_REFRACTOR_ARBITRARY(1.39), density=StandardProperties.DENSITY_GENERIC, speed_of_sound=StandardProperties.SPEED_OF_SOUND_GENERIC, alpha_coefficient=StandardProperties.ALPHA_COEFF_GENERIC @@ -322,6 +333,8 @@ def muscle_scatterer(volume_fraction: float = 1.0): scattering_spectrum=ScatteringSpectrumLibrary().get_spectrum_by_name("muscle_scattering"), anisotropy_spectrum=AnisotropySpectrumLibrary.CONSTANT_ANISOTROPY_ARBITRARY( OpticalTissueProperties.STANDARD_ANISOTROPY), + # for n: DOI:10.1371/journal.pone.0150268 + refractive_index=RefractiveIndexSpectrumLibrary.CONSTANT_REFRACTOR_ARBITRARY(1.39), density=StandardProperties.DENSITY_GENERIC, speed_of_sound=StandardProperties.SPEED_OF_SOUND_GENERIC, alpha_coefficient=StandardProperties.ALPHA_COEFF_GENERIC @@ -336,6 +349,8 @@ def epidermal_scatterer(volume_fraction: float = 1.0): "epidermis", OpticalTissueProperties.MUS500_EPIDERMIS, OpticalTissueProperties.FRAY_EPIDERMIS, OpticalTissueProperties.BMIE_EPIDERMIS), anisotropy_spectrum=AnisotropySpectrumLibrary().get_spectrum_by_name("Epidermis_Anisotropy"), + # for n: DOI:10.1371/journal.pone.0150268 + refractive_index=RefractiveIndexSpectrumLibrary.CONSTANT_REFRACTOR_ARBITRARY(1.36), density=StandardProperties.DENSITY_SKIN, speed_of_sound=StandardProperties.SPEED_OF_SOUND_SKIN, alpha_coefficient=StandardProperties.ALPHA_COEFF_SKIN @@ -365,6 +380,8 @@ def bone(volume_fraction: float = 1.0): scattering_spectrum=ScatteringSpectrumLibrary().get_spectrum_by_name("bone_scattering"), anisotropy_spectrum=AnisotropySpectrumLibrary.CONSTANT_ANISOTROPY_ARBITRARY( OpticalTissueProperties.STANDARD_ANISOTROPY), + # for n: DOI:10.1371/journal.pone.0150268 + refractive_index=RefractiveIndexSpectrumLibrary.CONSTANT_REFRACTOR_ARBITRARY(1.55), density=StandardProperties.DENSITY_BONE, speed_of_sound=StandardProperties.SPEED_OF_SOUND_BONE, alpha_coefficient=StandardProperties.ALPHA_COEFF_BONE @@ -378,6 +395,8 @@ def mediprene(volume_fraction: float = 1.0): scattering_spectrum=ScatteringSpectrumLibrary.CONSTANT_SCATTERING_ARBITRARY((-np.log(0.85)) - (-np.log(0.85) / 10)), anisotropy_spectrum=AnisotropySpectrumLibrary.CONSTANT_ANISOTROPY_ARBITRARY(0.9), + # for n: This is basically just a guess + refractive_index=RefractiveIndexSpectrumLibrary.CONSTANT_REFRACTOR_ARBITRARY(1.4), density=StandardProperties.DENSITY_GEL_PAD, speed_of_sound=StandardProperties.SPEED_OF_SOUND_GEL_PAD, alpha_coefficient=StandardProperties.ALPHA_COEFF_GEL_PAD @@ -393,6 +412,7 @@ def heavy_water(volume_fraction: float = 1.0): StandardProperties.WATER_MUS), anisotropy_spectrum=AnisotropySpectrumLibrary.CONSTANT_ANISOTROPY_ARBITRARY( StandardProperties.WATER_G), + refractive_index=RefractiveIndexSpectrumLibrary().get_spectrum_by_name("Heavy_Water"), density=StandardProperties.DENSITY_HEAVY_WATER, speed_of_sound=StandardProperties.SPEED_OF_SOUND_HEAVY_WATER, alpha_coefficient=StandardProperties.ALPHA_COEFF_WATER @@ -408,6 +428,7 @@ def air(volume_fraction: float = 1.0): StandardProperties.AIR_MUS), anisotropy_spectrum=AnisotropySpectrumLibrary.CONSTANT_ANISOTROPY_ARBITRARY( StandardProperties.AIR_G), + refractive_index=RefractiveIndexSpectrumLibrary.CONSTANT_REFRACTOR_ARBITRARY(1), density=StandardProperties.DENSITY_AIR, speed_of_sound=StandardProperties.SPEED_OF_SOUND_AIR, alpha_coefficient=StandardProperties.ALPHA_COEFF_AIR diff --git a/simpa/utils/libraries/refractive_index_spectra_data/Deoxyhemoglobin.npz b/simpa/utils/libraries/refractive_index_spectra_data/Deoxyhemoglobin.npz new file mode 100644 index 0000000000000000000000000000000000000000..f3fb492ef1e80da9ec31883a972a4cb20018d8d9 GIT binary patch literal 1704 zcmd5-dr*{B6kk{#2FSy~1~Y1d$+ZLu1__b8M>0YfX)RKevRPsgYF&_L%fhm|u*)t| zFpz{0n#2TYGfXDPL**f{6bb?{6c0s_7J+!hB^Y$<-u-YuoBru9?>FE6=G=SExxe2z zbN1r}u#Ie4%zA-UF;q8m-H^qiuNljhmB1Iq3u0YF(P9IZwSJd+(o?$f0^i%iJ7B;{ zU`e=Pg4mB@xLzEtcep3liNg(#iiwNii$08s2@}wLU%oI_K=xxJ_|XDlZ+^|)=_Myl z3g`bXv&!kcricH=NYOpxpEI|DxAReh7GtFG4+}I|#K<_f$nnm7jO2o@g+{}9bY9_B zSztVmEIQV54-z|UBFt{JE)Nj8c9% zd1CYyMtQ~S=j{mAzrn5$-^7S7tK8;u1EbG2eKT7!fKkcfhPS)>F{-|rbC$gd+fSLU zaqq(@YM>_f-ftKwD_)&)T7^Z5vVaeIG16{6Zgjb41>ecN?zWHEgG=W8$zL&gN`C=H zTm5uh6`dG0{NP~VM{r`lb!~YE;qgV&uA}W3xpc;LY`caL5xM2#t1EagF;rdHhSA7S zag`fEl|66g($D|Kucljyw~+nT{PX*=v=~Kphu;cn!D!CA#OlwV2oLKGLVJR#-wwX6 zBzRmLR9M+ecvYFaD7}JF==*bnHySbe^p#J%tO!!?-g=BmdxHOpBKYPybE)TLj6z1_ zeG!*1I$FKpapEFIHuy{b*&jw`otI`*%q|ymak@Nx&s$Ff+e7jFO$I5$ga}e-4ZZg|8h{d zbutV!$l0cW$xtQy{hZ)fG904`Ta&@m@>$PlP72_$p0oR-QXshE(50lx6!6U=4n|TS zr$Y@prA(e2=`tTF=>I(+m5Kj4m5HB91?nz$lmShH{bg{3T-TN$1HyuLxvwJ{XBXujfo3PW9pKT#>8Dn12bBe=`^77hdAkQayz;B zKstob0x8phmfPk^I?#6LCexR7DS^uH5h#JmWS1#{mZ^DE3I4P(wkoDSyH)U|Gsz@Y z0WEi1l?o_L?Uah?qiY6}SCp~LKZD6TlELV8P{SzI8>nXLe_GAdXF|>7^UwgL&y#By zU0MzFk!$kTW&#CgNhZ+n$%~o53otOWW&hXbLElCFkIaC=%E#jIxJO56b({s8_4r`% R0*pMEnq7F9}6!6V_MR5X)i`F4d7aia{iH0Ta)O(gq|J+}@P4nsb{?2WFX@B=# zop#u8g(SvAY1i!Xx%I;&$@X@T6jHW6eYG*u&t%S(N%GgJtxg11UZqHhO{2b0}<}V*c(pbWtKOLD(2FR**3_L8GDo!W4+p?2n(pPdoVxqVqa`Nz@mQznp)X9SFEd!O!S zNw8}&+4XgCn2a2{-)gE+B6wO39*-agtHPwd@dy`NYCkC^y*q-BtI5&HD@Xm{gs{%# zxgm|TYp(omB&jw;$Qs5WOjkIm z$r+hjJr(5Y{<=#|W9Yft*sP5w8;*1}q9a1@RCRwP*;Mk-VLBPzVwAe%^qjo0#&!CU z`~F_z@6j6nT%T=BH=0tiQvbIL{q=66`BL-R0ajNa>Uvfx6!l{E7&~IE$dlP-R=!T; zscbu|TrYAn+rg@eM4rRmV7-b(UcmOS0UJbK!dlso5|Nj&k62Bq$Sv#u8@*BF)$9va zw@Kvnth8Cwi`irBh%F*dW}8|0R*|Q&?W}T}$j#d{uWaKI*YYxvhqI~d4z`_r!Ya!} ze?2lDCHggNI%{FivjeQ_PSGFDX0g@mRrUqzX%YQ8_6xS2{gIV+X>?kdonn|m v665XToMWOuX8Zil;r{l#H@Ap?&wD^e|63b|PWz6%t;LRBtI2Ec+N!?*B9?1q literal 0 HcmV?d00001 diff --git a/simpa/utils/libraries/refractive_index_spectra_data/Oxyhemoglobin.npz b/simpa/utils/libraries/refractive_index_spectra_data/Oxyhemoglobin.npz new file mode 100644 index 0000000000000000000000000000000000000000..4f14c95169f8eadb11dc806ab350fed23ea49eaa GIT binary patch literal 1576 zcmWIWW@Zs#fB;1XBVPV#6POqnK$wd`grO`kr!=)#FR!4IkwE|~3Q`G@1%b(ap}ql; zj0|NA)#@p!#mPnLRtoAiX%^}_3hHV3MI}XvdGYy0DXAcFx5S*{RG@fqMq)uKkgs88 zrlYB&P^&;b;L-`%V9)g9tG({~C%jodzuFsgOWnQn`>VZC)HYX+e?VG2V2w49HdOOp zZT0u7z5c$2viU%HJ!PL2=D&dYI>c|C2kO_}RZ}$k`&WA{d5RXj(_-Sul)2DbHe+t_DbH%&3?Z7 zYOlDlICBz^R(f)iDIUase1kFW?N@sRQM*~1Z@${gS96^`^cqM%y!JmDNXu1mo;mmm z$bWG4Ulfp*y?^CzTFmdU7m;Xcn#idu}By~Rr(m-E_18J$1*@<1x zfHc3w6lDeF7hjeU*AAq`xJ@R=KLXM> z&VCC5(xOY!Vp<;}(bvy>4FvK_c?Fzl59(n1T9Bb)94X?BAi zX&^1QASt5pE|9)_;-fE+7RdR%ck>-28syB|U+wv_e(v6Q3rJr$`pyePn@v)Da`US_ z@A)HdJ%Kb2lXklph;KSk;qi^H_S_lYcdiG~>ECv&yAGroG+RZ0bO1_PURt+a3YM1n zfoZusu`D$wH7~s+W6&n(O%qymQUog0q(tGso1%9!#Qyqbi1?mnh&?}> zA^PoF96;W9IkN?#{&tJQ6_9_0TOsauZiSdz(+Y9tj#h~Op0q;rX|_Ss<+nlXncoI+ z&-FHlKAv_6-?|-Q-=TJhJ2*NZ?r`mZ_-|teMBJp)0p!irIh_!HKI??oo6!Xc|BGD? z0p5&EBFwmoX9hTEU<5G{g*Cc9)T{v4#?Zic23VA%WsCrCRyL3lCLk;Y(sNlsJOCD) BYf}IK literal 0 HcmV?d00001 diff --git a/simpa/utils/libraries/refractive_index_spectra_data/Water.npz b/simpa/utils/libraries/refractive_index_spectra_data/Water.npz new file mode 100644 index 0000000000000000000000000000000000000000..7009223657165ab16986b258086a0574d5d9ab86 GIT binary patch literal 2136 zcmd7TX;4#F6bJC2h-h7C#T9CU3OxUlD4s+s*52oNwWBlr;-{WW=9hDG-}~RZyhSD% zY;G-}vJ)&W?3dOL76`caaDlZzEl)EwQzBATiWqL(SDus}!^YF&<;ob=JG#zvb)M!dED(Nh z4UfLzQ+*`g1kxKLeDg+{KyPXasyl0h0^1s^cD)gLCCd#Pql{p1XluDDHbSE8rXkb8 z2sK4sC!tAd|m~YtX+)#ti@+iOYbR~kt zzUK|zWeAO;K7YF+gtSnDoBuil8>>|*O?e2TZ)ImEWFzcc(Qq+56Jhg&YTE)8LSn7% z#l=(v&)+OcrO612<3jJJ2?#RZ$NLY)BM62#+U<%(=|LL zMRS4?fiK_i8 z5e!2;#;sj};H~JkoEMDnz~Go*y&Rz_GH>OpWeD4?9@ssVA>>ToVgE1)!CHDZtY;}g zeVhDFe;|U!qvN_cbIa=8Gd`0dyjXWZ?Ba|Gg0r40QbIXIN^sNx7=^) z`vM{IVr`cbbAz|eA;A~H82l`}VF}CcIS|sj7@>B;^fR7JdFR5y(nSd2DOP^h79#j1 z-}!Ya^L#|wK)w%xe^uw9HZj5`U&$e7<`B2WiV8ru+ZwR_p*KRA?zMUe(?dJH@oO&x z$LXf#Va%Sc7LViw2(!k9KL2q(J0HrLi{~+Gy0w};o(PX7$)#nGVfC7-`Y!GnqO0^>@aK*$Cq|b(k4F5dLsE{&O@_IIyj?)g9sc zd*&{Sm=ASN#@4&B-)S3CXv>T(KN6&!g>YKmT#T;lI4;i}2w~bcDbHP=iO?+xt#f6L zak_S)?sJ5`+1A&GGEEr;_PH}ypWrn0whKF7MDoNyrtF<-T+nx-|DS7oG`q&LM*Y)jrw2GXkLzr{5YLFkWTyY$)Mbuyvj`KE!E;!u6C=%ah$WW$kr^%Q)JV626(sR zQ16^8cIVb{Hkb0(!=^0q3u`k?$glzf7j4@JhimW zQZ4mAX=%TouBYF*c0Kh+_};9D)+y!P&U?Rz@@5v3`Mej3DSu1}8Nu7YYbv4Kg{8EP zD&AYAlqsl3{zMt&Tb9#t3@)dB5ATz5%AZ?77V}=MpnMx08K Date: Thu, 14 Mar 2024 11:00:03 +0100 Subject: [PATCH 04/34] Added refractive index to constant generic tissue --- simpa/utils/libraries/tissue_library.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/simpa/utils/libraries/tissue_library.py b/simpa/utils/libraries/tissue_library.py index e6d6dab5..0bfe3b1a 100644 --- a/simpa/utils/libraries/tissue_library.py +++ b/simpa/utils/libraries/tissue_library.py @@ -5,7 +5,8 @@ from simpa.utils import OpticalTissueProperties, SegmentationClasses, StandardProperties, MolecularCompositionGenerator from simpa.utils import Molecule from simpa.utils import MOLECULE_LIBRARY -from simpa.utils.libraries.spectrum_library import AnisotropySpectrumLibrary, ScatteringSpectrumLibrary +from simpa.utils.libraries.spectrum_library import (AnisotropySpectrumLibrary, ScatteringSpectrumLibrary, + RefractiveIndexSpectrumLibrary) from simpa.utils.calculate import randomize_uniform from simpa.utils.libraries.spectrum_library import AbsorptionSpectrumLibrary @@ -21,17 +22,21 @@ def get_blood_volume_fractions(self, total_blood_volume_fraction=1e-10, oxygenat """ return [total_blood_volume_fraction*oxygenation, total_blood_volume_fraction*(1-oxygenation)] - def constant(self, mua=1e-10, mus=1e-10, g=1e-10): + def constant(self, mua=1e-10, mus=1e-10, g=1e-10, n=1.3): """ TODO """ return (MolecularCompositionGenerator().append(Molecule(name="constant_mua_mus_g", - absorption_spectrum=AbsorptionSpectrumLibrary().CONSTANT_ABSORBER_ARBITRARY(mua), + absorption_spectrum=AbsorptionSpectrumLibrary(). + CONSTANT_ABSORBER_ARBITRARY(mua), volume_fraction=1.0, scattering_spectrum=ScatteringSpectrumLibrary. CONSTANT_SCATTERING_ARBITRARY(mus), anisotropy_spectrum=AnisotropySpectrumLibrary. - CONSTANT_ANISOTROPY_ARBITRARY(g))) + CONSTANT_ANISOTROPY_ARBITRARY(g), + refractive_index=RefractiveIndexSpectrumLibrary. + CONSTANT_REFRACTOR_ARBITRARY(n))) + .get_molecular_composition(SegmentationClasses.GENERIC)) def muscle(self, background_oxy=None, blood_volume_fraction=None): From c90f30ea67f29cc74335a4d6ff817328bcdae230 Mon Sep 17 00:00:00 2001 From: Liam Keegan Date: Thu, 14 Mar 2024 13:41:33 +0100 Subject: [PATCH 05/34] Add Tags.DATA_FIELD_REFRACTIVE_INDEX to wavelength_dependent_properties, and `-b 1` to mcx command line args --- .../optical_forward_model_mcx_adapter.py | 2 ++ .../optical_forward_model_mcx_reflectance_adapter.py | 2 ++ simpa/utils/constants.py | 3 ++- simpa/utils/dict_path_manager.py | 2 +- 4 files changed, 7 insertions(+), 2 deletions(-) diff --git a/simpa/core/simulation_modules/optical_simulation_module/optical_forward_model_mcx_adapter.py b/simpa/core/simulation_modules/optical_simulation_module/optical_forward_model_mcx_adapter.py index ed4617a4..e76fe524 100644 --- a/simpa/core/simulation_modules/optical_simulation_module/optical_forward_model_mcx_adapter.py +++ b/simpa/core/simulation_modules/optical_simulation_module/optical_forward_model_mcx_adapter.py @@ -184,6 +184,8 @@ def get_command(self) -> List: cmd.append("1") cmd.append("-F") cmd.append("jnii") + cmd.append("-b") + cmd.append("1") return cmd @staticmethod diff --git a/simpa/core/simulation_modules/optical_simulation_module/optical_forward_model_mcx_reflectance_adapter.py b/simpa/core/simulation_modules/optical_simulation_module/optical_forward_model_mcx_reflectance_adapter.py index fa9a64a7..7bdd88d5 100644 --- a/simpa/core/simulation_modules/optical_simulation_module/optical_forward_model_mcx_reflectance_adapter.py +++ b/simpa/core/simulation_modules/optical_simulation_module/optical_forward_model_mcx_reflectance_adapter.py @@ -106,6 +106,8 @@ def get_command(self) -> List: cmd.append("1") cmd.append("-F") cmd.append("jnii") + cmd.append("-b") + cmd.append("1") if Tags.COMPUTE_PHOTON_DIRECTION_AT_EXIT in self.component_settings and \ self.component_settings[Tags.COMPUTE_PHOTON_DIRECTION_AT_EXIT]: cmd.append("-H") diff --git a/simpa/utils/constants.py b/simpa/utils/constants.py index b4b7c17c..9647f116 100644 --- a/simpa/utils/constants.py +++ b/simpa/utils/constants.py @@ -34,7 +34,8 @@ class SegmentationClasses: wavelength_dependent_properties = [ Tags.DATA_FIELD_ABSORPTION_PER_CM, Tags.DATA_FIELD_SCATTERING_PER_CM, - Tags.DATA_FIELD_ANISOTROPY + Tags.DATA_FIELD_ANISOTROPY, + Tags.DATA_FIELD_REFRACTIVE_INDEX ] wavelength_independent_properties = [ diff --git a/simpa/utils/dict_path_manager.py b/simpa/utils/dict_path_manager.py index c4e5e105..f8c64705 100644 --- a/simpa/utils/dict_path_manager.py +++ b/simpa/utils/dict_path_manager.py @@ -50,7 +50,7 @@ def generate_dict_path(data_field, wavelength: (int, float) = None) -> str: elif data_field in wavelength_independent_image_processing_output: dict_path = "/" + Tags.IMAGE_PROCESSING + "/" + data_field + "/" else: - raise ValueError(f"The requested data_field: '{data_field}: is not a valid argument. " + raise ValueError(f"The requested data_field: '{data_field}': is not a valid argument. " f"Please specify a valid data_field using the Tags from simpa/utils/tags.py!") return dict_path From 2c0e8ceb23bd2f6171c3a3849e024fec422486b5 Mon Sep 17 00:00:00 2001 From: Liam Keegan Date: Fri, 8 Mar 2024 14:02:29 +0100 Subject: [PATCH 06/34] Add `refractive_index` arg to OpticalForwardModule and use `asgn_float` MediaFormat to pass mua/mus/g/n for each voxel to mcx --- .../optical_simulation_module/__init__.py | 13 +++++- .../optical_forward_model_mcx_adapter.py | 42 ++++++++----------- ...l_forward_model_mcx_reflectance_adapter.py | 12 ++++-- .../optical_forward_model_test_adapter.py | 2 +- 4 files changed, 39 insertions(+), 30 deletions(-) diff --git a/simpa/core/simulation_modules/optical_simulation_module/__init__.py b/simpa/core/simulation_modules/optical_simulation_module/__init__.py index 2cd97ad8..2e883e2a 100644 --- a/simpa/core/simulation_modules/optical_simulation_module/__init__.py +++ b/simpa/core/simulation_modules/optical_simulation_module/__init__.py @@ -36,6 +36,7 @@ def forward_model(self, absorption_cm: np.ndarray, scattering_cm: np.ndarray, anisotropy: np.ndarray, + refractive_index: np.ndarray, illumination_geometry: IlluminationGeometryBase): """ A deriving class needs to implement this method according to its model. @@ -43,6 +44,7 @@ def forward_model(self, :param absorption_cm: Absorption in units of per centimeter :param scattering_cm: Scattering in units of per centimeter :param anisotropy: Dimensionless scattering anisotropy + :param refractive_index: Refractive index :param illumination_geometry: A device that represents a detection geometry :return: Fluence in units of J/cm^2 """ @@ -65,6 +67,7 @@ def run(self, device: Union[IlluminationGeometryBase, PhotoacousticDevice]) -> N absorption = load_data_field(file_path, Tags.DATA_FIELD_ABSORPTION_PER_CM, wl) scattering = load_data_field(file_path, Tags.DATA_FIELD_SCATTERING_PER_CM, wl) anisotropy = load_data_field(file_path, Tags.DATA_FIELD_ANISOTROPY, wl) + refractive_index = load_data_field(file_path, Tags.DATA_FIELD_REFRACTIVE_INDEX, wl) gruneisen_parameter = load_data_field(file_path, Tags.DATA_FIELD_GRUNEISEN_PARAMETER) _device = None @@ -79,7 +82,8 @@ def run(self, device: Union[IlluminationGeometryBase, PhotoacousticDevice]) -> N device=device, absorption=absorption, scattering=scattering, - anisotropy=anisotropy) + anisotropy=anisotropy, + refractive_index=refractive_index) fluence = results[Tags.DATA_FIELD_FLUENCE] if not (Tags.IGNORE_QA_ASSERTIONS in self.global_settings and Tags.IGNORE_QA_ASSERTIONS): assert_array_well_defined(fluence, assume_non_negativity=True, array_name="fluence") @@ -114,7 +118,8 @@ def run_forward_model(self, device: Union[IlluminationGeometryBase, PhotoacousticDevice], absorption: np.ndarray, scattering: np.ndarray, - anisotropy: np.ndarray) -> Dict: + anisotropy: np.ndarray, + refractive_index: np.ndarray) -> Dict: """ runs `self.forward_model` as many times as defined by `device` and aggregates the results. @@ -123,6 +128,7 @@ def run_forward_model(self, :param absorption: Absorption volume :param scattering: Scattering volume :param anisotropy: Dimensionless scattering anisotropy + :param refractive_index: Refractive index :return: """ if isinstance(_device, list): @@ -130,6 +136,7 @@ def run_forward_model(self, results = self.forward_model(absorption_cm=absorption, scattering_cm=scattering, anisotropy=anisotropy, + refractive_index=refractive_index, illumination_geometry=_device[0]) fluence = results[Tags.DATA_FIELD_FLUENCE] for idx in range(1, len(_device)): @@ -137,6 +144,7 @@ def run_forward_model(self, results = self.forward_model(absorption_cm=absorption, scattering_cm=scattering, anisotropy=anisotropy, + refractive_index=refractive_index, illumination_geometry=_device[idx]) fluence += results[Tags.DATA_FIELD_FLUENCE] @@ -146,6 +154,7 @@ def run_forward_model(self, results = self.forward_model(absorption_cm=absorption, scattering_cm=scattering, anisotropy=anisotropy, + refractive_index=refractive_index, illumination_geometry=_device) fluence = results[Tags.DATA_FIELD_FLUENCE] return {Tags.DATA_FIELD_FLUENCE: fluence} diff --git a/simpa/core/simulation_modules/optical_simulation_module/optical_forward_model_mcx_adapter.py b/simpa/core/simulation_modules/optical_simulation_module/optical_forward_model_mcx_adapter.py index e76fe524..f6e63cf0 100644 --- a/simpa/core/simulation_modules/optical_simulation_module/optical_forward_model_mcx_adapter.py +++ b/simpa/core/simulation_modules/optical_simulation_module/optical_forward_model_mcx_adapter.py @@ -42,6 +42,7 @@ def forward_model(self, absorption_cm: np.ndarray, scattering_cm: np.ndarray, anisotropy: np.ndarray, + refractive_index: np.ndarray, illumination_geometry: IlluminationGeometryBase) -> Dict: """ runs the MCX simulations. Binary file containing scattering and absorption volumes is temporarily created as @@ -52,6 +53,7 @@ def forward_model(self, :param absorption_cm: array containing the absorption of the tissue in `cm` units :param scattering_cm: array containing the scattering of the tissue in `cm` units :param anisotropy: array containing the anisotropy of the volume defined by `absorption_cm` and `scattering_cm` + :param refractive_index: array containing the refractive index of the volume defined by `absorption_cm` and `scattering_cm` :param illumination_geometry: and instance of `IlluminationGeometryBase` defining the illumination geometry :return: `Dict` containing the results of optical simulations, the keys in this dictionary-like object depend on the Tags defined in `self.component_settings` @@ -64,6 +66,7 @@ def forward_model(self, self.generate_mcx_bin_input(absorption_cm=absorption_cm, scattering_cm=scattering_cm, anisotropy=anisotropy, + refractive_index=refractive_index, assumed_anisotropy=_assumed_anisotropy) settings_dict = self.get_mcx_settings(illumination_geometry=illumination_geometry, @@ -155,7 +158,7 @@ def get_mcx_settings(self, "n": 1 } ], - "MediaFormat": "muamus_float", + "MediaFormat": "asgn_float", "Dim": [self.nx, self.ny, self.nz], "VolumeFile": self.global_settings[Tags.SIMULATION_PATH] + "/" + self.global_settings[Tags.VOLUME_NAME] + ".bin" @@ -207,6 +210,7 @@ def generate_mcx_bin_input(self, absorption_cm: np.ndarray, scattering_cm: np.ndarray, anisotropy: np.ndarray, + refractive_index: np.ndarray, assumed_anisotropy: np.ndarray) -> None: """ generates binary file containing volume scattering and absorption as input for MCX @@ -214,15 +218,17 @@ def generate_mcx_bin_input(self, :param absorption_cm: Absorption in units of per centimeter :param scattering_cm: Scattering in units of per centimeter :param anisotropy: Dimensionless scattering anisotropy + :param refractive_index: Refractive index :param assumed_anisotropy: :return: None """ - absorption_mm, scattering_mm = self.pre_process_volumes(**{'absorption_cm': absorption_cm, - 'scattering_cm': scattering_cm, - 'anisotropy': anisotropy, - 'assumed_anisotropy': assumed_anisotropy}) - # stack arrays to give array with shape (nx,ny,nz,2) - op_array = np.stack([absorption_mm, scattering_mm], axis=-1, dtype=np.float32) + absorption_mm, scattering_mm, anisotropy, refractive_index = self.pre_process_volumes(**{'absorption_cm': absorption_cm, + 'scattering_cm': scattering_cm, + 'anisotropy': anisotropy, + 'refractive_index': refractive_index, + 'assumed_anisotropy': assumed_anisotropy}) + # stack arrays to give array with shape (nx,ny,nz,4) - where the 4 floats correspond to mua/mus/g/n + op_array = np.stack([absorption_mm, scattering_mm, anisotropy, refractive_index], axis=-1, dtype=np.float32) [self.nx, self.ny, self.nz, _] = np.shape(op_array) # # create a binary of the volume tmp_input_path = self.global_settings[Tags.SIMULATION_PATH] + "/" + \ @@ -263,7 +269,7 @@ def pre_process_volumes(self, **kwargs) -> Tuple: """ pre-process volumes before running simulations with MCX. The volumes are transformed to `mm` units - :param kwargs: dictionary containing at least the keys `scattering_cm, absorption_cm, anisotropy` and + :param kwargs: dictionary containing at least the keys `scattering_cm, absorption_cm, anisotropy, refractive_index` and `assumed_anisotropy` :return: `Tuple` of volumes after transformation """ @@ -274,29 +280,17 @@ def volumes_to_mm(**kwargs) -> Tuple: """ transforms volumes into `mm` units - :param kwargs: dictionary containing at least the keys `scattering_cm, absorption_cm, anisotropy` and + :param kwargs: dictionary containing at least the keys `scattering_cm, absorption_cm, anisotropy, refractive_index` and `assumed_anisotropy` :return: `Tuple` of volumes after transformation """ scattering_cm = kwargs.get('scattering_cm') absorption_cm = kwargs.get('absorption_cm') + anisotropy = kwargs.get('anisotropy') + refractive_index = kwargs.get('refractive_index') absorption_mm = absorption_cm / 10 scattering_mm = scattering_cm / 10 - - # FIXME Currently, mcx only accepts a single value for the anisotropy. - # In order to use the correct reduced scattering coefficient throughout the simulation, - # we adjust the scattering parameter to be more accurate in the diffuse regime. - # This will lead to errors, especially in the quasi-ballistic regime. - - given_reduced_scattering = (scattering_mm * (1 - kwargs.get('anisotropy'))) - - # If the anisotropy is 1, all scattering is forward scattering which is equal to no scattering at all - if kwargs.get("assumed_anisotropy") == 1: - scattering_mm = given_reduced_scattering * 0 - else: - scattering_mm = given_reduced_scattering / (1 - kwargs.get('assumed_anisotropy')) - scattering_mm[scattering_mm < 1e-10] = 1e-10 - return absorption_mm, scattering_mm + return absorption_mm, scattering_mm, anisotropy, refractive_index @staticmethod def post_process_volumes(**kwargs) -> Tuple: diff --git a/simpa/core/simulation_modules/optical_simulation_module/optical_forward_model_mcx_reflectance_adapter.py b/simpa/core/simulation_modules/optical_simulation_module/optical_forward_model_mcx_reflectance_adapter.py index 7bdd88d5..1c933195 100644 --- a/simpa/core/simulation_modules/optical_simulation_module/optical_forward_model_mcx_reflectance_adapter.py +++ b/simpa/core/simulation_modules/optical_simulation_module/optical_forward_model_mcx_reflectance_adapter.py @@ -2,7 +2,6 @@ # SPDX-FileCopyrightText: 2021 Janek Groehl # SPDX-License-Identifier: MIT import numpy as np -import struct import jdata import os from typing import List, Tuple, Dict, Union @@ -45,6 +44,7 @@ def forward_model(self, absorption_cm: np.ndarray, scattering_cm: np.ndarray, anisotropy: np.ndarray, + refractive_index: np.ndarray, illumination_geometry: IlluminationGeometryBase) -> Dict: """ runs the MCX simulations. Binary file containing scattering and absorption volumes is temporarily created as @@ -55,6 +55,7 @@ def forward_model(self, :param absorption_cm: array containing the absorption of the tissue in `cm` units :param scattering_cm: array containing the scattering of the tissue in `cm` units :param anisotropy: array containing the anisotropy of the volume defined by `absorption_cm` and `scattering_cm` + :param refractive_index: array containing the refractive index of the volume defined by `absorption_cm` and `scattering_cm` :param illumination_geometry: and instance of `IlluminationGeometryBase` defining the illumination geometry :param probe_position_mm: position of a probe in `mm` units. This is parsed to `illumination_geometry.get_mcx_illuminator_definition` @@ -69,6 +70,7 @@ def forward_model(self, self.generate_mcx_bin_input(absorption_cm=absorption_cm, scattering_cm=scattering_cm, anisotropy=_assumed_anisotropy, + refractive_index=refractive_index, assumed_anisotropy=_assumed_anisotropy) settings_dict = self.get_mcx_settings(illumination_geometry=illumination_geometry, @@ -83,7 +85,6 @@ def forward_model(self, # Read output results = self.read_mcx_output() - struct._clearcache() # clean temporary files self.remove_mcx_output() @@ -226,7 +227,8 @@ def run_forward_model(self, device: Union[IlluminationGeometryBase, PhotoacousticDevice], absorption: np.ndarray, scattering: np.ndarray, - anisotropy: np.ndarray + anisotropy: np.ndarray, + refractive_index: np.ndarray ) -> Dict: """ runs `self.forward_model` as many times as defined by `device` and aggregates the results. @@ -236,6 +238,7 @@ def run_forward_model(self, :param absorption: Absorption volume :param scattering: Scattering volume :param anisotropy: Dimensionless scattering anisotropy + :param refractive_index: Refractive index :return: """ reflectance = [] @@ -247,6 +250,7 @@ def run_forward_model(self, results = self.forward_model(absorption_cm=absorption, scattering_cm=scattering, anisotropy=anisotropy, + refractive_index=refractive_index, illumination_geometry=_device[0]) self._append_results(results=results, reflectance=reflectance, @@ -259,6 +263,7 @@ def run_forward_model(self, results = self.forward_model(absorption_cm=absorption, scattering_cm=scattering, anisotropy=anisotropy, + refractive_index=refractive_index, illumination_geometry=_device[idx]) self._append_results(results=results, reflectance=reflectance, @@ -273,6 +278,7 @@ def run_forward_model(self, results = self.forward_model(absorption_cm=absorption, scattering_cm=scattering, anisotropy=anisotropy, + refractive_index=refractive_index, illumination_geometry=_device) self._append_results(results=results, reflectance=reflectance, diff --git a/simpa/core/simulation_modules/optical_simulation_module/optical_forward_model_test_adapter.py b/simpa/core/simulation_modules/optical_simulation_module/optical_forward_model_test_adapter.py index fb891954..220865b7 100644 --- a/simpa/core/simulation_modules/optical_simulation_module/optical_forward_model_test_adapter.py +++ b/simpa/core/simulation_modules/optical_simulation_module/optical_forward_model_test_adapter.py @@ -11,6 +11,6 @@ class OpticalForwardModelTestAdapter(OpticalForwardModuleBase): This Adapter was created for testing purposes and only """ - def forward_model(self, absorption_cm, scattering_cm, anisotropy, illumination_geometry): + def forward_model(self, absorption_cm, scattering_cm, anisotropy, refractive_index, illumination_geometry): results = {Tags.DATA_FIELD_FLUENCE: absorption_cm / ((1 - anisotropy) * scattering_cm)} return results From eaf196ed9d974aa0c4bbf6157914bc0303145f1c Mon Sep 17 00:00:00 2001 From: Liam Keegan Date: Fri, 15 Mar 2024 14:06:56 +0100 Subject: [PATCH 07/34] remove assumed_anisotropy, refactor some duplicated code --- .../optical_simulation_module/__init__.py | 31 ++++-------------- .../optical_forward_model_mcx_adapter.py | 32 ++++--------------- ...l_forward_model_mcx_reflectance_adapter.py | 27 +++------------- simpa/utils/tags.py | 7 ---- 4 files changed, 16 insertions(+), 81 deletions(-) diff --git a/simpa/core/simulation_modules/optical_simulation_module/__init__.py b/simpa/core/simulation_modules/optical_simulation_module/__init__.py index 2e883e2a..54b87111 100644 --- a/simpa/core/simulation_modules/optical_simulation_module/__init__.py +++ b/simpa/core/simulation_modules/optical_simulation_module/__init__.py @@ -2,7 +2,7 @@ # SPDX-FileCopyrightText: 2021 Janek Groehl # SPDX-License-Identifier: MIT from abc import abstractmethod -from typing import Dict, Union +from typing import Dict, Union, Iterable import numpy as np @@ -131,30 +131,11 @@ def run_forward_model(self, :param refractive_index: Refractive index :return: """ - if isinstance(_device, list): - # per convention this list has at least two elements - results = self.forward_model(absorption_cm=absorption, + _devices = _device if isinstance(_device, Iterable) else (_device,) + fluence = sum(self.forward_model(absorption_cm=absorption, scattering_cm=scattering, anisotropy=anisotropy, refractive_index=refractive_index, - illumination_geometry=_device[0]) - fluence = results[Tags.DATA_FIELD_FLUENCE] - for idx in range(1, len(_device)): - # we already looked at the 0th element, so go from 1 to n-1 - results = self.forward_model(absorption_cm=absorption, - scattering_cm=scattering, - anisotropy=anisotropy, - refractive_index=refractive_index, - illumination_geometry=_device[idx]) - fluence += results[Tags.DATA_FIELD_FLUENCE] - - fluence = fluence / len(_device) - - else: - results = self.forward_model(absorption_cm=absorption, - scattering_cm=scattering, - anisotropy=anisotropy, - refractive_index=refractive_index, - illumination_geometry=_device) - fluence = results[Tags.DATA_FIELD_FLUENCE] - return {Tags.DATA_FIELD_FLUENCE: fluence} + illumination_geometry=illumination_geometry)[Tags.DATA_FIELD_FLUENCE] + for illumination_geometry in _devices) + return {Tags.DATA_FIELD_FLUENCE: fluence / len(_devices)} diff --git a/simpa/core/simulation_modules/optical_simulation_module/optical_forward_model_mcx_adapter.py b/simpa/core/simulation_modules/optical_simulation_module/optical_forward_model_mcx_adapter.py index f6e63cf0..2dfe3d4d 100644 --- a/simpa/core/simulation_modules/optical_simulation_module/optical_forward_model_mcx_adapter.py +++ b/simpa/core/simulation_modules/optical_simulation_module/optical_forward_model_mcx_adapter.py @@ -58,19 +58,12 @@ def forward_model(self, :return: `Dict` containing the results of optical simulations, the keys in this dictionary-like object depend on the Tags defined in `self.component_settings` """ - if Tags.MCX_ASSUMED_ANISOTROPY in self.component_settings: - _assumed_anisotropy = self.component_settings[Tags.MCX_ASSUMED_ANISOTROPY] - else: - _assumed_anisotropy = 0.9 - self.generate_mcx_bin_input(absorption_cm=absorption_cm, scattering_cm=scattering_cm, anisotropy=anisotropy, - refractive_index=refractive_index, - assumed_anisotropy=_assumed_anisotropy) + refractive_index=refractive_index) - settings_dict = self.get_mcx_settings(illumination_geometry=illumination_geometry, - assumed_anisotropy=_assumed_anisotropy) + settings_dict = self.get_mcx_settings(illumination_geometry=illumination_geometry) print(settings_dict) self.generate_mcx_json_input(settings_dict=settings_dict) @@ -101,14 +94,12 @@ def generate_mcx_json_input(self, settings_dict: Dict) -> None: def get_mcx_settings(self, illumination_geometry: IlluminationGeometryBase, - assumed_anisotropy: np.ndarray, **kwargs) -> Dict: """ generates MCX-specific settings for simulations based on Tags in `self.global_settings` and `self.component_settings` . Among others, it defines the volume type, dimensions and path to binary file. :param illumination_geometry: and instance of `IlluminationGeometryBase` defining the illumination geometry - :param assumed_anisotropy: :param kwargs: dummy, used for class inheritance :return: dictionary with settings to be used by MCX """ @@ -150,12 +141,6 @@ def get_mcx_settings(self, "mus": 0, "g": 1, "n": 1 - }, - { - "mua": 1, - "mus": 1, - "g": assumed_anisotropy, - "n": 1 } ], "MediaFormat": "asgn_float", @@ -210,8 +195,7 @@ def generate_mcx_bin_input(self, absorption_cm: np.ndarray, scattering_cm: np.ndarray, anisotropy: np.ndarray, - refractive_index: np.ndarray, - assumed_anisotropy: np.ndarray) -> None: + refractive_index: np.ndarray) -> None: """ generates binary file containing volume scattering and absorption as input for MCX @@ -219,14 +203,12 @@ def generate_mcx_bin_input(self, :param scattering_cm: Scattering in units of per centimeter :param anisotropy: Dimensionless scattering anisotropy :param refractive_index: Refractive index - :param assumed_anisotropy: :return: None """ absorption_mm, scattering_mm, anisotropy, refractive_index = self.pre_process_volumes(**{'absorption_cm': absorption_cm, 'scattering_cm': scattering_cm, 'anisotropy': anisotropy, - 'refractive_index': refractive_index, - 'assumed_anisotropy': assumed_anisotropy}) + 'refractive_index': refractive_index}) # stack arrays to give array with shape (nx,ny,nz,4) - where the 4 floats correspond to mua/mus/g/n op_array = np.stack([absorption_mm, scattering_mm, anisotropy, refractive_index], axis=-1, dtype=np.float32) [self.nx, self.ny, self.nz, _] = np.shape(op_array) @@ -269,8 +251,7 @@ def pre_process_volumes(self, **kwargs) -> Tuple: """ pre-process volumes before running simulations with MCX. The volumes are transformed to `mm` units - :param kwargs: dictionary containing at least the keys `scattering_cm, absorption_cm, anisotropy, refractive_index` and - `assumed_anisotropy` + :param kwargs: dictionary containing at least the keys `scattering_cm, absorption_cm, anisotropy, refractive_index` :return: `Tuple` of volumes after transformation """ return self.volumes_to_mm(**kwargs) @@ -280,8 +261,7 @@ def volumes_to_mm(**kwargs) -> Tuple: """ transforms volumes into `mm` units - :param kwargs: dictionary containing at least the keys `scattering_cm, absorption_cm, anisotropy, refractive_index` and - `assumed_anisotropy` + :param kwargs: dictionary containing at least the keys `scattering_cm, absorption_cm, anisotropy, refractive_index` :return: `Tuple` of volumes after transformation """ scattering_cm = kwargs.get('scattering_cm') diff --git a/simpa/core/simulation_modules/optical_simulation_module/optical_forward_model_mcx_reflectance_adapter.py b/simpa/core/simulation_modules/optical_simulation_module/optical_forward_model_mcx_reflectance_adapter.py index 1c933195..9ab6d800 100644 --- a/simpa/core/simulation_modules/optical_simulation_module/optical_forward_model_mcx_reflectance_adapter.py +++ b/simpa/core/simulation_modules/optical_simulation_module/optical_forward_model_mcx_reflectance_adapter.py @@ -62,20 +62,13 @@ def forward_model(self, :return: `Settings` containing the results of optical simulations, the keys in this dictionary-like object depend on the Tags defined in `self.component_settings` """ - if Tags.MCX_ASSUMED_ANISOTROPY in self.component_settings: - _assumed_anisotropy = self.component_settings[Tags.MCX_ASSUMED_ANISOTROPY] - else: - _assumed_anisotropy = 0.9 self.generate_mcx_bin_input(absorption_cm=absorption_cm, scattering_cm=scattering_cm, - anisotropy=_assumed_anisotropy, - refractive_index=refractive_index, - assumed_anisotropy=_assumed_anisotropy) + anisotropy=anisotropy, + refractive_index=refractive_index) - settings_dict = self.get_mcx_settings(illumination_geometry=illumination_geometry, - assumed_anisotropy=_assumed_anisotropy, - ) + settings_dict = self.get_mcx_settings(illumination_geometry=illumination_geometry) print(settings_dict) self.generate_mcx_json_input(settings_dict=settings_dict) @@ -96,19 +89,7 @@ def get_command(self) -> List: :return: list of MCX commands """ - cmd = list() - cmd.append(self.component_settings[Tags.OPTICAL_MODEL_BINARY_PATH]) - cmd.append("-f") - cmd.append(self.mcx_json_config_file) - cmd.append("-O") - cmd.append("F") - # use 'C' order array format for binary input file - cmd.append("-a") - cmd.append("1") - cmd.append("-F") - cmd.append("jnii") - cmd.append("-b") - cmd.append("1") + cmd = super().get_command() if Tags.COMPUTE_PHOTON_DIRECTION_AT_EXIT in self.component_settings and \ self.component_settings[Tags.COMPUTE_PHOTON_DIRECTION_AT_EXIT]: cmd.append("-H") diff --git a/simpa/utils/tags.py b/simpa/utils/tags.py index bc937cf8..f8517284 100644 --- a/simpa/utils/tags.py +++ b/simpa/utils/tags.py @@ -401,13 +401,6 @@ class Tags: Usage: module optical_modelling, adapter mcx_adapter """ - MCX_ASSUMED_ANISOTROPY = ("mcx_assumed_anisotropy", (int, float)) - """ - The anisotropy that should be assumed for the mcx simulations. - If not set, a default value of 0.9 will be assumed. - Usage: module optical_modelling, adapter mcx_adapter - """ - ILLUMINATION_TYPE = ("optical_model_illumination_type", str) """ Type of the illumination geometry used in mcx.\n From 8a7cd5493ff40d04e3f3183034a769ceb59969d3 Mon Sep 17 00:00:00 2001 From: Liam Keegan Date: Fri, 15 Mar 2024 15:15:40 +0100 Subject: [PATCH 08/34] remove debugging output --- .../optical_forward_model_mcx_adapter.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/simpa/core/simulation_modules/optical_simulation_module/optical_forward_model_mcx_adapter.py b/simpa/core/simulation_modules/optical_simulation_module/optical_forward_model_mcx_adapter.py index 2dfe3d4d..258c9899 100644 --- a/simpa/core/simulation_modules/optical_simulation_module/optical_forward_model_mcx_adapter.py +++ b/simpa/core/simulation_modules/optical_simulation_module/optical_forward_model_mcx_adapter.py @@ -228,11 +228,9 @@ def read_mcx_output(self, **kwargs) -> Dict: """ content = jdata.load(self.mcx_volumetric_data_file) fluence = content['NIFTIData'] - print(f"fluence.shape {fluence.shape}") if fluence.ndim > 3: # remove the 1 or 2 (for mcx >= v2024.1) additional dimensions of size 1 if present to obtain a 3d array fluence = fluence.reshape(fluence.shape[0], fluence.shape[1], -1) - print(f"fluence.shape {fluence.shape}") results = dict() results[Tags.DATA_FIELD_FLUENCE] = fluence return results From aca42b484066920c8c22082c6e7f9b70a74dbc9b Mon Sep 17 00:00:00 2001 From: Liam Keegan Date: Mon, 18 Mar 2024 09:50:36 +0100 Subject: [PATCH 09/34] use total absorption BC option in mcx --- .../optical_forward_model_mcx_adapter.py | 2 ++ .../optical_forward_model_mcx_reflectance_adapter.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/simpa/core/simulation_modules/optical_simulation_module/optical_forward_model_mcx_adapter.py b/simpa/core/simulation_modules/optical_simulation_module/optical_forward_model_mcx_adapter.py index 258c9899..dccfdc6d 100644 --- a/simpa/core/simulation_modules/optical_simulation_module/optical_forward_model_mcx_adapter.py +++ b/simpa/core/simulation_modules/optical_simulation_module/optical_forward_model_mcx_adapter.py @@ -174,6 +174,8 @@ def get_command(self) -> List: cmd.append("jnii") cmd.append("-b") cmd.append("1") + cmd.append("--bc") + cmd.append("aaaaaa") return cmd @staticmethod diff --git a/simpa/core/simulation_modules/optical_simulation_module/optical_forward_model_mcx_reflectance_adapter.py b/simpa/core/simulation_modules/optical_simulation_module/optical_forward_model_mcx_reflectance_adapter.py index 9ab6d800..99aeb3c8 100644 --- a/simpa/core/simulation_modules/optical_simulation_module/optical_forward_model_mcx_reflectance_adapter.py +++ b/simpa/core/simulation_modules/optical_simulation_module/optical_forward_model_mcx_reflectance_adapter.py @@ -95,7 +95,7 @@ def get_command(self) -> List: cmd.append("-H") cmd.append(f"{int(self.component_settings[Tags.OPTICAL_MODEL_NUMBER_PHOTONS])}") cmd.append("--bc") # save photon exit position and direction - cmd.append("______000010") + cmd.append("aaaaaa000010") cmd.append("--savedetflag") cmd.append("XV") if Tags.COMPUTE_DIFFUSE_REFLECTANCE in self.component_settings and \ From a3dadafb517e6cb376cb878adbe4305ce4d8991e Mon Sep 17 00:00:00 2001 From: Liam Keegan Date: Mon, 18 Mar 2024 16:12:24 +0100 Subject: [PATCH 10/34] Use binary bnii format without compression to reduce mcx IO time, add bjdata dependency --- pyproject.toml | 1 + .../optical_forward_model_mcx_adapter.py | 6 ++++-- .../optical_forward_model_mcx_reflectance_adapter.py | 5 ++--- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 979b6fd8..6f24aa7f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,6 +38,7 @@ pacfish = ">=0.4.4" # Uses BSD-License (MIT compatible) requests = ">=2.26.0" # Uses Apache 2.0-License (MIT compatible) wget = ">=3.2" # Is Public Domain (MIT compatible) jdata = ">=0.5.2" # Uses Apache 2.0-License (MIT compatible) +bjdata = ">=0.4.1" # Uses Apache 2.0-License (MIT compatible) pre-commit = ">=3.2.2" # Uses MIT-License (MIT compatible) [tool.poetry.group.docs.dependencies] diff --git a/simpa/core/simulation_modules/optical_simulation_module/optical_forward_model_mcx_adapter.py b/simpa/core/simulation_modules/optical_simulation_module/optical_forward_model_mcx_adapter.py index dccfdc6d..52a15a3a 100644 --- a/simpa/core/simulation_modules/optical_simulation_module/optical_forward_model_mcx_adapter.py +++ b/simpa/core/simulation_modules/optical_simulation_module/optical_forward_model_mcx_adapter.py @@ -36,7 +36,7 @@ def __init__(self, global_settings: Settings): self.mcx_json_config_file = None self.mcx_volumetric_data_file = None self.frames = None - self.mcx_output_suffixes = {'mcx_volumetric_data_file': '.jnii'} + self.mcx_output_suffixes = {'mcx_volumetric_data_file': '.bnii'} def forward_model(self, absorption_cm: np.ndarray, @@ -171,7 +171,9 @@ def get_command(self) -> List: cmd.append("-a") cmd.append("1") cmd.append("-F") - cmd.append("jnii") + cmd.append("bnii") + cmd.append("-Z") + cmd.append("2") cmd.append("-b") cmd.append("1") cmd.append("--bc") diff --git a/simpa/core/simulation_modules/optical_simulation_module/optical_forward_model_mcx_reflectance_adapter.py b/simpa/core/simulation_modules/optical_simulation_module/optical_forward_model_mcx_reflectance_adapter.py index 99aeb3c8..584ae6b5 100644 --- a/simpa/core/simulation_modules/optical_simulation_module/optical_forward_model_mcx_reflectance_adapter.py +++ b/simpa/core/simulation_modules/optical_simulation_module/optical_forward_model_mcx_reflectance_adapter.py @@ -37,8 +37,7 @@ def __init__(self, global_settings: Settings): super(MCXAdapterReflectance, self).__init__(global_settings=global_settings) self.mcx_photon_data_file = None self.padded = None - self.mcx_output_suffixes = {'mcx_volumetric_data_file': '.jnii', - 'mcx_photon_data_file': '_detp.jdat'} + self.mcx_output_suffixes['mcx_photon_data_file'] = '_detp.jdat' def forward_model(self, absorption_cm: np.ndarray, @@ -123,7 +122,7 @@ def read_mcx_output(self, **kwargs) -> Dict: fluence *= 100 # Convert from J/mm^2 to J/cm^2 results[Tags.DATA_FIELD_FLUENCE] = fluence else: - raise FileNotFoundError(f"Could not find .jnii file for {self.mcx_volumetric_data_file}") + raise FileNotFoundError(f"Could not find .bnii file for {self.mcx_volumetric_data_file}") if Tags.COMPUTE_DIFFUSE_REFLECTANCE in self.component_settings and \ self.component_settings[Tags.COMPUTE_DIFFUSE_REFLECTANCE]: results[Tags.DATA_FIELD_DIFFUSE_REFLECTANCE] = ref From d65590d9db3fb2129a23b1389abb6fb50d502d7e Mon Sep 17 00:00:00 2001 From: Liam Keegan Date: Mon, 18 Mar 2024 16:42:11 +0100 Subject: [PATCH 11/34] refactor bc --- .../optical_forward_model_mcx_adapter.py | 4 ++-- .../optical_forward_model_mcx_reflectance_adapter.py | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/simpa/core/simulation_modules/optical_simulation_module/optical_forward_model_mcx_adapter.py b/simpa/core/simulation_modules/optical_simulation_module/optical_forward_model_mcx_adapter.py index 52a15a3a..cab67a02 100644 --- a/simpa/core/simulation_modules/optical_simulation_module/optical_forward_model_mcx_adapter.py +++ b/simpa/core/simulation_modules/optical_simulation_module/optical_forward_model_mcx_adapter.py @@ -155,7 +155,7 @@ def get_mcx_settings(self, settings_dict["Session"]["RNGSeed"] = self.component_settings[Tags.MCX_SEED] return settings_dict - def get_command(self) -> List: + def get_command(self, bc="aaaaaa") -> List: """ generates list of commands to be parse to MCX in a subprocess @@ -177,7 +177,7 @@ def get_command(self) -> List: cmd.append("-b") cmd.append("1") cmd.append("--bc") - cmd.append("aaaaaa") + cmd.append(bc) return cmd @staticmethod diff --git a/simpa/core/simulation_modules/optical_simulation_module/optical_forward_model_mcx_reflectance_adapter.py b/simpa/core/simulation_modules/optical_simulation_module/optical_forward_model_mcx_reflectance_adapter.py index 584ae6b5..82267de4 100644 --- a/simpa/core/simulation_modules/optical_simulation_module/optical_forward_model_mcx_reflectance_adapter.py +++ b/simpa/core/simulation_modules/optical_simulation_module/optical_forward_model_mcx_reflectance_adapter.py @@ -37,7 +37,8 @@ def __init__(self, global_settings: Settings): super(MCXAdapterReflectance, self).__init__(global_settings=global_settings) self.mcx_photon_data_file = None self.padded = None - self.mcx_output_suffixes['mcx_photon_data_file'] = '_detp.jdat' + self.mcx_output_suffixes = {'mcx_volumetric_data_file': '.bnii', + 'mcx_photon_data_file': '_detp.jdat'} def forward_model(self, absorption_cm: np.ndarray, @@ -82,24 +83,23 @@ def forward_model(self, self.remove_mcx_output() return results - def get_command(self) -> List: + def get_command(self, bc="aaaaaa000010") -> List: """ generates list of commands to be parse to MCX in a subprocess :return: list of MCX commands """ - cmd = super().get_command() + cmd = super().get_command(bc=bc) if Tags.COMPUTE_PHOTON_DIRECTION_AT_EXIT in self.component_settings and \ self.component_settings[Tags.COMPUTE_PHOTON_DIRECTION_AT_EXIT]: cmd.append("-H") cmd.append(f"{int(self.component_settings[Tags.OPTICAL_MODEL_NUMBER_PHOTONS])}") - cmd.append("--bc") # save photon exit position and direction - cmd.append("aaaaaa000010") cmd.append("--savedetflag") cmd.append("XV") if Tags.COMPUTE_DIFFUSE_REFLECTANCE in self.component_settings and \ self.component_settings[Tags.COMPUTE_DIFFUSE_REFLECTANCE]: cmd.append("--saveref") # save diffuse reflectance at 0 filled voxels outside of domain + cmd.append("1") return cmd def read_mcx_output(self, **kwargs) -> Dict: From f51de56cfac50bd0dc86fc9ccd71f7dab3f6276b Mon Sep 17 00:00:00 2001 From: v496a Date: Wed, 30 Oct 2024 11:17:30 +0100 Subject: [PATCH 12/34] Merged with develop, fixed simulation issues --- .github/workflows/pypi.yml | 53 +- .gitignore | 3 + .pre-commit-config.yaml | 15 +- .readthedocs.yaml | 14 +- CONTRIBUTING.md | 22 +- MANIFEST.in | 1 - README.md | 25 +- docs/source/clean_up_rst_files.py | 2 +- docs/source/introduction.md | 30 +- ...inimal_optical_simulation_uniform_cube.rst | 7 + ...ice_digital_twins.detection_geometries.rst | 6 - ..._digital_twins.illumination_geometries.rst | 16 +- .../simpa.core.device_digital_twins.rst | 5 - ...tion_modules.optical_simulation_module.rst | 6 + ...ibraries.refractive_index_spectra_data.rst | 7 + docs/source/simpa.utils.libraries.rst | 1 + docs/source/simpa.utils.rst | 12 + docs/source/simpa_examples.rst | 9 +- poetry.lock | 1987 ----------------- .../license_header.txt | 0 pre_commit_configs/link-config.json | 7 + pyproject.toml | 100 +- simpa/core/__init__.py | 8 +- simpa/core/device_digital_twins/__init__.py | 168 +- .../detection_geometries/__init__.py | 133 ++ .../detection_geometries/curved_array.py | 3 +- .../detection_geometry_base.py | 136 -- .../digital_device_twin_base.py | 296 --- .../illumination_geometries/__init__.py | 67 + .../illumination_geometry_base.py | 71 - .../ithera_msot_invision_illumination.py | 2 +- .../pencil_array_illumination.py | 2 +- .../rectangle_illumination.py | 115 + .../ring_illumination.py | 124 + .../slit_illumination.py | 6 +- .../pa_devices/__init__.py | 158 ++ .../pa_devices/ithera_msot_acuity.py | 3 + simpa/core/processing_components/__init__.py | 8 +- simpa/core/simulation.py | 3 +- simpa/core/simulation_modules/__init__.py | 29 + .../acoustic_forward_module/__init__.py | 10 +- .../acoustic_forward_module_k_wave_adapter.py | 16 +- .../optical_simulation_module/__init__.py | 10 +- .../optical_forward_model_mcx_adapter.py | 4 +- ...l_forward_model_mcx_reflectance_adapter.py | 52 +- .../volume_boundary_condition.py | 29 + .../reconstruction_module/__init__.py | 12 +- ...nstruction_module_time_reversal_adapter.py | 2 +- .../volume_creation_module/__init__.py | 13 +- ...ation_module_segmentation_based_adapter.py | 10 +- simpa/utils/calculate.py | 88 +- simpa/utils/deformation_manager.py | 34 +- simpa/utils/libraries/molecule_library.py | 233 +- simpa/utils/libraries/spectrum_library.py | 178 +- .../CircularTubularStructure.py | 7 +- .../EllipticalTubularStructure.py | 7 +- .../HorizontalLayerStructure.py | 7 +- .../ParallelepipedStructure.py | 2 +- .../structure_library/VesselStructure.py | 8 +- simpa/utils/libraries/tissue_library.py | 220 +- simpa/utils/path_manager.py | 19 +- simpa/utils/profiling.py | 29 + simpa/utils/tags.py | 30 +- .../matplotlib_data_visualisation.py | 8 +- simpa_examples/minimal_optical_simulation.py | 4 +- ...minimal_optical_simulation_uniform_cube.py | 94 + simpa_examples/path_config.env.example | 7 + .../perform_iterative_qPAI_reconstruction.py | 4 +- simpa_tests/__init__.py | 13 - .../device_tests/test_field_of_view.py | 22 +- .../test_rectangle_illumination.py | 67 + .../device_tests/test_ring_illumination.py | 101 + .../structure_tests/test_elliptical_tubes.py | 12 +- .../structure_tests/test_vesseltree.py | 88 +- .../automatic_tests/test_bandpass_filter.py | 10 - .../automatic_tests/test_device_UUID.py | 2 +- simpa_tests/automatic_tests/test_equal.py | 78 + .../automatic_tests/test_io_handling.py | 3 +- .../automatic_tests/test_path_manager.py | 74 +- .../tissue_library/test_tissue_library.py | 80 + simpa_tests/do_coverage.py | 15 +- simpa_tests/manual_tests/__init__.py | 6 +- ...KWaveAcousticForwardConvenienceFunction.py | 27 +- .../MinimalKWaveTest.py | 6 +- .../QPAIReconstruction.py | 8 +- .../TestLinearUnmixingVisual.py | 6 +- 86 files changed, 2371 insertions(+), 3044 deletions(-) delete mode 100644 MANIFEST.in create mode 100644 docs/source/minimal_optical_simulation_uniform_cube.rst create mode 100644 docs/source/simpa.utils.libraries.refractive_index_spectra_data.rst rename license_header.txt => pre_commit_configs/license_header.txt (100%) create mode 100644 pre_commit_configs/link-config.json delete mode 100644 simpa/core/device_digital_twins/detection_geometries/detection_geometry_base.py delete mode 100644 simpa/core/device_digital_twins/digital_device_twin_base.py delete mode 100644 simpa/core/device_digital_twins/illumination_geometries/illumination_geometry_base.py create mode 100644 simpa/core/device_digital_twins/illumination_geometries/rectangle_illumination.py create mode 100644 simpa/core/device_digital_twins/illumination_geometries/ring_illumination.py create mode 100644 simpa/core/simulation_modules/optical_simulation_module/volume_boundary_condition.py create mode 100644 simpa/utils/profiling.py create mode 100644 simpa_examples/minimal_optical_simulation_uniform_cube.py create mode 100644 simpa_examples/path_config.env.example create mode 100644 simpa_tests/automatic_tests/device_tests/test_rectangle_illumination.py create mode 100644 simpa_tests/automatic_tests/device_tests/test_ring_illumination.py create mode 100644 simpa_tests/automatic_tests/test_equal.py create mode 100644 simpa_tests/automatic_tests/tissue_library/test_tissue_library.py diff --git a/.github/workflows/pypi.yml b/.github/workflows/pypi.yml index 6ab15958..fe72e9e8 100644 --- a/.github/workflows/pypi.yml +++ b/.github/workflows/pypi.yml @@ -1,20 +1,53 @@ -# Uploads the current version of the package to PyPI when commit are pushed to main (including merges via pull requests) +# Uploads the current version of the package to PyPI when publishing a release name: Upload to PyPI on: - push: - branches: - - main + release: + types: [published] jobs: - build-and-publish: + release-build: if: github.repository == 'IMSY-DKFZ/simpa' runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - name: Build and publish to pypi - uses: JRubics/poetry-publish@v1.17 + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: "3.x" + + - name: Build release distributions + run: | + python -m pip install build + python -m build + + - name: Upload distributions + uses: actions/upload-artifact@v4 + with: + name: release-dists + path: dist/ + + pypi-publish: + if: github.repository == 'IMSY-DKFZ/simpa' + runs-on: ubuntu-latest + + needs: + - release-build + + permissions: + id-token: write + + environment: + name: pypi + url: https://pypi.org/project/simpa/ + + steps: + - name: Retrieve release distributions + uses: actions/download-artifact@v4 with: - pypi_token: ${{ secrets.PYPI_TOKEN }} - python_version: '3.10' + name: release-dists + path: dist/ + + - name: Publish release distributions to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/.gitignore b/.gitignore index 09d1928c..d65fe579 100644 --- a/.gitignore +++ b/.gitignore @@ -143,6 +143,9 @@ dmypy.json .idea/* /*/.idea/*/ +#VSCode +.vscode/ + # numpy files *npy /.mailmap diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 529ef16a..fd899a52 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -23,7 +23,7 @@ repos: types: [python] args: - --license-filepath - - license_header.txt # defaults to: LICENSE.txt + - pre_commit_configs/license_header.txt # defaults to: LICENSE.txt - repo: https://github.com/jorisroovers/gitlint # Uses MIT License (MIT compatible) rev: v0.19.1 @@ -36,6 +36,19 @@ repos: - id: markdown-link-check # checks if links in markdown files work exclude: docs/ types: [markdown] + args: + - -c + - pre_commit_configs/link-config.json + always_run: true + +- repo: local + hooks: + - id: build-sphinx-docs + name: Build Sphinx documentation + entry: make -C docs html # builds the sphinx documentation + language: system + pass_filenames: false + always_run: true # toggle comment to perform git config user email check. Note that you have to place the check-email.sh script in your .git/hooks/ folder # - repo: local diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 86e7a6d4..55958258 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -3,16 +3,9 @@ version: 2 # Set the version of Python and other tools you might need build: - os: ubuntu-20.04 - tools: {python: "3.9"} - jobs: - pre_create_environment: - - asdf plugin add poetry - - asdf install poetry latest - - asdf global poetry latest - - poetry config virtualenvs.create true - post_install: - - poetry install + os: ubuntu-22.04 + tools: + python: "3.10" # Build documentation in the docs/ directory with Sphinx sphinx: @@ -20,6 +13,7 @@ sphinx: configuration: docs/source/conf.py fail_on_warning: false +# Python configuration python: install: - method: pip diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ec51832f..a8fb28f6 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -29,19 +29,19 @@ In general the following steps are involved during a contribution: 2. Discuss potential contribution with core development team 3. Fork the [SIMPA repository](https://github.com/IMSY-DKFZ/simpa) 4. Create feature branch from develop using the naming convention T_, - where represent the number github assigned the created issue and describes - what is being developed in CamelCaseNotation. - Examples: `T13_FixSimulatorBug`, `T27_AddNewSimulator` - + where represent the number github assigned the created issue and describes + what is being developed in CamelCaseNotation. + Examples: `T13_FixSimulatorBug`, `T27_AddNewSimulator` 5. Perform test driven development on feature branch. - A new implemented feature / a bug fix should be accompanied by a test. - Additionally, all previously existing tests must still pass after the contribution. -6. Run pre-commit hooks and make sure all hooks are passing. + A new implemented feature / a bug fix should be accompanied by a test. + Additionally, all previously existing tests must still pass after the contribution. +6. Run pre-commit hooks and make sure all hooks are passing. 7. Once development is finished, create a pull request including your changes. - For more information on how to create pull request, see GitHub's [about pull requests](https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/about-pull-requests). -8. A member of the core development team will review your pull request and potentially require further changes - (see [Contribution review and integration](#contribution-review-and-integration)). - Once all remarks have been resolved, your changes will be merged into the develop branch. + For more information on how to create pull request, see GitHub's [about pull requests](https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/about-pull-requests). +8. If there are conflicts between the simpa develop branch and your branch, you should update your feature branch with the simpa develop branch using a "merge" strategy instead of "rebase". +9. A member of the core development team will review your pull request and potentially require further changes + (see [Contribution review and integration](#contribution-review-and-integration)). + Once all remarks have been resolved, your changes will be merged into the develop branch. For each contribution, please make sure to consider the following: diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index ae9d67e9..00000000 --- a/MANIFEST.in +++ /dev/null @@ -1 +0,0 @@ -recursive-exclude simpa_tests * \ No newline at end of file diff --git a/README.md b/README.md index f61481dc..9945dfb7 100755 --- a/README.md +++ b/README.md @@ -75,11 +75,11 @@ acoustic simulations possible. ### mcx (Optical Forward Model) -Download the latest nightly build of [mcx](http://mcx.space/) for your operating system: +Download the latest nightly build of [mcx](http://mcx.space/) on [this page](http://mcx.space/nightly/github/) for your operating system: -- [Linux](http://mcx.space/nightly/github/mcx-linux-x64-github-latest.zip) -- [MacOS](http://mcx.space/nightly/github/mcx-macos-x64-github-latest.zip) -- [Windows](http://mcx.space/nightly/github/mcx-windows-x64-github-latest.zip) +- Linux: `mcx-linux-x64-github-latest.zip` +- MacOS: `mcx-macos-x64-github-latest.zip` +- Windows: `mcx-windows-x64-github-latest.zip` Then extract the files and set `MCX_BINARY_PATH=/.../mcx/bin/mcx` in your path_config.env. @@ -100,15 +100,19 @@ for further (and much better) guidance under: As a pipelining tool that serves as a communication layer between different numerical forward models and processing tools, SIMPA needs to be configured with the paths to these tools on your local hard drive. -To this end, we have implemented the `PathManager` class that you can import to your project using -`from simpa.utils import PathManager`. The PathManager looks for a `path_config.env` file (just like the -one we provided in the `simpa_examples`) in the following places in this order: +You have a couple of options to define the required path variables. +### Option 1: +Ensure that the environment variables defined in `simpa_examples/path_config.env.example` are accessible to your script during runtime. This can be done through any method you prefer, as long as the environment variables are accessible through `os.environ`. +### Option 2: +Import the `PathManager` class to your project using +`from simpa.utils import PathManager`. If a path to a `.env` file is not provided, the `PathManager` looks for a `path_config.env` file (just like the +one we provided in the `simpa_examples/path_config.env.example`) in the following places, in this order: 1. The optional path you give the PathManager 2. Your $HOME$ directory 3. The current working directory 4. The SIMPA home directory path - -Please follow the instructions in the `path_config.env` file in the `simpa_examples` folder. + +For this option, please follow the instructions in the `simpa_examples/path_config.env.example` file. # Simulation examples @@ -169,6 +173,7 @@ suggested changes. The core developers will then review the suggested changes an base. Please make sure that you have included unit tests for your code and that all previous tests still run through. Please also run the pre-commit hooks and make sure they are passing. +Details are found in our [contribution guidelines](CONTRIBUTING.md). There is a regular SIMPA status meeting every Friday on even calendar weeks at 10:00 CET/CEST, and you are very welcome to participate and raise any issues or suggest new features. If you want to join this meeting, write one of the core developers. @@ -221,4 +226,4 @@ Gröhl, Janek, Kris K. Dreher, Melanie Schellenberg, Tom Rix, Niklas Holzwarth, This project has received funding from the European Research Council (ERC) under the European Union’s Horizon 2020 research and innovation programme (grant agreement No. [101002198]). -![ERC](docs/source/images/LOGO_ERC-FLAG_EU_.jpg "ERC") \ No newline at end of file +![ERC](docs/source/images/LOGO_ERC-FLAG_EU_.jpg "ERC") diff --git a/docs/source/clean_up_rst_files.py b/docs/source/clean_up_rst_files.py index 5b24fda4..b844956f 100644 --- a/docs/source/clean_up_rst_files.py +++ b/docs/source/clean_up_rst_files.py @@ -42,7 +42,7 @@ simpa_examples_rst_file = open(os.path.join(current_dir, "simpa_examples.rst"), "w") simpa_examples_rst_file.write( "simpa\_examples\n=========================================\n\n.. toctree::\n :maxdepth: 2\n\n") -examples = glob.glob(os.path.join(current_dir, "../" + folder_level + "simpa_examples/*.py")) +examples = sorted(glob.glob(os.path.join(current_dir, "../" + folder_level + "simpa_examples/*.py"))) for example in examples: example_file_name = example.split("/")[-1] if example_file_name == "__init__.py": diff --git a/docs/source/introduction.md b/docs/source/introduction.md index 593b74d1..9ce346d4 100644 --- a/docs/source/introduction.md +++ b/docs/source/introduction.md @@ -39,11 +39,11 @@ acoustic simulations possible. ### mcx (Optical Forward Model) -Download the latest nightly build of [mcx](http://mcx.space/) for your operating system: +Download the latest nightly build of [mcx](http://mcx.space/) on [this page](http://mcx.space/nightly/github/) for your operating system: -- [Linux](http://mcx.space/nightly/github/mcx-linux-x64-github-latest.zip) -- [MacOS](http://mcx.space/nightly/github/mcx-macos-x64-github-latest.zip) -- [Windows](http://mcx.space/nightly/github/mcx-windows-x64-github-latest.zip) +- Linux: `mcx-linux-x64-github-latest.zip` +- MacOS: `mcx-macos-x64-github-latest.zip` +- Windows: `mcx-windows-x64-github-latest.zip` Then extract the files and set `MCX_BINARY_PATH=/.../mcx/bin/mcx` in your path_config.env. @@ -55,26 +55,28 @@ for further (and much better) guidance under: [http://www.k-wave.org/](http://www.k-wave.org/) 1. Install MATLAB with the core, image processing and parallel computing toolboxes activated at the minimum. -2. Download the kWave toolbox +2. Download the kWave toolbox (version >= 1.4) 3. Add the kWave toolbox base path to the toolbox paths in MATLAB -4. Download the kWaveArray addition from the link given in this user forum post [http://www.k-wave.org/forum/topic/alpha-version-of-kwavearray-off-grid-sources](http://www.k-wave.org/forum/topic/alpha-version-of-kwavearray-off-grid-sources) -5. Add the kWaveArray folder to the toolbox paths in MATLAB as well -6. If wanted: Download the CPP and CUDA binary files and place them in the k-Wave/binaries folder -7. Note down the system path to the `matlab` executable file. +4. If wanted: Download the CPP and CUDA binary files and place them in the k-Wave/binaries folder +5. Note down the system path to the `matlab` executable file. ## Path management As a pipelining tool that serves as a communication layer between different numerical forward models and processing tools, SIMPA needs to be configured with the paths to these tools on your local hard drive. -To this end, we have implemented the `PathManager` class that you can import to your project using -`from simpa.utils import PathManager`. The PathManager looks for a `path_config.env` file (just like the -one we provided in the `simpa_examples`) in the following places in this order: +You have a couple of options to define the required path variables. +### Option 1: +Ensure that the environment variables defined in `simpa_examples/path_config.env.example` are accessible to your script during runtime. This can be done through any method you prefer, as long as the environment variables are accessible through `os.environ`. +### Option 2: +Import the `PathManager` class to your project using +`from simpa.utils import PathManager`. If a path to a `.env` file is not provided, the `PathManager` looks for a `path_config.env` file (just like the +one we provided in the `simpa_examples/path_config.env.example`) in the following places, in this order: 1. The optional path you give the PathManager 2. Your $HOME$ directory 3. The current working directory 4. The SIMPA home directory path - -Please follow the instructions in the `path_config.env` file in the `simpa_examples` folder. + +For this option, please follow the instructions in the `simpa_examples/path_config.env.example` file. # Simulation examples diff --git a/docs/source/minimal_optical_simulation_uniform_cube.rst b/docs/source/minimal_optical_simulation_uniform_cube.rst new file mode 100644 index 00000000..5ae3e687 --- /dev/null +++ b/docs/source/minimal_optical_simulation_uniform_cube.rst @@ -0,0 +1,7 @@ +minimal_optical_simulation_uniform_cube +========================================= + +.. literalinclude:: ../../simpa_examples/minimal_optical_simulation_uniform_cube.py + :language: python + :lines: 1- + diff --git a/docs/source/simpa.core.device_digital_twins.detection_geometries.rst b/docs/source/simpa.core.device_digital_twins.detection_geometries.rst index 413d22fa..3eb9caa8 100644 --- a/docs/source/simpa.core.device_digital_twins.detection_geometries.rst +++ b/docs/source/simpa.core.device_digital_twins.detection_geometries.rst @@ -12,12 +12,6 @@ detection\_geometries :show-inheritance: -.. automodule:: simpa.core.device_digital_twins.detection_geometries.detection_geometry_base - :members: - :undoc-members: - :show-inheritance: - - .. automodule:: simpa.core.device_digital_twins.detection_geometries.linear_array :members: :undoc-members: diff --git a/docs/source/simpa.core.device_digital_twins.illumination_geometries.rst b/docs/source/simpa.core.device_digital_twins.illumination_geometries.rst index 0759ced0..eb29ecbd 100644 --- a/docs/source/simpa.core.device_digital_twins.illumination_geometries.rst +++ b/docs/source/simpa.core.device_digital_twins.illumination_geometries.rst @@ -18,31 +18,37 @@ illumination\_geometries :show-inheritance: -.. automodule:: simpa.core.device_digital_twins.illumination_geometries.illumination_geometry_base +.. automodule:: simpa.core.device_digital_twins.illumination_geometries.ithera_msot_acuity_illumination :members: :undoc-members: :show-inheritance: -.. automodule:: simpa.core.device_digital_twins.illumination_geometries.ithera_msot_acuity_illumination +.. automodule:: simpa.core.device_digital_twins.illumination_geometries.ithera_msot_invision_illumination :members: :undoc-members: :show-inheritance: -.. automodule:: simpa.core.device_digital_twins.illumination_geometries.ithera_msot_invision_illumination +.. automodule:: simpa.core.device_digital_twins.illumination_geometries.pencil_array_illumination :members: :undoc-members: :show-inheritance: -.. automodule:: simpa.core.device_digital_twins.illumination_geometries.pencil_array_illumination +.. automodule:: simpa.core.device_digital_twins.illumination_geometries.pencil_beam_illumination :members: :undoc-members: :show-inheritance: -.. automodule:: simpa.core.device_digital_twins.illumination_geometries.pencil_beam_illumination +.. automodule:: simpa.core.device_digital_twins.illumination_geometries.rectangle_illumination + :members: + :undoc-members: + :show-inheritance: + + +.. automodule:: simpa.core.device_digital_twins.illumination_geometries.ring_illumination :members: :undoc-members: :show-inheritance: diff --git a/docs/source/simpa.core.device_digital_twins.rst b/docs/source/simpa.core.device_digital_twins.rst index a8aeb366..5b25dcc9 100644 --- a/docs/source/simpa.core.device_digital_twins.rst +++ b/docs/source/simpa.core.device_digital_twins.rst @@ -12,8 +12,3 @@ device\_digital\_twins simpa.core.device_digital_twins.detection_geometries simpa.core.device_digital_twins.illumination_geometries simpa.core.device_digital_twins.pa_devices - -.. automodule:: simpa.core.device_digital_twins.digital_device_twin_base - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/simpa.core.simulation_modules.optical_simulation_module.rst b/docs/source/simpa.core.simulation_modules.optical_simulation_module.rst index 8d6bc5c9..558a8de6 100644 --- a/docs/source/simpa.core.simulation_modules.optical_simulation_module.rst +++ b/docs/source/simpa.core.simulation_modules.optical_simulation_module.rst @@ -22,3 +22,9 @@ optical\_simulation\_module :members: :undoc-members: :show-inheritance: + + +.. automodule:: simpa.core.simulation_modules.optical_simulation_module.volume_boundary_condition + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/simpa.utils.libraries.refractive_index_spectra_data.rst b/docs/source/simpa.utils.libraries.refractive_index_spectra_data.rst new file mode 100644 index 00000000..730b7918 --- /dev/null +++ b/docs/source/simpa.utils.libraries.refractive_index_spectra_data.rst @@ -0,0 +1,7 @@ +refractive\_index\_spectra\_data +============================================================== + +.. automodule:: simpa.utils.libraries.refractive_index_spectra_data + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/simpa.utils.libraries.rst b/docs/source/simpa.utils.libraries.rst index 5c38a7a0..5e1dd335 100644 --- a/docs/source/simpa.utils.libraries.rst +++ b/docs/source/simpa.utils.libraries.rst @@ -11,6 +11,7 @@ libraries simpa.utils.libraries.absorption_spectra_data simpa.utils.libraries.anisotropy_spectra_data + simpa.utils.libraries.refractive_index_spectra_data simpa.utils.libraries.scattering_spectra_data simpa.utils.libraries.structure_library diff --git a/docs/source/simpa.utils.rst b/docs/source/simpa.utils.rst index c8b151b9..5563d127 100644 --- a/docs/source/simpa.utils.rst +++ b/docs/source/simpa.utils.rst @@ -36,6 +36,12 @@ utils :show-inheritance: +.. automodule:: simpa.utils.matlab + :members: + :undoc-members: + :show-inheritance: + + .. automodule:: simpa.utils.path_manager :members: :undoc-members: @@ -48,6 +54,12 @@ utils :show-inheritance: +.. automodule:: simpa.utils.profiling + :members: + :undoc-members: + :show-inheritance: + + .. automodule:: simpa.utils.serializer :members: :undoc-members: diff --git a/docs/source/simpa_examples.rst b/docs/source/simpa_examples.rst index 458cd47a..e8a54665 100644 --- a/docs/source/simpa_examples.rst +++ b/docs/source/simpa_examples.rst @@ -4,12 +4,13 @@ simpa\_examples .. toctree:: :maxdepth: 2 - linear_unmixing create_a_custom_digital_device_twin + create_custom_tissues + linear_unmixing + minimal_optical_simulation + minimal_optical_simulation_uniform_cube msot_invision_simulation + optical_and_acoustic_simulation perform_image_reconstruction perform_iterative_qPAI_reconstruction - create_custom_tissues segmentation_loader - minimal_optical_simulation - optical_and_acoustic_simulation diff --git a/poetry.lock b/poetry.lock index 03a20b61..e69de29b 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,1987 +0,0 @@ -# This file is automatically @generated by Poetry 1.5.1 and should not be changed by hand. - -[[package]] -name = "alabaster" -version = "0.7.13" -description = "A configurable sidebar-enabled Sphinx theme" -optional = false -python-versions = ">=3.6" -files = [ - {file = "alabaster-0.7.13-py3-none-any.whl", hash = "sha256:1ee19aca801bbabb5ba3f5f258e4422dfa86f82f3e9cefb0859b283cdd7f62a3"}, - {file = "alabaster-0.7.13.tar.gz", hash = "sha256:a27a4a084d5e690e16e01e03ad2b2e552c61a65469419b907243193de1a84ae2"}, -] - -[[package]] -name = "babel" -version = "2.12.1" -description = "Internationalization utilities" -optional = false -python-versions = ">=3.7" -files = [ - {file = "Babel-2.12.1-py3-none-any.whl", hash = "sha256:b4246fb7677d3b98f501a39d43396d3cafdc8eadb045f4a31be01863f655c610"}, - {file = "Babel-2.12.1.tar.gz", hash = "sha256:cc2d99999cd01d44420ae725a21c9e3711b3aadc7976d6147f622d8581963455"}, -] - -[package.dependencies] -pytz = {version = ">=2015.7", markers = "python_version < \"3.9\""} - -[[package]] -name = "certifi" -version = "2023.7.22" -description = "Python package for providing Mozilla's CA Bundle." -optional = false -python-versions = ">=3.6" -files = [ - {file = "certifi-2023.7.22-py3-none-any.whl", hash = "sha256:92d6037539857d8206b8f6ae472e8b77db8058fec5937a1ef3f54304089edbb9"}, - {file = "certifi-2023.7.22.tar.gz", hash = "sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082"}, -] - -[[package]] -name = "cfgv" -version = "3.3.1" -description = "Validate configuration and produce human readable error messages." -optional = false -python-versions = ">=3.6.1" -files = [ - {file = "cfgv-3.3.1-py2.py3-none-any.whl", hash = "sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426"}, - {file = "cfgv-3.3.1.tar.gz", hash = "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736"}, -] - -[[package]] -name = "charset-normalizer" -version = "3.2.0" -description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." -optional = false -python-versions = ">=3.7.0" -files = [ - {file = "charset-normalizer-3.2.0.tar.gz", hash = "sha256:3bb3d25a8e6c0aedd251753a79ae98a093c7e7b471faa3aa9a93a81431987ace"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0b87549028f680ca955556e3bd57013ab47474c3124dc069faa0b6545b6c9710"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7c70087bfee18a42b4040bb9ec1ca15a08242cf5867c58726530bdf3945672ed"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a103b3a7069b62f5d4890ae1b8f0597618f628b286b03d4bc9195230b154bfa9"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94aea8eff76ee6d1cdacb07dd2123a68283cb5569e0250feab1240058f53b623"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:db901e2ac34c931d73054d9797383d0f8009991e723dab15109740a63e7f902a"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b0dac0ff919ba34d4df1b6131f59ce95b08b9065233446be7e459f95554c0dc8"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:193cbc708ea3aca45e7221ae58f0fd63f933753a9bfb498a3b474878f12caaad"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09393e1b2a9461950b1c9a45d5fd251dc7c6f228acab64da1c9c0165d9c7765c"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:baacc6aee0b2ef6f3d308e197b5d7a81c0e70b06beae1f1fcacffdbd124fe0e3"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:bf420121d4c8dce6b889f0e8e4ec0ca34b7f40186203f06a946fa0276ba54029"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:c04a46716adde8d927adb9457bbe39cf473e1e2c2f5d0a16ceb837e5d841ad4f"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:aaf63899c94de41fe3cf934601b0f7ccb6b428c6e4eeb80da72c58eab077b19a"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d62e51710986674142526ab9f78663ca2b0726066ae26b78b22e0f5e571238dd"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-win32.whl", hash = "sha256:04e57ab9fbf9607b77f7d057974694b4f6b142da9ed4a199859d9d4d5c63fe96"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:48021783bdf96e3d6de03a6e39a1171ed5bd7e8bb93fc84cc649d11490f87cea"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4957669ef390f0e6719db3613ab3a7631e68424604a7b448f079bee145da6e09"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:46fb8c61d794b78ec7134a715a3e564aafc8f6b5e338417cb19fe9f57a5a9bf2"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f779d3ad205f108d14e99bb3859aa7dd8e9c68874617c72354d7ecaec2a054ac"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f25c229a6ba38a35ae6e25ca1264621cc25d4d38dca2942a7fce0b67a4efe918"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2efb1bd13885392adfda4614c33d3b68dee4921fd0ac1d3988f8cbb7d589e72a"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f30b48dd7fa1474554b0b0f3fdfdd4c13b5c737a3c6284d3cdc424ec0ffff3a"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:246de67b99b6851627d945db38147d1b209a899311b1305dd84916f2b88526c6"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9bd9b3b31adcb054116447ea22caa61a285d92e94d710aa5ec97992ff5eb7cf3"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:8c2f5e83493748286002f9369f3e6607c565a6a90425a3a1fef5ae32a36d749d"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:3170c9399da12c9dc66366e9d14da8bf7147e1e9d9ea566067bbce7bb74bd9c2"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:7a4826ad2bd6b07ca615c74ab91f32f6c96d08f6fcc3902ceeedaec8cdc3bcd6"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:3b1613dd5aee995ec6d4c69f00378bbd07614702a315a2cf6c1d21461fe17c23"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9e608aafdb55eb9f255034709e20d5a83b6d60c054df0802fa9c9883d0a937aa"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-win32.whl", hash = "sha256:f2a1d0fd4242bd8643ce6f98927cf9c04540af6efa92323e9d3124f57727bfc1"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:681eb3d7e02e3c3655d1b16059fbfb605ac464c834a0c629048a30fad2b27489"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c57921cda3a80d0f2b8aec7e25c8aa14479ea92b5b51b6876d975d925a2ea346"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:41b25eaa7d15909cf3ac4c96088c1f266a9a93ec44f87f1d13d4a0e86c81b982"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f058f6963fd82eb143c692cecdc89e075fa0828db2e5b291070485390b2f1c9c"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a7647ebdfb9682b7bb97e2a5e7cb6ae735b1c25008a70b906aecca294ee96cf4"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eef9df1eefada2c09a5e7a40991b9fc6ac6ef20b1372abd48d2794a316dc0449"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e03b8895a6990c9ab2cdcd0f2fe44088ca1c65ae592b8f795c3294af00a461c3"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:ee4006268ed33370957f55bf2e6f4d263eaf4dc3cfc473d1d90baff6ed36ce4a"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c4983bf937209c57240cff65906b18bb35e64ae872da6a0db937d7b4af845dd7"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:3bb7fda7260735efe66d5107fb7e6af6a7c04c7fce9b2514e04b7a74b06bf5dd"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:72814c01533f51d68702802d74f77ea026b5ec52793c791e2da806a3844a46c3"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:70c610f6cbe4b9fce272c407dd9d07e33e6bf7b4aa1b7ffb6f6ded8e634e3592"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-win32.whl", hash = "sha256:a401b4598e5d3f4a9a811f3daf42ee2291790c7f9d74b18d75d6e21dda98a1a1"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-win_amd64.whl", hash = "sha256:c0b21078a4b56965e2b12f247467b234734491897e99c1d51cee628da9786959"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:95eb302ff792e12aba9a8b8f8474ab229a83c103d74a750ec0bd1c1eea32e669"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1a100c6d595a7f316f1b6f01d20815d916e75ff98c27a01ae817439ea7726329"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:6339d047dab2780cc6220f46306628e04d9750f02f983ddb37439ca47ced7149"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e4b749b9cc6ee664a3300bb3a273c1ca8068c46be705b6c31cf5d276f8628a94"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a38856a971c602f98472050165cea2cdc97709240373041b69030be15047691f"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f87f746ee241d30d6ed93969de31e5ffd09a2961a051e60ae6bddde9ec3583aa"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:89f1b185a01fe560bc8ae5f619e924407efca2191b56ce749ec84982fc59a32a"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e1c8a2f4c69e08e89632defbfabec2feb8a8d99edc9f89ce33c4b9e36ab63037"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2f4ac36d8e2b4cc1aa71df3dd84ff8efbe3bfb97ac41242fbcfc053c67434f46"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a386ebe437176aab38c041de1260cd3ea459c6ce5263594399880bbc398225b2"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:ccd16eb18a849fd8dcb23e23380e2f0a354e8daa0c984b8a732d9cfaba3a776d"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:e6a5bf2cba5ae1bb80b154ed68a3cfa2fa00fde979a7f50d6598d3e17d9ac20c"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:45de3f87179c1823e6d9e32156fb14c1927fcc9aba21433f088fdfb555b77c10"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-win32.whl", hash = "sha256:1000fba1057b92a65daec275aec30586c3de2401ccdcd41f8a5c1e2c87078706"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:8b2c760cfc7042b27ebdb4a43a4453bd829a5742503599144d54a032c5dc7e9e"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:855eafa5d5a2034b4621c74925d89c5efef61418570e5ef9b37717d9c796419c"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:203f0c8871d5a7987be20c72442488a0b8cfd0f43b7973771640fc593f56321f"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e857a2232ba53ae940d3456f7533ce6ca98b81917d47adc3c7fd55dad8fab858"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5e86d77b090dbddbe78867a0275cb4df08ea195e660f1f7f13435a4649e954e5"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c4fb39a81950ec280984b3a44f5bd12819953dc5fa3a7e6fa7a80db5ee853952"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2dee8e57f052ef5353cf608e0b4c871aee320dd1b87d351c28764fc0ca55f9f4"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8700f06d0ce6f128de3ccdbc1acaea1ee264d2caa9ca05daaf492fde7c2a7200"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1920d4ff15ce893210c1f0c0e9d19bfbecb7983c76b33f046c13a8ffbd570252"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:c1c76a1743432b4b60ab3358c937a3fe1341c828ae6194108a94c69028247f22"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f7560358a6811e52e9c4d142d497f1a6e10103d3a6881f18d04dbce3729c0e2c"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:c8063cf17b19661471ecbdb3df1c84f24ad2e389e326ccaf89e3fb2484d8dd7e"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:cd6dbe0238f7743d0efe563ab46294f54f9bc8f4b9bcf57c3c666cc5bc9d1299"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:1249cbbf3d3b04902ff081ffbb33ce3377fa6e4c7356f759f3cd076cc138d020"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-win32.whl", hash = "sha256:6c409c0deba34f147f77efaa67b8e4bb83d2f11c8806405f76397ae5b8c0d1c9"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:7095f6fbfaa55defb6b733cfeb14efaae7a29f0b59d8cf213be4e7ca0b857b80"}, - {file = "charset_normalizer-3.2.0-py3-none-any.whl", hash = "sha256:8e098148dd37b4ce3baca71fb394c81dc5d9c7728c95df695d2dca218edf40e6"}, -] - -[[package]] -name = "colorama" -version = "0.4.6" -description = "Cross-platform colored terminal text." -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" -files = [ - {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, - {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, -] - -[[package]] -name = "contourpy" -version = "1.1.0" -description = "Python library for calculating contours of 2D quadrilateral grids" -optional = false -python-versions = ">=3.8" -files = [ - {file = "contourpy-1.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:89f06eff3ce2f4b3eb24c1055a26981bffe4e7264acd86f15b97e40530b794bc"}, - {file = "contourpy-1.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dffcc2ddec1782dd2f2ce1ef16f070861af4fb78c69862ce0aab801495dda6a3"}, - {file = "contourpy-1.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25ae46595e22f93592d39a7eac3d638cda552c3e1160255258b695f7b58e5655"}, - {file = "contourpy-1.1.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:17cfaf5ec9862bc93af1ec1f302457371c34e688fbd381f4035a06cd47324f48"}, - {file = "contourpy-1.1.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:18a64814ae7bce73925131381603fff0116e2df25230dfc80d6d690aa6e20b37"}, - {file = "contourpy-1.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90c81f22b4f572f8a2110b0b741bb64e5a6427e0a198b2cdc1fbaf85f352a3aa"}, - {file = "contourpy-1.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:53cc3a40635abedbec7f1bde60f8c189c49e84ac180c665f2cd7c162cc454baa"}, - {file = "contourpy-1.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:1f795597073b09d631782e7245016a4323cf1cf0b4e06eef7ea6627e06a37ff2"}, - {file = "contourpy-1.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0b7b04ed0961647691cfe5d82115dd072af7ce8846d31a5fac6c142dcce8b882"}, - {file = "contourpy-1.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:27bc79200c742f9746d7dd51a734ee326a292d77e7d94c8af6e08d1e6c15d545"}, - {file = "contourpy-1.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:052cc634bf903c604ef1a00a5aa093c54f81a2612faedaa43295809ffdde885e"}, - {file = "contourpy-1.1.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9382a1c0bc46230fb881c36229bfa23d8c303b889b788b939365578d762b5c18"}, - {file = "contourpy-1.1.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5cec36c5090e75a9ac9dbd0ff4a8cf7cecd60f1b6dc23a374c7d980a1cd710e"}, - {file = "contourpy-1.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f0cbd657e9bde94cd0e33aa7df94fb73c1ab7799378d3b3f902eb8eb2e04a3a"}, - {file = "contourpy-1.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:181cbace49874f4358e2929aaf7ba84006acb76694102e88dd15af861996c16e"}, - {file = "contourpy-1.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:fb3b7d9e6243bfa1efb93ccfe64ec610d85cfe5aec2c25f97fbbd2e58b531256"}, - {file = "contourpy-1.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:bcb41692aa09aeb19c7c213411854402f29f6613845ad2453d30bf421fe68fed"}, - {file = "contourpy-1.1.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5d123a5bc63cd34c27ff9c7ac1cd978909e9c71da12e05be0231c608048bb2ae"}, - {file = "contourpy-1.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62013a2cf68abc80dadfd2307299bfa8f5aa0dcaec5b2954caeb5fa094171103"}, - {file = "contourpy-1.1.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0b6616375d7de55797d7a66ee7d087efe27f03d336c27cf1f32c02b8c1a5ac70"}, - {file = "contourpy-1.1.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:317267d915490d1e84577924bd61ba71bf8681a30e0d6c545f577363157e5e94"}, - {file = "contourpy-1.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d551f3a442655f3dcc1285723f9acd646ca5858834efeab4598d706206b09c9f"}, - {file = "contourpy-1.1.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:e7a117ce7df5a938fe035cad481b0189049e8d92433b4b33aa7fc609344aafa1"}, - {file = "contourpy-1.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:d4f26b25b4f86087e7d75e63212756c38546e70f2a92d2be44f80114826e1cd4"}, - {file = "contourpy-1.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:bc00bb4225d57bff7ebb634646c0ee2a1298402ec10a5fe7af79df9a51c1bfd9"}, - {file = "contourpy-1.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:189ceb1525eb0655ab8487a9a9c41f42a73ba52d6789754788d1883fb06b2d8a"}, - {file = "contourpy-1.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9f2931ed4741f98f74b410b16e5213f71dcccee67518970c42f64153ea9313b9"}, - {file = "contourpy-1.1.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:30f511c05fab7f12e0b1b7730ebdc2ec8deedcfb505bc27eb570ff47c51a8f15"}, - {file = "contourpy-1.1.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:143dde50520a9f90e4a2703f367cf8ec96a73042b72e68fcd184e1279962eb6f"}, - {file = "contourpy-1.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e94bef2580e25b5fdb183bf98a2faa2adc5b638736b2c0a4da98691da641316a"}, - {file = "contourpy-1.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ed614aea8462735e7d70141374bd7650afd1c3f3cb0c2dbbcbe44e14331bf002"}, - {file = "contourpy-1.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:438ba416d02f82b692e371858143970ed2eb6337d9cdbbede0d8ad9f3d7dd17d"}, - {file = "contourpy-1.1.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a698c6a7a432789e587168573a864a7ea374c6be8d4f31f9d87c001d5a843493"}, - {file = "contourpy-1.1.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:397b0ac8a12880412da3551a8cb5a187d3298a72802b45a3bd1805e204ad8439"}, - {file = "contourpy-1.1.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:a67259c2b493b00e5a4d0f7bfae51fb4b3371395e47d079a4446e9b0f4d70e76"}, - {file = "contourpy-1.1.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:2b836d22bd2c7bb2700348e4521b25e077255ebb6ab68e351ab5aa91ca27e027"}, - {file = "contourpy-1.1.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:084eaa568400cfaf7179b847ac871582199b1b44d5699198e9602ecbbb5f6104"}, - {file = "contourpy-1.1.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:911ff4fd53e26b019f898f32db0d4956c9d227d51338fb3b03ec72ff0084ee5f"}, - {file = "contourpy-1.1.0.tar.gz", hash = "sha256:e53046c3863828d21d531cc3b53786e6580eb1ba02477e8681009b6aa0870b21"}, -] - -[package.dependencies] -numpy = ">=1.16" - -[package.extras] -bokeh = ["bokeh", "selenium"] -docs = ["furo", "sphinx-copybutton"] -mypy = ["contourpy[bokeh,docs]", "docutils-stubs", "mypy (==1.2.0)", "types-Pillow"] -test = ["Pillow", "contourpy[test-no-images]", "matplotlib"] -test-no-images = ["pytest", "pytest-cov", "wurlitzer"] - -[[package]] -name = "coverage" -version = "7.2.7" -description = "Code coverage measurement for Python" -optional = false -python-versions = ">=3.7" -files = [ - {file = "coverage-7.2.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d39b5b4f2a66ccae8b7263ac3c8170994b65266797fb96cbbfd3fb5b23921db8"}, - {file = "coverage-7.2.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6d040ef7c9859bb11dfeb056ff5b3872436e3b5e401817d87a31e1750b9ae2fb"}, - {file = "coverage-7.2.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba90a9563ba44a72fda2e85302c3abc71c5589cea608ca16c22b9804262aaeb6"}, - {file = "coverage-7.2.7-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7d9405291c6928619403db1d10bd07888888ec1abcbd9748fdaa971d7d661b2"}, - {file = "coverage-7.2.7-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31563e97dae5598556600466ad9beea39fb04e0229e61c12eaa206e0aa202063"}, - {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:ebba1cd308ef115925421d3e6a586e655ca5a77b5bf41e02eb0e4562a111f2d1"}, - {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:cb017fd1b2603ef59e374ba2063f593abe0fc45f2ad9abdde5b4d83bd922a353"}, - {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d62a5c7dad11015c66fbb9d881bc4caa5b12f16292f857842d9d1871595f4495"}, - {file = "coverage-7.2.7-cp310-cp310-win32.whl", hash = "sha256:ee57190f24fba796e36bb6d3aa8a8783c643d8fa9760c89f7a98ab5455fbf818"}, - {file = "coverage-7.2.7-cp310-cp310-win_amd64.whl", hash = "sha256:f75f7168ab25dd93110c8a8117a22450c19976afbc44234cbf71481094c1b850"}, - {file = "coverage-7.2.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:06a9a2be0b5b576c3f18f1a241f0473575c4a26021b52b2a85263a00f034d51f"}, - {file = "coverage-7.2.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5baa06420f837184130752b7c5ea0808762083bf3487b5038d68b012e5937dbe"}, - {file = "coverage-7.2.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdec9e8cbf13a5bf63290fc6013d216a4c7232efb51548594ca3631a7f13c3a3"}, - {file = "coverage-7.2.7-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:52edc1a60c0d34afa421c9c37078817b2e67a392cab17d97283b64c5833f427f"}, - {file = "coverage-7.2.7-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63426706118b7f5cf6bb6c895dc215d8a418d5952544042c8a2d9fe87fcf09cb"}, - {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:afb17f84d56068a7c29f5fa37bfd38d5aba69e3304af08ee94da8ed5b0865833"}, - {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:48c19d2159d433ccc99e729ceae7d5293fbffa0bdb94952d3579983d1c8c9d97"}, - {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0e1f928eaf5469c11e886fe0885ad2bf1ec606434e79842a879277895a50942a"}, - {file = "coverage-7.2.7-cp311-cp311-win32.whl", hash = "sha256:33d6d3ea29d5b3a1a632b3c4e4f4ecae24ef170b0b9ee493883f2df10039959a"}, - {file = "coverage-7.2.7-cp311-cp311-win_amd64.whl", hash = "sha256:5b7540161790b2f28143191f5f8ec02fb132660ff175b7747b95dcb77ac26562"}, - {file = "coverage-7.2.7-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f2f67fe12b22cd130d34d0ef79206061bfb5eda52feb6ce0dba0644e20a03cf4"}, - {file = "coverage-7.2.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a342242fe22407f3c17f4b499276a02b01e80f861f1682ad1d95b04018e0c0d4"}, - {file = "coverage-7.2.7-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:171717c7cb6b453aebac9a2ef603699da237f341b38eebfee9be75d27dc38e01"}, - {file = "coverage-7.2.7-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49969a9f7ffa086d973d91cec8d2e31080436ef0fb4a359cae927e742abfaaa6"}, - {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b46517c02ccd08092f4fa99f24c3b83d8f92f739b4657b0f146246a0ca6a831d"}, - {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:a3d33a6b3eae87ceaefa91ffdc130b5e8536182cd6dfdbfc1aa56b46ff8c86de"}, - {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:976b9c42fb2a43ebf304fa7d4a310e5f16cc99992f33eced91ef6f908bd8f33d"}, - {file = "coverage-7.2.7-cp312-cp312-win32.whl", hash = "sha256:8de8bb0e5ad103888d65abef8bca41ab93721647590a3f740100cd65c3b00511"}, - {file = "coverage-7.2.7-cp312-cp312-win_amd64.whl", hash = "sha256:9e31cb64d7de6b6f09702bb27c02d1904b3aebfca610c12772452c4e6c21a0d3"}, - {file = "coverage-7.2.7-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:58c2ccc2f00ecb51253cbe5d8d7122a34590fac9646a960d1430d5b15321d95f"}, - {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d22656368f0e6189e24722214ed8d66b8022db19d182927b9a248a2a8a2f67eb"}, - {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a895fcc7b15c3fc72beb43cdcbdf0ddb7d2ebc959edac9cef390b0d14f39f8a9"}, - {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e84606b74eb7de6ff581a7915e2dab7a28a0517fbe1c9239eb227e1354064dcd"}, - {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:0a5f9e1dbd7fbe30196578ca36f3fba75376fb99888c395c5880b355e2875f8a"}, - {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:419bfd2caae268623dd469eff96d510a920c90928b60f2073d79f8fe2bbc5959"}, - {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:2aee274c46590717f38ae5e4650988d1af340fe06167546cc32fe2f58ed05b02"}, - {file = "coverage-7.2.7-cp37-cp37m-win32.whl", hash = "sha256:61b9a528fb348373c433e8966535074b802c7a5d7f23c4f421e6c6e2f1697a6f"}, - {file = "coverage-7.2.7-cp37-cp37m-win_amd64.whl", hash = "sha256:b1c546aca0ca4d028901d825015dc8e4d56aac4b541877690eb76490f1dc8ed0"}, - {file = "coverage-7.2.7-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:54b896376ab563bd38453cecb813c295cf347cf5906e8b41d340b0321a5433e5"}, - {file = "coverage-7.2.7-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3d376df58cc111dc8e21e3b6e24606b5bb5dee6024f46a5abca99124b2229ef5"}, - {file = "coverage-7.2.7-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5e330fc79bd7207e46c7d7fd2bb4af2963f5f635703925543a70b99574b0fea9"}, - {file = "coverage-7.2.7-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e9d683426464e4a252bf70c3498756055016f99ddaec3774bf368e76bbe02b6"}, - {file = "coverage-7.2.7-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d13c64ee2d33eccf7437961b6ea7ad8673e2be040b4f7fd4fd4d4d28d9ccb1e"}, - {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b7aa5f8a41217360e600da646004f878250a0d6738bcdc11a0a39928d7dc2050"}, - {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8fa03bce9bfbeeef9f3b160a8bed39a221d82308b4152b27d82d8daa7041fee5"}, - {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:245167dd26180ab4c91d5e1496a30be4cd721a5cf2abf52974f965f10f11419f"}, - {file = "coverage-7.2.7-cp38-cp38-win32.whl", hash = "sha256:d2c2db7fd82e9b72937969bceac4d6ca89660db0a0967614ce2481e81a0b771e"}, - {file = "coverage-7.2.7-cp38-cp38-win_amd64.whl", hash = "sha256:2e07b54284e381531c87f785f613b833569c14ecacdcb85d56b25c4622c16c3c"}, - {file = "coverage-7.2.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:537891ae8ce59ef63d0123f7ac9e2ae0fc8b72c7ccbe5296fec45fd68967b6c9"}, - {file = "coverage-7.2.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:06fb182e69f33f6cd1d39a6c597294cff3143554b64b9825d1dc69d18cc2fff2"}, - {file = "coverage-7.2.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:201e7389591af40950a6480bd9edfa8ed04346ff80002cec1a66cac4549c1ad7"}, - {file = "coverage-7.2.7-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f6951407391b639504e3b3be51b7ba5f3528adbf1a8ac3302b687ecababf929e"}, - {file = "coverage-7.2.7-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f48351d66575f535669306aa7d6d6f71bc43372473b54a832222803eb956fd1"}, - {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b29019c76039dc3c0fd815c41392a044ce555d9bcdd38b0fb60fb4cd8e475ba9"}, - {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:81c13a1fc7468c40f13420732805a4c38a105d89848b7c10af65a90beff25250"}, - {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:975d70ab7e3c80a3fe86001d8751f6778905ec723f5b110aed1e450da9d4b7f2"}, - {file = "coverage-7.2.7-cp39-cp39-win32.whl", hash = "sha256:7ee7d9d4822c8acc74a5e26c50604dff824710bc8de424904c0982e25c39c6cb"}, - {file = "coverage-7.2.7-cp39-cp39-win_amd64.whl", hash = "sha256:eb393e5ebc85245347950143969b241d08b52b88a3dc39479822e073a1a8eb27"}, - {file = "coverage-7.2.7-pp37.pp38.pp39-none-any.whl", hash = "sha256:b7b4c971f05e6ae490fef852c218b0e79d4e52f79ef0c8475566584a8fb3e01d"}, - {file = "coverage-7.2.7.tar.gz", hash = "sha256:924d94291ca674905fe9481f12294eb11f2d3d3fd1adb20314ba89e94f44ed59"}, -] - -[package.dependencies] -tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} - -[package.extras] -toml = ["tomli"] - -[[package]] -name = "cycler" -version = "0.11.0" -description = "Composable style cycles" -optional = false -python-versions = ">=3.6" -files = [ - {file = "cycler-0.11.0-py3-none-any.whl", hash = "sha256:3a27e95f763a428a739d2add979fa7494c912a32c17c4c38c4d5f082cad165a3"}, - {file = "cycler-0.11.0.tar.gz", hash = "sha256:9c87405839a19696e837b3b818fed3f5f69f16f1eec1a1ad77e043dcea9c772f"}, -] - -[[package]] -name = "deprecated" -version = "1.2.14" -description = "Python @deprecated decorator to deprecate old python classes, functions or methods." -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -files = [ - {file = "Deprecated-1.2.14-py2.py3-none-any.whl", hash = "sha256:6fac8b097794a90302bdbb17b9b815e732d3c4720583ff1b198499d78470466c"}, - {file = "Deprecated-1.2.14.tar.gz", hash = "sha256:e5323eb936458dccc2582dc6f9c322c852a775a27065ff2b0c4970b9d53d01b3"}, -] - -[package.dependencies] -wrapt = ">=1.10,<2" - -[package.extras] -dev = ["PyTest", "PyTest-Cov", "bump2version (<1)", "sphinx (<2)", "tox"] - -[[package]] -name = "distlib" -version = "0.3.7" -description = "Distribution utilities" -optional = false -python-versions = "*" -files = [ - {file = "distlib-0.3.7-py2.py3-none-any.whl", hash = "sha256:2e24928bc811348f0feb63014e97aaae3037f2cf48712d51ae61df7fd6075057"}, - {file = "distlib-0.3.7.tar.gz", hash = "sha256:9dafe54b34a028eafd95039d5e5d4851a13734540f1331060d31c9916e7147a8"}, -] - -[[package]] -name = "docutils" -version = "0.18.1" -description = "Docutils -- Python Documentation Utilities" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -files = [ - {file = "docutils-0.18.1-py2.py3-none-any.whl", hash = "sha256:23010f129180089fbcd3bc08cfefccb3b890b0050e1ca00c867036e9d161b98c"}, - {file = "docutils-0.18.1.tar.gz", hash = "sha256:679987caf361a7539d76e584cbeddc311e3aee937877c87346f31debc63e9d06"}, -] - -[[package]] -name = "exceptiongroup" -version = "1.1.2" -description = "Backport of PEP 654 (exception groups)" -optional = false -python-versions = ">=3.7" -files = [ - {file = "exceptiongroup-1.1.2-py3-none-any.whl", hash = "sha256:e346e69d186172ca7cf029c8c1d16235aa0e04035e5750b4b95039e65204328f"}, - {file = "exceptiongroup-1.1.2.tar.gz", hash = "sha256:12c3e887d6485d16943a309616de20ae5582633e0a2eda17f4e10fd61c1e8af5"}, -] - -[package.extras] -test = ["pytest (>=6)"] - -[[package]] -name = "filelock" -version = "3.12.2" -description = "A platform independent file lock." -optional = false -python-versions = ">=3.7" -files = [ - {file = "filelock-3.12.2-py3-none-any.whl", hash = "sha256:cbb791cdea2a72f23da6ac5b5269ab0a0d161e9ef0100e653b69049a7706d1ec"}, - {file = "filelock-3.12.2.tar.gz", hash = "sha256:002740518d8aa59a26b0c76e10fb8c6e15eae825d34b6fdf670333fd7b938d81"}, -] - -[package.extras] -docs = ["furo (>=2023.5.20)", "sphinx (>=7.0.1)", "sphinx-autodoc-typehints (>=1.23,!=1.23.4)"] -testing = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "diff-cover (>=7.5)", "pytest (>=7.3.1)", "pytest-cov (>=4.1)", "pytest-mock (>=3.10)", "pytest-timeout (>=2.1)"] - -[[package]] -name = "fonttools" -version = "4.41.1" -description = "Tools to manipulate font files" -optional = false -python-versions = ">=3.8" -files = [ - {file = "fonttools-4.41.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a7bbb290d13c6dd718ec2c3db46fe6c5f6811e7ea1e07f145fd8468176398224"}, - {file = "fonttools-4.41.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ec453a45778524f925a8f20fd26a3326f398bfc55d534e37bab470c5e415caa1"}, - {file = "fonttools-4.41.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c2071267deaa6d93cb16288613419679c77220543551cbe61da02c93d92df72f"}, - {file = "fonttools-4.41.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e3334d51f0e37e2c6056e67141b2adabc92613a968797e2571ca8a03bd64773"}, - {file = "fonttools-4.41.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:cac73bbef7734e78c60949da11c4903ee5837168e58772371bd42a75872f4f82"}, - {file = "fonttools-4.41.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:edee0900cf0eedb29d17c7876102d6e5a91ee333882b1f5abc83e85b934cadb5"}, - {file = "fonttools-4.41.1-cp310-cp310-win32.whl", hash = "sha256:2a22b2c425c698dcd5d6b0ff0b566e8e9663172118db6fd5f1941f9b8063da9b"}, - {file = "fonttools-4.41.1-cp310-cp310-win_amd64.whl", hash = "sha256:547ab36a799dded58a46fa647266c24d0ed43a66028cd1cd4370b246ad426cac"}, - {file = "fonttools-4.41.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:849ec722bbf7d3501a0e879e57dec1fc54919d31bff3f690af30bb87970f9784"}, - {file = "fonttools-4.41.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:38cdecd8f1fd4bf4daae7fed1b3170dfc1b523388d6664b2204b351820aa78a7"}, - {file = "fonttools-4.41.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ae64303ba670f8959fdaaa30ba0c2dabe75364fdec1caeee596c45d51ca3425"}, - {file = "fonttools-4.41.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f14f3ccea4cc7dd1b277385adf3c3bf18f9860f87eab9c2fb650b0af16800f55"}, - {file = "fonttools-4.41.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:33191f062549e6bb1a4782c22a04ebd37009c09360e2d6686ac5083774d06d95"}, - {file = "fonttools-4.41.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:704bccd69b0abb6fab9f5e4d2b75896afa48b427caa2c7988792a2ffce35b441"}, - {file = "fonttools-4.41.1-cp311-cp311-win32.whl", hash = "sha256:4edc795533421e98f60acee7d28fc8d941ff5ac10f44668c9c3635ad72ae9045"}, - {file = "fonttools-4.41.1-cp311-cp311-win_amd64.whl", hash = "sha256:aaaef294d8e411f0ecb778a0aefd11bb5884c9b8333cc1011bdaf3b58ca4bd75"}, - {file = "fonttools-4.41.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:3d1f9471134affc1e3b1b806db6e3e2ad3fa99439e332f1881a474c825101096"}, - {file = "fonttools-4.41.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:59eba8b2e749a1de85760da22333f3d17c42b66e03758855a12a2a542723c6e7"}, - {file = "fonttools-4.41.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9b3cc10dc9e0834b6665fd63ae0c6964c6bc3d7166e9bc84772e0edd09f9fa2"}, - {file = "fonttools-4.41.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:da2c2964bdc827ba6b8a91dc6de792620be4da3922c4cf0599f36a488c07e2b2"}, - {file = "fonttools-4.41.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:7763316111df7b5165529f4183a334aa24c13cdb5375ffa1dc8ce309c8bf4e5c"}, - {file = "fonttools-4.41.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b2d1ee95be42b80d1f002d1ee0a51d7a435ea90d36f1a5ae331be9962ee5a3f1"}, - {file = "fonttools-4.41.1-cp38-cp38-win32.whl", hash = "sha256:f48602c0b3fd79cd83a34c40af565fe6db7ac9085c8823b552e6e751e3a5b8be"}, - {file = "fonttools-4.41.1-cp38-cp38-win_amd64.whl", hash = "sha256:b0938ebbeccf7c80bb9a15e31645cf831572c3a33d5cc69abe436e7000c61b14"}, - {file = "fonttools-4.41.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e5c2b0a95a221838991e2f0e455dec1ca3a8cc9cd54febd68cc64d40fdb83669"}, - {file = "fonttools-4.41.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:891cfc5a83b0307688f78b9bb446f03a7a1ad981690ac8362f50518bc6153975"}, - {file = "fonttools-4.41.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:73ef0bb5d60eb02ba4d3a7d23ada32184bd86007cb2de3657cfcb1175325fc83"}, - {file = "fonttools-4.41.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f240d9adf0583ac8fc1646afe7f4ac039022b6f8fa4f1575a2cfa53675360b69"}, - {file = "fonttools-4.41.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bdd729744ae7ecd7f7311ad25d99da4999003dcfe43b436cf3c333d4e68de73d"}, - {file = "fonttools-4.41.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:b927e5f466d99c03e6e20961946314b81d6e3490d95865ef88061144d9f62e38"}, - {file = "fonttools-4.41.1-cp39-cp39-win32.whl", hash = "sha256:afce2aeb80be72b4da7dd114f10f04873ff512793d13ce0b19d12b2a4c44c0f0"}, - {file = "fonttools-4.41.1-cp39-cp39-win_amd64.whl", hash = "sha256:1df1b6f4c7c4bc8201eb47f3b268adbf2539943aa43c400f84556557e3e109c0"}, - {file = "fonttools-4.41.1-py3-none-any.whl", hash = "sha256:952cb405f78734cf6466252fec42e206450d1a6715746013f64df9cbd4f896fa"}, - {file = "fonttools-4.41.1.tar.gz", hash = "sha256:e16a9449f21a93909c5be2f5ed5246420f2316e94195dbfccb5238aaa38f9751"}, -] - -[package.extras] -all = ["brotli (>=1.0.1)", "brotlicffi (>=0.8.0)", "fs (>=2.2.0,<3)", "lxml (>=4.0,<5)", "lz4 (>=1.7.4.2)", "matplotlib", "munkres", "scipy", "skia-pathops (>=0.5.0)", "sympy", "uharfbuzz (>=0.23.0)", "unicodedata2 (>=15.0.0)", "xattr", "zopfli (>=0.1.4)"] -graphite = ["lz4 (>=1.7.4.2)"] -interpolatable = ["munkres", "scipy"] -lxml = ["lxml (>=4.0,<5)"] -pathops = ["skia-pathops (>=0.5.0)"] -plot = ["matplotlib"] -repacker = ["uharfbuzz (>=0.23.0)"] -symfont = ["sympy"] -type1 = ["xattr"] -ufo = ["fs (>=2.2.0,<3)"] -unicode = ["unicodedata2 (>=15.0.0)"] -woff = ["brotli (>=1.0.1)", "brotlicffi (>=0.8.0)", "zopfli (>=0.1.4)"] - -[[package]] -name = "h5py" -version = "3.9.0" -description = "Read and write HDF5 files from Python" -optional = false -python-versions = ">=3.8" -files = [ - {file = "h5py-3.9.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:eb7bdd5e601dd1739698af383be03f3dad0465fe67184ebd5afca770f50df9d6"}, - {file = "h5py-3.9.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:78e44686334cbbf2dd21d9df15823bc38663f27a3061f6a032c68a3e30c47bf7"}, - {file = "h5py-3.9.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f68b41efd110ce9af1cbe6fa8af9f4dcbadace6db972d30828b911949e28fadd"}, - {file = "h5py-3.9.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:12aa556d540f11a2cae53ea7cfb94017353bd271fb3962e1296b342f6550d1b8"}, - {file = "h5py-3.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:d97409e17915798029e297a84124705c8080da901307ea58f29234e09b073ddc"}, - {file = "h5py-3.9.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:551e358db05a874a0f827b22e95b30092f2303edc4b91bb62ad2f10e0236e1a0"}, - {file = "h5py-3.9.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6822a814b9d8b8363ff102f76ea8d026f0ca25850bb579d85376029ee3e73b93"}, - {file = "h5py-3.9.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54f01202cdea754ab4227dd27014bdbd561a4bbe4b631424fd812f7c2ce9c6ac"}, - {file = "h5py-3.9.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64acceaf6aff92af091a4b83f6dee3cf8d3061f924a6bb3a33eb6c4658a8348b"}, - {file = "h5py-3.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:804c7fb42a34c8ab3a3001901c977a5c24d2e9c586a0f3e7c0a389130b4276fc"}, - {file = "h5py-3.9.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8d9492391ff5c3c80ec30ae2fe82a3f0efd1e750833739c25b0d090e3be1b095"}, - {file = "h5py-3.9.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:9da9e7e63376c32704e37ad4cea2dceae6964cee0d8515185b3ab9cbd6b947bc"}, - {file = "h5py-3.9.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a4e20897c88759cbcbd38fb45b507adc91af3e0f67722aa302d71f02dd44d286"}, - {file = "h5py-3.9.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dbf5225543ca35ce9f61c950b73899a82be7ba60d58340e76d0bd42bf659235a"}, - {file = "h5py-3.9.0-cp38-cp38-win_amd64.whl", hash = "sha256:36408f8c62f50007d14e000f9f3acf77e103b9e932c114cbe52a3089e50ebf94"}, - {file = "h5py-3.9.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:23e74b878bbe1653ab34ca49b83cac85529cd0b36b9d625516c5830cc5ca2eac"}, - {file = "h5py-3.9.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3f457089c5d524b7998e3649bc63240679b8fb0a3859ea53bbb06841f3d755f1"}, - {file = "h5py-3.9.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a6284061f3214335e1eec883a6ee497dbe7a79f19e6a57fed2dd1f03acd5a8cb"}, - {file = "h5py-3.9.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95f7a745efd0d56076999b52e8da5fad5d30823bac98b59c68ae75588d09991a"}, - {file = "h5py-3.9.0-cp39-cp39-win_amd64.whl", hash = "sha256:79bbca34696c6f9eeeb36a91776070c49a060b2879828e2c8fa6c58b8ed10dd1"}, - {file = "h5py-3.9.0.tar.gz", hash = "sha256:e604db6521c1e367c6bd7fad239c847f53cc46646f2d2651372d05ae5e95f817"}, -] - -[package.dependencies] -numpy = ">=1.17.3" - -[[package]] -name = "identify" -version = "2.5.26" -description = "File identification library for Python" -optional = false -python-versions = ">=3.8" -files = [ - {file = "identify-2.5.26-py2.py3-none-any.whl", hash = "sha256:c22a8ead0d4ca11f1edd6c9418c3220669b3b7533ada0a0ffa6cc0ef85cf9b54"}, - {file = "identify-2.5.26.tar.gz", hash = "sha256:7243800bce2f58404ed41b7c002e53d4d22bcf3ae1b7900c2d7aefd95394bf7f"}, -] - -[package.extras] -license = ["ukkonen"] - -[[package]] -name = "idna" -version = "3.4" -description = "Internationalized Domain Names in Applications (IDNA)" -optional = false -python-versions = ">=3.5" -files = [ - {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, - {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, -] - -[[package]] -name = "imageio" -version = "2.31.1" -description = "Library for reading and writing a wide range of image, video, scientific, and volumetric data formats." -optional = false -python-versions = ">=3.7" -files = [ - {file = "imageio-2.31.1-py3-none-any.whl", hash = "sha256:4106fb395ef7f8dc0262d6aa1bb03daba818445c381ca8b7d5dfc7a2089b04df"}, - {file = "imageio-2.31.1.tar.gz", hash = "sha256:f8436a02af02fd63f272dab50f7d623547a38f0e04a4a73e2b02ae1b8b180f27"}, -] - -[package.dependencies] -numpy = "*" -pillow = ">=8.3.2" - -[package.extras] -all-plugins = ["astropy", "av", "imageio-ffmpeg", "psutil", "tifffile"] -all-plugins-pypy = ["av", "imageio-ffmpeg", "psutil", "tifffile"] -build = ["wheel"] -dev = ["black", "flake8", "fsspec[github]", "pytest", "pytest-cov"] -docs = ["numpydoc", "pydata-sphinx-theme", "sphinx (<6)"] -ffmpeg = ["imageio-ffmpeg", "psutil"] -fits = ["astropy"] -full = ["astropy", "av", "black", "flake8", "fsspec[github]", "gdal", "imageio-ffmpeg", "itk", "numpydoc", "psutil", "pydata-sphinx-theme", "pytest", "pytest-cov", "sphinx (<6)", "tifffile", "wheel"] -gdal = ["gdal"] -itk = ["itk"] -linting = ["black", "flake8"] -pyav = ["av"] -test = ["fsspec[github]", "pytest", "pytest-cov"] -tifffile = ["tifffile"] - -[[package]] -name = "imagesize" -version = "1.4.1" -description = "Getting image size from png/jpeg/jpeg2000/gif file" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -files = [ - {file = "imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b"}, - {file = "imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a"}, -] - -[[package]] -name = "importlib-metadata" -version = "6.8.0" -description = "Read metadata from Python packages" -optional = false -python-versions = ">=3.8" -files = [ - {file = "importlib_metadata-6.8.0-py3-none-any.whl", hash = "sha256:3ebb78df84a805d7698245025b975d9d67053cd94c79245ba4b3eb694abe68bb"}, - {file = "importlib_metadata-6.8.0.tar.gz", hash = "sha256:dbace7892d8c0c4ac1ad096662232f831d4e64f4c4545bd53016a3e9d4654743"}, -] - -[package.dependencies] -zipp = ">=0.5" - -[package.extras] -docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -perf = ["ipython"] -testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)", "pytest-ruff"] - -[[package]] -name = "importlib-resources" -version = "6.0.0" -description = "Read resources from Python packages" -optional = false -python-versions = ">=3.8" -files = [ - {file = "importlib_resources-6.0.0-py3-none-any.whl", hash = "sha256:d952faee11004c045f785bb5636e8f885bed30dc3c940d5d42798a2a4541c185"}, - {file = "importlib_resources-6.0.0.tar.gz", hash = "sha256:4cf94875a8368bd89531a756df9a9ebe1f150e0f885030b461237bc7f2d905f2"}, -] - -[package.dependencies] -zipp = {version = ">=3.1.0", markers = "python_version < \"3.10\""} - -[package.extras] -docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -testing = ["pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-ruff"] - -[[package]] -name = "iniconfig" -version = "2.0.0" -description = "brain-dead simple config-ini parsing" -optional = false -python-versions = ">=3.7" -files = [ - {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, - {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, -] - -[[package]] -name = "jdata" -version = "0.5.3" -description = "Encoding and decoding Python data structrues using portable JData-annotated formats" -optional = false -python-versions = "*" -files = [ - {file = "jdata-0.5.3-py2.py3-none-any.whl", hash = "sha256:96bc4bdf22a4aa9a59074ab0ae4f7c8999714b1bbf63c2b71dfed727534809f5"}, - {file = "jdata-0.5.3.tar.gz", hash = "sha256:b748fb0d7d7ebe63696600590e92b63affb3fe97f9c445f9c66271eaf253dd79"}, -] - -[package.dependencies] -numpy = ">=1.8.0" - -[[package]] -name = "jinja2" -version = "3.1.2" -description = "A very fast and expressive template engine." -optional = false -python-versions = ">=3.7" -files = [ - {file = "Jinja2-3.1.2-py3-none-any.whl", hash = "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"}, - {file = "Jinja2-3.1.2.tar.gz", hash = "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852"}, -] - -[package.dependencies] -MarkupSafe = ">=2.0" - -[package.extras] -i18n = ["Babel (>=2.7)"] - -[[package]] -name = "kiwisolver" -version = "1.4.4" -description = "A fast implementation of the Cassowary constraint solver" -optional = false -python-versions = ">=3.7" -files = [ - {file = "kiwisolver-1.4.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:2f5e60fabb7343a836360c4f0919b8cd0d6dbf08ad2ca6b9cf90bf0c76a3c4f6"}, - {file = "kiwisolver-1.4.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:10ee06759482c78bdb864f4109886dff7b8a56529bc1609d4f1112b93fe6423c"}, - {file = "kiwisolver-1.4.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c79ebe8f3676a4c6630fd3f777f3cfecf9289666c84e775a67d1d358578dc2e3"}, - {file = "kiwisolver-1.4.4-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:abbe9fa13da955feb8202e215c4018f4bb57469b1b78c7a4c5c7b93001699938"}, - {file = "kiwisolver-1.4.4-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:7577c1987baa3adc4b3c62c33bd1118c3ef5c8ddef36f0f2c950ae0b199e100d"}, - {file = "kiwisolver-1.4.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f8ad8285b01b0d4695102546b342b493b3ccc6781fc28c8c6a1bb63e95d22f09"}, - {file = "kiwisolver-1.4.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8ed58b8acf29798b036d347791141767ccf65eee7f26bde03a71c944449e53de"}, - {file = "kiwisolver-1.4.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a68b62a02953b9841730db7797422f983935aeefceb1679f0fc85cbfbd311c32"}, - {file = "kiwisolver-1.4.4-cp310-cp310-win32.whl", hash = "sha256:e92a513161077b53447160b9bd8f522edfbed4bd9759e4c18ab05d7ef7e49408"}, - {file = "kiwisolver-1.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:3fe20f63c9ecee44560d0e7f116b3a747a5d7203376abeea292ab3152334d004"}, - {file = "kiwisolver-1.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e0ea21f66820452a3f5d1655f8704a60d66ba1191359b96541eaf457710a5fc6"}, - {file = "kiwisolver-1.4.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:bc9db8a3efb3e403e4ecc6cd9489ea2bac94244f80c78e27c31dcc00d2790ac2"}, - {file = "kiwisolver-1.4.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d5b61785a9ce44e5a4b880272baa7cf6c8f48a5180c3e81c59553ba0cb0821ca"}, - {file = "kiwisolver-1.4.4-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c2dbb44c3f7e6c4d3487b31037b1bdbf424d97687c1747ce4ff2895795c9bf69"}, - {file = "kiwisolver-1.4.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6295ecd49304dcf3bfbfa45d9a081c96509e95f4b9d0eb7ee4ec0530c4a96514"}, - {file = "kiwisolver-1.4.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4bd472dbe5e136f96a4b18f295d159d7f26fd399136f5b17b08c4e5f498cd494"}, - {file = "kiwisolver-1.4.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bf7d9fce9bcc4752ca4a1b80aabd38f6d19009ea5cbda0e0856983cf6d0023f5"}, - {file = "kiwisolver-1.4.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78d6601aed50c74e0ef02f4204da1816147a6d3fbdc8b3872d263338a9052c51"}, - {file = "kiwisolver-1.4.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:877272cf6b4b7e94c9614f9b10140e198d2186363728ed0f701c6eee1baec1da"}, - {file = "kiwisolver-1.4.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:db608a6757adabb32f1cfe6066e39b3706d8c3aa69bbc353a5b61edad36a5cb4"}, - {file = "kiwisolver-1.4.4-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:5853eb494c71e267912275e5586fe281444eb5e722de4e131cddf9d442615626"}, - {file = "kiwisolver-1.4.4-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:f0a1dbdb5ecbef0d34eb77e56fcb3e95bbd7e50835d9782a45df81cc46949750"}, - {file = "kiwisolver-1.4.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:283dffbf061a4ec60391d51e6155e372a1f7a4f5b15d59c8505339454f8989e4"}, - {file = "kiwisolver-1.4.4-cp311-cp311-win32.whl", hash = "sha256:d06adcfa62a4431d404c31216f0f8ac97397d799cd53800e9d3efc2fbb3cf14e"}, - {file = "kiwisolver-1.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:e7da3fec7408813a7cebc9e4ec55afed2d0fd65c4754bc376bf03498d4e92686"}, - {file = "kiwisolver-1.4.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:62ac9cc684da4cf1778d07a89bf5f81b35834cb96ca523d3a7fb32509380cbf6"}, - {file = "kiwisolver-1.4.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:41dae968a94b1ef1897cb322b39360a0812661dba7c682aa45098eb8e193dbdf"}, - {file = "kiwisolver-1.4.4-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:02f79693ec433cb4b5f51694e8477ae83b3205768a6fb48ffba60549080e295b"}, - {file = "kiwisolver-1.4.4-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d0611a0a2a518464c05ddd5a3a1a0e856ccc10e67079bb17f265ad19ab3c7597"}, - {file = "kiwisolver-1.4.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:db5283d90da4174865d520e7366801a93777201e91e79bacbac6e6927cbceede"}, - {file = "kiwisolver-1.4.4-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:1041feb4cda8708ce73bb4dcb9ce1ccf49d553bf87c3954bdfa46f0c3f77252c"}, - {file = "kiwisolver-1.4.4-cp37-cp37m-win32.whl", hash = "sha256:a553dadda40fef6bfa1456dc4be49b113aa92c2a9a9e8711e955618cd69622e3"}, - {file = "kiwisolver-1.4.4-cp37-cp37m-win_amd64.whl", hash = "sha256:03baab2d6b4a54ddbb43bba1a3a2d1627e82d205c5cf8f4c924dc49284b87166"}, - {file = "kiwisolver-1.4.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:841293b17ad704d70c578f1f0013c890e219952169ce8a24ebc063eecf775454"}, - {file = "kiwisolver-1.4.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f4f270de01dd3e129a72efad823da90cc4d6aafb64c410c9033aba70db9f1ff0"}, - {file = "kiwisolver-1.4.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f9f39e2f049db33a908319cf46624a569b36983c7c78318e9726a4cb8923b26c"}, - {file = "kiwisolver-1.4.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c97528e64cb9ebeff9701e7938653a9951922f2a38bd847787d4a8e498cc83ae"}, - {file = "kiwisolver-1.4.4-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d1573129aa0fd901076e2bfb4275a35f5b7aa60fbfb984499d661ec950320b0"}, - {file = "kiwisolver-1.4.4-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ad881edc7ccb9d65b0224f4e4d05a1e85cf62d73aab798943df6d48ab0cd79a1"}, - {file = "kiwisolver-1.4.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b428ef021242344340460fa4c9185d0b1f66fbdbfecc6c63eff4b7c29fad429d"}, - {file = "kiwisolver-1.4.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:2e407cb4bd5a13984a6c2c0fe1845e4e41e96f183e5e5cd4d77a857d9693494c"}, - {file = "kiwisolver-1.4.4-cp38-cp38-win32.whl", hash = "sha256:75facbe9606748f43428fc91a43edb46c7ff68889b91fa31f53b58894503a191"}, - {file = "kiwisolver-1.4.4-cp38-cp38-win_amd64.whl", hash = "sha256:5bce61af018b0cb2055e0e72e7d65290d822d3feee430b7b8203d8a855e78766"}, - {file = "kiwisolver-1.4.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8c808594c88a025d4e322d5bb549282c93c8e1ba71b790f539567932722d7bd8"}, - {file = "kiwisolver-1.4.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f0a71d85ecdd570ded8ac3d1c0f480842f49a40beb423bb8014539a9f32a5897"}, - {file = "kiwisolver-1.4.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b533558eae785e33e8c148a8d9921692a9fe5aa516efbdff8606e7d87b9d5824"}, - {file = "kiwisolver-1.4.4-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:efda5fc8cc1c61e4f639b8067d118e742b812c930f708e6667a5ce0d13499e29"}, - {file = "kiwisolver-1.4.4-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:7c43e1e1206cd421cd92e6b3280d4385d41d7166b3ed577ac20444b6995a445f"}, - {file = "kiwisolver-1.4.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bc8d3bd6c72b2dd9decf16ce70e20abcb3274ba01b4e1c96031e0c4067d1e7cd"}, - {file = "kiwisolver-1.4.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4ea39b0ccc4f5d803e3337dd46bcce60b702be4d86fd0b3d7531ef10fd99a1ac"}, - {file = "kiwisolver-1.4.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:968f44fdbf6dd757d12920d63b566eeb4d5b395fd2d00d29d7ef00a00582aac9"}, - {file = "kiwisolver-1.4.4-cp39-cp39-win32.whl", hash = "sha256:da7e547706e69e45d95e116e6939488d62174e033b763ab1496b4c29b76fabea"}, - {file = "kiwisolver-1.4.4-cp39-cp39-win_amd64.whl", hash = "sha256:ba59c92039ec0a66103b1d5fe588fa546373587a7d68f5c96f743c3396afc04b"}, - {file = "kiwisolver-1.4.4-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:91672bacaa030f92fc2f43b620d7b337fd9a5af28b0d6ed3f77afc43c4a64b5a"}, - {file = "kiwisolver-1.4.4-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:787518a6789009c159453da4d6b683f468ef7a65bbde796bcea803ccf191058d"}, - {file = "kiwisolver-1.4.4-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da152d8cdcab0e56e4f45eb08b9aea6455845ec83172092f09b0e077ece2cf7a"}, - {file = "kiwisolver-1.4.4-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:ecb1fa0db7bf4cff9dac752abb19505a233c7f16684c5826d1f11ebd9472b871"}, - {file = "kiwisolver-1.4.4-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:28bc5b299f48150b5f822ce68624e445040595a4ac3d59251703779836eceff9"}, - {file = "kiwisolver-1.4.4-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:81e38381b782cc7e1e46c4e14cd997ee6040768101aefc8fa3c24a4cc58e98f8"}, - {file = "kiwisolver-1.4.4-pp38-pypy38_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:2a66fdfb34e05b705620dd567f5a03f239a088d5a3f321e7b6ac3239d22aa286"}, - {file = "kiwisolver-1.4.4-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:872b8ca05c40d309ed13eb2e582cab0c5a05e81e987ab9c521bf05ad1d5cf5cb"}, - {file = "kiwisolver-1.4.4-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:70e7c2e7b750585569564e2e5ca9845acfaa5da56ac46df68414f29fea97be9f"}, - {file = "kiwisolver-1.4.4-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:9f85003f5dfa867e86d53fac6f7e6f30c045673fa27b603c397753bebadc3008"}, - {file = "kiwisolver-1.4.4-pp39-pypy39_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2e307eb9bd99801f82789b44bb45e9f541961831c7311521b13a6c85afc09767"}, - {file = "kiwisolver-1.4.4-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b1792d939ec70abe76f5054d3f36ed5656021dcad1322d1cc996d4e54165cef9"}, - {file = "kiwisolver-1.4.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6cb459eea32a4e2cf18ba5fcece2dbdf496384413bc1bae15583f19e567f3b2"}, - {file = "kiwisolver-1.4.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:36dafec3d6d6088d34e2de6b85f9d8e2324eb734162fba59d2ba9ed7a2043d5b"}, - {file = "kiwisolver-1.4.4.tar.gz", hash = "sha256:d41997519fcba4a1e46eb4a2fe31bc12f0ff957b2b81bac28db24744f333e955"}, -] - -[[package]] -name = "lazy-loader" -version = "0.3" -description = "lazy_loader" -optional = false -python-versions = ">=3.7" -files = [ - {file = "lazy_loader-0.3-py3-none-any.whl", hash = "sha256:1e9e76ee8631e264c62ce10006718e80b2cfc74340d17d1031e0f84af7478554"}, - {file = "lazy_loader-0.3.tar.gz", hash = "sha256:3b68898e34f5b2a29daaaac172c6555512d0f32074f147e2254e4a6d9d838f37"}, -] - -[package.extras] -lint = ["pre-commit (>=3.3)"] -test = ["pytest (>=7.4)", "pytest-cov (>=4.1)"] - -[[package]] -name = "markdown-it-py" -version = "2.2.0" -description = "Python port of markdown-it. Markdown parsing, done right!" -optional = false -python-versions = ">=3.7" -files = [ - {file = "markdown-it-py-2.2.0.tar.gz", hash = "sha256:7c9a5e412688bc771c67432cbfebcdd686c93ce6484913dccf06cb5a0bea35a1"}, - {file = "markdown_it_py-2.2.0-py3-none-any.whl", hash = "sha256:5a35f8d1870171d9acc47b99612dc146129b631baf04970128b568f190d0cc30"}, -] - -[package.dependencies] -mdurl = ">=0.1,<1.0" - -[package.extras] -benchmarking = ["psutil", "pytest", "pytest-benchmark"] -code-style = ["pre-commit (>=3.0,<4.0)"] -compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "mistletoe (>=1.0,<2.0)", "mistune (>=2.0,<3.0)", "panflute (>=2.3,<3.0)"] -linkify = ["linkify-it-py (>=1,<3)"] -plugins = ["mdit-py-plugins"] -profiling = ["gprof2dot"] -rtd = ["attrs", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"] -testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] - -[[package]] -name = "markupsafe" -version = "2.1.3" -description = "Safely add untrusted strings to HTML/XML markup." -optional = false -python-versions = ">=3.7" -files = [ - {file = "MarkupSafe-2.1.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cd0f502fe016460680cd20aaa5a76d241d6f35a1c3350c474bac1273803893fa"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e09031c87a1e51556fdcb46e5bd4f59dfb743061cf93c4d6831bf894f125eb57"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68e78619a61ecf91e76aa3e6e8e33fc4894a2bebe93410754bd28fce0a8a4f9f"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65c1a9bcdadc6c28eecee2c119465aebff8f7a584dd719facdd9e825ec61ab52"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:525808b8019e36eb524b8c68acdd63a37e75714eac50e988180b169d64480a00"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:962f82a3086483f5e5f64dbad880d31038b698494799b097bc59c2edf392fce6"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:aa7bd130efab1c280bed0f45501b7c8795f9fdbeb02e965371bbef3523627779"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c9c804664ebe8f83a211cace637506669e7890fec1b4195b505c214e50dd4eb7"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-win32.whl", hash = "sha256:10bbfe99883db80bdbaff2dcf681dfc6533a614f700da1287707e8a5d78a8431"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-win_amd64.whl", hash = "sha256:1577735524cdad32f9f694208aa75e422adba74f1baee7551620e43a3141f559"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ad9e82fb8f09ade1c3e1b996a6337afac2b8b9e365f926f5a61aacc71adc5b3c"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3c0fae6c3be832a0a0473ac912810b2877c8cb9d76ca48de1ed31e1c68386575"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b076b6226fb84157e3f7c971a47ff3a679d837cf338547532ab866c57930dbee"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bfce63a9e7834b12b87c64d6b155fdd9b3b96191b6bd334bf37db7ff1fe457f2"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:338ae27d6b8745585f87218a3f23f1512dbf52c26c28e322dbe54bcede54ccb9"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e4dd52d80b8c83fdce44e12478ad2e85c64ea965e75d66dbeafb0a3e77308fcc"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:df0be2b576a7abbf737b1575f048c23fb1d769f267ec4358296f31c2479db8f9"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-win32.whl", hash = "sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-win_amd64.whl", hash = "sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686"}, - {file = "MarkupSafe-2.1.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2"}, - {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b"}, - {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707"}, - {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca379055a47383d02a5400cb0d110cef0a776fc644cda797db0c5696cfd7e18e"}, - {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:b7ff0f54cb4ff66dd38bebd335a38e2c22c41a8ee45aa608efc890ac3e3931bc"}, - {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c011a4149cfbcf9f03994ec2edffcb8b1dc2d2aede7ca243746df97a5d41ce48"}, - {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:56d9f2ecac662ca1611d183feb03a3fa4406469dafe241673d521dd5ae92a155"}, - {file = "MarkupSafe-2.1.3-cp37-cp37m-win32.whl", hash = "sha256:8758846a7e80910096950b67071243da3e5a20ed2546e6392603c096778d48e0"}, - {file = "MarkupSafe-2.1.3-cp37-cp37m-win_amd64.whl", hash = "sha256:787003c0ddb00500e49a10f2844fac87aa6ce977b90b0feaaf9de23c22508b24"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:2ef12179d3a291be237280175b542c07a36e7f60718296278d8593d21ca937d4"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2c1b19b3aaacc6e57b7e25710ff571c24d6c3613a45e905b1fde04d691b98ee0"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8afafd99945ead6e075b973fefa56379c5b5c53fd8937dad92c662da5d8fd5ee"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c41976a29d078bb235fea9b2ecd3da465df42a562910f9022f1a03107bd02be"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d080e0a5eb2529460b30190fcfcc4199bd7f827663f858a226a81bc27beaa97e"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:69c0f17e9f5a7afdf2cc9fb2d1ce6aabdb3bafb7f38017c0b77862bcec2bbad8"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:504b320cd4b7eff6f968eddf81127112db685e81f7e36e75f9f84f0df46041c3"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:42de32b22b6b804f42c5d98be4f7e5e977ecdd9ee9b660fda1a3edf03b11792d"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-win32.whl", hash = "sha256:ceb01949af7121f9fc39f7d27f91be8546f3fb112c608bc4029aef0bab86a2a5"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-win_amd64.whl", hash = "sha256:1b40069d487e7edb2676d3fbdb2b0829ffa2cd63a2ec26c4938b2d34391b4ecc"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8023faf4e01efadfa183e863fefde0046de576c6f14659e8782065bcece22198"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6b2b56950d93e41f33b4223ead100ea0fe11f8e6ee5f641eb753ce4b77a7042b"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9dcdfd0eaf283af041973bff14a2e143b8bd64e069f4c383416ecd79a81aab58"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:05fb21170423db021895e1ea1e1f3ab3adb85d1c2333cbc2310f2a26bc77272e"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:282c2cb35b5b673bbcadb33a585408104df04f14b2d9b01d4c345a3b92861c2c"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ab4a0df41e7c16a1392727727e7998a467472d0ad65f3ad5e6e765015df08636"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7ef3cb2ebbf91e330e3bb937efada0edd9003683db6b57bb108c4001f37a02ea"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:0a4e4a1aff6c7ac4cd55792abf96c915634c2b97e3cc1c7129578aa68ebd754e"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-win32.whl", hash = "sha256:fec21693218efe39aa7f8599346e90c705afa52c5b31ae019b2e57e8f6542bb2"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-win_amd64.whl", hash = "sha256:3fd4abcb888d15a94f32b75d8fd18ee162ca0c064f35b11134be77050296d6ba"}, - {file = "MarkupSafe-2.1.3.tar.gz", hash = "sha256:af598ed32d6ae86f1b747b82783958b1a4ab8f617b06fe68795c7f026abbdcad"}, -] - -[[package]] -name = "matplotlib" -version = "3.7.2" -description = "Python plotting package" -optional = false -python-versions = ">=3.8" -files = [ - {file = "matplotlib-3.7.2-cp310-cp310-macosx_10_12_universal2.whl", hash = "sha256:2699f7e73a76d4c110f4f25be9d2496d6ab4f17345307738557d345f099e07de"}, - {file = "matplotlib-3.7.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:a8035ba590658bae7562786c9cc6ea1a84aa49d3afab157e414c9e2ea74f496d"}, - {file = "matplotlib-3.7.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2f8e4a49493add46ad4a8c92f63e19d548b2b6ebbed75c6b4c7f46f57d36cdd1"}, - {file = "matplotlib-3.7.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71667eb2ccca4c3537d9414b1bc00554cb7f91527c17ee4ec38027201f8f1603"}, - {file = "matplotlib-3.7.2-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:152ee0b569a37630d8628534c628456b28686e085d51394da6b71ef84c4da201"}, - {file = "matplotlib-3.7.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:070f8dddd1f5939e60aacb8fa08f19551f4b0140fab16a3669d5cd6e9cb28fc8"}, - {file = "matplotlib-3.7.2-cp310-cp310-win32.whl", hash = "sha256:fdbb46fad4fb47443b5b8ac76904b2e7a66556844f33370861b4788db0f8816a"}, - {file = "matplotlib-3.7.2-cp310-cp310-win_amd64.whl", hash = "sha256:23fb1750934e5f0128f9423db27c474aa32534cec21f7b2153262b066a581fd1"}, - {file = "matplotlib-3.7.2-cp311-cp311-macosx_10_12_universal2.whl", hash = "sha256:30e1409b857aa8a747c5d4f85f63a79e479835f8dffc52992ac1f3f25837b544"}, - {file = "matplotlib-3.7.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:50e0a55ec74bf2d7a0ebf50ac580a209582c2dd0f7ab51bc270f1b4a0027454e"}, - {file = "matplotlib-3.7.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ac60daa1dc83e8821eed155796b0f7888b6b916cf61d620a4ddd8200ac70cd64"}, - {file = "matplotlib-3.7.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:305e3da477dc8607336ba10bac96986d6308d614706cae2efe7d3ffa60465b24"}, - {file = "matplotlib-3.7.2-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1c308b255efb9b06b23874236ec0f10f026673ad6515f602027cc8ac7805352d"}, - {file = "matplotlib-3.7.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:60c521e21031632aa0d87ca5ba0c1c05f3daacadb34c093585a0be6780f698e4"}, - {file = "matplotlib-3.7.2-cp311-cp311-win32.whl", hash = "sha256:26bede320d77e469fdf1bde212de0ec889169b04f7f1179b8930d66f82b30cbc"}, - {file = "matplotlib-3.7.2-cp311-cp311-win_amd64.whl", hash = "sha256:af4860132c8c05261a5f5f8467f1b269bf1c7c23902d75f2be57c4a7f2394b3e"}, - {file = "matplotlib-3.7.2-cp38-cp38-macosx_10_12_universal2.whl", hash = "sha256:a1733b8e84e7e40a9853e505fe68cc54339f97273bdfe6f3ed980095f769ddc7"}, - {file = "matplotlib-3.7.2-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:d9881356dc48e58910c53af82b57183879129fa30492be69058c5b0d9fddf391"}, - {file = "matplotlib-3.7.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f081c03f413f59390a80b3e351cc2b2ea0205839714dbc364519bcf51f4b56ca"}, - {file = "matplotlib-3.7.2-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:1cd120fca3407a225168238b790bd5c528f0fafde6172b140a2f3ab7a4ea63e9"}, - {file = "matplotlib-3.7.2-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:a2c1590b90aa7bd741b54c62b78de05d4186271e34e2377e0289d943b3522273"}, - {file = "matplotlib-3.7.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6d2ff3c984b8a569bc1383cd468fc06b70d7b59d5c2854ca39f1436ae8394117"}, - {file = "matplotlib-3.7.2-cp38-cp38-win32.whl", hash = "sha256:5dea00b62d28654b71ca92463656d80646675628d0828e08a5f3b57e12869e13"}, - {file = "matplotlib-3.7.2-cp38-cp38-win_amd64.whl", hash = "sha256:0f506a1776ee94f9e131af1ac6efa6e5bc7cb606a3e389b0ccb6e657f60bb676"}, - {file = "matplotlib-3.7.2-cp39-cp39-macosx_10_12_universal2.whl", hash = "sha256:6515e878f91894c2e4340d81f0911857998ccaf04dbc1bba781e3d89cbf70608"}, - {file = "matplotlib-3.7.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:71f7a8c6b124e904db550f5b9fe483d28b896d4135e45c4ea381ad3b8a0e3256"}, - {file = "matplotlib-3.7.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:12f01b92ecd518e0697da4d97d163b2b3aa55eb3eb4e2c98235b3396d7dad55f"}, - {file = "matplotlib-3.7.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a7e28d6396563955f7af437894a36bf2b279462239a41028323e04b85179058b"}, - {file = "matplotlib-3.7.2-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dbcf59334ff645e6a67cd5f78b4b2cdb76384cdf587fa0d2dc85f634a72e1a3e"}, - {file = "matplotlib-3.7.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:318c89edde72ff95d8df67d82aca03861240512994a597a435a1011ba18dbc7f"}, - {file = "matplotlib-3.7.2-cp39-cp39-win32.whl", hash = "sha256:ce55289d5659b5b12b3db4dc9b7075b70cef5631e56530f14b2945e8836f2d20"}, - {file = "matplotlib-3.7.2-cp39-cp39-win_amd64.whl", hash = "sha256:2ecb5be2b2815431c81dc115667e33da0f5a1bcf6143980d180d09a717c4a12e"}, - {file = "matplotlib-3.7.2-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:fdcd28360dbb6203fb5219b1a5658df226ac9bebc2542a9e8f457de959d713d0"}, - {file = "matplotlib-3.7.2-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0c3cca3e842b11b55b52c6fb8bd6a4088693829acbfcdb3e815fa9b7d5c92c1b"}, - {file = "matplotlib-3.7.2-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ebf577c7a6744e9e1bd3fee45fc74a02710b214f94e2bde344912d85e0c9af7c"}, - {file = "matplotlib-3.7.2-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:936bba394682049919dda062d33435b3be211dc3dcaa011e09634f060ec878b2"}, - {file = "matplotlib-3.7.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:bc221ffbc2150458b1cd71cdd9ddd5bb37962b036e41b8be258280b5b01da1dd"}, - {file = "matplotlib-3.7.2-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:35d74ebdb3f71f112b36c2629cf32323adfbf42679e2751252acd468f5001c07"}, - {file = "matplotlib-3.7.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:717157e61b3a71d3d26ad4e1770dc85156c9af435659a25ee6407dc866cb258d"}, - {file = "matplotlib-3.7.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:20f844d6be031948148ba49605c8b96dfe7d3711d1b63592830d650622458c11"}, - {file = "matplotlib-3.7.2.tar.gz", hash = "sha256:a8cdb91dddb04436bd2f098b8fdf4b81352e68cf4d2c6756fcc414791076569b"}, -] - -[package.dependencies] -contourpy = ">=1.0.1" -cycler = ">=0.10" -fonttools = ">=4.22.0" -importlib-resources = {version = ">=3.2.0", markers = "python_version < \"3.10\""} -kiwisolver = ">=1.0.1" -numpy = ">=1.20" -packaging = ">=20.0" -pillow = ">=6.2.0" -pyparsing = ">=2.3.1,<3.1" -python-dateutil = ">=2.7" - -[[package]] -name = "mdit-py-plugins" -version = "0.3.5" -description = "Collection of plugins for markdown-it-py" -optional = false -python-versions = ">=3.7" -files = [ - {file = "mdit-py-plugins-0.3.5.tar.gz", hash = "sha256:eee0adc7195e5827e17e02d2a258a2ba159944a0748f59c5099a4a27f78fcf6a"}, - {file = "mdit_py_plugins-0.3.5-py3-none-any.whl", hash = "sha256:ca9a0714ea59a24b2b044a1831f48d817dd0c817e84339f20e7889f392d77c4e"}, -] - -[package.dependencies] -markdown-it-py = ">=1.0.0,<3.0.0" - -[package.extras] -code-style = ["pre-commit"] -rtd = ["attrs", "myst-parser (>=0.16.1,<0.17.0)", "sphinx-book-theme (>=0.1.0,<0.2.0)"] -testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] - -[[package]] -name = "mdurl" -version = "0.1.2" -description = "Markdown URL utilities" -optional = false -python-versions = ">=3.7" -files = [ - {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, - {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, -] - -[[package]] -name = "mpmath" -version = "1.3.0" -description = "Python library for arbitrary-precision floating-point arithmetic" -optional = false -python-versions = "*" -files = [ - {file = "mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c"}, - {file = "mpmath-1.3.0.tar.gz", hash = "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f"}, -] - -[package.extras] -develop = ["codecov", "pycodestyle", "pytest (>=4.6)", "pytest-cov", "wheel"] -docs = ["sphinx"] -gmpy = ["gmpy2 (>=2.1.0a4)"] -tests = ["pytest (>=4.6)"] - -[[package]] -name = "myst-parser" -version = "0.18.0" -description = "An extended commonmark compliant parser, with bridges to docutils & sphinx." -optional = false -python-versions = ">=3.7" -files = [ - {file = "myst-parser-0.18.0.tar.gz", hash = "sha256:739a4d96773a8e55a2cacd3941ce46a446ee23dcd6b37e06f73f551ad7821d86"}, - {file = "myst_parser-0.18.0-py3-none-any.whl", hash = "sha256:4965e51918837c13bf1c6f6fe2c6bddddf193148360fbdaefe743a4981358f6a"}, -] - -[package.dependencies] -docutils = ">=0.15,<0.19" -jinja2 = "*" -markdown-it-py = ">=1.0.0,<3.0.0" -mdit-py-plugins = ">=0.3.0,<0.4.0" -pyyaml = "*" -sphinx = ">=4,<6" -typing-extensions = "*" - -[package.extras] -code-style = ["pre-commit (>=2.12,<3.0)"] -linkify = ["linkify-it-py (>=1.0,<2.0)"] -rtd = ["ipython", "sphinx-book-theme", "sphinx-design", "sphinxcontrib.mermaid (>=0.7.1,<0.8.0)", "sphinxext-opengraph (>=0.6.3,<0.7.0)", "sphinxext-rediraffe (>=0.2.7,<0.3.0)"] -testing = ["beautifulsoup4", "coverage[toml]", "pytest (>=6,<7)", "pytest-cov", "pytest-param-files (>=0.3.4,<0.4.0)", "pytest-regressions", "sphinx-pytest"] - -[[package]] -name = "networkx" -version = "3.1" -description = "Python package for creating and manipulating graphs and networks" -optional = false -python-versions = ">=3.8" -files = [ - {file = "networkx-3.1-py3-none-any.whl", hash = "sha256:4f33f68cb2afcf86f28a45f43efc27a9386b535d567d2127f8f61d51dec58d36"}, - {file = "networkx-3.1.tar.gz", hash = "sha256:de346335408f84de0eada6ff9fafafff9bcda11f0a0dfaa931133debb146ab61"}, -] - -[package.extras] -default = ["matplotlib (>=3.4)", "numpy (>=1.20)", "pandas (>=1.3)", "scipy (>=1.8)"] -developer = ["mypy (>=1.1)", "pre-commit (>=3.2)"] -doc = ["nb2plots (>=0.6)", "numpydoc (>=1.5)", "pillow (>=9.4)", "pydata-sphinx-theme (>=0.13)", "sphinx (>=6.1)", "sphinx-gallery (>=0.12)", "texext (>=0.6.7)"] -extra = ["lxml (>=4.6)", "pydot (>=1.4.2)", "pygraphviz (>=1.10)", "sympy (>=1.10)"] -test = ["codecov (>=2.1)", "pytest (>=7.2)", "pytest-cov (>=4.0)"] - -[[package]] -name = "nodeenv" -version = "1.8.0" -description = "Node.js virtual environment builder" -optional = false -python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*" -files = [ - {file = "nodeenv-1.8.0-py2.py3-none-any.whl", hash = "sha256:df865724bb3c3adc86b3876fa209771517b0cfe596beff01a92700e0e8be4cec"}, - {file = "nodeenv-1.8.0.tar.gz", hash = "sha256:d51e0c37e64fbf47d017feac3145cdbb58836d7eee8c6f6d3b6880c5456227d2"}, -] - -[package.dependencies] -setuptools = "*" - -[[package]] -name = "nptyping" -version = "2.5.0" -description = "Type hints for NumPy." -optional = false -python-versions = ">=3.7" -files = [ - {file = "nptyping-2.5.0-py3-none-any.whl", hash = "sha256:764e51836faae33a7ae2e928af574cfb701355647accadcc89f2ad793630b7c8"}, - {file = "nptyping-2.5.0.tar.gz", hash = "sha256:e3d35b53af967e6fb407c3016ff9abae954d3a0568f7cc13a461084224e8e20a"}, -] - -[package.dependencies] -numpy = {version = ">=1.20.0,<2.0.0", markers = "python_version >= \"3.8\""} -typing-extensions = {version = ">=4.0.0,<5.0.0", markers = "python_version < \"3.10\""} - -[package.extras] -build = ["invoke (>=1.6.0)", "pip-tools (>=6.5.0)"] -complete = ["pandas", "pandas-stubs-fork"] -dev = ["autoflake", "beartype (<0.10.0)", "beartype (>=0.10.0)", "black", "codecov (>=2.1.0)", "coverage", "feedparser", "invoke (>=1.6.0)", "isort", "mypy", "pandas", "pandas-stubs-fork", "pip-tools (>=6.5.0)", "pylint", "pyright", "setuptools", "typeguard", "wheel"] -pandas = ["pandas", "pandas-stubs-fork"] -qa = ["autoflake", "beartype (<0.10.0)", "beartype (>=0.10.0)", "black", "codecov (>=2.1.0)", "coverage", "feedparser", "isort", "mypy", "pylint", "pyright", "setuptools", "typeguard", "wheel"] - -[[package]] -name = "numpy" -version = "1.24.4" -description = "Fundamental package for array computing in Python" -optional = false -python-versions = ">=3.8" -files = [ - {file = "numpy-1.24.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c0bfb52d2169d58c1cdb8cc1f16989101639b34c7d3ce60ed70b19c63eba0b64"}, - {file = "numpy-1.24.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ed094d4f0c177b1b8e7aa9cba7d6ceed51c0e569a5318ac0ca9a090680a6a1b1"}, - {file = "numpy-1.24.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79fc682a374c4a8ed08b331bef9c5f582585d1048fa6d80bc6c35bc384eee9b4"}, - {file = "numpy-1.24.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ffe43c74893dbf38c2b0a1f5428760a1a9c98285553c89e12d70a96a7f3a4d6"}, - {file = "numpy-1.24.4-cp310-cp310-win32.whl", hash = "sha256:4c21decb6ea94057331e111a5bed9a79d335658c27ce2adb580fb4d54f2ad9bc"}, - {file = "numpy-1.24.4-cp310-cp310-win_amd64.whl", hash = "sha256:b4bea75e47d9586d31e892a7401f76e909712a0fd510f58f5337bea9572c571e"}, - {file = "numpy-1.24.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f136bab9c2cfd8da131132c2cf6cc27331dd6fae65f95f69dcd4ae3c3639c810"}, - {file = "numpy-1.24.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e2926dac25b313635e4d6cf4dc4e51c8c0ebfed60b801c799ffc4c32bf3d1254"}, - {file = "numpy-1.24.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:222e40d0e2548690405b0b3c7b21d1169117391c2e82c378467ef9ab4c8f0da7"}, - {file = "numpy-1.24.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7215847ce88a85ce39baf9e89070cb860c98fdddacbaa6c0da3ffb31b3350bd5"}, - {file = "numpy-1.24.4-cp311-cp311-win32.whl", hash = "sha256:4979217d7de511a8d57f4b4b5b2b965f707768440c17cb70fbf254c4b225238d"}, - {file = "numpy-1.24.4-cp311-cp311-win_amd64.whl", hash = "sha256:b7b1fc9864d7d39e28f41d089bfd6353cb5f27ecd9905348c24187a768c79694"}, - {file = "numpy-1.24.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1452241c290f3e2a312c137a9999cdbf63f78864d63c79039bda65ee86943f61"}, - {file = "numpy-1.24.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:04640dab83f7c6c85abf9cd729c5b65f1ebd0ccf9de90b270cd61935eef0197f"}, - {file = "numpy-1.24.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5425b114831d1e77e4b5d812b69d11d962e104095a5b9c3b641a218abcc050e"}, - {file = "numpy-1.24.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd80e219fd4c71fc3699fc1dadac5dcf4fd882bfc6f7ec53d30fa197b8ee22dc"}, - {file = "numpy-1.24.4-cp38-cp38-win32.whl", hash = "sha256:4602244f345453db537be5314d3983dbf5834a9701b7723ec28923e2889e0bb2"}, - {file = "numpy-1.24.4-cp38-cp38-win_amd64.whl", hash = "sha256:692f2e0f55794943c5bfff12b3f56f99af76f902fc47487bdfe97856de51a706"}, - {file = "numpy-1.24.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2541312fbf09977f3b3ad449c4e5f4bb55d0dbf79226d7724211acc905049400"}, - {file = "numpy-1.24.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9667575fb6d13c95f1b36aca12c5ee3356bf001b714fc354eb5465ce1609e62f"}, - {file = "numpy-1.24.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3a86ed21e4f87050382c7bc96571755193c4c1392490744ac73d660e8f564a9"}, - {file = "numpy-1.24.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d11efb4dbecbdf22508d55e48d9c8384db795e1b7b51ea735289ff96613ff74d"}, - {file = "numpy-1.24.4-cp39-cp39-win32.whl", hash = "sha256:6620c0acd41dbcb368610bb2f4d83145674040025e5536954782467100aa8835"}, - {file = "numpy-1.24.4-cp39-cp39-win_amd64.whl", hash = "sha256:befe2bf740fd8373cf56149a5c23a0f601e82869598d41f8e188a0e9869926f8"}, - {file = "numpy-1.24.4-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:31f13e25b4e304632a4619d0e0777662c2ffea99fcae2029556b17d8ff958aef"}, - {file = "numpy-1.24.4-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95f7ac6540e95bc440ad77f56e520da5bf877f87dca58bd095288dce8940532a"}, - {file = "numpy-1.24.4-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:e98f220aa76ca2a977fe435f5b04d7b3470c0a2e6312907b37ba6068f26787f2"}, - {file = "numpy-1.24.4.tar.gz", hash = "sha256:80f5e3a4e498641401868df4208b74581206afbee7cf7b8329daae82676d9463"}, -] - -[[package]] -name = "pacfish" -version = "0.4.4" -description = "Photoacoustic Converter for Information Sharing (PACFISH)" -optional = false -python-versions = ">=3.7" -files = [ - {file = "pacfish-0.4.4-py3-none-any.whl", hash = "sha256:c82322d482b1720e99743d26a8d3d240989cba7d6a24644f68177d4adf10b790"}, - {file = "pacfish-0.4.4.tar.gz", hash = "sha256:57ee1faa60857c410ebbaaddef6715985f793bf2988093b1e6706944aa5f8ae2"}, -] - -[package.dependencies] -coverage = "*" -h5py = "*" -imageio = "*" -matplotlib = "*" -myst-parser = "*" -numpy = "*" -pynrrd = "*" -pytest-cov = "*" -scipy = "*" - -[[package]] -name = "packaging" -version = "23.1" -description = "Core utilities for Python packages" -optional = false -python-versions = ">=3.7" -files = [ - {file = "packaging-23.1-py3-none-any.whl", hash = "sha256:994793af429502c4ea2ebf6bf664629d07c1a9fe974af92966e4b8d2df7edc61"}, - {file = "packaging-23.1.tar.gz", hash = "sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f"}, -] - -[[package]] -name = "pandas" -version = "2.0.3" -description = "Powerful data structures for data analysis, time series, and statistics" -optional = false -python-versions = ">=3.8" -files = [ - {file = "pandas-2.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e4c7c9f27a4185304c7caf96dc7d91bc60bc162221152de697c98eb0b2648dd8"}, - {file = "pandas-2.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f167beed68918d62bffb6ec64f2e1d8a7d297a038f86d4aed056b9493fca407f"}, - {file = "pandas-2.0.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce0c6f76a0f1ba361551f3e6dceaff06bde7514a374aa43e33b588ec10420183"}, - {file = "pandas-2.0.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba619e410a21d8c387a1ea6e8a0e49bb42216474436245718d7f2e88a2f8d7c0"}, - {file = "pandas-2.0.3-cp310-cp310-win32.whl", hash = "sha256:3ef285093b4fe5058eefd756100a367f27029913760773c8bf1d2d8bebe5d210"}, - {file = "pandas-2.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:9ee1a69328d5c36c98d8e74db06f4ad518a1840e8ccb94a4ba86920986bb617e"}, - {file = "pandas-2.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b084b91d8d66ab19f5bb3256cbd5ea661848338301940e17f4492b2ce0801fe8"}, - {file = "pandas-2.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:37673e3bdf1551b95bf5d4ce372b37770f9529743d2498032439371fc7b7eb26"}, - {file = "pandas-2.0.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9cb1e14fdb546396b7e1b923ffaeeac24e4cedd14266c3497216dd4448e4f2d"}, - {file = "pandas-2.0.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d9cd88488cceb7635aebb84809d087468eb33551097d600c6dad13602029c2df"}, - {file = "pandas-2.0.3-cp311-cp311-win32.whl", hash = "sha256:694888a81198786f0e164ee3a581df7d505024fbb1f15202fc7db88a71d84ebd"}, - {file = "pandas-2.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:6a21ab5c89dcbd57f78d0ae16630b090eec626360085a4148693def5452d8a6b"}, - {file = "pandas-2.0.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9e4da0d45e7f34c069fe4d522359df7d23badf83abc1d1cef398895822d11061"}, - {file = "pandas-2.0.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:32fca2ee1b0d93dd71d979726b12b61faa06aeb93cf77468776287f41ff8fdc5"}, - {file = "pandas-2.0.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:258d3624b3ae734490e4d63c430256e716f488c4fcb7c8e9bde2d3aa46c29089"}, - {file = "pandas-2.0.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9eae3dc34fa1aa7772dd3fc60270d13ced7346fcbcfee017d3132ec625e23bb0"}, - {file = "pandas-2.0.3-cp38-cp38-win32.whl", hash = "sha256:f3421a7afb1a43f7e38e82e844e2bca9a6d793d66c1a7f9f0ff39a795bbc5e02"}, - {file = "pandas-2.0.3-cp38-cp38-win_amd64.whl", hash = "sha256:69d7f3884c95da3a31ef82b7618af5710dba95bb885ffab339aad925c3e8ce78"}, - {file = "pandas-2.0.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5247fb1ba347c1261cbbf0fcfba4a3121fbb4029d95d9ef4dc45406620b25c8b"}, - {file = "pandas-2.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:81af086f4543c9d8bb128328b5d32e9986e0c84d3ee673a2ac6fb57fd14f755e"}, - {file = "pandas-2.0.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1994c789bf12a7c5098277fb43836ce090f1073858c10f9220998ac74f37c69b"}, - {file = "pandas-2.0.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5ec591c48e29226bcbb316e0c1e9423622bc7a4eaf1ef7c3c9fa1a3981f89641"}, - {file = "pandas-2.0.3-cp39-cp39-win32.whl", hash = "sha256:04dbdbaf2e4d46ca8da896e1805bc04eb85caa9a82e259e8eed00254d5e0c682"}, - {file = "pandas-2.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:1168574b036cd8b93abc746171c9b4f1b83467438a5e45909fed645cf8692dbc"}, - {file = "pandas-2.0.3.tar.gz", hash = "sha256:c02f372a88e0d17f36d3093a644c73cfc1788e876a7c4bcb4020a77512e2043c"}, -] - -[package.dependencies] -numpy = [ - {version = ">=1.20.3", markers = "python_version < \"3.10\""}, - {version = ">=1.21.0", markers = "python_version >= \"3.10\""}, - {version = ">=1.23.2", markers = "python_version >= \"3.11\""}, -] -python-dateutil = ">=2.8.2" -pytz = ">=2020.1" -tzdata = ">=2022.1" - -[package.extras] -all = ["PyQt5 (>=5.15.1)", "SQLAlchemy (>=1.4.16)", "beautifulsoup4 (>=4.9.3)", "bottleneck (>=1.3.2)", "brotlipy (>=0.7.0)", "fastparquet (>=0.6.3)", "fsspec (>=2021.07.0)", "gcsfs (>=2021.07.0)", "html5lib (>=1.1)", "hypothesis (>=6.34.2)", "jinja2 (>=3.0.0)", "lxml (>=4.6.3)", "matplotlib (>=3.6.1)", "numba (>=0.53.1)", "numexpr (>=2.7.3)", "odfpy (>=1.4.1)", "openpyxl (>=3.0.7)", "pandas-gbq (>=0.15.0)", "psycopg2 (>=2.8.6)", "pyarrow (>=7.0.0)", "pymysql (>=1.0.2)", "pyreadstat (>=1.1.2)", "pytest (>=7.3.2)", "pytest-asyncio (>=0.17.0)", "pytest-xdist (>=2.2.0)", "python-snappy (>=0.6.0)", "pyxlsb (>=1.0.8)", "qtpy (>=2.2.0)", "s3fs (>=2021.08.0)", "scipy (>=1.7.1)", "tables (>=3.6.1)", "tabulate (>=0.8.9)", "xarray (>=0.21.0)", "xlrd (>=2.0.1)", "xlsxwriter (>=1.4.3)", "zstandard (>=0.15.2)"] -aws = ["s3fs (>=2021.08.0)"] -clipboard = ["PyQt5 (>=5.15.1)", "qtpy (>=2.2.0)"] -compression = ["brotlipy (>=0.7.0)", "python-snappy (>=0.6.0)", "zstandard (>=0.15.2)"] -computation = ["scipy (>=1.7.1)", "xarray (>=0.21.0)"] -excel = ["odfpy (>=1.4.1)", "openpyxl (>=3.0.7)", "pyxlsb (>=1.0.8)", "xlrd (>=2.0.1)", "xlsxwriter (>=1.4.3)"] -feather = ["pyarrow (>=7.0.0)"] -fss = ["fsspec (>=2021.07.0)"] -gcp = ["gcsfs (>=2021.07.0)", "pandas-gbq (>=0.15.0)"] -hdf5 = ["tables (>=3.6.1)"] -html = ["beautifulsoup4 (>=4.9.3)", "html5lib (>=1.1)", "lxml (>=4.6.3)"] -mysql = ["SQLAlchemy (>=1.4.16)", "pymysql (>=1.0.2)"] -output-formatting = ["jinja2 (>=3.0.0)", "tabulate (>=0.8.9)"] -parquet = ["pyarrow (>=7.0.0)"] -performance = ["bottleneck (>=1.3.2)", "numba (>=0.53.1)", "numexpr (>=2.7.1)"] -plot = ["matplotlib (>=3.6.1)"] -postgresql = ["SQLAlchemy (>=1.4.16)", "psycopg2 (>=2.8.6)"] -spss = ["pyreadstat (>=1.1.2)"] -sql-other = ["SQLAlchemy (>=1.4.16)"] -test = ["hypothesis (>=6.34.2)", "pytest (>=7.3.2)", "pytest-asyncio (>=0.17.0)", "pytest-xdist (>=2.2.0)"] -xml = ["lxml (>=4.6.3)"] - -[[package]] -name = "pillow" -version = "10.0.0" -description = "Python Imaging Library (Fork)" -optional = false -python-versions = ">=3.8" -files = [ - {file = "Pillow-10.0.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:1f62406a884ae75fb2f818694469519fb685cc7eaff05d3451a9ebe55c646891"}, - {file = "Pillow-10.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d5db32e2a6ccbb3d34d87c87b432959e0db29755727afb37290e10f6e8e62614"}, - {file = "Pillow-10.0.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:edf4392b77bdc81f36e92d3a07a5cd072f90253197f4a52a55a8cec48a12483b"}, - {file = "Pillow-10.0.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:520f2a520dc040512699f20fa1c363eed506e94248d71f85412b625026f6142c"}, - {file = "Pillow-10.0.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:8c11160913e3dd06c8ffdb5f233a4f254cb449f4dfc0f8f4549eda9e542c93d1"}, - {file = "Pillow-10.0.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:a74ba0c356aaa3bb8e3eb79606a87669e7ec6444be352870623025d75a14a2bf"}, - {file = "Pillow-10.0.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d5d0dae4cfd56969d23d94dc8e89fb6a217be461c69090768227beb8ed28c0a3"}, - {file = "Pillow-10.0.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:22c10cc517668d44b211717fd9775799ccec4124b9a7f7b3635fc5386e584992"}, - {file = "Pillow-10.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:dffe31a7f47b603318c609f378ebcd57f1554a3a6a8effbc59c3c69f804296de"}, - {file = "Pillow-10.0.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:9fb218c8a12e51d7ead2a7c9e101a04982237d4855716af2e9499306728fb485"}, - {file = "Pillow-10.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d35e3c8d9b1268cbf5d3670285feb3528f6680420eafe35cccc686b73c1e330f"}, - {file = "Pillow-10.0.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ed64f9ca2f0a95411e88a4efbd7a29e5ce2cea36072c53dd9d26d9c76f753b3"}, - {file = "Pillow-10.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b6eb5502f45a60a3f411c63187db83a3d3107887ad0d036c13ce836f8a36f1d"}, - {file = "Pillow-10.0.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:c1fbe7621c167ecaa38ad29643d77a9ce7311583761abf7836e1510c580bf3dd"}, - {file = "Pillow-10.0.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:cd25d2a9d2b36fcb318882481367956d2cf91329f6892fe5d385c346c0649629"}, - {file = "Pillow-10.0.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:3b08d4cc24f471b2c8ca24ec060abf4bebc6b144cb89cba638c720546b1cf538"}, - {file = "Pillow-10.0.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d737a602fbd82afd892ca746392401b634e278cb65d55c4b7a8f48e9ef8d008d"}, - {file = "Pillow-10.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:3a82c40d706d9aa9734289740ce26460a11aeec2d9c79b7af87bb35f0073c12f"}, - {file = "Pillow-10.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:bc2ec7c7b5d66b8ec9ce9f720dbb5fa4bace0f545acd34870eff4a369b44bf37"}, - {file = "Pillow-10.0.0-cp312-cp312-macosx_10_10_x86_64.whl", hash = "sha256:d80cf684b541685fccdd84c485b31ce73fc5c9b5d7523bf1394ce134a60c6883"}, - {file = "Pillow-10.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:76de421f9c326da8f43d690110f0e79fe3ad1e54be811545d7d91898b4c8493e"}, - {file = "Pillow-10.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:81ff539a12457809666fef6624684c008e00ff6bf455b4b89fd00a140eecd640"}, - {file = "Pillow-10.0.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce543ed15570eedbb85df19b0a1a7314a9c8141a36ce089c0a894adbfccb4568"}, - {file = "Pillow-10.0.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:685ac03cc4ed5ebc15ad5c23bc555d68a87777586d970c2c3e216619a5476223"}, - {file = "Pillow-10.0.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:d72e2ecc68a942e8cf9739619b7f408cc7b272b279b56b2c83c6123fcfa5cdff"}, - {file = "Pillow-10.0.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d50b6aec14bc737742ca96e85d6d0a5f9bfbded018264b3b70ff9d8c33485551"}, - {file = "Pillow-10.0.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:00e65f5e822decd501e374b0650146063fbb30a7264b4d2744bdd7b913e0cab5"}, - {file = "Pillow-10.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:f31f9fdbfecb042d046f9d91270a0ba28368a723302786c0009ee9b9f1f60199"}, - {file = "Pillow-10.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:1ce91b6ec08d866b14413d3f0bbdea7e24dfdc8e59f562bb77bc3fe60b6144ca"}, - {file = "Pillow-10.0.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:349930d6e9c685c089284b013478d6f76e3a534e36ddfa912cde493f235372f3"}, - {file = "Pillow-10.0.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3a684105f7c32488f7153905a4e3015a3b6c7182e106fe3c37fbb5ef3e6994c3"}, - {file = "Pillow-10.0.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b4f69b3700201b80bb82c3a97d5e9254084f6dd5fb5b16fc1a7b974260f89f43"}, - {file = "Pillow-10.0.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f07ea8d2f827d7d2a49ecf1639ec02d75ffd1b88dcc5b3a61bbb37a8759ad8d"}, - {file = "Pillow-10.0.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:040586f7d37b34547153fa383f7f9aed68b738992380ac911447bb78f2abe530"}, - {file = "Pillow-10.0.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:f88a0b92277de8e3ca715a0d79d68dc82807457dae3ab8699c758f07c20b3c51"}, - {file = "Pillow-10.0.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:c7cf14a27b0d6adfaebb3ae4153f1e516df54e47e42dcc073d7b3d76111a8d86"}, - {file = "Pillow-10.0.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:3400aae60685b06bb96f99a21e1ada7bc7a413d5f49bce739828ecd9391bb8f7"}, - {file = "Pillow-10.0.0-cp38-cp38-win_amd64.whl", hash = "sha256:dbc02381779d412145331789b40cc7b11fdf449e5d94f6bc0b080db0a56ea3f0"}, - {file = "Pillow-10.0.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:9211e7ad69d7c9401cfc0e23d49b69ca65ddd898976d660a2fa5904e3d7a9baa"}, - {file = "Pillow-10.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:faaf07ea35355b01a35cb442dd950d8f1bb5b040a7787791a535de13db15ed90"}, - {file = "Pillow-10.0.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c9f72a021fbb792ce98306ffb0c348b3c9cb967dce0f12a49aa4c3d3fdefa967"}, - {file = "Pillow-10.0.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f7c16705f44e0504a3a2a14197c1f0b32a95731d251777dcb060aa83022cb2d"}, - {file = "Pillow-10.0.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:76edb0a1fa2b4745fb0c99fb9fb98f8b180a1bbceb8be49b087e0b21867e77d3"}, - {file = "Pillow-10.0.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:368ab3dfb5f49e312231b6f27b8820c823652b7cd29cfbd34090565a015e99ba"}, - {file = "Pillow-10.0.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:608bfdee0d57cf297d32bcbb3c728dc1da0907519d1784962c5f0c68bb93e5a3"}, - {file = "Pillow-10.0.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5c6e3df6bdd396749bafd45314871b3d0af81ff935b2d188385e970052091017"}, - {file = "Pillow-10.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:7be600823e4c8631b74e4a0d38384c73f680e6105a7d3c6824fcf226c178c7e6"}, - {file = "Pillow-10.0.0-pp310-pypy310_pp73-macosx_10_10_x86_64.whl", hash = "sha256:92be919bbc9f7d09f7ae343c38f5bb21c973d2576c1d45600fce4b74bafa7ac0"}, - {file = "Pillow-10.0.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f8182b523b2289f7c415f589118228d30ac8c355baa2f3194ced084dac2dbba"}, - {file = "Pillow-10.0.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:38250a349b6b390ee6047a62c086d3817ac69022c127f8a5dc058c31ccef17f3"}, - {file = "Pillow-10.0.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:88af2003543cc40c80f6fca01411892ec52b11021b3dc22ec3bc9d5afd1c5334"}, - {file = "Pillow-10.0.0-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:c189af0545965fa8d3b9613cfdb0cd37f9d71349e0f7750e1fd704648d475ed2"}, - {file = "Pillow-10.0.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce7b031a6fc11365970e6a5686d7ba8c63e4c1cf1ea143811acbb524295eabed"}, - {file = "Pillow-10.0.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:db24668940f82321e746773a4bc617bfac06ec831e5c88b643f91f122a785684"}, - {file = "Pillow-10.0.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:efe8c0681042536e0d06c11f48cebe759707c9e9abf880ee213541c5b46c5bf3"}, - {file = "Pillow-10.0.0.tar.gz", hash = "sha256:9c82b5b3e043c7af0d95792d0d20ccf68f61a1fec6b3530e718b688422727396"}, -] - -[package.extras] -docs = ["furo", "olefile", "sphinx (>=2.4)", "sphinx-copybutton", "sphinx-inline-tabs", "sphinx-removed-in", "sphinxext-opengraph"] -tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout"] - -[[package]] -name = "platformdirs" -version = "3.9.1" -description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." -optional = false -python-versions = ">=3.7" -files = [ - {file = "platformdirs-3.9.1-py3-none-any.whl", hash = "sha256:ad8291ae0ae5072f66c16945166cb11c63394c7a3ad1b1bc9828ca3162da8c2f"}, - {file = "platformdirs-3.9.1.tar.gz", hash = "sha256:1b42b450ad933e981d56e59f1b97495428c9bd60698baab9f3eb3d00d5822421"}, -] - -[package.extras] -docs = ["furo (>=2023.5.20)", "proselint (>=0.13)", "sphinx (>=7.0.1)", "sphinx-autodoc-typehints (>=1.23,!=1.23.4)"] -test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.3.1)", "pytest-cov (>=4.1)", "pytest-mock (>=3.10)"] - -[[package]] -name = "pluggy" -version = "1.2.0" -description = "plugin and hook calling mechanisms for python" -optional = false -python-versions = ">=3.7" -files = [ - {file = "pluggy-1.2.0-py3-none-any.whl", hash = "sha256:c2fd55a7d7a3863cba1a013e4e2414658b1d07b6bc57b3919e0c63c9abb99849"}, - {file = "pluggy-1.2.0.tar.gz", hash = "sha256:d12f0c4b579b15f5e054301bb226ee85eeeba08ffec228092f8defbaa3a4c4b3"}, -] - -[package.extras] -dev = ["pre-commit", "tox"] -testing = ["pytest", "pytest-benchmark"] - -[[package]] -name = "pre-commit" -version = "3.3.3" -description = "A framework for managing and maintaining multi-language pre-commit hooks." -optional = false -python-versions = ">=3.8" -files = [ - {file = "pre_commit-3.3.3-py2.py3-none-any.whl", hash = "sha256:10badb65d6a38caff29703362271d7dca483d01da88f9d7e05d0b97171c136cb"}, - {file = "pre_commit-3.3.3.tar.gz", hash = "sha256:a2256f489cd913d575c145132ae196fe335da32d91a8294b7afe6622335dd023"}, -] - -[package.dependencies] -cfgv = ">=2.0.0" -identify = ">=1.0.0" -nodeenv = ">=0.11.1" -pyyaml = ">=5.1" -virtualenv = ">=20.10.0" - -[[package]] -name = "pygments" -version = "2.15.1" -description = "Pygments is a syntax highlighting package written in Python." -optional = false -python-versions = ">=3.7" -files = [ - {file = "Pygments-2.15.1-py3-none-any.whl", hash = "sha256:db2db3deb4b4179f399a09054b023b6a586b76499d36965813c71aa8ed7b5fd1"}, - {file = "Pygments-2.15.1.tar.gz", hash = "sha256:8ace4d3c1dd481894b2005f560ead0f9f19ee64fe983366be1a21e171d12775c"}, -] - -[package.extras] -plugins = ["importlib-metadata"] - -[[package]] -name = "pynrrd" -version = "1.0.0" -description = "Pure python module for reading and writing NRRD files." -optional = false -python-versions = ">=3.7" -files = [ - {file = "pynrrd-1.0.0-py2.py3-none-any.whl", hash = "sha256:65e5a61920d2f01ecf321eb41b0472940e181e4ba5e8a32f01ef5499d4192db5"}, - {file = "pynrrd-1.0.0.tar.gz", hash = "sha256:4eb4caba03fbca1b832114515e748336cb67bce70c7f3ae36bfa2e135fc990d2"}, -] - -[package.dependencies] -nptyping = "*" -numpy = ">=1.11.1" -typing-extensions = "*" - -[[package]] -name = "pyparsing" -version = "3.0.9" -description = "pyparsing module - Classes and methods to define and execute parsing grammars" -optional = false -python-versions = ">=3.6.8" -files = [ - {file = "pyparsing-3.0.9-py3-none-any.whl", hash = "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"}, - {file = "pyparsing-3.0.9.tar.gz", hash = "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb"}, -] - -[package.extras] -diagrams = ["jinja2", "railroad-diagrams"] - -[[package]] -name = "pytest" -version = "7.4.0" -description = "pytest: simple powerful testing with Python" -optional = false -python-versions = ">=3.7" -files = [ - {file = "pytest-7.4.0-py3-none-any.whl", hash = "sha256:78bf16451a2eb8c7a2ea98e32dc119fd2aa758f1d5d66dbf0a59d69a3969df32"}, - {file = "pytest-7.4.0.tar.gz", hash = "sha256:b4bf8c45bd59934ed84001ad51e11b4ee40d40a1229d2c79f9c592b0a3f6bd8a"}, -] - -[package.dependencies] -colorama = {version = "*", markers = "sys_platform == \"win32\""} -exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} -iniconfig = "*" -packaging = "*" -pluggy = ">=0.12,<2.0" -tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} - -[package.extras] -testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] - -[[package]] -name = "pytest-cov" -version = "4.1.0" -description = "Pytest plugin for measuring coverage." -optional = false -python-versions = ">=3.7" -files = [ - {file = "pytest-cov-4.1.0.tar.gz", hash = "sha256:3904b13dfbfec47f003b8e77fd5b589cd11904a21ddf1ab38a64f204d6a10ef6"}, - {file = "pytest_cov-4.1.0-py3-none-any.whl", hash = "sha256:6ba70b9e97e69fcc3fb45bfeab2d0a138fb65c4d0d6a41ef33983ad114be8c3a"}, -] - -[package.dependencies] -coverage = {version = ">=5.2.1", extras = ["toml"]} -pytest = ">=4.6" - -[package.extras] -testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"] - -[[package]] -name = "python-dateutil" -version = "2.8.2" -description = "Extensions to the standard Python datetime module" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" -files = [ - {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, - {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, -] - -[package.dependencies] -six = ">=1.5" - -[[package]] -name = "python-dotenv" -version = "1.0.0" -description = "Read key-value pairs from a .env file and set them as environment variables" -optional = false -python-versions = ">=3.8" -files = [ - {file = "python-dotenv-1.0.0.tar.gz", hash = "sha256:a8df96034aae6d2d50a4ebe8216326c61c3eb64836776504fcca410e5937a3ba"}, - {file = "python_dotenv-1.0.0-py3-none-any.whl", hash = "sha256:f5971a9226b701070a4bf2c38c89e5a3f0d64de8debda981d1db98583009122a"}, -] - -[package.extras] -cli = ["click (>=5.0)"] - -[[package]] -name = "pytz" -version = "2023.3" -description = "World timezone definitions, modern and historical" -optional = false -python-versions = "*" -files = [ - {file = "pytz-2023.3-py2.py3-none-any.whl", hash = "sha256:a151b3abb88eda1d4e34a9814df37de2a80e301e68ba0fd856fb9b46bfbbbffb"}, - {file = "pytz-2023.3.tar.gz", hash = "sha256:1d8ce29db189191fb55338ee6d0387d82ab59f3d00eac103412d64e0ebd0c588"}, -] - -[[package]] -name = "pywavelets" -version = "1.4.1" -description = "PyWavelets, wavelet transform module" -optional = false -python-versions = ">=3.8" -files = [ - {file = "PyWavelets-1.4.1-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:d854411eb5ee9cb4bc5d0e66e3634aeb8f594210f6a1bed96dbed57ec70f181c"}, - {file = "PyWavelets-1.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:231b0e0b1cdc1112f4af3c24eea7bf181c418d37922a67670e9bf6cfa2d544d4"}, - {file = "PyWavelets-1.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:754fa5085768227c4f4a26c1e0c78bc509a266d9ebd0eb69a278be7e3ece943c"}, - {file = "PyWavelets-1.4.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:da7b9c006171be1f9ddb12cc6e0d3d703b95f7f43cb5e2c6f5f15d3233fcf202"}, - {file = "PyWavelets-1.4.1-cp310-cp310-win32.whl", hash = "sha256:67a0d28a08909f21400cb09ff62ba94c064882ffd9e3a6b27880a111211d59bd"}, - {file = "PyWavelets-1.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:91d3d393cffa634f0e550d88c0e3f217c96cfb9e32781f2960876f1808d9b45b"}, - {file = "PyWavelets-1.4.1-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:64c6bac6204327321db30b775060fbe8e8642316e6bff17f06b9f34936f88875"}, - {file = "PyWavelets-1.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3f19327f2129fb7977bc59b966b4974dfd72879c093e44a7287500a7032695de"}, - {file = "PyWavelets-1.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad987748f60418d5f4138db89d82ba0cb49b086e0cbb8fd5c3ed4a814cfb705e"}, - {file = "PyWavelets-1.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:875d4d620eee655346e3589a16a73790cf9f8917abba062234439b594e706784"}, - {file = "PyWavelets-1.4.1-cp311-cp311-win32.whl", hash = "sha256:7231461d7a8eb3bdc7aa2d97d9f67ea5a9f8902522818e7e2ead9c2b3408eeb1"}, - {file = "PyWavelets-1.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:daf0aa79842b571308d7c31a9c43bc99a30b6328e6aea3f50388cd8f69ba7dbc"}, - {file = "PyWavelets-1.4.1-cp38-cp38-macosx_10_13_x86_64.whl", hash = "sha256:ab7da0a17822cd2f6545626946d3b82d1a8e106afc4b50e3387719ba01c7b966"}, - {file = "PyWavelets-1.4.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:578af438a02a86b70f1975b546f68aaaf38f28fb082a61ceb799816049ed18aa"}, - {file = "PyWavelets-1.4.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cb5ca8d11d3f98e89e65796a2125be98424d22e5ada360a0dbabff659fca0fc"}, - {file = "PyWavelets-1.4.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:058b46434eac4c04dd89aeef6fa39e4b6496a951d78c500b6641fd5b2cc2f9f4"}, - {file = "PyWavelets-1.4.1-cp38-cp38-win32.whl", hash = "sha256:de7cd61a88a982edfec01ea755b0740e94766e00a1ceceeafef3ed4c85c605cd"}, - {file = "PyWavelets-1.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:7ab8d9db0fe549ab2ee0bea61f614e658dd2df419d5b75fba47baa761e95f8f2"}, - {file = "PyWavelets-1.4.1-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:23bafd60350b2b868076d976bdd92f950b3944f119b4754b1d7ff22b7acbf6c6"}, - {file = "PyWavelets-1.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d0e56cd7a53aed3cceca91a04d62feb3a0aca6725b1912d29546c26f6ea90426"}, - {file = "PyWavelets-1.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:030670a213ee8fefa56f6387b0c8e7d970c7f7ad6850dc048bd7c89364771b9b"}, - {file = "PyWavelets-1.4.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:71ab30f51ee4470741bb55fc6b197b4a2b612232e30f6ac069106f0156342356"}, - {file = "PyWavelets-1.4.1-cp39-cp39-win32.whl", hash = "sha256:47cac4fa25bed76a45bc781a293c26ac63e8eaae9eb8f9be961758d22b58649c"}, - {file = "PyWavelets-1.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:88aa5449e109d8f5e7f0adef85f7f73b1ab086102865be64421a3a3d02d277f4"}, - {file = "PyWavelets-1.4.1.tar.gz", hash = "sha256:6437af3ddf083118c26d8f97ab43b0724b956c9f958e9ea788659f6a2834ba93"}, -] - -[package.dependencies] -numpy = ">=1.17.3" - -[[package]] -name = "pyyaml" -version = "6.0.1" -description = "YAML parser and emitter for Python" -optional = false -python-versions = ">=3.6" -files = [ - {file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"}, - {file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"}, - {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, - {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, - {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, - {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, - {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, - {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, - {file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"}, - {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, - {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, - {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, - {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, - {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, - {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, - {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, - {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, - {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd"}, - {file = "PyYAML-6.0.1-cp36-cp36m-win32.whl", hash = "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585"}, - {file = "PyYAML-6.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa"}, - {file = "PyYAML-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3"}, - {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27"}, - {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3"}, - {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c"}, - {file = "PyYAML-6.0.1-cp37-cp37m-win32.whl", hash = "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba"}, - {file = "PyYAML-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867"}, - {file = "PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595"}, - {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, - {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, - {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, - {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, - {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, - {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, - {file = "PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859"}, - {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, - {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, - {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, - {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, - {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, - {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, -] - -[[package]] -name = "requests" -version = "2.31.0" -description = "Python HTTP for Humans." -optional = false -python-versions = ">=3.7" -files = [ - {file = "requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f"}, - {file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"}, -] - -[package.dependencies] -certifi = ">=2017.4.17" -charset-normalizer = ">=2,<4" -idna = ">=2.5,<4" -urllib3 = ">=1.21.1,<3" - -[package.extras] -socks = ["PySocks (>=1.5.6,!=1.5.7)"] -use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] - -[[package]] -name = "scikit-image" -version = "0.21.0" -description = "Image processing in Python" -optional = false -python-versions = ">=3.8" -files = [ - {file = "scikit_image-0.21.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:978ac3302252155a8556cdfe067bad2d18d5ccef4e91c2f727bc564ed75566bc"}, - {file = "scikit_image-0.21.0-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:82c22e008527e5ee26ab08e3ce919998ef164d538ff30b9e5764b223cfda06b1"}, - {file = "scikit_image-0.21.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fd29d2631d3e975c377066acfc1f4cb2cc95e2257cf70e7fedfcb96441096e88"}, - {file = "scikit_image-0.21.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c6c12925ceb9f3aede555921e26642d601b2d37d1617002a2636f2cb5178ae2f"}, - {file = "scikit_image-0.21.0-cp310-cp310-win_amd64.whl", hash = "sha256:1f538d4de77e4f3225d068d9ea2965bed3f7dda7f457a8f89634fa22ffb9ad8c"}, - {file = "scikit_image-0.21.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ec9bab6920ac43037d7434058b67b5778d42c60f67b8679239f48a471e7ed6f8"}, - {file = "scikit_image-0.21.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:a54720430dba833ffbb6dedd93d9f0938c5dd47d20ab9ba3e4e61c19d95f6f19"}, - {file = "scikit_image-0.21.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7e40dd102da14cdadc09210f930b4556c90ff8f99cd9d8bcccf9f73a86c44245"}, - {file = "scikit_image-0.21.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ff5719c7eb99596a39c3e1d9b564025bae78ecf1da3ee6842d34f6965b5f1474"}, - {file = "scikit_image-0.21.0-cp311-cp311-win_amd64.whl", hash = "sha256:146c3824253eee9ff346c4ad10cb58376f91aefaf4a4bb2fe11aa21691f7de76"}, - {file = "scikit_image-0.21.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:4e1b09f81a99c9c390215929194847b3cd358550b4b65bb6e42c5393d69cb74a"}, - {file = "scikit_image-0.21.0-cp38-cp38-macosx_12_0_arm64.whl", hash = "sha256:9f7b5fb4a22f0d5ae0fa13beeb887c925280590145cd6d8b2630794d120ff7c7"}, - {file = "scikit_image-0.21.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4814033717f0b6491fee252facb9df92058d6a72ab78dd6408a50f3915a88b8"}, - {file = "scikit_image-0.21.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2b0d6ed6502cca0c9719c444caafa0b8cda0f9e29e01ca42f621a240073284be"}, - {file = "scikit_image-0.21.0-cp38-cp38-win_amd64.whl", hash = "sha256:9194cb7bc21215fde6c1b1e9685d312d2aa8f65ea9736bc6311126a91c860032"}, - {file = "scikit_image-0.21.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:54df1ddc854f37a912d42bc724e456e86858107e94048a81a27720bc588f9937"}, - {file = "scikit_image-0.21.0-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:c01e3ab0a1fabfd8ce30686d4401b7ed36e6126c9d4d05cb94abf6bdc46f7ac9"}, - {file = "scikit_image-0.21.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8ef5d8d1099317b7b315b530348cbfa68ab8ce32459de3c074d204166951025c"}, - {file = "scikit_image-0.21.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78b1e96c59cab640ca5c5b22c501524cfaf34cbe0cb51ba73bd9a9ede3fb6e1d"}, - {file = "scikit_image-0.21.0-cp39-cp39-win_amd64.whl", hash = "sha256:9cffcddd2a5594c0a06de2ae3e1e25d662745a26f94fda31520593669677c010"}, - {file = "scikit_image-0.21.0.tar.gz", hash = "sha256:b33e823c54e6f11873ea390ee49ef832b82b9f70752c8759efd09d5a4e3d87f0"}, -] - -[package.dependencies] -imageio = ">=2.27" -lazy_loader = ">=0.2" -networkx = ">=2.8" -numpy = ">=1.21.1" -packaging = ">=21" -pillow = ">=9.0.1" -PyWavelets = ">=1.1.1" -scipy = ">=1.8" -tifffile = ">=2022.8.12" - -[package.extras] -build = ["Cython (>=0.29.32)", "build", "meson-python (>=0.13)", "ninja", "numpy (>=1.21.1)", "packaging (>=21)", "pythran", "setuptools (>=67)", "spin (==0.3)", "wheel"] -data = ["pooch (>=1.6.0)"] -default = ["PyWavelets (>=1.1.1)", "imageio (>=2.27)", "lazy_loader (>=0.2)", "networkx (>=2.8)", "numpy (>=1.21.1)", "packaging (>=21)", "pillow (>=9.0.1)", "scipy (>=1.8)", "tifffile (>=2022.8.12)"] -developer = ["pre-commit", "rtoml"] -docs = ["dask[array] (>=2022.9.2)", "ipykernel", "ipywidgets", "kaleido", "matplotlib (>=3.5)", "myst-parser", "numpydoc (>=1.5)", "pandas (>=1.5)", "plotly (>=5.10)", "pooch (>=1.6)", "pydata-sphinx-theme (>=0.13)", "pytest-runner", "scikit-learn (>=0.24.0)", "seaborn (>=0.11)", "sphinx (>=5.0)", "sphinx-copybutton", "sphinx-gallery (>=0.11)", "sphinx_design (>=0.3)", "tifffile (>=2022.8.12)"] -optional = ["SimpleITK", "astropy (>=5.0)", "cloudpickle (>=0.2.1)", "dask[array] (>=2021.1.0)", "matplotlib (>=3.5)", "pooch (>=1.6.0)", "pyamg", "scikit-learn (>=0.24.0)"] -test = ["asv", "matplotlib (>=3.5)", "pooch (>=1.6.0)", "pytest (>=7.0)", "pytest-cov (>=2.11.0)", "pytest-faulthandler", "pytest-localserver"] - -[[package]] -name = "scipy" -version = "1.9.3" -description = "Fundamental algorithms for scientific computing in Python" -optional = false -python-versions = ">=3.8" -files = [ - {file = "scipy-1.9.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1884b66a54887e21addf9c16fb588720a8309a57b2e258ae1c7986d4444d3bc0"}, - {file = "scipy-1.9.3-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:83b89e9586c62e787f5012e8475fbb12185bafb996a03257e9675cd73d3736dd"}, - {file = "scipy-1.9.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a72d885fa44247f92743fc20732ae55564ff2a519e8302fb7e18717c5355a8b"}, - {file = "scipy-1.9.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d01e1dd7b15bd2449c8bfc6b7cc67d630700ed655654f0dfcf121600bad205c9"}, - {file = "scipy-1.9.3-cp310-cp310-win_amd64.whl", hash = "sha256:68239b6aa6f9c593da8be1509a05cb7f9efe98b80f43a5861cd24c7557e98523"}, - {file = "scipy-1.9.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b41bc822679ad1c9a5f023bc93f6d0543129ca0f37c1ce294dd9d386f0a21096"}, - {file = "scipy-1.9.3-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:90453d2b93ea82a9f434e4e1cba043e779ff67b92f7a0e85d05d286a3625df3c"}, - {file = "scipy-1.9.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:83c06e62a390a9167da60bedd4575a14c1f58ca9dfde59830fc42e5197283dab"}, - {file = "scipy-1.9.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:abaf921531b5aeaafced90157db505e10345e45038c39e5d9b6c7922d68085cb"}, - {file = "scipy-1.9.3-cp311-cp311-win_amd64.whl", hash = "sha256:06d2e1b4c491dc7d8eacea139a1b0b295f74e1a1a0f704c375028f8320d16e31"}, - {file = "scipy-1.9.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:5a04cd7d0d3eff6ea4719371cbc44df31411862b9646db617c99718ff68d4840"}, - {file = "scipy-1.9.3-cp38-cp38-macosx_12_0_arm64.whl", hash = "sha256:545c83ffb518094d8c9d83cce216c0c32f8c04aaf28b92cc8283eda0685162d5"}, - {file = "scipy-1.9.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d54222d7a3ba6022fdf5773931b5d7c56efe41ede7f7128c7b1637700409108"}, - {file = "scipy-1.9.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cff3a5295234037e39500d35316a4c5794739433528310e117b8a9a0c76d20fc"}, - {file = "scipy-1.9.3-cp38-cp38-win_amd64.whl", hash = "sha256:2318bef588acc7a574f5bfdff9c172d0b1bf2c8143d9582e05f878e580a3781e"}, - {file = "scipy-1.9.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d644a64e174c16cb4b2e41dfea6af722053e83d066da7343f333a54dae9bc31c"}, - {file = "scipy-1.9.3-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:da8245491d73ed0a994ed9c2e380fd058ce2fa8a18da204681f2fe1f57f98f95"}, - {file = "scipy-1.9.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4db5b30849606a95dcf519763dd3ab6fe9bd91df49eba517359e450a7d80ce2e"}, - {file = "scipy-1.9.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c68db6b290cbd4049012990d7fe71a2abd9ffbe82c0056ebe0f01df8be5436b0"}, - {file = "scipy-1.9.3-cp39-cp39-win_amd64.whl", hash = "sha256:5b88e6d91ad9d59478fafe92a7c757d00c59e3bdc3331be8ada76a4f8d683f58"}, - {file = "scipy-1.9.3.tar.gz", hash = "sha256:fbc5c05c85c1a02be77b1ff591087c83bc44579c6d2bd9fb798bb64ea5e1a027"}, -] - -[package.dependencies] -numpy = ">=1.18.5,<1.26.0" - -[package.extras] -dev = ["flake8", "mypy", "pycodestyle", "typing_extensions"] -doc = ["matplotlib (>2)", "numpydoc", "pydata-sphinx-theme (==0.9.0)", "sphinx (!=4.1.0)", "sphinx-panels (>=0.5.2)", "sphinx-tabs"] -test = ["asv", "gmpy2", "mpmath", "pytest", "pytest-cov", "pytest-xdist", "scikit-umfpack", "threadpoolctl"] - -[[package]] -name = "setuptools" -version = "68.0.0" -description = "Easily download, build, install, upgrade, and uninstall Python packages" -optional = false -python-versions = ">=3.7" -files = [ - {file = "setuptools-68.0.0-py3-none-any.whl", hash = "sha256:11e52c67415a381d10d6b462ced9cfb97066179f0e871399e006c4ab101fc85f"}, - {file = "setuptools-68.0.0.tar.gz", hash = "sha256:baf1fdb41c6da4cd2eae722e135500da913332ab3f2f5c7d33af9b492acb5235"}, -] - -[package.extras] -docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"] -testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] -testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] - -[[package]] -name = "six" -version = "1.16.0" -description = "Python 2 and 3 compatibility utilities" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" -files = [ - {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, - {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, -] - -[[package]] -name = "snowballstemmer" -version = "2.2.0" -description = "This package provides 29 stemmers for 28 languages generated from Snowball algorithms." -optional = false -python-versions = "*" -files = [ - {file = "snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a"}, - {file = "snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1"}, -] - -[[package]] -name = "sphinx" -version = "5.3.0" -description = "Python documentation generator" -optional = false -python-versions = ">=3.6" -files = [ - {file = "Sphinx-5.3.0.tar.gz", hash = "sha256:51026de0a9ff9fc13c05d74913ad66047e104f56a129ff73e174eb5c3ee794b5"}, - {file = "sphinx-5.3.0-py3-none-any.whl", hash = "sha256:060ca5c9f7ba57a08a1219e547b269fadf125ae25b06b9fa7f66768efb652d6d"}, -] - -[package.dependencies] -alabaster = ">=0.7,<0.8" -babel = ">=2.9" -colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""} -docutils = ">=0.14,<0.20" -imagesize = ">=1.3" -importlib-metadata = {version = ">=4.8", markers = "python_version < \"3.10\""} -Jinja2 = ">=3.0" -packaging = ">=21.0" -Pygments = ">=2.12" -requests = ">=2.5.0" -snowballstemmer = ">=2.0" -sphinxcontrib-applehelp = "*" -sphinxcontrib-devhelp = "*" -sphinxcontrib-htmlhelp = ">=2.0.0" -sphinxcontrib-jsmath = "*" -sphinxcontrib-qthelp = "*" -sphinxcontrib-serializinghtml = ">=1.1.5" - -[package.extras] -docs = ["sphinxcontrib-websupport"] -lint = ["docutils-stubs", "flake8 (>=3.5.0)", "flake8-bugbear", "flake8-comprehensions", "flake8-simplify", "isort", "mypy (>=0.981)", "sphinx-lint", "types-requests", "types-typed-ast"] -test = ["cython", "html5lib", "pytest (>=4.6)", "typed_ast"] - -[[package]] -name = "sphinx-rtd-theme" -version = "1.2.2" -description = "Read the Docs theme for Sphinx" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" -files = [ - {file = "sphinx_rtd_theme-1.2.2-py2.py3-none-any.whl", hash = "sha256:6a7e7d8af34eb8fc57d52a09c6b6b9c46ff44aea5951bc831eeb9245378f3689"}, - {file = "sphinx_rtd_theme-1.2.2.tar.gz", hash = "sha256:01c5c5a72e2d025bd23d1f06c59a4831b06e6ce6c01fdd5ebfe9986c0a880fc7"}, -] - -[package.dependencies] -docutils = "<0.19" -sphinx = ">=1.6,<7" -sphinxcontrib-jquery = ">=4,<5" - -[package.extras] -dev = ["bump2version", "sphinxcontrib-httpdomain", "transifex-client", "wheel"] - -[[package]] -name = "sphinxcontrib-applehelp" -version = "1.0.4" -description = "sphinxcontrib-applehelp is a Sphinx extension which outputs Apple help books" -optional = false -python-versions = ">=3.8" -files = [ - {file = "sphinxcontrib-applehelp-1.0.4.tar.gz", hash = "sha256:828f867945bbe39817c210a1abfd1bc4895c8b73fcaade56d45357a348a07d7e"}, - {file = "sphinxcontrib_applehelp-1.0.4-py3-none-any.whl", hash = "sha256:29d341f67fb0f6f586b23ad80e072c8e6ad0b48417db2bde114a4c9746feb228"}, -] - -[package.extras] -lint = ["docutils-stubs", "flake8", "mypy"] -test = ["pytest"] - -[[package]] -name = "sphinxcontrib-devhelp" -version = "1.0.2" -description = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp document." -optional = false -python-versions = ">=3.5" -files = [ - {file = "sphinxcontrib-devhelp-1.0.2.tar.gz", hash = "sha256:ff7f1afa7b9642e7060379360a67e9c41e8f3121f2ce9164266f61b9f4b338e4"}, - {file = "sphinxcontrib_devhelp-1.0.2-py2.py3-none-any.whl", hash = "sha256:8165223f9a335cc1af7ffe1ed31d2871f325254c0423bc0c4c7cd1c1e4734a2e"}, -] - -[package.extras] -lint = ["docutils-stubs", "flake8", "mypy"] -test = ["pytest"] - -[[package]] -name = "sphinxcontrib-htmlhelp" -version = "2.0.1" -description = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML help files" -optional = false -python-versions = ">=3.8" -files = [ - {file = "sphinxcontrib-htmlhelp-2.0.1.tar.gz", hash = "sha256:0cbdd302815330058422b98a113195c9249825d681e18f11e8b1f78a2f11efff"}, - {file = "sphinxcontrib_htmlhelp-2.0.1-py3-none-any.whl", hash = "sha256:c38cb46dccf316c79de6e5515e1770414b797162b23cd3d06e67020e1d2a6903"}, -] - -[package.extras] -lint = ["docutils-stubs", "flake8", "mypy"] -test = ["html5lib", "pytest"] - -[[package]] -name = "sphinxcontrib-jquery" -version = "4.1" -description = "Extension to include jQuery on newer Sphinx releases" -optional = false -python-versions = ">=2.7" -files = [ - {file = "sphinxcontrib-jquery-4.1.tar.gz", hash = "sha256:1620739f04e36a2c779f1a131a2dfd49b2fd07351bf1968ced074365933abc7a"}, - {file = "sphinxcontrib_jquery-4.1-py2.py3-none-any.whl", hash = "sha256:f936030d7d0147dd026a4f2b5a57343d233f1fc7b363f68b3d4f1cb0993878ae"}, -] - -[package.dependencies] -Sphinx = ">=1.8" - -[[package]] -name = "sphinxcontrib-jsmath" -version = "1.0.1" -description = "A sphinx extension which renders display math in HTML via JavaScript" -optional = false -python-versions = ">=3.5" -files = [ - {file = "sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8"}, - {file = "sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178"}, -] - -[package.extras] -test = ["flake8", "mypy", "pytest"] - -[[package]] -name = "sphinxcontrib-qthelp" -version = "1.0.3" -description = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp document." -optional = false -python-versions = ">=3.5" -files = [ - {file = "sphinxcontrib-qthelp-1.0.3.tar.gz", hash = "sha256:4c33767ee058b70dba89a6fc5c1892c0d57a54be67ddd3e7875a18d14cba5a72"}, - {file = "sphinxcontrib_qthelp-1.0.3-py2.py3-none-any.whl", hash = "sha256:bd9fc24bcb748a8d51fd4ecaade681350aa63009a347a8c14e637895444dfab6"}, -] - -[package.extras] -lint = ["docutils-stubs", "flake8", "mypy"] -test = ["pytest"] - -[[package]] -name = "sphinxcontrib-serializinghtml" -version = "1.1.5" -description = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)." -optional = false -python-versions = ">=3.5" -files = [ - {file = "sphinxcontrib-serializinghtml-1.1.5.tar.gz", hash = "sha256:aa5f6de5dfdf809ef505c4895e51ef5c9eac17d0f287933eb49ec495280b6952"}, - {file = "sphinxcontrib_serializinghtml-1.1.5-py2.py3-none-any.whl", hash = "sha256:352a9a00ae864471d3a7ead8d7d79f5fc0b57e8b3f95e9867eb9eb28999b92fd"}, -] - -[package.extras] -lint = ["docutils-stubs", "flake8", "mypy"] -test = ["pytest"] - -[[package]] -name = "sympy" -version = "1.12" -description = "Computer algebra system (CAS) in Python" -optional = false -python-versions = ">=3.8" -files = [ - {file = "sympy-1.12-py3-none-any.whl", hash = "sha256:c3588cd4295d0c0f603d0f2ae780587e64e2efeedb3521e46b9bb1d08d184fa5"}, - {file = "sympy-1.12.tar.gz", hash = "sha256:ebf595c8dac3e0fdc4152c51878b498396ec7f30e7a914d6071e674d49420fb8"}, -] - -[package.dependencies] -mpmath = ">=0.19" - -[[package]] -name = "tifffile" -version = "2023.7.10" -description = "Read and write TIFF files" -optional = false -python-versions = ">=3.8" -files = [ - {file = "tifffile-2023.7.10-py3-none-any.whl", hash = "sha256:94dfdec321ace96abbfe872a66cfd824800c099a2db558443453eebc2c11b304"}, - {file = "tifffile-2023.7.10.tar.gz", hash = "sha256:c06ec460926d16796eeee249a560bcdddf243daae36ac62af3c84a953cd60b4a"}, -] - -[package.dependencies] -numpy = "*" - -[package.extras] -all = ["defusedxml", "fsspec", "imagecodecs (>=2023.1.23)", "lxml", "matplotlib", "zarr"] - -[[package]] -name = "tomli" -version = "2.0.1" -description = "A lil' TOML parser" -optional = false -python-versions = ">=3.7" -files = [ - {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, - {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, -] - -[[package]] -name = "torch" -version = "2.0.1" -description = "Tensors and Dynamic neural networks in Python with strong GPU acceleration" -optional = false -python-versions = ">=3.8.0" -files = [ - {file = "torch-2.0.1-cp310-cp310-manylinux1_x86_64.whl", hash = "sha256:8ced00b3ba471856b993822508f77c98f48a458623596a4c43136158781e306a"}, - {file = "torch-2.0.1-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:359bfaad94d1cda02ab775dc1cc386d585712329bb47b8741607ef6ef4950747"}, - {file = "torch-2.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:7c84e44d9002182edd859f3400deaa7410f5ec948a519cc7ef512c2f9b34d2c4"}, - {file = "torch-2.0.1-cp310-none-macosx_10_9_x86_64.whl", hash = "sha256:567f84d657edc5582d716900543e6e62353dbe275e61cdc36eda4929e46df9e7"}, - {file = "torch-2.0.1-cp310-none-macosx_11_0_arm64.whl", hash = "sha256:787b5a78aa7917465e9b96399b883920c88a08f4eb63b5a5d2d1a16e27d2f89b"}, - {file = "torch-2.0.1-cp311-cp311-manylinux1_x86_64.whl", hash = "sha256:e617b1d0abaf6ced02dbb9486803abfef0d581609b09641b34fa315c9c40766d"}, - {file = "torch-2.0.1-cp311-cp311-manylinux2014_aarch64.whl", hash = "sha256:b6019b1de4978e96daa21d6a3ebb41e88a0b474898fe251fd96189587408873e"}, - {file = "torch-2.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:dbd68cbd1cd9da32fe5d294dd3411509b3d841baecb780b38b3b7b06c7754434"}, - {file = "torch-2.0.1-cp311-none-macosx_10_9_x86_64.whl", hash = "sha256:ef654427d91600129864644e35deea761fb1fe131710180b952a6f2e2207075e"}, - {file = "torch-2.0.1-cp311-none-macosx_11_0_arm64.whl", hash = "sha256:25aa43ca80dcdf32f13da04c503ec7afdf8e77e3a0183dd85cd3e53b2842e527"}, - {file = "torch-2.0.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:5ef3ea3d25441d3957348f7e99c7824d33798258a2bf5f0f0277cbcadad2e20d"}, - {file = "torch-2.0.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:0882243755ff28895e8e6dc6bc26ebcf5aa0911ed81b2a12f241fc4b09075b13"}, - {file = "torch-2.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:f66aa6b9580a22b04d0af54fcd042f52406a8479e2b6a550e3d9f95963e168c8"}, - {file = "torch-2.0.1-cp38-none-macosx_10_9_x86_64.whl", hash = "sha256:1adb60d369f2650cac8e9a95b1d5758e25d526a34808f7448d0bd599e4ae9072"}, - {file = "torch-2.0.1-cp38-none-macosx_11_0_arm64.whl", hash = "sha256:1bcffc16b89e296826b33b98db5166f990e3b72654a2b90673e817b16c50e32b"}, - {file = "torch-2.0.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:e10e1597f2175365285db1b24019eb6f04d53dcd626c735fc502f1e8b6be9875"}, - {file = "torch-2.0.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:423e0ae257b756bb45a4b49072046772d1ad0c592265c5080070e0767da4e490"}, - {file = "torch-2.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:8742bdc62946c93f75ff92da00e3803216c6cce9b132fbca69664ca38cfb3e18"}, - {file = "torch-2.0.1-cp39-none-macosx_10_9_x86_64.whl", hash = "sha256:c62df99352bd6ee5a5a8d1832452110435d178b5164de450831a3a8cc14dc680"}, - {file = "torch-2.0.1-cp39-none-macosx_11_0_arm64.whl", hash = "sha256:671a2565e3f63b8fe8e42ae3e36ad249fe5e567435ea27b94edaa672a7d0c416"}, -] - -[package.dependencies] -filelock = "*" -jinja2 = "*" -networkx = "*" -sympy = "*" -typing-extensions = "*" - -[package.extras] -opt-einsum = ["opt-einsum (>=3.3)"] - -[[package]] -name = "typing-extensions" -version = "4.7.1" -description = "Backported and Experimental Type Hints for Python 3.7+" -optional = false -python-versions = ">=3.7" -files = [ - {file = "typing_extensions-4.7.1-py3-none-any.whl", hash = "sha256:440d5dd3af93b060174bf433bccd69b0babc3b15b1a8dca43789fd7f61514b36"}, - {file = "typing_extensions-4.7.1.tar.gz", hash = "sha256:b75ddc264f0ba5615db7ba217daeb99701ad295353c45f9e95963337ceeeffb2"}, -] - -[[package]] -name = "tzdata" -version = "2023.3" -description = "Provider of IANA time zone data" -optional = false -python-versions = ">=2" -files = [ - {file = "tzdata-2023.3-py2.py3-none-any.whl", hash = "sha256:7e65763eef3120314099b6939b5546db7adce1e7d6f2e179e3df563c70511eda"}, - {file = "tzdata-2023.3.tar.gz", hash = "sha256:11ef1e08e54acb0d4f95bdb1be05da659673de4acbd21bf9c69e94cc5e907a3a"}, -] - -[[package]] -name = "urllib3" -version = "2.0.4" -description = "HTTP library with thread-safe connection pooling, file post, and more." -optional = false -python-versions = ">=3.7" -files = [ - {file = "urllib3-2.0.4-py3-none-any.whl", hash = "sha256:de7df1803967d2c2a98e4b11bb7d6bd9210474c46e8a0401514e3a42a75ebde4"}, - {file = "urllib3-2.0.4.tar.gz", hash = "sha256:8d22f86aae8ef5e410d4f539fde9ce6b2113a001bb4d189e0aed70642d602b11"}, -] - -[package.extras] -brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] -secure = ["certifi", "cryptography (>=1.9)", "idna (>=2.0.0)", "pyopenssl (>=17.1.0)", "urllib3-secure-extra"] -socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] -zstd = ["zstandard (>=0.18.0)"] - -[[package]] -name = "virtualenv" -version = "20.24.2" -description = "Virtual Python Environment builder" -optional = false -python-versions = ">=3.7" -files = [ - {file = "virtualenv-20.24.2-py3-none-any.whl", hash = "sha256:43a3052be36080548bdee0b42919c88072037d50d56c28bd3f853cbe92b953ff"}, - {file = "virtualenv-20.24.2.tar.gz", hash = "sha256:fd8a78f46f6b99a67b7ec5cf73f92357891a7b3a40fd97637c27f854aae3b9e0"}, -] - -[package.dependencies] -distlib = ">=0.3.7,<1" -filelock = ">=3.12.2,<4" -platformdirs = ">=3.9.1,<4" - -[package.extras] -docs = ["furo (>=2023.5.20)", "proselint (>=0.13)", "sphinx (>=7.0.1)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] -test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"] - -[[package]] -name = "wget" -version = "3.2" -description = "pure python download utility" -optional = false -python-versions = "*" -files = [ - {file = "wget-3.2.zip", hash = "sha256:35e630eca2aa50ce998b9b1a127bb26b30dfee573702782aa982f875e3f16061"}, -] - -[[package]] -name = "wrapt" -version = "1.15.0" -description = "Module for decorators, wrappers and monkey patching." -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" -files = [ - {file = "wrapt-1.15.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:ca1cccf838cd28d5a0883b342474c630ac48cac5df0ee6eacc9c7290f76b11c1"}, - {file = "wrapt-1.15.0-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:e826aadda3cae59295b95343db8f3d965fb31059da7de01ee8d1c40a60398b29"}, - {file = "wrapt-1.15.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:5fc8e02f5984a55d2c653f5fea93531e9836abbd84342c1d1e17abc4a15084c2"}, - {file = "wrapt-1.15.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:96e25c8603a155559231c19c0349245eeb4ac0096fe3c1d0be5c47e075bd4f46"}, - {file = "wrapt-1.15.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:40737a081d7497efea35ab9304b829b857f21558acfc7b3272f908d33b0d9d4c"}, - {file = "wrapt-1.15.0-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:f87ec75864c37c4c6cb908d282e1969e79763e0d9becdfe9fe5473b7bb1e5f09"}, - {file = "wrapt-1.15.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:1286eb30261894e4c70d124d44b7fd07825340869945c79d05bda53a40caa079"}, - {file = "wrapt-1.15.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:493d389a2b63c88ad56cdc35d0fa5752daac56ca755805b1b0c530f785767d5e"}, - {file = "wrapt-1.15.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:58d7a75d731e8c63614222bcb21dd992b4ab01a399f1f09dd82af17bbfc2368a"}, - {file = "wrapt-1.15.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:21f6d9a0d5b3a207cdf7acf8e58d7d13d463e639f0c7e01d82cdb671e6cb7923"}, - {file = "wrapt-1.15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ce42618f67741d4697684e501ef02f29e758a123aa2d669e2d964ff734ee00ee"}, - {file = "wrapt-1.15.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:41d07d029dd4157ae27beab04d22b8e261eddfc6ecd64ff7000b10dc8b3a5727"}, - {file = "wrapt-1.15.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:54accd4b8bc202966bafafd16e69da9d5640ff92389d33d28555c5fd4f25ccb7"}, - {file = "wrapt-1.15.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2fbfbca668dd15b744418265a9607baa970c347eefd0db6a518aaf0cfbd153c0"}, - {file = "wrapt-1.15.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:76e9c727a874b4856d11a32fb0b389afc61ce8aaf281ada613713ddeadd1cfec"}, - {file = "wrapt-1.15.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e20076a211cd6f9b44a6be58f7eeafa7ab5720eb796975d0c03f05b47d89eb90"}, - {file = "wrapt-1.15.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a74d56552ddbde46c246b5b89199cb3fd182f9c346c784e1a93e4dc3f5ec9975"}, - {file = "wrapt-1.15.0-cp310-cp310-win32.whl", hash = "sha256:26458da5653aa5b3d8dc8b24192f574a58984c749401f98fff994d41d3f08da1"}, - {file = "wrapt-1.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:75760a47c06b5974aa5e01949bf7e66d2af4d08cb8c1d6516af5e39595397f5e"}, - {file = "wrapt-1.15.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ba1711cda2d30634a7e452fc79eabcadaffedf241ff206db2ee93dd2c89a60e7"}, - {file = "wrapt-1.15.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:56374914b132c702aa9aa9959c550004b8847148f95e1b824772d453ac204a72"}, - {file = "wrapt-1.15.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a89ce3fd220ff144bd9d54da333ec0de0399b52c9ac3d2ce34b569cf1a5748fb"}, - {file = "wrapt-1.15.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3bbe623731d03b186b3d6b0d6f51865bf598587c38d6f7b0be2e27414f7f214e"}, - {file = "wrapt-1.15.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3abbe948c3cbde2689370a262a8d04e32ec2dd4f27103669a45c6929bcdbfe7c"}, - {file = "wrapt-1.15.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:b67b819628e3b748fd3c2192c15fb951f549d0f47c0449af0764d7647302fda3"}, - {file = "wrapt-1.15.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:7eebcdbe3677e58dd4c0e03b4f2cfa346ed4049687d839adad68cc38bb559c92"}, - {file = "wrapt-1.15.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:74934ebd71950e3db69960a7da29204f89624dde411afbfb3b4858c1409b1e98"}, - {file = "wrapt-1.15.0-cp311-cp311-win32.whl", hash = "sha256:bd84395aab8e4d36263cd1b9308cd504f6cf713b7d6d3ce25ea55670baec5416"}, - {file = "wrapt-1.15.0-cp311-cp311-win_amd64.whl", hash = "sha256:a487f72a25904e2b4bbc0817ce7a8de94363bd7e79890510174da9d901c38705"}, - {file = "wrapt-1.15.0-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:4ff0d20f2e670800d3ed2b220d40984162089a6e2c9646fdb09b85e6f9a8fc29"}, - {file = "wrapt-1.15.0-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:9ed6aa0726b9b60911f4aed8ec5b8dd7bf3491476015819f56473ffaef8959bd"}, - {file = "wrapt-1.15.0-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:896689fddba4f23ef7c718279e42f8834041a21342d95e56922e1c10c0cc7afb"}, - {file = "wrapt-1.15.0-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:75669d77bb2c071333417617a235324a1618dba66f82a750362eccbe5b61d248"}, - {file = "wrapt-1.15.0-cp35-cp35m-win32.whl", hash = "sha256:fbec11614dba0424ca72f4e8ba3c420dba07b4a7c206c8c8e4e73f2e98f4c559"}, - {file = "wrapt-1.15.0-cp35-cp35m-win_amd64.whl", hash = "sha256:fd69666217b62fa5d7c6aa88e507493a34dec4fa20c5bd925e4bc12fce586639"}, - {file = "wrapt-1.15.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:b0724f05c396b0a4c36a3226c31648385deb6a65d8992644c12a4963c70326ba"}, - {file = "wrapt-1.15.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bbeccb1aa40ab88cd29e6c7d8585582c99548f55f9b2581dfc5ba68c59a85752"}, - {file = "wrapt-1.15.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:38adf7198f8f154502883242f9fe7333ab05a5b02de7d83aa2d88ea621f13364"}, - {file = "wrapt-1.15.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:578383d740457fa790fdf85e6d346fda1416a40549fe8db08e5e9bd281c6a475"}, - {file = "wrapt-1.15.0-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:a4cbb9ff5795cd66f0066bdf5947f170f5d63a9274f99bdbca02fd973adcf2a8"}, - {file = "wrapt-1.15.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:af5bd9ccb188f6a5fdda9f1f09d9f4c86cc8a539bd48a0bfdc97723970348418"}, - {file = "wrapt-1.15.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:b56d5519e470d3f2fe4aa7585f0632b060d532d0696c5bdfb5e8319e1d0f69a2"}, - {file = "wrapt-1.15.0-cp36-cp36m-win32.whl", hash = "sha256:77d4c1b881076c3ba173484dfa53d3582c1c8ff1f914c6461ab70c8428b796c1"}, - {file = "wrapt-1.15.0-cp36-cp36m-win_amd64.whl", hash = "sha256:077ff0d1f9d9e4ce6476c1a924a3332452c1406e59d90a2cf24aeb29eeac9420"}, - {file = "wrapt-1.15.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:5c5aa28df055697d7c37d2099a7bc09f559d5053c3349b1ad0c39000e611d317"}, - {file = "wrapt-1.15.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3a8564f283394634a7a7054b7983e47dbf39c07712d7b177b37e03f2467a024e"}, - {file = "wrapt-1.15.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:780c82a41dc493b62fc5884fb1d3a3b81106642c5c5c78d6a0d4cbe96d62ba7e"}, - {file = "wrapt-1.15.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e169e957c33576f47e21864cf3fc9ff47c223a4ebca8960079b8bd36cb014fd0"}, - {file = "wrapt-1.15.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:b02f21c1e2074943312d03d243ac4388319f2456576b2c6023041c4d57cd7019"}, - {file = "wrapt-1.15.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:f2e69b3ed24544b0d3dbe2c5c0ba5153ce50dcebb576fdc4696d52aa22db6034"}, - {file = "wrapt-1.15.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d787272ed958a05b2c86311d3a4135d3c2aeea4fc655705f074130aa57d71653"}, - {file = "wrapt-1.15.0-cp37-cp37m-win32.whl", hash = "sha256:02fce1852f755f44f95af51f69d22e45080102e9d00258053b79367d07af39c0"}, - {file = "wrapt-1.15.0-cp37-cp37m-win_amd64.whl", hash = "sha256:abd52a09d03adf9c763d706df707c343293d5d106aea53483e0ec8d9e310ad5e"}, - {file = "wrapt-1.15.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cdb4f085756c96a3af04e6eca7f08b1345e94b53af8921b25c72f096e704e145"}, - {file = "wrapt-1.15.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:230ae493696a371f1dbffaad3dafbb742a4d27a0afd2b1aecebe52b740167e7f"}, - {file = "wrapt-1.15.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63424c681923b9f3bfbc5e3205aafe790904053d42ddcc08542181a30a7a51bd"}, - {file = "wrapt-1.15.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d6bcbfc99f55655c3d93feb7ef3800bd5bbe963a755687cbf1f490a71fb7794b"}, - {file = "wrapt-1.15.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c99f4309f5145b93eca6e35ac1a988f0dc0a7ccf9ccdcd78d3c0adf57224e62f"}, - {file = "wrapt-1.15.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b130fe77361d6771ecf5a219d8e0817d61b236b7d8b37cc045172e574ed219e6"}, - {file = "wrapt-1.15.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:96177eb5645b1c6985f5c11d03fc2dbda9ad24ec0f3a46dcce91445747e15094"}, - {file = "wrapt-1.15.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d5fe3e099cf07d0fb5a1e23d399e5d4d1ca3e6dfcbe5c8570ccff3e9208274f7"}, - {file = "wrapt-1.15.0-cp38-cp38-win32.whl", hash = "sha256:abd8f36c99512755b8456047b7be10372fca271bf1467a1caa88db991e7c421b"}, - {file = "wrapt-1.15.0-cp38-cp38-win_amd64.whl", hash = "sha256:b06fa97478a5f478fb05e1980980a7cdf2712015493b44d0c87606c1513ed5b1"}, - {file = "wrapt-1.15.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2e51de54d4fb8fb50d6ee8327f9828306a959ae394d3e01a1ba8b2f937747d86"}, - {file = "wrapt-1.15.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0970ddb69bba00670e58955f8019bec4a42d1785db3faa043c33d81de2bf843c"}, - {file = "wrapt-1.15.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76407ab327158c510f44ded207e2f76b657303e17cb7a572ffe2f5a8a48aa04d"}, - {file = "wrapt-1.15.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cd525e0e52a5ff16653a3fc9e3dd827981917d34996600bbc34c05d048ca35cc"}, - {file = "wrapt-1.15.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d37ac69edc5614b90516807de32d08cb8e7b12260a285ee330955604ed9dd29"}, - {file = "wrapt-1.15.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:078e2a1a86544e644a68422f881c48b84fef6d18f8c7a957ffd3f2e0a74a0d4a"}, - {file = "wrapt-1.15.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:2cf56d0e237280baed46f0b5316661da892565ff58309d4d2ed7dba763d984b8"}, - {file = "wrapt-1.15.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:7dc0713bf81287a00516ef43137273b23ee414fe41a3c14be10dd95ed98a2df9"}, - {file = "wrapt-1.15.0-cp39-cp39-win32.whl", hash = "sha256:46ed616d5fb42f98630ed70c3529541408166c22cdfd4540b88d5f21006b0eff"}, - {file = "wrapt-1.15.0-cp39-cp39-win_amd64.whl", hash = "sha256:eef4d64c650f33347c1f9266fa5ae001440b232ad9b98f1f43dfe7a79435c0a6"}, - {file = "wrapt-1.15.0-py3-none-any.whl", hash = "sha256:64b1df0f83706b4ef4cfb4fb0e4c2669100fd7ecacfb59e091fad300d4e04640"}, - {file = "wrapt-1.15.0.tar.gz", hash = "sha256:d06730c6aed78cee4126234cf2d071e01b44b915e725a6cb439a879ec9754a3a"}, -] - -[[package]] -name = "xmltodict" -version = "0.13.0" -description = "Makes working with XML feel like you are working with JSON" -optional = false -python-versions = ">=3.4" -files = [ - {file = "xmltodict-0.13.0-py2.py3-none-any.whl", hash = "sha256:aa89e8fd76320154a40d19a0df04a4695fb9dc5ba977cbb68ab3e4eb225e7852"}, - {file = "xmltodict-0.13.0.tar.gz", hash = "sha256:341595a488e3e01a85a9d8911d8912fd922ede5fecc4dce437eb4b6c8d037e56"}, -] - -[[package]] -name = "zipp" -version = "3.16.2" -description = "Backport of pathlib-compatible object wrapper for zip files" -optional = false -python-versions = ">=3.8" -files = [ - {file = "zipp-3.16.2-py3-none-any.whl", hash = "sha256:679e51dd4403591b2d6838a48de3d283f3d188412a9782faadf845f298736ba0"}, - {file = "zipp-3.16.2.tar.gz", hash = "sha256:ebc15946aa78bd63458992fc81ec3b6f7b1e92d51c35e6de1c3804e73b799147"}, -] - -[package.extras] -docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy (>=0.9.1)", "pytest-ruff"] - -[metadata] -lock-version = "2.0" -python-versions = ">=3.8" -content-hash = "df229890fcc3919670a7bd56fb82cd1dcacb9d3087b350ec62e9b2e4f7835ec2" diff --git a/license_header.txt b/pre_commit_configs/license_header.txt similarity index 100% rename from license_header.txt rename to pre_commit_configs/license_header.txt diff --git a/pre_commit_configs/link-config.json b/pre_commit_configs/link-config.json new file mode 100644 index 00000000..f1dd61d8 --- /dev/null +++ b/pre_commit_configs/link-config.json @@ -0,0 +1,7 @@ +{ + "ignorePatterns": [ + { + "pattern": "https://doi.org/10.1117/1.JBO.27.8.083010" + } + ] +} \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 6f24aa7f..b1937a95 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,57 +1,61 @@ -[tool.poetry] +[build-system] +requires = ["setuptools", "setuptools-scm"] +build-backend = "setuptools.build_meta" + +[project] name = "simpa" -version = "0.8.20" -description = "Simulation and Image Processing for Photonics and Acoustics" +dynamic = ["version"] authors = [ - "Division of Intelligent Medical Systems (IMSY), DKFZ ", - "Janek Groehl <>"] -license = "MIT" + {name = "Division of Intelligent Medical Systems (IMSY), DKFZ", email = "k.dreher@dkfz-heidelberg.de"}, + {name = "Janek Groehl"} +] +description = "Simulation and Image Processing for Photonics and Acoustics" +license = {text = "MIT"} readme = "README.md" -# requires-python = ">=3.7" -keywords = ["simulation", "photonics", "acoustics",] - -homepage = "https://github.com/IMSY-DKFZ/simpa" -documentation = "https://simpa.readthedocs.io/en/develop/" -repository = "https://github.com/IMSY-DKFZ/simpa" +keywords = ["simulation", "photonics", "acoustics"] +requires-python = ">=3.8" +dependencies = [ + "matplotlib>=3.5.0", # Uses PSF-License (MIT compatible) + "numpy>=1.21.4", # Uses BSD-License (MIT compatible) + "scipy>=1.7.2,<1.14.0", # Uses BSD-like-License (MIT compatible) + "pynrrd>=0.4.2", # Uses MIT-License (MIT compatible) + "scikit-image>=0.18.3", # Uses BSD-License (MIT compatible) + "xmltodict>=0.12.0", # Uses MIT-License (MIT compatible) + "h5py>=3.6.0", # Uses BSD-License (MIT compatible) + "pandas>=1.3.4", # Uses BSD-License (MIT compatible) + "coverage>=6.1.2", # Uses Apache 2.0-License (MIT compatible) + "Deprecated>=1.2.13", # Uses MIT-License (MIT compatible) + "torch>=1.10.0", # Uses BSD-License (MIT compatible) + "python-dotenv>=0.19.2", # Uses BSD-License (MIT compatible) + "pacfish>=0.4.4", # Uses BSD-License (MIT compatible) + "requests>=2.26.0", # Uses Apache 2.0-License (MIT compatible) + "wget>=3.2", # Is Public Domain (MIT compatible) + "jdata>=0.5.2", # Uses Apache 2.0-License (MIT compatible) + "pre-commit>=3.2.2", # Uses MIT-License (MIT compatible) + "PyWavelets", # Uses MIT-License (MIT compatible) +] -packages = [ - { include = "simpa" }, - { include = "simpa_tests" }, +[project.optional-dependencies] +docs = [ + "sphinx-rtd-theme>=2.0.0,<3.0.0", + "Sphinx>=5.1.1,<6.0.0", + "myst-parser>=0.18.0,<1.1" +] +profile = [ + "pytorch_memlab>=0.3.0,<0.4.0", + "line_profiler>=4.0.0,<5.0.0", + "memory_profiler>=0.61.0,<0.62.0" ] -# Requirements -[tool.poetry.dependencies] -python = ">=3.8" -matplotlib = ">=3.5.0" # Uses PSF-License (MIT compatible) -numpy = ">=1.21.4" # Uses BSD-License (MIT compatible) -scipy = ">=1.7.2" # Uses BSD-like-License (MIT compatible) -pynrrd = ">=0.4.2" # Uses MIT-License (MIT compatible) -scikit-image = ">=0.18.3" # Uses BSD-License (MIT compatible) -xmltodict = ">=0.12.0" # Uses MIT-License (MIT compatible) -h5py = ">=3.6.0" # Uses BSD-License (MIT compatible) -pandas = ">=1.3.4" # Uses BSD-License (MIT compatible) -coverage = ">=6.1.2" # Uses Apache 2.0-License (MIT compatible) -Deprecated = ">=1.2.13" # Uses MIT-License (MIT compatible) -torch = ">=1.10.0" # Uses BSD-License (MIT compatible) -python-dotenv = ">=0.19.2" # Uses BSD-License (MIT compatible) -pacfish = ">=0.4.4" # Uses BSD-License (MIT compatible) -requests = ">=2.26.0" # Uses Apache 2.0-License (MIT compatible) -wget = ">=3.2" # Is Public Domain (MIT compatible) -jdata = ">=0.5.2" # Uses Apache 2.0-License (MIT compatible) -bjdata = ">=0.4.1" # Uses Apache 2.0-License (MIT compatible) -pre-commit = ">=3.2.2" # Uses MIT-License (MIT compatible) +[project.urls] +Homepage = "https://github.com/IMSY-DKFZ/simpa" +Documentation = "https://simpa.readthedocs.io/en/main/" +Repository = "https://github.com/IMSY-DKFZ/simpa" -[tool.poetry.group.docs.dependencies] -sphinx-rtd-theme = "^1.0.0" -Sphinx = "^5.1.1" -myst-parser = "0.18.0, <1.1" +[tool.setuptools.packages.find] +include = ["simpa", "simpa_tests"] -# autopep8 config -[tool.autopep8] -max_line_length = 120 +[tool.setuptools_scm] -[build-system] -requires = [ - "poetry >= 0.12" -] -build-backend = "poetry.masonry.api" +[tool.autopep8] +max_line_length = 120 \ No newline at end of file diff --git a/simpa/core/__init__.py b/simpa/core/__init__.py index a2b9b34d..9a49b941 100644 --- a/simpa/core/__init__.py +++ b/simpa/core/__init__.py @@ -6,20 +6,22 @@ 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 SimulationModule: +class PipelineModule: """ - Defines a simulation module that is callable via the SIMPA core.simulation.simulate method. + 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): + 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): diff --git a/simpa/core/device_digital_twins/__init__.py b/simpa/core/device_digital_twins/__init__.py index 1ff6f7fb..13bd5bcf 100644 --- a/simpa/core/device_digital_twins/__init__.py +++ b/simpa/core/device_digital_twins/__init__.py @@ -2,20 +2,154 @@ # SPDX-FileCopyrightText: 2021 Janek Groehl # SPDX-License-Identifier: MIT -from .digital_device_twin_base import DigitalDeviceTwinBase -from .digital_device_twin_base import PhotoacousticDevice -from simpa.core.device_digital_twins.detection_geometries.detection_geometry_base import DetectionGeometryBase -from simpa.core.device_digital_twins.illumination_geometries.illumination_geometry_base import IlluminationGeometryBase -from .detection_geometries.curved_array import CurvedArrayDetectionGeometry -from .detection_geometries.linear_array import LinearArrayDetectionGeometry -from .detection_geometries.planar_array import PlanarArrayDetectionGeometry -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.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 +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 diff --git a/simpa/core/device_digital_twins/detection_geometries/__init__.py b/simpa/core/device_digital_twins/detection_geometries/__init__.py index 89cc8954..eba6bdd5 100644 --- a/simpa/core/device_digital_twins/detection_geometries/__init__.py +++ b/simpa/core/device_digital_twins/detection_geometries/__init__.py @@ -1,3 +1,136 @@ # 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 +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 diff --git a/simpa/core/device_digital_twins/detection_geometries/curved_array.py b/simpa/core/device_digital_twins/detection_geometries/curved_array.py index 5b55a2b2..c530e4f7 100644 --- a/simpa/core/device_digital_twins/detection_geometries/curved_array.py +++ b/simpa/core/device_digital_twins/detection_geometries/curved_array.py @@ -77,7 +77,8 @@ def check_settings_prerequisites(self, global_settings) -> bool: if global_settings[Tags.DIM_VOLUME_X_MM] < (self.probe_width_mm + global_settings[Tags.SPACING_MM]): self.logger.error("Volume x dimension is too small to encompass MSOT device in simulation!" "Must be at least {} mm but was {} mm" - .format(self.probe_width_mm, global_settings[Tags.DIM_VOLUME_X_MM])) + .format(self.probe_width_mm + global_settings[Tags.SPACING_MM], + global_settings[Tags.DIM_VOLUME_X_MM])) return False return True diff --git a/simpa/core/device_digital_twins/detection_geometries/detection_geometry_base.py b/simpa/core/device_digital_twins/detection_geometries/detection_geometry_base.py deleted file mode 100644 index a1e63c56..00000000 --- a/simpa/core/device_digital_twins/detection_geometries/detection_geometry_base.py +++ /dev/null @@ -1,136 +0,0 @@ -# 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.digital_device_twin_base 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 diff --git a/simpa/core/device_digital_twins/digital_device_twin_base.py b/simpa/core/device_digital_twins/digital_device_twin_base.py deleted file mode 100644 index 56c90128..00000000 --- a/simpa/core/device_digital_twins/digital_device_twin_base.py +++ /dev/null @@ -1,296 +0,0 @@ -# SPDX-FileCopyrightText: 2021 Division of Intelligent Medical Systems, DKFZ -# SPDX-FileCopyrightText: 2021 Janek Groehl -# SPDX-License-Identifier: MIT - -from abc import abstractmethod, ABC -from simpa.log import Logger -from simpa.utils import Settings -import numpy as np -from numpy import ndarray -import hashlib -import uuid -from simpa.utils.serializer import SerializableSIMPAClass - - -def is_equal(a, b) -> bool: - if isinstance(a, list): - return all(is_equal(ai, bi) for ai, bi in zip(a, b)) - elif isinstance(a, np.ndarray): - return (a == b).all - else: - return a == b - - -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 is_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 - - -class PhotoacousticDevice(DigitalDeviceTwinBase, ABC): - """Base class of a photoacoustic device. It consists of one detection geometry that describes the geometry of the - single detector elements and a list of illuminators. - - A Photoacoustic Device can be initialized as follows:: - - import simpa as sp - import numpy as np - - # Initialise a PhotoacousticDevice with its position and field of view - device = sp.PhotoacousticDevice(device_position_mm=np.array([10, 10, 0]), - field_of_view_extent_mm=np.array([-20, 20, 0, 0, 0, 20])) - - # Option 1) Set the detection geometry position relative to the PhotoacousticDevice - device.set_detection_geometry(sp.DetectionGeometry(), - detector_position_relative_to_pa_device=np.array([0, 0, -10])) - - # Option 2) Set the detection geometry position absolute - device.set_detection_geometry( - sp.DetectionGeometryBase(device_position_mm=np.array([10, 10, -10]))) - - # Option 1) Add the illumination geometry position relative to the PhotoacousticDevice - device.add_illumination_geometry(sp.IlluminationGeometry(), - illuminator_position_relative_to_pa_device=np.array([0, 0, 0])) - - # Option 2) Add the illumination geometry position absolute - device.add_illumination_geometry( - sp.IlluminationGeometryBase(device_position_mm=np.array([10, 10, 0])) - - Attributes: - detection_geometry (DetectionGeometryBase): Geometry of the detector elements. - illumination_geometries (list): List of illuminations defined by :py:class:`IlluminationGeometryBase`. - """ - - 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 - """ - super(PhotoacousticDevice, self).__init__(device_position_mm=device_position_mm, - field_of_view_extent_mm=field_of_view_extent_mm) - self.detection_geometry = None - self.illumination_geometries = [] - - def set_detection_geometry(self, detection_geometry, - detector_position_relative_to_pa_device=None): - """Sets the detection geometry for the PA device. The detection geometry can be instantiated with an absolute - position or it can be instantiated without the device_position_mm argument but a position relative to the - position of the PhotoacousticDevice. If both absolute and relative positions are given, the absolute position - is chosen as position of the detection geometry. - - :param detection_geometry: Detection geometry of the PA device. - :type detection_geometry: DetectionGeometryBase - :param detector_position_relative_to_pa_device: Position of the detection geometry relative to the PA device. - :type detector_position_relative_to_pa_device: ndarray - :raises ValueError: if the detection_geometry is None - - """ - if detection_geometry is None: - msg = "The given detection_geometry must not be None!" - self.logger.critical(msg) - raise ValueError(msg) - if np.linalg.norm(detection_geometry.device_position_mm) == 0 and \ - detector_position_relative_to_pa_device is not None: - detection_geometry.device_position_mm = np.add(self.device_position_mm, - detector_position_relative_to_pa_device) - self.detection_geometry = detection_geometry - - def add_illumination_geometry(self, illumination_geometry, illuminator_position_relative_to_pa_device=None): - """Adds an illuminator to the PA device. The illumination geometry can be instantiated with an absolute - position or it can be instantiated without the device_position_mm argument but a position relative to the - position of the PhotoacousticDevice. If both absolute and relative positions are given, the absolute position - is chosen as position of the illumination geometry. - - :param illumination_geometry: Geometry of the illuminator. - :type illumination_geometry: IlluminationGeometryBase - :param illuminator_position_relative_to_pa_device: Position of the illuminator relative to the PA device. - :type illuminator_position_relative_to_pa_device: ndarray - :raises ValueError: if the illumination_geometry is None - - """ - if illumination_geometry is None: - msg = "The given illumination_geometry must not be None!" - self.logger.critical(msg) - raise ValueError(msg) - if np.linalg.norm(illumination_geometry.device_position_mm) == 0: - if illuminator_position_relative_to_pa_device is not None: - illumination_geometry.device_position_mm = np.add(self.device_position_mm, - illuminator_position_relative_to_pa_device) - else: - illumination_geometry.device_position_mm = self.device_position_mm - self.illumination_geometries.append(illumination_geometry) - - def get_detection_geometry(self): - """ - :return: None if no detection geometry was set or an instance of DetectionGeometryBase. - :rtype: None, DetectionGeometryBase - """ - return self.detection_geometry - - def get_illumination_geometry(self): - """ - :return: None, if no illumination geometry was defined, - an instance of IlluminationGeometryBase if exactly one geometry was defined, - a list of IlluminationGeometryBase instances if more than one device was defined. - :rtype: None, IlluminationGeometryBase - """ - if len(self.illumination_geometries) == 0: - return None - - if len(self.illumination_geometries) == 1: - return self.illumination_geometries[0] - - return self.illumination_geometries - - def check_settings_prerequisites(self, global_settings) -> bool: - _result = True - if self.detection_geometry is not None \ - and not self.detection_geometry.check_settings_prerequisites(global_settings): - _result = False - for illumination_geometry in self.illumination_geometries: - if illumination_geometry is not None \ - and not illumination_geometry.check_settings_prerequisites(global_settings): - _result = False - return _result - - def update_settings_for_use_of_model_based_volume_creator(self, global_settings): - pass - - def serialize(self) -> dict: - serialized_device = self.__dict__ - device_dict = {"PhotoacousticDevice": serialized_device} - return device_dict - - @staticmethod - def deserialize(dictionary_to_deserialize): - deserialized_device = PhotoacousticDevice( - device_position_mm=dictionary_to_deserialize["device_position_mm"], - field_of_view_extent_mm=dictionary_to_deserialize["field_of_view_extent_mm"]) - det_geometry = dictionary_to_deserialize["detection_geometry"] - if det_geometry != "None": - deserialized_device.set_detection_geometry(dictionary_to_deserialize["detection_geometry"]) - if "illumination_geometries" in dictionary_to_deserialize: - for illumination_geometry in dictionary_to_deserialize["illumination_geometries"]: - deserialized_device.illumination_geometries.append(illumination_geometry) - - return deserialized_device diff --git a/simpa/core/device_digital_twins/illumination_geometries/__init__.py b/simpa/core/device_digital_twins/illumination_geometries/__init__.py index 89cc8954..6c5780bf 100644 --- a/simpa/core/device_digital_twins/illumination_geometries/__init__.py +++ b/simpa/core/device_digital_twins/illumination_geometries/__init__.py @@ -1,3 +1,70 @@ # 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.utils import Settings +import numpy as np + + +class IlluminationGeometryBase(DigitalDeviceTwinBase): + """ + This class is the base class for representing all illumination geometries. + """ + + def __init__(self, device_position_mm=None, source_direction_vector=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 illuminator positions. + :type device_position_mm: ndarray + + :param source_direction_vector: Direction of the illumination source. + :type source_direction_vector: 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(IlluminationGeometryBase, self).__init__(device_position_mm=device_position_mm, + field_of_view_extent_mm=field_of_view_extent_mm) + + if source_direction_vector is None: + self.source_direction_vector = [0, 0, 1] + else: + self.source_direction_vector = source_direction_vector + self.normalized_source_direction_vector = self.source_direction_vector / np.linalg.norm( + self.source_direction_vector) + + @abstractmethod + def get_mcx_illuminator_definition(self, global_settings) -> dict: + """ + IMPORTANT: This method creates a dictionary that contains tags as they are expected for the + mcx simulation tool to represent the illumination geometry of this device. + + :param global_settings: The global_settings instance containing the simulation instructions. + :type global_settings: Settings + + :return: Dictionary that includes all parameters needed for mcx. + :rtype: dict + """ + pass + + def check_settings_prerequisites(self, global_settings) -> bool: + return True + + def update_settings_for_use_of_model_based_volume_creator(self, global_settings) -> Settings: + return global_settings + + def serialize(self) -> dict: + serialized_device = self.__dict__ + device_dict = {"IlluminationGeometryBase": serialized_device} + return device_dict + + @staticmethod + def deserialize(dictionary_to_deserialize): + deserialized_device = IlluminationGeometryBase() + for key, value in dictionary_to_deserialize.items(): + deserialized_device.__dict__[key] = value + return deserialized_device diff --git a/simpa/core/device_digital_twins/illumination_geometries/illumination_geometry_base.py b/simpa/core/device_digital_twins/illumination_geometries/illumination_geometry_base.py deleted file mode 100644 index 74224198..00000000 --- a/simpa/core/device_digital_twins/illumination_geometries/illumination_geometry_base.py +++ /dev/null @@ -1,71 +0,0 @@ -# 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.digital_device_twin_base import DigitalDeviceTwinBase -from simpa.utils import Settings -from numpy import ndarray -import numpy as np - - -class IlluminationGeometryBase(DigitalDeviceTwinBase): - """ - This class is the base class for representing all illumination geometries. - """ - - def __init__(self, device_position_mm=None, source_direction_vector=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 illuminator positions. - :type device_position_mm: ndarray - - :param source_direction_vector: Direction of the illumination source. - :type source_direction_vector: 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(IlluminationGeometryBase, self).__init__(device_position_mm=device_position_mm, - field_of_view_extent_mm=field_of_view_extent_mm) - - if source_direction_vector is None: - self.source_direction_vector = [0, 0, 1] - else: - self.source_direction_vector = source_direction_vector - self.normalized_source_direction_vector = self.source_direction_vector / np.linalg.norm( - self.source_direction_vector) - - @abstractmethod - def get_mcx_illuminator_definition(self, global_settings) -> dict: - """ - IMPORTANT: This method creates a dictionary that contains tags as they are expected for the - mcx simulation tool to represent the illumination geometry of this device. - - :param global_settings: The global_settings instance containing the simulation instructions. - :type global_settings: Settings - - :return: Dictionary that includes all parameters needed for mcx. - :rtype: dict - """ - pass - - def check_settings_prerequisites(self, global_settings) -> bool: - return True - - def update_settings_for_use_of_model_based_volume_creator(self, global_settings) -> Settings: - return global_settings - - def serialize(self) -> dict: - serialized_device = self.__dict__ - device_dict = {"IlluminationGeometryBase": serialized_device} - return device_dict - - @staticmethod - def deserialize(dictionary_to_deserialize): - deserialized_device = IlluminationGeometryBase() - for key, value in dictionary_to_deserialize.items(): - deserialized_device.__dict__[key] = value - return deserialized_device diff --git a/simpa/core/device_digital_twins/illumination_geometries/ithera_msot_invision_illumination.py b/simpa/core/device_digital_twins/illumination_geometries/ithera_msot_invision_illumination.py index 5713fb54..0c22a346 100644 --- a/simpa/core/device_digital_twins/illumination_geometries/ithera_msot_invision_illumination.py +++ b/simpa/core/device_digital_twins/illumination_geometries/ithera_msot_invision_illumination.py @@ -5,7 +5,7 @@ import numpy as np from simpa.core.device_digital_twins import IlluminationGeometryBase -from simpa.utils import Settings, Tags +from simpa.utils import Tags class MSOTInVisionIlluminationGeometry(IlluminationGeometryBase): diff --git a/simpa/core/device_digital_twins/illumination_geometries/pencil_array_illumination.py b/simpa/core/device_digital_twins/illumination_geometries/pencil_array_illumination.py index 0712c689..e5a4deab 100644 --- a/simpa/core/device_digital_twins/illumination_geometries/pencil_array_illumination.py +++ b/simpa/core/device_digital_twins/illumination_geometries/pencil_array_illumination.py @@ -5,7 +5,7 @@ import numpy as np from simpa.core.device_digital_twins import IlluminationGeometryBase -from simpa.utils import Settings, Tags +from simpa.utils import Tags class PencilArrayIlluminationGeometry(IlluminationGeometryBase): diff --git a/simpa/core/device_digital_twins/illumination_geometries/rectangle_illumination.py b/simpa/core/device_digital_twins/illumination_geometries/rectangle_illumination.py new file mode 100644 index 00000000..69bd3588 --- /dev/null +++ b/simpa/core/device_digital_twins/illumination_geometries/rectangle_illumination.py @@ -0,0 +1,115 @@ +# SPDX-FileCopyrightText: 2021 Division of Intelligent Medical Systems, DKFZ +# SPDX-FileCopyrightText: 2021 Janek Groehl +# SPDX-License-Identifier: MIT + +import typing + +from simpa.core.device_digital_twins import IlluminationGeometryBase +from simpa.utils import Settings, Tags +import numpy as np +from simpa.utils.serializer import SerializableSIMPAClass + + +class RectangleIlluminationGeometry(IlluminationGeometryBase): + """ + Defines a rectangle illumination geometry. + The device position is defined as the UPPER LEFT CORNER of the rectangle. + + Note: To create a global light which illuminates the entire tissue evenly (= creating a planar illumination geometry), + create the following geometry using the tissue length and width: + + >>> global_light = RectangleIlluminationGeometry(length_mm=tissue_length_mm, width_mm=tissue_width_mm) + """ + + def __init__(self, + length_mm: int = 10, + width_mm: int = 10, + device_position_mm: typing.Optional[np.ndarray] = None, + source_direction_vector: typing.Optional[np.ndarray] = None, + field_of_view_extent_mm: typing.Optional[np.ndarray] = None): + """ + :param length_mm: The length of the rectangle in mm. + :param width_mm: The width of the rectangle in mm. + :param device_position_mm: The device position in mm, the UPPER LEFT CORNER of the rectangle. + If None, the position is defined as [0, 0, 0]. + :param source_direction_vector: Direction of the illumination source. + If None, the direction is defined as [0, 0, 1]. + :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. + """ + if device_position_mm is None: + device_position_mm = np.zeros(3) + + if source_direction_vector is None: + source_direction_vector = np.array([0, 0, 1]) + + super(RectangleIlluminationGeometry, self).__init__(device_position_mm=device_position_mm, + source_direction_vector=source_direction_vector, + field_of_view_extent_mm=field_of_view_extent_mm) + + assert length_mm > 0 + assert width_mm > 0 + + self.length_mm = length_mm + self.width_mm = width_mm + + def get_mcx_illuminator_definition(self, global_settings: Settings) -> dict: + """ + Returns the illumination parameters for MCX simulations. + + :param global_settings: The global settings. + + :return: The illumination parameters as a dictionary. + """ + assert isinstance(global_settings, Settings), type(global_settings) + + source_type = Tags.ILLUMINATION_TYPE_PLANAR + + spacing = global_settings[Tags.SPACING_MM] + + device_position = list(np.rint(self.device_position_mm / spacing)) + + self.logger.debug(device_position) + + source_direction = list(self.normalized_source_direction_vector) + + source_param1 = [np.rint(self.width_mm / spacing) + 1, 0, 0] + source_param2 = [0, np.rint(self.length_mm / spacing) + 1, 0] + + # If Pos=[10, 12, 0], Param1=[10, 0, 0], Param2=[0, 20, 0], + # then illumination covers: x in [10, 20], y in [12, 32] + # (https://github.com/fangq/mcx/discussions/220) + return { + "Type": source_type, + "Pos": device_position, + "Dir": source_direction, + "Param1": source_param1, + "Param2": source_param2 + } + + def serialize(self) -> dict: + """ + Serializes the object into a dictionary. + + :return: The dictionary representing the serialized object. + """ + serialized_device = self.__dict__ + device_dict = {RectangleIlluminationGeometry.__name__: serialized_device} + return device_dict + + @staticmethod + def deserialize(dictionary_to_deserialize: dict) -> SerializableSIMPAClass: + """ + Deserializes the provided dict into an object of this type. + :param dictionary_to_deserialize: The dictionary to deserialize. + + :return: The deserialized object from the dictionary. + """ + assert isinstance(dictionary_to_deserialize, dict), type(dictionary_to_deserialize) + + deserialized_device = RectangleIlluminationGeometry() + for key, value in dictionary_to_deserialize.items(): + deserialized_device.__dict__[key] = value + + return deserialized_device diff --git a/simpa/core/device_digital_twins/illumination_geometries/ring_illumination.py b/simpa/core/device_digital_twins/illumination_geometries/ring_illumination.py new file mode 100644 index 00000000..042e198f --- /dev/null +++ b/simpa/core/device_digital_twins/illumination_geometries/ring_illumination.py @@ -0,0 +1,124 @@ +# SPDX-FileCopyrightText: 2021 Division of Intelligent Medical Systems, DKFZ +# SPDX-FileCopyrightText: 2021 Janek Groehl +# SPDX-License-Identifier: MIT + +import typing + +from simpa.core.device_digital_twins import IlluminationGeometryBase +from simpa.utils import Settings, Tags +import numpy as np +from simpa.utils.serializer import SerializableSIMPAClass + + +class RingIlluminationGeometry(IlluminationGeometryBase): + """ + Defines a ring illumination geometry. + The device position is defined as the center of the ring. + + Note: To create a ring light which illuminates a square tissue with the same center and any inner radius r, + create the following geometry using the tissue width: + + >>> ring_light = RingIlluminationGeometry(inner_radius_in_mm=r, + outer_radius_in_mm=tissue_width / 2., + device_position_mm=np.array([tissue_width / 2., tissue_width / 2., 0])) + """ + + def __init__(self, + outer_radius_in_mm: float = 1, + inner_radius_in_mm: float = 0, + lower_angular_bound: float = 0, + upper_angular_bound: float = 0, + device_position_mm: typing.Optional[np.ndarray] = None, + source_direction_vector: typing.Optional[np.ndarray] = None, + field_of_view_extent_mm: typing.Optional[np.ndarray] = None): + """ + :param outer_radius_in_mm: The outer radius of the ring in mm. + :param inner_radius_in_mm: The inner radius of the ring in mm. If 0, should match the disk illumination. + :param lower_angular_bound: The lower angular bound in radians. If both bounds are 0, than no bound is applied. + Note that the bound of 0 starts from the x-axis on the right side and is applied clockwise. + :param upper_angular_bound: The upper angular bound in radians. If both bounds are 0, than no bound is applied. + Note that the bound is applied clockwise in relation to the lower bound. + :param device_position_mm: The device position in mm, the center of the ring. + If None, the position is defined as [0, 0, 0]. + :param source_direction_vector: Direction of the illumination source. + If None, the direction is defined as [0, 0, 1]. + :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. + """ + if device_position_mm is None: + device_position_mm = np.zeros(3) + + if source_direction_vector is None: + source_direction_vector = np.array([0, 0, 1]) + + super(RingIlluminationGeometry, self).__init__(device_position_mm=device_position_mm, + source_direction_vector=source_direction_vector, + field_of_view_extent_mm=field_of_view_extent_mm) + + assert inner_radius_in_mm >= 0, f"The inner radius has to be 0 or positive, not {inner_radius_in_mm}!" + assert outer_radius_in_mm >= inner_radius_in_mm, \ + f"The outer radius ({outer_radius_in_mm}) has to be at least as large " \ + f"as the inner radius ({inner_radius_in_mm})!" + + assert lower_angular_bound >= 0, f"The lower angular bound has to be 0 or positive, not {lower_angular_bound}!" + assert upper_angular_bound >= lower_angular_bound, \ + f"The outer radius ({upper_angular_bound}) has to be at least as large " \ + f"as the inner radius ({lower_angular_bound})!" + + self.outer_radius_in_mm = outer_radius_in_mm + self.inner_radius_in_mm = inner_radius_in_mm + self.lower_angular_bound = lower_angular_bound + self.upper_angular_bound = upper_angular_bound + + def get_mcx_illuminator_definition(self, global_settings: Settings) -> dict: + """ + Returns the illumination parameters for MCX simulations. + :param global_settings: The global settings. + :return: The illumination parameters as a dictionary. + """ + assert isinstance(global_settings, Settings), type(global_settings) + + source_type = Tags.ILLUMINATION_TYPE_RING + + spacing = global_settings[Tags.SPACING_MM] + + device_position = list(self.device_position_mm / spacing + 1) # No need to round + + source_direction = list(self.normalized_source_direction_vector) + + source_param1 = [self.outer_radius_in_mm / spacing, + self.inner_radius_in_mm / spacing, + self.lower_angular_bound, + self.upper_angular_bound] + + return { + "Type": source_type, + "Pos": device_position, + "Dir": source_direction, + "Param1": source_param1 + } + + def serialize(self) -> dict: + """ + Serializes the object into a dictionary. + :return: The dictionary representing the serialized object. + """ + serialized_device = self.__dict__ + device_dict = {RingIlluminationGeometry.__name__: serialized_device} + return device_dict + + @staticmethod + def deserialize(dictionary_to_deserialize: dict) -> SerializableSIMPAClass: + """ + Deserializes the provided dict into an object of this type. + :param dictionary_to_deserialize: The dictionary to deserialize. + :return: The deserialized object from the dictionary. + """ + assert isinstance(dictionary_to_deserialize, dict), type(dictionary_to_deserialize) + + deserialized_device = RingIlluminationGeometry() + for key, value in dictionary_to_deserialize.items(): + deserialized_device.__dict__[key] = value + + return deserialized_device diff --git a/simpa/core/device_digital_twins/illumination_geometries/slit_illumination.py b/simpa/core/device_digital_twins/illumination_geometries/slit_illumination.py index f5303bba..4dc58eba 100644 --- a/simpa/core/device_digital_twins/illumination_geometries/slit_illumination.py +++ b/simpa/core/device_digital_twins/illumination_geometries/slit_illumination.py @@ -5,7 +5,7 @@ import numpy as np from simpa.core.device_digital_twins import IlluminationGeometryBase -from simpa.utils import Settings, Tags +from simpa.utils import Tags class SlitIlluminationGeometry(IlluminationGeometryBase): @@ -41,10 +41,6 @@ def __init__(self, slit_vector_mm=None, direction_vector_mm=None, device_positio direction_vector_mm = [0, 0, 1] self.slit_vector_mm = slit_vector_mm - direction_vector_mm[0] = direction_vector_mm[0] / np.linalg.norm(direction_vector_mm) - direction_vector_mm[1] = direction_vector_mm[1] / np.linalg.norm(direction_vector_mm) - direction_vector_mm[2] = direction_vector_mm[2] / np.linalg.norm(direction_vector_mm) - self.direction_vector_norm = direction_vector_mm def get_mcx_illuminator_definition(self, global_settings) -> dict: source_type = Tags.ILLUMINATION_TYPE_SLIT diff --git a/simpa/core/device_digital_twins/pa_devices/__init__.py b/simpa/core/device_digital_twins/pa_devices/__init__.py index 89cc8954..38b0d202 100644 --- a/simpa/core/device_digital_twins/pa_devices/__init__.py +++ b/simpa/core/device_digital_twins/pa_devices/__init__.py @@ -1,3 +1,161 @@ # SPDX-FileCopyrightText: 2021 Division of Intelligent Medical Systems, DKFZ # SPDX-FileCopyrightText: 2021 Janek Groehl # SPDX-License-Identifier: MIT + +import numpy as np +from abc import ABC +from simpa.core.device_digital_twins import DigitalDeviceTwinBase + + +class PhotoacousticDevice(DigitalDeviceTwinBase, ABC): + """Base class of a photoacoustic device. It consists of one detection geometry that describes the geometry of the + single detector elements and a list of illuminators. + + A Photoacoustic Device can be initialized as follows:: + + import simpa as sp + import numpy as np + + # Initialise a PhotoacousticDevice with its position and field of view + device = sp.PhotoacousticDevice(device_position_mm=np.array([10, 10, 0]), + field_of_view_extent_mm=np.array([-20, 20, 0, 0, 0, 20])) + + # Option 1) Set the detection geometry position relative to the PhotoacousticDevice + device.set_detection_geometry(sp.DetectionGeometry(), + detector_position_relative_to_pa_device=np.array([0, 0, -10])) + + # Option 2) Set the detection geometry position absolute + device.set_detection_geometry( + sp.DetectionGeometryBase(device_position_mm=np.array([10, 10, -10]))) + + # Option 1) Add the illumination geometry position relative to the PhotoacousticDevice + device.add_illumination_geometry(sp.IlluminationGeometry(), + illuminator_position_relative_to_pa_device=np.array([0, 0, 0])) + + # Option 2) Add the illumination geometry position absolute + device.add_illumination_geometry( + sp.IlluminationGeometryBase(device_position_mm=np.array([10, 10, 0])) + + Attributes: + detection_geometry (DetectionGeometryBase): Geometry of the detector elements. + illumination_geometries (list): List of illuminations defined by :py:class:`IlluminationGeometryBase`. + """ + + 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 + """ + super(PhotoacousticDevice, self).__init__(device_position_mm=device_position_mm, + field_of_view_extent_mm=field_of_view_extent_mm) + self.detection_geometry = None + self.illumination_geometries = [] + + def set_detection_geometry(self, detection_geometry, + detector_position_relative_to_pa_device=None): + """Sets the detection geometry for the PA device. The detection geometry can be instantiated with an absolute + position or it can be instantiated without the device_position_mm argument but a position relative to the + position of the PhotoacousticDevice. If both absolute and relative positions are given, the absolute position + is chosen as position of the detection geometry. + + :param detection_geometry: Detection geometry of the PA device. + :type detection_geometry: DetectionGeometryBase + :param detector_position_relative_to_pa_device: Position of the detection geometry relative to the PA device. + :type detector_position_relative_to_pa_device: ndarray + :raises ValueError: if the detection_geometry is None + + """ + if detection_geometry is None: + msg = "The given detection_geometry must not be None!" + self.logger.critical(msg) + raise ValueError(msg) + if np.linalg.norm(detection_geometry.device_position_mm) == 0 and \ + detector_position_relative_to_pa_device is not None: + detection_geometry.device_position_mm = np.add(self.device_position_mm, + detector_position_relative_to_pa_device) + self.detection_geometry = detection_geometry + + def add_illumination_geometry(self, illumination_geometry, illuminator_position_relative_to_pa_device=None): + """Adds an illuminator to the PA device. The illumination geometry can be instantiated with an absolute + position or it can be instantiated without the device_position_mm argument but a position relative to the + position of the PhotoacousticDevice. If both absolute and relative positions are given, the absolute position + is chosen as position of the illumination geometry. + + :param illumination_geometry: Geometry of the illuminator. + :type illumination_geometry: IlluminationGeometryBase + :param illuminator_position_relative_to_pa_device: Position of the illuminator relative to the PA device. + :type illuminator_position_relative_to_pa_device: ndarray + :raises ValueError: if the illumination_geometry is None + + """ + if illumination_geometry is None: + msg = "The given illumination_geometry must not be None!" + self.logger.critical(msg) + raise ValueError(msg) + if np.linalg.norm(illumination_geometry.device_position_mm) == 0: + if illuminator_position_relative_to_pa_device is not None: + illumination_geometry.device_position_mm = np.add(self.device_position_mm, + illuminator_position_relative_to_pa_device) + else: + illumination_geometry.device_position_mm = self.device_position_mm + self.illumination_geometries.append(illumination_geometry) + + def get_detection_geometry(self): + """ + :return: None if no detection geometry was set or an instance of DetectionGeometryBase. + :rtype: None, DetectionGeometryBase + """ + return self.detection_geometry + + def get_illumination_geometry(self): + """ + :return: None, if no illumination geometry was defined, + an instance of IlluminationGeometryBase if exactly one geometry was defined, + a list of IlluminationGeometryBase instances if more than one device was defined. + :rtype: None, IlluminationGeometryBase + """ + if len(self.illumination_geometries) == 0: + return None + + if len(self.illumination_geometries) == 1: + return self.illumination_geometries[0] + + return self.illumination_geometries + + def check_settings_prerequisites(self, global_settings) -> bool: + _result = True + if self.detection_geometry is not None \ + and not self.detection_geometry.check_settings_prerequisites(global_settings): + _result = False + for illumination_geometry in self.illumination_geometries: + if illumination_geometry is not None \ + and not illumination_geometry.check_settings_prerequisites(global_settings): + _result = False + return _result + + def update_settings_for_use_of_model_based_volume_creator(self, global_settings): + pass + + def serialize(self) -> dict: + serialized_device = self.__dict__ + device_dict = {"PhotoacousticDevice": serialized_device} + return device_dict + + @staticmethod + def deserialize(dictionary_to_deserialize): + deserialized_device = PhotoacousticDevice( + device_position_mm=dictionary_to_deserialize["device_position_mm"], + field_of_view_extent_mm=dictionary_to_deserialize["field_of_view_extent_mm"]) + det_geometry = dictionary_to_deserialize["detection_geometry"] + if det_geometry != "None": + deserialized_device.set_detection_geometry(dictionary_to_deserialize["detection_geometry"]) + if "illumination_geometries" in dictionary_to_deserialize: + for illumination_geometry in dictionary_to_deserialize["illumination_geometries"]: + deserialized_device.illumination_geometries.append(illumination_geometry) + + return deserialized_device diff --git a/simpa/core/device_digital_twins/pa_devices/ithera_msot_acuity.py b/simpa/core/device_digital_twins/pa_devices/ithera_msot_acuity.py index a0bdb545..091c9396 100644 --- a/simpa/core/device_digital_twins/pa_devices/ithera_msot_acuity.py +++ b/simpa/core/device_digital_twins/pa_devices/ithera_msot_acuity.py @@ -4,6 +4,9 @@ from simpa.core.device_digital_twins import PhotoacousticDevice, \ CurvedArrayDetectionGeometry, MSOTAcuityIlluminationGeometry + +from simpa.core.device_digital_twins.pa_devices import PhotoacousticDevice +from simpa.core.device_digital_twins.detection_geometries.curved_array import CurvedArrayDetectionGeometry from simpa.utils.settings import Settings from simpa.utils import Tags from simpa.utils.libraries.tissue_library import TISSUE_LIBRARY diff --git a/simpa/core/processing_components/__init__.py b/simpa/core/processing_components/__init__.py index 8c27b88c..881a8f2d 100644 --- a/simpa/core/processing_components/__init__.py +++ b/simpa/core/processing_components/__init__.py @@ -3,13 +3,12 @@ # SPDX-License-Identifier: MIT from abc import ABC -from simpa.core import SimulationModule -from simpa.utils.processing_device import get_processing_device +from simpa.core import PipelineModule -class ProcessingComponent(SimulationModule, ABC): +class ProcessingComponent(PipelineModule, ABC): """ - Defines a simulation component, which can be used to pre- or post-process simulation data. + Defines a pipeline processing component, which can be used to pre- or post-process simulation data. """ def __init__(self, global_settings, component_settings_key: str): @@ -20,4 +19,3 @@ def __init__(self, global_settings, component_settings_key: str): """ super(ProcessingComponent, self).__init__(global_settings=global_settings) self.component_settings = global_settings[component_settings_key] - self.torch_device = get_processing_device(global_settings) diff --git a/simpa/core/simulation.py b/simpa/core/simulation.py index ce53cc45..e19a884d 100644 --- a/simpa/core/simulation.py +++ b/simpa/core/simulation.py @@ -7,9 +7,8 @@ from simpa.io_handling.ipasc import export_to_ipasc from simpa.utils.settings import Settings from simpa.log import Logger -from .device_digital_twins.digital_device_twin_base import DigitalDeviceTwinBase +from .device_digital_twins import DigitalDeviceTwinBase -from pathlib import Path import numpy as np import os import time diff --git a/simpa/core/simulation_modules/__init__.py b/simpa/core/simulation_modules/__init__.py index 89cc8954..c87e1e26 100644 --- a/simpa/core/simulation_modules/__init__.py +++ b/simpa/core/simulation_modules/__init__.py @@ -1,3 +1,32 @@ # 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 import PipelineModule +from simpa.utils import Settings + + +class SimulationModule(PipelineModule): + """ + Defines a simulation module that is a step in the simulation pipeline. + Each simulation module can only be one of Volume Creation, Light Propagation Modeling, Acoustic Wave Propagation Modeling, Image Reconstruction. + """ + + def __init__(self, global_settings: Settings): + """ + :param global_settings: The SIMPA settings dictionary + :type global_settings: Settings + """ + super(SimulationModule, self).__init__(global_settings=global_settings) + self.component_settings = self.load_component_settings() + if self.component_settings is None: + raise ValueError("The component settings should not be None at this point") + + @abstractmethod + def load_component_settings(self) -> Settings: + """ + :return: Loads component settings corresponding to this simulation component + """ + pass diff --git a/simpa/core/simulation_modules/acoustic_forward_module/__init__.py b/simpa/core/simulation_modules/acoustic_forward_module/__init__.py index b88a63b8..e9f33d10 100644 --- a/simpa/core/simulation_modules/acoustic_forward_module/__init__.py +++ b/simpa/core/simulation_modules/acoustic_forward_module/__init__.py @@ -4,7 +4,7 @@ from abc import abstractmethod import numpy as np -from simpa.core import SimulationModule +from simpa.core.simulation_modules import SimulationModule from simpa.utils import Tags, Settings from simpa.io_handling.io_hdf5 import save_hdf5 from simpa.utils.dict_path_manager import generate_dict_path @@ -33,7 +33,13 @@ class AcousticForwardModelBaseAdapter(SimulationModule): def __init__(self, global_settings: Settings): super(AcousticForwardModelBaseAdapter, self).__init__(global_settings=global_settings) - self.component_settings = global_settings.get_acoustic_settings() + + def load_component_settings(self) -> Settings: + """Implements abstract method to serve acoustic settings as component settings + + :return: Settings: acoustic component settings + """ + return self.global_settings.get_acoustic_settings() @abstractmethod def forward_model(self, detection_geometry) -> np.ndarray: diff --git a/simpa/core/simulation_modules/acoustic_forward_module/acoustic_forward_module_k_wave_adapter.py b/simpa/core/simulation_modules/acoustic_forward_module/acoustic_forward_module_k_wave_adapter.py index 109ff596..7bb4e2b1 100644 --- a/simpa/core/simulation_modules/acoustic_forward_module/acoustic_forward_module_k_wave_adapter.py +++ b/simpa/core/simulation_modules/acoustic_forward_module/acoustic_forward_module_k_wave_adapter.py @@ -110,14 +110,10 @@ def forward_model(self, detection_geometry: DetectionGeometryBase) -> np.ndarray axes = (0, 2) image_slice = np.s_[:] - data_dict[Tags.DATA_FIELD_SPEED_OF_SOUND] = np.rot90(data_dict[Tags.DATA_FIELD_SPEED_OF_SOUND][image_slice], - 3, axes=axes) - data_dict[Tags.DATA_FIELD_DENSITY] = np.rot90(data_dict[Tags.DATA_FIELD_DENSITY][image_slice], - 3, axes=axes) - data_dict[Tags.DATA_FIELD_ALPHA_COEFF] = np.rot90(data_dict[Tags.DATA_FIELD_ALPHA_COEFF][image_slice], - 3, axes=axes) - data_dict[Tags.DATA_FIELD_INITIAL_PRESSURE] = np.rot90(data_dict[Tags.DATA_FIELD_INITIAL_PRESSURE] - [image_slice], 3, axes=axes) + data_dict[Tags.DATA_FIELD_SPEED_OF_SOUND] = data_dict[Tags.DATA_FIELD_SPEED_OF_SOUND][image_slice].T + data_dict[Tags.DATA_FIELD_DENSITY] = data_dict[Tags.DATA_FIELD_DENSITY][image_slice].T + data_dict[Tags.DATA_FIELD_ALPHA_COEFF] = data_dict[Tags.DATA_FIELD_ALPHA_COEFF][image_slice].T + data_dict[Tags.DATA_FIELD_INITIAL_PRESSURE] = data_dict[Tags.DATA_FIELD_INITIAL_PRESSURE][image_slice].T time_series_data, global_settings = self.k_wave_acoustic_forward_model( detection_geometry, @@ -249,10 +245,6 @@ def k_wave_acoustic_forward_model(self, detection_geometry: DetectionGeometryBas subprocess.run(cmd) raw_time_series_data = sio.loadmat(optical_path)[Tags.DATA_FIELD_TIME_SERIES_DATA] - - # reverse the order of detector elements from matlab to python order - raw_time_series_data = raw_time_series_data[::-1, :] - time_grid = sio.loadmat(optical_path + "dt.mat") num_time_steps = int(np.round(time_grid["number_time_steps"])) diff --git a/simpa/core/simulation_modules/optical_simulation_module/__init__.py b/simpa/core/simulation_modules/optical_simulation_module/__init__.py index 54b87111..29249b86 100644 --- a/simpa/core/simulation_modules/optical_simulation_module/__init__.py +++ b/simpa/core/simulation_modules/optical_simulation_module/__init__.py @@ -6,7 +6,7 @@ import numpy as np -from simpa.core import SimulationModule +from simpa.core.simulation_modules import SimulationModule from simpa.core.device_digital_twins import (IlluminationGeometryBase, PhotoacousticDevice) from simpa.io_handling.io_hdf5 import load_data_field, save_hdf5 @@ -25,12 +25,18 @@ class OpticalForwardModuleBase(SimulationModule): def __init__(self, global_settings: Settings): super(OpticalForwardModuleBase, self).__init__(global_settings=global_settings) - self.component_settings = self.global_settings.get_optical_settings() self.nx = None self.ny = None self.nz = None self.temporary_output_files = [] + def load_component_settings(self) -> Settings: + """Implements abstract method to serve optical settings as component settings + + :return: Settings: optical component settings + """ + return self.global_settings.get_optical_settings() + @abstractmethod def forward_model(self, absorption_cm: np.ndarray, diff --git a/simpa/core/simulation_modules/optical_simulation_module/optical_forward_model_mcx_adapter.py b/simpa/core/simulation_modules/optical_simulation_module/optical_forward_model_mcx_adapter.py index cab67a02..682f2985 100644 --- a/simpa/core/simulation_modules/optical_simulation_module/optical_forward_model_mcx_adapter.py +++ b/simpa/core/simulation_modules/optical_simulation_module/optical_forward_model_mcx_adapter.py @@ -6,7 +6,7 @@ import subprocess from simpa.utils import Tags, Settings from simpa.core.simulation_modules.optical_simulation_module import OpticalForwardModuleBase -from simpa.core.device_digital_twins.illumination_geometries.illumination_geometry_base import IlluminationGeometryBase +from simpa.core.device_digital_twins.illumination_geometries import IlluminationGeometryBase import json import jdata import os @@ -36,7 +36,7 @@ def __init__(self, global_settings: Settings): self.mcx_json_config_file = None self.mcx_volumetric_data_file = None self.frames = None - self.mcx_output_suffixes = {'mcx_volumetric_data_file': '.bnii'} + self.mcx_output_suffixes = {'mcx_volumetric_data_file': '.jnii'} def forward_model(self, absorption_cm: np.ndarray, diff --git a/simpa/core/simulation_modules/optical_simulation_module/optical_forward_model_mcx_reflectance_adapter.py b/simpa/core/simulation_modules/optical_simulation_module/optical_forward_model_mcx_reflectance_adapter.py index 82267de4..814d631a 100644 --- a/simpa/core/simulation_modules/optical_simulation_module/optical_forward_model_mcx_reflectance_adapter.py +++ b/simpa/core/simulation_modules/optical_simulation_module/optical_forward_model_mcx_reflectance_adapter.py @@ -1,6 +1,8 @@ # SPDX-FileCopyrightText: 2021 Division of Intelligent Medical Systems, DKFZ # SPDX-FileCopyrightText: 2021 Janek Groehl # SPDX-License-Identifier: MIT +import typing + import numpy as np import jdata import os @@ -37,7 +39,8 @@ def __init__(self, global_settings: Settings): super(MCXAdapterReflectance, self).__init__(global_settings=global_settings) self.mcx_photon_data_file = None self.padded = None - self.mcx_output_suffixes = {'mcx_volumetric_data_file': '.bnii', + self.volume_boundary_condition_str = global_settings[Tags.VOLUME_BOUNDARY_BONDITION] + self.mcx_output_suffixes = {'mcx_volumetric_data_file': '.jnii', 'mcx_photon_data_file': '_detp.jdat'} def forward_model(self, @@ -83,23 +86,44 @@ def forward_model(self, self.remove_mcx_output() return results - def get_command(self, bc="aaaaaa000010") -> List: - """ - generates list of commands to be parse to MCX in a subprocess + def get_command(self) -> typing.List: + """Generates list of commands to be parse to MCX in a subprocess. :return: list of MCX commands """ - cmd = super().get_command(bc=bc) - if Tags.COMPUTE_PHOTON_DIRECTION_AT_EXIT in self.component_settings and \ - self.component_settings[Tags.COMPUTE_PHOTON_DIRECTION_AT_EXIT]: - cmd.append("-H") - cmd.append(f"{int(self.component_settings[Tags.OPTICAL_MODEL_NUMBER_PHOTONS])}") + cmd = list() + cmd.append(self.component_settings[Tags.OPTICAL_MODEL_BINARY_PATH]) + cmd.append("-f") + cmd.append(self.mcx_json_config_file) + cmd.append("-O") + cmd.append("F") + # use 'C' order array format for binary input file + cmd.append("-a") + cmd.append("1") + cmd.append("-F") + cmd.append("jnii") + + if ( + Tags.COMPUTE_PHOTON_DIRECTION_AT_EXIT in self.component_settings + and self.component_settings[Tags.COMPUTE_PHOTON_DIRECTION_AT_EXIT] + ): + # FIXME + raise NotImplementedError("Does not work with volume boundary condition") cmd.append("--savedetflag") cmd.append("XV") - if Tags.COMPUTE_DIFFUSE_REFLECTANCE in self.component_settings and \ - self.component_settings[Tags.COMPUTE_DIFFUSE_REFLECTANCE]: - cmd.append("--saveref") # save diffuse reflectance at 0 filled voxels outside of domain - cmd.append("1") + + if ( + Tags.COMPUTE_DIFFUSE_REFLECTANCE in self.component_settings + and self.component_settings[Tags.COMPUTE_DIFFUSE_REFLECTANCE] + ): + cmd.append("-H") + cmd.append( + f"{int(self.component_settings[Tags.OPTICAL_MODEL_NUMBER_PHOTONS])}" + ) + cmd.append("--bc") # save photon exit position and direction + cmd.append(self.volume_boundary_condition_str) + cmd.append("--saveref") + return cmd def read_mcx_output(self, **kwargs) -> Dict: @@ -122,7 +146,7 @@ def read_mcx_output(self, **kwargs) -> Dict: fluence *= 100 # Convert from J/mm^2 to J/cm^2 results[Tags.DATA_FIELD_FLUENCE] = fluence else: - raise FileNotFoundError(f"Could not find .bnii file for {self.mcx_volumetric_data_file}") + raise FileNotFoundError(f"Could not find .jnii file for {self.mcx_volumetric_data_file}") if Tags.COMPUTE_DIFFUSE_REFLECTANCE in self.component_settings and \ self.component_settings[Tags.COMPUTE_DIFFUSE_REFLECTANCE]: results[Tags.DATA_FIELD_DIFFUSE_REFLECTANCE] = ref diff --git a/simpa/core/simulation_modules/optical_simulation_module/volume_boundary_condition.py b/simpa/core/simulation_modules/optical_simulation_module/volume_boundary_condition.py new file mode 100644 index 00000000..3955fd1e --- /dev/null +++ b/simpa/core/simulation_modules/optical_simulation_module/volume_boundary_condition.py @@ -0,0 +1,29 @@ +# SPDX-FileCopyrightText: 2021 Division of Intelligent Medical Systems, DKFZ +# SPDX-FileCopyrightText: 2021 Janek Groehl +# SPDX-License-Identifier: MIT + +from enum import Enum + + +# region Public enums + +class MCXVolumeBoundaryCondition(Enum): + """Defines the behavior of the photons touching the volume boundaries. + + Sets the letters of the --bc command (https://mcx.space/wiki/index.cgi?Doc/mcx_help). + Note: The behavior is only defined on the volume faces in the x- and y-axis, the behavior for the faces + on the z-axis always remains the default. + """ + + DEFAULT = "______000000" + """The default behavior.""" + MIRROR_REFLECTION = "mm_mm_000000" + """The photons are totally reflected as if the volume faces are mirrors.""" + CYCLIC = "cc_cc_000000" + """The photons reenter from the opposite volume face.""" + ABSORB = "aa_aa_000000" + """The photons are fully absorbed.""" + FRESNEL_REFLECTION = "rr_rr_000000" + """The photons are reflected based on the Fresnel equations.""" + +# endregion diff --git a/simpa/core/simulation_modules/reconstruction_module/__init__.py b/simpa/core/simulation_modules/reconstruction_module/__init__.py index 53dc8d05..05f8b3ef 100644 --- a/simpa/core/simulation_modules/reconstruction_module/__init__.py +++ b/simpa/core/simulation_modules/reconstruction_module/__init__.py @@ -7,7 +7,7 @@ from simpa.core.device_digital_twins import PhotoacousticDevice from simpa.io_handling.io_hdf5 import load_data_field from abc import abstractmethod -from simpa.core import SimulationModule +from simpa.core.simulation_modules import SimulationModule from simpa.utils.dict_path_manager import generate_dict_path from simpa.io_handling.io_hdf5 import save_hdf5 import numpy as np @@ -25,7 +25,13 @@ class ReconstructionAdapterBase(SimulationModule): def __init__(self, global_settings: Settings): super(ReconstructionAdapterBase, self).__init__(global_settings=global_settings) - self.component_settings = global_settings.get_reconstruction_settings() + + def load_component_settings(self) -> Settings: + """Implements abstract method to serve reconstruction settings as component settings + + :return: Settings: reconstruction component settings + """ + return self.global_settings.get_reconstruction_settings() @abstractmethod def reconstruction_algorithm(self, time_series_sensor_data, @@ -110,7 +116,7 @@ def create_reconstruction_settings(speed_of_sound_in_m_per_s: int = 1540, time_s Tags.SPACING_MM: sensor_spacing_in_mm, Tags.RECONSTRUCTION_APODIZATION_METHOD: apodization, Tags.RECONSTRUCTION_MODE: recon_mode, - Tags.SENSOR_SAMPLING_RATE_MHZ: (1.0 / time_spacing_in_s) / 1000000 }) + settings[Tags.K_WAVE_SPECIFIC_DT] = time_spacing_in_s return settings diff --git a/simpa/core/simulation_modules/reconstruction_module/reconstruction_module_time_reversal_adapter.py b/simpa/core/simulation_modules/reconstruction_module/reconstruction_module_time_reversal_adapter.py index 5472d9f1..6b4bd43f 100644 --- a/simpa/core/simulation_modules/reconstruction_module/reconstruction_module_time_reversal_adapter.py +++ b/simpa/core/simulation_modules/reconstruction_module/reconstruction_module_time_reversal_adapter.py @@ -178,7 +178,7 @@ def reconstruction_algorithm(self, time_series_sensor_data, detection_geometry): reconstructed_data = sio.loadmat(acoustic_path + "tr.mat")[Tags.DATA_FIELD_RECONSTRUCTED_DATA] - reconstructed_data = np.flipud(np.rot90(reconstructed_data, 1, axes)) + reconstructed_data = reconstructed_data.T field_of_view_mm = detection_geometry.get_field_of_view_mm() field_of_view_voxels = (field_of_view_mm / spacing_in_mm).astype(np.int32) diff --git a/simpa/core/simulation_modules/volume_creation_module/__init__.py b/simpa/core/simulation_modules/volume_creation_module/__init__.py index 0b6b7f6f..6b9f0753 100644 --- a/simpa/core/simulation_modules/volume_creation_module/__init__.py +++ b/simpa/core/simulation_modules/volume_creation_module/__init__.py @@ -7,11 +7,9 @@ from simpa.utils import Tags from simpa.utils.constants import wavelength_independent_properties, property_tags import torch -from simpa.core import SimulationModule +from simpa.core.simulation_modules import SimulationModule from simpa.io_handling import save_data_field from simpa.utils.quality_assurance.data_sanity_testing import assert_equal_shapes, assert_array_well_defined -from simpa.utils.processing_device import get_processing_device -from simpa.utils.constants import wavelength_independent_properties class VolumeCreatorModuleBase(SimulationModule): @@ -22,8 +20,13 @@ class VolumeCreatorModuleBase(SimulationModule): def __init__(self, global_settings: Settings): super(VolumeCreatorModuleBase, self).__init__(global_settings=global_settings) - self.component_settings = global_settings.get_volume_creation_settings() - self.torch_device = get_processing_device(self.global_settings) + + def load_component_settings(self) -> Settings: + """Implements abstract method to serve volume creation settings as component settings + + :return: Settings: volume creation component settings + """ + return self.global_settings.get_volume_creation_settings() def create_empty_volumes(self): volumes = dict() diff --git a/simpa/core/simulation_modules/volume_creation_module/volume_creation_module_segmentation_based_adapter.py b/simpa/core/simulation_modules/volume_creation_module/volume_creation_module_segmentation_based_adapter.py index f766b109..490ce2eb 100644 --- a/simpa/core/simulation_modules/volume_creation_module/volume_creation_module_segmentation_based_adapter.py +++ b/simpa/core/simulation_modules/volume_creation_module/volume_creation_module_segmentation_based_adapter.py @@ -7,6 +7,7 @@ from simpa.utils.constants import property_tags from simpa.io_handling import save_hdf5 import numpy as np +import torch class SegmentationBasedVolumeCreationAdapter(VolumeCreatorModuleBase): @@ -42,8 +43,15 @@ def create_simulation_volume(self) -> dict: for seg_class in segmentation_classes: class_properties = class_mapping[seg_class].get_properties_for_wavelength(wavelength) for prop_tag in property_tags: - volumes[prop_tag][segmentation_volume == seg_class] = class_properties[prop_tag] + assigned_prop = class_properties[prop_tag] + if assigned_prop is None: + assigned_prop = torch.nan + volumes[prop_tag][segmentation_volume == seg_class] = assigned_prop save_hdf5(self.global_settings, self.global_settings[Tags.SIMPA_OUTPUT_PATH], "/settings/") + # convert volumes back to CPU + for key in volumes.keys(): + volumes[key] = volumes[key].cpu().numpy().astype(np.float64, copy=False) + return volumes diff --git a/simpa/utils/calculate.py b/simpa/utils/calculate.py index 3533f4bd..8f4aee4f 100644 --- a/simpa/utils/calculate.py +++ b/simpa/utils/calculate.py @@ -3,16 +3,24 @@ # SPDX-License-Identifier: MIT +from typing import Union import numpy as np +import torch from scipy.interpolate import interp1d -def calculate_oxygenation(molecule_list): +def calculate_oxygenation(molecule_list: list) -> Union[float, int, torch.Tensor]: """ - :return: an oxygenation value between 0 and 1 if possible, or None, if not computable. + Calculate the oxygenation level based on the volume fractions of deoxyhaemoglobin and oxyhaemoglobin. + + This function takes a list of molecules and returns an oxygenation value between 0 and 1 if computable, + otherwise returns None. + + :param molecule_list: List of molecules with their spectrum information and volume fractions. + :return: An oxygenation value between 0 and 1 if possible, or None if not computable. """ - hb = None - hbO2 = None + hb = None # Volume fraction of deoxyhaemoglobin + hbO2 = None # Volume fraction of oxyhaemoglobin for molecule in molecule_list: if molecule.absorption_spectrum.spectrum_name == "Deoxyhemoglobin": @@ -34,7 +42,8 @@ def calculate_oxygenation(molecule_list): return hbO2 / (hb + hbO2) -def create_spline_for_range(xmin_mm=0, xmax_mm=10, maximum_y_elevation_mm=1, spacing=0.1): +def create_spline_for_range(xmin_mm: Union[float, int] = 0, xmax_mm: Union[float, int] = 10, + maximum_y_elevation_mm: Union[float, int] = 1, spacing: Union[float, int] = 0.1) -> tuple: """ Creates a functional that simulates distortion along the y position between the minimum and maximum x positions. The elevation can never be @@ -43,6 +52,7 @@ def create_spline_for_range(xmin_mm=0, xmax_mm=10, maximum_y_elevation_mm=1, spa :param xmin_mm: the minimum x axis value the return functional is defined in :param xmax_mm: the maximum x axis value the return functional is defined in :param maximum_y_elevation_mm: the maximum y axis value the return functional will yield + :param spacing: the voxel spacing in the simulation :return: a functional that describes a distortion field along the y axis """ @@ -83,7 +93,22 @@ def create_spline_for_range(xmin_mm=0, xmax_mm=10, maximum_y_elevation_mm=1, spa return spline, max_el -def spline_evaluator2d_voxel(x, y, spline, offset_voxel, thickness_voxel): +def spline_evaluator2d_voxel(x: int, y: int, spline: Union[list, np.ndarray], offset_voxel: Union[float, int], + thickness_voxel: int) -> bool: + """ + Evaluate whether a given point (x, y) lies within the thickness bounds around a spline curve. + + This function checks if the y-coordinate of a point lies within a vertical range defined + around a spline curve at a specific x-coordinate. The range is determined by the spline elevation, + an offset, and a thickness. + + :param x: The x-coordinate of the point to evaluate. + :param y: The y-coordinate of the point to evaluate. + :param spline: A 1D array or list representing the spline curve elevations at each x-coordinate. + :param offset_voxel: The offset to be added to the spline elevation to define the starting y-coordinate of the range. + :param thickness_voxel: The vertical thickness of the range around the spline. + :return: True if the point (x, y) lies within the range around the spline, False otherwise. + """ elevation = spline[x] y_value = np.round(elevation + offset_voxel) if y_value <= y < thickness_voxel + y_value: @@ -92,7 +117,7 @@ def spline_evaluator2d_voxel(x, y, spline, offset_voxel, thickness_voxel): return False -def calculate_gruneisen_parameter_from_temperature(temperature_in_celcius): +def calculate_gruneisen_parameter_from_temperature(temperature_in_celcius: Union[float, int]) -> Union[float, int]: """ This function returns the dimensionless gruneisen parameter based on a heuristic formula that was determined experimentally:: @@ -112,7 +137,7 @@ def calculate_gruneisen_parameter_from_temperature(temperature_in_celcius): return 0.0043 + 0.0053 * temperature_in_celcius -def randomize_uniform(min_value: float, max_value: float): +def randomize_uniform(min_value: float, max_value: float) -> Union[float, int]: """ returns a uniformly drawn random number in [min_value, max_value[ @@ -124,43 +149,43 @@ def randomize_uniform(min_value: float, max_value: float): return (np.random.random() * (max_value-min_value)) + min_value -def rotation_x(theta): +def rotation_x(theta: Union[float, int]) -> torch.Tensor: """ Rotation matrix around the x-axis with angle theta. :param theta: Angle through which the matrix is supposed to rotate. :return: rotation matrix """ - return np.array([[1, 0, 0], - [0, np.cos(theta), -np.sin(theta)], - [0, np.sin(theta), np.cos(theta)]]) + return torch.tensor([[1, 0, 0], + [0, torch.cos(theta), -torch.sin(theta)], + [0, torch.sin(theta), torch.cos(theta)]]) -def rotation_y(theta): +def rotation_y(theta: Union[float, int]) -> torch.Tensor: """ Rotation matrix around the y-axis with angle theta. :param theta: Angle through which the matrix is supposed to rotate. :return: rotation matrix """ - return np.array([[np.cos(theta), 0, np.sin(theta)], - [0, 1, 0], - [-np.sin(theta), 0, np.cos(theta)]]) + return torch.tensor([[torch.cos(theta), 0, torch.sin(theta)], + [0, 1, 0], + [-torch.sin(theta), 0, torch.cos(theta)]]) -def rotation_z(theta): +def rotation_z(theta: Union[float, int]) -> torch.Tensor: """ Rotation matrix around the z-axis with angle theta. :param theta: Angle through which the matrix is supposed to rotate. :return: rotation matrix """ - return np.array([[np.cos(theta), -np.sin(theta), 0], - [np.sin(theta), np.cos(theta), 0], - [0, 0, 1]]) + return torch.tensor([[torch.cos(theta), -torch.sin(theta), 0], + [torch.sin(theta), torch.cos(theta), 0], + [0, 0, 1]]) -def rotation(angles): +def rotation(angles: Union[list, np.ndarray]) -> torch.Tensor: """ Rotation matrix around the x-, y-, and z-axis with angles [theta_x, theta_y, theta_z]. @@ -170,7 +195,7 @@ def rotation(angles): return rotation_x(angles[0]) * rotation_y(angles[1]) * rotation_z(angles[2]) -def rotation_matrix_between_vectors(a, b): +def rotation_matrix_between_vectors(a: np.ndarray, b: np.ndarray) -> np.ndarray: """ Returns the rotation matrix from a to b @@ -223,3 +248,22 @@ def positive_gauss(mean, std) -> float: return positive_gauss(mean, std) else: return random_value + + +def are_equal(obj1: Union[list, tuple, np.ndarray, object], obj2: Union[list, tuple, np.ndarray, object]) -> bool: + """Compare if two objects are equal. For lists, tuples and arrays, all entries need to be equal to return True. + + :param obj1: The first object to compare. Can be of any type, but typically a list, numpy array, or scalar. + :type obj1: Union[list, tuple, np.ndarray, object] + :param obj2: The second object to compare. Can be of any type, but typically a list, numpy array, or scalar. + :type obj2: Union[list, tuple, np.ndarray, object] + :return: True if the objects are equal, False otherwise. For lists and numpy arrays, returns True only if all + corresponding elements are equal. + :rtype: bool + """ + # Check if one object is numpy array or list + if isinstance(obj1, (list, np.ndarray, tuple)) or isinstance(obj2, (list, np.ndarray, tuple)): + return np.array_equal(obj1, obj2) + # For other types, use standard equality check which also works for lists + else: + return obj1 == obj2 diff --git a/simpa/utils/deformation_manager.py b/simpa/utils/deformation_manager.py index 00774a9f..0c786a0d 100644 --- a/simpa/utils/deformation_manager.py +++ b/simpa/utils/deformation_manager.py @@ -4,7 +4,7 @@ import matplotlib.pyplot as plt from simpa.utils import Tags -from scipy.interpolate import interp2d +from scipy.interpolate import RegularGridInterpolator from scipy.ndimage import gaussian_filter import numpy as np @@ -24,25 +24,20 @@ def create_deformation_settings(bounds_mm, maximum_z_elevation_mm=1, filter_sigm x_positions_vector = np.linspace(bounds_mm[0][0], bounds_mm[0][1], number_of_boundary_points[0]) y_positions_vector = np.linspace(bounds_mm[1][0], bounds_mm[1][1], number_of_boundary_points[1]) - xx, yy = np.meshgrid(x_positions_vector, y_positions_vector, indexing='ij') - # Add random permutations to the y-axis of the division knots - for x_idx, x_position in enumerate(x_positions_vector): - for y_idx, y_position in enumerate(y_positions_vector): - scaling_value = (np.cos(x_position / (bounds_mm[0][1] * (cosine_scaling_factor / np.pi)) - - np.pi/(cosine_scaling_factor * 2)) ** 2 * - np.cos(y_position / (bounds_mm[1][1] * (cosine_scaling_factor / np.pi)) - - np.pi/(cosine_scaling_factor * 2)) ** 2) - - surface_elevations[x_idx, y_idx] = scaling_value * surface_elevations[x_idx, y_idx] + all_scaling_value = np.multiply.outer( + np.cos(x_positions_vector / (bounds_mm[0][1] * (cosine_scaling_factor / + np.pi)) - np.pi / (cosine_scaling_factor * 2)) ** 2, + np.cos(y_positions_vector / (bounds_mm[1][1] * (cosine_scaling_factor / np.pi)) - np.pi / (cosine_scaling_factor * 2)) ** 2) + surface_elevations *= all_scaling_value # This rescales and sets the maximum to 0. surface_elevations = surface_elevations * maximum_z_elevation_mm de_facto_max_elevation = np.max(surface_elevations) surface_elevations = surface_elevations - de_facto_max_elevation - deformation_settings[Tags.DEFORMATION_X_COORDINATES_MM] = xx - deformation_settings[Tags.DEFORMATION_Y_COORDINATES_MM] = yy + deformation_settings[Tags.DEFORMATION_X_COORDINATES_MM] = x_positions_vector + deformation_settings[Tags.DEFORMATION_Y_COORDINATES_MM] = y_positions_vector deformation_settings[Tags.DEFORMATION_Z_ELEVATIONS_MM] = surface_elevations deformation_settings[Tags.MAX_DEFORMATION_MM] = de_facto_max_elevation @@ -66,7 +61,8 @@ def get_functional_from_deformation_settings(deformation_settings: dict): z_elevations_mm = deformation_settings[Tags.DEFORMATION_Z_ELEVATIONS_MM] order = "cubic" - functional_mm = interp2d(x_coordinates_mm, y_coordinates_mm, z_elevations_mm, kind=order) + functional_mm = RegularGridInterpolator( + points=[x_coordinates_mm, y_coordinates_mm], values=z_elevations_mm, method=order) return functional_mm @@ -81,12 +77,12 @@ def get_functional_from_deformation_settings(deformation_settings: dict): x_pos_vector = np.linspace(x_bounds[0], x_bounds[1], 100) y_pos_vector = np.linspace(y_bounds[0], y_bounds[1], 100) - _xx, _yy = np.meshgrid(x_pos_vector, y_pos_vector, indexing='ij') + eval_points = tuple(np.meshgrid(x_pos_vector, y_pos_vector, indexing='ij')) - values = functional(x_pos_vector, y_pos_vector) + values = functional(eval_points) max_elevation = -np.min(values) - plt3d = plt.figure().gca(projection='3d') - plt3d.plot_surface(_xx, _yy, values, cmap="viridis") - plt3d.set_zlim(-max_elevation, 0) + ax = plt.figure().add_subplot(projection='3d') + ax.plot_surface(eval_points[0], eval_points[1], values, cmap="viridis") + ax.set_zlim(-max_elevation, 0) plt.show() diff --git a/simpa/utils/libraries/molecule_library.py b/simpa/utils/libraries/molecule_library.py index 3c813e43..f033b14d 100644 --- a/simpa/utils/libraries/molecule_library.py +++ b/simpa/utils/libraries/molecule_library.py @@ -1,6 +1,7 @@ # SPDX-FileCopyrightText: 2021 Division of Intelligent Medical Systems, DKFZ # SPDX-FileCopyrightText: 2021 Janek Groehl # SPDX-License-Identifier: MIT +from typing import Optional, Union import numpy as np from simpa.utils import Tags @@ -16,7 +17,15 @@ class MolecularComposition(SerializableSIMPAClass, list): - def __init__(self, segmentation_type=None, molecular_composition_settings=None): + def __init__(self, segmentation_type: Optional[str] = None, molecular_composition_settings: Optional[dict] = None): + """ + Initialize the MolecularComposition object. + + :param segmentation_type: The type of segmentation. + :type segmentation_type: str, optional + :param molecular_composition_settings: Settings for the molecular composition. + :type molecular_composition_settings: dict, optional + """ super().__init__() self.segmentation_type = segmentation_type self.internal_properties = TissueProperties() @@ -30,11 +39,15 @@ def __init__(self, segmentation_type=None, molecular_composition_settings=None): def update_internal_properties(self): """ - FIXME + Update the internal tissue properties based on the molecular composition. + + Raises: + AssertionError: If the total volume fraction of all molecules is not exactly 100%. """ self.internal_properties = TissueProperties() self.internal_properties[Tags.DATA_FIELD_SEGMENTATION] = self.segmentation_type self.internal_properties[Tags.DATA_FIELD_OXYGENATION] = calculate_oxygenation(self) + for molecule in self: self.internal_properties.volume_fraction += molecule.volume_fraction self.internal_properties[Tags.DATA_FIELD_GRUNEISEN_PARAMETER] += \ @@ -49,8 +62,13 @@ def update_internal_properties(self): raise AssertionError("Invalid Molecular composition! The volume fractions of all molecules must be" "exactly 100%!") - def get_properties_for_wavelength(self, wavelength) -> TissueProperties: + def get_properties_for_wavelength(self, wavelength: Union[int, float]) -> TissueProperties: + """ + Get the tissue properties for a specific wavelength. + :param wavelength: The wavelength to get properties for. + :return: The updated tissue properties. + """ self.update_internal_properties() self.internal_properties[Tags.DATA_FIELD_ABSORPTION_PER_CM] = 0 self.internal_properties[Tags.DATA_FIELD_SCATTERING_PER_CM] = 0 @@ -73,12 +91,23 @@ def get_properties_for_wavelength(self, wavelength) -> TissueProperties: return self.internal_properties def serialize(self) -> dict: + """ + Serialize the molecular composition to a dictionary. + + :return: The serialized molecular composition. + """ dict_items = self.__dict__ list_items = [molecule for molecule in self] return {"MolecularComposition": {"dict_items": dict_items, "list_items": list_items}} @staticmethod def deserialize(dictionary_to_deserialize: dict): + """ + Deserialize a dictionary into a MolecularComposition object. + + :param dictionary_to_deserialize: The dictionary to deserialize. + :return: The deserialized MolecularComposition object. + """ deserialized_molecular_composition = MolecularCompositionGenerator() for molecule in dictionary_to_deserialize["list_items"]: deserialized_molecular_composition.append(molecule) @@ -89,6 +118,20 @@ def deserialize(dictionary_to_deserialize: dict): class Molecule(SerializableSIMPAClass, object): + """ + A class representing a molecule with various properties. + + Attributes: + name (str): The name of the molecule. + spectrum (Spectrum): The absorption spectrum of the molecule. + volume_fraction (float): The volume fraction of the molecule. + scattering_spectrum (Spectrum): The scattering spectrum of the molecule. + anisotropy_spectrum (Spectrum): The anisotropy spectrum of the molecule. + gruneisen_parameter (float): The Grüneisen parameter of the molecule. + density (float): The density of the molecule. + speed_of_sound (float): The speed of sound in the molecule. + alpha_coefficient (float): The alpha coefficient of the molecule. + """ def __init__(self, name: str = None, absorption_spectrum: Spectrum = None, @@ -100,16 +143,16 @@ def __init__(self, name: str = None, density: float = None, speed_of_sound: float = None, alpha_coefficient: float = None): """ - :param name: str - :param absorption_spectrum: Spectrum - :param volume_fraction: float - :param scattering_spectrum: Spectrum - :param anisotropy_spectrum: Spectrum + :param name: The name of the molecule. + :param absorption_spectrum: The absorption spectrum of the molecule. + :param volume_fraction: The volume fraction of the molecule. + :param scattering_spectrum: The scattering spectrum of the molecule. + :param anisotropy_spectrum: The anisotropy spectrum of the molecule. :param refractive_index: Spectrum - :param gruneisen_parameter: float - :param density: float - :param speed_of_sound: float - :param alpha_coefficient: float + :param gruneisen_parameter: The Grüneisen parameter of the molecule. + :param density: The density of the molecule. + :param speed_of_sound: The speed of sound in the molecule. + :param alpha_coefficient: The alpha coefficient of the molecule. """ if name is None: name = "GenericMoleculeName" @@ -182,7 +225,13 @@ def __init__(self, name: str = None, .format(type(alpha_coefficient))) self.alpha_coefficient = alpha_coefficient - def __eq__(self, other): + def __eq__(self, other) -> bool: + """ + Check equality between two Molecule objects. + + :param other: The other Molecule object to compare. + :return: True if the Molecule objects are equal, False otherwise. + """ if isinstance(other, Molecule): return (self.name == other.name and self.absorption_spectrum == other.absorption_spectrum and @@ -199,11 +248,23 @@ def __eq__(self, other): return super().__eq__(other) def serialize(self): + """ + Serialize the molecule to a dictionary. + + :return: The serialized molecule. + :rtype: dict + """ serialized_molecule = self.__dict__ return {"Molecule": serialized_molecule} @staticmethod def deserialize(dictionary_to_deserialize: dict): + """ + Deserialize a dictionary into a Molecule object. + + :param dictionary_to_deserialize: The dictionary to deserialize. + :return: The deserialized Molecule object. + """ deserialized_molecule = Molecule(name=dictionary_to_deserialize["name"], absorption_spectrum=dictionary_to_deserialize["absorption_spectrum"], volume_fraction=dictionary_to_deserialize["volume_fraction"], @@ -217,10 +278,21 @@ def deserialize(dictionary_to_deserialize: dict): class MoleculeLibrary(object): + """ + A class to create predefined molecules with specific properties. + Each method in this class returns a Molecule object with predefined settings + representing different types of tissues or substances. + """ # Main absorbers @staticmethod - def water(volume_fraction: float = 1.0): + def water(volume_fraction: float = 1.0) -> Molecule: + """ + Create a water molecule with predefined properties. + + :param volume_fraction: The volume fraction of the molecule, defaults to 1.0 + :return: A Molecule object representing water + """ return Molecule(name="water", absorption_spectrum=AbsorptionSpectrumLibrary().get_spectrum_by_name("Water"), volume_fraction=volume_fraction, @@ -235,7 +307,13 @@ def water(volume_fraction: float = 1.0): ) @staticmethod - def oxyhemoglobin(volume_fraction: float = 1.0): + def oxyhemoglobin(volume_fraction: float = 1.0) -> Molecule: + """ + Create an oxyhemoglobin molecule with predefined properties. + + :param volume_fraction: The volume fraction of the molecule, defaults to 1.0 + :return: A Molecule object representing oxyhemoglobin + """ return Molecule(name="oxyhemoglobin", absorption_spectrum=AbsorptionSpectrumLibrary().get_spectrum_by_name("Oxyhemoglobin"), volume_fraction=volume_fraction, @@ -249,7 +327,13 @@ def oxyhemoglobin(volume_fraction: float = 1.0): ) @staticmethod - def deoxyhemoglobin(volume_fraction: float = 1.0): + def deoxyhemoglobin(volume_fraction: float = 1.0) -> Molecule: + """ + Create a deoxyhemoglobin molecule with predefined properties. + + :param volume_fraction: The volume fraction of the molecule, defaults to 1.0 + :return: A Molecule object representing deoxyhemoglobin + """ return Molecule(name="deoxyhemoglobin", absorption_spectrum=AbsorptionSpectrumLibrary().get_spectrum_by_name("Deoxyhemoglobin"), @@ -264,7 +348,13 @@ def deoxyhemoglobin(volume_fraction: float = 1.0): ) @staticmethod - def melanin(volume_fraction: float = 1.0): + def melanin(volume_fraction: float = 1.0) -> Molecule: + """ + Create a melanin molecule with predefined properties. + + :param volume_fraction: The volume fraction of the molecule, defaults to 1.0 + :return: A Molecule object representing melanin + """ return Molecule(name="melanin", absorption_spectrum=AbsorptionSpectrumLibrary().get_spectrum_by_name("Melanin"), volume_fraction=volume_fraction, @@ -280,7 +370,13 @@ def melanin(volume_fraction: float = 1.0): ) @staticmethod - def fat(volume_fraction: float = 1.0): + def fat(volume_fraction: float = 1.0) -> Molecule: + """ + Create a fat molecule with predefined properties. + + :param volume_fraction: The volume fraction of the molecule, defaults to 1.0 + :return: A Molecule object representing fat + """ return Molecule(name="fat", absorption_spectrum=AbsorptionSpectrumLibrary().get_spectrum_by_name("Fat"), volume_fraction=volume_fraction, @@ -298,6 +394,14 @@ def fat(volume_fraction: float = 1.0): @staticmethod def constant_scatterer(scattering_coefficient: float = 100.0, anisotropy: float = 0.9, refractive_index: float = 1.329, volume_fraction: float = 1.0): + """ + Create a constant scatterer molecule with predefined properties. + + :param scattering_coefficient: The scattering coefficient, defaults to 100.0 + :param anisotropy: The anisotropy factor, defaults to 0.9 + :param volume_fraction: The volume fraction of the molecule, defaults to 1.0 + :return: A Molecule object representing a constant scatterer + """ return Molecule(name="constant_scatterer", absorption_spectrum=AbsorptionSpectrumLibrary().CONSTANT_ABSORBER_ARBITRARY(1e-20), volume_fraction=volume_fraction, @@ -311,7 +415,13 @@ def constant_scatterer(scattering_coefficient: float = 100.0, anisotropy: float ) @staticmethod - def soft_tissue_scatterer(volume_fraction: float = 1.0): + def soft_tissue_scatterer(volume_fraction: float = 1.0) -> Molecule: + """ + Create a soft tissue scatterer molecule with predefined properties. + + :param volume_fraction: The volume fraction of the molecule, defaults to 1.0 + :return: A Molecule object representing a soft tissue scatterer + """ return Molecule(name="soft_tissue_scatterer", absorption_spectrum=AbsorptionSpectrumLibrary().CONSTANT_ABSORBER_ARBITRARY(1e-20), volume_fraction=volume_fraction, @@ -326,7 +436,13 @@ def soft_tissue_scatterer(volume_fraction: float = 1.0): ) @staticmethod - def muscle_scatterer(volume_fraction: float = 1.0): + def muscle_scatterer(volume_fraction: float = 1.0) -> Molecule: + """ + Create a muscle scatterer molecule with predefined properties. + + :param volume_fraction: The volume fraction of the molecule, defaults to 1.0 + :return: A Molecule object representing a muscle scatterer + """ return Molecule(name="muscle_scatterer", absorption_spectrum=AbsorptionSpectrumLibrary().CONSTANT_ABSORBER_ARBITRARY(1e-20), volume_fraction=volume_fraction, @@ -341,7 +457,13 @@ def muscle_scatterer(volume_fraction: float = 1.0): ) @staticmethod - def epidermal_scatterer(volume_fraction: float = 1.0): + def epidermal_scatterer(volume_fraction: float = 1.0) -> Molecule: + """ + Create an epidermal scatterer molecule with predefined properties. + + :param volume_fraction: The volume fraction of the molecule, defaults to 1.0 + :return: A Molecule object representing an epidermal scatterer + """ return Molecule(name="epidermal_scatterer", absorption_spectrum=AbsorptionSpectrumLibrary().CONSTANT_ABSORBER_ARBITRARY(1e-20), volume_fraction=volume_fraction, @@ -357,7 +479,13 @@ def epidermal_scatterer(volume_fraction: float = 1.0): ) @staticmethod - def dermal_scatterer(volume_fraction: float = 1.0): + def dermal_scatterer(volume_fraction: float = 1.0) -> Molecule: + """ + Create a dermal scatterer molecule with predefined properties. + + :param volume_fraction: The volume fraction of the molecule, defaults to 1.0 + :return: A Molecule object representing a dermal scatterer + """ return Molecule(name="dermal_scatterer", absorption_spectrum=AbsorptionSpectrumLibrary().get_spectrum_by_name("Skin_Baseline"), volume_fraction=volume_fraction, @@ -372,7 +500,13 @@ def dermal_scatterer(volume_fraction: float = 1.0): ) @staticmethod - def bone(volume_fraction: float = 1.0): + def bone(volume_fraction: float = 1.0) -> Molecule: + """ + Create a bone molecule with predefined properties. + + :param volume_fraction: The volume fraction of the molecule, defaults to 1.0 + :return: A Molecule object representing bone + """ return Molecule(name="bone", absorption_spectrum=AbsorptionSpectrumLibrary().CONSTANT_ABSORBER_ARBITRARY( OpticalTissueProperties.BONE_ABSORPTION), @@ -388,7 +522,13 @@ def bone(volume_fraction: float = 1.0): ) @staticmethod - def mediprene(volume_fraction: float = 1.0): + def mediprene(volume_fraction: float = 1.0) -> Molecule: + """ + Create a mediprene molecule with predefined properties. + + :param volume_fraction: The volume fraction of the molecule, defaults to 1.0 + :return: A Molecule object representing mediprene + """ return Molecule(name="mediprene", absorption_spectrum=AbsorptionSpectrumLibrary().CONSTANT_ABSORBER_ARBITRARY(-np.log(0.85) / 10), # FIXME volume_fraction=volume_fraction, @@ -403,7 +543,13 @@ def mediprene(volume_fraction: float = 1.0): ) @staticmethod - def heavy_water(volume_fraction: float = 1.0): + def heavy_water(volume_fraction: float = 1.0) -> Molecule: + """ + Create a heavy water molecule with predefined properties. + + :param volume_fraction: The volume fraction of the molecule, defaults to 1.0 + :return: A Molecule object representing heavy water + """ return Molecule(name="heavy_water", absorption_spectrum=AbsorptionSpectrumLibrary().CONSTANT_ABSORBER_ARBITRARY( StandardProperties.HEAVY_WATER_MUA), @@ -419,7 +565,13 @@ def heavy_water(volume_fraction: float = 1.0): ) @staticmethod - def air(volume_fraction: float = 1.0): + def air(volume_fraction: float = 1.0) -> Molecule: + """ + Create an air molecule with predefined properties. + + :param volume_fraction: The volume fraction of the molecule, defaults to 1.0 + :return: A Molecule object representing air + """ return Molecule(name="air", absorption_spectrum=AbsorptionSpectrumLibrary().CONSTANT_ABSORBER_ARBITRARY( StandardProperties.AIR_MUA), @@ -442,12 +594,32 @@ class MolecularCompositionGenerator(object): """ The MolecularCompositionGenerator is a helper class to facilitate the creation of a MolecularComposition instance. + + This class provides methods to build and retrieve a molecular composition by + appending Molecule objects to a dictionary, which is then used to create a + MolecularComposition instance. """ def __init__(self): + """ + Initialize a new MolecularCompositionGenerator. + + The constructor initializes an empty dictionary to store the molecular composition. + """ self.molecular_composition_dictionary = dict() def append(self, value: Molecule = None, key: str = None): + """ + Append a Molecule to the molecular composition. + + Adds the given Molecule object to the molecular composition dictionary with the + specified key. If no key is provided, the name attribute of the Molecule is used as the key. + + :param value: The Molecule object to add to the molecular composition. + :param key: The key under which the Molecule should be stored. If None, the Molecule's name is used. + :raises KeyError: If the specified key already exists in the molecular composition. + :return: The current instance of MolecularCompositionGenerator. + """ if key is None: key = value.name if key in self.molecular_composition_dictionary: @@ -456,5 +628,14 @@ def append(self, value: Molecule = None, key: str = None): return self def get_molecular_composition(self, segmentation_type): + """ + Retrieve the molecular composition as a MolecularComposition instance. + + Creates a MolecularComposition instance using the current state of the + molecular composition dictionary and the specified segmentation type. + + :param segmentation_type: The type of segmentation to be used in the MolecularComposition. + :return: A MolecularComposition instance. + """ return MolecularComposition(segmentation_type=segmentation_type, molecular_composition_settings=self.molecular_composition_dictionary) diff --git a/simpa/utils/libraries/spectrum_library.py b/simpa/utils/libraries/spectrum_library.py index b13099dd..07163ff8 100644 --- a/simpa/utils/libraries/spectrum_library.py +++ b/simpa/utils/libraries/spectrum_library.py @@ -13,14 +13,26 @@ class Spectrum(SerializableSIMPAClass, object): """ - An instance of this class represents the absorption spectrum over wavelength for a particular + An instance of this class represents a spectrum over a range of wavelengths. + + Attributes: + spectrum_name (str): The name of the spectrum. + wavelengths (np.ndarray): Array of wavelengths. + max_wavelength (int): Maximum wavelength in the spectrum. + min_wavelength (int): Minimum wavelength in the spectrum. + values (np.ndarray): Corresponding values for each wavelength. + values_interp (np.ndarray): Interpolated values across a continuous range of wavelengths. """ def __init__(self, spectrum_name: str, wavelengths: np.ndarray, values: np.ndarray): """ - :param spectrum_name: - :param wavelengths: - :param values: + Initializes a Spectrum instance. + + :param spectrum_name: Name of the spectrum. + :param wavelengths: Array of wavelengths. + :param values: Corresponding values of the spectrum at each wavelength. + + :raises ValueError: If the shape of wavelengths does not match the shape of values. """ self.spectrum_name = spectrum_name self.wavelengths = wavelengths @@ -29,27 +41,43 @@ def __init__(self, spectrum_name: str, wavelengths: np.ndarray, values: np.ndarr self.values = values if np.shape(wavelengths) != np.shape(values): - raise ValueError("The shape of the wavelengths and the absorption coefficients did not match: " + + raise ValueError("The shape of the wavelengths and the values did not match: " + str(np.shape(wavelengths)) + " vs " + str(np.shape(values))) new_wavelengths = np.arange(self.min_wavelength, self.max_wavelength+1, 1) - self.new_values = np.interp(new_wavelengths, self.wavelengths, self.values) + self.values_interp = np.interp(new_wavelengths, self.wavelengths, self.values) - def get_value_over_wavelength(self): + def get_value_over_wavelength(self) -> np.ndarray: """ - :return: numpy array with the available wavelengths and the corresponding absorption properties + Returns an array with the available wavelengths and their corresponding values. + + :return: numpy array with the available wavelengths and the corresponding properties """ return np.asarray([self.wavelengths, self.values]) def get_value_for_wavelength(self, wavelength: int) -> float: """ - :param wavelength: the wavelength to retrieve a optical absorption value for [cm^{-1}]. + Retrieves the interpolated value for a given wavelength within the spectrum range. + + + :param wavelength: the wavelength to retrieve a value from the defined spectrum. Must be an integer value between the minimum and maximum wavelength. - :return: the best matching linearly interpolated absorption value for the given wavelength. + :return: the best matching linearly interpolated values for the given wavelength. + :raises ValueError: if the given wavelength is not within the range of the spectrum. """ - return self.new_values[wavelength-self.min_wavelength] + if wavelength < self.min_wavelength or wavelength > self.max_wavelength: + raise ValueError(f"The given wavelength ({wavelength}) is not within the range of the spectrum " + f"({self.min_wavelength} - {self.max_wavelength})") + return self.values_interp[wavelength-self.min_wavelength] def __eq__(self, other): + """ + Compares two Spectrum objects for equality. + + :param other: Another Spectrum object to compare with. + + :return: True if both objects are equal, False otherwise. + """ if isinstance(other, Spectrum): return (self.spectrum_name == other.spectrum_name, self.wavelengths == other.wavelengths, @@ -58,11 +86,23 @@ def __eq__(self, other): return super().__eq__(other) def serialize(self) -> dict: + """ + Serializes the spectrum instance into a dictionary format. + + :return: Dictionary representation of the Spectrum instance. + """ serialized_spectrum = self.__dict__ return {"Spectrum": serialized_spectrum} @staticmethod def deserialize(dictionary_to_deserialize: dict): + """ + Static method to deserialize a dictionary representation back into a Spectrum object. + + :param dictionary_to_deserialize: Dictionary containing the serialized Spectrum object. + + :return: Deserialized Spectrum object. + """ deserialized_spectrum = Spectrum(spectrum_name=dictionary_to_deserialize["spectrum_name"], wavelengths=dictionary_to_deserialize["wavelengths"], values=dictionary_to_deserialize["values"]) @@ -70,14 +110,33 @@ def deserialize(dictionary_to_deserialize: dict): class SpectraLibrary(object): + """ + A library to manage and store spectral data. + + This class provides functionality to load and manage spectra data from specified folders. + + Attributes: + spectra (list): A list to store spectra objects. + """ def __init__(self, folder_name: str, additional_folder_path: str = None): + """ + Initializes the SpectraLibrary with spectra data from the specified folder(s). + + :param folder_name: The name of the folder containing spectra data files. + :param additional_folder_path: An additional folder path for more spectra data. + """ self.spectra = list() self.add_spectra_from_folder(folder_name) if additional_folder_path is not None: self.add_spectra_from_folder(additional_folder_path) - def add_spectra_from_folder(self, folder_name): + def add_spectra_from_folder(self, folder_name: str): + """ + Adds spectra from a specified folder to the spectra list. + + :param folder_name: The name of the folder containing spectra data files. + """ base_path = os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe()))) for absorption_spectrum in glob.glob(os.path.join(base_path, folder_name, "*.npz")): name = absorption_spectrum.split(os.path.sep)[-1][:-4] @@ -96,10 +155,22 @@ def __iter__(self): self.i = len(self.spectra) return self - def get_spectra_names(self): + def get_spectra_names(self) -> list: + """ + Returns the names of all spectra in the library. + + :return: List of spectra names. + """ return [spectrum.spectrum_name for spectrum in self] def get_spectrum_by_name(self, spectrum_name: str) -> Spectrum: + """ + Retrieves a spectrum by its name. + + :param spectrum_name: The name of the spectrum to retrieve. + :return: The spectrum with the specified name. + :raises LookupError: If no spectrum with the given name exists. + """ for spectrum in self: if spectrum.spectrum_name == spectrum_name: return spectrum @@ -109,29 +180,67 @@ def get_spectrum_by_name(self, spectrum_name: str) -> Spectrum: class AnisotropySpectrumLibrary(SpectraLibrary): + """ + A library to manage and store anisotropy spectra data. + """ def __init__(self, additional_folder_path: str = None): + """ + Initializes the AnisotropySpectrumLibrary with anisotropy spectra data. + + :param additional_folder_path: An additional folder path for more anisotropy spectra data. + """ super(AnisotropySpectrumLibrary, self).__init__("anisotropy_spectra_data", additional_folder_path) @staticmethod - def CONSTANT_ANISOTROPY_ARBITRARY(anisotropy: float = 1): + def CONSTANT_ANISOTROPY_ARBITRARY(anisotropy: float = 1) -> Spectrum: + """ + Creates a constant anisotropy spectrum with an arbitrary value. + + :param anisotropy: The anisotropy value to use. + :return: A Spectrum instance with constant anisotropy. + """ return Spectrum("Constant Anisotropy (arb)", np.asarray([450, 1000]), np.asarray([anisotropy, anisotropy])) class ScatteringSpectrumLibrary(SpectraLibrary): + """ + A library to manage and store scattering spectra data. + """ def __init__(self, additional_folder_path: str = None): + """ + Initializes the ScatteringSpectrumLibrary with scattering spectra data. + + :param additional_folder_path: An additional folder path for more scattering spectra data. + """ super(ScatteringSpectrumLibrary, self).__init__("scattering_spectra_data", additional_folder_path) @staticmethod - def CONSTANT_SCATTERING_ARBITRARY(scattering: float = 1): + def CONSTANT_SCATTERING_ARBITRARY(scattering: float = 1) -> Spectrum: + """ + Creates a constant scattering spectrum with an arbitrary value. + + :param scattering: The scattering value to use. + :return: A Spectrum instance with constant scattering. + """ return Spectrum("Constant Scattering (arb)", np.asarray([450, 1000]), np.asarray([scattering, scattering])) @staticmethod - def scattering_from_rayleigh_and_mie_theory(name: str, mus_at_500_nm: float = 1.0, fraction_rayleigh_scattering: float = 0.0, - mie_power_law_coefficient: float = 0.0): + def scattering_from_rayleigh_and_mie_theory(name: str, mus_at_500_nm: float = 1.0, + fraction_rayleigh_scattering: float = 0.0, + mie_power_law_coefficient: float = 0.0) -> Spectrum: + """ + Creates a scattering spectrum based on Rayleigh and Mie scattering theory. + + :param name: The name of the spectrum. + :param mus_at_500_nm: Scattering coefficient at 500 nm. + :param fraction_rayleigh_scattering: Fraction of Rayleigh scattering. + :param mie_power_law_coefficient: Power law coefficient for Mie scattering. + :return: A Spectrum instance based on Rayleigh and Mie scattering theory. + """ wavelengths = np.arange(450, 1001, 1) scattering = (mus_at_500_nm * (fraction_rayleigh_scattering * (wavelengths / 500) ** 1e-4 + (1 - fraction_rayleigh_scattering) * (wavelengths / 500) ** -mie_power_law_coefficient)) @@ -139,12 +248,26 @@ def scattering_from_rayleigh_and_mie_theory(name: str, mus_at_500_nm: float = 1. class AbsorptionSpectrumLibrary(SpectraLibrary): + """ + A library to manage and store absorption spectra data. + """ def __init__(self, additional_folder_path: str = None): + """ + Initializes the AbsorptionSpectrumLibrary with absorption spectra data. + + :param additional_folder_path: An additional folder path for more absorption spectra data. + """ super(AbsorptionSpectrumLibrary, self).__init__("absorption_spectra_data", additional_folder_path) @staticmethod - def CONSTANT_ABSORBER_ARBITRARY(absorption_coefficient: float = 1): + def CONSTANT_ABSORBER_ARBITRARY(absorption_coefficient: float = 1) -> Spectrum: + """ + Creates a constant absorption spectrum with an arbitrary value. + + :param absorption_coefficient: The absorption coefficient to use. + :return: A Spectrum instance with constant absorption. + """ return Spectrum("Constant Absorber (arb)", np.asarray([450, 1000]), np.asarray([absorption_coefficient, absorption_coefficient])) @@ -161,6 +284,12 @@ def CONSTANT_REFRACTOR_ARBITRARY(refractive_index: float = 1): def get_simpa_internal_absorption_spectra_by_names(absorption_spectrum_names: list): + """ + Retrieves SIMPA internal absorption spectra by their names. + + :param absorption_spectrum_names: List of absorption spectrum names to retrieve. + :return: List of Spectrum instances corresponding to the given names. + """ lib = AbsorptionSpectrumLibrary() spectra = [] for spectrum_name in absorption_spectrum_names: @@ -170,10 +299,10 @@ def get_simpa_internal_absorption_spectra_by_names(absorption_spectrum_names: li def view_saved_spectra(save_path=None, mode="absorption"): """ - Opens a matplotlib plot and visualizes the available absorption spectra. + Opens a matplotlib plot and visualizes the available spectra. - :param save_path: If not None, then the figure will be saved as a png file to the destination. - :param mode: string that is "absorption", "scattering", or "anisotropy" + :param save_path: If not None, then the figure will be saved as a PNG file to the destination. + :param mode: Specifies the type of spectra to visualize ("absorption", "scattering", or "anisotropy"). """ plt.figure(figsize=(11, 8)) if mode == "absorption": @@ -181,16 +310,19 @@ def view_saved_spectra(save_path=None, mode="absorption"): plt.semilogy(spectrum.wavelengths, spectrum.values, label=spectrum.spectrum_name) - if mode == "scattering": + elif mode == "scattering": for spectrum in ScatteringSpectrumLibrary(): plt.semilogy(spectrum.wavelengths, spectrum.values, label=spectrum.spectrum_name) - if mode == "anisotropy": + elif mode == "anisotropy": for spectrum in AnisotropySpectrumLibrary(): plt.semilogy(spectrum.wavelengths, spectrum.values, label=spectrum.spectrum_name) + else: + raise ValueError(f"Invalid mode: {mode}. Choose from 'absorption', 'scattering', or 'anisotropy'.") + ax = plt.gca() box = ax.get_position() ax.set_position([box.x0, box.y0, box.width * 0.8, box.height]) diff --git a/simpa/utils/libraries/structure_library/CircularTubularStructure.py b/simpa/utils/libraries/structure_library/CircularTubularStructure.py index a618df67..3e2fcaa3 100644 --- a/simpa/utils/libraries/structure_library/CircularTubularStructure.py +++ b/simpa/utils/libraries/structure_library/CircularTubularStructure.py @@ -70,10 +70,9 @@ def get_enclosed_indices(self): if self.do_deformation: # the deformation functional needs mm as inputs and returns the result in reverse indexing order... - deformation_values_mm = self.deformation_functional_mm(torch.arange(self.volume_dimensions_voxels[0]) * - self.voxel_spacing, - torch.arange(self.volume_dimensions_voxels[1]) * - self.voxel_spacing).T + eval_points = torch.meshgrid(torch.arange(self.volume_dimensions_voxels[0]) * self.voxel_spacing, + torch.arange(self.volume_dimensions_voxels[1]) * self.voxel_spacing, indexing='ij') + deformation_values_mm = self.deformation_functional_mm(eval_points) deformation_values_mm = deformation_values_mm.reshape(self.volume_dimensions_voxels[0], self.volume_dimensions_voxels[1], 1, 1) deformation_values_mm = torch.tile(torch.as_tensor( diff --git a/simpa/utils/libraries/structure_library/EllipticalTubularStructure.py b/simpa/utils/libraries/structure_library/EllipticalTubularStructure.py index 7986e431..6423ef06 100644 --- a/simpa/utils/libraries/structure_library/EllipticalTubularStructure.py +++ b/simpa/utils/libraries/structure_library/EllipticalTubularStructure.py @@ -77,10 +77,9 @@ def get_enclosed_indices(self): if self.do_deformation: # the deformation functional needs mm as inputs and returns the result in reverse indexing order... - deformation_values_mm = self.deformation_functional_mm(torch.arange(self.volume_dimensions_voxels[0]) * - self.voxel_spacing, - torch.arange(self.volume_dimensions_voxels[1]) * - self.voxel_spacing).T + eval_points = torch.meshgrid(torch.arange(self.volume_dimensions_voxels[0], dtype=torch.float) * self.voxel_spacing, + torch.arange(self.volume_dimensions_voxels[1], dtype=torch.float) * self.voxel_spacing, indexing='ij') + deformation_values_mm = self.deformation_functional_mm(eval_points) deformation_values_mm = deformation_values_mm.reshape(self.volume_dimensions_voxels[0], self.volume_dimensions_voxels[1], 1, 1) deformation_values_mm = torch.tile(torch.as_tensor( diff --git a/simpa/utils/libraries/structure_library/HorizontalLayerStructure.py b/simpa/utils/libraries/structure_library/HorizontalLayerStructure.py index 152b1cf0..9a439e5d 100644 --- a/simpa/utils/libraries/structure_library/HorizontalLayerStructure.py +++ b/simpa/utils/libraries/structure_library/HorizontalLayerStructure.py @@ -62,10 +62,9 @@ def get_enclosed_indices(self): target_vector_voxels = target_vector_voxels[:, :, :, 2] if self.do_deformation: # the deformation functional needs mm as inputs and returns the result in reverse indexing order... - deformation_values_mm = self.deformation_functional_mm(torch.arange(self.volume_dimensions_voxels[0], dtype=torch.float) * - self.voxel_spacing, - torch.arange(self.volume_dimensions_voxels[1], dtype=torch.float) * - self.voxel_spacing).T + eval_points = torch.meshgrid(torch.arange(self.volume_dimensions_voxels[0], dtype=torch.float) * self.voxel_spacing, + torch.arange(self.volume_dimensions_voxels[1], dtype=torch.float) * self.voxel_spacing, indexing='ij') + deformation_values_mm = self.deformation_functional_mm(eval_points) target_vector_voxels = (target_vector_voxels + torch.from_numpy(deformation_values_mm.reshape( self.volume_dimensions_voxels[0], self.volume_dimensions_voxels[1], 1)).to(self.torch_device) / self.voxel_spacing).float() diff --git a/simpa/utils/libraries/structure_library/ParallelepipedStructure.py b/simpa/utils/libraries/structure_library/ParallelepipedStructure.py index a84fd49c..05c90220 100644 --- a/simpa/utils/libraries/structure_library/ParallelepipedStructure.py +++ b/simpa/utils/libraries/structure_library/ParallelepipedStructure.py @@ -72,7 +72,7 @@ def get_enclosed_indices(self): 1/torch.linalg.norm(y_edge_voxels), 1/torch.linalg.norm(z_edge_voxels)], device=self.torch_device) - filled_mask_bool = (0 <= result) & (result <= 1 - norm_vector) + filled_mask_bool = (0 <= result) & (result + norm_vector <= 1) volume_fractions = torch.zeros(tuple(self.volume_dimensions_voxels), dtype=torch.float, device=self.torch_device) diff --git a/simpa/utils/libraries/structure_library/VesselStructure.py b/simpa/utils/libraries/structure_library/VesselStructure.py index c6274303..a8c34771 100644 --- a/simpa/utils/libraries/structure_library/VesselStructure.py +++ b/simpa/utils/libraries/structure_library/VesselStructure.py @@ -71,9 +71,9 @@ def calculate_vessel_samples(self, position, direction, bifurcation_length, radi if samples >= bifurcation_length: vessel_branch_positions1 = position vessel_branch_positions2 = position - angles = np.random.normal(np.pi / 16, np.pi / 8, 3) - vessel_branch_directions1 = torch.tensor(np.matmul(rotation(angles), direction)).to(self.torch_device) - vessel_branch_directions2 = torch.tensor(np.matmul(rotation(-angles), direction)).to(self.torch_device) + angles = torch.normal(np.pi / 16, np.pi / 8, (3,)) + vessel_branch_directions1 = torch.matmul(rotation(angles).to(self.torch_device), direction) + vessel_branch_directions2 = torch.matmul(rotation(-angles).to(self.torch_device), direction) vessel_branch_radius1 = 1 / math.sqrt(2) * radius vessel_branch_radius2 = 1 / math.sqrt(2) * radius vessel_branch_radius_variation1 = 1 / math.sqrt(2) * radius_variation @@ -104,7 +104,7 @@ def calculate_vessel_samples(self, position, direction, bifurcation_length, radi position_array.append(position) radius_array.append(np.random.uniform(-1, 1) * radius_variation + radius) - step_vector = torch.from_numpy(np.random.uniform(-1, 1, 3)).to(self.torch_device) + step_vector = torch.rand(3).to(self.torch_device) * 2 - 1 step_vector = direction + curvature_factor * step_vector direction = step_vector / torch.linalg.norm(step_vector) samples += 1 diff --git a/simpa/utils/libraries/tissue_library.py b/simpa/utils/libraries/tissue_library.py index 0bfe3b1a..2c08209b 100644 --- a/simpa/utils/libraries/tissue_library.py +++ b/simpa/utils/libraries/tissue_library.py @@ -1,63 +1,99 @@ # SPDX-FileCopyrightText: 2021 Division of Intelligent Medical Systems, DKFZ # SPDX-FileCopyrightText: 2021 Janek Groehl # SPDX-License-Identifier: MIT +import typing from simpa.utils import OpticalTissueProperties, SegmentationClasses, StandardProperties, MolecularCompositionGenerator from simpa.utils import Molecule from simpa.utils import MOLECULE_LIBRARY from simpa.utils.libraries.spectrum_library import (AnisotropySpectrumLibrary, ScatteringSpectrumLibrary, RefractiveIndexSpectrumLibrary) +from simpa.utils import Spectrum +from simpa.utils.libraries.molecule_library import MolecularComposition +from simpa.utils.libraries.spectrum_library import AnisotropySpectrumLibrary, ScatteringSpectrumLibrary from simpa.utils.calculate import randomize_uniform from simpa.utils.libraries.spectrum_library import AbsorptionSpectrumLibrary +from typing import Union, List +import torch + class TissueLibrary(object): """ - TODO + A library, returning molecular compositions for various typical tissue segmentations. """ - def get_blood_volume_fractions(self, total_blood_volume_fraction=1e-10, oxygenation=1e-10): + def get_blood_volume_fractions(self, oxygenation: Union[float, int, torch.Tensor] = 1e-10, + blood_volume_fraction: Union[float, int, torch.Tensor] = 1e-10)\ + -> List[Union[int, float, torch.Tensor]]: """ - TODO + A function that returns the volume fraction of the oxygenated and deoxygenated blood. + :param oxygenation: The oxygenation level of the blood volume fraction (as a decimal). + Default: 1e-10 + :param blood_volume_fraction: The total blood volume fraction (including oxygenated and deoxygenated blood). + Default: 1e-10 + :return: the blood volume fraction of the oxygenated and deoxygenated blood separately. """ - return [total_blood_volume_fraction*oxygenation, total_blood_volume_fraction*(1-oxygenation)] + return [blood_volume_fraction*oxygenation, blood_volume_fraction*(1-oxygenation)] - def constant(self, mua=1e-10, mus=1e-10, g=1e-10, n=1.3): + def constant(self, mua: Union[float, int, torch.Tensor] = 1e-10, mus: Union[float, int, torch.Tensor] = 1e-10, + g: Union[float, int, torch.Tensor] = 0, n: float = 1.3) -> MolecularComposition: """ - TODO + A function returning a molecular composition as specified by the user. Typically intended for the use of wanting + specific mua, mus and g values. + :param mua: optical absorption coefficient + Default: 1e-10 cm^⁻1 + :param mus: optical scattering coefficient + Default: 1e-10 cm^⁻1 + :param g: optical scattering anisotropy + Default: 0 + :return: the molecular composition as specified by the user """ - return (MolecularCompositionGenerator().append(Molecule(name="constant_mua_mus_g", - absorption_spectrum=AbsorptionSpectrumLibrary(). - CONSTANT_ABSORBER_ARBITRARY(mua), - volume_fraction=1.0, - scattering_spectrum=ScatteringSpectrumLibrary. - CONSTANT_SCATTERING_ARBITRARY(mus), - anisotropy_spectrum=AnisotropySpectrumLibrary. - CONSTANT_ANISOTROPY_ARBITRARY(g), - refractive_index=RefractiveIndexSpectrumLibrary. - CONSTANT_REFRACTOR_ARBITRARY(n))) + mua_as_spectrum = AbsorptionSpectrumLibrary().CONSTANT_ABSORBER_ARBITRARY(mua) + mus_as_spectrum = ScatteringSpectrumLibrary.CONSTANT_SCATTERING_ARBITRARY(mus) + g_as_spectrum = AnisotropySpectrumLibrary.CONSTANT_ANISOTROPY_ARBITRARY(g) + n_as_spectrum = RefractiveIndexSpectrumLibrary.CONSTANT_REFRACTOR_ARBITRARY(n) + return self.generic_tissue(mua_as_spectrum, mus_as_spectrum, g_as_spectrum, n_as_spectrum, "constant_mua_mus_g_n") + + def generic_tissue(self, + mua: Spectrum = AbsorptionSpectrumLibrary().CONSTANT_ABSORBER_ARBITRARY(1e-10), + mus: Spectrum = AbsorptionSpectrumLibrary().CONSTANT_ABSORBER_ARBITRARY(1e-10), + g: Spectrum = AbsorptionSpectrumLibrary().CONSTANT_ABSORBER_ARBITRARY(1e-10), + n: Spectrum = RefractiveIndexSpectrumLibrary.CONSTANT_REFRACTOR_ARBITRARY(1.3), + molecule_name: typing.Optional[str] = "generic_tissue") -> MolecularComposition: + """ + Returns a generic issue defined by the provided optical parameters. + :param mua: The absorption coefficient spectrum in cm^{-1}. + :param mus: The scattering coefficient spectrum in cm^{-1}. + :param g: The anisotropy spectrum. + :param n: The refractive index spectrum. + :param molecule_name: The molecule name. + + :returns: The molecular composition of the tissue. + """ + assert isinstance(mua, Spectrum), type(mua) + assert isinstance(mus, Spectrum), type(mus) + assert isinstance(g, Spectrum), type(g) + assert isinstance(n, Spectrum), type(n) + assert isinstance(molecule_name, str) or molecule_name is None, type(molecule_name) + + return (MolecularCompositionGenerator().append(Molecule(name=molecule_name, + absorption_spectrum=mua, + volume_fraction=1.0, + scattering_spectrum=mus, + anisotropy_spectrum=g, + refractive_index=n)) .get_molecular_composition(SegmentationClasses.GENERIC)) - def muscle(self, background_oxy=None, blood_volume_fraction=None): + def muscle(self, oxygenation: Union[float, int, torch.Tensor] = 0.175, + blood_volume_fraction: Union[float, int, torch.Tensor] = 0.06) -> MolecularComposition: """ :return: a settings dictionary containing all min and max parameters fitting for muscle tissue. """ - # Determine muscle oxygenation - if background_oxy is None: - oxy = 0.175 - else: - oxy = background_oxy - - # Get the blood volume fractions for oxyhemoglobin and deoxyhemoglobin - if blood_volume_fraction is None: - bvf = 0.06 - else: - bvf = blood_volume_fraction - - [fraction_oxy, fraction_deoxy] = self.get_blood_volume_fractions(bvf, oxy) + [fraction_oxy, fraction_deoxy] = self.get_blood_volume_fractions(oxygenation, blood_volume_fraction) # Get the water volume fraction water_volume_fraction = OpticalTissueProperties.WATER_VOLUME_FRACTION_HUMAN_BODY @@ -82,27 +118,20 @@ def muscle(self, background_oxy=None, blood_volume_fraction=None): .append(custom_water) .get_molecular_composition(SegmentationClasses.MUSCLE)) - def soft_tissue(self, background_oxy=None, blood_volume_fraction=None): + def soft_tissue(self, oxygenation: Union[float, int, torch.Tensor] = OpticalTissueProperties.BACKGROUND_OXYGENATION, + blood_volume_fraction: Union[float, int, torch.Tensor] = OpticalTissueProperties.BLOOD_VOLUME_FRACTION_MUSCLE_TISSUE) -> MolecularComposition: """ IMPORTANT! This tissue is not tested and it is not based on a specific real tissue type. It is a mixture of muscle (mostly optical properties) and water (mostly acoustic properties). This tissue type roughly resembles the generic background tissue that we see in real PA images. + :param oxygenation: The oxygenation level of the blood volume fraction (as a decimal). + Default: OpticalTissueProperties.BACKGROUND_OXYGENATION + :param blood_volume_fraction: The total blood volume fraction (including oxygenated and deoxygenated blood). + Default: OpticalTissueProperties.BLOOD_VOLUME_FRACTION_MUSCLE_TISSUE :return: a settings dictionary containing all min and max parameters fitting for generic soft tissue. """ - # Determine muscle oxygenation - if background_oxy is None: - oxy = OpticalTissueProperties.BACKGROUND_OXYGENATION - else: - oxy = background_oxy - - # Get the blood volume fractions for oxyhemoglobin and deoxyhemoglobin - if blood_volume_fraction is None: - bvf = OpticalTissueProperties.BLOOD_VOLUME_FRACTION_MUSCLE_TISSUE - else: - bvf = blood_volume_fraction - - [fraction_oxy, fraction_deoxy] = self.get_blood_volume_fractions(bvf, oxy) + [fraction_oxy, fraction_deoxy] = self.get_blood_volume_fractions(oxygenation, blood_volume_fraction) # Get the water volume fraction water_volume_fraction = OpticalTissueProperties.WATER_VOLUME_FRACTION_HUMAN_BODY @@ -127,63 +156,58 @@ def soft_tissue(self, background_oxy=None, blood_volume_fraction=None): .append(custom_water) .get_molecular_composition(SegmentationClasses.SOFT_TISSUE)) - def epidermis(self, melanosom_volume_fraction=None): + def epidermis(self, melanosom_volume_fraction: Union[float, int, torch.Tensor] = 0.014) -> MolecularComposition: """ :return: a settings dictionary containing all min and max parameters fitting for epidermis tissue. """ - # Get melanin volume fraction - if melanosom_volume_fraction is None: - melanin_volume_fraction = 0.014 - else: - melanin_volume_fraction = melanosom_volume_fraction - # generate the tissue dictionary return (MolecularCompositionGenerator() - .append(MOLECULE_LIBRARY.melanin(melanin_volume_fraction)) - .append(MOLECULE_LIBRARY.epidermal_scatterer(1 - melanin_volume_fraction)) + .append(MOLECULE_LIBRARY.melanin(melanosom_volume_fraction)) + .append(MOLECULE_LIBRARY.epidermal_scatterer(1 - melanosom_volume_fraction)) .get_molecular_composition(SegmentationClasses.EPIDERMIS)) - def dermis(self, background_oxy=None, blood_volume_fraction=None): + def dermis(self, oxygenation: Union[float, int, torch.Tensor] = 0.5, + blood_volume_fraction: Union[float, int, torch.Tensor] = 0.002) -> MolecularComposition: """ - + Create a molecular composition mimicking that of dermis + :param oxygenation: The oxygenation level of the blood volume fraction (as a decimal). + Default: 0.5 + :param blood_volume_fraction: The total blood volume fraction (including oxygenated and deoxygenated blood). + Default: 0.002 :return: a settings dictionary containing all min and max parameters fitting for dermis tissue. """ - # Determine muscle oxygenation - if background_oxy is None: - oxy = 0.5 - else: - oxy = background_oxy - - if blood_volume_fraction is None: - bvf = 0.002 - else: - bvf = blood_volume_fraction - - # Get the bloood volume fractions for oxyhemoglobin and deoxyhemoglobin - [fraction_oxy, fraction_deoxy] = self.get_blood_volume_fractions(bvf, oxy) + # Get the blood volume fractions for oxyhemoglobin and deoxyhemoglobin + [fraction_oxy, fraction_deoxy] = self.get_blood_volume_fractions(oxygenation, blood_volume_fraction) # generate the tissue dictionary return (MolecularCompositionGenerator() .append(MOLECULE_LIBRARY.oxyhemoglobin(fraction_oxy)) .append(MOLECULE_LIBRARY.deoxyhemoglobin(fraction_deoxy)) - .append(MOLECULE_LIBRARY.dermal_scatterer(1.0 - bvf)) + .append(MOLECULE_LIBRARY.dermal_scatterer(1.0 - blood_volume_fraction)) .get_molecular_composition(SegmentationClasses.DERMIS)) - def subcutaneous_fat(self, oxy=OpticalTissueProperties.BACKGROUND_OXYGENATION): + def subcutaneous_fat(self, + oxygenation: Union[float, int, torch.Tensor] = OpticalTissueProperties.BACKGROUND_OXYGENATION, + blood_volume_fraction: Union[float, int, torch.Tensor] + = OpticalTissueProperties.BLOOD_VOLUME_FRACTION_MUSCLE_TISSUE) -> MolecularComposition: """ - + Create a molecular composition mimicking that of subcutaneous fat + :param oxygenation: The oxygenation level of the blood volume fraction (as a decimal). + Default: OpticalTissueProperties.BACKGROUND_OXYGENATION + :param blood_volume_fraction: The total blood volume fraction (including oxygenated and deoxygenated blood). + Default: OpticalTissueProperties.BLOOD_VOLUME_FRACTION_MUSCLE_TISSUE :return: a settings dictionary containing all min and max parameters fitting for subcutaneous fat tissue. """ # Get water volume fraction water_volume_fraction = OpticalTissueProperties.WATER_VOLUME_FRACTION_HUMAN_BODY - # Get the bloood volume fractions for oxyhemoglobin and deoxyhemoglobin + # Get the blood volume fractions for oxyhemoglobin and deoxyhemoglobin [fraction_oxy, fraction_deoxy] = self.get_blood_volume_fractions( - OpticalTissueProperties.BLOOD_VOLUME_FRACTION_MUSCLE_TISSUE, oxy) + oxygenation, blood_volume_fraction) # Determine fat volume fraction fat_volume_fraction = randomize_uniform(0.2, 1 - (water_volume_fraction + fraction_oxy + fraction_deoxy)) @@ -198,17 +222,20 @@ def subcutaneous_fat(self, oxy=OpticalTissueProperties.BACKGROUND_OXYGENATION): .append(MOLECULE_LIBRARY.water(water_volume_fraction)) .get_molecular_composition(SegmentationClasses.FAT)) - def blood(self, oxygenation=None): + def blood(self, oxygenation: Union[float, int, torch.Tensor, None] = None) -> MolecularComposition: """ - + Create a molecular composition mimicking that of blood + :param oxygenation: The oxygenation level of the blood(as a decimal). + Default: random oxygenation between 0 and 1. :return: a settings dictionary containing all min and max parameters fitting for full blood. """ - # Get the bloood volume fractions for oxyhemoglobin and deoxyhemoglobin + # Get the blood volume fractions for oxyhemoglobin and deoxyhemoglobin if oxygenation is None: oxygenation = randomize_uniform(0.0, 1.0) - [fraction_oxy, fraction_deoxy] = self.get_blood_volume_fractions(1.0, oxygenation) + # Get the blood volume fractions for oxyhemoglobin and deoxyhemoglobin + [fraction_oxy, fraction_deoxy] = self.get_blood_volume_fractions(oxygenation, 1.0) # generate the tissue dictionary return (MolecularCompositionGenerator() @@ -216,11 +243,11 @@ def blood(self, oxygenation=None): .append(MOLECULE_LIBRARY.deoxyhemoglobin(fraction_deoxy)) .get_molecular_composition(SegmentationClasses.BLOOD)) - def bone(self): + def bone(self) -> MolecularComposition: + """ + Create a molecular composition mimicking that of bone + :return: a settings dictionary fitting for bone. """ - - :return: a settings dictionary containing all min and max parameters fitting for full blood. - """ # Get water volume fraction water_volume_fraction = randomize_uniform(OpticalTissueProperties.WATER_VOLUME_FRACTION_BONE_MEAN - OpticalTissueProperties.WATER_VOLUME_FRACTION_BONE_STD, @@ -234,37 +261,54 @@ def bone(self): .append(MOLECULE_LIBRARY.water(water_volume_fraction)) .get_molecular_composition(SegmentationClasses.BONE)) - def mediprene(self): + def mediprene(self) -> MolecularComposition: + """ + Create a molecular composition mimicking that of mediprene + :return: a settings dictionary fitting for mediprene. + """ return (MolecularCompositionGenerator() .append(MOLECULE_LIBRARY.mediprene()) .get_molecular_composition(SegmentationClasses.MEDIPRENE)) - def heavy_water(self): + def heavy_water(self) -> MolecularComposition: + """ + Create a molecular composition mimicking that of heavy water + :return: a settings dictionary containing all min and max parameters fitting for heavy water. + """ return (MolecularCompositionGenerator() .append(MOLECULE_LIBRARY.heavy_water()) .get_molecular_composition(SegmentationClasses.HEAVY_WATER)) - def ultrasound_gel(self): + def ultrasound_gel(self) -> MolecularComposition: + """ + Create a molecular composition mimicking that of ultrasound gel + :return: a settings dictionary fitting for generic ultrasound gel. + """ return (MolecularCompositionGenerator() .append(MOLECULE_LIBRARY.water()) .get_molecular_composition(SegmentationClasses.ULTRASOUND_GEL)) - def lymph_node(self, oxy=None, blood_volume_fraction=None): + def lymph_node(self, oxygenation: Union[float, int, torch.Tensor, None] = None, + blood_volume_fraction: Union[float, int, torch.Tensor, None] = None) -> MolecularComposition: """ IMPORTANT! This tissue is not tested and it is not based on a specific real tissue type. It is a mixture of oxyhemoglobin, deoxyhemoglobin, and lymph node customized water. + :param oxygenation: The oxygenation level of the blood volume fraction (as a decimal). + Default: 0.175 + :param blood_volume_fraction: The total blood volume fraction (including oxygenated and deoxygenated blood). + Default: 0.06 :return: a settings dictionary fitting for generic lymph node tissue. """ # Determine muscle oxygenation - if oxy is None: - oxy = OpticalTissueProperties.LYMPH_NODE_OXYGENATION + if oxygenation is None: + oxygenation = OpticalTissueProperties.LYMPH_NODE_OXYGENATION # Get the blood volume fractions for oxyhemoglobin and deoxyhemoglobin if blood_volume_fraction is None: blood_volume_fraction = OpticalTissueProperties.BLOOD_VOLUME_FRACTION_LYMPH_NODE - [fraction_oxy, fraction_deoxy] = self.get_blood_volume_fractions(blood_volume_fraction, oxy) + [fraction_oxy, fraction_deoxy] = self.get_blood_volume_fractions(oxygenation, blood_volume_fraction) # Get the water volume fraction # water_volume_fraction = OpticalTissueProperties.WATER_VOLUME_FRACTION_HUMAN_BODY diff --git a/simpa/utils/path_manager.py b/simpa/utils/path_manager.py index 5d423afc..2e7ba1ed 100644 --- a/simpa/utils/path_manager.py +++ b/simpa/utils/path_manager.py @@ -8,6 +8,8 @@ from pathlib import Path from simpa.log import Logger +from .tags import Tags + class PathManager: """ @@ -45,8 +47,7 @@ def __init__(self, environment_path=None): if environment_path is None or not os.path.exists(environment_path) or not os.path.isfile(environment_path): error_message = f"Did not find a { self.path_config_file_name} file in any of the standard directories..." - self.logger.critical(error_message) - raise FileNotFoundError(error_message) + self.logger.warning(error_message) self.environment_path = environment_path load_dotenv(environment_path, override=True) @@ -77,18 +78,18 @@ def detect_local_path_config(self): return None def get_hdf5_file_save_path(self): - path = self.get_path_from_environment('SAVE_PATH') - self.logger.debug(f"Retrieved SAVE_PATH={path}") + path = self.get_path_from_environment(f"{Tags.SIMPA_SAVE_PATH_VARNAME}") + self.logger.debug(f"Retrieved {Tags.SIMPA_SAVE_PATH_VARNAME}={path}") return path def get_mcx_binary_path(self): - path = self.get_path_from_environment('MCX_BINARY_PATH') - self.logger.debug(f"Retrieved MCX_BINARY_PATH={path}") + path = self.get_path_from_environment(f"{Tags.MCX_BINARY_PATH_VARNAME}") + self.logger.debug(f"Retrieved {Tags.MCX_BINARY_PATH_VARNAME}={path}") return path def get_matlab_binary_path(self): - path = self.get_path_from_environment('MATLAB_BINARY_PATH') - self.logger.debug(f"Retrieved MATLAB_BINARY_PATH={path}") + path = self.get_path_from_environment(f"{Tags.MATLAB_BINARY_PATH_VARNAME}") + self.logger.debug(f"Retrieved {Tags.MATLAB_BINARY_PATH_VARNAME}={path}") return path def get_path_from_environment(self, env_variable_name): @@ -97,5 +98,5 @@ def get_path_from_environment(self, env_variable_name): error_string = f"The desired environment path variable {env_variable_name} is not available in"\ f" the given config path {self.environment_path}" self.logger.critical(error_string) - raise FileNotFoundError(error_string) + raise KeyError(error_string) return env_variable_content diff --git a/simpa/utils/profiling.py b/simpa/utils/profiling.py new file mode 100644 index 00000000..7eb1a44e --- /dev/null +++ b/simpa/utils/profiling.py @@ -0,0 +1,29 @@ +# SPDX-FileCopyrightText: 2021 Division of Intelligent Medical Systems, DKFZ +# SPDX-FileCopyrightText: 2021 Janek Groehl +# SPDX-License-Identifier: MIT + +import os + +profile_type = os.getenv("SIMPA_PROFILE") +if profile_type is None: + # define a no-op @profile decorator + def profile(f): + return f +elif profile_type == "TIME": + import atexit + from line_profiler import LineProfiler + + profile = LineProfiler() + atexit.register(profile.print_stats) +elif profile_type == "MEMORY": + from memory_profiler import profile +elif profile_type == "GPU_MEMORY": + from pytorch_memlab import profile + from torch.cuda import memory_summary + import atexit + + @atexit.register + def print_memory_summary(): + print(memory_summary()) +else: + raise RuntimeError("SIMPA_PROFILE env var is defined but invalid: valid values are TIME, MEMORY, or GPU_MEMORY") diff --git a/simpa/utils/tags.py b/simpa/utils/tags.py index f8517284..778a83ba 100644 --- a/simpa/utils/tags.py +++ b/simpa/utils/tags.py @@ -2,9 +2,10 @@ # SPDX-FileCopyrightText: 2021 Janek Groehl # SPDX-License-Identifier: MIT -import numpy as np from numbers import Number +import numpy as np + class Tags: """ @@ -169,13 +170,13 @@ class Tags: DEFORMATION_X_COORDINATES_MM = "deformation_x_coordinates" """ - Mesh that defines the x coordinates of the deformation.\n + Array that defines the x coordinates of the deformation.\n Usage: adapter versatile_volume_creation, naming convention """ DEFORMATION_Y_COORDINATES_MM = "deformation_y_coordinates" """ - Mesh that defines the y coordinates of the deformation.\n + Array that defines the y coordinates of the deformation.\n Usage: adapter versatile_volume_creation, naming convention """ @@ -463,6 +464,12 @@ class Tags: Usage: adapter mcx_adapter, naming convention """ + ILLUMINATION_TYPE_RING = "ring" + """ + Corresponds to ring source in mcx.\n + Usage: adapter mcx_adapter, naming convention + """ + ILLUMINATION_TYPE_SLIT = "slit" """ Corresponds to slit source in mcx.\n @@ -1441,6 +1448,8 @@ class Tags: Usage: simpa.core.simulation_modules.optical_simulation_module.optical_forward_model_mcx_reflectance_adapter """ + VOLUME_BOUNDARY_BONDITION = "volume_boundary_condition" + COMPUTE_PHOTON_DIRECTION_AT_EXIT = "save_dir_at_exit" """ Flag that indicates if the direction of photons when they exit the volume should be stored @@ -1473,3 +1482,18 @@ class Tags: are detected. Usage: simpa.core.simulation_modules.optical_simulation_module.optical_forward_model_mcx_reflectance_adapter """ + + SIMPA_SAVE_PATH_VARNAME = "SIMPA_SAVE_PATH" + """ + Identifier for the environment variable that defines where the results generated with SIMPA will be sotred + """ + + MCX_BINARY_PATH_VARNAME = "MCX_BINARY_PATH" + """ + Identified for the environment varibale that defines the path to the MCX executable. + """ + + MATLAB_BINARY_PATH_VARNAME = "MATLAB_BINARY_PATH" + """ + Identifier for the environment varibale that defines the path the the matlab executable. + """ diff --git a/simpa/visualisation/matplotlib_data_visualisation.py b/simpa/visualisation/matplotlib_data_visualisation.py index 4f9f2472..e7836dc8 100644 --- a/simpa/visualisation/matplotlib_data_visualisation.py +++ b/simpa/visualisation/matplotlib_data_visualisation.py @@ -228,10 +228,10 @@ def visualise_data(wavelength: int = None, plt.title(data_item_names[i]) if len(np.shape(data_to_show[i])) > 2: pos = int(np.shape(data_to_show[i])[1] / 2) - 1 - data = np.rot90(data_to_show[i][:, pos, :], -1) + data = data_to_show[i][:, pos, :].T plt.imshow(np.log10(data) if logscales[i] else data, cmap=cmaps[i]) else: - data = np.rot90(data_to_show[i][:, :], -1) + data = data_to_show[i][:, :].T plt.imshow(np.log10(data) if logscales[i] else data, cmap=cmaps[i]) plt.colorbar() @@ -240,10 +240,10 @@ def visualise_data(wavelength: int = None, plt.title(data_item_names[i]) if len(np.shape(data_to_show[i])) > 2: pos = int(np.shape(data_to_show[i])[0] / 2) - data = np.rot90(data_to_show[i][pos, :, :], -1) + data = data_to_show[i][pos, :, :].T plt.imshow(np.log10(data) if logscales[i] else data, cmap=cmaps[i]) else: - data = np.rot90(data_to_show[i][:, :], -1) + data = data_to_show[i][:, :].T plt.imshow(np.log10(data) if logscales[i] else data, cmap=cmaps[i]) plt.colorbar() diff --git a/simpa_examples/minimal_optical_simulation.py b/simpa_examples/minimal_optical_simulation.py index 2367e24b..73961f87 100644 --- a/simpa_examples/minimal_optical_simulation.py +++ b/simpa_examples/minimal_optical_simulation.py @@ -10,8 +10,8 @@ import os os.environ["KMP_DUPLICATE_LIB_OK"] = "TRUE" -# TODO: Please make sure that a valid path_config.env file is located in your home directory, or that you -# point to the correct file in the PathManager(). +# TODO: Please make sure that you have set the correct path to MCX binary and SAVE_PATH in the file path_config.env +# located in the simpa_examples directory path_manager = sp.PathManager() VOLUME_TRANSDUCER_DIM_IN_MM = 60 diff --git a/simpa_examples/minimal_optical_simulation_uniform_cube.py b/simpa_examples/minimal_optical_simulation_uniform_cube.py new file mode 100644 index 00000000..39f299e5 --- /dev/null +++ b/simpa_examples/minimal_optical_simulation_uniform_cube.py @@ -0,0 +1,94 @@ +# SPDX-FileCopyrightText: 2021 Division of Intelligent Medical Systems, DKFZ +# SPDX-FileCopyrightText: 2021 Janek Groehl +# SPDX-License-Identifier: MIT + +# SPDX-FileCopyrightText: 2021 Division of Intelligent Medical Systems, DKFZ +# SPDX-License-Identifier: MIT + + +from simpa import Tags +import simpa as sp +import numpy as np + +# FIXME temporary workaround for newest Intel architectures +import os +os.environ["KMP_DUPLICATE_LIB_OK"] = "TRUE" + +# TODO: Please make sure that you have set the correct path to MCX binary as described in the README.md file. +path_manager = sp.PathManager() + +VOLUME_TRANSDUCER_DIM_IN_MM = 60 +VOLUME_PLANAR_DIM_IN_MM = 30 +VOLUME_HEIGHT_IN_MM = 60 +SPACING = 0.5 +RANDOM_SEED = 471 +VOLUME_NAME = "MyVolumeName_"+str(RANDOM_SEED) +SAVE_REFLECTANCE = True +SAVE_PHOTON_DIRECTION = False + +# If VISUALIZE is set to True, the simulation result will be plotted +VISUALIZE = True + + +def create_example_tissue(): + """ + This is a very simple example script of how to create a tissue definition. + It contains only a generic background tissue material. + """ + background_dictionary = sp.Settings() + background_dictionary[Tags.MOLECULE_COMPOSITION] = sp.TISSUE_LIBRARY.constant(1e-4, 1e-4, 0.9) + background_dictionary[Tags.STRUCTURE_TYPE] = Tags.BACKGROUND + + tissue_dict = sp.Settings() + tissue_dict[Tags.BACKGROUND] = background_dictionary + return tissue_dict + + +# Seed the numpy random configuration prior to creating the global_settings file in +# order to ensure that the same volume +# is generated with the same random seed every time. + +np.random.seed(RANDOM_SEED) + +general_settings = { + # These parameters set the general properties of the simulated volume + Tags.RANDOM_SEED: RANDOM_SEED, + Tags.VOLUME_NAME: VOLUME_NAME, + Tags.SIMULATION_PATH: path_manager.get_hdf5_file_save_path(), + Tags.SPACING_MM: SPACING, + Tags.DIM_VOLUME_Z_MM: VOLUME_HEIGHT_IN_MM, + Tags.DIM_VOLUME_X_MM: VOLUME_TRANSDUCER_DIM_IN_MM, + Tags.DIM_VOLUME_Y_MM: VOLUME_PLANAR_DIM_IN_MM, + Tags.WAVELENGTHS: [500], + Tags.DO_FILE_COMPRESSION: True +} + +settings = sp.Settings(general_settings) + +settings.set_volume_creation_settings({ + Tags.STRUCTURES: create_example_tissue() +}) +settings.set_optical_settings({ + Tags.OPTICAL_MODEL_NUMBER_PHOTONS: 5e7, + Tags.OPTICAL_MODEL_BINARY_PATH: path_manager.get_mcx_binary_path(), + Tags.COMPUTE_DIFFUSE_REFLECTANCE: SAVE_REFLECTANCE, + Tags.COMPUTE_PHOTON_DIRECTION_AT_EXIT: SAVE_PHOTON_DIRECTION +}) + +pipeline = [ + sp.ModelBasedVolumeCreationAdapter(settings), + sp.MCXAdapterReflectance(settings), +] + +device = sp.PencilBeamIlluminationGeometry(device_position_mm=np.asarray([VOLUME_TRANSDUCER_DIM_IN_MM/2, + VOLUME_PLANAR_DIM_IN_MM/2, 0])) + +sp.simulate(pipeline, settings, device) + +if VISUALIZE: + sp.visualise_data(path_to_hdf5_file=path_manager.get_hdf5_file_save_path() + "/" + VOLUME_NAME + ".hdf5", + wavelength=settings[Tags.WAVELENGTH], + show_initial_pressure=True, + show_absorption=True, + show_diffuse_reflectance=SAVE_REFLECTANCE, + log_scale=True) diff --git a/simpa_examples/path_config.env.example b/simpa_examples/path_config.env.example new file mode 100644 index 00000000..23b1bd16 --- /dev/null +++ b/simpa_examples/path_config.env.example @@ -0,0 +1,7 @@ +# Example path_config file. Please define all required paths for your simulation here. +# Afterwards, either copy this file to your current working directory, to your home directory, +# or to the SIMPA base directory, and rename it to path_config.env + +SIMPA_SAVE_PATH=/workplace/data # Path to a directory where all data will be stored. This path is always required. +MCX_BINARY_PATH=/workplace/mcx # On Linux systems, the .exe at the end must be omitted. This path is required if you plan to run optical simulations. +MATLAB_BINARY_PATH=/path/to/matlab.exe # On Linux systems, the .exe at the end must be omitted. This path is required if you plan to run acoustic simulations. diff --git a/simpa_examples/perform_iterative_qPAI_reconstruction.py b/simpa_examples/perform_iterative_qPAI_reconstruction.py index b47b6734..90d4ec93 100644 --- a/simpa_examples/perform_iterative_qPAI_reconstruction.py +++ b/simpa_examples/perform_iterative_qPAI_reconstruction.py @@ -208,7 +208,7 @@ def __init__(self): if i == 0: plt.ylabel("x-z", fontsize=10) plt.title(label[i], fontsize=10) - plt.imshow(np.rot90(quantity, -1)) + plt.imshow(quantity.T) plt.xticks(fontsize=6) plt.yticks(fontsize=6) plt.colorbar() @@ -222,7 +222,7 @@ def __init__(self): if i == 0: plt.ylabel("y-z", fontsize=10) plt.title(label[i], fontsize=10) - plt.imshow(np.rot90(quantity, -1)) + plt.imshow(quantity.T) plt.xticks(fontsize=6) plt.yticks(fontsize=6) plt.colorbar() diff --git a/simpa_tests/__init__.py b/simpa_tests/__init__.py index 26836a2e..89cc8954 100644 --- a/simpa_tests/__init__.py +++ b/simpa_tests/__init__.py @@ -1,16 +1,3 @@ # SPDX-FileCopyrightText: 2021 Division of Intelligent Medical Systems, DKFZ # SPDX-FileCopyrightText: 2021 Janek Groehl # SPDX-License-Identifier: MIT - -import glob -import os -import inspect - -base_path = os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe()))) - -files = glob.glob(os.path.join(base_path, "automatic_tests", "*/*.py"), recursive=True) -files += glob.glob(os.path.join(base_path, "automatic_tests", "*.py"), recursive=True) -files += glob.glob(os.path.join(base_path, "automatic_tests", "*/*/*.py"), recursive=True) - -automatic_test_classes = [file.replace(os.path.sep, ".")[file.replace(os.path.sep, ".").find("simpa_tests"):-3] - for file in files] diff --git a/simpa_tests/automatic_tests/device_tests/test_field_of_view.py b/simpa_tests/automatic_tests/device_tests/test_field_of_view.py index a57d0e94..cc63680c 100644 --- a/simpa_tests/automatic_tests/device_tests/test_field_of_view.py +++ b/simpa_tests/automatic_tests/device_tests/test_field_of_view.py @@ -3,7 +3,7 @@ # SPDX-License-Identifier: MIT import unittest -from simpa.core.device_digital_twins.detection_geometries.detection_geometry_base import DetectionGeometryBase +from simpa.core.device_digital_twins.detection_geometries import DetectionGeometryBase from simpa.log.file_logger import Logger from simpa.core.device_digital_twins.detection_geometries.linear_array import LinearArrayDetectionGeometry from simpa.core.simulation_modules.reconstruction_module.reconstruction_utils import compute_image_dimensions @@ -29,7 +29,7 @@ def _test(self, field_of_view_extent_mm: list, spacing_in_mm: float, detection_g return xdim, zdim, ydim, xdim_start, xdim_end, ydim_start, ydim_end, zdim_start, zdim_end - def symmetric_test(self): + def test_symmetric(self): image_dimensions = self._test([-25, 25, 0, 0, -12, 8], 0.2, self.detection_geometry) xdim, zdim, ydim, xdim_start, xdim_end, ydim_start, ydim_end, zdim_start, zdim_end = image_dimensions @@ -43,7 +43,7 @@ def symmetric_test(self): self.assertAlmostEqual(zdim_start, 0) self.assertAlmostEqual(zdim_end, 0) - def symmetric_test_with_small_spacing(self): + def test_symmetric_with_small_spacing(self): image_dimensions = self._test([-25, 25, 0, 0, -12, 8], 0.1, self.detection_geometry) xdim, zdim, ydim, xdim_start, xdim_end, ydim_start, ydim_end, zdim_start, zdim_end = image_dimensions @@ -57,7 +57,7 @@ def symmetric_test_with_small_spacing(self): self.assertAlmostEqual(zdim_start, 0) self.assertAlmostEqual(zdim_end, 0) - def unsymmetric_test_with_small_spacing(self): + def test_unsymmetric_with_small_spacing(self): image_dimensions = self._test([-25, 24.9, 0, 0, -12, 8], 0.1, self.detection_geometry) xdim, zdim, ydim, xdim_start, xdim_end, ydim_start, ydim_end, zdim_start, zdim_end = image_dimensions @@ -71,7 +71,7 @@ def unsymmetric_test_with_small_spacing(self): self.assertAlmostEqual(zdim_start, 0) self.assertAlmostEqual(zdim_end, 0) - def unsymmetric_test(self): + def test_unsymmetric(self): image_dimensions = self._test([-25, 24.9, 0, 0, -12, 8], 0.2, self.detection_geometry) xdim, zdim, ydim, xdim_start, xdim_end, ydim_start, ydim_end, zdim_start, zdim_end = image_dimensions @@ -85,7 +85,7 @@ def unsymmetric_test(self): self.assertAlmostEqual(zdim_start, 0) self.assertAlmostEqual(zdim_end, 0) - def symmetric_test_with_odd_number_of_elements(self): + def test_symmetric_with_odd_number_of_elements(self): """ The number of sensor elements should not affect the image dimensionality """ @@ -101,13 +101,3 @@ def symmetric_test_with_odd_number_of_elements(self): self.assertAlmostEqual(ydim_end, 40) self.assertAlmostEqual(zdim_start, 0) self.assertAlmostEqual(zdim_end, 0) - - -if __name__ == '__main__': - test = TestFieldOfView() - test.setUp() - test.symmetric_test() - test.symmetric_test_with_small_spacing() - test.unsymmetric_test_with_small_spacing() - test.unsymmetric_test() - test.symmetric_test_with_odd_number_of_elements() diff --git a/simpa_tests/automatic_tests/device_tests/test_rectangle_illumination.py b/simpa_tests/automatic_tests/device_tests/test_rectangle_illumination.py new file mode 100644 index 00000000..7fdd0545 --- /dev/null +++ b/simpa_tests/automatic_tests/device_tests/test_rectangle_illumination.py @@ -0,0 +1,67 @@ +# SPDX-FileCopyrightText: 2021 Division of Intelligent Medical Systems, DKFZ +# SPDX-FileCopyrightText: 2021 Janek Groehl +# SPDX-License-Identifier: MIT + +import unittest +import numpy as np +from simpa import Settings, Tags +from simpa.core.device_digital_twins import RectangleIlluminationGeometry + + +class TestRectangleIlluminationGeometry(unittest.TestCase): + def setUp(self) -> None: + self.test_object = RectangleIlluminationGeometry(length_mm=15, width_mm=12) + + def test_if_constructor_is_called_with_invalid_arguments_error_is_raised(self): + with self.assertRaises(TypeError): + RectangleIlluminationGeometry(length_mm=None, width_mm=12) + with self.assertRaises(TypeError): + RectangleIlluminationGeometry(length_mm=None, width_mm=None) + with self.assertRaises(AssertionError): + RectangleIlluminationGeometry(length_mm=0, width_mm=12) + with self.assertRaises(AssertionError): + RectangleIlluminationGeometry(length_mm=15, width_mm=0) + + def test_if_get_mcx_illuminator_definition_is_called_with_invalid_arguments_error_is_raised(self): + with self.assertRaises(AssertionError): + self.test_object.get_mcx_illuminator_definition(None) + + def test_if_get_mcx_illuminator_definition_is_called_correct_parameters_are_returned(self): + # Arrange + global_settings = Settings() + global_settings[Tags.SPACING_MM] = 0.2 + expected_dict = { + "Type": Tags.ILLUMINATION_TYPE_PLANAR, + "Pos": [0, 0, 0], + "Dir": [0, 0, 1], + "Param1": [61, 0, 0], + "Param2": [0, 76, 0] + } + + # Act + actual_dict = self.test_object.get_mcx_illuminator_definition(global_settings) + + # Assert + assert expected_dict == actual_dict + + def test_serialize_and_deserialize_are_inverse_to_each_other(self): + # Act + serialized_dict = self.test_object.serialize() + assert len(serialized_dict.keys()) == 1 + + class_name = list(serialized_dict.keys())[0] + deserialized_object = globals()[class_name].deserialize(serialized_dict[class_name]) + + # Assert + assert set(self.test_object.__dict__.keys()) == set(deserialized_object.__dict__.keys()) + for key, value in deserialized_object.__dict__.items(): + value2 = self.test_object.__dict__[key] + + if isinstance(value, np.ndarray): + assert np.array_equal(value, value2) + else: + assert value == value2 + + def test_if_deserialize_is_called_with_invalid_arguments_error_is_raised(self): + with self.assertRaises(AssertionError): + RectangleIlluminationGeometry.deserialize(None) diff --git a/simpa_tests/automatic_tests/device_tests/test_ring_illumination.py b/simpa_tests/automatic_tests/device_tests/test_ring_illumination.py new file mode 100644 index 00000000..42f396b0 --- /dev/null +++ b/simpa_tests/automatic_tests/device_tests/test_ring_illumination.py @@ -0,0 +1,101 @@ +# SPDX-FileCopyrightText: 2021 Division of Intelligent Medical Systems, DKFZ +# SPDX-FileCopyrightText: 2021 Janek Groehl +# SPDX-License-Identifier: MIT + +import unittest +import numpy as np +from simpa import Settings, Tags +from simpa.core.device_digital_twins import RingIlluminationGeometry + + +class TestRingIlluminationGeometry(unittest.TestCase): + def setUp(self) -> None: + self.test_object = RingIlluminationGeometry(outer_radius_in_mm=13.5, + inner_radius_in_mm=8, + lower_angular_bound=3.5, + upper_angular_bound=6.25) + + def test_if_constructor_is_called_with_invalid_arguments_error_is_raised(self): + with self.assertRaises(TypeError): + RingIlluminationGeometry(outer_radius_in_mm=None, + inner_radius_in_mm=8, + lower_angular_bound=3.5, + upper_angular_bound=6.25) + with self.assertRaises(TypeError): + RingIlluminationGeometry(outer_radius_in_mm=13.5, + inner_radius_in_mm=None, + lower_angular_bound=3.5, + upper_angular_bound=6.25) + with self.assertRaises(TypeError): + RingIlluminationGeometry(outer_radius_in_mm=13.5, + inner_radius_in_mm=8, + lower_angular_bound=None, + upper_angular_bound=6.25) + with self.assertRaises(TypeError): + RingIlluminationGeometry(outer_radius_in_mm=13.5, + inner_radius_in_mm=8, + lower_angular_bound=3.5, + upper_angular_bound=None) + with self.assertRaises(AssertionError): + RingIlluminationGeometry(outer_radius_in_mm=13.5, + inner_radius_in_mm=-1, + lower_angular_bound=0, + upper_angular_bound=0) + with self.assertRaises(AssertionError): + RingIlluminationGeometry(outer_radius_in_mm=7.5, + inner_radius_in_mm=8, + lower_angular_bound=0, + upper_angular_bound=0) + with self.assertRaises(AssertionError): + RingIlluminationGeometry(outer_radius_in_mm=13, + inner_radius_in_mm=8, + lower_angular_bound=-1, + upper_angular_bound=0) + with self.assertRaises(AssertionError): + RingIlluminationGeometry(outer_radius_in_mm=13, + inner_radius_in_mm=8, + lower_angular_bound=15.5, + upper_angular_bound=12) + + def test_if_get_mcx_illuminator_definition_is_called_with_invalid_arguments_error_is_raised(self): + with self.assertRaises(AssertionError): + self.test_object.get_mcx_illuminator_definition(None) + + def test_if_get_mcx_illuminator_definition_is_called_correct_parameters_are_returned(self): + # Arrange + global_settings = Settings() + global_settings[Tags.SPACING_MM] = 0.2 + expected_dict = { + "Type": Tags.ILLUMINATION_TYPE_RING, + "Pos": [1, 1, 1], + "Dir": [0, 0, 1], + "Param1": [67.5, 40, 3.5, 6.25] + } + + # Act + actual_dict = self.test_object.get_mcx_illuminator_definition(global_settings) + + # Assert + assert expected_dict == actual_dict + + def test_serialize_and_deserialize_are_inverse_to_each_other(self): + # Act + serialized_dict = self.test_object.serialize() + assert len(serialized_dict.keys()) == 1 + + class_name = list(serialized_dict.keys())[0] + deserialized_object = globals()[class_name].deserialize(serialized_dict[class_name]) + + # Assert + assert set(self.test_object.__dict__.keys()) == set(deserialized_object.__dict__.keys()) + for key, value in deserialized_object.__dict__.items(): + value2 = self.test_object.__dict__[key] + + if isinstance(value, np.ndarray): + assert np.array_equal(value, value2) + else: + assert value == value2 + + def test_if_deserialize_is_called_with_invalid_arguments_error_is_raised(self): + with self.assertRaises(AssertionError): + RingIlluminationGeometry.deserialize(None) diff --git a/simpa_tests/automatic_tests/structure_tests/test_elliptical_tubes.py b/simpa_tests/automatic_tests/structure_tests/test_elliptical_tubes.py index cd25ecd8..e20681ee 100644 --- a/simpa_tests/automatic_tests/structure_tests/test_elliptical_tubes.py +++ b/simpa_tests/automatic_tests/structure_tests/test_elliptical_tubes.py @@ -36,17 +36,17 @@ def setUp(self): ) def assert_values(self, volume, values): - assert abs(volume[0][0][0] - values[0]) < 1e-5, "excpected " + \ + assert abs(volume[0][0][0] - values[0]) < 1e-5, "expected " + \ str(values[0]) + " but was " + str(volume[0][0][0]) - assert abs(volume[0][0][1] - values[1]) < 1e-5, "excpected " + \ + assert abs(volume[0][0][1] - values[1]) < 1e-5, "expected " + \ str(values[1]) + " but was " + str(volume[0][0][1]) - assert abs(volume[0][0][2] - values[2]) < 1e-5, "excpected " + \ + assert abs(volume[0][0][2] - values[2]) < 1e-5, "expected " + \ str(values[2]) + " but was " + str(volume[0][0][2]) - assert abs(volume[0][0][3] - values[3]) < 1e-5, "excpected " + \ + assert abs(volume[0][0][3] - values[3]) < 1e-5, "expected " + \ str(values[3]) + " but was " + str(volume[0][0][3]) - assert abs(volume[0][0][4] - values[4]) < 1e-5, "excpected " + \ + assert abs(volume[0][0][4] - values[4]) < 1e-5, "expected " + \ str(values[4]) + " but was " + str(volume[0][0][4]) - assert abs(volume[0][0][5] - values[5]) < 1e-5, "excpected " + \ + assert abs(volume[0][0][5] - values[5]) < 1e-5, "expected " + \ str(values[5]) + " but was " + str(volume[0][0][5]) def test_elliptical_tube_structures_partial_volume_within_one_voxel(self): diff --git a/simpa_tests/automatic_tests/structure_tests/test_vesseltree.py b/simpa_tests/automatic_tests/structure_tests/test_vesseltree.py index f60c2d4d..af3cbaf4 100644 --- a/simpa_tests/automatic_tests/structure_tests/test_vesseltree.py +++ b/simpa_tests/automatic_tests/structure_tests/test_vesseltree.py @@ -4,6 +4,8 @@ import unittest import numpy as np +import torch +from skimage import measure from simpa.utils.libraries.tissue_library import TISSUE_LIBRARY from simpa.utils import Tags from simpa.utils.settings import Settings @@ -15,18 +17,18 @@ class TestVesselTree(unittest.TestCase): def setUp(self): self.global_settings = Settings() self.global_settings[Tags.SPACING_MM] = 1 - - self.global_settings[Tags.DIM_VOLUME_X_MM] = 5 - self.global_settings[Tags.DIM_VOLUME_Y_MM] = 5 - self.global_settings[Tags.DIM_VOLUME_Z_MM] = 5 + self.global_settings[Tags.RANDOM_SEED] = 42 + self.global_settings[Tags.DIM_VOLUME_X_MM] = 10 + self.global_settings[Tags.DIM_VOLUME_Y_MM] = 10 + self.global_settings[Tags.DIM_VOLUME_Z_MM] = 10 self.vesseltree_settings = Settings() - self.vesseltree_settings[Tags.STRUCTURE_START_MM] = [0, 0, 0] - self.vesseltree_settings[Tags.STRUCTURE_RADIUS_MM] = 5 + self.vesseltree_settings[Tags.STRUCTURE_START_MM] = [5, 0, 5] + self.vesseltree_settings[Tags.STRUCTURE_RADIUS_MM] = 2 self.vesseltree_settings[Tags.STRUCTURE_DIRECTION] = [0, 1, 0] - self.vesseltree_settings[Tags.STRUCTURE_BIFURCATION_LENGTH_MM] = 5 - self.vesseltree_settings[Tags.STRUCTURE_RADIUS_VARIATION_FACTOR] = 1.0 - self.vesseltree_settings[Tags.STRUCTURE_CURVATURE_FACTOR] = 1.0 + self.vesseltree_settings[Tags.STRUCTURE_BIFURCATION_LENGTH_MM] = 20 + self.vesseltree_settings[Tags.STRUCTURE_RADIUS_VARIATION_FACTOR] = 0 + self.vesseltree_settings[Tags.STRUCTURE_CURVATURE_FACTOR] = 0 self.vesseltree_settings[Tags.MOLECULE_COMPOSITION] = TISSUE_LIBRARY.muscle() self.vesseltree_settings[Tags.ADHERE_TO_DEFORMATION] = True self.vesseltree_settings[Tags.CONSIDER_PARTIAL_VOLUME] = True @@ -37,6 +39,74 @@ def setUp(self): } ) + def test_bifurcation(self): + """ + Test bifurcation + Let bifurcation occur once at a large angle and see if the branch does indeed split into two, thus two spots + at the edges + WARNING: this method uses a pre-specified random seed which ensures ONE SPLIT for the CURRENT pipeline. + :return: Assertion for if bifurcation occurs once + """ + torch.manual_seed(10) + self.global_settings[Tags.SPACING_MM] = 0.04 + self.vesseltree_settings[Tags.STRUCTURE_RADIUS_MM] = 0.5 + self.vesseltree_settings[Tags.STRUCTURE_BIFURCATION_LENGTH_MM] = 7 + self.vesseltree_settings[Tags.STRUCTURE_CURVATURE_FACTOR] = 0.1 + ts = VesselStructure(self.global_settings, self.vesseltree_settings) + + end_plane = ts.geometrical_volume[:, -1, :] + top_plane = ts.geometrical_volume[:, :, -1] + bottom_plane = ts.geometrical_volume[:, :, 0] + left_plane = ts.geometrical_volume[0, :, :] + right_plane = ts.geometrical_volume[-1, :, :] + + end_plane_count = measure.label(end_plane, background=0, return_num=True)[1] + top_plane_count = measure.label(top_plane, background=0, return_num=True)[1] + bottom_plane_count = measure.label(bottom_plane, background=0, return_num=True)[1] + left_plane_count = measure.label(left_plane, background=0, return_num=True)[1] + right_plane_count = measure.label(right_plane, background=0, return_num=True)[1] + has_split = end_plane_count + top_plane_count + bottom_plane_count + left_plane_count + right_plane_count == 2 + + assert has_split + + def test_radius_variation_factor(self): + """ + Test radius variation factor + Let there be no bifurcation or curvature, and see if the radius changes + :return: Assertion for radius variation + """ + self.vesseltree_settings[Tags.STRUCTURE_RADIUS_MM] = 2.5 + self.vesseltree_settings[Tags.STRUCTURE_RADIUS_VARIATION_FACTOR] = 1 + ts = VesselStructure(self.global_settings, self.vesseltree_settings) + + vessel_centre = 5 + edge_of_vessel = vessel_centre + self.vesseltree_settings[Tags.STRUCTURE_RADIUS_MM] + has_reduced = np.min(ts.geometrical_volume[int(edge_of_vessel-0.5), :, vessel_centre]) != 1 + has_increased = np.max(ts.geometrical_volume[int(edge_of_vessel+0.5), :, vessel_centre]) != 0 + + assert has_reduced or has_increased + + def test_curvature_factor(self): + """ + Test curvature factor + Let there be no bifurcation or radius change and observe if the vessel leaves its original trajectory, + therefore breaching the imaginary box put around it + :return: Assertion of curvature + """ + curvature_factor = 0.2 + self.vesseltree_settings[Tags.STRUCTURE_CURVATURE_FACTOR] = curvature_factor + ts = VesselStructure(self.global_settings, self.vesseltree_settings) + radius = self.vesseltree_settings[Tags.STRUCTURE_RADIUS_MM] + vessel_centre = 5 + + edge_of_vessel = radius + vessel_centre + has_breached_top = np.max(np.nditer(ts.geometrical_volume[:, :, edge_of_vessel+1])) != 0 + has_breached_bottom = np.max(np.nditer(ts.geometrical_volume[:, :, -edge_of_vessel-1])) != 0 + has_breached_left = np.max(np.nditer(ts.geometrical_volume[-edge_of_vessel-1, :, :])) != 0 + has_breached_right = np.max(np.nditer(ts.geometrical_volume[edge_of_vessel+1, :, :])) != 0 + + assert has_breached_top or has_breached_bottom or has_breached_left or has_breached_right + def test_vessel_tree_geometrical_volume(self): ts = VesselStructure(self.global_settings, self.vesseltree_settings) for value in np.nditer(ts.geometrical_volume): diff --git a/simpa_tests/automatic_tests/test_bandpass_filter.py b/simpa_tests/automatic_tests/test_bandpass_filter.py index 6287515f..404527d1 100644 --- a/simpa_tests/automatic_tests/test_bandpass_filter.py +++ b/simpa_tests/automatic_tests/test_bandpass_filter.py @@ -260,13 +260,3 @@ def test_butter_filter_with_random_signal(self, show_figure_on_screen=False): if show_figure_on_screen: self.visualize_filtered_spectrum(FILTERED_SIGNAL) - - -if __name__ == '__main__': - test = TestBandpassFilter() - test.setUp() - test.test_tukey_bandpass_filter(show_figure_on_screen=False) - test.test_tukey_window_function(show_figure_on_screen=False) - test.test_butter_bandpass_filter(show_figure_on_screen=False) - test.test_tukey_filter_with_random_signal(show_figure_on_screen=False) - test.test_butter_filter_with_random_signal(show_figure_on_screen=False) diff --git a/simpa_tests/automatic_tests/test_device_UUID.py b/simpa_tests/automatic_tests/test_device_UUID.py index b7db6c5b..89d0e6b4 100644 --- a/simpa_tests/automatic_tests/test_device_UUID.py +++ b/simpa_tests/automatic_tests/test_device_UUID.py @@ -12,7 +12,7 @@ class TestDeviceUUID(unittest.TestCase): - def testUUIDGeneration(self): + def test_UUIDGeneration(self): device1 = RSOMExplorerP50() device2 = InVision256TF() device3 = MSOTAcuityEcho() diff --git a/simpa_tests/automatic_tests/test_equal.py b/simpa_tests/automatic_tests/test_equal.py new file mode 100644 index 00000000..af0a7d3f --- /dev/null +++ b/simpa_tests/automatic_tests/test_equal.py @@ -0,0 +1,78 @@ +# SPDX-FileCopyrightText: 2021 Division of Intelligent Medical Systems, DKFZ +# SPDX-FileCopyrightText: 2021 Janek Groehl +# SPDX-License-Identifier: MIT + +import unittest +import numpy as np +from simpa.utils.calculate import are_equal + + +class TestEqual(unittest.TestCase): + + def setUp(self): + self.list1 = [1, 2, 3] + self.list2 = [1, 2, 3] + self.list_unqeual = [1, 2, 4] + self.array1 = np.array([1, 2, 3]) + self.array2 = np.array([1, 2, 3]) + self.array_unequal = np.array([1, 2, 4]) + self.scalar1 = 1 + self.scalar2 = 1 + self.scalar3 = 2.0 + self.scalar4 = 2.0 + self.scalarunequal1 = 2 + self.scalarunequal2 = 1.0 + self.string1 = 'Test1' + self.string2 = 'Test1' + self.string_unequal = 'Test2' + self.nested_list1 = [self.list1, self.array1] + self.nested_list2 = [self.list2, self.array2] + self.nested_list_unequal = [self.list_unqeual, self.array_unequal] + + def test_lists_are_equal(self): + self.assertTrue(are_equal(self.list1, self.list2)) + + @unittest.expectedFailure + def test_lists_are_unequal(self): + self.assertTrue(are_equal(self.list1, self.list_unqeual)) + + def test_numpy_arrays_are_equal(self): + self.assertTrue(are_equal(self.array1, self.array2)) + + @unittest.expectedFailure + def test_numpy_arrays_are_unequal(self): + self.assertTrue(are_equal(self.array1, self.array_unequal)) + + def test_scalars_are_equal(self): + self.assertTrue(are_equal(self.scalar1, self.scalar2)) + self.assertTrue(are_equal(self.scalar3, self.scalar4)) + + @unittest.expectedFailure + def test_scalars_are_unequal(self): + self.assertTrue(are_equal(self.scalar1, self.scalarunequal1)) + self.assertTrue(are_equal(self.scalar3, self.scalarunequal2)) + + def test_strings_are_equal(self): + self.assertTrue(are_equal(self.string1, self.string2)) + + @unittest.expectedFailure + def test_strings_are_unequal(self): + self.assertTrue(are_equal(self.string1, self.string_unequal)) + + def test_mixed_types_are_equal(self): + self.assertTrue(are_equal(self.list1, self.array1)) + self.assertTrue(are_equal(self.array1, self.list1)) + + @unittest.expectedFailure + def test_mixed_types_are_unequal(self): + self.assertTrue(are_equal(self.list1, self.scalar1)) + self.assertTrue(are_equal(self.array1, self.scalar1)) + self.assertTrue(are_equal(self.scalar1, self.scalar3)) + self.assertTrue(are_equal(self.string1, self.scalar1)) + + def test_nested_lists_are_equal(self): + self.assertTrue(are_equal(self.nested_list1, self.nested_list2)) + + @unittest.expectedFailure + def test_nested_lists_are_unequal(self): + self.assertTrue(are_equal(self.nested_list1, self.nested_list_unequal)) diff --git a/simpa_tests/automatic_tests/test_io_handling.py b/simpa_tests/automatic_tests/test_io_handling.py index 456e87b6..2423362f 100644 --- a/simpa_tests/automatic_tests/test_io_handling.py +++ b/simpa_tests/automatic_tests/test_io_handling.py @@ -3,6 +3,7 @@ # SPDX-License-Identifier: MIT import unittest + from simpa.io_handling import load_hdf5 from simpa.io_handling import save_hdf5 from simpa.utils import Tags @@ -63,7 +64,7 @@ def test_write_and_read_devices(self): det_geometries = [CurvedArrayDetectionGeometry, LinearArrayDetectionGeometry, PlanarArrayDetectionGeometry] ill_geometries = [SlitIlluminationGeometry, GaussianBeamIlluminationGeometry, PencilArrayIlluminationGeometry, PencilBeamIlluminationGeometry, DiskIlluminationGeometry, MSOTAcuityIlluminationGeometry, - MSOTInVisionIlluminationGeometry] + MSOTInVisionIlluminationGeometry, RectangleIlluminationGeometry, RingIlluminationGeometry] # Test all predefined PA devices for device in pa_devices: diff --git a/simpa_tests/automatic_tests/test_path_manager.py b/simpa_tests/automatic_tests/test_path_manager.py index fa675d9f..11f4e565 100644 --- a/simpa_tests/automatic_tests/test_path_manager.py +++ b/simpa_tests/automatic_tests/test_path_manager.py @@ -2,12 +2,14 @@ # SPDX-FileCopyrightText: 2021 Janek Groehl # SPDX-License-Identifier: MIT -import unittest -import os import inspect -from simpa.utils import PathManager +import os +import unittest +from unittest.mock import patch from pathlib import Path -from dotenv import unset_key + +from simpa.utils import PathManager +from simpa import Tags class TestPathManager(unittest.TestCase): @@ -19,7 +21,7 @@ def setUp(self): self.file_content = (f"# Example path_config file. Please define all required paths for your simulation here.\n" f"# Afterwards, either copy this file to your current working directory, to your home directory,\n" f"# or to the SIMPA base directry.\n" - f"SAVE_PATH={self.save_path}\n" + f"SIMPA_SAVE_PATH={self.save_path}\n" f"MCX_BINARY_PATH={self.mcx_path}\n" f"MATLAB_BINARY_PATH={self.matlab_path}") self.home_file = str(Path.home()) + self.path @@ -31,8 +33,18 @@ def setUp(self): self.simpa_home_exists = os.path.exists(self.simpa_home) @unittest.expectedFailure - def test_instantiate_path_manager_with_wrong_path(self): - PathManager("rubbish/path/does/not/exist") + def test_variables_not_set(): + path_manager = PathManager() + _ = path_manager.get_mcx_binary_path() + _ = path_manager.get_hdf5_file_save_path() + _ = path_manager.get_matlab_binary_path() + + @patch.dict(os.environ, {Tags.SIMPA_SAVE_PATH_VARNAME: "test_simpa_save_path", + Tags.MCX_BINARY_PATH_VARNAME: "test_mcx_path"}) + def test_instantiate_without_file(self): + path_manager = PathManager() + self.assertEqual(path_manager.get_mcx_binary_path(), "test_mcx_path") + self.assertEqual(path_manager.get_hdf5_file_save_path(), "test_simpa_save_path") def test_instantiate_when_file_is_in_home(self): @@ -47,26 +59,6 @@ def test_instantiate_when_file_is_in_home(self): if self.home_file_exists: self.restore_config_file(self.home_file) - @unittest.expectedFailure - def test_fail_if_no_default_directories_set(self): - - if self.home_file_exists: - self.hide_config_file(self.home_file) - if self.cwd_file_exists: - self.hide_config_file(self.cwd_file) - if self.simpa_home_exists: - self.hide_config_file(self.simpa_home) - - try: - PathManager() - finally: - if self.home_file_exists: - self.restore_config_file(self.home_file) - if self.cwd_file_exists: - self.restore_config_file(self.cwd_file) - if self.simpa_home_exists: - self.restore_config_file(self.simpa_home) - def test_instantiate_when_file_is_in_cwd(self): if self.home_file_exists: self.hide_config_file(self.home_file) @@ -86,22 +78,26 @@ def test_instantiate_when_file_is_in_cwd(self): self.delete_config_file(self.cwd_file) def test_instantiate_when_file_is_in_simpa_home(self): - if self.home_file_exists: - self.hide_config_file(self.home_file) - if self.cwd_file_exists: - self.hide_config_file(self.cwd_file) - if not self.simpa_home_exists: + pathmanager_source_file = inspect.getsourcefile(PathManager) + if "site-packages" not in pathmanager_source_file: + if self.home_file_exists: + self.hide_config_file(self.home_file) + if self.cwd_file_exists: + self.hide_config_file(self.cwd_file) + if self.simpa_home_exists: + self.hide_config_file(self.simpa_home) self.write_config_file(self.simpa_home) - path_manager = PathManager() - self.check_path_manager_correctly_loaded(path_manager) + path_manager = PathManager() + self.check_path_manager_correctly_loaded(path_manager) - if self.home_file_exists: - self.restore_config_file(self.home_file) - if self.cwd_file_exists: - self.restore_config_file(self.cwd_file) - if not self.simpa_home_exists: + if self.home_file_exists: + self.restore_config_file(self.home_file) + if self.cwd_file_exists: + self.restore_config_file(self.cwd_file) self.delete_config_file(self.simpa_home) + if self.simpa_home_exists: + self.restore_config_file(self.simpa_home) def check_path_manager_correctly_loaded(self, path_manager: PathManager): self.assertEqual(path_manager.get_hdf5_file_save_path(), self.save_path) diff --git a/simpa_tests/automatic_tests/tissue_library/test_tissue_library.py b/simpa_tests/automatic_tests/tissue_library/test_tissue_library.py new file mode 100644 index 00000000..b36a734b --- /dev/null +++ b/simpa_tests/automatic_tests/tissue_library/test_tissue_library.py @@ -0,0 +1,80 @@ +# SPDX-FileCopyrightText: 2021 Division of Intelligent Medical Systems, DKFZ +# SPDX-FileCopyrightText: 2021 Janek Groehl +# SPDX-License-Identifier: MIT + +import numpy as np +import pytest + +from simpa import Spectrum, SegmentationClasses, TISSUE_LIBRARY, Molecule + + +def test_if_optical_parameter_spectra_are_provided_correct_tissue_definition_is_returned_from_generic_tissue(): + wavelengths_sample = np.arange(400, 800, 25) + mua_sample = np.linspace(1e-6, 1e-5, wavelengths_sample.shape[0]) + mus_sample = np.linspace(1e-5, 5e-6, wavelengths_sample.shape[0]) + g_sample = np.linspace(0.8, 0.9, wavelengths_sample.shape[0]) + + mua_spectrum = Spectrum("Mua", wavelengths_sample, mua_sample) + mus_spectrum = Spectrum("Mus", wavelengths_sample, mus_sample) + g_spectrum = Spectrum("g", wavelengths_sample, g_sample) + + actual_tissue = TISSUE_LIBRARY.generic_tissue(mua_spectrum, mus_spectrum, g_spectrum) + assert actual_tissue.segmentation_type == SegmentationClasses.GENERIC + + assert len(actual_tissue) == 1 + molecule: Molecule = actual_tissue[0] + assert molecule.volume_fraction == 1.0 + assert molecule.spectrum == mua_spectrum + assert molecule.scattering_spectrum == mus_spectrum + assert molecule.anisotropy_spectrum == g_spectrum + + +def test_if_generic_tissue_is_called_with_invalid_arguments_error_is_raised(): + wavelengths_sample = np.arange(400, 800, 25) + mua_sample = np.linspace(1e-6, 1e-5, wavelengths_sample.shape[0]) + mus_sample = np.linspace(1e-5, 5e-6, wavelengths_sample.shape[0]) + g_sample = np.linspace(0.8, 0.9, wavelengths_sample.shape[0]) + + mua_spectrum = Spectrum("Mua", wavelengths_sample, mua_sample) + mus_spectrum = Spectrum("Mus", wavelengths_sample, mus_sample) + g_spectrum = Spectrum("g", wavelengths_sample, g_sample) + + with pytest.raises(AssertionError): + TISSUE_LIBRARY.generic_tissue(None, mus_spectrum, g_spectrum) + + with pytest.raises(AssertionError): + TISSUE_LIBRARY.generic_tissue(mua_spectrum, None, g_spectrum) + + with pytest.raises(AssertionError): + TISSUE_LIBRARY.generic_tissue(mua_spectrum, mus_spectrum, None) + + +def test_if_optical_parameter_spectra_are_provided_correct_tissue_definition_is_returned_from_constant(): + mua_sample = 1e-5 + mus_sample = 3e-6 + g_sample = 0.85 + + actual_tissue = TISSUE_LIBRARY.constant(mua_sample, mus_sample, g_sample) + assert actual_tissue.segmentation_type == SegmentationClasses.GENERIC + + assert len(actual_tissue) == 1 + molecule: Molecule = actual_tissue[0] + assert molecule.volume_fraction == 1.0 + assert (molecule.spectrum.values == mua_sample).all() + assert (molecule.scattering_spectrum.values == mus_sample).all() + assert (molecule.anisotropy_spectrum.values == g_sample).all() + + +def test_if_constant_is_called_with_invalid_arguments_error_is_raised(): + mua_sample = 1e-5 + mus_sample = 3e-6 + g_sample = 0.85 + + with pytest.raises(TypeError): + TISSUE_LIBRARY.constant(None, mus_sample, g_sample) + + with pytest.raises(TypeError): + TISSUE_LIBRARY.constant(mua_sample, None, g_sample) + + with pytest.raises(TypeError): + TISSUE_LIBRARY.constant(mua_sample, mus_sample, None) diff --git a/simpa_tests/do_coverage.py b/simpa_tests/do_coverage.py index 64b7c5d5..88490637 100644 --- a/simpa_tests/do_coverage.py +++ b/simpa_tests/do_coverage.py @@ -4,19 +4,19 @@ import unittest from coverage import Coverage -from simpa_tests import automatic_test_classes import sys - import os + os.environ["KMP_DUPLICATE_LIB_OK"] = "TRUE" cov = Coverage(source=['simpa']) cov.start() -suite = unittest.TestSuite() -for test_class in automatic_test_classes: - suite.addTests(unittest.defaultTestLoader.loadTestsFromName(test_class)) -automatic_test_return = not unittest.TextTestRunner().run(suite).wasSuccessful() +# Discover all tests in the 'simpa_tests' package +loader = unittest.TestLoader() +tests = loader.discover('automatic_tests') # Specify the directory where your tests are located +runner = unittest.TextTestRunner() +result = runner.run(tests) cov.stop() cov.save() @@ -24,4 +24,5 @@ cov.report(skip_empty=True, skip_covered=False) cov.html_report(directory="../docs/test_coverage") -sys.exit(automatic_test_return) +# Exit with an appropriate code based on the test results +sys.exit(not result.wasSuccessful()) diff --git a/simpa_tests/manual_tests/__init__.py b/simpa_tests/manual_tests/__init__.py index 68469a9a..79a6a5b1 100644 --- a/simpa_tests/manual_tests/__init__.py +++ b/simpa_tests/manual_tests/__init__.py @@ -228,15 +228,15 @@ def visualise_result(self, show_figure_on_screen=True, save_path=None): plt.figure(figsize=(9, 3)) plt.subplot(1, 3, 1) plt.title("Initial pressure") - plt.imshow(np.rot90(initial_pressure[:, 20, :], 3)) + plt.imshow(initial_pressure[:, 20, :].T) plt.subplot(1, 3, 2) plt.title("Pipeline\nReconstruction") if self.reconstructed_image_pipeline is not None: - plt.imshow(np.rot90(self.reconstructed_image_pipeline, 3)) + plt.imshow(self.reconstructed_image_pipeline.T) plt.subplot(1, 3, 3) plt.title("Convenience Method\nReconstruction") if self.reconstructed_image_convenience is not None: - plt.imshow(np.rot90(self.reconstructed_image_convenience, 3)) + plt.imshow(self.reconstructed_image_convenience.T) plt.tight_layout() if show_figure_on_screen: diff --git a/simpa_tests/manual_tests/acoustic_forward_models/KWaveAcousticForwardConvenienceFunction.py b/simpa_tests/manual_tests/acoustic_forward_models/KWaveAcousticForwardConvenienceFunction.py index 00cf8bff..a9157c33 100644 --- a/simpa_tests/manual_tests/acoustic_forward_models/KWaveAcousticForwardConvenienceFunction.py +++ b/simpa_tests/manual_tests/acoustic_forward_models/KWaveAcousticForwardConvenienceFunction.py @@ -33,7 +33,7 @@ class KWaveAcousticForwardConvenienceFunction(ManualIntegrationTestClass): def setup(self): """ Runs a pipeline consisting of volume creation and optical simulation. The resulting hdf5 file of the - simple test volume is saved at SAVE_PATH location defined in the path_config.env file. + simple test volume is saved at SIMPA_SAVE_PATH location defined in the path_config.env file. """ self.path_manager = PathManager() @@ -83,7 +83,7 @@ def setup(self): self.VOLUME_PLANAR_DIM_IN_MM/2, 0])) self.device.set_detection_geometry(LinearArrayDetectionGeometry(device_position_mm=self.device.device_position_mm, pitch_mm=0.25, - number_detector_elements=200)) + number_detector_elements=200, field_of_view_extent_mm=[-37.5, 37.5, 0, 0, 0, 25])) self.device.add_illumination_geometry(SlitIlluminationGeometry(slit_vector_mm=[100, 0, 0])) # run pipeline including volume creation and optical mcx simulation @@ -107,28 +107,13 @@ def test_convenience_function(self): self.VOLUME_NAME + ".hdf5", Tags.DATA_FIELD_INITIAL_PRESSURE, wavelength=700) image_slice = np.s_[:, 40, :] - self.initial_pressure = np.rot90(initial_pressure[image_slice], -1) - - # define acoustic settings and run simulation with convenience function - acoustic_settings = { - Tags.ACOUSTIC_SIMULATION_3D: True, - Tags.ACOUSTIC_MODEL_BINARY_PATH: self.path_manager.get_matlab_binary_path(), - Tags.KWAVE_PROPERTY_ALPHA_POWER: 0.00, - Tags.KWAVE_PROPERTY_SENSOR_RECORD: "p", - Tags.KWAVE_PROPERTY_PMLInside: False, - Tags.KWAVE_PROPERTY_PMLSize: [31, 32], - Tags.KWAVE_PROPERTY_PMLAlpha: 1.5, - Tags.KWAVE_PROPERTY_PlotPML: False, - Tags.RECORDMOVIE: False, - Tags.MOVIENAME: "visualization_log", - Tags.ACOUSTIC_LOG_SCALE: True, - Tags.MODEL_SENSOR_FREQUENCY_RESPONSE: False - } + self.initial_pressure = initial_pressure[image_slice].T + time_series_data = perform_k_wave_acoustic_forward_simulation(initial_pressure=self.initial_pressure, detection_geometry=self.device. get_detection_geometry(), speed_of_sound=1540, density=1000, - alpha_coeff=0.0) + alpha_coeff=0.0, spacing_mm=0.25) # reconstruct the time series data to compare it with initial pressure self.settings.set_reconstruction_settings({ @@ -155,7 +140,7 @@ def visualise_result(self, show_figure_on_screen=True, save_path=None): plt.imshow(self.initial_pressure) plt.subplot(2, 2, 2) plt.title("Reconstructed Image Pipeline") - plt.imshow(np.rot90(self.reconstructed, -1)) + plt.imshow(self.reconstructed.T) plt.tight_layout() if show_figure_on_screen: plt.show() diff --git a/simpa_tests/manual_tests/acoustic_forward_models/MinimalKWaveTest.py b/simpa_tests/manual_tests/acoustic_forward_models/MinimalKWaveTest.py index 6afebeb6..7f781d5b 100644 --- a/simpa_tests/manual_tests/acoustic_forward_models/MinimalKWaveTest.py +++ b/simpa_tests/manual_tests/acoustic_forward_models/MinimalKWaveTest.py @@ -121,13 +121,13 @@ def visualise_result(self, show_figure_on_screen=True, save_path=None): plt.figure(figsize=(9, 3)) plt.subplot(1, 3, 1) plt.title(f"{self.SPEED_OF_SOUND * 0.975} m/s") - plt.imshow(np.rot90(self.reconstructed_image_950, 3)) + plt.imshow(self.reconstructed_image_950.T) plt.subplot(1, 3, 2) plt.title(f"{self.SPEED_OF_SOUND} m/s") - plt.imshow(np.rot90(self.reconstructed_image_1000, 3)) + plt.imshow(self.reconstructed_image_1000.T) plt.subplot(1, 3, 3) plt.title(f"{self.SPEED_OF_SOUND * 1.025} m/s") - plt.imshow(np.rot90(self.reconstructed_image_1050, 3)) + plt.imshow(self.reconstructed_image_1050.T) plt.tight_layout() if show_figure_on_screen: plt.show() diff --git a/simpa_tests/manual_tests/processing_components/QPAIReconstruction.py b/simpa_tests/manual_tests/processing_components/QPAIReconstruction.py index 2a4eff98..c262bcbb 100644 --- a/simpa_tests/manual_tests/processing_components/QPAIReconstruction.py +++ b/simpa_tests/manual_tests/processing_components/QPAIReconstruction.py @@ -30,7 +30,7 @@ class TestqPAIReconstruction(ManualIntegrationTestClass): def setup(self): """ Runs a pipeline consisting of volume creation and optical simulation. The resulting hdf5 file of the - simple test volume is saved at SAVE_PATH location defined in the path_config.env file. + simple test volume is saved at SIMPA_SAVE_PATH location defined in the path_config.env file. """ self.path_manager = PathManager() @@ -178,7 +178,7 @@ def visualise_result(self, show_figure_on_screen=True, save_path=None): plt.subplot(4, int(np.ceil(len(self.list_2d_reconstructed_absorptions) / 2)), i + 1) if i == 0: plt.ylabel("y-z", fontsize=10) - plt.imshow(np.rot90(quantity, -1)) + plt.imshow(quantity.T) plt.title(label[i], fontsize=10) plt.xticks(fontsize=6) plt.yticks(fontsize=6) @@ -193,7 +193,7 @@ def visualise_result(self, show_figure_on_screen=True, save_path=None): i + int(np.ceil(len(self.list_2d_reconstructed_absorptions) / 2)) + 1) if i == 0: plt.ylabel("x-z", fontsize=10) - plt.imshow(np.rot90(quantity, -1)) + plt.imshow(quantity.T) plt.title(label[i], fontsize=10) plt.xticks(fontsize=6) plt.yticks(fontsize=6) @@ -207,7 +207,7 @@ def visualise_result(self, show_figure_on_screen=True, save_path=None): plt.subplot(4, int(np.ceil(len(self.list_2d_reconstructed_absorptions) / 2)), i + 2 * int(np.ceil(len(self.list_2d_reconstructed_absorptions) / 2)) + 1) plt.title("Iteration step: " + str(i + 1), fontsize=8) - plt.imshow(np.rot90(quantity, -1)) # absorption maps in list are already 2-d + plt.imshow(quantity.T) # absorption maps in list are already 2-d plt.colorbar() plt.clim(cmin, cmax) plt.axis('off') diff --git a/simpa_tests/manual_tests/processing_components/TestLinearUnmixingVisual.py b/simpa_tests/manual_tests/processing_components/TestLinearUnmixingVisual.py index b0382880..38896d64 100644 --- a/simpa_tests/manual_tests/processing_components/TestLinearUnmixingVisual.py +++ b/simpa_tests/manual_tests/processing_components/TestLinearUnmixingVisual.py @@ -104,15 +104,15 @@ def visualise_result(self, show_figure_on_screen=True, save_path=None): plt.suptitle("Linear Unmixing - Visual Test") plt.subplot(131) plt.title("Ground Truth sO2 [%]") - plt.imshow(np.rot90(ground_truth_sO2[:, y_dim, :] * 100, -1), vmin=0, vmax=100) + plt.imshow(ground_truth_sO2[:, y_dim, :].T * 100, vmin=0, vmax=100) plt.colorbar(fraction=0.05) plt.subplot(132) plt.title("Estimated sO2 [%]") - plt.imshow(np.rot90(self.sO2[:, y_dim, :] * 100, -1), vmin=0, vmax=100) + plt.imshow(self.sO2[:, y_dim, :].T * 100, vmin=0, vmax=100) plt.colorbar(fraction=0.05) plt.subplot(133) plt.title("Absolute Difference") - plt.imshow(np.rot90(np.abs(self.sO2 * 100 - ground_truth_sO2 * 100)[:, y_dim, :], -1), cmap="Reds", vmin=0) + plt.imshow(np.abs(self.sO2 * 100 - ground_truth_sO2 * 100)[:, y_dim, :].T, cmap="Reds", vmin=0) plt.colorbar() plt.tight_layout() if show_figure_on_screen: From 2bda19f3ab9d606c8efa23a9aba1792380f7a70a Mon Sep 17 00:00:00 2001 From: v496a Date: Wed, 30 Oct 2024 13:54:54 +0100 Subject: [PATCH 13/34] Merged with develop --- .github/dependabot.yml | 2 + .github/release-drafter.yml | 32 + .github/workflows/automatic_testing.yml | 2 +- .github/workflows/release-drafter.yml | 41 ++ .gitignore | 8 + .pre-commit-config.yaml | 4 +- .../license_header.txt | 0 .pre_commit_configs/link-config.json | 7 + CONTRIBUTING.md | 24 +- README.md | 61 +- docs/source/bench_link.md | 2 + docs/source/benchmarking.md | 139 +++++ docs/source/images/benchmarking_table.png | Bin 0 -> 96297 bytes docs/source/index.rst | 2 + docs/source/introduction.md | 22 +- ...ptical_simulation_heterogeneous_tissue.rst | 7 + ...ice_digital_twins.detection_geometries.rst | 6 + ..._digital_twins.illumination_geometries.rst | 6 + ...a.core.device_digital_twins.pa_devices.rst | 6 + .../simpa.core.device_digital_twins.rst | 5 + ...re.processing_components.multispectral.rst | 6 + .../simpa.core.processing_components.rst | 5 + docs/source/simpa.core.rst | 6 + ...ore.simulation_modules.acoustic_module.rst | 24 + ...core.simulation_modules.optical_module.rst | 36 ++ ...mulation_modules.reconstruction_module.rst | 18 +- docs/source/simpa.core.simulation_modules.rst | 9 +- ...ulation_modules.volume_creation_module.rst | 10 +- docs/source/simpa.utils.libraries.rst | 6 + docs/source/simpa_examples.rst | 2 + ..._vs_two_dimensional_simulation_example.rst | 7 + docs/source/understanding_link.md | 2 + docs/source/understanding_simpa.md | 113 ++++ pyproject.toml | 28 +- simpa/__init__.py | 39 +- simpa/core/__init__.py | 31 +- simpa/core/device_digital_twins/__init__.py | 170 +----- .../detection_geometries/__init__.py | 133 +--- .../detection_geometry_base.py | 136 +++++ .../digital_device_twin_base.py | 132 ++++ .../illumination_geometries/__init__.py | 67 +- .../illumination_geometry_base.py | 70 +++ .../pa_devices/__init__.py | 158 +---- .../pa_devices/ithera_msot_acuity.py | 26 +- .../pa_devices/photoacoustic_device.py | 161 +++++ simpa/core/pipeline_element_base.py | 33 + simpa/core/processing_components/__init__.py | 18 +- .../monospectral/field_of_view_cropping.py | 16 +- .../monospectral/iterative_qPAI_algorithm.py | 18 +- .../monospectral/noise/gamma_noise.py | 8 +- .../monospectral/noise/gaussian_noise.py | 8 +- .../monospectral/noise/poisson_noise.py | 8 +- .../noise/salt_and_pepper_noise.py | 8 +- .../monospectral/noise/uniform_noise.py | 8 +- .../multispectral/__init__.py | 55 +- .../multispectral/linear_unmixing.py | 2 +- .../multispectral_processing_algorithm.py | 57 ++ .../processing_component_base.py | 21 + simpa/core/simulation.py | 19 +- simpa/core/simulation_modules/__init__.py | 29 +- .../acoustic_module/__init__.py | 4 + .../acoustic_adapter_base.py} | 10 +- .../acoustic_test_adapter.py} | 4 +- .../k_wave_adapter.py} | 25 +- .../simulate_2D.m | 0 .../simulate_3D.m | 0 .../optical_module/__init__.py | 4 + .../mcx_adapter.py} | 16 +- .../mcx_reflectance_adapter.py} | 11 +- .../optical_adapter_base.py} | 10 +- .../optical_test_adapter.py} | 4 +- .../volume_boundary_condition.py | 0 .../reconstruction_module/__init__.py | 93 +-- ...um_adapter.py => delay_and_sum_adapter.py} | 2 +- ...r.py => delay_multiply_and_sum_adapter.py} | 2 +- .../reconstruction_adapter_base.py | 95 +++ ...pter.py => reconstruction_test_adapter.py} | 2 +- .../reconstruction_utils.py | 28 +- ... signed_delay_multiply_and_sum_adapter.py} | 2 +- ...al_adapter.py => time_reversal_adapter.py} | 16 +- .../simulation_module_base.py | 44 ++ .../volume_creation_module/__init__.py | 75 +-- ...ased_adapter.py => model_based_adapter.py} | 19 +- ...apter.py => segmentation_based_adapter.py} | 33 +- .../volume_creation_adapter_base.py | 78 +++ simpa/io_handling/io_hdf5.py | 2 + simpa/utils/__init__.py | 11 +- simpa/utils/calculate.py | 92 ++- simpa/utils/constants.py | 1 + simpa/utils/dict_path_manager.py | 2 +- .../libraries/heterogeneity_generator.py | 327 ++++++++++ simpa/utils/libraries/molecule_library.py | 107 ++-- simpa/utils/libraries/spectrum_library.py | 29 +- .../EllipticalTubularStructure.py | 2 +- .../HorizontalLayerStructure.py | 33 + .../structure_library/StructureBase.py | 41 +- .../libraries/structure_library/__init__.py | 1 + simpa/utils/libraries/tissue_library.py | 89 +-- simpa/utils/matlab.py | 17 +- simpa/utils/path_manager.py | 15 +- simpa/utils/profiling.py | 36 +- simpa/utils/settings.py | 26 + simpa/utils/tags.py | 107 +++- simpa/utils/tissue_properties.py | 15 +- .../matplotlib_data_visualisation.py | 16 + simpa_examples/benchmarking/__init__.py | 3 + .../benchmarking/extract_benchmarking_data.py | 130 ++++ .../benchmarking/get_final_table.py | 69 +++ .../benchmarking/performance_check.py | 55 ++ .../benchmarking/run_benchmarking.sh | 99 +++ .../create_a_custom_digital_device_twin.py | 21 +- simpa_examples/linear_unmixing.py | 326 +++++----- simpa_examples/minimal_optical_simulation.py | 300 ++++----- ...optical_simulation_heterogeneous_tissue.py | 177 ++++++ ...minimal_optical_simulation_uniform_cube.py | 169 +++--- simpa_examples/msot_invision_simulation.py | 394 ++++++------ .../optical_and_acoustic_simulation.py | 389 ++++++------ simpa_examples/path_config.env | 7 - simpa_examples/path_config.env.example | 2 +- .../perform_image_reconstruction.py | 59 +- .../perform_iterative_qPAI_reconstruction.py | 436 ++++++------- simpa_examples/segmentation_loader.py | 179 +++--- ...e_vs_two_dimensional_simulation_example.py | 220 +++++++ .../device_tests/test_field_of_view.py | 45 +- .../automatic_tests/test_IPASC_export.py | 50 +- .../automatic_tests/test_additional_flags.py | 61 ++ .../automatic_tests/test_calculation_utils.py | 77 ++- .../automatic_tests/test_create_a_volume.py | 10 +- .../test_heterogeneity_generator.py | 223 +++++++ .../automatic_tests/test_linear_unmixing.py | 20 +- .../automatic_tests/test_noise_models.py | 12 +- .../automatic_tests/test_path_manager.py | 13 +- simpa_tests/automatic_tests/test_pipeline.py | 22 +- .../automatic_tests/test_processing_device.py | 1 - .../tissue_library/test_core_assumptions.py | 57 +- simpa_tests/manual_tests/__init__.py | 2 +- ...KWaveAcousticForwardConvenienceFunction.py | 12 +- .../MinimalKWaveTest.py | 12 +- .../SimulationWithMSOTInvision.py | 6 +- .../digital_device_twins/VisualiseDevices.py | 15 +- .../executables/MATLABAdditionalFlags.py | 149 +++++ .../executables/MCXAdditionalFlags.py | 128 ++++ simpa_tests/manual_tests/generate_overview.py | 379 ++++++++++++ .../DelayAndSumReconstruction.py | 10 +- .../DelayMultiplyAndSumReconstruction.py | 10 +- .../PointSourceReconstruction.py | 572 ++++++++++-------- ...SignedDelayMultiplyAndSumReconstruction.py | 10 +- .../TimeReversalReconstruction.py | 7 +- ...tteringWithInifinitesimalSlabExperiment.py | 14 +- ...tionAndScatteringWithinHomogenousMedium.py | 160 ++--- .../CompareMCXResultsWithDiffusionTheory.py | 6 +- .../ComputeDiffuseReflectance.py | 14 +- .../QPAIReconstruction.py | 8 +- .../TestLinearUnmixingVisual.py | 10 +- .../ReproduceDISMeasurements.py | 9 +- .../volume_creation/SegmentationLoader.py | 8 +- .../test_utils/tissue_composition_tests.py | 186 ++++-- 157 files changed, 6224 insertions(+), 2754 deletions(-) create mode 100644 .github/release-drafter.yml create mode 100644 .github/workflows/release-drafter.yml rename {pre_commit_configs => .pre_commit_configs}/license_header.txt (100%) create mode 100644 .pre_commit_configs/link-config.json create mode 100644 docs/source/bench_link.md create mode 100644 docs/source/benchmarking.md create mode 100644 docs/source/images/benchmarking_table.png create mode 100644 docs/source/minimal_optical_simulation_heterogeneous_tissue.rst create mode 100644 docs/source/simpa.core.simulation_modules.acoustic_module.rst create mode 100644 docs/source/simpa.core.simulation_modules.optical_module.rst create mode 100644 docs/source/three_vs_two_dimensional_simulation_example.rst create mode 100644 docs/source/understanding_link.md create mode 100644 docs/source/understanding_simpa.md create mode 100644 simpa/core/device_digital_twins/detection_geometries/detection_geometry_base.py create mode 100644 simpa/core/device_digital_twins/digital_device_twin_base.py create mode 100644 simpa/core/device_digital_twins/illumination_geometries/illumination_geometry_base.py create mode 100644 simpa/core/device_digital_twins/pa_devices/photoacoustic_device.py create mode 100644 simpa/core/pipeline_element_base.py create mode 100644 simpa/core/processing_components/multispectral/multispectral_processing_algorithm.py create mode 100644 simpa/core/processing_components/processing_component_base.py create mode 100644 simpa/core/simulation_modules/acoustic_module/__init__.py rename simpa/core/simulation_modules/{acoustic_forward_module/__init__.py => acoustic_module/acoustic_adapter_base.py} (90%) rename simpa/core/simulation_modules/{acoustic_forward_module/acoustic_forward_model_test_adapter.py => acoustic_module/acoustic_test_adapter.py} (75%) rename simpa/core/simulation_modules/{acoustic_forward_module/acoustic_forward_module_k_wave_adapter.py => acoustic_module/k_wave_adapter.py} (95%) rename simpa/core/simulation_modules/{acoustic_forward_module => acoustic_module}/simulate_2D.m (100%) rename simpa/core/simulation_modules/{acoustic_forward_module => acoustic_module}/simulate_3D.m (100%) create mode 100644 simpa/core/simulation_modules/optical_module/__init__.py rename simpa/core/simulation_modules/{optical_simulation_module/optical_forward_model_mcx_adapter.py => optical_module/mcx_adapter.py} (97%) rename simpa/core/simulation_modules/{optical_simulation_module/optical_forward_model_mcx_reflectance_adapter.py => optical_module/mcx_reflectance_adapter.py} (98%) rename simpa/core/simulation_modules/{optical_simulation_module/__init__.py => optical_module/optical_adapter_base.py} (95%) rename simpa/core/simulation_modules/{optical_simulation_module/optical_forward_model_test_adapter.py => optical_module/optical_test_adapter.py} (75%) rename simpa/core/simulation_modules/{optical_simulation_module => optical_module}/volume_boundary_condition.py (100%) rename simpa/core/simulation_modules/reconstruction_module/{reconstruction_module_delay_and_sum_adapter.py => delay_and_sum_adapter.py} (98%) rename simpa/core/simulation_modules/reconstruction_module/{reconstruction_module_delay_multiply_and_sum_adapter.py => delay_multiply_and_sum_adapter.py} (98%) create mode 100644 simpa/core/simulation_modules/reconstruction_module/reconstruction_adapter_base.py rename simpa/core/simulation_modules/reconstruction_module/{reconstruction_module_test_adapter.py => reconstruction_test_adapter.py} (85%) rename simpa/core/simulation_modules/reconstruction_module/{reconstruction_module_signed_delay_multiply_and_sum_adapter.py => signed_delay_multiply_and_sum_adapter.py} (98%) rename simpa/core/simulation_modules/reconstruction_module/{reconstruction_module_time_reversal_adapter.py => time_reversal_adapter.py} (92%) create mode 100644 simpa/core/simulation_modules/simulation_module_base.py rename simpa/core/simulation_modules/volume_creation_module/{volume_creation_module_model_based_adapter.py => model_based_adapter.py} (83%) rename simpa/core/simulation_modules/volume_creation_module/{volume_creation_module_segmentation_based_adapter.py => segmentation_based_adapter.py} (59%) create mode 100644 simpa/core/simulation_modules/volume_creation_module/volume_creation_adapter_base.py create mode 100644 simpa/utils/libraries/heterogeneity_generator.py create mode 100644 simpa_examples/benchmarking/__init__.py create mode 100644 simpa_examples/benchmarking/extract_benchmarking_data.py create mode 100644 simpa_examples/benchmarking/get_final_table.py create mode 100644 simpa_examples/benchmarking/performance_check.py create mode 100644 simpa_examples/benchmarking/run_benchmarking.sh create mode 100644 simpa_examples/minimal_optical_simulation_heterogeneous_tissue.py delete mode 100644 simpa_examples/path_config.env create mode 100644 simpa_examples/three_vs_two_dimensional_simulation_example.py create mode 100644 simpa_tests/automatic_tests/test_additional_flags.py create mode 100644 simpa_tests/automatic_tests/test_heterogeneity_generator.py create mode 100644 simpa_tests/manual_tests/executables/MATLABAdditionalFlags.py create mode 100644 simpa_tests/manual_tests/executables/MCXAdditionalFlags.py create mode 100644 simpa_tests/manual_tests/generate_overview.py diff --git a/.github/dependabot.yml b/.github/dependabot.yml index f45fb1d8..272046e7 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -4,9 +4,11 @@ updates: directory: "/" schedule: interval: "weekly" + target-branch: "develop" open-pull-requests-limit: 0 - package-ecosystem: "pip" directory: "/" schedule: interval: "weekly" + target-branch: "develop" open-pull-requests-limit: 0 diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml new file mode 100644 index 00000000..bea11f1e --- /dev/null +++ b/.github/release-drafter.yml @@ -0,0 +1,32 @@ +name-template: 'v$RESOLVED_VERSION 🌈' +tag-template: 'v$RESOLVED_VERSION' +categories: + - title: '🚀 Features' + labels: + - 'feature' + - 'enhancement' + - 'documentation' + - title: '🐛 Bug Fixes' + labels: + - 'fix' + - 'bugfix' + - 'bug' + - title: '🧰 Maintenance' + label: 'chore' +change-template: '- $TITLE @$AUTHOR (#$NUMBER)' +change-title-escapes: '\<*_&' # You can add # and @ to disable mentions, and add ` to disable code blocks. +version-resolver: + major: + labels: + - 'major' + minor: + labels: + - 'minor' + patch: + labels: + - 'patch' + default: patch +template: | + ## Changes + + $CHANGES \ No newline at end of file diff --git a/.github/workflows/automatic_testing.yml b/.github/workflows/automatic_testing.yml index 6e6add93..63cf84e8 100644 --- a/.github/workflows/automatic_testing.yml +++ b/.github/workflows/automatic_testing.yml @@ -23,7 +23,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.8", "3.9", "3.10", "3.11"] + python-version: ["3.9", "3.10", "3.11", "3.12"] os: [ubuntu-latest, macos-latest, windows-latest] steps: diff --git a/.github/workflows/release-drafter.yml b/.github/workflows/release-drafter.yml new file mode 100644 index 00000000..93fc6347 --- /dev/null +++ b/.github/workflows/release-drafter.yml @@ -0,0 +1,41 @@ +name: Release Drafter + +on: + push: + # branches to consider in the event; optional, defaults to all + branches: + - main + # pull_request event is required only for autolabeler +# pull_request: + # Only following types are handled by the action, but one can default to all as well +# types: [ opened, reopened, synchronize ] + # pull_request_target event is required for autolabeler to support PRs from forks + # pull_request_target: + # types: [opened, reopened, synchronize] + +permissions: + contents: read + +jobs: + update_release_draft: + permissions: + # write permission is required to create a github release + contents: write + # write permission is required for autolabeler + # otherwise, read permission is required at least + pull-requests: read + runs-on: ubuntu-latest + steps: + # (Optional) GitHub Enterprise requires GHE_HOST variable set + #- name: Set GHE_HOST + # run: | + # echo "GHE_HOST=${GITHUB_SERVER_URL##https:\/\/}" >> $GITHUB_ENV + + # Drafts your next Release notes as Pull Requests are merged into "main" + - uses: release-drafter/release-drafter@v6 + # (Optional) specify config name to use, relative to .github/. Default: release-drafter.yml + # with: + # config-name: my-config.yml + # disable-autolabeler: true + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/.gitignore b/.gitignore index d65fe579..79570db7 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,14 @@ docs/objects.inv simpa_tests/figures simpa_tests/figures/* +simpa_tests/**/figures +simpa_tests/**/figures/* +simpa_tests/manual_tests/figures +simpa_tests/manual_tests/reference_figures +simpa_tests/manual_tests/manual_tests_* + +simpa_examples/benchmarking/*.csv +simpa_examples/benchmarking/*.txt path_config.env diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index fd899a52..6e941488 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -23,7 +23,7 @@ repos: types: [python] args: - --license-filepath - - pre_commit_configs/license_header.txt # defaults to: LICENSE.txt + - .pre_commit_configs/license_header.txt # defaults to: LICENSE.txt - repo: https://github.com/jorisroovers/gitlint # Uses MIT License (MIT compatible) rev: v0.19.1 @@ -38,7 +38,7 @@ repos: types: [markdown] args: - -c - - pre_commit_configs/link-config.json + - .pre_commit_configs/link-config.json always_run: true - repo: local diff --git a/pre_commit_configs/license_header.txt b/.pre_commit_configs/license_header.txt similarity index 100% rename from pre_commit_configs/license_header.txt rename to .pre_commit_configs/license_header.txt diff --git a/.pre_commit_configs/link-config.json b/.pre_commit_configs/link-config.json new file mode 100644 index 00000000..f1dd61d8 --- /dev/null +++ b/.pre_commit_configs/link-config.json @@ -0,0 +1,7 @@ +{ + "ignorePatterns": [ + { + "pattern": "https://doi.org/10.1117/1.JBO.27.8.083010" + } + ] +} \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a8fb28f6..95ebd68c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -25,21 +25,25 @@ Once you reached out to us, you will be provided with the information on how to In general the following steps are involved during a contribution: ### Contribution process -1. Create feature request / bug report on the [SIMPA issues page](https://github.com/IMSY-DKFZ/simpa/issues) -2. Discuss potential contribution with core development team -3. Fork the [SIMPA repository](https://github.com/IMSY-DKFZ/simpa) -4. Create feature branch from develop using the naming convention T_, - where represent the number github assigned the created issue and describes +1. Create feature request / bug report on the [SIMPA issues page](https://github.com/IMSY-DKFZ/simpa/issues) +2. Discuss potential contribution with core development team +3. Fork the [SIMPA repository](https://github.com/IMSY-DKFZ/simpa) +4. Make sure that you've installed all the optional dependencies by running `pip install .[docs,profile,testing]` + in the root directory of the repository. +5. Create feature branch from develop using the naming convention T_, + where represents the number Github assigned the created issue and describes what is being developed in CamelCaseNotation. Examples: `T13_FixSimulatorBug`, `T27_AddNewSimulator` -5. Perform test driven development on feature branch. +6. Perform test driven development on a feature branch. A new implemented feature / a bug fix should be accompanied by a test. Additionally, all previously existing tests must still pass after the contribution. -6. Run pre-commit hooks and make sure all hooks are passing. -7. Once development is finished, create a pull request including your changes. +7. Run pre-commit hooks and make sure all hooks are passing. +8. Please also make sure that you benchmark your contributions please use the benchmarking bash script (see [benchmarking.md](docs/source/benchmarking.md) for more details). + Please add the results to the PR and compare them to the current develop. +9. Once development is finished, create a pull request including your changes. For more information on how to create pull request, see GitHub's [about pull requests](https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/about-pull-requests). -8. If there are conflicts between the simpa develop branch and your branch, you should update your feature branch with the simpa develop branch using a "merge" strategy instead of "rebase". -9. A member of the core development team will review your pull request and potentially require further changes +10. If there are conflicts between the simpa develop branch and your branch, you should update your feature branch with the simpa develop branch using a "merge" strategy instead of "rebase". +11. A member of the core development team will review your pull request and potentially require further changes (see [Contribution review and integration](#contribution-review-and-integration)). Once all remarks have been resolved, your changes will be merged into the develop branch. diff --git a/README.md b/README.md index 9945dfb7..0693fe7e 100755 --- a/README.md +++ b/README.md @@ -23,6 +23,7 @@ The paper that introduces SIMPA including visualisations and explanations can be * [Getting started](#getting-started) * [Simulation examples](#simulation-examples) * [Documentation](#documentation) +* [Reproducibility](#reproducibility) * [Contributing](#how-to-contribute) * [Performance profiling](#performance-profiling) * [Troubleshooting](#troubleshooting) @@ -43,6 +44,7 @@ The SIMPA path management takes care of that. * [SIMPA installation instructions](#simpa-installation-instructions) * [External tools installation instructions](#external-tools-installation-instructions) * [Path Management](#path-management) +* [Testing](#run-manual-tests) ## SIMPA installation instructions @@ -54,8 +56,8 @@ The recommended way to install SIMPA is a manual installation from the GitHub re 4. `git pull` Now open a python instance in the 'simpa' folder that you have just downloaded. Make sure that you have your preferred -virtual environment activated (we also recommend python 3.8) -1. `pip install .` +virtual environment activated (we also recommend python 3.10) +1. `pip install .` or `pip install -e .` for an editable mode. 2. Test if the installation worked by using `python` followed by `import simpa` then `exit()` If no error messages arise, you are now setup to use SIMPA in your project. @@ -114,6 +116,9 @@ one we provided in the `simpa_examples/path_config.env.example`) in the followin For this option, please follow the instructions in the `simpa_examples/path_config.env.example` file. +## Run manual tests +To check the success of your installation ot to assess how your contributions affect the Simpa simulation outcomes, you can run the manual tests automatically. Install the testing requirements by doing `pip install .[testing]` and run the `simpa_tests/manual_tests/generate_overview.py` file. This script runs all manual tests and generates both a markdown and an HTML file that compare your results with the reference results. + # Simulation examples To get started with actual simulations, SIMPA provides an [example package](simpa_examples) of simple simulation @@ -136,10 +141,10 @@ settings.set_acoustic_settings(acoustic_settings) settings.set_reconstruction_settings(reconstruction_settings) # Set the simulation pipeline -simulation_pipeline = [sp.VolumeCreatorModule(settings), - sp.OpticalForwardModule(settings), - sp.AcousticForwardModule(settings), - sp.ReconstructionModule(settings)] +simulation_pipeline = [sp.VolumeCreationModule(settings), + sp.OpticalModule(settings), + sp.AcousticModule(settings), + sp.ReconstructionModule(settings)] # Choose a PA device with device position in the volume device = sp.CustomDevice() @@ -148,6 +153,12 @@ device = sp.CustomDevice() sp.simulate(simulation_pipeline, settings, device) ``` +# Reproducibility + +For reproducibility, we provide the exact version number including the commit hash in the simpa output file. +This can be accessed via `simpa.__version__` or by checking the tag `Tags.SIMPA_VERSION` in the output file. +This way, you can always trace back the exact version of the code that was used to generate the simulation results. + # Documentation The updated version of the SIMPA documentation can be found at [https://simpa.readthedocs.io/en/develop](https://simpa.readthedocs.io/en/develop). @@ -158,7 +169,8 @@ It is also easily possible to build the SIMPA documentation from scratch. When the installation succeeded, and you want to make sure that you have the latest documentation you should do the following steps in a command line: -1. Navigate to the `simpa/docs` directory +1. Make sure that you've installed the optional dependencies needed for the documentation by running `pip install .[docs]` +2. Navigate to the `simpa/docs` directory 2. If you would like the documentation to have the https://readthedocs.org/ style, type `pip install sphinx-rtd-theme` 3. Type `make html` 4. Open the `index.html` file in the `simpa/docs/build/html` directory with your favourite browser. @@ -183,13 +195,33 @@ Please see the github guidelines for creating pull requests: [https://docs.githu # Performance profiling -Do you wish to know which parts of the simulation pipeline cost the most amount of time? -If that is the case then you can use the following commands to profile the execution of your simulation script. -You simply need to replace the `myscript` name with your script name. +When changing the SIMPA core, e.g., by refactoring/optimizing, or if you are curious about how fast your machine runs +SIMPA, you can run the SIMPA [benchmarking scripts](simpa_examples/benchmarking/run_benchmarking.sh). Make sure to install the necessary dependencies via +`pip install .[profile]` and then run: + +```bash +bash ./run_benchmark.sh +``` + +once for checking if it works and then parse [--number 100] to run it at eg 100 times for actual benchmarking. +Please see [benchmarking.md](docs/source/benchmarking.md) for a complete explanation. -`python -m cProfile -o myscript.cprof myscript.py` -`pyprof2calltree -k -i myscript.cprof` +# Understanding SIMPA + +**Tags** are identifiers in SIMPA used to categorize settings and components within simulations, making configurations +modular, readable, and manageable. Tags offer organizational, flexible, and reusable benefits by acting as keys in +configuration dictionaries. + +**Settings** in SIMPA control simulation behavior. They include: + +- **Global Settings**: Apply to the entire simulation, affecting overall properties and parameters. +- **Component Settings**: Specific to individual components, allowing for detailed customization and optimization of +each part of the simulation. + +Settings are defined in a hierarchical structure, where global settings are established first, followed by +component-specific settings. This approach ensures comprehensive and precise control over the simulation process. +For detailed information, users can refer to the [understanding SIMPA documentation](./docs/source/understanding_simpa.md). # Troubleshooting @@ -203,6 +235,11 @@ If you encounter an error similar to: The filename specified was either not found on the MATLAB path or it contains unsupported characters. Look up the solution in [this thread of the k-Wave forum](http://www.k-wave.org/forum/topic/error-reading-h5-files-when-using-binaries). + +## 2. KeyError: 'time_series_data' + +This is the error which will occur for ANY k-Wave problem. For the actual root of the problem, please either look above in +the terminal for the source of the bug or run the scripts in Matlab to find it manually. # Citation diff --git a/docs/source/bench_link.md b/docs/source/bench_link.md new file mode 100644 index 00000000..e53ff117 --- /dev/null +++ b/docs/source/bench_link.md @@ -0,0 +1,2 @@ +```{include} benchmarking.md +``` \ No newline at end of file diff --git a/docs/source/benchmarking.md b/docs/source/benchmarking.md new file mode 100644 index 00000000..f76207d0 --- /dev/null +++ b/docs/source/benchmarking.md @@ -0,0 +1,139 @@ +# Benchmarking SIMPA + +## Overview +The [run_benchmarking.sh](../../simpa_examples/benchmarking/run_benchmarking.sh) bash script helps you benchmark +SIMPA simulations with various profiling options such as time, GPU memory, and memory usage. +It allows customization of initial spacing, final spacing, step size, output file location, and number of simulations. +To check and amend which simulations are run, see the [performance check script](../../simpa_examples/benchmarking/performance_check.py). + +## Usage +In order to be able to use this script, please first ensure that you have the dependencies required for the benchmarking +scripts. To do this, please navigate to the simpa directory and execute `pip install .[profile]`. + +Now, you can run [performance_check.py](../../simpa_examples/benchmarking/performance_check.py) and [run_benchmarking.sh](../../simpa_examples/benchmarking/run_benchmarking.sh) from the command line with the desired +options. Please ensure you check two things before running the script: First, ensure that the device will not be in use +for the duration - ideally restart before benchmarking - of the benchmarking process, +as this will create large uncertainties within the outcomes. Second, ensure that you don't accidentally write over any +existing file by saving the files created by this script after runtime to different location. + +The both scripts create text files (eg. benchmarking_data_TIME_0.2.txt), showing the line by line profiling of +the most recent runs. The [run_benchmarking.sh](../../simpa_examples/benchmarking/run_benchmarking.sh) also creates two csv's: one with the data from all the runs +(benchmarking_data_frame.csv) and one with the means and standard deviations of all the runs +(benchmarking_data_frame_mean.csv). With both scripts, unless the user intentionally changes the save folder name, +the output files will be overwritten. + +Below is a description of the available options and how to use them. + +### Benchmarking for contributions +When contributing, you may be asked by the development team to benchmarking the changes you've made to help them +understand how your changes have effected the performance of SIMPA. Therefore, we ask that you run this script with +**`-n, --number`** as 100 before AND after your changes, on a clean setup with no browser or other applications running. +Please put this in the conversation of the pull request, and not add it to the files of the pull request itself. + + +## Options +- **`-i, --init`**: First spacing to benchmark (default = 0.2mm). +- **`-c, --cease`**: Final spacing to benchmark (default = 0.25mm). +- **`-s, --step`**: Step between spacings (default = 0.05mm). +- **`-f, --file`**: Where to store the output files (default = save in current directory; 'print' prints it in console). +- **`-t, --time`**: Profile times taken (if no profile is specified, all are set). +- **`-g, --gpu`**: Profile GPU usage (if no profile is specified, all are set). +- **`-m, --memory`**: Profile memory usage (if no profile is specified, all are set). +- **`-n, --number`**: Number of simulations (default = 1). +- **`-h, --help`**: Display this help message. + +## Default Values +If no options are provided for initial spacing, final spacing, or step size, the script uses the following default +values: +- **Initial Spacing**: 0.2mm +- **Final Spacing**: 0.25mm +- **Step Size**: 0.05mm + +If no profiling options are specified, all three profilers (time, GPU memory, and memory) are used by default. + +## Examples +Here are some examples of how to use the script: + +1. **Default Usage**: + ```bash + bash ./run_benchmark.sh + ``` + +2. **Custom Spacing and File Output**: + ```bash + bash ./run_benchmark.sh -i 0.1 -c 0.5 -s 0.05 -f results + ``` + +3. **Profile Time and GPU Memory for 3 Simulations**: + ```bash + bash ./run_benchmark.sh -t -g -n 3 + ``` + +To read the results, just click on the generated `benchmarking_data_frame_mean.md` file. +Or you can also read the csv with: +```python +import pandas as pd +from tabulate import tabulate +benchmarking_results = pd.read_csv('path/to/simpa/simpa_examples/benchmarking/benchmarking_data_frame_mean.csv') +print(tabulate(benchmarking_results)) +# or use display(benchmarking_results) which works for ipynb +``` + +The expected outcome should look something similar to the below: + +![img.png](images/benchmarking_table.png) + +# Line Profiler (more advanced - for specific function profiling) + +Within SIMPA we have an [inbuilt python script](../../simpa/utils/profiling.py) to help benchmark specific functions and +understand the holdups in our code. This script is designed to set up a profiling environment based on an environment +variable named `SIMPA_PROFILE`. The `@profile` decorator can then be added to functions to see line-by-line statistics +of code performance. + +Here is a breakdown of the script's functionality: + +1. **Determine Profile Type and Stream:** + - `profile_type` is fetched from the environment variable `SIMPA_PROFILE`. + - `stream` is set to an open file object if the environment variable `SIMPA_PROFILE_SAVE_FILE` is set; otherwise, it is `None`. + +2. **No Profiling:** + - If `profile_type` is `None` + +3. **Time Profiling:** + - If `profile_type` is `"TIME"` + +4. **Memory Profiling:** + - If `profile_type` is `"MEMORY"` + +5. **GPU Memory Profiling:** + - If `profile_type` is `"GPU_MEMORY"` + +6. **Profile Decorator** + - The `profile` decorator is defined to register functions to be profiled and to print statistics upon program exit. + +7. **Invalid Profile Type:** + - If `profile_type` does not match any of the expected values (`"TIME"`, `"MEMORY"`, or `"GPU_MEMORY"`), a `RuntimeError` is raised. + +### Example Usage + +To use this script, you need to set the `SIMPA_PROFILE` environment variable to one of the supported values +(`TIME`, `MEMORY`, or `GPU_MEMORY`) and optionally set the `SIMPA_PROFILE_SAVE_FILE` to specify where to save the +profiling results. + +The environment variables must be set before importing simpa in your python script like below. +```python +import os +os.environ("SIMPA_PROFILE")="TIME" +os.environ("SIMPA_PROFILE_SAVE_FILE")=profile_results.txt +``` + +To use the `@profile` decorator, simply apply it to the functions you want to profile within your script: + +```python +@profile +def some_function(): + # function implementation +``` + +Make sure the necessary profiling modules (`line_profiler`, `memory_profiler`, `pytorch_memlab`) are installed in your +environment. \ No newline at end of file diff --git a/docs/source/images/benchmarking_table.png b/docs/source/images/benchmarking_table.png new file mode 100644 index 0000000000000000000000000000000000000000..03d3d8f808cc4983e66a7b1de4a93436276ef77f GIT binary patch literal 96297 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 E7Zr9n`~Uy| literal 0 HcmV?d00001 diff --git a/docs/source/index.rst b/docs/source/index.rst index 041f3cfc..1472bfc4 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -20,6 +20,8 @@ pipeline according to their specific use cases and tool requirements. :caption: SIMPA toolkit intro_link + understanding_link.md + bench_link.md contributing_link =================================== diff --git a/docs/source/introduction.md b/docs/source/introduction.md index 9ce346d4..51999ae9 100644 --- a/docs/source/introduction.md +++ b/docs/source/introduction.md @@ -7,6 +7,7 @@ The SIMPA path management takes care of that. * [SIMPA installation instructions](#simpa-installation-instructions) * [External tools installation instructions](#external-tools-installation-instructions) * [Path Management](#path-management) +* [Testing](#run-manual-tests) ## SIMPA installation instructions @@ -18,8 +19,8 @@ The recommended way to install SIMPA is a manual installation from the GitHub re 4. `git pull` Now open a python instance in the 'simpa' folder that you have just downloaded. Make sure that you have your preferred -virtual environment activated (we also recommend python 3.8) -1. `pip install .` +virtual environment activated (we also recommend python 3.10) +1. `pip install .` or `pip install -e .` for an editable mode. 2. Test if the installation worked by using `python` followed by `import simpa` then `exit()` If no error messages arise, you are now setup to use SIMPA in your project. @@ -78,6 +79,9 @@ one we provided in the `simpa_examples/path_config.env.example`) in the followin For this option, please follow the instructions in the `simpa_examples/path_config.env.example` file. +## Run manual tests +To check the success of your installation ot to assess how your contributions affect the Simpa simulation outcomes, you can run the manual tests automatically. Install the testing requirements by doing `pip install .[testing]` and run the `simpa_tests/manual_tests/generate_overview.py` file. This script runs all manual tests and generates both a markdown and an HTML file that compare your results with the reference results. + # Simulation examples To get started with actual simulations, SIMPA provides an [example package](simpa_examples) of simple simulation @@ -100,10 +104,10 @@ settings.set_acoustic_settings(acoustic_settings) settings.set_reconstruction_settings(reconstruction_settings) # Set the simulation pipeline -simulation_pipeline = [sp.VolumeCreatorModule(settings), - sp.OpticalForwardModule(settings), - sp.AcousticForwardModule(settings), - sp.ReconstructionModule(settings)] +simulation_pipeline = [sp.VolumeCreationModule(settings), + sp.OpticalModule(settings), + sp.AcousticModule(settings), + sp.ReconstructionModule(settings)] # Choose a PA device with device position in the volume device = sp.CustomDevice() @@ -112,3 +116,9 @@ device = sp.CustomDevice() sp.simulate(simulation_pipeline, settings, device) ``` +# Reproducibility + +For reproducibility, we provide the exact version number including the commit hash in the simpa output file. +This can be accessed via `simpa.__version__` or by checking the tag `Tags.SIMPA_VERSION` in the output file. +This way, you can always trace back the exact version of the code that was used to generate the simulation results. + diff --git a/docs/source/minimal_optical_simulation_heterogeneous_tissue.rst b/docs/source/minimal_optical_simulation_heterogeneous_tissue.rst new file mode 100644 index 00000000..13cb3588 --- /dev/null +++ b/docs/source/minimal_optical_simulation_heterogeneous_tissue.rst @@ -0,0 +1,7 @@ +minimal_optical_simulation_heterogeneous_tissue +========================================= + +.. literalinclude:: ../../simpa_examples/minimal_optical_simulation_heterogeneous_tissue.py + :language: python + :lines: 1- + diff --git a/docs/source/simpa.core.device_digital_twins.detection_geometries.rst b/docs/source/simpa.core.device_digital_twins.detection_geometries.rst index 3eb9caa8..413d22fa 100644 --- a/docs/source/simpa.core.device_digital_twins.detection_geometries.rst +++ b/docs/source/simpa.core.device_digital_twins.detection_geometries.rst @@ -12,6 +12,12 @@ detection\_geometries :show-inheritance: +.. automodule:: simpa.core.device_digital_twins.detection_geometries.detection_geometry_base + :members: + :undoc-members: + :show-inheritance: + + .. automodule:: simpa.core.device_digital_twins.detection_geometries.linear_array :members: :undoc-members: diff --git a/docs/source/simpa.core.device_digital_twins.illumination_geometries.rst b/docs/source/simpa.core.device_digital_twins.illumination_geometries.rst index eb29ecbd..b398a489 100644 --- a/docs/source/simpa.core.device_digital_twins.illumination_geometries.rst +++ b/docs/source/simpa.core.device_digital_twins.illumination_geometries.rst @@ -18,6 +18,12 @@ illumination\_geometries :show-inheritance: +.. automodule:: simpa.core.device_digital_twins.illumination_geometries.illumination_geometry_base + :members: + :undoc-members: + :show-inheritance: + + .. automodule:: simpa.core.device_digital_twins.illumination_geometries.ithera_msot_acuity_illumination :members: :undoc-members: diff --git a/docs/source/simpa.core.device_digital_twins.pa_devices.rst b/docs/source/simpa.core.device_digital_twins.pa_devices.rst index dd796554..462b9051 100644 --- a/docs/source/simpa.core.device_digital_twins.pa_devices.rst +++ b/docs/source/simpa.core.device_digital_twins.pa_devices.rst @@ -22,3 +22,9 @@ pa\_devices :members: :undoc-members: :show-inheritance: + + +.. automodule:: simpa.core.device_digital_twins.pa_devices.photoacoustic_device + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/simpa.core.device_digital_twins.rst b/docs/source/simpa.core.device_digital_twins.rst index 5b25dcc9..a8aeb366 100644 --- a/docs/source/simpa.core.device_digital_twins.rst +++ b/docs/source/simpa.core.device_digital_twins.rst @@ -12,3 +12,8 @@ device\_digital\_twins simpa.core.device_digital_twins.detection_geometries simpa.core.device_digital_twins.illumination_geometries simpa.core.device_digital_twins.pa_devices + +.. automodule:: simpa.core.device_digital_twins.digital_device_twin_base + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/simpa.core.processing_components.multispectral.rst b/docs/source/simpa.core.processing_components.multispectral.rst index ef6f10e1..9a2e368b 100644 --- a/docs/source/simpa.core.processing_components.multispectral.rst +++ b/docs/source/simpa.core.processing_components.multispectral.rst @@ -10,3 +10,9 @@ multispectral :members: :undoc-members: :show-inheritance: + + +.. automodule:: simpa.core.processing_components.multispectral.multispectral_processing_algorithm + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/simpa.core.processing_components.rst b/docs/source/simpa.core.processing_components.rst index 7780b57d..52a88285 100644 --- a/docs/source/simpa.core.processing_components.rst +++ b/docs/source/simpa.core.processing_components.rst @@ -11,3 +11,8 @@ processing\_components simpa.core.processing_components.monospectral simpa.core.processing_components.multispectral + +.. automodule:: simpa.core.processing_components.processing_component_base + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/simpa.core.rst b/docs/source/simpa.core.rst index e86ea34b..78a560aa 100644 --- a/docs/source/simpa.core.rst +++ b/docs/source/simpa.core.rst @@ -13,6 +13,12 @@ core simpa.core.processing_components simpa.core.simulation_modules +.. automodule:: simpa.core.pipeline_element_base + :members: + :undoc-members: + :show-inheritance: + + .. automodule:: simpa.core.simulation :members: :undoc-members: diff --git a/docs/source/simpa.core.simulation_modules.acoustic_module.rst b/docs/source/simpa.core.simulation_modules.acoustic_module.rst new file mode 100644 index 00000000..43ede865 --- /dev/null +++ b/docs/source/simpa.core.simulation_modules.acoustic_module.rst @@ -0,0 +1,24 @@ +acoustic\_module +======================================================= + +.. automodule:: simpa.core.simulation_modules.acoustic_module + :members: + :undoc-members: + :show-inheritance: + +.. automodule:: simpa.core.simulation_modules.acoustic_module.acoustic_adapter_base + :members: + :undoc-members: + :show-inheritance: + + +.. automodule:: simpa.core.simulation_modules.acoustic_module.acoustic_test_adapter + :members: + :undoc-members: + :show-inheritance: + + +.. automodule:: simpa.core.simulation_modules.acoustic_module.k_wave_adapter + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/simpa.core.simulation_modules.optical_module.rst b/docs/source/simpa.core.simulation_modules.optical_module.rst new file mode 100644 index 00000000..f8809ef0 --- /dev/null +++ b/docs/source/simpa.core.simulation_modules.optical_module.rst @@ -0,0 +1,36 @@ +optical\_module +====================================================== + +.. automodule:: simpa.core.simulation_modules.optical_module + :members: + :undoc-members: + :show-inheritance: + +.. automodule:: simpa.core.simulation_modules.optical_module.mcx_adapter + :members: + :undoc-members: + :show-inheritance: + + +.. automodule:: simpa.core.simulation_modules.optical_module.mcx_reflectance_adapter + :members: + :undoc-members: + :show-inheritance: + + +.. automodule:: simpa.core.simulation_modules.optical_module.optical_adapter_base + :members: + :undoc-members: + :show-inheritance: + + +.. automodule:: simpa.core.simulation_modules.optical_module.optical_test_adapter + :members: + :undoc-members: + :show-inheritance: + + +.. automodule:: simpa.core.simulation_modules.optical_module.volume_boundary_condition + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/simpa.core.simulation_modules.reconstruction_module.rst b/docs/source/simpa.core.simulation_modules.reconstruction_module.rst index f14dd281..a875b39f 100644 --- a/docs/source/simpa.core.simulation_modules.reconstruction_module.rst +++ b/docs/source/simpa.core.simulation_modules.reconstruction_module.rst @@ -6,37 +6,43 @@ reconstruction\_module :undoc-members: :show-inheritance: -.. automodule:: simpa.core.simulation_modules.reconstruction_module.reconstruction_module_delay_and_sum_adapter +.. automodule:: simpa.core.simulation_modules.reconstruction_module.delay_and_sum_adapter :members: :undoc-members: :show-inheritance: -.. automodule:: simpa.core.simulation_modules.reconstruction_module.reconstruction_module_delay_multiply_and_sum_adapter +.. automodule:: simpa.core.simulation_modules.reconstruction_module.delay_multiply_and_sum_adapter :members: :undoc-members: :show-inheritance: -.. automodule:: simpa.core.simulation_modules.reconstruction_module.reconstruction_module_signed_delay_multiply_and_sum_adapter +.. automodule:: simpa.core.simulation_modules.reconstruction_module.reconstruction_adapter_base :members: :undoc-members: :show-inheritance: -.. automodule:: simpa.core.simulation_modules.reconstruction_module.reconstruction_module_test_adapter +.. automodule:: simpa.core.simulation_modules.reconstruction_module.reconstruction_test_adapter :members: :undoc-members: :show-inheritance: -.. automodule:: simpa.core.simulation_modules.reconstruction_module.reconstruction_module_time_reversal_adapter +.. automodule:: simpa.core.simulation_modules.reconstruction_module.reconstruction_utils :members: :undoc-members: :show-inheritance: -.. automodule:: simpa.core.simulation_modules.reconstruction_module.reconstruction_utils +.. automodule:: simpa.core.simulation_modules.reconstruction_module.signed_delay_multiply_and_sum_adapter + :members: + :undoc-members: + :show-inheritance: + + +.. automodule:: simpa.core.simulation_modules.reconstruction_module.time_reversal_adapter :members: :undoc-members: :show-inheritance: diff --git a/docs/source/simpa.core.simulation_modules.rst b/docs/source/simpa.core.simulation_modules.rst index 6ffa2e19..f2f2e85e 100644 --- a/docs/source/simpa.core.simulation_modules.rst +++ b/docs/source/simpa.core.simulation_modules.rst @@ -9,7 +9,12 @@ simulation\_modules .. toctree:: :maxdepth: 4 - simpa.core.simulation_modules.acoustic_forward_module - simpa.core.simulation_modules.optical_simulation_module + simpa.core.simulation_modules.acoustic_module + simpa.core.simulation_modules.optical_module simpa.core.simulation_modules.reconstruction_module simpa.core.simulation_modules.volume_creation_module + +.. automodule:: simpa.core.simulation_modules.simulation_module_base + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/simpa.core.simulation_modules.volume_creation_module.rst b/docs/source/simpa.core.simulation_modules.volume_creation_module.rst index 8fda1e0b..acf284c9 100644 --- a/docs/source/simpa.core.simulation_modules.volume_creation_module.rst +++ b/docs/source/simpa.core.simulation_modules.volume_creation_module.rst @@ -6,13 +6,19 @@ volume\_creation\_module :undoc-members: :show-inheritance: -.. automodule:: simpa.core.simulation_modules.volume_creation_module.volume_creation_module_model_based_adapter +.. automodule:: simpa.core.simulation_modules.volume_creation_module.model_based_adapter :members: :undoc-members: :show-inheritance: -.. automodule:: simpa.core.simulation_modules.volume_creation_module.volume_creation_module_segmentation_based_adapter +.. automodule:: simpa.core.simulation_modules.volume_creation_module.segmentation_based_adapter + :members: + :undoc-members: + :show-inheritance: + + +.. automodule:: simpa.core.simulation_modules.volume_creation_module.volume_creation_adapter_base :members: :undoc-members: :show-inheritance: diff --git a/docs/source/simpa.utils.libraries.rst b/docs/source/simpa.utils.libraries.rst index 5e1dd335..b128b0c2 100644 --- a/docs/source/simpa.utils.libraries.rst +++ b/docs/source/simpa.utils.libraries.rst @@ -15,6 +15,12 @@ libraries simpa.utils.libraries.scattering_spectra_data simpa.utils.libraries.structure_library +.. automodule:: simpa.utils.libraries.heterogeneity_generator + :members: + :undoc-members: + :show-inheritance: + + .. automodule:: simpa.utils.libraries.literature_values :members: :undoc-members: diff --git a/docs/source/simpa_examples.rst b/docs/source/simpa_examples.rst index e8a54665..06c1cb14 100644 --- a/docs/source/simpa_examples.rst +++ b/docs/source/simpa_examples.rst @@ -8,9 +8,11 @@ simpa\_examples create_custom_tissues linear_unmixing minimal_optical_simulation + minimal_optical_simulation_heterogeneous_tissue minimal_optical_simulation_uniform_cube msot_invision_simulation optical_and_acoustic_simulation perform_image_reconstruction perform_iterative_qPAI_reconstruction segmentation_loader + three_vs_two_dimensional_simulation_example diff --git a/docs/source/three_vs_two_dimensional_simulation_example.rst b/docs/source/three_vs_two_dimensional_simulation_example.rst new file mode 100644 index 00000000..f68198fb --- /dev/null +++ b/docs/source/three_vs_two_dimensional_simulation_example.rst @@ -0,0 +1,7 @@ +three_vs_two_dimensional_simulation_example +========================================= + +.. literalinclude:: ../../simpa_examples/three_vs_two_dimensional_simulation_example.py + :language: python + :lines: 1- + diff --git a/docs/source/understanding_link.md b/docs/source/understanding_link.md new file mode 100644 index 00000000..0af7fad5 --- /dev/null +++ b/docs/source/understanding_link.md @@ -0,0 +1,2 @@ +```{include} understanding_simpa.md +``` \ No newline at end of file diff --git a/docs/source/understanding_simpa.md b/docs/source/understanding_simpa.md new file mode 100644 index 00000000..faa5603c --- /dev/null +++ b/docs/source/understanding_simpa.md @@ -0,0 +1,113 @@ +# Understanding SIMPA + +## Understanding Tags + +### What are Tags? + +In SIMPA, Tags are identifiers used to specify and categorize various settings and components within the simulation. +They act as keys in the configuration dictionaries, enabling a clear and organized way to define simulation parameters. +Tags ensure that the configuration is modular, readable, and easy to manage. + +### Purpose of Tags + +- **Organization**: Tags help in structuring the configuration settings systematically. +- **Flexibility**: They allow users to easily modify and extend configurations. +- **Reusability**: Tags facilitate the reuse of settings across different simulations. + +### How Tags Work + +Tags are used to identify different components and their settings within the configuration dictionaries. Each component +has a predefined set of tags associated with it. These tags are used to specify the parameters and properties of the +components. + +### What Tags are Available? + +The list of Tags available in SIMPA is very extensive (see [simpa.utils](simpa.utils.rst) for full list), due to the +level of customisation available to the user. To get to grips with the more commonly used Tags, we highly recommend +consulting the [examples](simpa_examples.rst). + +## Concept of Settings + +Settings in SIMPA are configurations that control the behavior of the simulation. They are used to specify parameters +and options for both the overall simulation and individual components of the simulation pipeline. Proper configuration +of these settings is crucial for accurate and efficient simulations. This documentation provides a foundational +understanding of these settings, allowing users to customize their simulations effectively. For more detailed +information on specific settings and components, users are encouraged to refer to the source code and additional +documentation provided within the SIMPA repository. + +### Global Settings + +Global settings apply to the entire simulation and include parameters that are relevant across multiple components. +These settings typically encompass general simulation properties such as physical constants and overarching simulation +parameters. + +#### Example of Global Settings + +- `Tags.SPACING_MM`: The voxel spacing in the simulation. +- `Tags.GPU`: Whether there is a GPU available to perform the computation. +- `Tags.WAVELENGTHS`: The wavelengths that will later be simulated. + +### Component Settings + +Component settings are specific to individual components within the simulation pipeline. Each component can have its own +set of settings that determine how it behaves. These settings allow for fine-grained control over the simulation +process, enabling customization and optimization for specific experimental conditions. + +#### Difference Between Global and Component Settings + +- **Scope**: + - Global settings affect the entire simulation framework. + - Component settings only influence the behavior of their respective components. + +- **Usage**: + - Global settings are defined once and applied universally. + - Component settings are defined for each component individually, allowing for component-specific customization. + +#### Implementation +For a given simulation, the overall simulation settings will first be created from the global settings. Then, each +components setting will be added. Overall, a dictionary instance will be created with all of the global settings as well +as the components settings as sub-dictionaries. + +## Available Component Settings + +The following list describes the available component settings for various components in the SIMPA framework. Each component may have a unique set of settings that control its behavior. + +### 1. Volume Creation + +Settings for the volume creation component, which defines the method used to create the simulation volume; and therefore +ultimately decides the properties of the simulation volume. It is added to the simulation settings using: +[set_volume_creator_settings](../../simpa/utils/settings.py). + +#### Examples of Volume Creation Settings +- `Tags.STRUCTURES`: When using the model based volume creation adapter, sets the structures to be fill the volume. +- `Tags.INPUT_SEGMENTATION_VOLUME`: When using the segmentation based volume creation adapter, the segmentation mapping +will be specified under this tag. + +### 2. Acoustic Model + +Settings for the acoustic forward model component, which simulates the propagation of acoustic waves. It is added to the +simulation settings using: [set_acoustic_settings](../../simpa/utils/settings.py). + +#### Examples of Acoustic Settings +- `Tags.KWAVE_PROPERTY_ALPHA_POWER`: The exponent in the exponential acoustic attenuation law of k-Wave. +- `Tags.RECORDMOVIE`: If true, a movie of the k-Wave simulation will be recorded. + +### 3. Optical Model + +Settings for the optical model component, which simulates the propagation of light through the medium. It is added to +the simulation settings using: [set_optical_settings](../../simpa/utils/settings.py). + +#### Examples of Optical Settings +- `Tags.OPTICAL_MODEL_NUMBER_PHOTONS`: The number of photons used in the optical simulation. +- `Tags.LASER_PULSE_ENERGY_IN_MILLIJOULE`: The laser pulse energy used in the optical simulation. + +### 4. Reconstruction model + +Settings for the reconstruction model, which reconstructs the image from the simulated signals. It is added to the +simulation settings using: [set_reconstruction_settings](../../simpa/utils/settings.py). + +#### Examples of Reconstruction Settings + +- `Tags.RECONSTRUCTION_BMODE_AFTER_RECONSTRUCTION`: Specifies whether an envelope detection should be performed after +reconstruction. +- `Tags.RECONSTRUCTION_PERFORM_BANDPASS_FILTERING`: Whether bandpass filtering should be applied or not. diff --git a/pyproject.toml b/pyproject.toml index b1937a95..65100c0d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,17 +7,17 @@ name = "simpa" dynamic = ["version"] authors = [ {name = "Division of Intelligent Medical Systems (IMSY), DKFZ", email = "k.dreher@dkfz-heidelberg.de"}, - {name = "Janek Groehl"} + {name = "Janek Groehl "} ] description = "Simulation and Image Processing for Photonics and Acoustics" license = {text = "MIT"} readme = "README.md" keywords = ["simulation", "photonics", "acoustics"] -requires-python = ">=3.8" +requires-python = ">=3.9" dependencies = [ "matplotlib>=3.5.0", # Uses PSF-License (MIT compatible) "numpy>=1.21.4", # Uses BSD-License (MIT compatible) - "scipy>=1.7.2,<1.14.0", # Uses BSD-like-License (MIT compatible) + "scipy>=1.13.0", # Uses BSD-like-License (MIT compatible) "pynrrd>=0.4.2", # Uses MIT-License (MIT compatible) "scikit-image>=0.18.3", # Uses BSD-License (MIT compatible) "xmltodict>=0.12.0", # Uses MIT-License (MIT compatible) @@ -33,19 +33,27 @@ dependencies = [ "jdata>=0.5.2", # Uses Apache 2.0-License (MIT compatible) "pre-commit>=3.2.2", # Uses MIT-License (MIT compatible) "PyWavelets", # Uses MIT-License (MIT compatible) + "scikit-learn>=1.1.0", # Uses BSD-License (MIT compatible) ] [project.optional-dependencies] docs = [ - "sphinx-rtd-theme>=2.0.0,<3.0.0", - "Sphinx>=5.1.1,<6.0.0", - "myst-parser>=0.18.0,<1.1" + "sphinx-rtd-theme>=2.0.0,<3.0.0", # Uses MIT-License (MIT compatible) + "Sphinx>=5.1.1,<6.0.0", # Uses BSD-License (MIT compatible) + "myst-parser>=0.18.0,<1.1" # Uses MIT-License (MIT compatible) ] profile = [ - "pytorch_memlab>=0.3.0,<0.4.0", - "line_profiler>=4.0.0,<5.0.0", - "memory_profiler>=0.61.0,<0.62.0" + "pytorch_memlab>=0.3.0", # Uses MIT-License (MIT compatible) + "line_profiler>=4.0.0", # Uses BSD-License (MIT compatible) + "memory_profiler>=0.61.0", # Uses BSD-License (MIT compatible) + "tabulate>=0.9.0" # Uses MIT-License (MIT compatible) + ] +testing = [ + "mdutils>=1.4.0", # Uses MIT-License (MIT compatible) + "pypandoc>=1.13", # Uses MIT-License (MIT compatible) + "pypandoc_binary>=1.13" # Uses MIT-License (MIT compatible) + ] [project.urls] Homepage = "https://github.com/IMSY-DKFZ/simpa" @@ -53,7 +61,7 @@ Documentation = "https://simpa.readthedocs.io/en/main/" Repository = "https://github.com/IMSY-DKFZ/simpa" [tool.setuptools.packages.find] -include = ["simpa", "simpa_tests"] +include = ["simpa", "simpa_tests", "simpa_examples"] [tool.setuptools_scm] diff --git a/simpa/__init__.py b/simpa/__init__.py index 58fff0a3..c653bfe1 100644 --- a/simpa/__init__.py +++ b/simpa/__init__.py @@ -4,33 +4,40 @@ from .utils import * from .log import Logger +from importlib.metadata import version, PackageNotFoundError -from .core.simulation_modules.volume_creation_module.volume_creation_module_model_based_adapter import \ - ModelBasedVolumeCreationAdapter -from .core.simulation_modules.volume_creation_module.volume_creation_module_segmentation_based_adapter import \ - SegmentationBasedVolumeCreationAdapter -from .core.simulation_modules.optical_simulation_module.optical_forward_model_mcx_adapter import \ + +try: + __version__ = version("simpa") +except PackageNotFoundError: + __version__ = "unknown version" + +from .core.simulation_modules.volume_creation_module.model_based_adapter import \ + ModelBasedAdapter +from .core.simulation_modules.volume_creation_module.segmentation_based_adapter import \ + SegmentationBasedAdapter +from .core.simulation_modules.optical_module.mcx_adapter import \ MCXAdapter -from .core.simulation_modules.optical_simulation_module.optical_forward_model_mcx_reflectance_adapter import \ - MCXAdapterReflectance -from .core.simulation_modules.acoustic_forward_module.acoustic_forward_module_k_wave_adapter import \ +from .core.simulation_modules.optical_module.mcx_reflectance_adapter import \ + MCXReflectanceAdapter +from .core.simulation_modules.acoustic_module.k_wave_adapter import \ KWaveAdapter -from .core.simulation_modules.reconstruction_module.reconstruction_module_delay_and_sum_adapter import \ +from .core.simulation_modules.reconstruction_module.delay_and_sum_adapter import \ DelayAndSumAdapter -from .core.simulation_modules.reconstruction_module.reconstruction_module_delay_multiply_and_sum_adapter import \ +from .core.simulation_modules.reconstruction_module.delay_multiply_and_sum_adapter import \ DelayMultiplyAndSumAdapter -from .core.simulation_modules.reconstruction_module.reconstruction_module_signed_delay_multiply_and_sum_adapter import \ +from .core.simulation_modules.reconstruction_module.signed_delay_multiply_and_sum_adapter import \ SignedDelayMultiplyAndSumAdapter -from .core.simulation_modules.reconstruction_module.reconstruction_module_time_reversal_adapter import \ +from .core.simulation_modules.reconstruction_module.time_reversal_adapter import \ TimeReversalAdapter -from .core.simulation_modules.reconstruction_module.reconstruction_module_delay_and_sum_adapter import \ +from .core.simulation_modules.reconstruction_module.delay_and_sum_adapter import \ reconstruct_delay_and_sum_pytorch -from .core.simulation_modules.reconstruction_module.reconstruction_module_delay_multiply_and_sum_adapter import \ +from .core.simulation_modules.reconstruction_module.delay_multiply_and_sum_adapter import \ reconstruct_delay_multiply_and_sum_pytorch -from .core.simulation_modules.reconstruction_module.reconstruction_module_signed_delay_multiply_and_sum_adapter import \ +from .core.simulation_modules.reconstruction_module.signed_delay_multiply_and_sum_adapter import \ reconstruct_signed_delay_multiply_and_sum_pytorch -from .core.simulation_modules.acoustic_forward_module.acoustic_forward_module_k_wave_adapter import \ +from .core.simulation_modules.acoustic_module.k_wave_adapter import \ perform_k_wave_acoustic_forward_simulation from simpa.core.processing_components.monospectral.noise import GaussianNoise diff --git a/simpa/core/__init__.py b/simpa/core/__init__.py index 9a49b941..b16f1abf 100644 --- a/simpa/core/__init__.py +++ b/simpa/core/__init__.py @@ -1,33 +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_element_base import PipelineElementBase diff --git a/simpa/core/device_digital_twins/__init__.py b/simpa/core/device_digital_twins/__init__.py index 13bd5bcf..0c7de4d2 100644 --- a/simpa/core/device_digital_twins/__init__.py +++ b/simpa/core/device_digital_twins/__init__.py @@ -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 diff --git a/simpa/core/device_digital_twins/detection_geometries/__init__.py b/simpa/core/device_digital_twins/detection_geometries/__init__.py index eba6bdd5..e8d6c131 100644 --- a/simpa/core/device_digital_twins/detection_geometries/__init__.py +++ b/simpa/core/device_digital_twins/detection_geometries/__init__.py @@ -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 diff --git a/simpa/core/device_digital_twins/detection_geometries/detection_geometry_base.py b/simpa/core/device_digital_twins/detection_geometries/detection_geometry_base.py new file mode 100644 index 00000000..eba6bdd5 --- /dev/null +++ b/simpa/core/device_digital_twins/detection_geometries/detection_geometry_base.py @@ -0,0 +1,136 @@ +# 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 +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 diff --git a/simpa/core/device_digital_twins/digital_device_twin_base.py b/simpa/core/device_digital_twins/digital_device_twin_base.py new file mode 100644 index 00000000..478361a0 --- /dev/null +++ b/simpa/core/device_digital_twins/digital_device_twin_base.py @@ -0,0 +1,132 @@ +# SPDX-FileCopyrightText: 2021 Division of Intelligent Medical Systems, DKFZ +# 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 diff --git a/simpa/core/device_digital_twins/illumination_geometries/__init__.py b/simpa/core/device_digital_twins/illumination_geometries/__init__.py index 6c5780bf..cd10d903 100644 --- a/simpa/core/device_digital_twins/illumination_geometries/__init__.py +++ b/simpa/core/device_digital_twins/illumination_geometries/__init__.py @@ -2,69 +2,4 @@ # SPDX-FileCopyrightText: 2021 Janek Groehl # SPDX-License-Identifier: MIT -from abc import abstractmethod -from simpa.core.device_digital_twins import DigitalDeviceTwinBase -from simpa.utils import Settings -import numpy as np - - -class IlluminationGeometryBase(DigitalDeviceTwinBase): - """ - This class is the base class for representing all illumination geometries. - """ - - def __init__(self, device_position_mm=None, source_direction_vector=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 illuminator positions. - :type device_position_mm: ndarray - - :param source_direction_vector: Direction of the illumination source. - :type source_direction_vector: 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(IlluminationGeometryBase, self).__init__(device_position_mm=device_position_mm, - field_of_view_extent_mm=field_of_view_extent_mm) - - if source_direction_vector is None: - self.source_direction_vector = [0, 0, 1] - else: - self.source_direction_vector = source_direction_vector - self.normalized_source_direction_vector = self.source_direction_vector / np.linalg.norm( - self.source_direction_vector) - - @abstractmethod - def get_mcx_illuminator_definition(self, global_settings) -> dict: - """ - IMPORTANT: This method creates a dictionary that contains tags as they are expected for the - mcx simulation tool to represent the illumination geometry of this device. - - :param global_settings: The global_settings instance containing the simulation instructions. - :type global_settings: Settings - - :return: Dictionary that includes all parameters needed for mcx. - :rtype: dict - """ - pass - - def check_settings_prerequisites(self, global_settings) -> bool: - return True - - def update_settings_for_use_of_model_based_volume_creator(self, global_settings) -> Settings: - return global_settings - - def serialize(self) -> dict: - serialized_device = self.__dict__ - device_dict = {"IlluminationGeometryBase": serialized_device} - return device_dict - - @staticmethod - def deserialize(dictionary_to_deserialize): - deserialized_device = IlluminationGeometryBase() - for key, value in dictionary_to_deserialize.items(): - deserialized_device.__dict__[key] = value - return deserialized_device +from .illumination_geometry_base import IlluminationGeometryBase diff --git a/simpa/core/device_digital_twins/illumination_geometries/illumination_geometry_base.py b/simpa/core/device_digital_twins/illumination_geometries/illumination_geometry_base.py new file mode 100644 index 00000000..6c5780bf --- /dev/null +++ b/simpa/core/device_digital_twins/illumination_geometries/illumination_geometry_base.py @@ -0,0 +1,70 @@ +# 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.utils import Settings +import numpy as np + + +class IlluminationGeometryBase(DigitalDeviceTwinBase): + """ + This class is the base class for representing all illumination geometries. + """ + + def __init__(self, device_position_mm=None, source_direction_vector=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 illuminator positions. + :type device_position_mm: ndarray + + :param source_direction_vector: Direction of the illumination source. + :type source_direction_vector: 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(IlluminationGeometryBase, self).__init__(device_position_mm=device_position_mm, + field_of_view_extent_mm=field_of_view_extent_mm) + + if source_direction_vector is None: + self.source_direction_vector = [0, 0, 1] + else: + self.source_direction_vector = source_direction_vector + self.normalized_source_direction_vector = self.source_direction_vector / np.linalg.norm( + self.source_direction_vector) + + @abstractmethod + def get_mcx_illuminator_definition(self, global_settings) -> dict: + """ + IMPORTANT: This method creates a dictionary that contains tags as they are expected for the + mcx simulation tool to represent the illumination geometry of this device. + + :param global_settings: The global_settings instance containing the simulation instructions. + :type global_settings: Settings + + :return: Dictionary that includes all parameters needed for mcx. + :rtype: dict + """ + pass + + def check_settings_prerequisites(self, global_settings) -> bool: + return True + + def update_settings_for_use_of_model_based_volume_creator(self, global_settings) -> Settings: + return global_settings + + def serialize(self) -> dict: + serialized_device = self.__dict__ + device_dict = {"IlluminationGeometryBase": serialized_device} + return device_dict + + @staticmethod + def deserialize(dictionary_to_deserialize): + deserialized_device = IlluminationGeometryBase() + for key, value in dictionary_to_deserialize.items(): + deserialized_device.__dict__[key] = value + return deserialized_device diff --git a/simpa/core/device_digital_twins/pa_devices/__init__.py b/simpa/core/device_digital_twins/pa_devices/__init__.py index 38b0d202..7b8845f7 100644 --- a/simpa/core/device_digital_twins/pa_devices/__init__.py +++ b/simpa/core/device_digital_twins/pa_devices/__init__.py @@ -2,160 +2,4 @@ # SPDX-FileCopyrightText: 2021 Janek Groehl # SPDX-License-Identifier: MIT -import numpy as np -from abc import ABC -from simpa.core.device_digital_twins import DigitalDeviceTwinBase - - -class PhotoacousticDevice(DigitalDeviceTwinBase, ABC): - """Base class of a photoacoustic device. It consists of one detection geometry that describes the geometry of the - single detector elements and a list of illuminators. - - A Photoacoustic Device can be initialized as follows:: - - import simpa as sp - import numpy as np - - # Initialise a PhotoacousticDevice with its position and field of view - device = sp.PhotoacousticDevice(device_position_mm=np.array([10, 10, 0]), - field_of_view_extent_mm=np.array([-20, 20, 0, 0, 0, 20])) - - # Option 1) Set the detection geometry position relative to the PhotoacousticDevice - device.set_detection_geometry(sp.DetectionGeometry(), - detector_position_relative_to_pa_device=np.array([0, 0, -10])) - - # Option 2) Set the detection geometry position absolute - device.set_detection_geometry( - sp.DetectionGeometryBase(device_position_mm=np.array([10, 10, -10]))) - - # Option 1) Add the illumination geometry position relative to the PhotoacousticDevice - device.add_illumination_geometry(sp.IlluminationGeometry(), - illuminator_position_relative_to_pa_device=np.array([0, 0, 0])) - - # Option 2) Add the illumination geometry position absolute - device.add_illumination_geometry( - sp.IlluminationGeometryBase(device_position_mm=np.array([10, 10, 0])) - - Attributes: - detection_geometry (DetectionGeometryBase): Geometry of the detector elements. - illumination_geometries (list): List of illuminations defined by :py:class:`IlluminationGeometryBase`. - """ - - 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 - """ - super(PhotoacousticDevice, self).__init__(device_position_mm=device_position_mm, - field_of_view_extent_mm=field_of_view_extent_mm) - self.detection_geometry = None - self.illumination_geometries = [] - - def set_detection_geometry(self, detection_geometry, - detector_position_relative_to_pa_device=None): - """Sets the detection geometry for the PA device. The detection geometry can be instantiated with an absolute - position or it can be instantiated without the device_position_mm argument but a position relative to the - position of the PhotoacousticDevice. If both absolute and relative positions are given, the absolute position - is chosen as position of the detection geometry. - - :param detection_geometry: Detection geometry of the PA device. - :type detection_geometry: DetectionGeometryBase - :param detector_position_relative_to_pa_device: Position of the detection geometry relative to the PA device. - :type detector_position_relative_to_pa_device: ndarray - :raises ValueError: if the detection_geometry is None - - """ - if detection_geometry is None: - msg = "The given detection_geometry must not be None!" - self.logger.critical(msg) - raise ValueError(msg) - if np.linalg.norm(detection_geometry.device_position_mm) == 0 and \ - detector_position_relative_to_pa_device is not None: - detection_geometry.device_position_mm = np.add(self.device_position_mm, - detector_position_relative_to_pa_device) - self.detection_geometry = detection_geometry - - def add_illumination_geometry(self, illumination_geometry, illuminator_position_relative_to_pa_device=None): - """Adds an illuminator to the PA device. The illumination geometry can be instantiated with an absolute - position or it can be instantiated without the device_position_mm argument but a position relative to the - position of the PhotoacousticDevice. If both absolute and relative positions are given, the absolute position - is chosen as position of the illumination geometry. - - :param illumination_geometry: Geometry of the illuminator. - :type illumination_geometry: IlluminationGeometryBase - :param illuminator_position_relative_to_pa_device: Position of the illuminator relative to the PA device. - :type illuminator_position_relative_to_pa_device: ndarray - :raises ValueError: if the illumination_geometry is None - - """ - if illumination_geometry is None: - msg = "The given illumination_geometry must not be None!" - self.logger.critical(msg) - raise ValueError(msg) - if np.linalg.norm(illumination_geometry.device_position_mm) == 0: - if illuminator_position_relative_to_pa_device is not None: - illumination_geometry.device_position_mm = np.add(self.device_position_mm, - illuminator_position_relative_to_pa_device) - else: - illumination_geometry.device_position_mm = self.device_position_mm - self.illumination_geometries.append(illumination_geometry) - - def get_detection_geometry(self): - """ - :return: None if no detection geometry was set or an instance of DetectionGeometryBase. - :rtype: None, DetectionGeometryBase - """ - return self.detection_geometry - - def get_illumination_geometry(self): - """ - :return: None, if no illumination geometry was defined, - an instance of IlluminationGeometryBase if exactly one geometry was defined, - a list of IlluminationGeometryBase instances if more than one device was defined. - :rtype: None, IlluminationGeometryBase - """ - if len(self.illumination_geometries) == 0: - return None - - if len(self.illumination_geometries) == 1: - return self.illumination_geometries[0] - - return self.illumination_geometries - - def check_settings_prerequisites(self, global_settings) -> bool: - _result = True - if self.detection_geometry is not None \ - and not self.detection_geometry.check_settings_prerequisites(global_settings): - _result = False - for illumination_geometry in self.illumination_geometries: - if illumination_geometry is not None \ - and not illumination_geometry.check_settings_prerequisites(global_settings): - _result = False - return _result - - def update_settings_for_use_of_model_based_volume_creator(self, global_settings): - pass - - def serialize(self) -> dict: - serialized_device = self.__dict__ - device_dict = {"PhotoacousticDevice": serialized_device} - return device_dict - - @staticmethod - def deserialize(dictionary_to_deserialize): - deserialized_device = PhotoacousticDevice( - device_position_mm=dictionary_to_deserialize["device_position_mm"], - field_of_view_extent_mm=dictionary_to_deserialize["field_of_view_extent_mm"]) - det_geometry = dictionary_to_deserialize["detection_geometry"] - if det_geometry != "None": - deserialized_device.set_detection_geometry(dictionary_to_deserialize["detection_geometry"]) - if "illumination_geometries" in dictionary_to_deserialize: - for illumination_geometry in dictionary_to_deserialize["illumination_geometries"]: - deserialized_device.illumination_geometries.append(illumination_geometry) - - return deserialized_device +from .photoacoustic_device import PhotoacousticDevice diff --git a/simpa/core/device_digital_twins/pa_devices/ithera_msot_acuity.py b/simpa/core/device_digital_twins/pa_devices/ithera_msot_acuity.py index 091c9396..749e0f19 100644 --- a/simpa/core/device_digital_twins/pa_devices/ithera_msot_acuity.py +++ b/simpa/core/device_digital_twins/pa_devices/ithera_msot_acuity.py @@ -1,6 +1,8 @@ # SPDX-FileCopyrightText: 2021 Division of Intelligent Medical Systems, DKFZ # SPDX-FileCopyrightText: 2021 Janek Groehl # SPDX-License-Identifier: MIT +import torch +import torch.nn.functional as F from simpa.core.device_digital_twins import PhotoacousticDevice, \ CurvedArrayDetectionGeometry, MSOTAcuityIlluminationGeometry @@ -88,7 +90,7 @@ def __init__(self, device_position_mm: np.ndarray = None, -y_pos_relative_to_membrane, -43.2])) - def update_settings_for_use_of_model_based_volume_creator(self, global_settings): + def update_settings_for_use_of_model_based_volume_creator(self, global_settings: Settings): """ Updates the volume creation settings of the model based volume creator according to the size of the device. :param global_settings: Settings for the entire simulation pipeline. @@ -105,6 +107,8 @@ def update_settings_for_use_of_model_based_volume_creator(self, global_settings) probe_size_mm = self.probe_height_mm mediprene_layer_height_mm = self.mediprene_membrane_height_mm heavy_water_layer_height_mm = probe_size_mm - mediprene_layer_height_mm + spacing_mm = global_settings[Tags.SPACING_MM] + old_volume_height_pixels = round(global_settings[Tags.DIM_VOLUME_Z_MM] / spacing_mm) if Tags.US_GEL in volume_creator_settings and volume_creator_settings[Tags.US_GEL]: us_gel_thickness = np.random.normal(0.4, 0.1) @@ -122,13 +126,10 @@ def update_settings_for_use_of_model_based_volume_creator(self, global_settings) # 1 voxel is added (0.5 on both sides) to make sure no rounding errors lead to a detector element being outside # of the simulated volume. - if global_settings[Tags.DIM_VOLUME_X_MM] < round(self.detection_geometry.probe_width_mm) + \ - global_settings[Tags.SPACING_MM]: - width_shift_for_structures_mm = (round(self.detection_geometry.probe_width_mm) + - global_settings[Tags.SPACING_MM] - + if global_settings[Tags.DIM_VOLUME_X_MM] < round(self.detection_geometry.probe_width_mm) + spacing_mm: + width_shift_for_structures_mm = (round(self.detection_geometry.probe_width_mm) + spacing_mm - global_settings[Tags.DIM_VOLUME_X_MM]) / 2 - global_settings[Tags.DIM_VOLUME_X_MM] = round(self.detection_geometry.probe_width_mm) + \ - global_settings[Tags.SPACING_MM] + global_settings[Tags.DIM_VOLUME_X_MM] = round(self.detection_geometry.probe_width_mm) + spacing_mm self.logger.debug(f"Changed Tags.DIM_VOLUME_X_MM to {global_settings[Tags.DIM_VOLUME_X_MM]}") else: width_shift_for_structures_mm = 0 @@ -139,6 +140,17 @@ def update_settings_for_use_of_model_based_volume_creator(self, global_settings) self.logger.debug("Adjusting " + str(structure_key)) structure_dict = volume_creator_settings[Tags.STRUCTURES][structure_key] if Tags.STRUCTURE_START_MM in structure_dict: + for molecule in structure_dict[Tags.MOLECULE_COMPOSITION]: + old_volume_fraction = getattr(molecule, "volume_fraction") + if isinstance(old_volume_fraction, torch.Tensor): + if old_volume_fraction.shape[2] == old_volume_height_pixels: + width_shift_pixels = round(width_shift_for_structures_mm / spacing_mm) + z_shift_pixels = round(z_dim_position_shift_mm / spacing_mm) + padding_height = (z_shift_pixels, 0, 0, 0, 0, 0) + padding_width = ((width_shift_pixels, width_shift_pixels), (0, 0), (0, 0)) + padded_up = F.pad(old_volume_fraction, padding_height, mode='constant', value=0) + padded_vol = np.pad(padded_up.numpy(), padding_width, mode='edge') + setattr(molecule, "volume_fraction", torch.from_numpy(padded_vol)) structure_dict[Tags.STRUCTURE_START_MM][0] = structure_dict[Tags.STRUCTURE_START_MM][ 0] + width_shift_for_structures_mm structure_dict[Tags.STRUCTURE_START_MM][2] = structure_dict[Tags.STRUCTURE_START_MM][ diff --git a/simpa/core/device_digital_twins/pa_devices/photoacoustic_device.py b/simpa/core/device_digital_twins/pa_devices/photoacoustic_device.py new file mode 100644 index 00000000..38b0d202 --- /dev/null +++ b/simpa/core/device_digital_twins/pa_devices/photoacoustic_device.py @@ -0,0 +1,161 @@ +# SPDX-FileCopyrightText: 2021 Division of Intelligent Medical Systems, DKFZ +# SPDX-FileCopyrightText: 2021 Janek Groehl +# SPDX-License-Identifier: MIT + +import numpy as np +from abc import ABC +from simpa.core.device_digital_twins import DigitalDeviceTwinBase + + +class PhotoacousticDevice(DigitalDeviceTwinBase, ABC): + """Base class of a photoacoustic device. It consists of one detection geometry that describes the geometry of the + single detector elements and a list of illuminators. + + A Photoacoustic Device can be initialized as follows:: + + import simpa as sp + import numpy as np + + # Initialise a PhotoacousticDevice with its position and field of view + device = sp.PhotoacousticDevice(device_position_mm=np.array([10, 10, 0]), + field_of_view_extent_mm=np.array([-20, 20, 0, 0, 0, 20])) + + # Option 1) Set the detection geometry position relative to the PhotoacousticDevice + device.set_detection_geometry(sp.DetectionGeometry(), + detector_position_relative_to_pa_device=np.array([0, 0, -10])) + + # Option 2) Set the detection geometry position absolute + device.set_detection_geometry( + sp.DetectionGeometryBase(device_position_mm=np.array([10, 10, -10]))) + + # Option 1) Add the illumination geometry position relative to the PhotoacousticDevice + device.add_illumination_geometry(sp.IlluminationGeometry(), + illuminator_position_relative_to_pa_device=np.array([0, 0, 0])) + + # Option 2) Add the illumination geometry position absolute + device.add_illumination_geometry( + sp.IlluminationGeometryBase(device_position_mm=np.array([10, 10, 0])) + + Attributes: + detection_geometry (DetectionGeometryBase): Geometry of the detector elements. + illumination_geometries (list): List of illuminations defined by :py:class:`IlluminationGeometryBase`. + """ + + 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 + """ + super(PhotoacousticDevice, self).__init__(device_position_mm=device_position_mm, + field_of_view_extent_mm=field_of_view_extent_mm) + self.detection_geometry = None + self.illumination_geometries = [] + + def set_detection_geometry(self, detection_geometry, + detector_position_relative_to_pa_device=None): + """Sets the detection geometry for the PA device. The detection geometry can be instantiated with an absolute + position or it can be instantiated without the device_position_mm argument but a position relative to the + position of the PhotoacousticDevice. If both absolute and relative positions are given, the absolute position + is chosen as position of the detection geometry. + + :param detection_geometry: Detection geometry of the PA device. + :type detection_geometry: DetectionGeometryBase + :param detector_position_relative_to_pa_device: Position of the detection geometry relative to the PA device. + :type detector_position_relative_to_pa_device: ndarray + :raises ValueError: if the detection_geometry is None + + """ + if detection_geometry is None: + msg = "The given detection_geometry must not be None!" + self.logger.critical(msg) + raise ValueError(msg) + if np.linalg.norm(detection_geometry.device_position_mm) == 0 and \ + detector_position_relative_to_pa_device is not None: + detection_geometry.device_position_mm = np.add(self.device_position_mm, + detector_position_relative_to_pa_device) + self.detection_geometry = detection_geometry + + def add_illumination_geometry(self, illumination_geometry, illuminator_position_relative_to_pa_device=None): + """Adds an illuminator to the PA device. The illumination geometry can be instantiated with an absolute + position or it can be instantiated without the device_position_mm argument but a position relative to the + position of the PhotoacousticDevice. If both absolute and relative positions are given, the absolute position + is chosen as position of the illumination geometry. + + :param illumination_geometry: Geometry of the illuminator. + :type illumination_geometry: IlluminationGeometryBase + :param illuminator_position_relative_to_pa_device: Position of the illuminator relative to the PA device. + :type illuminator_position_relative_to_pa_device: ndarray + :raises ValueError: if the illumination_geometry is None + + """ + if illumination_geometry is None: + msg = "The given illumination_geometry must not be None!" + self.logger.critical(msg) + raise ValueError(msg) + if np.linalg.norm(illumination_geometry.device_position_mm) == 0: + if illuminator_position_relative_to_pa_device is not None: + illumination_geometry.device_position_mm = np.add(self.device_position_mm, + illuminator_position_relative_to_pa_device) + else: + illumination_geometry.device_position_mm = self.device_position_mm + self.illumination_geometries.append(illumination_geometry) + + def get_detection_geometry(self): + """ + :return: None if no detection geometry was set or an instance of DetectionGeometryBase. + :rtype: None, DetectionGeometryBase + """ + return self.detection_geometry + + def get_illumination_geometry(self): + """ + :return: None, if no illumination geometry was defined, + an instance of IlluminationGeometryBase if exactly one geometry was defined, + a list of IlluminationGeometryBase instances if more than one device was defined. + :rtype: None, IlluminationGeometryBase + """ + if len(self.illumination_geometries) == 0: + return None + + if len(self.illumination_geometries) == 1: + return self.illumination_geometries[0] + + return self.illumination_geometries + + def check_settings_prerequisites(self, global_settings) -> bool: + _result = True + if self.detection_geometry is not None \ + and not self.detection_geometry.check_settings_prerequisites(global_settings): + _result = False + for illumination_geometry in self.illumination_geometries: + if illumination_geometry is not None \ + and not illumination_geometry.check_settings_prerequisites(global_settings): + _result = False + return _result + + def update_settings_for_use_of_model_based_volume_creator(self, global_settings): + pass + + def serialize(self) -> dict: + serialized_device = self.__dict__ + device_dict = {"PhotoacousticDevice": serialized_device} + return device_dict + + @staticmethod + def deserialize(dictionary_to_deserialize): + deserialized_device = PhotoacousticDevice( + device_position_mm=dictionary_to_deserialize["device_position_mm"], + field_of_view_extent_mm=dictionary_to_deserialize["field_of_view_extent_mm"]) + det_geometry = dictionary_to_deserialize["detection_geometry"] + if det_geometry != "None": + deserialized_device.set_detection_geometry(dictionary_to_deserialize["detection_geometry"]) + if "illumination_geometries" in dictionary_to_deserialize: + for illumination_geometry in dictionary_to_deserialize["illumination_geometries"]: + deserialized_device.illumination_geometries.append(illumination_geometry) + + return deserialized_device diff --git a/simpa/core/pipeline_element_base.py b/simpa/core/pipeline_element_base.py new file mode 100644 index 00000000..2fdcfb10 --- /dev/null +++ b/simpa/core/pipeline_element_base.py @@ -0,0 +1,33 @@ +# 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 PipelineElementBase: + """ + Defines a pipeline element (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 diff --git a/simpa/core/processing_components/__init__.py b/simpa/core/processing_components/__init__.py index 881a8f2d..2c0e2695 100644 --- a/simpa/core/processing_components/__init__.py +++ b/simpa/core/processing_components/__init__.py @@ -2,20 +2,4 @@ # SPDX-FileCopyrightText: 2021 Janek Groehl # SPDX-License-Identifier: MIT -from abc import ABC -from simpa.core import PipelineModule - - -class ProcessingComponent(PipelineModule, ABC): - """ - Defines a pipeline processing component, which can be used to pre- or post-process simulation data. - """ - - def __init__(self, global_settings, component_settings_key: str): - """ - Initialises the ProcessingComponent. - - :param component_settings_key: The key where the component settings are stored in - """ - super(ProcessingComponent, self).__init__(global_settings=global_settings) - self.component_settings = global_settings[component_settings_key] +from .processing_component_base import ProcessingComponentBase diff --git a/simpa/core/processing_components/monospectral/field_of_view_cropping.py b/simpa/core/processing_components/monospectral/field_of_view_cropping.py index b950dc8f..004a0eba 100644 --- a/simpa/core/processing_components/monospectral/field_of_view_cropping.py +++ b/simpa/core/processing_components/monospectral/field_of_view_cropping.py @@ -2,15 +2,16 @@ # SPDX-FileCopyrightText: 2021 Janek Groehl # SPDX-License-Identifier: MIT -from simpa.utils import Tags, Settings +from simpa.core.simulation_modules.reconstruction_module.reconstruction_utils import compute_image_dimensions +from simpa.utils import Tags, Settings, round_x5_away_from_zero from simpa.utils.constants import property_tags, wavelength_independent_properties, toolkit_tags from simpa.io_handling import load_data_field, save_data_field -from simpa.core.processing_components import ProcessingComponent +from simpa.core.processing_components import ProcessingComponentBase from simpa.core.device_digital_twins import DigitalDeviceTwinBase, PhotoacousticDevice import numpy as np -class FieldOfViewCropping(ProcessingComponent): +class FieldOfViewCropping(ProcessingComponentBase): def __init__(self, global_settings, settings_key=None): if settings_key is None: @@ -45,7 +46,10 @@ def run(self, device: DigitalDeviceTwinBase): else: field_of_view_mm = device.get_field_of_view_mm() self.logger.debug(f"FOV (mm): {field_of_view_mm}") - field_of_view_voxels = np.round(field_of_view_mm / self.global_settings[Tags.SPACING_MM]).astype(np.int32) + _, _, _, xdim_start, xdim_end, ydim_start, ydim_end, zdim_start, zdim_end = compute_image_dimensions( + field_of_view_mm, self.global_settings[Tags.SPACING_MM], self.logger) + field_of_view_voxels = [xdim_start, xdim_end, zdim_start, zdim_end, ydim_start, ydim_end] # change ordering + field_of_view_voxels = [int(dim) for dim in field_of_view_voxels] # cast to int self.logger.debug(f"FOV (voxels): {field_of_view_voxels}") # In case it should be cropped from A to A, then crop from A to A+1 @@ -64,7 +68,7 @@ def run(self, device: DigitalDeviceTwinBase): continue try: self.logger.debug(f"Cropping data field {data_field}...") - data_array = load_data_field(self.global_settings[Tags.SIMPA_OUTPUT_PATH], data_field, wavelength) + data_array = load_data_field(self.global_settings[Tags.SIMPA_OUTPUT_FILE_PATH], data_field, wavelength) self.logger.debug(f"data array shape before cropping: {np.shape(data_array)}") self.logger.debug(f"data array shape len: {len(np.shape(data_array))}") @@ -101,6 +105,6 @@ def run(self, device: DigitalDeviceTwinBase): self.logger.debug(f"data array shape after cropping: {np.shape(data_array)}") # save - save_data_field(data_array, self.global_settings[Tags.SIMPA_OUTPUT_PATH], data_field, wavelength) + save_data_field(data_array, self.global_settings[Tags.SIMPA_OUTPUT_FILE_PATH], data_field, wavelength) self.logger.info("Cropping field of view...[Done]") diff --git a/simpa/core/processing_components/monospectral/iterative_qPAI_algorithm.py b/simpa/core/processing_components/monospectral/iterative_qPAI_algorithm.py index 3a942564..fda821a9 100644 --- a/simpa/core/processing_components/monospectral/iterative_qPAI_algorithm.py +++ b/simpa/core/processing_components/monospectral/iterative_qPAI_algorithm.py @@ -11,16 +11,16 @@ from simpa.utils.libraries.literature_values import OpticalTissueProperties, StandardProperties from simpa.utils.libraries.molecule_library import MolecularComposition from simpa.utils.calculate import calculate_gruneisen_parameter_from_temperature -from simpa.core.simulation_modules.optical_simulation_module.optical_forward_model_mcx_adapter import \ +from simpa.core.simulation_modules.optical_module.mcx_adapter import \ MCXAdapter from simpa.utils import Settings from simpa.io_handling import save_data_field, load_data_field from simpa.utils import TISSUE_LIBRARY -from simpa.core.processing_components import ProcessingComponent +from simpa.core.processing_components import ProcessingComponentBase import os -class IterativeqPAI(ProcessingComponent): +class IterativeqPAI(ProcessingComponentBase): """ Applies iterative qPAI Algorithm [1] on simulated initial pressure map and saves the reconstruction result in the hdf5 output file. If a 2-d map of initial_pressure is passed the algorithm saves @@ -45,7 +45,7 @@ class IterativeqPAI(ProcessingComponent): """ def __init__(self, global_settings, component_settings_key: str): - super(ProcessingComponent, self).__init__(global_settings=global_settings) + super(ProcessingComponentBase, self).__init__(global_settings=global_settings) self.global_settings = global_settings self.optical_settings = global_settings.get_optical_settings() @@ -106,7 +106,7 @@ def run(self, pa_device): else: wavelength = self.global_settings[Tags.WAVELENGTHS][0] data_field = Tags.ITERATIVE_qPAI_RESULT - save_data_field(reconstructed_absorption, self.global_settings[Tags.SIMPA_OUTPUT_PATH], + save_data_field(reconstructed_absorption, self.global_settings[Tags.SIMPA_OUTPUT_FILE_PATH], data_field, wavelength) # save a list of all intermediate absorption (2-d only) updates in npy file if intended @@ -224,13 +224,13 @@ def extract_initial_data_from_hdf5(self) -> Tuple[np.ndarray, np.ndarray, np.nda wavelength = self.global_settings[Tags.WAVELENGTHS][0] self.logger.debug(f"Wavelength: {wavelength}") # get initial pressure and scattering - initial_pressure = load_data_field(self.global_settings[Tags.SIMPA_OUTPUT_PATH], + initial_pressure = load_data_field(self.global_settings[Tags.SIMPA_OUTPUT_FILE_PATH], Tags.DATA_FIELD_INITIAL_PRESSURE, wavelength) - scattering = load_data_field(self.global_settings[Tags.SIMPA_OUTPUT_PATH], Tags.DATA_FIELD_SCATTERING_PER_CM, + scattering = load_data_field(self.global_settings[Tags.SIMPA_OUTPUT_FILE_PATH], Tags.DATA_FIELD_SCATTERING_PER_CM, wavelength) - anisotropy = load_data_field(self.global_settings[Tags.SIMPA_OUTPUT_PATH], Tags.DATA_FIELD_ANISOTROPY, + anisotropy = load_data_field(self.global_settings[Tags.SIMPA_OUTPUT_FILE_PATH], Tags.DATA_FIELD_ANISOTROPY, wavelength) # function returns the last iteration result as a numpy array and all iteration results in a list @@ -369,7 +369,7 @@ def standard_optical_properties(self, image_data: np.ndarray) -> dict: scattering = float(self.global_settings[Tags.DATA_FIELD_SCATTERING_PER_CM]) * np.ones(shape) else: background_dict = TISSUE_LIBRARY.muscle() - scattering = float(MolecularComposition.get_properties_for_wavelength(background_dict, + scattering = float(MolecularComposition.get_properties_for_wavelength(background_dict, self.global_settings, wavelength=800)["mus"]) scattering = scattering * np.ones(shape) diff --git a/simpa/core/processing_components/monospectral/noise/gamma_noise.py b/simpa/core/processing_components/monospectral/noise/gamma_noise.py index 011ddffa..23a0ea0d 100644 --- a/simpa/core/processing_components/monospectral/noise/gamma_noise.py +++ b/simpa/core/processing_components/monospectral/noise/gamma_noise.py @@ -5,13 +5,13 @@ import numpy as np import torch -from simpa.core.processing_components import ProcessingComponent +from simpa.core.processing_components import ProcessingComponentBase from simpa.io_handling import load_data_field, save_data_field from simpa.utils import Tags from simpa.utils.quality_assurance.data_sanity_testing import assert_array_well_defined -class GammaNoise(ProcessingComponent): +class GammaNoise(ProcessingComponentBase): """ Applies Gamma noise to the defined data field. The noise will be applied to all wavelengths. @@ -51,7 +51,7 @@ def run(self, device): self.logger.debug(f"Noise model scale: {scale}") wavelength = self.global_settings[Tags.WAVELENGTH] - data_array = load_data_field(self.global_settings[Tags.SIMPA_OUTPUT_PATH], data_field, wavelength) + data_array = load_data_field(self.global_settings[Tags.SIMPA_OUTPUT_FILE_PATH], data_field, wavelength) data_tensor = torch.as_tensor(data_array, dtype=torch.float32, device=self.torch_device) dist = torch.distributions.gamma.Gamma(torch.tensor(shape, dtype=torch.float32, device=self.torch_device), torch.tensor(1.0/scale, dtype=torch.float32, device=self.torch_device)) @@ -65,6 +65,6 @@ def run(self, device): assert_array_well_defined(data_tensor) save_data_field(data_tensor.cpu().numpy().astype(np.float64, copy=False), - self.global_settings[Tags.SIMPA_OUTPUT_PATH], data_field, wavelength) + self.global_settings[Tags.SIMPA_OUTPUT_FILE_PATH], data_field, wavelength) self.logger.info("Applying Gamma Noise Model...[Done]") diff --git a/simpa/core/processing_components/monospectral/noise/gaussian_noise.py b/simpa/core/processing_components/monospectral/noise/gaussian_noise.py index 5c31a50d..5b391597 100644 --- a/simpa/core/processing_components/monospectral/noise/gaussian_noise.py +++ b/simpa/core/processing_components/monospectral/noise/gaussian_noise.py @@ -5,13 +5,13 @@ from simpa.utils import Tags from simpa.utils import EPS from simpa.io_handling import load_data_field, save_data_field -from simpa.core.processing_components import ProcessingComponent +from simpa.core.processing_components import ProcessingComponentBase from simpa.utils.quality_assurance.data_sanity_testing import assert_array_well_defined import numpy as np import torch -class GaussianNoise(ProcessingComponent): +class GaussianNoise(ProcessingComponentBase): """ Applies Gaussian noise to the defined data field. The noise will be applied to all wavelengths. @@ -56,7 +56,7 @@ def run(self, device): self.logger.debug(f"Noise model non-negative: {non_negative}") wavelength = self.global_settings[Tags.WAVELENGTH] - data_array = load_data_field(self.global_settings[Tags.SIMPA_OUTPUT_PATH], data_field, wavelength) + data_array = load_data_field(self.global_settings[Tags.SIMPA_OUTPUT_FILE_PATH], data_field, wavelength) data_tensor = torch.as_tensor(data_array, dtype=torch.float32, device=self.torch_device) dist = torch.distributions.normal.Normal(torch.tensor(mean, dtype=torch.float32, device=self.torch_device), torch.tensor(std, dtype=torch.float32, device=self.torch_device)) @@ -72,6 +72,6 @@ def run(self, device): if non_negative: data_tensor[data_tensor < EPS] = EPS save_data_field(data_tensor.cpu().numpy().astype(np.float64, copy=False), - self.global_settings[Tags.SIMPA_OUTPUT_PATH], data_field, wavelength) + self.global_settings[Tags.SIMPA_OUTPUT_FILE_PATH], data_field, wavelength) self.logger.info("Applying Gaussian Noise Model...[Done]") diff --git a/simpa/core/processing_components/monospectral/noise/poisson_noise.py b/simpa/core/processing_components/monospectral/noise/poisson_noise.py index 0fd544e6..159a5f51 100644 --- a/simpa/core/processing_components/monospectral/noise/poisson_noise.py +++ b/simpa/core/processing_components/monospectral/noise/poisson_noise.py @@ -4,13 +4,13 @@ from simpa.utils import Tags from simpa.io_handling import load_data_field, save_data_field -from simpa.core.processing_components import ProcessingComponent +from simpa.core.processing_components import ProcessingComponentBase from simpa.utils.quality_assurance.data_sanity_testing import assert_array_well_defined import numpy as np import torch -class PoissonNoise(ProcessingComponent): +class PoissonNoise(ProcessingComponentBase): """ Applies Poisson noise to the defined data field. The noise will be applied to all wavelengths. @@ -44,7 +44,7 @@ def run(self, device): self.logger.debug(f"Noise model mean: {mean}") wavelength = self.global_settings[Tags.WAVELENGTH] - data_array = load_data_field(self.global_settings[Tags.SIMPA_OUTPUT_PATH], data_field, wavelength) + data_array = load_data_field(self.global_settings[Tags.SIMPA_OUTPUT_FILE_PATH], data_field, wavelength) data_tensor = torch.as_tensor(data_array, dtype=torch.float32, device=self.torch_device) dist = torch.distributions.poisson.Poisson(torch.tensor(mean, dtype=torch.float32, device=self.torch_device)) @@ -57,6 +57,6 @@ def run(self, device): assert_array_well_defined(data_tensor) save_data_field(data_tensor.cpu().numpy().astype(np.float64, copy=False), - self.global_settings[Tags.SIMPA_OUTPUT_PATH], data_field, wavelength) + self.global_settings[Tags.SIMPA_OUTPUT_FILE_PATH], data_field, wavelength) self.logger.info("Applying Poisson Noise Model...[Done]") diff --git a/simpa/core/processing_components/monospectral/noise/salt_and_pepper_noise.py b/simpa/core/processing_components/monospectral/noise/salt_and_pepper_noise.py index 4204b32e..5e82a6f4 100644 --- a/simpa/core/processing_components/monospectral/noise/salt_and_pepper_noise.py +++ b/simpa/core/processing_components/monospectral/noise/salt_and_pepper_noise.py @@ -4,13 +4,13 @@ from simpa.utils import Tags from simpa.io_handling import load_data_field, save_data_field -from simpa.core.processing_components import ProcessingComponent +from simpa.core.processing_components import ProcessingComponentBase from simpa.utils.quality_assurance.data_sanity_testing import assert_array_well_defined import numpy as np import torch -class SaltAndPepperNoise(ProcessingComponent): +class SaltAndPepperNoise(ProcessingComponentBase): """ Applies salt and pepper noise to the defined data field. The noise will be applied to all wavelengths. @@ -38,7 +38,7 @@ def run(self, device): data_field = self.component_settings[Tags.DATA_FIELD] wavelength = self.global_settings[Tags.WAVELENGTH] - data_array = load_data_field(self.global_settings[Tags.SIMPA_OUTPUT_PATH], data_field, wavelength) + data_array = load_data_field(self.global_settings[Tags.SIMPA_OUTPUT_FILE_PATH], data_field, wavelength) data_tensor = torch.as_tensor(data_array, dtype=torch.float32, device=self.torch_device) min_noise = torch.min(data_tensor).item() @@ -69,6 +69,6 @@ def run(self, device): assert_array_well_defined(data_tensor) save_data_field(data_tensor.cpu().numpy().astype(np.float64, copy=False), - self.global_settings[Tags.SIMPA_OUTPUT_PATH], data_field, wavelength) + self.global_settings[Tags.SIMPA_OUTPUT_FILE_PATH], data_field, wavelength) self.logger.info("Applying Salt And Pepper Noise Model...[Done]") diff --git a/simpa/core/processing_components/monospectral/noise/uniform_noise.py b/simpa/core/processing_components/monospectral/noise/uniform_noise.py index 5610dc43..fb6c831d 100644 --- a/simpa/core/processing_components/monospectral/noise/uniform_noise.py +++ b/simpa/core/processing_components/monospectral/noise/uniform_noise.py @@ -4,13 +4,13 @@ from simpa.utils import Tags from simpa.io_handling import load_data_field, save_data_field -from simpa.core.processing_components import ProcessingComponent +from simpa.core.processing_components import ProcessingComponentBase from simpa.utils.quality_assurance.data_sanity_testing import assert_array_well_defined import numpy as np import torch -class UniformNoise(ProcessingComponent): +class UniformNoise(ProcessingComponentBase): """ Applies uniform noise to the defined data field. The noise will be applied to all wavelengths. @@ -52,7 +52,7 @@ def run(self, device): self.logger.debug(f"Noise model max: {max_noise}") wavelength = self.global_settings[Tags.WAVELENGTH] - data_array = load_data_field(self.global_settings[Tags.SIMPA_OUTPUT_PATH], data_field, wavelength) + data_array = load_data_field(self.global_settings[Tags.SIMPA_OUTPUT_FILE_PATH], data_field, wavelength) data_tensor = torch.as_tensor(data_array, dtype=torch.float32, device=self.torch_device) dist = torch.distributions.uniform.Uniform(torch.tensor(min_noise, dtype=torch.float32, device=self.torch_device), torch.tensor(max_noise, dtype=torch.float32, device=self.torch_device)) @@ -66,6 +66,6 @@ def run(self, device): assert_array_well_defined(data_tensor) save_data_field(data_tensor.cpu().numpy().astype(np.float64, copy=False), - self.global_settings[Tags.SIMPA_OUTPUT_PATH], data_field, wavelength) + self.global_settings[Tags.SIMPA_OUTPUT_FILE_PATH], data_field, wavelength) self.logger.info("Applying Uniform Noise Model...[Done]") diff --git a/simpa/core/processing_components/multispectral/__init__.py b/simpa/core/processing_components/multispectral/__init__.py index d179c455..bf4ed09d 100644 --- a/simpa/core/processing_components/multispectral/__init__.py +++ b/simpa/core/processing_components/multispectral/__init__.py @@ -1,57 +1,4 @@ # SPDX-FileCopyrightText: 2021 Division of Intelligent Medical Systems, DKFZ # SPDX-FileCopyrightText: 2021 Janek Groehl # SPDX-License-Identifier: MIT -from simpa.io_handling import load_data_field -from simpa.utils import Tags -from simpa.log import Logger -import numpy as np -from abc import ABC, abstractmethod - - -class MultispectralProcessingAlgorithm(ABC): - """ - A MultispectralProcessingAlgorithm class represents an algorithm that works with multispectral input data. - """ - - def __init__(self, global_settings, component_settings_key: str): - """ - Instantiates a multispectral processing algorithm. - - Per default, this methods loads all data from a certain - Tags.DATA_FIELD into a data array for all - Tags.WAVELENGTHS. - - """ - if component_settings_key is None: - raise KeyError("The component settings must be set for a multispectral" - "processing algorithm!") - self.component_settings = global_settings[component_settings_key] - - if Tags.WAVELENGTHS not in self.component_settings: - raise KeyError("Tags.WAVELENGTHS must be in the component_settings of a multispectral processing algorithm") - - if Tags.DATA_FIELD not in self.component_settings: - raise KeyError("Tags.DATA_FIELD must be in the component_settings of a multispectral processing algorithm") - - self.logger = Logger() - self.global_settings = global_settings - self.wavelengths = self.component_settings[Tags.WAVELENGTHS] - self.data_field = self.component_settings[Tags.DATA_FIELD] - - self.data = list() - for i in range(len(self.wavelengths)): - self.data.append(load_data_field(self.global_settings[Tags.SIMPA_OUTPUT_PATH], - self.data_field, - self.wavelengths[i])) - - self.data = np.asarray(self.data) - if Tags.SIGNAL_THRESHOLD in self.component_settings: - self.data[self.data < self.component_settings[Tags.SIGNAL_THRESHOLD]*np.max(self.data)] = 0 - - @abstractmethod - def run(self): - """ - This method must be implemented by the multispectral algorithm, such that - any multispectral algorithm can be executed by invoking the run method. - """ - pass +from .multispectral_processing_algorithm import MultispectralProcessingAlgorithm diff --git a/simpa/core/processing_components/multispectral/linear_unmixing.py b/simpa/core/processing_components/multispectral/linear_unmixing.py index 04e24ff2..bb496df2 100644 --- a/simpa/core/processing_components/multispectral/linear_unmixing.py +++ b/simpa/core/processing_components/multispectral/linear_unmixing.py @@ -109,7 +109,7 @@ def run(self): save_dict["sO2"] = self.calculate_sO2() # save linear unmixing result in hdf5 - save_data_field(save_dict, self.global_settings[Tags.SIMPA_OUTPUT_PATH], + save_data_field(save_dict, self.global_settings[Tags.SIMPA_OUTPUT_FILE_PATH], Tags.LINEAR_UNMIXING_RESULT, wavelength=None) self.logger.info("Performing linear spectral unmixing......[Done]") diff --git a/simpa/core/processing_components/multispectral/multispectral_processing_algorithm.py b/simpa/core/processing_components/multispectral/multispectral_processing_algorithm.py new file mode 100644 index 00000000..a3abfdc1 --- /dev/null +++ b/simpa/core/processing_components/multispectral/multispectral_processing_algorithm.py @@ -0,0 +1,57 @@ +# SPDX-FileCopyrightText: 2021 Division of Intelligent Medical Systems, DKFZ +# SPDX-FileCopyrightText: 2021 Janek Groehl +# SPDX-License-Identifier: MIT +from simpa.io_handling import load_data_field +from simpa.utils import Tags +from simpa.log import Logger +import numpy as np +from abc import ABC, abstractmethod + + +class MultispectralProcessingAlgorithm(ABC): + """ + A MultispectralProcessingAlgorithm class represents an algorithm that works with multispectral input data. + """ + + def __init__(self, global_settings, component_settings_key: str): + """ + Instantiates a multispectral processing algorithm. + + Per default, this methods loads all data from a certain + Tags.DATA_FIELD into a data array for all + Tags.WAVELENGTHS. + + """ + if component_settings_key is None: + raise KeyError("The component settings must be set for a multispectral" + "processing algorithm!") + self.component_settings = global_settings[component_settings_key] + + if Tags.WAVELENGTHS not in self.component_settings: + raise KeyError("Tags.WAVELENGTHS must be in the component_settings of a multispectral processing algorithm") + + if Tags.DATA_FIELD not in self.component_settings: + raise KeyError("Tags.DATA_FIELD must be in the component_settings of a multispectral processing algorithm") + + self.logger = Logger() + self.global_settings = global_settings + self.wavelengths = self.component_settings[Tags.WAVELENGTHS] + self.data_field = self.component_settings[Tags.DATA_FIELD] + + self.data = list() + for i in range(len(self.wavelengths)): + self.data.append(load_data_field(self.global_settings[Tags.SIMPA_OUTPUT_FILE_PATH], + self.data_field, + self.wavelengths[i])) + + self.data = np.asarray(self.data) + if Tags.SIGNAL_THRESHOLD in self.component_settings: + self.data[self.data < self.component_settings[Tags.SIGNAL_THRESHOLD]*np.max(self.data)] = 0 + + @abstractmethod + def run(self): + """ + This method must be implemented by the multispectral algorithm, such that + any multispectral algorithm can be executed by invoking the run method. + """ + pass diff --git a/simpa/core/processing_components/processing_component_base.py b/simpa/core/processing_components/processing_component_base.py new file mode 100644 index 00000000..e74f6686 --- /dev/null +++ b/simpa/core/processing_components/processing_component_base.py @@ -0,0 +1,21 @@ +# SPDX-FileCopyrightText: 2021 Division of Intelligent Medical Systems, DKFZ +# SPDX-FileCopyrightText: 2021 Janek Groehl +# SPDX-License-Identifier: MIT + +from abc import ABC +from simpa.core import PipelineElementBase + + +class ProcessingComponentBase(PipelineElementBase, ABC): + """ + Defines a pipeline processing component, which can be used to pre- or post-process simulation data. + """ + + def __init__(self, global_settings, component_settings_key: str): + """ + Initialises the ProcessingComponent object. + + :param component_settings_key: The key where the component settings are stored in + """ + super(ProcessingComponentBase, self).__init__(global_settings=global_settings) + self.component_settings = global_settings[component_settings_key] diff --git a/simpa/core/simulation.py b/simpa/core/simulation.py index e19a884d..66170a42 100644 --- a/simpa/core/simulation.py +++ b/simpa/core/simulation.py @@ -3,6 +3,8 @@ # SPDX-License-Identifier: MIT from simpa.utils import Tags +from simpa import __version__ + from simpa.io_handling.io_hdf5 import save_hdf5, load_hdf5, save_data_field, load_data_field from simpa.io_handling.ipasc import export_to_ipasc from simpa.utils.settings import Settings @@ -53,8 +55,9 @@ def simulate(simulation_pipeline: list, settings: Settings, digital_device_twin: else: simpa_output_path = path + settings[Tags.VOLUME_NAME] - settings[Tags.SIMPA_OUTPUT_PATH] = simpa_output_path + ".hdf5" + settings[Tags.SIMPA_OUTPUT_FILE_PATH] = simpa_output_path + ".hdf5" + simpa_output[Tags.SIMPA_VERSION] = __version__ simpa_output[Tags.SETTINGS] = settings simpa_output[Tags.DIGITAL_DEVICE] = digital_device_twin simpa_output[Tags.SIMULATION_PIPELINE] = [type(x).__name__ for x in simulation_pipeline] @@ -65,18 +68,18 @@ def simulate(simulation_pipeline: list, settings: Settings, digital_device_twin: if Tags.CONTINUE_SIMULATION in settings and settings[Tags.CONTINUE_SIMULATION]: try: - old_pipe = load_data_field(settings[Tags.SIMPA_OUTPUT_PATH], Tags.SIMULATION_PIPELINE) + old_pipe = load_data_field(settings[Tags.SIMPA_OUTPUT_FILE_PATH], Tags.SIMULATION_PIPELINE) except KeyError as e: old_pipe = list() simpa_output[Tags.SIMULATION_PIPELINE] = old_pipe + simpa_output[Tags.SIMULATION_PIPELINE] - previous_settings = load_data_field(settings[Tags.SIMPA_OUTPUT_PATH], Tags.SETTINGS) + previous_settings = load_data_field(settings[Tags.SIMPA_OUTPUT_FILE_PATH], Tags.SETTINGS) previous_settings.update(settings) simpa_output[Tags.SETTINGS] = previous_settings for i in [Tags.SETTINGS, Tags.DIGITAL_DEVICE, Tags.SIMULATION_PIPELINE]: - save_data_field(simpa_output[i], settings[Tags.SIMPA_OUTPUT_PATH], i) + save_data_field(simpa_output[i], settings[Tags.SIMPA_OUTPUT_FILE_PATH], i) else: - save_hdf5(simpa_output, settings[Tags.SIMPA_OUTPUT_PATH]) + save_hdf5(simpa_output, settings[Tags.SIMPA_OUTPUT_FILE_PATH]) logger.debug("Saving settings dictionary...[Done]") for wavelength in settings[Tags.WAVELENGTHS]: @@ -102,15 +105,15 @@ def simulate(simulation_pipeline: list, settings: Settings, digital_device_twin: # by the user manually. Active by default. if not (Tags.DO_FILE_COMPRESSION in settings and not settings[Tags.DO_FILE_COMPRESSION]): - all_data = load_hdf5(settings[Tags.SIMPA_OUTPUT_PATH]) + all_data = load_hdf5(settings[Tags.SIMPA_OUTPUT_FILE_PATH]) if Tags.VOLUME_CREATION_MODEL_SETTINGS in all_data[Tags.SETTINGS] and \ Tags.INPUT_SEGMENTATION_VOLUME in all_data[Tags.SETTINGS][Tags.VOLUME_CREATION_MODEL_SETTINGS]: del all_data[Tags.SETTINGS][Tags.VOLUME_CREATION_MODEL_SETTINGS][Tags.INPUT_SEGMENTATION_VOLUME] - save_hdf5(all_data, settings[Tags.SIMPA_OUTPUT_PATH], file_compression="gzip") + save_hdf5(all_data, settings[Tags.SIMPA_OUTPUT_FILE_PATH], file_compression="gzip") # Export simulation result to the IPASC format. if Tags.DO_IPASC_EXPORT in settings and settings[Tags.DO_IPASC_EXPORT]: logger.info("Exporting to IPASC....") - export_to_ipasc(settings[Tags.SIMPA_OUTPUT_PATH], device=digital_device_twin) + export_to_ipasc(settings[Tags.SIMPA_OUTPUT_FILE_PATH], device=digital_device_twin) logger.info(f"The entire simulation pipeline required {time.time() - start_time} seconds.") diff --git a/simpa/core/simulation_modules/__init__.py b/simpa/core/simulation_modules/__init__.py index c87e1e26..2da4c6d2 100644 --- a/simpa/core/simulation_modules/__init__.py +++ b/simpa/core/simulation_modules/__init__.py @@ -2,31 +2,4 @@ # SPDX-FileCopyrightText: 2021 Janek Groehl # SPDX-License-Identifier: MIT -from abc import abstractmethod - -from simpa.core import PipelineModule -from simpa.utils import Settings - - -class SimulationModule(PipelineModule): - """ - Defines a simulation module that is a step in the simulation pipeline. - Each simulation module can only be one of Volume Creation, Light Propagation Modeling, Acoustic Wave Propagation Modeling, Image Reconstruction. - """ - - def __init__(self, global_settings: Settings): - """ - :param global_settings: The SIMPA settings dictionary - :type global_settings: Settings - """ - super(SimulationModule, self).__init__(global_settings=global_settings) - self.component_settings = self.load_component_settings() - if self.component_settings is None: - raise ValueError("The component settings should not be None at this point") - - @abstractmethod - def load_component_settings(self) -> Settings: - """ - :return: Loads component settings corresponding to this simulation component - """ - pass +from .simulation_module_base import SimulationModuleBase diff --git a/simpa/core/simulation_modules/acoustic_module/__init__.py b/simpa/core/simulation_modules/acoustic_module/__init__.py new file mode 100644 index 00000000..9ad0c1ea --- /dev/null +++ b/simpa/core/simulation_modules/acoustic_module/__init__.py @@ -0,0 +1,4 @@ +# SPDX-FileCopyrightText: 2021 Division of Intelligent Medical Systems, DKFZ +# SPDX-FileCopyrightText: 2021 Janek Groehl +# SPDX-License-Identifier: MIT +from .acoustic_adapter_base import AcousticAdapterBase diff --git a/simpa/core/simulation_modules/acoustic_forward_module/__init__.py b/simpa/core/simulation_modules/acoustic_module/acoustic_adapter_base.py similarity index 90% rename from simpa/core/simulation_modules/acoustic_forward_module/__init__.py rename to simpa/core/simulation_modules/acoustic_module/acoustic_adapter_base.py index e9f33d10..51d44440 100644 --- a/simpa/core/simulation_modules/acoustic_forward_module/__init__.py +++ b/simpa/core/simulation_modules/acoustic_module/acoustic_adapter_base.py @@ -4,7 +4,7 @@ from abc import abstractmethod import numpy as np -from simpa.core.simulation_modules import SimulationModule +from simpa.core.simulation_modules import SimulationModuleBase from simpa.utils import Tags, Settings from simpa.io_handling.io_hdf5 import save_hdf5 from simpa.utils.dict_path_manager import generate_dict_path @@ -12,7 +12,7 @@ from simpa.utils.quality_assurance.data_sanity_testing import assert_array_well_defined -class AcousticForwardModelBaseAdapter(SimulationModule): +class AcousticAdapterBase(SimulationModuleBase): """ This method is the entry method for running an acoustic forward model. It is invoked in the *simpa.core.simulation.simulate* method, but can also be called @@ -24,7 +24,7 @@ class AcousticForwardModelBaseAdapter(SimulationModule): tag in the settings dictionary. - :param settings: The settings dictionary containing key-value pairs that determine the simulation. + :param global_settings: The settings dictionary containing key-value pairs that determine the simulation. Here, it must contain the Tags.ACOUSTIC_MODEL tag and any tags that might be required by the specific acoustic model. :raises AssertionError: an assertion error is raised if the Tags.ACOUSTIC_MODEL tag is not given or @@ -32,7 +32,7 @@ class AcousticForwardModelBaseAdapter(SimulationModule): """ def __init__(self, global_settings: Settings): - super(AcousticForwardModelBaseAdapter, self).__init__(global_settings=global_settings) + super(AcousticAdapterBase, self).__init__(global_settings=global_settings) def load_component_settings(self) -> Settings: """Implements abstract method to serve acoustic settings as component settings @@ -79,6 +79,6 @@ def run(self, digital_device_twin): acoustic_output_path = generate_dict_path( Tags.DATA_FIELD_TIME_SERIES_DATA, wavelength=self.global_settings[Tags.WAVELENGTH]) - save_hdf5(time_series_data, self.global_settings[Tags.SIMPA_OUTPUT_PATH], acoustic_output_path) + save_hdf5(time_series_data, self.global_settings[Tags.SIMPA_OUTPUT_FILE_PATH], acoustic_output_path) self.logger.info("Simulating the acoustic forward process...[Done]") diff --git a/simpa/core/simulation_modules/acoustic_forward_module/acoustic_forward_model_test_adapter.py b/simpa/core/simulation_modules/acoustic_module/acoustic_test_adapter.py similarity index 75% rename from simpa/core/simulation_modules/acoustic_forward_module/acoustic_forward_model_test_adapter.py rename to simpa/core/simulation_modules/acoustic_module/acoustic_test_adapter.py index 9350da60..9477cb3e 100644 --- a/simpa/core/simulation_modules/acoustic_forward_module/acoustic_forward_model_test_adapter.py +++ b/simpa/core/simulation_modules/acoustic_module/acoustic_test_adapter.py @@ -4,10 +4,10 @@ import numpy as np from simpa.utils import Tags -from simpa.core.simulation_modules.acoustic_forward_module import AcousticForwardModelBaseAdapter +from simpa.core.simulation_modules.acoustic_module import AcousticAdapterBase -class AcousticForwardModelTestAdapter(AcousticForwardModelBaseAdapter): +class AcousticTestAdapter(AcousticAdapterBase): def forward_model(self, device) -> np.ndarray: diff --git a/simpa/core/simulation_modules/acoustic_forward_module/acoustic_forward_module_k_wave_adapter.py b/simpa/core/simulation_modules/acoustic_module/k_wave_adapter.py similarity index 95% rename from simpa/core/simulation_modules/acoustic_forward_module/acoustic_forward_module_k_wave_adapter.py rename to simpa/core/simulation_modules/acoustic_module/k_wave_adapter.py index 7bb4e2b1..2348ec37 100644 --- a/simpa/core/simulation_modules/acoustic_forward_module/acoustic_forward_module_k_wave_adapter.py +++ b/simpa/core/simulation_modules/acoustic_module/k_wave_adapter.py @@ -12,8 +12,8 @@ from simpa.core.device_digital_twins import (CurvedArrayDetectionGeometry, DetectionGeometryBase) -from simpa.core.simulation_modules.acoustic_forward_module import \ - AcousticForwardModelBaseAdapter +from simpa.core.simulation_modules.acoustic_module import \ + AcousticAdapterBase from simpa.io_handling.io_hdf5 import load_data_field, save_hdf5 from simpa.utils import Tags from simpa.utils.matlab import generate_matlab_cmd @@ -23,9 +23,9 @@ from simpa.utils.settings import Settings -class KWaveAdapter(AcousticForwardModelBaseAdapter): +class KWaveAdapter(AcousticAdapterBase): """ - The KwaveAcousticForwardModel adapter enables acoustic simulations to be run with the + The KwaveAdapter enables acoustic simulations to be run with the k-wave MATLAB toolbox. k-Wave is a free toolbox (http://www.k-wave.org/) developed by Bradley Treeby and Ben Cox (University College London) and Jiri Jaros (Brno University of Technology). @@ -83,7 +83,7 @@ def forward_model(self, detection_geometry: DetectionGeometryBase) -> np.ndarray self.logger.debug(f"OPTICAL_PATH: {str(optical_path)}") data_dict = {} - file_path = self.global_settings[Tags.SIMPA_OUTPUT_PATH] + file_path = self.global_settings[Tags.SIMPA_OUTPUT_FILE_PATH] data_dict[Tags.DATA_FIELD_INITIAL_PRESSURE] = load_data_field(file_path, Tags.DATA_FIELD_INITIAL_PRESSURE, wavelength=wavelength) data_dict[Tags.DATA_FIELD_SPEED_OF_SOUND] = load_data_field(file_path, Tags.DATA_FIELD_SPEED_OF_SOUND) @@ -98,8 +98,9 @@ def forward_model(self, detection_geometry: DetectionGeometryBase) -> np.ndarray detectors_are_aligned_along_x_axis = field_of_view_extent[2] == 0 and field_of_view_extent[3] == 0 detectors_are_aligned_along_y_axis = field_of_view_extent[0] == 0 and field_of_view_extent[1] == 0 - if detectors_are_aligned_along_x_axis or detectors_are_aligned_along_y_axis: - axes = (0, 1) + if not (Tags.ACOUSTIC_SIMULATION_3D in self.component_settings + and self.component_settings[Tags.ACOUSTIC_SIMULATION_3D]) and \ + (detectors_are_aligned_along_x_axis or detectors_are_aligned_along_y_axis): if detectors_are_aligned_along_y_axis: transducer_plane = int(round((detector_positions_mm[0, 0] / self.global_settings[Tags.SPACING_MM]))) - 1 image_slice = np.s_[transducer_plane, :, :] @@ -107,7 +108,6 @@ def forward_model(self, detection_geometry: DetectionGeometryBase) -> np.ndarray transducer_plane = int(round((detector_positions_mm[0, 1] / self.global_settings[Tags.SPACING_MM]))) - 1 image_slice = np.s_[:, transducer_plane, :] else: - axes = (0, 2) image_slice = np.s_[:] data_dict[Tags.DATA_FIELD_SPEED_OF_SOUND] = data_dict[Tags.DATA_FIELD_SPEED_OF_SOUND][image_slice].T @@ -121,8 +121,8 @@ def forward_model(self, detection_geometry: DetectionGeometryBase) -> np.ndarray data_dict[Tags.DATA_FIELD_DENSITY], data_dict[Tags.DATA_FIELD_ALPHA_COEFF], data_dict[Tags.DATA_FIELD_INITIAL_PRESSURE], - optical_path=self.global_settings[Tags.SIMPA_OUTPUT_PATH]) - save_hdf5(global_settings, global_settings[Tags.SIMPA_OUTPUT_PATH], "/settings/") + optical_path=self.global_settings[Tags.SIMPA_OUTPUT_FILE_PATH]) + save_hdf5(global_settings, global_settings[Tags.SIMPA_OUTPUT_FILE_PATH], "/settings/") return time_series_data @@ -155,7 +155,8 @@ def k_wave_acoustic_forward_model(self, detection_geometry: DetectionGeometryBas field_of_view = pa_device.get_field_of_view_mm() detector_positions_mm = pa_device.get_detector_element_positions_accounting_for_device_position_mm() - if not self.component_settings.get(Tags.ACOUSTIC_SIMULATION_3D): + if not (Tags.ACOUSTIC_SIMULATION_3D in self.component_settings + and self.component_settings[Tags.ACOUSTIC_SIMULATION_3D]): detectors_are_aligned_along_x_axis = np.abs(field_of_view[2] - field_of_view[3]) < 1e-5 detectors_are_aligned_along_y_axis = np.abs(field_of_view[0] - field_of_view[1]) < 1e-5 if detectors_are_aligned_along_x_axis or detectors_are_aligned_along_y_axis: @@ -238,7 +239,7 @@ def k_wave_acoustic_forward_model(self, detection_geometry: DetectionGeometryBas simulation_script_path = "simulate_2D" matlab_binary_path = self.component_settings[Tags.ACOUSTIC_MODEL_BINARY_PATH] - cmd = generate_matlab_cmd(matlab_binary_path, simulation_script_path, optical_path) + cmd = generate_matlab_cmd(matlab_binary_path, simulation_script_path, optical_path, self.get_additional_flags()) cur_dir = os.getcwd() self.logger.info(cmd) diff --git a/simpa/core/simulation_modules/acoustic_forward_module/simulate_2D.m b/simpa/core/simulation_modules/acoustic_module/simulate_2D.m similarity index 100% rename from simpa/core/simulation_modules/acoustic_forward_module/simulate_2D.m rename to simpa/core/simulation_modules/acoustic_module/simulate_2D.m diff --git a/simpa/core/simulation_modules/acoustic_forward_module/simulate_3D.m b/simpa/core/simulation_modules/acoustic_module/simulate_3D.m similarity index 100% rename from simpa/core/simulation_modules/acoustic_forward_module/simulate_3D.m rename to simpa/core/simulation_modules/acoustic_module/simulate_3D.m diff --git a/simpa/core/simulation_modules/optical_module/__init__.py b/simpa/core/simulation_modules/optical_module/__init__.py new file mode 100644 index 00000000..7666c9bf --- /dev/null +++ b/simpa/core/simulation_modules/optical_module/__init__.py @@ -0,0 +1,4 @@ +# SPDX-FileCopyrightText: 2021 Division of Intelligent Medical Systems, DKFZ +# SPDX-FileCopyrightText: 2021 Janek Groehl +# SPDX-License-Identifier: MIT +from .optical_adapter_base import OpticalAdapterBase diff --git a/simpa/core/simulation_modules/optical_simulation_module/optical_forward_model_mcx_adapter.py b/simpa/core/simulation_modules/optical_module/mcx_adapter.py similarity index 97% rename from simpa/core/simulation_modules/optical_simulation_module/optical_forward_model_mcx_adapter.py rename to simpa/core/simulation_modules/optical_module/mcx_adapter.py index 682f2985..9314aed5 100644 --- a/simpa/core/simulation_modules/optical_simulation_module/optical_forward_model_mcx_adapter.py +++ b/simpa/core/simulation_modules/optical_module/mcx_adapter.py @@ -5,7 +5,7 @@ import numpy as np import subprocess from simpa.utils import Tags, Settings -from simpa.core.simulation_modules.optical_simulation_module import OpticalForwardModuleBase +from simpa.core.simulation_modules.optical_module import OpticalAdapterBase from simpa.core.device_digital_twins.illumination_geometries import IlluminationGeometryBase import json import jdata @@ -13,7 +13,7 @@ from typing import List, Dict, Tuple -class MCXAdapter(OpticalForwardModuleBase): +class MCXAdapter(OpticalAdapterBase): """ This class implements a bridge to the mcx framework to integrate mcx into SIMPA. This adapter only allows for computation of fluence, for computations of diffuse reflectance, take a look at `simpa.ReflectanceMcxAdapter` @@ -69,6 +69,7 @@ def forward_model(self, self.generate_mcx_json_input(settings_dict=settings_dict) # run the simulation cmd = self.get_command() + self.logger.info(cmd) self.run_mcx(cmd) # Read output @@ -155,7 +156,7 @@ def get_mcx_settings(self, settings_dict["Session"]["RNGSeed"] = self.component_settings[Tags.MCX_SEED] return settings_dict - def get_command(self, bc="aaaaaa") -> List: + def get_command(self) -> List: """ generates list of commands to be parse to MCX in a subprocess @@ -171,13 +172,8 @@ def get_command(self, bc="aaaaaa") -> List: cmd.append("-a") cmd.append("1") cmd.append("-F") - cmd.append("bnii") - cmd.append("-Z") - cmd.append("2") - cmd.append("-b") - cmd.append("1") - cmd.append("--bc") - cmd.append(bc) + cmd.append("jnii") + cmd += self.get_additional_flags() return cmd @staticmethod diff --git a/simpa/core/simulation_modules/optical_simulation_module/optical_forward_model_mcx_reflectance_adapter.py b/simpa/core/simulation_modules/optical_module/mcx_reflectance_adapter.py similarity index 98% rename from simpa/core/simulation_modules/optical_simulation_module/optical_forward_model_mcx_reflectance_adapter.py rename to simpa/core/simulation_modules/optical_module/mcx_reflectance_adapter.py index 814d631a..d32d1a9b 100644 --- a/simpa/core/simulation_modules/optical_simulation_module/optical_forward_model_mcx_reflectance_adapter.py +++ b/simpa/core/simulation_modules/optical_module/mcx_reflectance_adapter.py @@ -4,16 +4,17 @@ import typing import numpy as np +import struct import jdata import os from typing import List, Tuple, Dict, Union from simpa.utils import Tags, Settings -from simpa.core.simulation_modules.optical_simulation_module.optical_forward_model_mcx_adapter import MCXAdapter +from simpa.core.simulation_modules.optical_module.mcx_adapter import MCXAdapter from simpa.core.device_digital_twins import IlluminationGeometryBase, PhotoacousticDevice -class MCXAdapterReflectance(MCXAdapter): +class MCXReflectanceAdapter(MCXAdapter): """ This class implements a bridge to the mcx framework to integrate mcx into SIMPA. This class targets specifically diffuse reflectance simulations. Specifically, it implements the capability to run diffuse reflectance simulations. @@ -36,7 +37,7 @@ def __init__(self, global_settings: Settings): :param global_settings: global settings used during simulations """ - super(MCXAdapterReflectance, self).__init__(global_settings=global_settings) + super(MCXReflectanceAdapter, self).__init__(global_settings=global_settings) self.mcx_photon_data_file = None self.padded = None self.volume_boundary_condition_str = global_settings[Tags.VOLUME_BOUNDARY_BONDITION] @@ -77,10 +78,12 @@ def forward_model(self, self.generate_mcx_json_input(settings_dict=settings_dict) # run the simulation cmd = self.get_command() + self.logger.info(cmd) self.run_mcx(cmd) # Read output results = self.read_mcx_output() + struct._clearcache() # clean temporary files self.remove_mcx_output() @@ -123,7 +126,7 @@ def get_command(self) -> typing.List: cmd.append("--bc") # save photon exit position and direction cmd.append(self.volume_boundary_condition_str) cmd.append("--saveref") - + cmd += self.get_additional_flags() return cmd def read_mcx_output(self, **kwargs) -> Dict: diff --git a/simpa/core/simulation_modules/optical_simulation_module/__init__.py b/simpa/core/simulation_modules/optical_module/optical_adapter_base.py similarity index 95% rename from simpa/core/simulation_modules/optical_simulation_module/__init__.py rename to simpa/core/simulation_modules/optical_module/optical_adapter_base.py index 29249b86..13473001 100644 --- a/simpa/core/simulation_modules/optical_simulation_module/__init__.py +++ b/simpa/core/simulation_modules/optical_module/optical_adapter_base.py @@ -6,7 +6,7 @@ import numpy as np -from simpa.core.simulation_modules import SimulationModule +from simpa.core.simulation_modules import SimulationModuleBase from simpa.core.device_digital_twins import (IlluminationGeometryBase, PhotoacousticDevice) from simpa.io_handling.io_hdf5 import load_data_field, save_hdf5 @@ -16,7 +16,7 @@ assert_array_well_defined -class OpticalForwardModuleBase(SimulationModule): +class OpticalAdapterBase(SimulationModuleBase): """ Use this class as a base for implementations of optical forward models. This class has the attributes `self.temporary_output_files` which stores file paths that are temporarily created as @@ -24,7 +24,7 @@ class OpticalForwardModuleBase(SimulationModule): """ def __init__(self, global_settings: Settings): - super(OpticalForwardModuleBase, self).__init__(global_settings=global_settings) + super(OpticalAdapterBase, self).__init__(global_settings=global_settings) self.nx = None self.ny = None self.nz = None @@ -67,7 +67,7 @@ def run(self, device: Union[IlluminationGeometryBase, PhotoacousticDevice]) -> N self.logger.info("Simulating the optical forward process...") - file_path = self.global_settings[Tags.SIMPA_OUTPUT_PATH] + file_path = self.global_settings[Tags.SIMPA_OUTPUT_FILE_PATH] wl = str(self.global_settings[Tags.WAVELENGTH]) absorption = load_data_field(file_path, Tags.DATA_FIELD_ABSORPTION_PER_CM, wl) @@ -116,7 +116,7 @@ def run(self, device: Union[IlluminationGeometryBase, PhotoacousticDevice]) -> N optical_output[k] = {self.global_settings[Tags.WAVELENGTH]: item} optical_output_path = generate_dict_path(Tags.OPTICAL_MODEL_OUTPUT_NAME) - save_hdf5(optical_output, self.global_settings[Tags.SIMPA_OUTPUT_PATH], optical_output_path) + save_hdf5(optical_output, self.global_settings[Tags.SIMPA_OUTPUT_FILE_PATH], optical_output_path) self.logger.info("Simulating the optical forward process...[Done]") def run_forward_model(self, diff --git a/simpa/core/simulation_modules/optical_simulation_module/optical_forward_model_test_adapter.py b/simpa/core/simulation_modules/optical_module/optical_test_adapter.py similarity index 75% rename from simpa/core/simulation_modules/optical_simulation_module/optical_forward_model_test_adapter.py rename to simpa/core/simulation_modules/optical_module/optical_test_adapter.py index 220865b7..10b16678 100644 --- a/simpa/core/simulation_modules/optical_simulation_module/optical_forward_model_test_adapter.py +++ b/simpa/core/simulation_modules/optical_module/optical_test_adapter.py @@ -2,11 +2,11 @@ # SPDX-FileCopyrightText: 2021 Janek Groehl # SPDX-License-Identifier: MIT -from simpa.core.simulation_modules.optical_simulation_module import OpticalForwardModuleBase +from simpa.core.simulation_modules.optical_module import OpticalAdapterBase from simpa import Tags -class OpticalForwardModelTestAdapter(OpticalForwardModuleBase): +class OpticalTestAdapter(OpticalAdapterBase): """ This Adapter was created for testing purposes and only """ diff --git a/simpa/core/simulation_modules/optical_simulation_module/volume_boundary_condition.py b/simpa/core/simulation_modules/optical_module/volume_boundary_condition.py similarity index 100% rename from simpa/core/simulation_modules/optical_simulation_module/volume_boundary_condition.py rename to simpa/core/simulation_modules/optical_module/volume_boundary_condition.py diff --git a/simpa/core/simulation_modules/reconstruction_module/__init__.py b/simpa/core/simulation_modules/reconstruction_module/__init__.py index 05f8b3ef..7c084f66 100644 --- a/simpa/core/simulation_modules/reconstruction_module/__init__.py +++ b/simpa/core/simulation_modules/reconstruction_module/__init__.py @@ -1,98 +1,9 @@ # SPDX-FileCopyrightText: 2021 Division of Intelligent Medical Systems, DKFZ # SPDX-FileCopyrightText: 2021 Janek Groehl # SPDX-License-Identifier: MIT +from .reconstruction_adapter_base import ReconstructionAdapterBase -from simpa.utils import Tags -from simpa.core.device_digital_twins import DetectionGeometryBase -from simpa.core.device_digital_twins import PhotoacousticDevice -from simpa.io_handling.io_hdf5 import load_data_field -from abc import abstractmethod -from simpa.core.simulation_modules import SimulationModule -from simpa.utils.dict_path_manager import generate_dict_path -from simpa.io_handling.io_hdf5 import save_hdf5 -import numpy as np -from simpa.utils import Settings -from simpa.core.simulation_modules.reconstruction_module.reconstruction_utils import bandpass_filter_with_settings, apply_b_mode -from simpa.utils.quality_assurance.data_sanity_testing import assert_array_well_defined - - -class ReconstructionAdapterBase(SimulationModule): - """ - This class is the main entry point to perform image reconstruction using the SIMPA toolkit. - All information necessary for the respective reconstruction method must be contained in the - respective settings dictionary. - """ - - def __init__(self, global_settings: Settings): - super(ReconstructionAdapterBase, self).__init__(global_settings=global_settings) - - def load_component_settings(self) -> Settings: - """Implements abstract method to serve reconstruction settings as component settings - - :return: Settings: reconstruction component settings - """ - return self.global_settings.get_reconstruction_settings() - - @abstractmethod - def reconstruction_algorithm(self, time_series_sensor_data, - detection_geometry: DetectionGeometryBase) -> np.ndarray: - """ - A deriving class needs to implement this method according to its model. - - :param time_series_sensor_data: the time series sensor data - :param detection_geometry: - :return: a reconstructed photoacoustic image - """ - pass - - def run(self, device): - self.logger.info("Performing reconstruction...") - - time_series_sensor_data = load_data_field(self.global_settings[Tags.SIMPA_OUTPUT_PATH], - Tags.DATA_FIELD_TIME_SERIES_DATA, self.global_settings[Tags.WAVELENGTH]) - - _device = None - if isinstance(device, DetectionGeometryBase): - _device = device - elif isinstance(device, PhotoacousticDevice): - _device = device.get_detection_geometry() - else: - raise TypeError(f"Type {type(device)} is not supported for performing image reconstruction.") - - if Tags.RECONSTRUCTION_PERFORM_BANDPASS_FILTERING in self.component_settings and \ - self.component_settings[Tags.RECONSTRUCTION_PERFORM_BANDPASS_FILTERING]: - - time_series_sensor_data = bandpass_filter_with_settings(time_series_sensor_data, - self.global_settings, - self.component_settings, - _device) - - # check for B-mode methods and perform envelope detection on time series data if specified - if Tags.RECONSTRUCTION_BMODE_BEFORE_RECONSTRUCTION in self.component_settings \ - and self.component_settings[Tags.RECONSTRUCTION_BMODE_BEFORE_RECONSTRUCTION] \ - and Tags.RECONSTRUCTION_BMODE_METHOD in self.component_settings: - time_series_sensor_data = apply_b_mode( - time_series_sensor_data, method=self.component_settings[Tags.RECONSTRUCTION_BMODE_METHOD]) - - reconstruction = self.reconstruction_algorithm(time_series_sensor_data, _device) - - # check for B-mode methods and perform envelope detection on time series data if specified - if Tags.RECONSTRUCTION_BMODE_AFTER_RECONSTRUCTION in self.component_settings \ - and self.component_settings[Tags.RECONSTRUCTION_BMODE_AFTER_RECONSTRUCTION] \ - and Tags.RECONSTRUCTION_BMODE_METHOD in self.component_settings: - reconstruction = apply_b_mode( - reconstruction, method=self.component_settings[Tags.RECONSTRUCTION_BMODE_METHOD]) - - if not (Tags.IGNORE_QA_ASSERTIONS in self.global_settings and Tags.IGNORE_QA_ASSERTIONS): - assert_array_well_defined(reconstruction, array_name="reconstruction") - - reconstruction_output_path = generate_dict_path( - Tags.DATA_FIELD_RECONSTRUCTED_DATA, self.global_settings[Tags.WAVELENGTH]) - - save_hdf5(reconstruction, self.global_settings[Tags.SIMPA_OUTPUT_PATH], - reconstruction_output_path) - - self.logger.info("Performing reconstruction...[Done]") +from simpa.utils import Tags, Settings def create_reconstruction_settings(speed_of_sound_in_m_per_s: int = 1540, time_spacing_in_s: float = 2.5e-8, diff --git a/simpa/core/simulation_modules/reconstruction_module/reconstruction_module_delay_and_sum_adapter.py b/simpa/core/simulation_modules/reconstruction_module/delay_and_sum_adapter.py similarity index 98% rename from simpa/core/simulation_modules/reconstruction_module/reconstruction_module_delay_and_sum_adapter.py rename to simpa/core/simulation_modules/reconstruction_module/delay_and_sum_adapter.py index b9f8deba..6e270d66 100644 --- a/simpa/core/simulation_modules/reconstruction_module/reconstruction_module_delay_and_sum_adapter.py +++ b/simpa/core/simulation_modules/reconstruction_module/delay_and_sum_adapter.py @@ -32,7 +32,7 @@ def reconstruction_algorithm(self, time_series_sensor_data, detection_geometry: ### ALGORITHM ITSELF ### xdim, zdim, ydim, xdim_start, xdim_end, ydim_start, ydim_end, zdim_start, zdim_end = compute_image_dimensions( - detection_geometry, spacing_in_mm, self.logger) + detection_geometry.field_of_view_extent_mm, spacing_in_mm, self.logger) if zdim == 1: sensor_positions[:, 1] = 0 # Assume imaging plane diff --git a/simpa/core/simulation_modules/reconstruction_module/reconstruction_module_delay_multiply_and_sum_adapter.py b/simpa/core/simulation_modules/reconstruction_module/delay_multiply_and_sum_adapter.py similarity index 98% rename from simpa/core/simulation_modules/reconstruction_module/reconstruction_module_delay_multiply_and_sum_adapter.py rename to simpa/core/simulation_modules/reconstruction_module/delay_multiply_and_sum_adapter.py index 65178230..e4a8678b 100644 --- a/simpa/core/simulation_modules/reconstruction_module/reconstruction_module_delay_multiply_and_sum_adapter.py +++ b/simpa/core/simulation_modules/reconstruction_module/delay_multiply_and_sum_adapter.py @@ -32,7 +32,7 @@ def reconstruction_algorithm(self, time_series_sensor_data, detection_geometry: ### ALGORITHM ITSELF ### xdim, zdim, ydim, xdim_start, xdim_end, ydim_start, ydim_end, zdim_start, zdim_end = compute_image_dimensions( - detection_geometry, spacing_in_mm, self.logger) + detection_geometry.field_of_view_extent_mm, spacing_in_mm, self.logger) if zdim == 1: sensor_positions[:, 1] = 0 # Assume imaging plane diff --git a/simpa/core/simulation_modules/reconstruction_module/reconstruction_adapter_base.py b/simpa/core/simulation_modules/reconstruction_module/reconstruction_adapter_base.py new file mode 100644 index 00000000..89a483c4 --- /dev/null +++ b/simpa/core/simulation_modules/reconstruction_module/reconstruction_adapter_base.py @@ -0,0 +1,95 @@ +# SPDX-FileCopyrightText: 2021 Division of Intelligent Medical Systems, DKFZ +# SPDX-FileCopyrightText: 2021 Janek Groehl +# SPDX-License-Identifier: MIT + +from simpa.utils import Tags +from simpa.core.device_digital_twins import DetectionGeometryBase +from simpa.core.device_digital_twins import PhotoacousticDevice +from simpa.io_handling.io_hdf5 import load_data_field +from abc import abstractmethod +from simpa.core.simulation_modules import SimulationModuleBase +from simpa.utils.dict_path_manager import generate_dict_path +from simpa.io_handling.io_hdf5 import save_hdf5 +import numpy as np +from simpa.utils import Settings +from simpa.core.simulation_modules.reconstruction_module.reconstruction_utils import bandpass_filter_with_settings, apply_b_mode +from simpa.utils.quality_assurance.data_sanity_testing import assert_array_well_defined + + +class ReconstructionAdapterBase(SimulationModuleBase): + """ + This class is the main entry point to perform image reconstruction using the SIMPA toolkit. + All information necessary for the respective reconstruction method must be contained in the + respective settings dictionary. + """ + + def __init__(self, global_settings: Settings): + super(ReconstructionAdapterBase, self).__init__(global_settings=global_settings) + + def load_component_settings(self) -> Settings: + """Implements abstract method to serve reconstruction settings as component settings + + :return: Settings: reconstruction component settings + """ + return self.global_settings.get_reconstruction_settings() + + @abstractmethod + def reconstruction_algorithm(self, time_series_sensor_data, + detection_geometry: DetectionGeometryBase) -> np.ndarray: + """ + A deriving class needs to implement this method according to its model. + + :param time_series_sensor_data: the time series sensor data + :param detection_geometry: + :return: a reconstructed photoacoustic image + """ + pass + + def run(self, device): + self.logger.info("Performing reconstruction...") + + time_series_sensor_data = load_data_field(self.global_settings[Tags.SIMPA_OUTPUT_FILE_PATH], + Tags.DATA_FIELD_TIME_SERIES_DATA, self.global_settings[Tags.WAVELENGTH]) + + _device = None + if isinstance(device, DetectionGeometryBase): + _device = device + elif isinstance(device, PhotoacousticDevice): + _device = device.get_detection_geometry() + else: + raise TypeError(f"Type {type(device)} is not supported for performing image reconstruction.") + + if Tags.RECONSTRUCTION_PERFORM_BANDPASS_FILTERING in self.component_settings and \ + self.component_settings[Tags.RECONSTRUCTION_PERFORM_BANDPASS_FILTERING]: + + time_series_sensor_data = bandpass_filter_with_settings(time_series_sensor_data, + self.global_settings, + self.component_settings, + _device) + + # check for B-mode methods and perform envelope detection on time series data if specified + if Tags.RECONSTRUCTION_BMODE_BEFORE_RECONSTRUCTION in self.component_settings \ + and self.component_settings[Tags.RECONSTRUCTION_BMODE_BEFORE_RECONSTRUCTION] \ + and Tags.RECONSTRUCTION_BMODE_METHOD in self.component_settings: + time_series_sensor_data = apply_b_mode( + time_series_sensor_data, method=self.component_settings[Tags.RECONSTRUCTION_BMODE_METHOD]) + + reconstruction = self.reconstruction_algorithm(time_series_sensor_data, _device) + + # check for B-mode methods and perform envelope detection on time series data if specified + if Tags.RECONSTRUCTION_BMODE_AFTER_RECONSTRUCTION in self.component_settings \ + and self.component_settings[Tags.RECONSTRUCTION_BMODE_AFTER_RECONSTRUCTION] \ + and Tags.RECONSTRUCTION_BMODE_METHOD in self.component_settings: + reconstruction = apply_b_mode( + reconstruction, method=self.component_settings[Tags.RECONSTRUCTION_BMODE_METHOD]) + + if not (Tags.IGNORE_QA_ASSERTIONS in self.global_settings and Tags.IGNORE_QA_ASSERTIONS): + assert_array_well_defined(reconstruction, array_name="reconstruction") + + reconstruction_output_path = generate_dict_path( + Tags.DATA_FIELD_RECONSTRUCTED_DATA, self.global_settings[Tags.WAVELENGTH]) + + save_hdf5(reconstruction, self.global_settings[Tags.SIMPA_OUTPUT_FILE_PATH], + reconstruction_output_path) + + self.logger.info("Performing reconstruction...[Done]") diff --git a/simpa/core/simulation_modules/reconstruction_module/reconstruction_module_test_adapter.py b/simpa/core/simulation_modules/reconstruction_module/reconstruction_test_adapter.py similarity index 85% rename from simpa/core/simulation_modules/reconstruction_module/reconstruction_module_test_adapter.py rename to simpa/core/simulation_modules/reconstruction_module/reconstruction_test_adapter.py index e3456c4e..4e2a7076 100644 --- a/simpa/core/simulation_modules/reconstruction_module/reconstruction_module_test_adapter.py +++ b/simpa/core/simulation_modules/reconstruction_module/reconstruction_test_adapter.py @@ -5,7 +5,7 @@ from simpa.core.simulation_modules.reconstruction_module import ReconstructionAdapterBase -class ReconstructionModuleTestAdapter(ReconstructionAdapterBase): +class ReconstructionTestAdapter(ReconstructionAdapterBase): def reconstruction_algorithm(self, time_series_sensor_data, detection_geometry): return time_series_sensor_data / 10 + 5 diff --git a/simpa/core/simulation_modules/reconstruction_module/reconstruction_utils.py b/simpa/core/simulation_modules/reconstruction_module/reconstruction_utils.py index 0b33a67a..1cdb1ae4 100644 --- a/simpa/core/simulation_modules/reconstruction_module/reconstruction_utils.py +++ b/simpa/core/simulation_modules/reconstruction_module/reconstruction_utils.py @@ -9,6 +9,7 @@ from simpa.utils.settings import Settings from simpa.io_handling.io_hdf5 import load_data_field from simpa.utils import Tags +from simpa.utils import round_x5_away_from_zero import torch import torch.fft from torch import Tensor @@ -381,7 +382,7 @@ def preparing_reconstruction_and_obtaining_reconstruction_settings( if Tags.DATA_FIELD_SPEED_OF_SOUND in component_settings and component_settings[Tags.DATA_FIELD_SPEED_OF_SOUND]: speed_of_sound_in_m_per_s = component_settings[Tags.DATA_FIELD_SPEED_OF_SOUND] elif Tags.WAVELENGTH in global_settings and global_settings[Tags.WAVELENGTH]: - sound_speed_m = load_data_field(global_settings[Tags.SIMPA_OUTPUT_PATH], Tags.DATA_FIELD_SPEED_OF_SOUND) + sound_speed_m = load_data_field(global_settings[Tags.SIMPA_OUTPUT_FILE_PATH], Tags.DATA_FIELD_SPEED_OF_SOUND) speed_of_sound_in_m_per_s = np.mean(sound_speed_m) else: raise AttributeError("Please specify a value for DATA_FIELD_SPEED_OF_SOUND " @@ -445,14 +446,15 @@ def preparing_reconstruction_and_obtaining_reconstruction_settings( time_spacing_in_ms, torch_device) -def compute_image_dimensions(detection_geometry: DetectionGeometryBase, spacing_in_mm: float, +def compute_image_dimensions(field_of_view_in_mm: np.ndarray, spacing_in_mm: float, logger: Logger) -> Tuple[int, int, int, np.float64, np.float64, np.float64, np.float64, np.float64, np.float64]: """ Computes size of beamformed image from field of view of detection geometry given the spacing. - :param detection_geometry: detection geometry with specified field of view - :type detection_geometry: DetectionGeometryBase + :param field_of_view_in_mm: field of view in mm as list of xdim_start, xdim_end, ydim_start, ydim_end, + zdim_start, zdim_end + :type field_of_view_in_mm: numpy ndarray :param spacing_in_mm: space betwenn pixels in mm :type spacing_in_mm: float :param logger: logger for debugging purposes @@ -463,14 +465,14 @@ def compute_image_dimensions(detection_geometry: DetectionGeometryBase, spacing_ :rtype: Tuple[int, int, int, np.float64, np.float64, np.float64, np.float64, np.float64, np.float64] """ - field_of_view = detection_geometry.field_of_view_extent_mm - logger.debug(f"Field of view: {field_of_view}") + logger.debug(f"Field of view: {field_of_view_in_mm}") def compute_for_one_dimension(start_in_mm: float, end_in_mm: float) -> Tuple[int, np.float64, np.float64]: """ Helper function to compute the image dimensions for a single dimension given a start and end point in mm. - Makes sure that image dimesion is an integer by flooring. - Spaces the pixels symmetrically between start and end. + Makes sure that image dimension is an integer by rounding, thus the resulting dimensions might be slightly + larger or smaller than the given field of view. The maximal deviation amounts to a quarter spacing on each side. + The algorithm spaces the pixels symmetrically between start and end. :param start_in_mm: lower limit of the field of view in this dimension :type start_in_mm: float @@ -483,15 +485,15 @@ def compute_for_one_dimension(start_in_mm: float, end_in_mm: float) -> Tuple[int start_temp = start_in_mm / spacing_in_mm end_temp = end_in_mm / spacing_in_mm dim_temp = np.abs(end_temp - start_temp) - dim = int(np.floor(dim_temp)) - diff = np.abs(dim_temp - dim) + dim = round_x5_away_from_zero(dim_temp) + diff = dim_temp - dim # the sign is important here start = start_temp - np.sign(start_temp) * diff/2 end = end_temp - np.sign(end_temp) * diff/2 return dim, start, end - xdim, xdim_start, xdim_end = compute_for_one_dimension(field_of_view[0], field_of_view[1]) - zdim, zdim_start, zdim_end = compute_for_one_dimension(field_of_view[2], field_of_view[3]) - ydim, ydim_start, ydim_end = compute_for_one_dimension(field_of_view[4], field_of_view[5]) + xdim, xdim_start, xdim_end = compute_for_one_dimension(field_of_view_in_mm[0], field_of_view_in_mm[1]) + zdim, zdim_start, zdim_end = compute_for_one_dimension(field_of_view_in_mm[2], field_of_view_in_mm[3]) + ydim, ydim_start, ydim_end = compute_for_one_dimension(field_of_view_in_mm[4], field_of_view_in_mm[5]) if xdim < 1: xdim = 1 diff --git a/simpa/core/simulation_modules/reconstruction_module/reconstruction_module_signed_delay_multiply_and_sum_adapter.py b/simpa/core/simulation_modules/reconstruction_module/signed_delay_multiply_and_sum_adapter.py similarity index 98% rename from simpa/core/simulation_modules/reconstruction_module/reconstruction_module_signed_delay_multiply_and_sum_adapter.py rename to simpa/core/simulation_modules/reconstruction_module/signed_delay_multiply_and_sum_adapter.py index 28c7d84c..bf735f43 100644 --- a/simpa/core/simulation_modules/reconstruction_module/reconstruction_module_signed_delay_multiply_and_sum_adapter.py +++ b/simpa/core/simulation_modules/reconstruction_module/signed_delay_multiply_and_sum_adapter.py @@ -33,7 +33,7 @@ def reconstruction_algorithm(self, time_series_sensor_data, detection_geometry: ### ALGORITHM ITSELF ### xdim, zdim, ydim, xdim_start, xdim_end, ydim_start, ydim_end, zdim_start, zdim_end = compute_image_dimensions( - detection_geometry, spacing_in_mm, self.logger) + detection_geometry.field_of_view_extent_mm, spacing_in_mm, self.logger) if zdim == 1: sensor_positions[:, 1] = 0 # Assume imaging plane diff --git a/simpa/core/simulation_modules/reconstruction_module/reconstruction_module_time_reversal_adapter.py b/simpa/core/simulation_modules/reconstruction_module/time_reversal_adapter.py similarity index 92% rename from simpa/core/simulation_modules/reconstruction_module/reconstruction_module_time_reversal_adapter.py rename to simpa/core/simulation_modules/reconstruction_module/time_reversal_adapter.py index 6b4bd43f..5c61ff44 100644 --- a/simpa/core/simulation_modules/reconstruction_module/reconstruction_module_time_reversal_adapter.py +++ b/simpa/core/simulation_modules/reconstruction_module/time_reversal_adapter.py @@ -2,7 +2,8 @@ # SPDX-FileCopyrightText: 2021 Janek Groehl # SPDX-License-Identifier: MIT -from simpa.utils import Tags +from simpa.core.simulation_modules.reconstruction_module.reconstruction_utils import compute_image_dimensions +from simpa.utils import Tags, round_x5_away_from_zero from simpa.utils.matlab import generate_matlab_cmd from simpa.utils.settings import Settings from simpa.core.simulation_modules.reconstruction_module import ReconstructionAdapterBase @@ -55,7 +56,8 @@ def get_acoustic_properties(self, input_data: dict, detection_geometry): raise AttributeError("Please specify a value for SPACING_MM") detector_positions = detection_geometry.get_detector_element_positions_accounting_for_device_position_mm() - detector_positions_voxels = np.round(detector_positions / spacing_in_mm).astype(int) + # we add eps of 1e-10 because numpy rounds 0.5 to the next even number + detector_positions_voxels = round_x5_away_from_zero(detector_positions / spacing_in_mm) # plus 2 because of off- volume_x_dim = int(np.ceil(self.global_settings[Tags.DIM_VOLUME_X_MM] / spacing_in_mm) + 1) @@ -127,7 +129,7 @@ def reconstruction_algorithm(self, time_series_sensor_data, detection_geometry): input_data[Tags.DATA_FIELD_TIME_SERIES_DATA] = time_series_sensor_data input_data, spacing_in_mm = self.get_acoustic_properties(input_data, detection_geometry) - acoustic_path = self.global_settings[Tags.SIMPA_OUTPUT_PATH] + ".mat" + acoustic_path = self.global_settings[Tags.SIMPA_OUTPUT_FILE_PATH] + ".mat" possible_k_wave_parameters = [Tags.MODEL_SENSOR_FREQUENCY_RESPONSE, Tags.KWAVE_PROPERTY_ALPHA_POWER, Tags.GPU, Tags.KWAVE_PROPERTY_PMLInside, Tags.KWAVE_PROPERTY_PMLAlpha, Tags.KWAVE_PROPERTY_PlotPML, @@ -169,7 +171,7 @@ def reconstruction_algorithm(self, time_series_sensor_data, detection_geometry): axes = (0, 1) matlab_binary_path = self.component_settings[Tags.ACOUSTIC_MODEL_BINARY_PATH] - cmd = generate_matlab_cmd(matlab_binary_path, time_reversal_script, acoustic_path) + cmd = generate_matlab_cmd(matlab_binary_path, time_reversal_script, acoustic_path, self.get_additional_flags()) cur_dir = os.getcwd() os.chdir(self.global_settings[Tags.SIMULATION_PATH]) @@ -181,7 +183,11 @@ def reconstruction_algorithm(self, time_series_sensor_data, detection_geometry): reconstructed_data = reconstructed_data.T field_of_view_mm = detection_geometry.get_field_of_view_mm() - field_of_view_voxels = (field_of_view_mm / spacing_in_mm).astype(np.int32) + _, _, _, xdim_start, xdim_end, ydim_start, ydim_end, zdim_start, zdim_end = compute_image_dimensions( + field_of_view_mm, spacing_in_mm, self.logger) + field_of_view_voxels = [xdim_start, xdim_end, zdim_start, zdim_end, ydim_start, ydim_end] # change ordering + field_of_view_voxels = [int(dim) for dim in field_of_view_voxels] # cast to int + self.logger.debug(f"FOV (voxels): {field_of_view_voxels}") # In case it should be cropped from A to A, then crop from A to A+1 x_offset_correct = 1 if (field_of_view_voxels[1] - field_of_view_voxels[0]) < 1 else 0 diff --git a/simpa/core/simulation_modules/simulation_module_base.py b/simpa/core/simulation_modules/simulation_module_base.py new file mode 100644 index 00000000..39415038 --- /dev/null +++ b/simpa/core/simulation_modules/simulation_module_base.py @@ -0,0 +1,44 @@ +# 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 import PipelineElementBase +from simpa.utils import Settings, Tags +from typing import List + + +class SimulationModuleBase(PipelineElementBase): + """ + Defines a simulation module that is a step in the simulation pipeline. + Each simulation module can only be one of Volume Creation, Light Propagation Modeling, Acoustic Wave Propagation Modeling, Image Reconstruction. + """ + + def __init__(self, global_settings: Settings): + """ + :param global_settings: The SIMPA settings dictionary + :type global_settings: Settings + """ + super(SimulationModuleBase, self).__init__(global_settings=global_settings) + self.component_settings = self.load_component_settings() + if self.component_settings is None: + raise ValueError("The component settings should not be None at this point") + + @abstractmethod + def load_component_settings(self) -> Settings: + """ + :return: Loads component settings corresponding to this simulation component + """ + pass + + def get_additional_flags(self) -> List[str]: + """Reads the list of additional flags from the corresponding component settings Tags.ADDITIONAL_FLAGS + + :return: List[str]: list of additional flags + """ + cmd = [] + if Tags.ADDITIONAL_FLAGS in self.component_settings: + for flag in self.component_settings[Tags.ADDITIONAL_FLAGS]: + cmd.append(str(flag)) + return cmd diff --git a/simpa/core/simulation_modules/volume_creation_module/__init__.py b/simpa/core/simulation_modules/volume_creation_module/__init__.py index 6b9f0753..c39607ef 100644 --- a/simpa/core/simulation_modules/volume_creation_module/__init__.py +++ b/simpa/core/simulation_modules/volume_creation_module/__init__.py @@ -2,77 +2,4 @@ # SPDX-FileCopyrightText: 2021 Janek Groehl # SPDX-License-Identifier: MIT -from abc import abstractmethod -from simpa.utils.settings import Settings -from simpa.utils import Tags -from simpa.utils.constants import wavelength_independent_properties, property_tags -import torch -from simpa.core.simulation_modules import SimulationModule -from simpa.io_handling import save_data_field -from simpa.utils.quality_assurance.data_sanity_testing import assert_equal_shapes, assert_array_well_defined - - -class VolumeCreatorModuleBase(SimulationModule): - """ - Use this class to define your own volume creation adapter. - - """ - - def __init__(self, global_settings: Settings): - super(VolumeCreatorModuleBase, self).__init__(global_settings=global_settings) - - def load_component_settings(self) -> Settings: - """Implements abstract method to serve volume creation settings as component settings - - :return: Settings: volume creation component settings - """ - return self.global_settings.get_volume_creation_settings() - - def create_empty_volumes(self): - volumes = dict() - voxel_spacing = self.global_settings[Tags.SPACING_MM] - volume_x_dim = int(round(self.global_settings[Tags.DIM_VOLUME_X_MM] / voxel_spacing)) - volume_y_dim = int(round(self.global_settings[Tags.DIM_VOLUME_Y_MM] / voxel_spacing)) - volume_z_dim = int(round(self.global_settings[Tags.DIM_VOLUME_Z_MM] / voxel_spacing)) - sizes = (volume_x_dim, volume_y_dim, volume_z_dim) - - wavelength = self.global_settings[Tags.WAVELENGTH] - first_wavelength = self.global_settings[Tags.WAVELENGTHS][0] - - for key in property_tags: - # Create wavelength-independent properties only in the first wavelength run - if key in wavelength_independent_properties and wavelength != first_wavelength: - continue - volumes[key] = torch.zeros(sizes, dtype=torch.float, device=self.torch_device) - - return volumes, volume_x_dim, volume_y_dim, volume_z_dim - - @abstractmethod - def create_simulation_volume(self) -> dict: - """ - This method creates an in silico representation of a tissue as described in the settings file that is given. - - :return: A dictionary containing optical and acoustic properties as well as other characteristics of the - simulated volume such as oxygenation, and a segmentation mask. All of these are given as 3d numpy arrays. - :rtype: dict - """ - pass - - def run(self, device): - self.logger.info("VOLUME CREATION") - - volumes = self.create_simulation_volume() - # explicitly empty cache to free reserved GPU memory after volume creation - torch.cuda.empty_cache() - - if not (Tags.IGNORE_QA_ASSERTIONS in self.global_settings and Tags.IGNORE_QA_ASSERTIONS): - assert_equal_shapes(list(volumes.values())) - for _volume_name in volumes.keys(): - if _volume_name == Tags.DATA_FIELD_OXYGENATION: - # oxygenation can have NaN by definition - continue - assert_array_well_defined(volumes[_volume_name], array_name=_volume_name) - - for key, value in volumes.items(): - save_data_field(value, self.global_settings[Tags.SIMPA_OUTPUT_PATH], - data_field=key, wavelength=self.global_settings[Tags.WAVELENGTH]) +from .volume_creation_adapter_base import VolumeCreationAdapterBase diff --git a/simpa/core/simulation_modules/volume_creation_module/volume_creation_module_model_based_adapter.py b/simpa/core/simulation_modules/volume_creation_module/model_based_adapter.py similarity index 83% rename from simpa/core/simulation_modules/volume_creation_module/volume_creation_module_model_based_adapter.py rename to simpa/core/simulation_modules/volume_creation_module/model_based_adapter.py index 2379adf3..e4aad5bb 100644 --- a/simpa/core/simulation_modules/volume_creation_module/volume_creation_module_model_based_adapter.py +++ b/simpa/core/simulation_modules/volume_creation_module/model_based_adapter.py @@ -2,7 +2,7 @@ # SPDX-FileCopyrightText: 2021 Janek Groehl # SPDX-License-Identifier: MIT -from simpa.core.simulation_modules.volume_creation_module import VolumeCreatorModuleBase +from simpa.core.simulation_modules.volume_creation_module import VolumeCreationAdapterBase from simpa.utils.libraries.structure_library import priority_sorted_structures from simpa.utils import Tags import numpy as np @@ -10,7 +10,7 @@ import torch -class ModelBasedVolumeCreationAdapter(VolumeCreatorModuleBase): +class ModelBasedAdapter(VolumeCreationAdapterBase): """ The model-based volume creator uses a set of rules how to generate structures to create a simulation volume. @@ -68,7 +68,7 @@ def create_simulation_volume(self) -> dict: for structure in priority_sorted_structures(self.global_settings, self.component_settings): self.logger.debug(type(structure)) - structure_properties = structure.properties_for_wavelength(wavelength) + structure_properties = structure.properties_for_wavelength(self.global_settings, wavelength) structure_volume_fractions = torch.as_tensor( structure.geometrical_volume, dtype=torch.float, device=self.torch_device) @@ -95,10 +95,21 @@ def create_simulation_volume(self) -> dict: max_added_fractions[added_fraction_greater_than_any_added_fraction & mask] = \ added_volume_fraction[added_fraction_greater_than_any_added_fraction & mask] else: - volumes[key][mask] += added_volume_fraction[mask] * structure_properties[key] + if isinstance(structure_properties[key], torch.Tensor): + volumes[key][mask] += added_volume_fraction[mask] * \ + structure_properties[key].to(self.torch_device)[mask] + elif isinstance(structure_properties[key], (float, np.float64, int, np.int64)): + volumes[key][mask] += added_volume_fraction[mask] * structure_properties[key] + else: + raise ValueError(f"Unsupported type of structure property. " + f"Was {type(structure_properties[key])}.") global_volume_fractions[mask] += added_volume_fraction[mask] + if (torch.abs(global_volume_fractions[global_volume_fractions > 1]) < 1e-5).any(): + raise AssertionError("Invalid Molecular composition! The volume fractions of all molecules must be" + "exactly 100%!") + # convert volumes back to CPU for key in volumes.keys(): volumes[key] = volumes[key].cpu().numpy().astype(np.float64, copy=False) diff --git a/simpa/core/simulation_modules/volume_creation_module/volume_creation_module_segmentation_based_adapter.py b/simpa/core/simulation_modules/volume_creation_module/segmentation_based_adapter.py similarity index 59% rename from simpa/core/simulation_modules/volume_creation_module/volume_creation_module_segmentation_based_adapter.py rename to simpa/core/simulation_modules/volume_creation_module/segmentation_based_adapter.py index 490ce2eb..bf2358d8 100644 --- a/simpa/core/simulation_modules/volume_creation_module/volume_creation_module_segmentation_based_adapter.py +++ b/simpa/core/simulation_modules/volume_creation_module/segmentation_based_adapter.py @@ -2,17 +2,15 @@ # SPDX-FileCopyrightText: 2021 Janek Groehl # SPDX-License-Identifier: MIT -from simpa.core.simulation_modules.volume_creation_module import VolumeCreatorModuleBase +from simpa.core.simulation_modules.volume_creation_module import VolumeCreationAdapterBase from simpa.utils import Tags -from simpa.utils.constants import property_tags -from simpa.io_handling import save_hdf5 import numpy as np import torch -class SegmentationBasedVolumeCreationAdapter(VolumeCreatorModuleBase): +class SegmentationBasedAdapter(VolumeCreationAdapterBase): """ - This volume creator expects a np.ndarray to be in the settigs + This volume creator expects a np.ndarray to be in the settings under the Tags.INPUT_SEGMENTATION_VOLUME tag and uses this array together with a SegmentationClass mapping which is a dict defined in the settings under Tags.SEGMENTATION_CLASS_MAPPING. @@ -23,6 +21,8 @@ class SegmentationBasedVolumeCreationAdapter(VolumeCreatorModuleBase): def create_simulation_volume(self) -> dict: volumes, x_dim_px, y_dim_px, z_dim_px = self.create_empty_volumes() wavelength = self.global_settings[Tags.WAVELENGTH] + for key in volumes.keys(): + volumes[key] = volumes[key].to('cpu') segmentation_volume = self.component_settings[Tags.INPUT_SEGMENTATION_VOLUME] segmentation_classes = np.unique(segmentation_volume, return_counts=False) @@ -41,17 +41,22 @@ def create_simulation_volume(self) -> dict: class_mapping = self.component_settings[Tags.SEGMENTATION_CLASS_MAPPING] for seg_class in segmentation_classes: - class_properties = class_mapping[seg_class].get_properties_for_wavelength(wavelength) - for prop_tag in property_tags: - assigned_prop = class_properties[prop_tag] - if assigned_prop is None: - assigned_prop = torch.nan - volumes[prop_tag][segmentation_volume == seg_class] = assigned_prop - - save_hdf5(self.global_settings, self.global_settings[Tags.SIMPA_OUTPUT_PATH], "/settings/") + class_properties = class_mapping[seg_class].get_properties_for_wavelength(self.global_settings, wavelength) + for volume_key in volumes.keys(): + if isinstance(class_properties[volume_key], (int, float)) or class_properties[volume_key] == None: # scalar + assigned_prop = class_properties[volume_key] + if assigned_prop is None: + assigned_prop = torch.nan + volumes[volume_key][segmentation_volume == seg_class] = assigned_prop + elif len(torch.Tensor.size(class_properties[volume_key])) == 3: # 3D map + assigned_prop = class_properties[volume_key][torch.tensor(segmentation_volume == seg_class)] + assigned_prop[assigned_prop is None] = torch.nan + volumes[volume_key][torch.tensor(segmentation_volume == seg_class)] = assigned_prop + else: + raise AssertionError("Properties need to either be a scalar or a 3D map.") # convert volumes back to CPU for key in volumes.keys(): - volumes[key] = volumes[key].cpu().numpy().astype(np.float64, copy=False) + volumes[key] = volumes[key].numpy().astype(np.float64, copy=False) return volumes diff --git a/simpa/core/simulation_modules/volume_creation_module/volume_creation_adapter_base.py b/simpa/core/simulation_modules/volume_creation_module/volume_creation_adapter_base.py new file mode 100644 index 00000000..9f435601 --- /dev/null +++ b/simpa/core/simulation_modules/volume_creation_module/volume_creation_adapter_base.py @@ -0,0 +1,78 @@ +# SPDX-FileCopyrightText: 2021 Division of Intelligent Medical Systems, DKFZ +# SPDX-FileCopyrightText: 2021 Janek Groehl +# SPDX-License-Identifier: MIT + +from abc import abstractmethod +from simpa.utils.settings import Settings +from simpa.utils import Tags +from simpa.utils.constants import wavelength_independent_properties, property_tags +import torch +from simpa.core.simulation_modules import SimulationModuleBase +from simpa.io_handling import save_data_field +from simpa.utils.quality_assurance.data_sanity_testing import assert_equal_shapes, assert_array_well_defined + + +class VolumeCreationAdapterBase(SimulationModuleBase): + """ + Use this class to define your own volume creation adapter. + + """ + + def __init__(self, global_settings: Settings): + super(VolumeCreationAdapterBase, self).__init__(global_settings=global_settings) + + def load_component_settings(self) -> Settings: + """Implements abstract method to serve volume creation settings as component settings + + :return: Settings: volume creation component settings + """ + return self.global_settings.get_volume_creation_settings() + + def create_empty_volumes(self): + volumes = dict() + voxel_spacing = self.global_settings[Tags.SPACING_MM] + volume_x_dim = int(round(self.global_settings[Tags.DIM_VOLUME_X_MM] / voxel_spacing)) + volume_y_dim = int(round(self.global_settings[Tags.DIM_VOLUME_Y_MM] / voxel_spacing)) + volume_z_dim = int(round(self.global_settings[Tags.DIM_VOLUME_Z_MM] / voxel_spacing)) + sizes = (volume_x_dim, volume_y_dim, volume_z_dim) + + wavelength = self.global_settings[Tags.WAVELENGTH] + first_wavelength = self.global_settings[Tags.WAVELENGTHS][0] + + for key in property_tags: + # Create wavelength-independent properties only in the first wavelength run + if key in wavelength_independent_properties and wavelength != first_wavelength: + continue + volumes[key] = torch.zeros(sizes, dtype=torch.float, device=self.torch_device) + + return volumes, volume_x_dim, volume_y_dim, volume_z_dim + + @abstractmethod + def create_simulation_volume(self) -> dict: + """ + This method creates an in silico representation of a tissue as described in the settings file that is given. + + :return: A dictionary containing optical and acoustic properties as well as other characteristics of the + simulated volume such as oxygenation, and a segmentation mask. All of these are given as 3d numpy arrays. + :rtype: dict + """ + pass + + def run(self, device): + self.logger.info("VOLUME CREATION") + + volumes = self.create_simulation_volume() + # explicitly empty cache to free reserved GPU memory after volume creation + torch.cuda.empty_cache() + + if not (Tags.IGNORE_QA_ASSERTIONS in self.global_settings and Tags.IGNORE_QA_ASSERTIONS): + assert_equal_shapes(list(volumes.values())) + for _volume_name in volumes.keys(): + if _volume_name == Tags.DATA_FIELD_OXYGENATION: + # oxygenation can have NaN by definition + continue + assert_array_well_defined(volumes[_volume_name], array_name=_volume_name) + + for key, value in volumes.items(): + save_data_field(value, self.global_settings[Tags.SIMPA_OUTPUT_FILE_PATH], + data_field=key, wavelength=self.global_settings[Tags.WAVELENGTH]) diff --git a/simpa/io_handling/io_hdf5.py b/simpa/io_handling/io_hdf5.py index 2464827b..eb10ae97 100644 --- a/simpa/io_handling/io_hdf5.py +++ b/simpa/io_handling/io_hdf5.py @@ -123,6 +123,8 @@ def data_grabber(file, path): """ if isinstance(h5file[path], h5py._hl.dataset.Dataset): + if isinstance(h5file[path][()], bytes): + return h5file[path][()].decode("utf-8") return h5file[path][()] dictionary = {} diff --git a/simpa/utils/__init__.py b/simpa/utils/__init__.py index 73bdd1e9..c8484709 100644 --- a/simpa/utils/__init__.py +++ b/simpa/utils/__init__.py @@ -4,6 +4,8 @@ # First load everything without internal dependencies from .tags import Tags +from .settings import Settings + from .libraries.literature_values import MorphologicalTissueProperties from .libraries.literature_values import StandardProperties from .libraries.literature_values import OpticalTissueProperties @@ -29,12 +31,11 @@ from .calculate import calculate_oxygenation from .calculate import calculate_gruneisen_parameter_from_temperature from .calculate import randomize_uniform +from .calculate import round_x5_away_from_zero from .deformation_manager import create_deformation_settings from .deformation_manager import get_functional_from_deformation_settings -from .settings import Settings - from .dict_path_manager import generate_dict_path from .dict_path_manager import get_data_field_from_simpa_output @@ -56,5 +57,11 @@ from .libraries.structure_library.SphericalStructure import SphericalStructure, define_spherical_structure_settings from .libraries.structure_library.VesselStructure import VesselStructure, define_vessel_structure_settings +# Heterogeneity + +from .libraries.heterogeneity_generator import RandomHeterogeneity +from .libraries.heterogeneity_generator import BlobHeterogeneity +from .libraries.heterogeneity_generator import ImageHeterogeneity + if __name__ == "__main__": view_saved_spectra() diff --git a/simpa/utils/calculate.py b/simpa/utils/calculate.py index 8f4aee4f..b0b13840 100644 --- a/simpa/utils/calculate.py +++ b/simpa/utils/calculate.py @@ -3,43 +3,68 @@ # SPDX-License-Identifier: MIT -from typing import Union +from typing import Union, List, Dict, Optional, Sized import numpy as np import torch from scipy.interpolate import interp1d -def calculate_oxygenation(molecule_list: list) -> Union[float, int, torch.Tensor]: +def extract_hemoglobin_fractions(molecule_list: List) -> Dict[str, float]: """ - Calculate the oxygenation level based on the volume fractions of deoxyhaemoglobin and oxyhaemoglobin. + Extract hemoglobin volume fractions from a list of molecules. - This function takes a list of molecules and returns an oxygenation value between 0 and 1 if computable, - otherwise returns None. + :param molecule_list: List of molecules with their spectrum information and volume fractions. + :return: A dictionary with hemoglobin types as keys and their volume fractions as values. + """ + + # Put 0.0 as default value for both hemoglobin types in case they are not present in the molecule list. + hemoglobin = { + "Deoxyhemoglobin": 0.0, + "Oxyhemoglobin": 0.0 + } + + for molecule in molecule_list: + spectrum_name = molecule.absorption_spectrum.spectrum_name + if spectrum_name in hemoglobin: + hemoglobin[spectrum_name] = molecule.volume_fraction + + return hemoglobin + + +def calculate_oxygenation(molecule_list: List) -> Optional[float]: + """ + Calculate the oxygenation level based on the volume fractions of deoxyhemoglobin and oxyhemoglobin. :param molecule_list: List of molecules with their spectrum information and volume fractions. :return: An oxygenation value between 0 and 1 if possible, or None if not computable. """ - hb = None # Volume fraction of deoxyhaemoglobin - hbO2 = None # Volume fraction of oxyhaemoglobin + hemoglobin = extract_hemoglobin_fractions(molecule_list) + hb, hbO2 = hemoglobin["Deoxyhemoglobin"], hemoglobin["Oxyhemoglobin"] - for molecule in molecule_list: - if molecule.absorption_spectrum.spectrum_name == "Deoxyhemoglobin": - hb = molecule.volume_fraction - if molecule.absorption_spectrum.spectrum_name == "Oxyhemoglobin": - hbO2 = molecule.volume_fraction + total = hb + hbO2 + + # Avoid division by zero. If none of the hemoglobin types are present, the oxygenation level is not computable. + if isinstance(hb, torch.Tensor) or isinstance(hbO2, torch.Tensor): + return torch.where(total < 1e-10, 0, hbO2 / total) - if hb is None and hbO2 is None: - return None + else: + if total < 1e-10: + return None + else: + return hbO2 / total - if hb is None: - hb = 0 - elif hbO2 is None: - hbO2 = 0 - if hb + hbO2 < 1e-10: # negative values are not allowed and division by (approx) zero - return None # will lead to negative side effects. +def calculate_bvf(molecule_list: List) -> Union[float, int]: + """ + Calculate the blood volume fraction based on the volume fractions of deoxyhemoglobin and oxyhemoglobin. - return hbO2 / (hb + hbO2) + :param molecule_list: List of molecules with their spectrum information and volume fractions. + :return: The blood volume fraction value between 0 and 1, or 0, if oxy and deoxy not present. + """ + hemoglobin = extract_hemoglobin_fractions(molecule_list) + hb, hbO2 = hemoglobin["Deoxyhemoglobin"], hemoglobin["Oxyhemoglobin"] + # We can use the sum of hb and hb02 to compute blood volume fraction as the volume fraction of all molecules is 1. + return hb + hbO2 def create_spline_for_range(xmin_mm: Union[float, int] = 0, xmax_mm: Union[float, int] = 10, @@ -110,7 +135,7 @@ def spline_evaluator2d_voxel(x: int, y: int, spline: Union[list, np.ndarray], of :return: True if the point (x, y) lies within the range around the spline, False otherwise. """ elevation = spline[x] - y_value = np.round(elevation + offset_voxel) + y_value = round_x5_away_from_zero(elevation + offset_voxel) if y_value <= y < thickness_voxel + y_value: return True else: @@ -267,3 +292,26 @@ def are_equal(obj1: Union[list, tuple, np.ndarray, object], obj2: Union[list, tu # For other types, use standard equality check which also works for lists else: return obj1 == obj2 + + +def round_x5_away_from_zero(x: Union[float, np.ndarray]) -> Union[int, np.ndarray]: + """ + Round a number away from zero. The np.round function rounds x.5 to the nearest even number, which is not always the + desired behavior. This function always rounds x.5 away from zero. For example, x.5 will be rounded to 1, and -x.5 + will be rounded to -1. All other numbers are rounded to the nearest integer. + :param x: input number or array of numbers + :return: rounded number or array of numbers + :rtype: int or np.ndarray of int + """ + + def round_single_value(value): + # If the value is positive, add 0.5 and use floor to round away from zero + # If the value is negative, subtract 0.5 and use ceil to round away from zero + return int(np.floor(value + 0.5)) if value > 0 else int(np.ceil(value - 0.5)) + + if isinstance(x, (np.ndarray, list, tuple)): + # Apply rounding function to each element in the array + return np.array([round_x5_away_from_zero(val) for val in x], dtype=int) + else: + # Apply rounding to a single value + return round_single_value(x) diff --git a/simpa/utils/constants.py b/simpa/utils/constants.py index 9647f116..ac073b25 100644 --- a/simpa/utils/constants.py +++ b/simpa/utils/constants.py @@ -42,6 +42,7 @@ class SegmentationClasses: Tags.DATA_FIELD_GRUNEISEN_PARAMETER, Tags.DATA_FIELD_SEGMENTATION, Tags.DATA_FIELD_OXYGENATION, + Tags.DATA_FIELD_BLOOD_VOLUME_FRACTION, Tags.DATA_FIELD_DENSITY, Tags.DATA_FIELD_SPEED_OF_SOUND, Tags.DATA_FIELD_ALPHA_COEFF diff --git a/simpa/utils/dict_path_manager.py b/simpa/utils/dict_path_manager.py index f8c64705..f83e6dcf 100644 --- a/simpa/utils/dict_path_manager.py +++ b/simpa/utils/dict_path_manager.py @@ -16,7 +16,7 @@ def generate_dict_path(data_field, wavelength: (int, float) = None) -> str: :return: String which defines the path to the data_field. """ - if data_field in [Tags.SIMULATIONS, Tags.SETTINGS, Tags.DIGITAL_DEVICE, Tags.SIMULATION_PIPELINE]: + if data_field in [Tags.SIMPA_VERSION, Tags.SIMULATIONS, Tags.SETTINGS, Tags.DIGITAL_DEVICE, Tags.SIMULATION_PIPELINE]: return "/" + data_field + "/" all_wl_independent_properties = wavelength_independent_properties + toolkit_tags diff --git a/simpa/utils/libraries/heterogeneity_generator.py b/simpa/utils/libraries/heterogeneity_generator.py new file mode 100644 index 00000000..b9f8cea2 --- /dev/null +++ b/simpa/utils/libraries/heterogeneity_generator.py @@ -0,0 +1,327 @@ +# SPDX-FileCopyrightText: 2021 Division of Intelligent Medical Systems, DKFZ +# SPDX-FileCopyrightText: 2021 Janek Groehl +# SPDX-License-Identifier: MIT + +import numpy as np +from sklearn.datasets import make_blobs +from scipy.ndimage.filters import gaussian_filter +from skimage import transform +from simpa.utils import Tags, round_x5_away_from_zero +from typing import Union, Optional +from simpa.log import Logger + + +class HeterogeneityGeneratorBase(object): + """ + This is the base class to define heterogeneous structure maps. + """ + + def __init__(self, xdim, ydim, zdim, spacing_mm, target_mean=None, + target_std=None, target_min=None, target_max=None, + eps=1e-5): + """ + :param xdim: the x dimension of the volume in voxels + :param ydim: the y dimension of the volume in voxels + :param zdim: the z dimension of the volume in voxels + :param spacing_mm: the spacing of the volume in mm + :param target_mean: (optional) the mean of the created heterogeneity map + :param target_std: (optional) the standard deviation of the created heterogeneity map + :param target_min: (optional) the minimum of the created heterogeneity map + :param target_max: (optional) the maximum of the created heterogeneity map + :param eps: (optional) the threshold when a re-normalisation should be triggered (default: 1e-5) + """ + self._xdim = xdim + self._ydim = ydim + self._zdim = zdim + self._spacing_mm = spacing_mm + self._mean = target_mean + self._std = target_std + self._min = target_min + self._max = target_max + self.eps = eps + + self.map = np.ones((self._xdim, self._ydim, self._zdim), dtype=float) + + def get_map(self): + self.normalise_map() + return self.map.astype(float) + + def normalise_map(self): + """ + If mean and std are set, then the data will be normalised to have the desired mean and the + desired standard deviation. + If min and max are set, then the data will be normalised to have the desired minimum and the + desired maximum value. + If all four values are set, then the data will be normalised to have the desired mean and the + desired standard deviation first. afterwards all values smaller than min will be ste to min and + all values larger than max will be set to max. + """ + # Testing mean mean/std normalisation needs to be done + if self._mean is not None and self._std is not None: + if (np.abs(np.mean(self.map) - self._mean) > self.eps or + np.abs(np.std(self.map) - self._std) > self.eps): + mean = np.mean(self.map) + std = np.std(self.map) + self.map = (self.map - mean) / std + self.map = (self.map * self._std) + self._mean + if self._min is not None and self._max is not None: + self.map[self.map < self._min] = self._min + self.map[self.map > self._max] = self._max + + # Testing if min max normalisation needs to be done + if self._min is None or self._max is None: + return + + if (np.abs(np.min(self.map) - self._min) < self.eps and + np.abs(np.max(self.map) - self._max) < self.eps): + return + + _min = np.min(self.map) + _max = np.max(self.map) + self.map = (self.map - _min) / (_max-_min) + self.map = (self.map * (self._max - self._min)) + self._min + + +class RandomHeterogeneity(HeterogeneityGeneratorBase): + """ + This heterogeneity generator represents a uniform random sampling between the given bounds. + Optionally, a Gaussian blur can be specified. Please not that a Gaussian blur will transform the random + distribution to a Gaussian. + """ + + def __init__(self, xdim, ydim, zdim, spacing_mm, gaussian_blur_size_mm=None, target_mean=None, target_std=None, + target_min=None, target_max=None, eps=1e-5): + """ + :param xdim: the x dimension of the volume in voxels + :param ydim: the y dimension of the volume in voxels + :param zdim: the z dimension of the volume in voxels + :param spacing_mm: the spacing of the volume in mm + :param gaussian_blur_size_mm: the size of the standard deviation for the Gaussian blur + :param target_mean: (optional) the mean of the created heterogeneity map + :param target_std: (optional) the standard deviation of the created heterogeneity map + :param target_min: (optional) the minimum of the created heterogeneity map + :param target_max: (optional) the maximum of the created heterogeneity map + :param eps: (optional) the threshold when a re-normalisation should be triggered (default: 1e-5) + """ + super().__init__(xdim, ydim, zdim, spacing_mm, target_mean, target_std, target_min, target_max, eps) + + self.map = np.random.random((xdim, ydim, zdim)) + if gaussian_blur_size_mm is not None: + _gaussian_blur_size_voxels = gaussian_blur_size_mm / spacing_mm + self.map = gaussian_filter(self.map, _gaussian_blur_size_voxels) + + +class BlobHeterogeneity(HeterogeneityGeneratorBase): + """ + This heterogeneity generator representes a blob-like random sampling between the given bounds using the + sklearn.datasets.make_blobs method. Please look into their documentation for optimising the given hyperparameters. + + """ + + def __init__(self, xdim, ydim, zdim, spacing_mm, num_centers=None, cluster_std=None, target_mean=None, + target_std=None, target_min=None, target_max=None, random_state=None): + """ + :param xdim: the x dimension of the volume in voxels + :param ydim: the y dimension of the volume in voxels + :param zdim: the z dimension of the volume in voxels + :param spacing_mm: the spacing of the volume in mm + :param num_centers: the number of blobs + :param cluster_std: the size of the blobs + :param target_mean: (optional) the mean of the created heterogeneity map + :param target_std: (optional) the standard deviation of the created heterogeneity map + :param target_min: (optional) the minimum of the created heterogeneity map + :param target_max: (optional) the maximum of the created heterogeneity map + """ + super().__init__(xdim, ydim, zdim, spacing_mm, target_mean, target_std, target_min, target_max) + + if num_centers is None: + num_centers = round_x5_away_from_zero(np.float_power((xdim * ydim * zdim) * spacing_mm, 1 / 3)) + + if cluster_std is None: + cluster_std = 1 + x, y = make_blobs(n_samples=(xdim * ydim * zdim) * 10, n_features=3, centers=num_centers, + random_state=random_state, cluster_std=cluster_std) + + self.map = np.histogramdd(x, bins=(xdim, ydim, zdim), range=((np.percentile(x[:, 0], 5), + np.percentile(x[:, 0], 95)), + (np.percentile(x[:, 1], 5), + np.percentile(x[:, 1], 95)), + (np.percentile(x[:, 2], 5), + np.percentile(x[:, 2], 95))))[0] + self.map = gaussian_filter(self.map, 5) + + +class ImageHeterogeneity(HeterogeneityGeneratorBase): + """ + This heterogeneity generator takes a pre-specified 2D image, currently only supporting numpy arrays, and uses them + as a map for heterogeneity within the tissue. + + Attributes: + map: the np array of the heterogeneity map transformed and augments to fill the area + """ + + def __init__(self, xdim: int, ydim: int, zdim: int, heterogeneity_image: np.ndarray, + spacing_mm: Union[int, float] = None, image_pixel_spacing_mm: Union[int, float] = None, + scaling_type: [None, str] = None, constant: Union[int, float] = 0, + crop_placement=('centre', 'centre'), target_mean: Union[int, float] = None, + target_std: Union[int, float] = None, target_min: Union[int, float] = None, + target_max: Union[int, float] = None): + """ + :param xdim: the x dimension of the volume in voxels + :param ydim: the y dimension of the volume in voxels + :param zdim: the z dimension of the volume in voxels + :param heterogeneity_image: the 2D prior image of the heterogeneity map + :param spacing_mm: the spacing of the volume in mm + :param image_pixel_spacing_mm: the pixel spacing of the image in mm (pixel spacing) + :param scaling_type: the scaling type of the heterogeneity map, with default being that no scaling occurs + OPTIONS: + TAGS.IMAGE_SCALING_SYMMETRIC: symmetric reflections of the image to span the area + TAGS.IMAGE_SCALING_STRETCH: stretch the image to span the area + TAGS.IMAGE_SCALING_WRAP: multiply the image to span the area + TAGS.IMAGE_SCALING_EDGE: continue the values at the edge of the area to fill the shape + TAGS.IMAGE_SCALING_CONSTANT: span the left-over area with a constant + :param constant: the scaling constant of the heterogeneity map, used only for scaling type 'constant' + WARNING: scaling constant must be in reference to the values in the heterogeneity_image + :param crop_placement: the placement of where the heterogeneity map is cropped + :param target_mean: (optional) the mean of the created heterogeneity map + :param target_std: (optional) the standard deviation of the created heterogeneity map + :param target_min: (optional) the minimum of the created heterogeneity map + :param target_max: (optional) the maximum of the created heterogeneity map + """ + super().__init__(xdim, ydim, zdim, spacing_mm, target_mean, target_std, target_min, target_max) + self.logger = Logger() + + if image_pixel_spacing_mm is None: + image_pixel_spacing_mm = spacing_mm + + (image_width_pixels, image_height_pixels) = heterogeneity_image.shape + [image_width_mm, image_height_mm] = np.array([image_width_pixels, image_height_pixels]) * image_pixel_spacing_mm + (xdim_mm, ydim_mm, zdim_mm) = np.array([xdim, ydim, zdim]) * spacing_mm + + wider = image_width_mm > xdim_mm + taller = image_height_mm > zdim_mm + + if taller or wider: + pixel_scaled_image = self.change_resolution(heterogeneity_image, spacing_mm=spacing_mm, + image_pixel_spacing_mm=image_pixel_spacing_mm) + cropped_image = self.crop_image(xdim, zdim, pixel_scaled_image, crop_placement) + + if taller and wider: + area_fill_image = cropped_image + else: + area_fill_image = self.upsize_to_fill_area(xdim, zdim, cropped_image, scaling_type, constant) + + else: + pixel_scaled_image = self.change_resolution(heterogeneity_image, spacing_mm=spacing_mm, + image_pixel_spacing_mm=image_pixel_spacing_mm) + area_fill_image = self.upsize_to_fill_area(xdim, zdim, pixel_scaled_image, scaling_type, constant) + + if scaling_type == Tags.IMAGE_SCALING_STRETCH: + area_fill_image = transform.resize(heterogeneity_image, output_shape=(xdim, zdim), mode='symmetric') + + self.map = np.repeat(area_fill_image[:, np.newaxis, :], ydim, axis=1) + + def upsize_to_fill_area(self, xdim: int, zdim: int, image: np.ndarray, scaling_type: Optional[str] = None, + constant: Union[int, float] = 0) -> np.ndarray: + """ + Fills an area with an image through various methods of expansion + :param xdim: the x dimension of the area to be filled in voxels + :param zdim: the z dimension of the area to be filled in voxels + :param image: the input image + :param scaling_type: the scaling type of the heterogeneity map, with default being that no scaling occurs + OPTIONS: + TAGS.IMAGE_SCALING_SYMMETRIC: symmetric reflections of the image to span the area + TAGS.IMAGE_SCALING_STRETCH: stretch the image to span the area + TAGS.IMAGE_SCALING_WRAP: multiply the image to span the area + TAGS.IMAGE_SCALING_EDGE: continue the values at the edge of the area to fill the shape + TAGS.IMAGE_SCALING_CONSTANT: span the left-over area with a constant + :param constant: the scaling constant of the heterogeneity map, used only for scaling type 'constant' + :return:A numpy array of size (xdim, zdim) containing the filled image expanded to fill the shape + """ + if scaling_type is None or scaling_type == Tags.IMAGE_SCALING_STRETCH: + scaled_image = image + elif scaling_type == Tags.IMAGE_SCALING_CONSTANT: + pad_left = int((xdim - len(image)) / 2) + pad_height = int(zdim - len(image[0])) + pad_right = xdim - pad_left - len(image) + scaled_image = np.pad(image, ((pad_left, pad_right), (0, pad_height)), + mode=scaling_type, constant_values=constant) + else: + pad_left = int((xdim - len(image)) / 2) + pad_height = int(zdim - len(image[0])) + pad_right = xdim - pad_left - len(image) + scaled_image = np.pad(image, ((pad_left, pad_right), (0, pad_height)), + mode=scaling_type) + + self.logger.warning("The input image has filled the area by using {} scaling type".format(scaling_type)) + return scaled_image + + def crop_image(self, xdim: int, zdim: int, image: np.ndarray, + crop_placement: Union[str, tuple] = Tags.CROP_POSITION_CENTRE) -> np.ndarray: + """ + Crop the image to fit specified dimensions xdim and zdim + :param xdim: the x dimension of the area to be filled in voxels + :param zdim: the z dimension of the area to be filled in voxels + :param image: the input image + :param crop_placement: the placement of where the heterogeneity map is cropped + OPTIONS: TAGS.CROP_PLACEMENT_[TOP,BOTTOM,LEFT,RIGHT,CENTRE,RANDOM] or position of left hand corner on image + :return: cropped image + """ + (image_width_pixels, image_height_pixels) = image.shape + crop_width = min(xdim, image_width_pixels) + crop_height = min(zdim, image_height_pixels) + + if isinstance(crop_placement, tuple): + if crop_placement[0] == Tags.CROP_POSITION_LEFT: + crop_horizontal = 0 + elif crop_placement[0] == Tags.CROP_POSITION_RIGHT: + crop_horizontal = image_width_pixels-crop_width-1 + elif crop_placement[0] == Tags.CROP_POSITION_CENTRE: + crop_horizontal = round((image_width_pixels - crop_width) / 2) + elif isinstance(crop_placement[0], int): + crop_horizontal = crop_placement[0] + + if crop_placement[1] == Tags.CROP_POSITION_TOP: + crop_vertical = 0 + elif crop_placement[1] == Tags.CROP_POSITION_BOTTOM: + crop_vertical = image_height_pixels-crop_height-1 + elif crop_placement[1] == Tags.CROP_POSITION_CENTRE: + crop_vertical = round((image_height_pixels - crop_height) / 2) + elif isinstance(crop_placement[1], int): + crop_vertical = crop_placement[1] + + elif isinstance(crop_placement, str): + if crop_placement == Tags.CROP_POSITION_CENTRE: + crop_horizontal = round((image_width_pixels - crop_width) / 2) + crop_vertical = round((image_height_pixels - crop_height) / 2) + elif crop_placement == Tags.CROP_POSITION_RANDOM: + crop_horizontal = image_width_pixels - crop_width + if crop_horizontal != 0: + crop_horizontal = np.random.randint(0, crop_horizontal) + crop_vertical = image_height_pixels - crop_height + if crop_vertical != 0: + crop_vertical = np.random.randint(0, crop_vertical) + + cropped_image = image[crop_horizontal: crop_horizontal + crop_width, crop_vertical: crop_vertical + crop_height] + + self.logger.warning( + "The input image has been cropped to the dimensions of the simulation volume ({} {})".format(xdim, zdim)) + return cropped_image + + def change_resolution(self, image: np.ndarray, spacing_mm: Union[int, float], + image_pixel_spacing_mm: Union[int, float]) -> np.ndarray: + """ + Method to change the resolution of an image + :param image: input image + :param image_pixel_spacing_mm: original image pixel spacing mm + :param spacing_mm: target pixel spacing mm + :return: image with new pixel spacing + """ + (image_width_pixels, image_height_pixels) = image.shape + [image_width_mm, image_height_mm] = np.array([image_width_pixels, image_height_pixels]) * image_pixel_spacing_mm + new_image_pixel_width = round(image_width_mm / spacing_mm) + new_image_pixel_height = round(image_height_mm / spacing_mm) + + self.logger.warning( + "The input image has changed pixel spacing to {} to match the simulation volume".format(spacing_mm)) + return transform.resize(image, (new_image_pixel_width, new_image_pixel_height)) diff --git a/simpa/utils/libraries/molecule_library.py b/simpa/utils/libraries/molecule_library.py index f033b14d..eaad1359 100644 --- a/simpa/utils/libraries/molecule_library.py +++ b/simpa/utils/libraries/molecule_library.py @@ -4,6 +4,7 @@ from typing import Optional, Union import numpy as np +import torch from simpa.utils import Tags from simpa.utils.tissue_properties import TissueProperties @@ -11,24 +12,35 @@ from simpa.utils.libraries.spectrum_library import (AnisotropySpectrumLibrary, ScatteringSpectrumLibrary, RefractiveIndexSpectrumLibrary, AbsorptionSpectrumLibrary) from simpa.utils import Spectrum -from simpa.utils.calculate import calculate_oxygenation, calculate_gruneisen_parameter_from_temperature +from simpa.utils.calculate import calculate_oxygenation, calculate_gruneisen_parameter_from_temperature, calculate_bvf from simpa.utils.serializer import SerializableSIMPAClass +from simpa.log import Logger +from typing import Optional, Union class MolecularComposition(SerializableSIMPAClass, list): + """ + A class representing a molecular composition which is a list of Molecules. + + Attributes: + segmentation_type (str): The type of segmentation. + internal_properties (TissueProperties): The internal tissue properties. + """ def __init__(self, segmentation_type: Optional[str] = None, molecular_composition_settings: Optional[dict] = None): """ Initialize the MolecularComposition object. - :param segmentation_type: The type of segmentation. + :param segmentation_type: The segmentation class associated with this molecular composition. :type segmentation_type: str, optional - :param molecular_composition_settings: Settings for the molecular composition. + :param molecular_composition_settings: A settings dictionary or dict containing the molecules that constitute + this composition :type molecular_composition_settings: dict, optional """ super().__init__() self.segmentation_type = segmentation_type - self.internal_properties = TissueProperties() + self.internal_properties: TissueProperties = None + self.logger = Logger() if molecular_composition_settings is None: return @@ -37,19 +49,22 @@ def __init__(self, segmentation_type: Optional[str] = None, molecular_compositio for molecule_name in _keys: self.append(molecular_composition_settings[molecule_name]) - def update_internal_properties(self): + def update_internal_properties(self, settings): """ - Update the internal tissue properties based on the molecular composition. + Re-defines the internal properties of the molecular composition. + For each data field and molecule, a linear mixing model is used to arrive at the final parameters. Raises: AssertionError: If the total volume fraction of all molecules is not exactly 100%. """ - self.internal_properties = TissueProperties() + self.internal_properties = TissueProperties(settings) self.internal_properties[Tags.DATA_FIELD_SEGMENTATION] = self.segmentation_type self.internal_properties[Tags.DATA_FIELD_OXYGENATION] = calculate_oxygenation(self) + self.internal_properties[Tags.DATA_FIELD_BLOOD_VOLUME_FRACTION] = calculate_bvf(self) + search_list = self.copy() - for molecule in self: - self.internal_properties.volume_fraction += molecule.volume_fraction + for molecule in search_list: + self.internal_properties.volume_fraction += molecule.get_volume_fraction() self.internal_properties[Tags.DATA_FIELD_GRUNEISEN_PARAMETER] += \ molecule.volume_fraction * molecule.gruneisen_parameter self.internal_properties[Tags.DATA_FIELD_DENSITY] += molecule.volume_fraction * molecule.density @@ -58,24 +73,27 @@ def update_internal_properties(self): self.internal_properties[Tags.DATA_FIELD_ALPHA_COEFF] += molecule.volume_fraction * \ molecule.alpha_coefficient - if np.abs(self.internal_properties.volume_fraction - 1.0) > 1e-3: - raise AssertionError("Invalid Molecular composition! The volume fractions of all molecules must be" - "exactly 100%!") + if (torch.abs(self.internal_properties.volume_fraction - 1.0) > 1e-5).any(): + if not (torch.abs(self.internal_properties.volume_fraction - 1.0) < 1e-5).any(): + raise AssertionError("Invalid Molecular composition! The volume fractions of all molecules must be" + "exactly 100% somewhere!") + self.logger.warning("Some of the volume has not been filled by this molecular composition. Please check" + "that this is correct") - def get_properties_for_wavelength(self, wavelength: Union[int, float]) -> TissueProperties: + def get_properties_for_wavelength(self, settings, wavelength: Union[int, float]) -> TissueProperties: """ Get the tissue properties for a specific wavelength. :param wavelength: The wavelength to get properties for. :return: The updated tissue properties. """ - self.update_internal_properties() + self.update_internal_properties(settings) self.internal_properties[Tags.DATA_FIELD_ABSORPTION_PER_CM] = 0 self.internal_properties[Tags.DATA_FIELD_SCATTERING_PER_CM] = 0 self.internal_properties[Tags.DATA_FIELD_ANISOTROPY] = 0 self.internal_properties[Tags.DATA_FIELD_REFRACTIVE_INDEX] = 0 - - for molecule in self: + search_list = self.copy() + for molecule in search_list: self.internal_properties[Tags.DATA_FIELD_ABSORPTION_PER_CM] += \ (molecule.volume_fraction * molecule.absorption_spectrum.get_value_for_wavelength(wavelength)) @@ -97,6 +115,7 @@ def serialize(self) -> dict: :return: The serialized molecular composition. """ dict_items = self.__dict__ + dict_items["internal_properties"] = None # Todo: Explain why. list_items = [molecule for molecule in self] return {"MolecularComposition": {"dict_items": dict_items, "list_items": list_items}} @@ -174,8 +193,11 @@ def __init__(self, name: str = None, if volume_fraction is None: volume_fraction = 0.0 - if not isinstance(volume_fraction, (int, float, np.int64)): - raise TypeError(f"The given volume_fraction was not of type float instead of {type(volume_fraction)}!") + if not isinstance(volume_fraction, (int, float, np.int64, np.ndarray)): + raise TypeError(f"The given volume_fraction was not of type float or array instead of " + f"{type(volume_fraction)}!") + if isinstance(volume_fraction, np.ndarray): + volume_fraction = torch.tensor(volume_fraction, dtype=torch.float32) self.volume_fraction = volume_fraction if scattering_spectrum is None: @@ -200,29 +222,37 @@ def __init__(self, name: str = None, if gruneisen_parameter is None: gruneisen_parameter = calculate_gruneisen_parameter_from_temperature( StandardProperties.BODY_TEMPERATURE_CELCIUS) - if not isinstance(gruneisen_parameter, (int, float)): + if not isinstance(gruneisen_parameter, (np.int32, np.int64, int, float, np.ndarray)): raise TypeError(f"The given gruneisen_parameter was not of type int or float instead " f"of {type(gruneisen_parameter)}!") + if isinstance(gruneisen_parameter, np.ndarray): + gruneisen_parameter = torch.tensor(gruneisen_parameter, dtype=torch.float32) self.gruneisen_parameter = gruneisen_parameter if density is None: density = StandardProperties.DENSITY_GENERIC - if not isinstance(density, (np.int32, np.int64, int, float)): + if not isinstance(density, (np.int32, np.int64, int, float, np.ndarray)): raise TypeError(f"The given density was not of type int or float instead of {type(density)}!") + if isinstance(density, np.ndarray): + density = torch.tensor(density, dtype=torch.float32) self.density = density if speed_of_sound is None: speed_of_sound = StandardProperties.SPEED_OF_SOUND_GENERIC - if not isinstance(speed_of_sound, (np.int32, np.int64, int, float)): + if not isinstance(speed_of_sound, (np.int32, np.int64, int, float, np.ndarray)): raise TypeError("The given speed_of_sound was not of type int or float instead of {}!" .format(type(speed_of_sound))) + if isinstance(speed_of_sound, np.ndarray): + speed_of_sound = torch.tensor(speed_of_sound, dtype=torch.float32) self.speed_of_sound = speed_of_sound if alpha_coefficient is None: alpha_coefficient = StandardProperties.ALPHA_COEFF_GENERIC - if not isinstance(alpha_coefficient, (int, float)): + if not isinstance(alpha_coefficient, (np.int32, np.int64, int, float, np.ndarray)): raise TypeError("The given alpha_coefficient was not of type int or float instead of {}!" .format(type(alpha_coefficient))) + if isinstance(alpha_coefficient, np.ndarray): + alpha_coefficient = torch.tensor(alpha_coefficient, dtype=torch.float32) self.alpha_coefficient = alpha_coefficient def __eq__(self, other) -> bool: @@ -247,6 +277,9 @@ def __eq__(self, other) -> bool: else: return super().__eq__(other) + def get_volume_fraction(self): + return self.volume_fraction + def serialize(self): """ Serialize the molecule to a dictionary. @@ -273,6 +306,7 @@ def deserialize(dictionary_to_deserialize: dict): speed_of_sound=dictionary_to_deserialize["speed_of_sound"], gruneisen_parameter=dictionary_to_deserialize["gruneisen_parameter"], anisotropy_spectrum=dictionary_to_deserialize["anisotropy_spectrum"], + refractive_index=dictionary_to_deserialize["refractive_index"], density=dictionary_to_deserialize["density"]) return deserialized_molecule @@ -286,7 +320,7 @@ class MoleculeLibrary(object): """ # Main absorbers @staticmethod - def water(volume_fraction: float = 1.0) -> Molecule: + def water(volume_fraction: (float, torch.Tensor) = 1.0) -> Molecule: """ Create a water molecule with predefined properties. @@ -307,7 +341,7 @@ def water(volume_fraction: float = 1.0) -> Molecule: ) @staticmethod - def oxyhemoglobin(volume_fraction: float = 1.0) -> Molecule: + def oxyhemoglobin(volume_fraction: (float, torch.Tensor) = 1.0) -> Molecule: """ Create an oxyhemoglobin molecule with predefined properties. @@ -327,7 +361,7 @@ def oxyhemoglobin(volume_fraction: float = 1.0) -> Molecule: ) @staticmethod - def deoxyhemoglobin(volume_fraction: float = 1.0) -> Molecule: + def deoxyhemoglobin(volume_fraction: (float, torch.Tensor) = 1.0) -> Molecule: """ Create a deoxyhemoglobin molecule with predefined properties. @@ -336,7 +370,6 @@ def deoxyhemoglobin(volume_fraction: float = 1.0) -> Molecule: """ return Molecule(name="deoxyhemoglobin", absorption_spectrum=AbsorptionSpectrumLibrary().get_spectrum_by_name("Deoxyhemoglobin"), - volume_fraction=volume_fraction, scattering_spectrum=ScatteringSpectrumLibrary().get_spectrum_by_name("blood_scattering"), anisotropy_spectrum=AnisotropySpectrumLibrary.CONSTANT_ANISOTROPY_ARBITRARY( @@ -348,7 +381,7 @@ def deoxyhemoglobin(volume_fraction: float = 1.0) -> Molecule: ) @staticmethod - def melanin(volume_fraction: float = 1.0) -> Molecule: + def melanin(volume_fraction: (float, torch.Tensor) = 1.0) -> Molecule: """ Create a melanin molecule with predefined properties. @@ -370,7 +403,7 @@ def melanin(volume_fraction: float = 1.0) -> Molecule: ) @staticmethod - def fat(volume_fraction: float = 1.0) -> Molecule: + def fat(volume_fraction: (float, torch.Tensor) = 1.0) -> Molecule: """ Create a fat molecule with predefined properties. @@ -393,7 +426,7 @@ def fat(volume_fraction: float = 1.0) -> Molecule: # Scatterers @staticmethod def constant_scatterer(scattering_coefficient: float = 100.0, anisotropy: float = 0.9, - refractive_index: float = 1.329, volume_fraction: float = 1.0): + volume_fraction: (float, torch.Tensor) = 1.0) -> Molecule: """ Create a constant scatterer molecule with predefined properties. @@ -415,7 +448,7 @@ def constant_scatterer(scattering_coefficient: float = 100.0, anisotropy: float ) @staticmethod - def soft_tissue_scatterer(volume_fraction: float = 1.0) -> Molecule: + def soft_tissue_scatterer(volume_fraction: (float, torch.Tensor) = 1.0) -> Molecule: """ Create a soft tissue scatterer molecule with predefined properties. @@ -436,7 +469,7 @@ def soft_tissue_scatterer(volume_fraction: float = 1.0) -> Molecule: ) @staticmethod - def muscle_scatterer(volume_fraction: float = 1.0) -> Molecule: + def muscle_scatterer(volume_fraction: (float, torch.Tensor) = 1.0) -> Molecule: """ Create a muscle scatterer molecule with predefined properties. @@ -457,7 +490,7 @@ def muscle_scatterer(volume_fraction: float = 1.0) -> Molecule: ) @staticmethod - def epidermal_scatterer(volume_fraction: float = 1.0) -> Molecule: + def epidermal_scatterer(volume_fraction: (float, torch.Tensor) = 1.0) -> Molecule: """ Create an epidermal scatterer molecule with predefined properties. @@ -479,7 +512,7 @@ def epidermal_scatterer(volume_fraction: float = 1.0) -> Molecule: ) @staticmethod - def dermal_scatterer(volume_fraction: float = 1.0) -> Molecule: + def dermal_scatterer(volume_fraction: (float, torch.Tensor) = 1.0) -> Molecule: """ Create a dermal scatterer molecule with predefined properties. @@ -500,7 +533,7 @@ def dermal_scatterer(volume_fraction: float = 1.0) -> Molecule: ) @staticmethod - def bone(volume_fraction: float = 1.0) -> Molecule: + def bone(volume_fraction: (float, torch.Tensor) = 1.0) -> Molecule: """ Create a bone molecule with predefined properties. @@ -522,7 +555,7 @@ def bone(volume_fraction: float = 1.0) -> Molecule: ) @staticmethod - def mediprene(volume_fraction: float = 1.0) -> Molecule: + def mediprene(volume_fraction: (float, torch.Tensor) = 1.0) -> Molecule: """ Create a mediprene molecule with predefined properties. @@ -543,7 +576,7 @@ def mediprene(volume_fraction: float = 1.0) -> Molecule: ) @staticmethod - def heavy_water(volume_fraction: float = 1.0) -> Molecule: + def heavy_water(volume_fraction: (float, torch.Tensor) = 1.0) -> Molecule: """ Create a heavy water molecule with predefined properties. @@ -565,7 +598,7 @@ def heavy_water(volume_fraction: float = 1.0) -> Molecule: ) @staticmethod - def air(volume_fraction: float = 1.0) -> Molecule: + def air(volume_fraction: (float, torch.Tensor) = 1.0) -> Molecule: """ Create an air molecule with predefined properties. diff --git a/simpa/utils/libraries/spectrum_library.py b/simpa/utils/libraries/spectrum_library.py index 07163ff8..9689fbee 100644 --- a/simpa/utils/libraries/spectrum_library.py +++ b/simpa/utils/libraries/spectrum_library.py @@ -7,7 +7,8 @@ import glob import numpy as np import matplotlib.pylab as plt -from simpa.utils.libraries.literature_values import OpticalTissueProperties +import torch +from scipy import interpolate from simpa.utils.serializer import SerializableSIMPAClass @@ -34,18 +35,22 @@ def __init__(self, spectrum_name: str, wavelengths: np.ndarray, values: np.ndarr :raises ValueError: If the shape of wavelengths does not match the shape of values. """ + if isinstance(values, np.ndarray): + values = torch.from_numpy(values) + wavelengths = torch.from_numpy(wavelengths) self.spectrum_name = spectrum_name self.wavelengths = wavelengths - self.max_wavelength = int(np.max(wavelengths)) - self.min_wavelength = int(np.min(wavelengths)) + self.max_wavelength = int(torch.floor(torch.max(wavelengths))) + self.min_wavelength = int(torch.ceil(torch.min(wavelengths))) self.values = values - if np.shape(wavelengths) != np.shape(values): + if torch.Tensor.size(wavelengths) != torch.Tensor.size(values): raise ValueError("The shape of the wavelengths and the values did not match: " + - str(np.shape(wavelengths)) + " vs " + str(np.shape(values))) + str(torch.Tensor.size(wavelengths)) + " vs " + str(torch.Tensor.size(values))) - new_wavelengths = np.arange(self.min_wavelength, self.max_wavelength+1, 1) - self.values_interp = np.interp(new_wavelengths, self.wavelengths, self.values) + new_wavelengths = torch.arange(self.min_wavelength, self.max_wavelength+1, 1) + new_absorptions_function = interpolate.interp1d(self.wavelengths, self.values) + self.values_interp = new_absorptions_function(new_wavelengths) def get_value_over_wavelength(self) -> np.ndarray: """ @@ -302,7 +307,7 @@ def view_saved_spectra(save_path=None, mode="absorption"): Opens a matplotlib plot and visualizes the available spectra. :param save_path: If not None, then the figure will be saved as a PNG file to the destination. - :param mode: Specifies the type of spectra to visualize ("absorption", "scattering", or "anisotropy"). + :param mode: Specifies the type of spectra to visualize ("absorption", "scattering", "anisotropy" or "refractive_index). """ plt.figure(figsize=(11, 8)) if mode == "absorption": @@ -320,8 +325,14 @@ def view_saved_spectra(save_path=None, mode="absorption"): plt.semilogy(spectrum.wavelengths, spectrum.values, label=spectrum.spectrum_name) + elif mode == "refractive_index": + for spectrum in RefractiveIndexSpectrumLibrary(): + plt.semilogy(spectrum.wavelengths, + spectrum.values, + label=spectrum.spectrum_name) else: - raise ValueError(f"Invalid mode: {mode}. Choose from 'absorption', 'scattering', or 'anisotropy'.") + raise ValueError( + f"Invalid mode: {mode}. Choose from 'absorption', 'scattering', 'anisotropy' or 'refractive_index'.") ax = plt.gca() box = ax.get_position() diff --git a/simpa/utils/libraries/structure_library/EllipticalTubularStructure.py b/simpa/utils/libraries/structure_library/EllipticalTubularStructure.py index 6423ef06..b6b1cf4d 100644 --- a/simpa/utils/libraries/structure_library/EllipticalTubularStructure.py +++ b/simpa/utils/libraries/structure_library/EllipticalTubularStructure.py @@ -94,7 +94,7 @@ def get_enclosed_indices(self): main_axis_vector = main_axis_vector/torch.linalg.norm(main_axis_vector) * main_axis_length minor_axis_length = main_axis_length*torch.sqrt(1-eccentricity**2) - minor_axis_vector = torch.cross(cylinder_vector, main_axis_vector) + minor_axis_vector = torch.linalg.cross(cylinder_vector, main_axis_vector) minor_axis_vector = minor_axis_vector / torch.linalg.norm(minor_axis_vector) * minor_axis_length dot_product = torch.matmul(target_vector, cylinder_vector)/torch.linalg.norm(cylinder_vector) diff --git a/simpa/utils/libraries/structure_library/HorizontalLayerStructure.py b/simpa/utils/libraries/structure_library/HorizontalLayerStructure.py index 9a439e5d..57097731 100644 --- a/simpa/utils/libraries/structure_library/HorizontalLayerStructure.py +++ b/simpa/utils/libraries/structure_library/HorizontalLayerStructure.py @@ -7,6 +7,9 @@ from simpa.utils import Tags from simpa.utils.libraries.molecule_library import MolecularComposition from simpa.utils.libraries.structure_library.StructureBase import GeometricalStructure +from simpa.log import Logger + +logger = Logger() class HorizontalLayerStructure(GeometricalStructure): @@ -100,6 +103,36 @@ def get_enclosed_indices(self): return bools_all_layers.cpu().numpy(), volume_fractions[bools_all_layers].cpu().numpy() + def update_molecule_volume_fractions(self, single_structure_settings): + for molecule in self.molecule_composition: + old_vol = getattr(molecule, "volume_fraction") + if isinstance(old_vol, torch.Tensor): + structure_start_voxels = (torch.tensor(single_structure_settings[Tags.STRUCTURE_START_MM]) / + self.voxel_spacing) + structure_end_voxels = (torch.tensor(single_structure_settings[Tags.STRUCTURE_END_MM]) / + self.voxel_spacing) + structure_size = structure_end_voxels - structure_start_voxels + + if self.volume_dimensions_voxels[2] != old_vol.shape[2]: + if self.volume_dimensions_voxels[2] == structure_size[2]: + pad_start = structure_start_voxels.flip(dims=[0]) + pad_end = ((torch.from_numpy(self.volume_dimensions_voxels) - structure_end_voxels) + .flip(dims=[0])) + for count, structure_end in enumerate(structure_end_voxels): + if structure_end == 0: + pad_end[2 - count] = 0 + + if (pad_start > 1e-6).any() or (pad_end > 1e-6).any(): + padding_list = torch.flatten(torch.stack((pad_start, pad_end), 1)).tolist() + padding = tuple(map(int, padding_list)) + padded_vol = torch.nn.functional.pad(old_vol, padding, mode='constant', value=0) + setattr(molecule, "volume_fraction", padded_vol) + else: + logger.critical("Tensor does not have the same dimensionality as the area it should fill" + + "{} or the dimensions of the entire ".format(old_vol.shape) + + "simulation volume{}! Please change this.".format( + self.volume_dimensions_voxels.shape)) + def define_horizontal_layer_structure_settings(molecular_composition: MolecularComposition, z_start_mm: float = 0, thickness_mm: float = 0, priority: int = 10, diff --git a/simpa/utils/libraries/structure_library/StructureBase.py b/simpa/utils/libraries/structure_library/StructureBase.py index 399bb988..3ffc31d1 100644 --- a/simpa/utils/libraries/structure_library/StructureBase.py +++ b/simpa/utils/libraries/structure_library/StructureBase.py @@ -7,7 +7,7 @@ import numpy as np from simpa.log import Logger -from simpa.utils import Settings, Tags, get_functional_from_deformation_settings +from simpa.utils import Settings, Tags, get_functional_from_deformation_settings, round_x5_away_from_zero from simpa.utils.libraries.molecule_library import MolecularComposition from simpa.utils.tissue_properties import TissueProperties from simpa.utils.processing_device import get_processing_device @@ -30,9 +30,9 @@ def __init__(self, global_settings: Settings, self.logger = Logger() self.voxel_spacing = global_settings[Tags.SPACING_MM] - volume_x_dim = int(np.round(global_settings[Tags.DIM_VOLUME_X_MM] / self.voxel_spacing)) - volume_y_dim = int(np.round(global_settings[Tags.DIM_VOLUME_Y_MM] / self.voxel_spacing)) - volume_z_dim = int(np.round(global_settings[Tags.DIM_VOLUME_Z_MM] / self.voxel_spacing)) + volume_x_dim = round_x5_away_from_zero(global_settings[Tags.DIM_VOLUME_X_MM] / self.voxel_spacing) + volume_y_dim = round_x5_away_from_zero(global_settings[Tags.DIM_VOLUME_Y_MM] / self.voxel_spacing) + volume_z_dim = round_x5_away_from_zero(global_settings[Tags.DIM_VOLUME_Z_MM] / self.voxel_spacing) self.volume_dimensions_voxels = np.asarray([volume_x_dim, volume_y_dim, volume_z_dim]) self.volume_dimensions_mm = self.volume_dimensions_voxels * self.voxel_spacing @@ -66,13 +66,18 @@ def __init__(self, global_settings: Settings, else: self.partial_volume = False - self.molecule_composition = single_structure_settings[Tags.MOLECULE_COMPOSITION] - self.molecule_composition.update_internal_properties() + self.molecule_composition: MolecularComposition = single_structure_settings[Tags.MOLECULE_COMPOSITION] + self.update_molecule_volume_fractions(single_structure_settings) + self.molecule_composition.update_internal_properties(global_settings) self.geometrical_volume = np.zeros(self.volume_dimensions_voxels, dtype=np.float32) self.params = self.get_params_from_settings(single_structure_settings) self.fill_internal_volume() + assert ((self.molecule_composition.internal_properties.volume_fraction[self.geometrical_volume != 0] - 1 < 1e-5) + .all()), ("Invalid Molecular composition! The volume fractions of all molecules in the structure must" + "be exactly 100%!") + def fill_internal_volume(self): """ Fills self.geometrical_volume of the GeometricalStructure. @@ -80,6 +85,12 @@ def fill_internal_volume(self): indices, values = self.get_enclosed_indices() self.geometrical_volume[indices] = values + def get_volume_fractions(self): + """ + Get the volume fraction this structure takes per voxel. + """ + return self.geometrical_volume + @abstractmethod def get_enclosed_indices(self): """ @@ -89,7 +100,7 @@ def get_enclosed_indices(self): pass @abstractmethod - def get_params_from_settings(self, single_structure_settings): + def get_params_from_settings(self, single_structure_settings: Settings): """ Gets all the parameters required for the specific GeometricalStructure. :param single_structure_settings: Settings which describe the specific GeometricalStructure. @@ -97,13 +108,25 @@ def get_params_from_settings(self, single_structure_settings): """ pass - def properties_for_wavelength(self, wavelength) -> TissueProperties: + def properties_for_wavelength(self, settings, wavelength) -> TissueProperties: """ Returns the values corresponding to each optical/acoustic property used in SIMPA. + :param settings: The global settings that contains the info on the volume dimensions. :param wavelength: Wavelength of the queried properties :return: optical/acoustic properties """ - return self.molecule_composition.get_properties_for_wavelength(wavelength) + return self.molecule_composition.get_properties_for_wavelength(settings, wavelength) + + def update_molecule_volume_fractions(self, single_structure_settings: Settings): + """ + In particular cases, only molecule volume fractions are determined by tensors that expand the structure. This + method allows the tensor to only have the size of the structure, with the rest of the volume filled with volume + fractions of 0. + In the case where the tensors defined are such that they fill the volume, they will be left. Later, when + using priority_sorted_structures the volume used will be that within the boundaries in the shape. + :param single_structure_settings: Settings which describe the specific GeometricalStructure. + """ + pass @abstractmethod def to_settings(self) -> Settings: diff --git a/simpa/utils/libraries/structure_library/__init__.py b/simpa/utils/libraries/structure_library/__init__.py index 7d821601..db527a4a 100644 --- a/simpa/utils/libraries/structure_library/__init__.py +++ b/simpa/utils/libraries/structure_library/__init__.py @@ -36,6 +36,7 @@ def priority_sorted_structures(settings: Settings, volume_creator_settings: dict sorted_structure_settings = sorted( [structure_setting for structure_setting in volume_creator_settings[Tags.STRUCTURES].values()], key=lambda s: s[Tags.PRIORITY] if Tags.PRIORITY in s else 0, reverse=True) + for structure_setting in sorted_structure_settings: try: structure_class = globals()[structure_setting[Tags.STRUCTURE_TYPE]] diff --git a/simpa/utils/libraries/tissue_library.py b/simpa/utils/libraries/tissue_library.py index 2c08209b..4c79b0df 100644 --- a/simpa/utils/libraries/tissue_library.py +++ b/simpa/utils/libraries/tissue_library.py @@ -1,20 +1,18 @@ # SPDX-FileCopyrightText: 2021 Division of Intelligent Medical Systems, DKFZ # SPDX-FileCopyrightText: 2021 Janek Groehl # SPDX-License-Identifier: MIT -import typing +import numpy as np +from typing import Union, List, Optional from simpa.utils import OpticalTissueProperties, SegmentationClasses, StandardProperties, MolecularCompositionGenerator from simpa.utils import Molecule from simpa.utils import MOLECULE_LIBRARY -from simpa.utils.libraries.spectrum_library import (AnisotropySpectrumLibrary, ScatteringSpectrumLibrary, - RefractiveIndexSpectrumLibrary) from simpa.utils import Spectrum from simpa.utils.libraries.molecule_library import MolecularComposition -from simpa.utils.libraries.spectrum_library import AnisotropySpectrumLibrary, ScatteringSpectrumLibrary +from simpa.utils.libraries.spectrum_library import (AnisotropySpectrumLibrary, ScatteringSpectrumLibrary, + RefractiveIndexSpectrumLibrary, AbsorptionSpectrumLibrary) from simpa.utils.calculate import randomize_uniform -from simpa.utils.libraries.spectrum_library import AbsorptionSpectrumLibrary -from typing import Union, List import torch @@ -23,9 +21,9 @@ class TissueLibrary(object): A library, returning molecular compositions for various typical tissue segmentations. """ - def get_blood_volume_fractions(self, oxygenation: Union[float, int, torch.Tensor] = 1e-10, - blood_volume_fraction: Union[float, int, torch.Tensor] = 1e-10)\ - -> List[Union[int, float, torch.Tensor]]: + def get_blood_volume_fractions(self, oxygenation: Union[float, int, np.ndarray, torch.Tensor] = 1e-10, + blood_volume_fraction: Union[float, int, np.ndarray, torch.Tensor] = 1e-10)\ + -> List[Union[int, float, np.ndarray]]: """ A function that returns the volume fraction of the oxygenated and deoxygenated blood. :param oxygenation: The oxygenation level of the blood volume fraction (as a decimal). @@ -36,8 +34,8 @@ def get_blood_volume_fractions(self, oxygenation: Union[float, int, torch.Tensor """ return [blood_volume_fraction*oxygenation, blood_volume_fraction*(1-oxygenation)] - def constant(self, mua: Union[float, int, torch.Tensor] = 1e-10, mus: Union[float, int, torch.Tensor] = 1e-10, - g: Union[float, int, torch.Tensor] = 0, n: float = 1.3) -> MolecularComposition: + def constant(self, mua: Union[float, int, np.ndarray, torch.Tensor] = 1e-10, mus: Union[float, int, np.ndarray, torch.Tensor] = 1e-10, + g: Union[float, int, np.ndarray, torch.Tensor] = 0, n: float = 1.3) -> MolecularComposition: """ A function returning a molecular composition as specified by the user. Typically intended for the use of wanting specific mua, mus and g values. @@ -60,7 +58,7 @@ def generic_tissue(self, mus: Spectrum = AbsorptionSpectrumLibrary().CONSTANT_ABSORBER_ARBITRARY(1e-10), g: Spectrum = AbsorptionSpectrumLibrary().CONSTANT_ABSORBER_ARBITRARY(1e-10), n: Spectrum = RefractiveIndexSpectrumLibrary.CONSTANT_REFRACTOR_ARBITRARY(1.3), - molecule_name: typing.Optional[str] = "generic_tissue") -> MolecularComposition: + molecule_name: Optional[str] = "generic_tissue") -> MolecularComposition: """ Returns a generic issue defined by the provided optical parameters. @@ -86,10 +84,14 @@ def generic_tissue(self, refractive_index=n)) .get_molecular_composition(SegmentationClasses.GENERIC)) - def muscle(self, oxygenation: Union[float, int, torch.Tensor] = 0.175, - blood_volume_fraction: Union[float, int, torch.Tensor] = 0.06) -> MolecularComposition: + def muscle(self, oxygenation: Union[float, int, np.ndarray, torch.Tensor] = 0.175, + blood_volume_fraction: Union[float, int, np.ndarray, torch.Tensor] = 0.06) -> MolecularComposition: """ - + Create a molecular composition mimicking that of muscle + :param oxygenation: The oxygenation level of the blood volume fraction (as a decimal). + Default: 0.175 + :param blood_volume_fraction: The total blood volume fraction (including oxygenated and deoxygenated blood). + Default: 0.06 :return: a settings dictionary containing all min and max parameters fitting for muscle tissue. """ @@ -98,6 +100,16 @@ def muscle(self, oxygenation: Union[float, int, torch.Tensor] = 0.175, # Get the water volume fraction water_volume_fraction = OpticalTissueProperties.WATER_VOLUME_FRACTION_HUMAN_BODY + if isinstance(blood_volume_fraction, np.ndarray): + if (blood_volume_fraction + water_volume_fraction - 1 > 1e-5).any(): + raise AssertionError(f"Blood volume fraction too large, must be less than {1 - water_volume_fraction}" + f" everywhere to leave space for water") + + else: + if blood_volume_fraction + water_volume_fraction - 1 > 1e-5: + raise AssertionError(f"Blood volume fraction too large, must be less than {1 - water_volume_fraction}" + f"everywhere to leave space for water") + custom_water = MOLECULE_LIBRARY.water(water_volume_fraction) custom_water.anisotropy_spectrum = AnisotropySpectrumLibrary.CONSTANT_ANISOTROPY_ARBITRARY( OpticalTissueProperties.STANDARD_ANISOTROPY - 0.005) @@ -118,8 +130,8 @@ def muscle(self, oxygenation: Union[float, int, torch.Tensor] = 0.175, .append(custom_water) .get_molecular_composition(SegmentationClasses.MUSCLE)) - def soft_tissue(self, oxygenation: Union[float, int, torch.Tensor] = OpticalTissueProperties.BACKGROUND_OXYGENATION, - blood_volume_fraction: Union[float, int, torch.Tensor] = OpticalTissueProperties.BLOOD_VOLUME_FRACTION_MUSCLE_TISSUE) -> MolecularComposition: + def soft_tissue(self, oxygenation: Union[float, int, np.ndarray, torch.Tensor] = OpticalTissueProperties.BACKGROUND_OXYGENATION, + blood_volume_fraction: Union[float, int, np.ndarray, torch.Tensor] = OpticalTissueProperties.BLOOD_VOLUME_FRACTION_MUSCLE_TISSUE) -> MolecularComposition: """ IMPORTANT! This tissue is not tested and it is not based on a specific real tissue type. It is a mixture of muscle (mostly optical properties) and water (mostly acoustic properties). @@ -136,6 +148,16 @@ def soft_tissue(self, oxygenation: Union[float, int, torch.Tensor] = OpticalTiss # Get the water volume fraction water_volume_fraction = OpticalTissueProperties.WATER_VOLUME_FRACTION_HUMAN_BODY + if isinstance(blood_volume_fraction, np.ndarray): + if (blood_volume_fraction + water_volume_fraction - 1 > 1e-5).any(): + raise AssertionError(f"Blood volume fraction too large, must be less than {1 - water_volume_fraction}" + f"everywhere to leave space for water") + + else: + if blood_volume_fraction + water_volume_fraction - 1 > 1e-5: + raise AssertionError(f"Blood volume fraction too large, must be less than {1 - water_volume_fraction}" + f"everywhere to leave space for water") + custom_water = MOLECULE_LIBRARY.water(water_volume_fraction) custom_water.anisotropy_spectrum = AnisotropySpectrumLibrary.CONSTANT_ANISOTROPY_ARBITRARY( OpticalTissueProperties.STANDARD_ANISOTROPY - 0.005) @@ -156,20 +178,21 @@ def soft_tissue(self, oxygenation: Union[float, int, torch.Tensor] = OpticalTiss .append(custom_water) .get_molecular_composition(SegmentationClasses.SOFT_TISSUE)) - def epidermis(self, melanosom_volume_fraction: Union[float, int, torch.Tensor] = 0.014) -> MolecularComposition: + def epidermis(self, melanin_volume_fraction: Union[float, int, np.ndarray, torch.Tensor] = 0.014) -> MolecularComposition: """ - + Create a molecular composition mimicking that of dermis + :param melanin_volume_fraction: the total volume fraction of melanin :return: a settings dictionary containing all min and max parameters fitting for epidermis tissue. """ # generate the tissue dictionary return (MolecularCompositionGenerator() - .append(MOLECULE_LIBRARY.melanin(melanosom_volume_fraction)) - .append(MOLECULE_LIBRARY.epidermal_scatterer(1 - melanosom_volume_fraction)) + .append(MOLECULE_LIBRARY.melanin(melanin_volume_fraction)) + .append(MOLECULE_LIBRARY.epidermal_scatterer(1 - melanin_volume_fraction)) .get_molecular_composition(SegmentationClasses.EPIDERMIS)) - def dermis(self, oxygenation: Union[float, int, torch.Tensor] = 0.5, - blood_volume_fraction: Union[float, int, torch.Tensor] = 0.002) -> MolecularComposition: + def dermis(self, oxygenation: Union[float, int, np.ndarray, torch.Tensor] = 0.5, + blood_volume_fraction: Union[float, int, np.ndarray, torch.Tensor] = 0.002) -> MolecularComposition: """ Create a molecular composition mimicking that of dermis :param oxygenation: The oxygenation level of the blood volume fraction (as a decimal). @@ -190,8 +213,9 @@ def dermis(self, oxygenation: Union[float, int, torch.Tensor] = 0.5, .get_molecular_composition(SegmentationClasses.DERMIS)) def subcutaneous_fat(self, - oxygenation: Union[float, int, torch.Tensor] = OpticalTissueProperties.BACKGROUND_OXYGENATION, - blood_volume_fraction: Union[float, int, torch.Tensor] + oxygenation: Union[float, int, np.ndarray, + torch.Tensor] = OpticalTissueProperties.BACKGROUND_OXYGENATION, + blood_volume_fraction: Union[float, int, np.ndarray, torch.Tensor] = OpticalTissueProperties.BLOOD_VOLUME_FRACTION_MUSCLE_TISSUE) -> MolecularComposition: """ Create a molecular composition mimicking that of subcutaneous fat @@ -222,7 +246,7 @@ def subcutaneous_fat(self, .append(MOLECULE_LIBRARY.water(water_volume_fraction)) .get_molecular_composition(SegmentationClasses.FAT)) - def blood(self, oxygenation: Union[float, int, torch.Tensor, None] = None) -> MolecularComposition: + def blood(self, oxygenation: Union[float, int, np.ndarray, torch.Tensor, None] = None) -> MolecularComposition: """ Create a molecular composition mimicking that of blood :param oxygenation: The oxygenation level of the blood(as a decimal). @@ -288,8 +312,9 @@ def ultrasound_gel(self) -> MolecularComposition: .append(MOLECULE_LIBRARY.water()) .get_molecular_composition(SegmentationClasses.ULTRASOUND_GEL)) - def lymph_node(self, oxygenation: Union[float, int, torch.Tensor, None] = None, - blood_volume_fraction: Union[float, int, torch.Tensor, None] = None) -> MolecularComposition: + def lymph_node(self, oxygenation: Union[float, int, np.ndarray] = OpticalTissueProperties.LYMPH_NODE_OXYGENATION, + blood_volume_fraction: Union[float, int, np.ndarray] = + OpticalTissueProperties.BLOOD_VOLUME_FRACTION_LYMPH_NODE) -> MolecularComposition: """ IMPORTANT! This tissue is not tested and it is not based on a specific real tissue type. It is a mixture of oxyhemoglobin, deoxyhemoglobin, and lymph node customized water. @@ -300,14 +325,6 @@ def lymph_node(self, oxygenation: Union[float, int, torch.Tensor, None] = None, :return: a settings dictionary fitting for generic lymph node tissue. """ - # Determine muscle oxygenation - if oxygenation is None: - oxygenation = OpticalTissueProperties.LYMPH_NODE_OXYGENATION - - # Get the blood volume fractions for oxyhemoglobin and deoxyhemoglobin - if blood_volume_fraction is None: - blood_volume_fraction = OpticalTissueProperties.BLOOD_VOLUME_FRACTION_LYMPH_NODE - [fraction_oxy, fraction_deoxy] = self.get_blood_volume_fractions(oxygenation, blood_volume_fraction) # Get the water volume fraction diff --git a/simpa/utils/matlab.py b/simpa/utils/matlab.py index e2ce4838..1459ba0b 100644 --- a/simpa/utils/matlab.py +++ b/simpa/utils/matlab.py @@ -7,7 +7,21 @@ from typing import List -def generate_matlab_cmd(matlab_binary_path: str, simulation_script_path: str, data_path: str) -> List[str]: +def generate_matlab_cmd(matlab_binary_path: str, simulation_script_path: str, data_path: str, additional_flags: List[str] = []) -> List[str]: + """Generates the MATLAB execution command from the given paths + + :param matlab_binary_path: path to the MATLAB binary file as defined by PathManager + :type matlab_binary_path: str + :param simulation_script_path: path to the MATLAB script that should be run (either simulate_2D.m or simulate_3D.m) + :type simulation_script_path: str + :param data_path: path to the .mat file used for simulating + :type data_path: str + :param additional_flags: list of optional additional flags for MATLAB + :type additional_flags: List[str] + :return: list of command parts + :rtype: List[str] + """ + # get path of calling script to add to matlab path base_script_path = os.path.dirname(os.path.abspath(inspect.stack()[1].filename)) # ensure data path is an absolute path @@ -19,6 +33,7 @@ def generate_matlab_cmd(matlab_binary_path: str, simulation_script_path: str, da cmd.append("-nosplash") cmd.append("-automation") cmd.append("-wait") + cmd += additional_flags cmd.append("-r") cmd.append(f"addpath('{base_script_path}');{simulation_script_path}('{data_path}');exit;") return cmd diff --git a/simpa/utils/path_manager.py b/simpa/utils/path_manager.py index 2e7ba1ed..3d02f61d 100644 --- a/simpa/utils/path_manager.py +++ b/simpa/utils/path_manager.py @@ -20,9 +20,10 @@ class PathManager: one we provided in the `simpa_examples`) in the following places in this order: 1. The optional path you give the PathManager - 2. Your $HOME$ directory - 3. The current working directory - 4. The SIMPA home directory path + 2. Your set environment variables + 3. Your $HOME$ directory + 4. The current working directory + 5. The SIMPA home directory path """ def __init__(self, environment_path=None): @@ -32,6 +33,7 @@ def __init__(self, environment_path=None): """ self.logger = Logger() self.path_config_file_name = 'path_config.env' + override = False if environment_path is None: environment_path = os.path.join(str(Path.home()), self.path_config_file_name) self.logger.debug(f"Using $HOME$ path to search for config file: {environment_path}") @@ -44,13 +46,14 @@ def __init__(self, environment_path=None): f"for {self.path_config_file_name}") environment_path = os.path.join(environment_path, self.path_config_file_name) self.logger.debug(f"Using supplied path to search for config file: {environment_path}") + override = True if environment_path is None or not os.path.exists(environment_path) or not os.path.isfile(environment_path): error_message = f"Did not find a { self.path_config_file_name} file in any of the standard directories..." self.logger.warning(error_message) self.environment_path = environment_path - load_dotenv(environment_path, override=True) + load_dotenv(environment_path, override=override) def detect_local_path_config(self): """ @@ -78,8 +81,8 @@ def detect_local_path_config(self): return None def get_hdf5_file_save_path(self): - path = self.get_path_from_environment(f"{Tags.SIMPA_SAVE_PATH_VARNAME}") - self.logger.debug(f"Retrieved {Tags.SIMPA_SAVE_PATH_VARNAME}={path}") + path = self.get_path_from_environment(f"{Tags.SIMPA_SAVE_DIRECTORY_VARNAME}") + self.logger.debug(f"Retrieved {Tags.SIMPA_SAVE_DIRECTORY_VARNAME}={path}") return path def get_mcx_binary_path(self): diff --git a/simpa/utils/profiling.py b/simpa/utils/profiling.py index 7eb1a44e..896f879b 100644 --- a/simpa/utils/profiling.py +++ b/simpa/utils/profiling.py @@ -4,9 +4,17 @@ import os +# Determine the type of profiling from the environment variable profile_type = os.getenv("SIMPA_PROFILE") + +# Determine if a save file for profiling results is specified +if os.getenv("SIMPA_PROFILE_SAVE_FILE"): + stream = open(os.getenv("SIMPA_PROFILE_SAVE_FILE"), 'w') +else: + stream = None + if profile_type is None: - # define a no-op @profile decorator + # Define a no-op @profile decorator if no profiling is specified def profile(f): return f elif profile_type == "TIME": @@ -14,16 +22,30 @@ def profile(f): from line_profiler import LineProfiler profile = LineProfiler() - atexit.register(profile.print_stats) + # Register to print stats on program exit + atexit.register(lambda: profile.print_stats(stream=stream, output_unit=10**(-3))) elif profile_type == "MEMORY": from memory_profiler import profile + profile = profile(stream=stream) elif profile_type == "GPU_MEMORY": - from pytorch_memlab import profile - from torch.cuda import memory_summary + from pytorch_memlab.line_profiler.line_profiler import LineProfiler, DEFAULT_COLUMNS import atexit - @atexit.register - def print_memory_summary(): - print(memory_summary()) + global_line_profiler = LineProfiler() + global_line_profiler.enable() + + def profile(func, columns: tuple[str, ...] = DEFAULT_COLUMNS): + """ + Profile the function for GPU memory usage + """ + global_line_profiler.add_function(func) + + def print_stats_atexit(): + global_line_profiler.print_stats(func, columns, stream=stream) + + atexit.register(print_stats_atexit) + return func + else: + # Raise an error if the SIMPA_PROFILE value is invalid raise RuntimeError("SIMPA_PROFILE env var is defined but invalid: valid values are TIME, MEMORY, or GPU_MEMORY") diff --git a/simpa/utils/settings.py b/simpa/utils/settings.py index a6a80a30..447ff48d 100644 --- a/simpa/utils/settings.py +++ b/simpa/utils/settings.py @@ -69,6 +69,32 @@ def __delitem__(self, key): except KeyError: raise KeyError("The key '{}' is not in the Settings dictionary".format(key)) from None + def get_volume_dimensions_voxels(self): + """ + returns: tuple + the x, y, and z dimension of the volumes as a tuple + the volume dimension gets rounded after converting from a mm grid to a voxel grid of unit Tags.SPACING_MM. + """ + if Tags.SPACING_MM not in self: + raise AssertionError("The simpa.Tags.SPACING_MM tag must be set " + "to compute the volume dimensions in voxels") + if Tags.DIM_VOLUME_X_MM not in self: + raise AssertionError("The simpa.Tags.DIM_VOLUME_X_MM tag must be " + "set to compute the volume dimensions in voxels") + if Tags.DIM_VOLUME_Y_MM not in self: + raise AssertionError("The simpa.Tags.DIM_VOLUME_Y_MM tag must be " + "set to compute the volume dimensions in voxels") + if Tags.DIM_VOLUME_Z_MM not in self: + raise AssertionError("The simpa.Tags.DIM_VOLUME_Z_MM tag must be " + "set to compute the volume dimensions in voxels") + + voxel_spacing = self[Tags.SPACING_MM] + volume_x_dim = int(round(self[Tags.DIM_VOLUME_X_MM] / voxel_spacing)) + volume_y_dim = int(round(self[Tags.DIM_VOLUME_Y_MM] / voxel_spacing)) + volume_z_dim = int(round(self[Tags.DIM_VOLUME_Z_MM] / voxel_spacing)) + + return volume_x_dim, volume_y_dim, volume_z_dim + def get_optical_settings(self): """" Returns the settings for the optical forward model that are saved in this settings dictionary diff --git a/simpa/utils/tags.py b/simpa/utils/tags.py index 778a83ba..2d8c80c0 100644 --- a/simpa/utils/tags.py +++ b/simpa/utils/tags.py @@ -2,9 +2,9 @@ # SPDX-FileCopyrightText: 2021 Janek Groehl # SPDX-License-Identifier: MIT -from numbers import Number - import numpy as np +from numbers import Number +from typing import Iterable class Tags: @@ -113,9 +113,9 @@ class Tags: Usage: module volume_creation_module, naming convention """ - VOLUME_CREATOR_SEGMENTATION_BASED = "volume_creator_segmentation_based" + VOLUME_CREATOR_SEGMENTATION_BASED = "segmentation_based_adapter" """ - Corresponds to the SegmentationBasedVolumeCreator.\n + Corresponds to the SegmentationBasedAdapter.\n Usage: module volume_creation_module, naming convention """ @@ -879,6 +879,12 @@ class Tags: Usage: SIMPA package, naming convention """ + DATA_FIELD_BLOOD_VOLUME_FRACTION = "bvf" + """ + Blood volume fraction of the generated volume/structure.\n + Usage: SIMPA package, naming convention + """ + DATA_FIELD_SEGMENTATION = "seg" """ Segmentation of the generated volume/structure.\n @@ -1244,7 +1250,7 @@ class Tags: IO settings """ - SIMPA_OUTPUT_PATH = ("simpa_output_path", str) + SIMPA_OUTPUT_FILE_PATH = ("simpa_output_path", str) """ Default path of the SIMPA output if not specified otherwise.\n Usage: SIMPA package @@ -1255,7 +1261,7 @@ class Tags: Default filename of the SIMPA output if not specified otherwise.\n Usage: SIMPA package, naming convention """ - SIMPA_VERSION = ("simpa_version", str) + SIMPA_VERSION = "simpa_version" """ Version number of the currently installed simpa package Usage: SIMPA package @@ -1449,6 +1455,9 @@ class Tags: """ VOLUME_BOUNDARY_BONDITION = "volume_boundary_condition" + """ + FIXME + """ COMPUTE_PHOTON_DIRECTION_AT_EXIT = "save_dir_at_exit" """ @@ -1460,7 +1469,7 @@ class Tags: """ Identifier for the diffuse reflectance values at the surface of the volume (interface to 0-values voxels) Usage: simpa.core.simulation_modules.optical_simulation_module.optical_forward_model_mcx_reflectance_adapter - """ + """ DATA_FIELD_DIFFUSE_REFLECTANCE_POS = "diffuse_reflectance_pos" """ @@ -1483,7 +1492,76 @@ class Tags: Usage: simpa.core.simulation_modules.optical_simulation_module.optical_forward_model_mcx_reflectance_adapter """ - SIMPA_SAVE_PATH_VARNAME = "SIMPA_SAVE_PATH" + IMAGE_SCALING_SYMMETRIC = "symmetric" + """ + Flag indicating the use of reflection on edges during interpolation when rescaling an image + Usage: simpa.utils.libraries.structure_library.heterogeneity_generator + """ + + IMAGE_SCALING_STRETCH = "stretch" + """ + Flag indicating the use of reflection on edges during interpolation when rescaling an image + At the boundary, the image will reflect to fill the area + Usage: simpa.utils.libraries.structure_library.heterogeneity_generator + """ + + IMAGE_SCALING_WRAP = "wrap" + """ + Flag indicating tessellating during interpolation when rescaling an image + Usage: simpa.utils.libraries.structure_library.heterogeneity_generator + """ + + IMAGE_SCALING_CONSTANT = "constant" + """ + Flag indicating the use of a constant on edges during interpolation when rescaling an image + The rest of the area will be filled by a constant value + Usage: simpa.utils.libraries.structure_library.heterogeneity_generator + """ + + IMAGE_SCALING_EDGE = "edge" + """ + Flag indicating the expansion of the edges during interpolation when rescaling an image + The edge value will continue across the area + Usage: simpa.utils.libraries.structure_library.heterogeneity_generator + """ + + CROP_POSITION_TOP = "top" + """ + Flag indicating the crop position: along top edge of image + Usage: simpa.utils.libraries.structure_library.heterogeneity_generator + """ + + CROP_POSITION_BOTTOM = "bottom" + """ + Flag indicating the crop position: along bottom edge of image + Usage: simpa.utils.libraries.structure_library.heterogeneity_generator + """ + + CROP_POSITION_CENTRE = "centre" + """ + Flag indicating the crop position: along centre edge of image + Usage: simpa.utils.libraries.structure_library.heterogeneity_generator + """ + + CROP_POSITION_LEFT = "left" + """ + Flag indicating the crop position: along left-hand edge of image + Usage: simpa.utils.libraries.structure_library.heterogeneity_generator + """ + + CROP_POSITION_RIGHT = "right" + """ + Flag indicating the crop position: along right-hand edge of image + Usage: simpa.utils.libraries.structure_library.heterogeneity_generator + """ + + CROP_POSITION_RANDOM = "random" + """ + Flag indicating the crop position: random placement + Usage: simpa.utils.libraries.structure_library.heterogeneity_generator + """ + + SIMPA_SAVE_DIRECTORY_VARNAME = "SIMPA_SAVE_DIRECTORY" """ Identifier for the environment variable that defines where the results generated with SIMPA will be sotred """ @@ -1497,3 +1575,16 @@ class Tags: """ Identifier for the environment varibale that defines the path the the matlab executable. """ + + ADDITIONAL_FLAGS = ("additional_flags", Iterable) + """ + Defines a sequence of extra flags to be parsed to executables for simulation modules. + Caution: The user is responsible for checking if these flags exist and don't break the predefined flags' behaviour. + It is assumed that if flags are specified multiple times the flag provided last is considered. + This can for example be used to override predefined flags. + """ + + VOLUME_FRACTION = "volume_fraction" + """ + Identifier for the volume fraction for the simulation + """ diff --git a/simpa/utils/tissue_properties.py b/simpa/utils/tissue_properties.py index 02f35013..059d6730 100644 --- a/simpa/utils/tissue_properties.py +++ b/simpa/utils/tissue_properties.py @@ -3,13 +3,24 @@ # SPDX-License-Identifier: MIT from simpa.utils.constants import property_tags +from simpa.utils import Settings +import torch class TissueProperties(dict): + """ + The tissue properties contain a volumetric representation of each tissue parameter currently + modelled in the SIMPA framework. - def __init__(self): + It is a dictionary that is populated with each of the parameters. + The values of the parameters can be either numbers or numpy arrays. + It also contains a volume fraction field. + """ + + def __init__(self, settings: Settings): super().__init__() - self.volume_fraction = 0 + volume_x_dim, volume_y_dim, volume_z_dim = settings.get_volume_dimensions_voxels() + self.volume_fraction = torch.zeros((volume_x_dim, volume_y_dim, volume_z_dim), dtype=torch.float32) for key in property_tags: self[key] = 0 diff --git a/simpa/visualisation/matplotlib_data_visualisation.py b/simpa/visualisation/matplotlib_data_visualisation.py index e7836dc8..9c132c2b 100644 --- a/simpa/visualisation/matplotlib_data_visualisation.py +++ b/simpa/visualisation/matplotlib_data_visualisation.py @@ -29,6 +29,7 @@ def visualise_data(wavelength: int = None, show_reconstructed_data=False, show_segmentation_map=False, show_oxygenation=False, + show_blood_volume_fraction=False, show_linear_unmixing_sO2=False, show_diffuse_reflectance=False, log_scale=False, @@ -56,6 +57,7 @@ def visualise_data(wavelength: int = None, time_series_data = None reconstructed_data = None oxygenation = None + blood_volume_fraction = None linear_unmixing_sO2 = None diffuse_reflectance = None diffuse_reflectance_position = None @@ -121,6 +123,15 @@ def visualise_data(wavelength: int = None, show_oxygenation = False oxygenation = None + if show_blood_volume_fraction: + try: + blood_volume_fraction = get_data_field_from_simpa_output( + file, Tags.DATA_FIELD_BLOOD_VOLUME_FRACTION, wavelength) + except KeyError as e: + logger.critical("The key " + str(Tags.DATA_FIELD_BLOOD_VOLUME_FRACTION) + " was not in the simpa output.") + show_blood_volume_fraction = False + blood_volume_fraction = None + if show_linear_unmixing_sO2: try: linear_unmixing_output = get_data_field_from_simpa_output(file, Tags.LINEAR_UNMIXING_RESULT) @@ -205,6 +216,11 @@ def visualise_data(wavelength: int = None, data_item_names.append("Oxygenation") cmaps.append("viridis") logscales.append(False and log_scale) + if blood_volume_fraction is not None and show_blood_volume_fraction: + data_to_show.append(blood_volume_fraction) + data_item_names.append("Blood Volume Fraction") + cmaps.append("viridis") + logscales.append(False and log_scale) if linear_unmixing_sO2 is not None and show_linear_unmixing_sO2: data_to_show.append(linear_unmixing_sO2) data_item_names.append("Linear Unmixed Oxygenation") diff --git a/simpa_examples/benchmarking/__init__.py b/simpa_examples/benchmarking/__init__.py new file mode 100644 index 00000000..89cc8954 --- /dev/null +++ b/simpa_examples/benchmarking/__init__.py @@ -0,0 +1,3 @@ +# SPDX-FileCopyrightText: 2021 Division of Intelligent Medical Systems, DKFZ +# SPDX-FileCopyrightText: 2021 Janek Groehl +# SPDX-License-Identifier: MIT diff --git a/simpa_examples/benchmarking/extract_benchmarking_data.py b/simpa_examples/benchmarking/extract_benchmarking_data.py new file mode 100644 index 00000000..345397a1 --- /dev/null +++ b/simpa_examples/benchmarking/extract_benchmarking_data.py @@ -0,0 +1,130 @@ +# SPDX-FileCopyrightText: 2021 Division of Intelligent Medical Systems, DKFZ +# SPDX-FileCopyrightText: 2021 Janek Groehl +# SPDX-License-Identifier: MIT +import os + +import numpy as np +import pandas as pd +from pathlib import Path +from argparse import ArgumentParser + + +def lines_that_contain(string, fp): + """ + Function to determine if a string contains a certain word/phrase and the returns the full line + :param string: string the line in the fp is compared to + :param fp: full page of text from profiler output + :return: lines in the full page that contain 'string' + """ + return [line for line in fp if string in line] + + +def read_out_benchmarking_data(profiles: list = None, start: float = .2, stop: float = .4, step: float = .1, + savefolder: str = None) -> None: + """ Reads benchmarking data and creates a pandas dataframe + :param profiles: list with profiles ['TIME', "GPU_MEMORY", "MEMORY"] + :param start: start spacing default .2 + :param stop: stop spacing default .4 + :param step: step size default .1 + :param savefolder: PATH TO benchmarking data txt --> assumes txt: savefolder + "/benchmarking_data_" + + profile + "_" + str(spacing) + ".txt" + :return: None + :raises: ImportError (unknown units from input data) + """ + + # init defaults + if savefolder is None or savefolder == "default": + savefolder = Path(__file__).parent.resolve() + else: + savefolder = Path(savefolder) + + if profiles is None: + profiles = ['TIME', "GPU_MEMORY", "MEMORY"] + + spacings = np.arange(start, stop+1e-6, step).tolist() + + # specific information for the location of the data in the line profiler files + info_starts = {"MEMORY": 19, "GPU_MEMORY": 12, "TIME": 16} + info_ends = {"MEMORY": 29, "GPU_MEMORY": 26, "TIME": 29} + + profiling_strings = {"TIME": ("File:", "simpa_examples/", ".py"), + "GPU_MEMORY": ("##", "run_", "\n"), + "MEMORY": ("Filename:", "simpa_examples/", ".py"), } + + benchmarking_lists = [] # init result + for profile in profiles: + for spacing in spacings: + txt_file = "benchmarking_data_" + profile + "_" + str(np.round(spacing, 4)) + ".txt" + file_name = savefolder / txt_file + benchmarking_file = open(file_name, 'r') + current_examples = [] + + # where to find which files have successfully run + example_name_lines = lines_that_contain(profiling_strings[profile][0], benchmarking_file) + for enl in example_name_lines: + example = enl.rpartition(profiling_strings[profile][1])[2].rpartition(profiling_strings[profile][2])[0] + if example not in current_examples: + current_examples.append(example) + else: + break + + with open(file_name, 'r') as benchmarking_file: + lines_with_sp_simulate = lines_that_contain("sp.simulate", benchmarking_file) + + for e_idx, example in enumerate(current_examples): + try: + value = float(lines_with_sp_simulate[e_idx][info_starts[profile]:info_ends[profile]]) + unit = str(lines_with_sp_simulate[e_idx][info_ends[profile]]) + except ValueError: + with open(file_name, 'r') as benchmarking_file: + lines_with_run_sim = lines_that_contain("= run_sim", benchmarking_file) + value = 0 + for line in lines_with_run_sim: + value += float(line[info_starts[profile]:info_ends[profile]]) + unit = str(line[info_ends[profile]]) + + if profile == "TIME": + value_with_unit = value + else: + if unit == 'K': + value_with_unit = value / 1000 + elif unit == 'M': + value_with_unit = value + elif unit == 'G': + value_with_unit = value * 1000 + else: + raise ImportError(f'Unit {unit} not supported') + + benchmarking_lists.append([example, spacing, profile, value_with_unit]) + + # creating data frame + new_df = pd.DataFrame(benchmarking_lists, columns=['Example', 'Spacing', 'Profile', 'Value']) + new_df.astype(dtype={"Example": "str", "Spacing": "float64", "Profile": "str", "Value": "float64"}) + + # if exists: remove old file + df_file = savefolder / 'benchmarking_data_frame.csv' + if df_file.is_file(): + os.remove(df_file) + new_df.to_csv(df_file, index=False) + + +if __name__ == "__main__": + parser = ArgumentParser(description='Run benchmarking tests') + parser.add_argument("--start", default=.15, + help='start spacing default .2mm') + parser.add_argument("--stop", default=.25, + help='stop spacing default .4mm') + parser.add_argument("--step", default=.05, + help='step size mm') + parser.add_argument("--profiles", default=None, type=str, + help='the profile to run') + parser.add_argument("--savefolder", default=None, type=str, help='where to save the results') + config = parser.parse_args() + + profiles = config.profiles + if profiles and "%" in profiles: + profiles = profiles.split('%')[:-1] + elif profiles: + profiles = [profiles] + read_out_benchmarking_data(start=float(config.start), stop=float(config.stop), step=float(config.step), + profiles=profiles, savefolder=config.savefolder) diff --git a/simpa_examples/benchmarking/get_final_table.py b/simpa_examples/benchmarking/get_final_table.py new file mode 100644 index 00000000..ab90576f --- /dev/null +++ b/simpa_examples/benchmarking/get_final_table.py @@ -0,0 +1,69 @@ +# SPDX-FileCopyrightText: 2021 Division of Intelligent Medical Systems, DKFZ +# SPDX-FileCopyrightText: 2021 Janek Groehl +# SPDX-License-Identifier: MIT + +import pandas as pd +import numpy as np +from pathlib import Path +from argparse import ArgumentParser + + +def get_final_table(savefolder: str = None): + """ Function to get the final table from a benchmarking file. Just call it after the last iteration of the + benchmarking. Saves csv in same location as savefolder! + :param savefolder: str to csv file containing benchmarking data frame + :return: None + :raise: None + """ + if savefolder is None or savefolder == "default": + savefolder = Path(__file__).parent.resolve() + else: + savefolder = Path(savefolder) + + # read the csv file + df_file = savefolder / 'benchmarking_data_frame.csv' + new_df = pd.read_csv(df_file) + + # init result + mean_list = [] + # loop over every example, spacing, profile and saves mean and std for the sub dataframe + for expl in np.unique(new_df['Example']): + for space in np.unique(new_df['Spacing']): + expl_list = [expl, space] # init row of result + for prof in np.unique(new_df['Profile']): + # get the sub data frame for a specific profile and spacing. + sub_df = new_df.loc[ + (new_df['Spacing'] == space) & (new_df['Example'] == expl) & (new_df['Profile'] == prof)] + _mean = sub_df.mean(numeric_only=True)['Value'] + _std = sub_df.std(numeric_only=True)['Value'] + expl_list.append(np.round(_mean, 2)) # append to output row + expl_list.append(np.round(_std, 2)) + mean_list.append(expl_list) + + # init naming and format of output data frame columns + my_profiles = [] + type_dict = {"Example": "str", "Spacing": "float64"} + for entry in np.unique(new_df['Profile']).tolist(): + my_profiles.append(entry + '_mean') + my_profiles.append(entry + '_std') + type_dict[entry + '_mean'] = 'float64' + type_dict[entry + '_std'] = 'float64' + + # concat rows (list of lists) to a single data frame + cols = ['Example', 'Spacing'] + my_profiles + mean_df = pd.DataFrame(mean_list, columns=cols) + mean_df.astype(dtype=type_dict) + # save to csv at input location + mean_df.to_csv(str(df_file).replace('.csv', '_mean.csv'), index=False) + + # save to markdown for nice visualization + mean_df.to_markdown(str(df_file).replace('data_frame.csv', 'mean.md'), index=False) + + +if __name__ == "__main__": + parser = ArgumentParser(description='Run benchmarking tests') + parser.add_argument("--savefolder", default=None, type=str, help='where to save the results') + config = parser.parse_args() + + savefolder = config.savefolder + get_final_table(savefolder=savefolder) diff --git a/simpa_examples/benchmarking/performance_check.py b/simpa_examples/benchmarking/performance_check.py new file mode 100644 index 00000000..ac3bd7be --- /dev/null +++ b/simpa_examples/benchmarking/performance_check.py @@ -0,0 +1,55 @@ +# SPDX-FileCopyrightText: 2021 Division of Intelligent Medical Systems, DKFZ +# SPDX-FileCopyrightText: 2021 Janek Groehl +# SPDX-License-Identifier: MIT + +import pathlib +import os +from argparse import ArgumentParser + + +def run_benchmarking_tests(spacing=0.4, profile: str = "TIME", savefolder: str = 'default'): + """ + + :param spacing: Simulation spacing in mm + :param profile: What tag to choose to benchmark the examples + :param savefolder: where to save the results + :return: file with benchmarking data line by line + """ + spacing = float(spacing) + os.environ["SIMPA_PROFILE"] = profile + if savefolder == 'default': + savefolder = (str(pathlib.Path(__file__).parent.resolve()) + "/benchmarking_data_" + profile + "_" + + str(spacing) + ".txt") + os.environ["SIMPA_PROFILE_SAVE_FILE"] = savefolder + elif savefolder == 'print': + pass + elif len(savefolder) > 0: + os.environ["SIMPA_PROFILE_SAVE_FILE"] = savefolder+"/benchmarking_data_"+profile+"_"+str(spacing)+".txt" + + from simpa_examples.minimal_optical_simulation import run_minimal_optical_simulation + from simpa_examples.minimal_optical_simulation_uniform_cube import run_minimal_optical_simulation_uniform_cube + from simpa_examples.optical_and_acoustic_simulation import run_optical_and_acoustic_simulation + from simpa_examples.segmentation_loader import run_segmentation_loader + from simpa_examples.three_vs_two_dimensional_simulation_example import ( + run_three_vs_two_dimensional_simulation_example) + + examples = [run_minimal_optical_simulation, + run_minimal_optical_simulation_uniform_cube, + run_optical_and_acoustic_simulation, + run_segmentation_loader, + run_three_vs_two_dimensional_simulation_example + ] + + for example in examples: + example(spacing=spacing, path_manager=None, visualise=False) + + +if __name__ == "__main__": + parser = ArgumentParser(description='Run benchmarking tests') + parser.add_argument("--spacing", default=0.2, help='the voxel spacing in mm') + parser.add_argument("--profile", default="TIME", type=str, + help='the profile to run') + parser.add_argument("--savefolder", default='default', type=str, help='where to save the results') + config = parser.parse_args() + + run_benchmarking_tests(spacing=float(config.spacing), profile=config.profile, savefolder=config.savefolder) diff --git a/simpa_examples/benchmarking/run_benchmarking.sh b/simpa_examples/benchmarking/run_benchmarking.sh new file mode 100644 index 00000000..7776ffcb --- /dev/null +++ b/simpa_examples/benchmarking/run_benchmarking.sh @@ -0,0 +1,99 @@ +#!/bin/bash + +set -e + +help() { +echo "Usage: calculate benchmarking for [options]" +echo "For further details see readme" +echo "Number of examples can be selected in performance_check.py" +echo "For comparable benchmarks, please use default" +echo "Options:" +echo " -i, --init First spacing to benchmark: default = 0.2mm" +echo " -c, --cease Final spacing to benchmark: default = 0.25mm" +echo " -s, --step Step between spacings: default = 0.05mm" +echo " -f, --file Where to store the output files: default save in current directory; 'print' prints it in console" +echo " -t, --time Profile times taken: if no profile, all are set" +echo " -g, --gpu Profile GPU usage: if no profile, all are set" +echo " -m, --memory Profile memory usage: if no profile, all are set" +echo " -n, --number Number of simulations: default = 1" +echo " -h, --help Display this help message" +exit 0 +} + +start=0 +stop=0 +step=0 +number=1 +profiles=() +filename='default' + +while [ -n "$1" ]; do +case "$1" in + -i | --init) start=$2 + shift 1 + ;; + -c | --cease) stop=$2 + shift 1 + ;; + -s | --step) step=$2 + shift 1 + ;; + -f | --file) filename=$2 + shift 1 + ;; + -t | --time) profiles+=("TIME") + ;; + -g | --gpu) profiles+=("GPU_MEMORY") + ;; + -m | --memory) profiles+=("MEMORY") + ;; + -n | --number) number=$2 + shift 1 + ;; + -h | --help) help + ;; + *) echo "Option $1 not recognized" + ;; +esac +shift 1 +done + +if [ "$start" == 0 ]; then + start=0.2 +fi + +if [ "$stop" == 0 ]; then + stop=0.25 +fi + +if [ "$step" == 0 ]; then + step=0.05 +fi + +if [ ${#profiles[@]} -eq 0 ]; then + echo "WARNING: using all three profilers by default" + profiles=($"TIME") + profiles+=($"GPU_MEMORY") + profiles+=($"MEMORY") +fi + +prfs='' +for profile in "${profiles[@]}" +do + prfs+="$profile" + prfs+="%" +done + +for ((i=0; i < number; i++)) +do + for spacing in $(LC_NUMERIC=C seq $start $step $stop) + do + for profile in "${profiles[@]}" + do + python3 performance_check.py --spacing $spacing --profile $profile --savefolder $filename + done + done + python3 extract_benchmarking_data.py --start $start --stop $stop --step $step --profiles "$prfs" --savefolder $filename +done + +python3 get_final_table.py --savefolder $filename diff --git a/simpa_examples/create_a_custom_digital_device_twin.py b/simpa_examples/create_a_custom_digital_device_twin.py index 22895e3c..a1adac68 100644 --- a/simpa_examples/create_a_custom_digital_device_twin.py +++ b/simpa_examples/create_a_custom_digital_device_twin.py @@ -4,6 +4,8 @@ import simpa as sp from simpa import Tags +from simpa.log import Logger +from simpa.core.simulation_modules.reconstruction_module.reconstruction_utils import compute_image_dimensions import numpy as np # FIXME temporary workaround for newest Intel architectures import os @@ -16,19 +18,19 @@ class ExampleDeviceSlitIlluminationLinearDetector(sp.PhotoacousticDevice): """ - def __init__(self): - super().__init__() + def __init__(self, device_position_mm): + super().__init__(device_position_mm=device_position_mm) # You can choose your detection geometries from simpa/core/device_digital_twins/detection_geometries # You can choose your illumination geometries from simpa/core/device_digital_twins/illumination_geometries - self.set_detection_geometry(sp.LinearArrayDetectionGeometry()) - self.add_illumination_geometry(sp.SlitIlluminationGeometry()) + self.set_detection_geometry(sp.LinearArrayDetectionGeometry(device_position_mm=device_position_mm)) + self.add_illumination_geometry(sp.SlitIlluminationGeometry(device_position_mm=device_position_mm)) if __name__ == "__main__": - device = ExampleDeviceSlitIlluminationLinearDetector() + device = ExampleDeviceSlitIlluminationLinearDetector(device_position_mm=np.array([25, 25, 0])) settings = sp.Settings() - settings[Tags.DIM_VOLUME_X_MM] = 20 + settings[Tags.DIM_VOLUME_X_MM] = 50 settings[Tags.DIM_VOLUME_Y_MM] = 50 settings[Tags.DIM_VOLUME_Z_MM] = 20 settings[Tags.SPACING_MM] = 0.5 @@ -39,8 +41,13 @@ def __init__(self): positions = device.get_detection_geometry().get_detector_element_positions_accounting_for_device_position_mm() detector_elements = device.get_detection_geometry().get_detector_element_orientations() - positions = np.round(positions/settings[Tags.SPACING_MM]).astype(int) + positions = sp.round_x5_away_from_zero(positions/settings[Tags.SPACING_MM]) + xdim, zdim, ydim, xdim_start, xdim_end, ydim_start, ydim_end, zdim_start, zdim_end = compute_image_dimensions( + device.get_detection_geometry().field_of_view_extent_mm, settings[Tags.SPACING_MM], Logger()) + import matplotlib.pyplot as plt + from matplotlib.patches import Rectangle + plt.gca().add_patch(Rectangle((xdim_start + 50, ydim_start), xdim, -ydim, linewidth=1, edgecolor='r', facecolor='r', alpha=.5)) plt.scatter(positions[:, 0], positions[:, 2]) plt.quiver(positions[:, 0], positions[:, 2], detector_elements[:, 0], detector_elements[:, 2]) plt.show() diff --git a/simpa_examples/linear_unmixing.py b/simpa_examples/linear_unmixing.py index b1ce4c70..0a2f5015 100644 --- a/simpa_examples/linear_unmixing.py +++ b/simpa_examples/linear_unmixing.py @@ -3,168 +3,186 @@ # SPDX-License-Identifier: MIT import os - import numpy as np +from argparse import ArgumentParser import simpa as sp from simpa import Tags from simpa.visualisation.matplotlib_data_visualisation import visualise_data +from typing import Union + # FIXME temporary workaround for newest Intel architectures os.environ["KMP_DUPLICATE_LIB_OK"] = "TRUE" -# TODO: Please make sure that a valid path_config.env file is located in your home directory, or that you -# point to the correct file in the PathManager(). -path_manager = sp.PathManager() - -# set global params characterizing the simulated volume -VOLUME_TRANSDUCER_DIM_IN_MM = 75 -VOLUME_PLANAR_DIM_IN_MM = 20 -VOLUME_HEIGHT_IN_MM = 25 -SPACING = 0.25 -RANDOM_SEED = 471 -VOLUME_NAME = "LinearUnmixingExample_" + str(RANDOM_SEED) -# since we want to perform linear unmixing, the simulation pipeline should be execute for at least two wavelengths -WAVELENGTHS = [750, 800, 850] - - -def create_example_tissue(): +def run_linear_unmixing(spacing: float | int = 0.25, path_manager=None, visualise: bool = True): """ - This is a very simple example script of how to create a tissue definition. - It contains a muscular background, an epidermis layer on top of the muscles - and two blood vessels. + + :param spacing: The simulation spacing between voxels + :param path_manager: the path manager to be used, typically sp.PathManager + :param visualise: If VISUALIZE is set to True, the reconstruction result will be plotted + :return: a run through of the example """ - background_dictionary = sp.Settings() - background_dictionary[Tags.MOLECULE_COMPOSITION] = sp.TISSUE_LIBRARY.constant(1e-4, 1e-4, 0.9) - background_dictionary[Tags.STRUCTURE_TYPE] = Tags.BACKGROUND - - muscle_dictionary = sp.Settings() - muscle_dictionary[Tags.PRIORITY] = 1 - muscle_dictionary[Tags.STRUCTURE_START_MM] = [0, 0, 0] - muscle_dictionary[Tags.STRUCTURE_END_MM] = [0, 0, 100] - muscle_dictionary[Tags.MOLECULE_COMPOSITION] = sp.TISSUE_LIBRARY.muscle() - muscle_dictionary[Tags.CONSIDER_PARTIAL_VOLUME] = True - muscle_dictionary[Tags.ADHERE_TO_DEFORMATION] = True - muscle_dictionary[Tags.STRUCTURE_TYPE] = Tags.HORIZONTAL_LAYER_STRUCTURE - - vessel_1_dictionary = sp.Settings() - vessel_1_dictionary[Tags.PRIORITY] = 3 - vessel_1_dictionary[Tags.STRUCTURE_START_MM] = [VOLUME_TRANSDUCER_DIM_IN_MM/2, - 10, - 5] - vessel_1_dictionary[Tags.STRUCTURE_END_MM] = [VOLUME_TRANSDUCER_DIM_IN_MM/2, - 12, - 5] - vessel_1_dictionary[Tags.STRUCTURE_RADIUS_MM] = 3 - vessel_1_dictionary[Tags.MOLECULE_COMPOSITION] = sp.TISSUE_LIBRARY.blood(oxygenation=0.99) - vessel_1_dictionary[Tags.CONSIDER_PARTIAL_VOLUME] = True - vessel_1_dictionary[Tags.STRUCTURE_TYPE] = Tags.CIRCULAR_TUBULAR_STRUCTURE - - vessel_2_dictionary = sp.Settings() - vessel_2_dictionary[Tags.PRIORITY] = 3 - vessel_2_dictionary[Tags.STRUCTURE_START_MM] = [VOLUME_TRANSDUCER_DIM_IN_MM/3, - 10, - 5] - vessel_2_dictionary[Tags.STRUCTURE_END_MM] = [VOLUME_TRANSDUCER_DIM_IN_MM/3, - 12, - 5] - vessel_2_dictionary[Tags.STRUCTURE_RADIUS_MM] = 2 - vessel_2_dictionary[Tags.MOLECULE_COMPOSITION] = sp.TISSUE_LIBRARY.blood(oxygenation=0.75) - vessel_2_dictionary[Tags.CONSIDER_PARTIAL_VOLUME] = True - vessel_2_dictionary[Tags.STRUCTURE_TYPE] = Tags.CIRCULAR_TUBULAR_STRUCTURE - - epidermis_dictionary = sp.Settings() - epidermis_dictionary[Tags.PRIORITY] = 8 - epidermis_dictionary[Tags.STRUCTURE_START_MM] = [0, 0, 0] - epidermis_dictionary[Tags.STRUCTURE_END_MM] = [0, 0, 0.1] - epidermis_dictionary[Tags.MOLECULE_COMPOSITION] = sp.TISSUE_LIBRARY.epidermis() - epidermis_dictionary[Tags.CONSIDER_PARTIAL_VOLUME] = True - epidermis_dictionary[Tags.ADHERE_TO_DEFORMATION] = True - epidermis_dictionary[Tags.STRUCTURE_TYPE] = Tags.HORIZONTAL_LAYER_STRUCTURE - - tissue_dict = sp.Settings() - tissue_dict[Tags.BACKGROUND] = background_dictionary - tissue_dict["muscle"] = muscle_dictionary - tissue_dict["epidermis"] = epidermis_dictionary - tissue_dict["vessel_1"] = vessel_1_dictionary - tissue_dict["vessel_2"] = vessel_2_dictionary - return tissue_dict - - -# Seed the numpy random configuration prior to creating the global_settings file in -# order to ensure that the same volume is generated with the same random seed every time. -np.random.seed(RANDOM_SEED) - -# Initialize global settings and prepare for simulation pipeline including -# volume creation and optical forward simulation. -general_settings = { - # These parameters set the general properties of the simulated volume - Tags.RANDOM_SEED: RANDOM_SEED, - Tags.VOLUME_NAME: VOLUME_NAME, - Tags.SIMULATION_PATH: path_manager.get_hdf5_file_save_path(), - Tags.SPACING_MM: SPACING, - Tags.DIM_VOLUME_Z_MM: VOLUME_HEIGHT_IN_MM, - Tags.DIM_VOLUME_X_MM: VOLUME_TRANSDUCER_DIM_IN_MM, - Tags.DIM_VOLUME_Y_MM: VOLUME_PLANAR_DIM_IN_MM, - Tags.WAVELENGTHS: WAVELENGTHS, - Tags.GPU: True, - Tags.DO_FILE_COMPRESSION: True -} -settings = sp.Settings(general_settings) -settings.set_volume_creation_settings({ - Tags.SIMULATE_DEFORMED_LAYERS: True, - Tags.STRUCTURES: create_example_tissue() -}) -settings.set_optical_settings({ - Tags.OPTICAL_MODEL_NUMBER_PHOTONS: 1e7, - Tags.OPTICAL_MODEL_BINARY_PATH: path_manager.get_mcx_binary_path(), - Tags.OPTICAL_MODEL: Tags.OPTICAL_MODEL_MCX, - Tags.LASER_PULSE_ENERGY_IN_MILLIJOULE: 50 -}) - -# Set component settings for linear unmixing. -# In this example we are only interested in the chromophore concentration of oxy- and deoxyhemoglobin and the -# resulting blood oxygen saturation. We want to perform the algorithm using all three wavelengths defined above. -# Please take a look at the component for more information. -settings["linear_unmixing"] = { - Tags.DATA_FIELD: Tags.DATA_FIELD_INITIAL_PRESSURE, - Tags.WAVELENGTHS: WAVELENGTHS, - Tags.LINEAR_UNMIXING_SPECTRA: sp.get_simpa_internal_absorption_spectra_by_names( - [Tags.SIMPA_NAMED_ABSORPTION_SPECTRUM_OXYHEMOGLOBIN, Tags.SIMPA_NAMED_ABSORPTION_SPECTRUM_DEOXYHEMOGLOBIN] - ), - Tags.LINEAR_UNMIXING_COMPUTE_SO2: True, - Tags.LINEAR_UNMIXING_NON_NEGATIVE: True -} - -# Get device for simulation -device = sp.MSOTAcuityEcho(device_position_mm=np.array([VOLUME_TRANSDUCER_DIM_IN_MM/2, - VOLUME_PLANAR_DIM_IN_MM/2, - 0])) -device.update_settings_for_use_of_model_based_volume_creator(settings) - -# Run simulation pipeline for all wavelengths in Tag.WAVELENGTHS -pipeline = [ - sp.ModelBasedVolumeCreationAdapter(settings), - sp.MCXAdapter(settings), - sp.FieldOfViewCropping(settings), -] -sp.simulate(pipeline, settings, device) - -# Run linear unmixing component with above specified settings. -sp.LinearUnmixing(settings, "linear_unmixing").run() - -# Load linear unmixing result (blood oxygen saturation) and reference absorption for first wavelength. -file_path = path_manager.get_hdf5_file_save_path() + "/" + VOLUME_NAME + ".hdf5" -lu_results = sp.load_data_field(file_path, Tags.LINEAR_UNMIXING_RESULT) -sO2 = lu_results["sO2"] - -mua = sp.load_data_field(file_path, Tags.DATA_FIELD_ABSORPTION_PER_CM, wavelength=WAVELENGTHS[0]) -p0 = sp.load_data_field(file_path, Tags.DATA_FIELD_INITIAL_PRESSURE, wavelength=WAVELENGTHS[0]) -gt_oxy = sp.load_data_field(file_path, Tags.DATA_FIELD_OXYGENATION, wavelength=WAVELENGTHS[0]) - -# Visualize linear unmixing result -visualise_data(path_to_hdf5_file=path_manager.get_hdf5_file_save_path() + "/" + VOLUME_NAME + ".hdf5", - wavelength=WAVELENGTHS[0], - show_initial_pressure=True, - show_oxygenation=True, - show_linear_unmixing_sO2=True) + if path_manager is None: + path_manager = sp.PathManager() + # TODO: Please make sure that a valid path_config.env file is located in your home directory, or that you + # set global params characterizing the simulated volume + VOLUME_TRANSDUCER_DIM_IN_MM = 75 + VOLUME_PLANAR_DIM_IN_MM = 20 + VOLUME_HEIGHT_IN_MM = 25 + RANDOM_SEED = 471 + VOLUME_NAME = "LinearUnmixingExample_" + str(RANDOM_SEED) + + # since we want to perform linear unmixing, the simulation pipeline should be execute for at least two wavelengths + WAVELENGTHS = [750, 800, 850] + + def create_example_tissue(): + """ + This is a very simple example script of how to create a tissue definition. + It contains a muscular background, an epidermis layer on top of the muscles + and two blood vessels. + """ + background_dictionary = sp.Settings() + background_dictionary[Tags.MOLECULE_COMPOSITION] = sp.TISSUE_LIBRARY.constant(1e-4, 1e-4, 0.9) + background_dictionary[Tags.STRUCTURE_TYPE] = Tags.BACKGROUND + + muscle_dictionary = sp.Settings() + muscle_dictionary[Tags.PRIORITY] = 1 + muscle_dictionary[Tags.STRUCTURE_START_MM] = [0, 0, 0] + muscle_dictionary[Tags.STRUCTURE_END_MM] = [0, 0, 100] + muscle_dictionary[Tags.MOLECULE_COMPOSITION] = sp.TISSUE_LIBRARY.muscle() + muscle_dictionary[Tags.CONSIDER_PARTIAL_VOLUME] = True + muscle_dictionary[Tags.ADHERE_TO_DEFORMATION] = True + muscle_dictionary[Tags.STRUCTURE_TYPE] = Tags.HORIZONTAL_LAYER_STRUCTURE + + vessel_1_dictionary = sp.Settings() + vessel_1_dictionary[Tags.PRIORITY] = 3 + vessel_1_dictionary[Tags.STRUCTURE_START_MM] = [VOLUME_TRANSDUCER_DIM_IN_MM/2, + 10, + 5] + vessel_1_dictionary[Tags.STRUCTURE_END_MM] = [VOLUME_TRANSDUCER_DIM_IN_MM/2, + 12, + 5] + vessel_1_dictionary[Tags.STRUCTURE_RADIUS_MM] = 3 + vessel_1_dictionary[Tags.MOLECULE_COMPOSITION] = sp.TISSUE_LIBRARY.blood(oxygenation=0.99) + vessel_1_dictionary[Tags.CONSIDER_PARTIAL_VOLUME] = True + vessel_1_dictionary[Tags.STRUCTURE_TYPE] = Tags.CIRCULAR_TUBULAR_STRUCTURE + + vessel_2_dictionary = sp.Settings() + vessel_2_dictionary[Tags.PRIORITY] = 3 + vessel_2_dictionary[Tags.STRUCTURE_START_MM] = [VOLUME_TRANSDUCER_DIM_IN_MM/3, + 10, + 5] + vessel_2_dictionary[Tags.STRUCTURE_END_MM] = [VOLUME_TRANSDUCER_DIM_IN_MM/3, + 12, + 5] + vessel_2_dictionary[Tags.STRUCTURE_RADIUS_MM] = 2 + vessel_2_dictionary[Tags.MOLECULE_COMPOSITION] = sp.TISSUE_LIBRARY.blood(oxygenation=0.75) + vessel_2_dictionary[Tags.CONSIDER_PARTIAL_VOLUME] = True + vessel_2_dictionary[Tags.STRUCTURE_TYPE] = Tags.CIRCULAR_TUBULAR_STRUCTURE + + epidermis_dictionary = sp.Settings() + epidermis_dictionary[Tags.PRIORITY] = 8 + epidermis_dictionary[Tags.STRUCTURE_START_MM] = [0, 0, 0] + epidermis_dictionary[Tags.STRUCTURE_END_MM] = [0, 0, 0.1] + epidermis_dictionary[Tags.MOLECULE_COMPOSITION] = sp.TISSUE_LIBRARY.epidermis() + epidermis_dictionary[Tags.CONSIDER_PARTIAL_VOLUME] = True + epidermis_dictionary[Tags.ADHERE_TO_DEFORMATION] = True + epidermis_dictionary[Tags.STRUCTURE_TYPE] = Tags.HORIZONTAL_LAYER_STRUCTURE + + tissue_dict = sp.Settings() + tissue_dict[Tags.BACKGROUND] = background_dictionary + tissue_dict["muscle"] = muscle_dictionary + tissue_dict["epidermis"] = epidermis_dictionary + tissue_dict["vessel_1"] = vessel_1_dictionary + tissue_dict["vessel_2"] = vessel_2_dictionary + return tissue_dict + + # Seed the numpy random configuration prior to creating the global_settings file in + # order to ensure that the same volume is generated with the same random seed every time. + np.random.seed(RANDOM_SEED) + + # Initialize global settings and prepare for simulation pipeline including + # volume creation and optical forward simulation. + general_settings = { + # These parameters set the general properties of the simulated volume + Tags.RANDOM_SEED: RANDOM_SEED, + Tags.VOLUME_NAME: VOLUME_NAME, + Tags.SIMULATION_PATH: path_manager.get_hdf5_file_save_path(), + Tags.SPACING_MM: spacing, + Tags.DIM_VOLUME_Z_MM: VOLUME_HEIGHT_IN_MM, + Tags.DIM_VOLUME_X_MM: VOLUME_TRANSDUCER_DIM_IN_MM, + Tags.DIM_VOLUME_Y_MM: VOLUME_PLANAR_DIM_IN_MM, + Tags.WAVELENGTHS: WAVELENGTHS, + Tags.GPU: True, + Tags.DO_FILE_COMPRESSION: True + } + settings = sp.Settings(general_settings) + settings.set_volume_creation_settings({ + Tags.SIMULATE_DEFORMED_LAYERS: True, + Tags.STRUCTURES: create_example_tissue() + }) + settings.set_optical_settings({ + Tags.OPTICAL_MODEL_NUMBER_PHOTONS: 1e7, + Tags.OPTICAL_MODEL_BINARY_PATH: path_manager.get_mcx_binary_path(), + Tags.OPTICAL_MODEL: Tags.OPTICAL_MODEL_MCX, + Tags.LASER_PULSE_ENERGY_IN_MILLIJOULE: 50 + }) + + # Set component settings for linear unmixing. + # In this example we are only interested in the chromophore concentration of oxy- and deoxyhemoglobin and the + # resulting blood oxygen saturation. We want to perform the algorithm using all three wavelengths defined above. + # Please take a look at the component for more information. + settings["linear_unmixing"] = { + Tags.DATA_FIELD: Tags.DATA_FIELD_INITIAL_PRESSURE, + Tags.WAVELENGTHS: WAVELENGTHS, + Tags.LINEAR_UNMIXING_SPECTRA: sp.get_simpa_internal_absorption_spectra_by_names( + [Tags.SIMPA_NAMED_ABSORPTION_SPECTRUM_OXYHEMOGLOBIN, Tags.SIMPA_NAMED_ABSORPTION_SPECTRUM_DEOXYHEMOGLOBIN] + ), + Tags.LINEAR_UNMIXING_COMPUTE_SO2: True, + Tags.LINEAR_UNMIXING_NON_NEGATIVE: True + } + + # Get device for simulation + device = sp.MSOTAcuityEcho(device_position_mm=np.array([VOLUME_TRANSDUCER_DIM_IN_MM/2, + VOLUME_PLANAR_DIM_IN_MM/2, + 0])) + device.update_settings_for_use_of_model_based_volume_creator(settings) + + # Run simulation pipeline for all wavelengths in Tag.WAVELENGTHS + pipeline = [ + sp.ModelBasedAdapter(settings), + sp.MCXAdapter(settings), + sp.FieldOfViewCropping(settings), + ] + sp.simulate(pipeline, settings, device) + + # Run linear unmixing component with above specified settings. + sp.LinearUnmixing(settings, "linear_unmixing").run() + + # Load linear unmixing result (blood oxygen saturation) and reference absorption for first wavelength. + file_path = path_manager.get_hdf5_file_save_path() + "/" + VOLUME_NAME + ".hdf5" + lu_results = sp.load_data_field(file_path, Tags.LINEAR_UNMIXING_RESULT) + sO2 = lu_results["sO2"] + + mua = sp.load_data_field(file_path, Tags.DATA_FIELD_ABSORPTION_PER_CM, wavelength=WAVELENGTHS[0]) + p0 = sp.load_data_field(file_path, Tags.DATA_FIELD_INITIAL_PRESSURE, wavelength=WAVELENGTHS[0]) + gt_oxy = sp.load_data_field(file_path, Tags.DATA_FIELD_OXYGENATION, wavelength=WAVELENGTHS[0]) + + # Visualize linear unmixing result + if visualise: + visualise_data(path_to_hdf5_file=path_manager.get_hdf5_file_save_path() + "/" + VOLUME_NAME + ".hdf5", + wavelength=WAVELENGTHS[0], + show_initial_pressure=True, + show_oxygenation=True, + show_linear_unmixing_sO2=True) + + +if __name__ == "__main__": + parser = ArgumentParser(description='Run the linear unmixing example') + parser.add_argument("--spacing", default=0.2, type=Union[float, int], help='the voxel spacing in mm') + parser.add_argument("--path_manager", default=None, help='the path manager, None uses sp.PathManager') + parser.add_argument("--visualise", default=True, type=bool, help='whether to visualise the result') + config = parser.parse_args() + + run_linear_unmixing(spacing=config.spacing, path_manager=config.path_manager, visualise=config.visualise) diff --git a/simpa_examples/minimal_optical_simulation.py b/simpa_examples/minimal_optical_simulation.py index 73961f87..6f43eeb3 100644 --- a/simpa_examples/minimal_optical_simulation.py +++ b/simpa_examples/minimal_optical_simulation.py @@ -5,6 +5,8 @@ from simpa import Tags import simpa as sp import numpy as np +from simpa.utils.profiling import profile +from argparse import ArgumentParser # FIXME temporary workaround for newest Intel architectures import os @@ -12,149 +14,165 @@ # TODO: Please make sure that you have set the correct path to MCX binary and SAVE_PATH in the file path_config.env # located in the simpa_examples directory -path_manager = sp.PathManager() -VOLUME_TRANSDUCER_DIM_IN_MM = 60 -VOLUME_PLANAR_DIM_IN_MM = 30 -VOLUME_HEIGHT_IN_MM = 60 -SPACING = 0.5 -RANDOM_SEED = 471 -VOLUME_NAME = "MyVolumeName_"+str(RANDOM_SEED) -SAVE_REFLECTANCE = False -SAVE_PHOTON_DIRECTION = False -# If VISUALIZE is set to True, the simulation result will be plotted -VISUALIZE = True - - -def create_example_tissue(): - """ - This is a very simple example script of how to create a tissue definition. - It contains a muscular background, an epidermis layer on top of the muscles - and a blood vessel. +@profile +def run_minimal_optical_simulation(spacing: float | int = 0.5, path_manager=None, visualise: bool = True): """ - background_dictionary = sp.Settings() - background_dictionary[Tags.MOLECULE_COMPOSITION] = sp.TISSUE_LIBRARY.constant(1e-4, 1e-4, 0.9) - background_dictionary[Tags.STRUCTURE_TYPE] = Tags.BACKGROUND - - muscle_dictionary = sp.Settings() - muscle_dictionary[Tags.PRIORITY] = 1 - muscle_dictionary[Tags.STRUCTURE_START_MM] = [0, 0, 10] - muscle_dictionary[Tags.STRUCTURE_END_MM] = [0, 0, 100] - muscle_dictionary[Tags.MOLECULE_COMPOSITION] = sp.TISSUE_LIBRARY.muscle() - muscle_dictionary[Tags.CONSIDER_PARTIAL_VOLUME] = True - muscle_dictionary[Tags.ADHERE_TO_DEFORMATION] = True - muscle_dictionary[Tags.STRUCTURE_TYPE] = Tags.HORIZONTAL_LAYER_STRUCTURE - - vessel_1_dictionary = sp.Settings() - vessel_1_dictionary[Tags.PRIORITY] = 3 - vessel_1_dictionary[Tags.STRUCTURE_START_MM] = [VOLUME_TRANSDUCER_DIM_IN_MM/2, - 10, - VOLUME_HEIGHT_IN_MM/2] - vessel_1_dictionary[Tags.STRUCTURE_END_MM] = [VOLUME_TRANSDUCER_DIM_IN_MM/2, - 12, - VOLUME_HEIGHT_IN_MM/2] - vessel_1_dictionary[Tags.STRUCTURE_RADIUS_MM] = 3 - vessel_1_dictionary[Tags.MOLECULE_COMPOSITION] = sp.TISSUE_LIBRARY.blood() - vessel_1_dictionary[Tags.CONSIDER_PARTIAL_VOLUME] = True - vessel_1_dictionary[Tags.STRUCTURE_TYPE] = Tags.CIRCULAR_TUBULAR_STRUCTURE - - epidermis_dictionary = sp.Settings() - epidermis_dictionary[Tags.PRIORITY] = 8 - epidermis_dictionary[Tags.STRUCTURE_START_MM] = [0, 0, 9] - epidermis_dictionary[Tags.STRUCTURE_END_MM] = [0, 0, 10] - epidermis_dictionary[Tags.MOLECULE_COMPOSITION] = sp.TISSUE_LIBRARY.epidermis() - epidermis_dictionary[Tags.CONSIDER_PARTIAL_VOLUME] = True - epidermis_dictionary[Tags.ADHERE_TO_DEFORMATION] = True - epidermis_dictionary[Tags.STRUCTURE_TYPE] = Tags.HORIZONTAL_LAYER_STRUCTURE - - tissue_dict = sp.Settings() - tissue_dict[Tags.BACKGROUND] = background_dictionary - tissue_dict["muscle"] = muscle_dictionary - tissue_dict["epidermis"] = epidermis_dictionary - tissue_dict["vessel_1"] = vessel_1_dictionary - return tissue_dict - - -# Seed the numpy random configuration prior to creating the global_settings file in -# order to ensure that the same volume -# is generated with the same random seed every time. - -np.random.seed(RANDOM_SEED) - -general_settings = { - # These parameters set the general properties of the simulated volume - Tags.RANDOM_SEED: RANDOM_SEED, - Tags.VOLUME_NAME: VOLUME_NAME, - Tags.SIMULATION_PATH: path_manager.get_hdf5_file_save_path(), - Tags.SPACING_MM: SPACING, - Tags.DIM_VOLUME_Z_MM: VOLUME_HEIGHT_IN_MM, - Tags.DIM_VOLUME_X_MM: VOLUME_TRANSDUCER_DIM_IN_MM, - Tags.DIM_VOLUME_Y_MM: VOLUME_PLANAR_DIM_IN_MM, - Tags.WAVELENGTHS: [798], - Tags.DO_FILE_COMPRESSION: True -} - -settings = sp.Settings(general_settings) - -settings.set_volume_creation_settings({ - Tags.SIMULATE_DEFORMED_LAYERS: True, - Tags.STRUCTURES: create_example_tissue() -}) -settings.set_optical_settings({ - Tags.OPTICAL_MODEL_NUMBER_PHOTONS: 5e7, - Tags.OPTICAL_MODEL_BINARY_PATH: path_manager.get_mcx_binary_path(), - Tags.COMPUTE_DIFFUSE_REFLECTANCE: SAVE_REFLECTANCE, - Tags.COMPUTE_PHOTON_DIRECTION_AT_EXIT: SAVE_PHOTON_DIRECTION -}) -settings["noise_model_1"] = { - Tags.NOISE_MEAN: 1.0, - Tags.NOISE_STD: 0.1, - Tags.NOISE_MODE: Tags.NOISE_MODE_MULTIPLICATIVE, - Tags.DATA_FIELD: Tags.DATA_FIELD_INITIAL_PRESSURE, - Tags.NOISE_NON_NEGATIVITY_CONSTRAINT: True -} - -if not SAVE_REFLECTANCE and not SAVE_PHOTON_DIRECTION: - pipeline = [ - sp.ModelBasedVolumeCreationAdapter(settings), - sp.MCXAdapter(settings), - sp.GaussianNoise(settings, "noise_model_1") - ] -else: - pipeline = [ - sp.ModelBasedVolumeCreationAdapter(settings), - sp.MCXAdapterReflectance(settings), - ] - - -class ExampleDeviceSlitIlluminationLinearDetector(sp.PhotoacousticDevice): - """ - This class represents a digital twin of a PA device with a slit as illumination next to a linear detection geometry. + :param spacing: The simulation spacing between voxels + :param path_manager: the path manager to be used, typically sp.PathManager + :param visualise: If VISUALIZE is set to True, the reconstruction result will be plotted + :return: a run through of the example """ - - def __init__(self): - super().__init__(device_position_mm=np.asarray([VOLUME_TRANSDUCER_DIM_IN_MM/2, - VOLUME_PLANAR_DIM_IN_MM/2, 0])) - self.set_detection_geometry(sp.LinearArrayDetectionGeometry()) - self.add_illumination_geometry(sp.SlitIlluminationGeometry(slit_vector_mm=[20, 0, 0], - direction_vector_mm=[0, 0, 1])) - - -device = ExampleDeviceSlitIlluminationLinearDetector() - -sp.simulate(pipeline, settings, device) - -if Tags.WAVELENGTH in settings: - WAVELENGTH = settings[Tags.WAVELENGTH] -else: - WAVELENGTH = 700 - -if VISUALIZE: - sp.visualise_data(path_to_hdf5_file=path_manager.get_hdf5_file_save_path() + "/" + VOLUME_NAME + ".hdf5", - wavelength=WAVELENGTH, - show_initial_pressure=True, - show_absorption=True, - show_diffuse_reflectance=SAVE_REFLECTANCE, - log_scale=True) + if path_manager is None: + path_manager = sp.PathManager() + VOLUME_TRANSDUCER_DIM_IN_MM = 60 + VOLUME_PLANAR_DIM_IN_MM = 30 + VOLUME_HEIGHT_IN_MM = 60 + RANDOM_SEED = 471 + VOLUME_NAME = "MyVolumeName_"+str(RANDOM_SEED) + SAVE_REFLECTANCE = False + SAVE_PHOTON_DIRECTION = False + + # If VISUALIZE is set to True, the simulation result will be plotted + + def create_example_tissue(): + """ + This is a very simple example script of how to create a tissue definition. + It contains a muscular background, an epidermis layer on top of the muscles + and a blood vessel. + """ + background_dictionary = sp.Settings() + background_dictionary[Tags.MOLECULE_COMPOSITION] = sp.TISSUE_LIBRARY.constant(1e-4, 1e-4, 0.9) + background_dictionary[Tags.STRUCTURE_TYPE] = Tags.BACKGROUND + + muscle_dictionary = sp.Settings() + muscle_dictionary[Tags.PRIORITY] = 1 + muscle_dictionary[Tags.STRUCTURE_START_MM] = [0, 0, 10] + muscle_dictionary[Tags.STRUCTURE_END_MM] = [0, 0, 100] + muscle_dictionary[Tags.MOLECULE_COMPOSITION] = sp.TISSUE_LIBRARY.muscle() + muscle_dictionary[Tags.CONSIDER_PARTIAL_VOLUME] = True + muscle_dictionary[Tags.ADHERE_TO_DEFORMATION] = True + muscle_dictionary[Tags.STRUCTURE_TYPE] = Tags.HORIZONTAL_LAYER_STRUCTURE + + vessel_1_dictionary = sp.Settings() + vessel_1_dictionary[Tags.PRIORITY] = 3 + vessel_1_dictionary[Tags.STRUCTURE_START_MM] = [VOLUME_TRANSDUCER_DIM_IN_MM/2, + 10, + VOLUME_HEIGHT_IN_MM/2] + vessel_1_dictionary[Tags.STRUCTURE_END_MM] = [VOLUME_TRANSDUCER_DIM_IN_MM/2, + 12, + VOLUME_HEIGHT_IN_MM/2] + vessel_1_dictionary[Tags.STRUCTURE_RADIUS_MM] = 3 + vessel_1_dictionary[Tags.MOLECULE_COMPOSITION] = sp.TISSUE_LIBRARY.blood() + vessel_1_dictionary[Tags.CONSIDER_PARTIAL_VOLUME] = True + vessel_1_dictionary[Tags.STRUCTURE_TYPE] = Tags.CIRCULAR_TUBULAR_STRUCTURE + + epidermis_dictionary = sp.Settings() + epidermis_dictionary[Tags.PRIORITY] = 8 + epidermis_dictionary[Tags.STRUCTURE_START_MM] = [0, 0, 9] + epidermis_dictionary[Tags.STRUCTURE_END_MM] = [0, 0, 10] + epidermis_dictionary[Tags.MOLECULE_COMPOSITION] = sp.TISSUE_LIBRARY.epidermis() + epidermis_dictionary[Tags.CONSIDER_PARTIAL_VOLUME] = True + epidermis_dictionary[Tags.ADHERE_TO_DEFORMATION] = True + epidermis_dictionary[Tags.STRUCTURE_TYPE] = Tags.HORIZONTAL_LAYER_STRUCTURE + + tissue_dict = sp.Settings() + tissue_dict[Tags.BACKGROUND] = background_dictionary + tissue_dict["muscle"] = muscle_dictionary + tissue_dict["epidermis"] = epidermis_dictionary + tissue_dict["vessel_1"] = vessel_1_dictionary + return tissue_dict + + # Seed the numpy random configuration prior to creating the global_settings file in + # order to ensure that the same volume + # is generated with the same random seed every time. + + np.random.seed(RANDOM_SEED) + + general_settings = { + # These parameters set the general properties of the simulated volume + Tags.RANDOM_SEED: RANDOM_SEED, + Tags.VOLUME_NAME: VOLUME_NAME, + Tags.SIMULATION_PATH: path_manager.get_hdf5_file_save_path(), + Tags.SPACING_MM: spacing, + Tags.DIM_VOLUME_Z_MM: VOLUME_HEIGHT_IN_MM, + Tags.DIM_VOLUME_X_MM: VOLUME_TRANSDUCER_DIM_IN_MM, + Tags.DIM_VOLUME_Y_MM: VOLUME_PLANAR_DIM_IN_MM, + Tags.WAVELENGTHS: [798], + Tags.DO_FILE_COMPRESSION: True, + Tags.GPU: True + } + + settings = sp.Settings(general_settings) + + settings.set_volume_creation_settings({ + Tags.SIMULATE_DEFORMED_LAYERS: True, + Tags.STRUCTURES: create_example_tissue() + }) + settings.set_optical_settings({ + Tags.OPTICAL_MODEL_NUMBER_PHOTONS: 5e7, + Tags.OPTICAL_MODEL_BINARY_PATH: path_manager.get_mcx_binary_path(), + Tags.COMPUTE_DIFFUSE_REFLECTANCE: SAVE_REFLECTANCE, + Tags.COMPUTE_PHOTON_DIRECTION_AT_EXIT: SAVE_PHOTON_DIRECTION + }) + settings["noise_model_1"] = { + Tags.NOISE_MEAN: 1.0, + Tags.NOISE_STD: 0.1, + Tags.NOISE_MODE: Tags.NOISE_MODE_MULTIPLICATIVE, + Tags.DATA_FIELD: Tags.DATA_FIELD_INITIAL_PRESSURE, + Tags.NOISE_NON_NEGATIVITY_CONSTRAINT: True + } + + if not SAVE_REFLECTANCE and not SAVE_PHOTON_DIRECTION: + pipeline = [ + sp.ModelBasedAdapter(settings), + sp.MCXAdapter(settings), + sp.GaussianNoise(settings, "noise_model_1") + ] + else: + pipeline = [ + sp.ModelBasedAdapter(settings), + sp.MCXReflectanceAdapter(settings), + ] + + class ExampleDeviceSlitIlluminationLinearDetector(sp.PhotoacousticDevice): + """ + This class represents a digital twin of a PA device with a slit as illumination next to a linear detection geometry. + + """ + + def __init__(self): + super().__init__(device_position_mm=np.asarray([VOLUME_TRANSDUCER_DIM_IN_MM/2, + VOLUME_PLANAR_DIM_IN_MM/2, 0])) + self.set_detection_geometry(sp.LinearArrayDetectionGeometry()) + self.add_illumination_geometry(sp.SlitIlluminationGeometry(slit_vector_mm=[20, 0, 0], + direction_vector_mm=[0, 0, 1])) + + device = ExampleDeviceSlitIlluminationLinearDetector() + + sp.simulate(pipeline, settings, device) + + if Tags.WAVELENGTH in settings: + WAVELENGTH = settings[Tags.WAVELENGTH] + else: + WAVELENGTH = 700 + + if visualise: + sp.visualise_data(path_to_hdf5_file=path_manager.get_hdf5_file_save_path() + "/" + VOLUME_NAME + ".hdf5", + wavelength=WAVELENGTH, + show_initial_pressure=True, + show_absorption=True, + show_diffuse_reflectance=SAVE_REFLECTANCE, + log_scale=True) + + +if __name__ == "__main__": + parser = ArgumentParser(description='Run the minimal optical simulation example') + parser.add_argument("--spacing", default=0.2, type=float, help='the voxel spacing in mm') + parser.add_argument("--path_manager", default=None, help='the path manager, None uses sp.PathManager') + parser.add_argument("--visualise", default=True, type=bool, help='whether to visualise the result') + config = parser.parse_args() + + run_minimal_optical_simulation(spacing=config.spacing, path_manager=config.path_manager, visualise=config.visualise) diff --git a/simpa_examples/minimal_optical_simulation_heterogeneous_tissue.py b/simpa_examples/minimal_optical_simulation_heterogeneous_tissue.py new file mode 100644 index 00000000..12e240c2 --- /dev/null +++ b/simpa_examples/minimal_optical_simulation_heterogeneous_tissue.py @@ -0,0 +1,177 @@ +# SPDX-FileCopyrightText: 2021 Division of Intelligent Medical Systems, DKFZ +# SPDX-FileCopyrightText: 2021 Janek Groehl +# SPDX-License-Identifier: MIT +# +# This file should give an example how heterogeneity can be used in the SIMPA framework to model non-homogeneous +# structures. +# The result of this simulation should be a optical simulation results with spatially varying absorption coefficients +# that is induced by a heterogeneous map of blood volume fraction and oxygenation both in the simulated vessel and +# the background. + +from simpa import Tags +import simpa as sp +import numpy as np + +# FIXME temporary workaround for newest Intel architectures +import os +os.environ["KMP_DUPLICATE_LIB_OK"] = "TRUE" + +# TODO: Please make sure that a valid path_config.env file is located in your home directory, or that you +# point to the correct file in the PathManager(). +path_manager = sp.PathManager() + +VOLUME_TRANSDUCER_DIM_IN_MM = 60 +VOLUME_PLANAR_DIM_IN_MM = 30 +VOLUME_HEIGHT_IN_MM = 60 +SPACING = 0.5 +RANDOM_SEED = 471 +VOLUME_NAME = "MyVolumeName_"+str(RANDOM_SEED) +SAVE_REFLECTANCE = False +SAVE_PHOTON_DIRECTION = False + +# If VISUALIZE is set to True, the simulation result will be plotted +VISUALIZE = True + + +def create_example_tissue(settings): + """ + This is a very simple example script of how to create a tissue definition. + It contains a muscular background, an epidermis layer on top of the muscles + and a blood vessel. + """ + dim_x, dim_y, dim_z = settings.get_volume_dimensions_voxels() + tissue_library = sp.TISSUE_LIBRARY + background_dictionary = sp.Settings() + background_dictionary[Tags.MOLECULE_COMPOSITION] = tissue_library.constant(1e-4, 1e-4, 0.9) + background_dictionary[Tags.STRUCTURE_TYPE] = Tags.BACKGROUND + + muscle_dictionary = sp.Settings() + muscle_dictionary[Tags.PRIORITY] = 1 + muscle_dictionary[Tags.STRUCTURE_START_MM] = [0, 0, 10] + muscle_dictionary[Tags.STRUCTURE_END_MM] = [0, 0, 100] + muscle_dictionary[Tags.MOLECULE_COMPOSITION] = tissue_library.muscle( + oxygenation=sp.BlobHeterogeneity(dim_x, dim_y, dim_z, SPACING, + target_min=0.5, target_max=0.8).get_map(), + blood_volume_fraction=sp.BlobHeterogeneity(dim_x, dim_y, dim_z, SPACING, + target_min=0.01, target_max=0.1).get_map()) + muscle_dictionary[Tags.CONSIDER_PARTIAL_VOLUME] = True + muscle_dictionary[Tags.ADHERE_TO_DEFORMATION] = True + muscle_dictionary[Tags.STRUCTURE_TYPE] = Tags.HORIZONTAL_LAYER_STRUCTURE + + vessel_1_dictionary = sp.Settings() + vessel_1_dictionary[Tags.PRIORITY] = 3 + vessel_1_dictionary[Tags.STRUCTURE_START_MM] = [VOLUME_TRANSDUCER_DIM_IN_MM/2, + 10, + VOLUME_HEIGHT_IN_MM/2] + vessel_1_dictionary[Tags.STRUCTURE_END_MM] = [VOLUME_TRANSDUCER_DIM_IN_MM/2, + 12, + VOLUME_HEIGHT_IN_MM/2] + vessel_1_dictionary[Tags.STRUCTURE_RADIUS_MM] = 3 + vessel_1_dictionary[Tags.MOLECULE_COMPOSITION] = tissue_library.blood( + oxygenation=sp.RandomHeterogeneity(dim_x, dim_y, dim_z, SPACING, + target_min=0.9, target_max=1.0).get_map()) + vessel_1_dictionary[Tags.CONSIDER_PARTIAL_VOLUME] = True + vessel_1_dictionary[Tags.STRUCTURE_TYPE] = Tags.CIRCULAR_TUBULAR_STRUCTURE + + epidermis_dictionary = sp.Settings() + epidermis_dictionary[Tags.PRIORITY] = 8 + epidermis_dictionary[Tags.STRUCTURE_START_MM] = [0, 0, 9] + epidermis_dictionary[Tags.STRUCTURE_END_MM] = [0, 0, 10] + epidermis_dictionary[Tags.MOLECULE_COMPOSITION] = tissue_library.epidermis( + melanin_volume_fraction=sp.RandomHeterogeneity(dim_x, dim_y, dim_z, SPACING, + target_min=0.1, target_max=0.2).get_map()) + epidermis_dictionary[Tags.CONSIDER_PARTIAL_VOLUME] = True + epidermis_dictionary[Tags.ADHERE_TO_DEFORMATION] = True + epidermis_dictionary[Tags.STRUCTURE_TYPE] = Tags.HORIZONTAL_LAYER_STRUCTURE + + tissue_dict = sp.Settings() + tissue_dict[Tags.BACKGROUND] = background_dictionary + tissue_dict["muscle"] = muscle_dictionary + tissue_dict["epidermis"] = epidermis_dictionary + tissue_dict["vessel_1"] = vessel_1_dictionary + return tissue_dict + + +# Seed the numpy random configuration prior to creating the global_settings file in +# order to ensure that the same volume +# is generated with the same random seed every time. + +np.random.seed(RANDOM_SEED) + +general_settings = { + # These parameters set the general properties of the simulated volume + Tags.RANDOM_SEED: RANDOM_SEED, + Tags.VOLUME_NAME: VOLUME_NAME, + Tags.SIMULATION_PATH: path_manager.get_hdf5_file_save_path(), + Tags.SPACING_MM: SPACING, + Tags.DIM_VOLUME_Z_MM: VOLUME_HEIGHT_IN_MM, + Tags.DIM_VOLUME_X_MM: VOLUME_TRANSDUCER_DIM_IN_MM, + Tags.DIM_VOLUME_Y_MM: VOLUME_PLANAR_DIM_IN_MM, + Tags.WAVELENGTHS: [798], + Tags.DO_FILE_COMPRESSION: True +} + +settings = sp.Settings(general_settings) + +settings.set_volume_creation_settings({ + Tags.SIMULATE_DEFORMED_LAYERS: True, + Tags.STRUCTURES: create_example_tissue(settings) +}) +settings.set_optical_settings({ + Tags.OPTICAL_MODEL_NUMBER_PHOTONS: 5e7, + Tags.OPTICAL_MODEL_BINARY_PATH: path_manager.get_mcx_binary_path(), + Tags.COMPUTE_DIFFUSE_REFLECTANCE: SAVE_REFLECTANCE, + Tags.COMPUTE_PHOTON_DIRECTION_AT_EXIT: SAVE_PHOTON_DIRECTION +}) +settings["noise_model_1"] = { + Tags.NOISE_MEAN: 1.0, + Tags.NOISE_STD: 0.1, + Tags.NOISE_MODE: Tags.NOISE_MODE_MULTIPLICATIVE, + Tags.DATA_FIELD: Tags.DATA_FIELD_INITIAL_PRESSURE, + Tags.NOISE_NON_NEGATIVITY_CONSTRAINT: True +} + +if not SAVE_REFLECTANCE and not SAVE_PHOTON_DIRECTION: + pipeline = [ + sp.ModelBasedAdapter(settings), + sp.MCXAdapter(settings), + sp.GaussianNoise(settings, "noise_model_1") + ] +else: + pipeline = [ + sp.ModelBasedAdapter(settings), + sp.MCXReflectanceAdapter(settings), + ] + + +class ExampleDeviceSlitIlluminationLinearDetector(sp.PhotoacousticDevice): + """ + This class represents a digital twin of a PA device with a slit as illumination next to a linear detection geometry. + + """ + + def __init__(self): + super().__init__(device_position_mm=np.asarray([VOLUME_TRANSDUCER_DIM_IN_MM/2, + VOLUME_PLANAR_DIM_IN_MM/2, 0])) + self.set_detection_geometry(sp.LinearArrayDetectionGeometry()) + self.add_illumination_geometry(sp.SlitIlluminationGeometry(slit_vector_mm=[20, 0, 0], + direction_vector_mm=[0, 0, 1])) + + +device = ExampleDeviceSlitIlluminationLinearDetector() + +sp.simulate(pipeline, settings, device) + +if Tags.WAVELENGTH in settings: + WAVELENGTH = settings[Tags.WAVELENGTH] +else: + WAVELENGTH = 700 + +if VISUALIZE: + sp.visualise_data(path_to_hdf5_file=path_manager.get_hdf5_file_save_path() + "/" + VOLUME_NAME + ".hdf5", + wavelength=WAVELENGTH, + show_initial_pressure=True, + show_oxygenation=True, + show_absorption=True, + show_diffuse_reflectance=SAVE_REFLECTANCE, + log_scale=True) diff --git a/simpa_examples/minimal_optical_simulation_uniform_cube.py b/simpa_examples/minimal_optical_simulation_uniform_cube.py index 39f299e5..2e08796c 100644 --- a/simpa_examples/minimal_optical_simulation_uniform_cube.py +++ b/simpa_examples/minimal_optical_simulation_uniform_cube.py @@ -12,83 +12,106 @@ # FIXME temporary workaround for newest Intel architectures import os +from simpa.utils.profiling import profile +from argparse import ArgumentParser + os.environ["KMP_DUPLICATE_LIB_OK"] = "TRUE" # TODO: Please make sure that you have set the correct path to MCX binary as described in the README.md file. -path_manager = sp.PathManager() - -VOLUME_TRANSDUCER_DIM_IN_MM = 60 -VOLUME_PLANAR_DIM_IN_MM = 30 -VOLUME_HEIGHT_IN_MM = 60 -SPACING = 0.5 -RANDOM_SEED = 471 -VOLUME_NAME = "MyVolumeName_"+str(RANDOM_SEED) -SAVE_REFLECTANCE = True -SAVE_PHOTON_DIRECTION = False -# If VISUALIZE is set to True, the simulation result will be plotted -VISUALIZE = True - -def create_example_tissue(): +@profile +def run_minimal_optical_simulation_uniform_cube(spacing: float | int = 0.5, path_manager=None, + visualise: bool = True): """ - This is a very simple example script of how to create a tissue definition. - It contains only a generic background tissue material. + + :param spacing: The simulation spacing between voxels + :param path_manager: the path manager to be used, typically sp.PathManager + :param visualise: If VISUALIZE is set to True, the reconstruction result will be plotted + :return: a run through of the example """ - background_dictionary = sp.Settings() - background_dictionary[Tags.MOLECULE_COMPOSITION] = sp.TISSUE_LIBRARY.constant(1e-4, 1e-4, 0.9) - background_dictionary[Tags.STRUCTURE_TYPE] = Tags.BACKGROUND - - tissue_dict = sp.Settings() - tissue_dict[Tags.BACKGROUND] = background_dictionary - return tissue_dict - - -# Seed the numpy random configuration prior to creating the global_settings file in -# order to ensure that the same volume -# is generated with the same random seed every time. - -np.random.seed(RANDOM_SEED) - -general_settings = { - # These parameters set the general properties of the simulated volume - Tags.RANDOM_SEED: RANDOM_SEED, - Tags.VOLUME_NAME: VOLUME_NAME, - Tags.SIMULATION_PATH: path_manager.get_hdf5_file_save_path(), - Tags.SPACING_MM: SPACING, - Tags.DIM_VOLUME_Z_MM: VOLUME_HEIGHT_IN_MM, - Tags.DIM_VOLUME_X_MM: VOLUME_TRANSDUCER_DIM_IN_MM, - Tags.DIM_VOLUME_Y_MM: VOLUME_PLANAR_DIM_IN_MM, - Tags.WAVELENGTHS: [500], - Tags.DO_FILE_COMPRESSION: True -} - -settings = sp.Settings(general_settings) - -settings.set_volume_creation_settings({ - Tags.STRUCTURES: create_example_tissue() -}) -settings.set_optical_settings({ - Tags.OPTICAL_MODEL_NUMBER_PHOTONS: 5e7, - Tags.OPTICAL_MODEL_BINARY_PATH: path_manager.get_mcx_binary_path(), - Tags.COMPUTE_DIFFUSE_REFLECTANCE: SAVE_REFLECTANCE, - Tags.COMPUTE_PHOTON_DIRECTION_AT_EXIT: SAVE_PHOTON_DIRECTION -}) - -pipeline = [ - sp.ModelBasedVolumeCreationAdapter(settings), - sp.MCXAdapterReflectance(settings), -] - -device = sp.PencilBeamIlluminationGeometry(device_position_mm=np.asarray([VOLUME_TRANSDUCER_DIM_IN_MM/2, - VOLUME_PLANAR_DIM_IN_MM/2, 0])) - -sp.simulate(pipeline, settings, device) - -if VISUALIZE: - sp.visualise_data(path_to_hdf5_file=path_manager.get_hdf5_file_save_path() + "/" + VOLUME_NAME + ".hdf5", - wavelength=settings[Tags.WAVELENGTH], - show_initial_pressure=True, - show_absorption=True, - show_diffuse_reflectance=SAVE_REFLECTANCE, - log_scale=True) + if path_manager is None: + path_manager = sp.PathManager() + VOLUME_TRANSDUCER_DIM_IN_MM = 60 + VOLUME_PLANAR_DIM_IN_MM = 30 + VOLUME_HEIGHT_IN_MM = 60 + RANDOM_SEED = 471 + VOLUME_NAME = "MyVolumeName_"+str(RANDOM_SEED) + SAVE_REFLECTANCE = True + SAVE_PHOTON_DIRECTION = False + + # If VISUALIZE is set to True, the simulation result will be plotted + VISUALIZE = True + + def create_example_tissue(): + """ + This is a very simple example script of how to create a tissue definition. + It contains only a generic background tissue material. + """ + background_dictionary = sp.Settings() + background_dictionary[Tags.MOLECULE_COMPOSITION] = sp.TISSUE_LIBRARY.constant(1e-4, 1e-4, 0.9) + background_dictionary[Tags.STRUCTURE_TYPE] = Tags.BACKGROUND + + tissue_dict = sp.Settings() + tissue_dict[Tags.BACKGROUND] = background_dictionary + return tissue_dict + + # Seed the numpy random configuration prior to creating the global_settings file in + # order to ensure that the same volume + # is generated with the same random seed every time. + + np.random.seed(RANDOM_SEED) + + general_settings = { + # These parameters set the general properties of the simulated volume + Tags.RANDOM_SEED: RANDOM_SEED, + Tags.VOLUME_NAME: VOLUME_NAME, + Tags.SIMULATION_PATH: path_manager.get_hdf5_file_save_path(), + Tags.SPACING_MM: spacing, + Tags.DIM_VOLUME_Z_MM: VOLUME_HEIGHT_IN_MM, + Tags.DIM_VOLUME_X_MM: VOLUME_TRANSDUCER_DIM_IN_MM, + Tags.DIM_VOLUME_Y_MM: VOLUME_PLANAR_DIM_IN_MM, + Tags.WAVELENGTHS: [500], + Tags.DO_FILE_COMPRESSION: True + } + + settings = sp.Settings(general_settings) + + settings.set_volume_creation_settings({ + Tags.STRUCTURES: create_example_tissue() + }) + settings.set_optical_settings({ + Tags.OPTICAL_MODEL_NUMBER_PHOTONS: 5e7, + Tags.OPTICAL_MODEL_BINARY_PATH: path_manager.get_mcx_binary_path(), + Tags.COMPUTE_DIFFUSE_REFLECTANCE: SAVE_REFLECTANCE, + Tags.COMPUTE_PHOTON_DIRECTION_AT_EXIT: SAVE_PHOTON_DIRECTION + }) + + pipeline = [ + sp.ModelBasedAdapter(settings), + sp.MCXReflectanceAdapter(settings), + ] + + device = sp.PencilBeamIlluminationGeometry(device_position_mm=np.asarray([VOLUME_TRANSDUCER_DIM_IN_MM/2, + VOLUME_PLANAR_DIM_IN_MM/2, 0])) + + sp.simulate(pipeline, settings, device) + + if visualise: + sp.visualise_data(path_to_hdf5_file=path_manager.get_hdf5_file_save_path() + "/" + VOLUME_NAME + ".hdf5", + wavelength=settings[Tags.WAVELENGTH], + show_initial_pressure=True, + show_absorption=True, + show_diffuse_reflectance=SAVE_REFLECTANCE, + log_scale=True) + + +if __name__ == "__main__": + parser = ArgumentParser(description='Run the minimal optical simulation uniform cube example') + parser.add_argument("--spacing", default=0.2, type=float, help='the voxel spacing in mm') + parser.add_argument("--path_manager", default=None, help='the path manager, None uses sp.PathManager') + parser.add_argument("--visualise", default=True, type=bool, help='whether to visualise the result') + config = parser.parse_args() + + run_minimal_optical_simulation_uniform_cube(spacing=config.spacing, path_manager=config.path_manager, + visualise=config.visualise) diff --git a/simpa_examples/msot_invision_simulation.py b/simpa_examples/msot_invision_simulation.py index 7c5de16d..6676de5a 100644 --- a/simpa_examples/msot_invision_simulation.py +++ b/simpa_examples/msot_invision_simulation.py @@ -5,193 +5,213 @@ from simpa import Tags import simpa as sp import numpy as np +from simpa.utils.profiling import profile +from argparse import ArgumentParser path_manager = sp.PathManager() -SPEED_OF_SOUND = 1500 -SPACING = 0.5 -XZ_DIM = 90 -Y_DIM = 40 - - -def create_pipeline(_settings: sp.Settings): - return [ - sp.ModelBasedVolumeCreationAdapter(settings), - sp.MCXAdapter(settings), - sp.KWaveAdapter(settings), - sp.FieldOfViewCropping(settings), - sp.TimeReversalAdapter(settings) - ] - - -def get_device(): - pa_device = sp.InVision256TF(device_position_mm=np.asarray([XZ_DIM/2, Y_DIM/2, XZ_DIM/2])) - return pa_device - - -def create_volume(): - inclusion_material = sp.Molecule(volume_fraction=1.0, - anisotropy_spectrum=sp.AnisotropySpectrumLibrary.CONSTANT_ANISOTROPY_ARBITRARY( - 0.9), - scattering_spectrum=sp.AnisotropySpectrumLibrary.CONSTANT_ANISOTROPY_ARBITRARY( - 100.0), - absorption_spectrum=sp.AnisotropySpectrumLibrary.CONSTANT_ANISOTROPY_ARBITRARY( - 4.0), - speed_of_sound=SPEED_OF_SOUND, - alpha_coefficient=1e-4, - density=1000, - gruneisen_parameter=1.0, - name="Inclusion") - - phantom_material = sp.Molecule(volume_fraction=1.0, - anisotropy_spectrum=sp.AnisotropySpectrumLibrary.CONSTANT_ANISOTROPY_ARBITRARY(0.9), - scattering_spectrum=sp.AnisotropySpectrumLibrary.CONSTANT_ANISOTROPY_ARBITRARY( - 100.0), - absorption_spectrum=sp.AnisotropySpectrumLibrary.CONSTANT_ANISOTROPY_ARBITRARY(0.05), - speed_of_sound=SPEED_OF_SOUND, - alpha_coefficient=1e-4, - density=1000, - gruneisen_parameter=1.0, - name="Phantom") - - heavy_water = sp.Molecule(volume_fraction=1.0, - anisotropy_spectrum=sp.AnisotropySpectrumLibrary.CONSTANT_ANISOTROPY_ARBITRARY(1.0), - scattering_spectrum=sp.ScatteringSpectrumLibrary.CONSTANT_SCATTERING_ARBITRARY(0.1), - absorption_spectrum=sp.AbsorptionSpectrumLibrary.CONSTANT_ABSORBER_ARBITRARY(1e-30), - speed_of_sound=SPEED_OF_SOUND, - alpha_coefficient=1e-4, - density=1000, - gruneisen_parameter=1.0, - name="background_water") - - background_dictionary = sp.Settings() - background_dictionary[Tags.MOLECULE_COMPOSITION] = (sp.MolecularCompositionGenerator() - .append(heavy_water) - .get_molecular_composition(segmentation_type=-1)) - background_dictionary[Tags.STRUCTURE_TYPE] = Tags.BACKGROUND - - phantom_material_dictionary = sp.Settings() - phantom_material_dictionary[Tags.PRIORITY] = 3 - phantom_material_dictionary[Tags.STRUCTURE_START_MM] = [31, 0, 38] - phantom_material_dictionary[Tags.STRUCTURE_X_EXTENT_MM] = 28 - phantom_material_dictionary[Tags.STRUCTURE_Y_EXTENT_MM] = 40 - phantom_material_dictionary[Tags.STRUCTURE_Z_EXTENT_MM] = 14 - phantom_material_dictionary[Tags.MOLECULE_COMPOSITION] = (sp.MolecularCompositionGenerator() - .append(phantom_material) - .get_molecular_composition(segmentation_type=0)) - phantom_material_dictionary[Tags.CONSIDER_PARTIAL_VOLUME] = False - phantom_material_dictionary[Tags.STRUCTURE_TYPE] = Tags.RECTANGULAR_CUBOID_STRUCTURE - - inclusion_1_dictionary = sp.Settings() - inclusion_1_dictionary[Tags.PRIORITY] = 8 - inclusion_1_dictionary[Tags.STRUCTURE_START_MM] = [38, 10, 40] - inclusion_1_dictionary[Tags.STRUCTURE_X_EXTENT_MM] = 2 - inclusion_1_dictionary[Tags.STRUCTURE_Y_EXTENT_MM] = 20 - inclusion_1_dictionary[Tags.STRUCTURE_Z_EXTENT_MM] = 10 - inclusion_1_dictionary[Tags.MOLECULE_COMPOSITION] = (sp.MolecularCompositionGenerator() - .append(inclusion_material) - .get_molecular_composition(segmentation_type=1)) - inclusion_1_dictionary[Tags.CONSIDER_PARTIAL_VOLUME] = False - inclusion_1_dictionary[Tags.STRUCTURE_TYPE] = Tags.RECTANGULAR_CUBOID_STRUCTURE - - inclusion_2_dictionary = sp.Settings() - inclusion_2_dictionary[Tags.PRIORITY] = 5 - inclusion_2_dictionary[Tags.STRUCTURE_START_MM] = [50, 0, 43] - inclusion_2_dictionary[Tags.STRUCTURE_END_MM] = [50, 40, 43] - inclusion_2_dictionary[Tags.STRUCTURE_RADIUS_MM] = 2 - inclusion_2_dictionary[Tags.MOLECULE_COMPOSITION] = (sp.MolecularCompositionGenerator() - .append(inclusion_material) - .get_molecular_composition(segmentation_type=2)) - inclusion_2_dictionary[Tags.CONSIDER_PARTIAL_VOLUME] = False - inclusion_2_dictionary[Tags.STRUCTURE_TYPE] = Tags.CIRCULAR_TUBULAR_STRUCTURE - - tissue_dict = sp.Settings() - tissue_dict[Tags.BACKGROUND] = background_dictionary - tissue_dict["phantom"] = phantom_material_dictionary - tissue_dict["inclusion_1"] = inclusion_1_dictionary - tissue_dict["inclusion_2"] = inclusion_2_dictionary - return { - Tags.STRUCTURES: tissue_dict, - Tags.SIMULATE_DEFORMED_LAYERS: False - } - - -def get_settings(): - general_settings = { - # These parameters set the general properties of the simulated volume - Tags.RANDOM_SEED: 4711, - Tags.VOLUME_NAME: "InVision Simulation Example", - Tags.SIMULATION_PATH: path_manager.get_hdf5_file_save_path(), - Tags.SPACING_MM: SPACING, - Tags.DIM_VOLUME_Z_MM: XZ_DIM, - Tags.DIM_VOLUME_X_MM: XZ_DIM, - Tags.DIM_VOLUME_Y_MM: Y_DIM, - Tags.VOLUME_CREATOR: Tags.VOLUME_CREATOR_VERSATILE, - Tags.GPU: True, - Tags.WAVELENGTHS: [700] - } - - volume_settings = create_volume() - - optical_settings = { - Tags.OPTICAL_MODEL_NUMBER_PHOTONS: 1e7, - Tags.OPTICAL_MODEL_BINARY_PATH: path_manager.get_mcx_binary_path(), - Tags.ILLUMINATION_TYPE: Tags.ILLUMINATION_TYPE_MSOT_INVISION, - Tags.LASER_PULSE_ENERGY_IN_MILLIJOULE: 50, - } - - acoustic_settings = { - Tags.ACOUSTIC_SIMULATION_3D: True, - Tags.ACOUSTIC_MODEL_BINARY_PATH: path_manager.get_matlab_binary_path(), - Tags.KWAVE_PROPERTY_ALPHA_POWER: 0.00, - Tags.KWAVE_PROPERTY_SENSOR_RECORD: "p", - Tags.KWAVE_PROPERTY_PMLInside: False, - Tags.KWAVE_PROPERTY_PMLSize: [31, 32], - Tags.KWAVE_PROPERTY_PMLAlpha: 1.5, - Tags.KWAVE_PROPERTY_PlotPML: False, - Tags.RECORDMOVIE: False, - Tags.MOVIENAME: "visualization_log", - Tags.ACOUSTIC_LOG_SCALE: True - } - - reconstruction_settings = { - Tags.RECONSTRUCTION_PERFORM_BANDPASS_FILTERING: False, - Tags.TUKEY_WINDOW_ALPHA: 0.5, - Tags.RECONSTRUCTION_BMODE_AFTER_RECONSTRUCTION: False, - Tags.RECONSTRUCTION_BMODE_METHOD: Tags.RECONSTRUCTION_BMODE_METHOD_HILBERT_TRANSFORM, - Tags.RECONSTRUCTION_APODIZATION_METHOD: Tags.RECONSTRUCTION_APODIZATION_HAMMING, - Tags.RECONSTRUCTION_MODE: Tags.RECONSTRUCTION_MODE_PRESSURE, - Tags.DATA_FIELD_SPEED_OF_SOUND: SPEED_OF_SOUND, - Tags.KWAVE_PROPERTY_SENSOR_RECORD: "p", - Tags.KWAVE_PROPERTY_PMLInside: False, - Tags.KWAVE_PROPERTY_PMLSize: [31, 32], - Tags.KWAVE_PROPERTY_PMLAlpha: 1.5, - Tags.KWAVE_PROPERTY_PlotPML: False, - Tags.RECORDMOVIE: False, - Tags.MOVIENAME: "visualization_log", - Tags.ACOUSTIC_LOG_SCALE: True, - Tags.ACOUSTIC_MODEL_BINARY_PATH: path_manager.get_matlab_binary_path(), - Tags.KWAVE_PROPERTY_ALPHA_POWER: 0.00, - Tags.SPACING_MM: 0.25, - } - - _settings = sp.Settings(general_settings) - _settings.set_volume_creation_settings(volume_settings) - _settings.set_optical_settings(optical_settings) - _settings.set_acoustic_settings(acoustic_settings) - _settings.set_reconstruction_settings(reconstruction_settings) - return _settings - - -device = get_device() -settings = get_settings() -pipeline = create_pipeline(settings) - -sp.simulate(simulation_pipeline=pipeline, digital_device_twin=device, settings=settings) - -sp.visualise_data(settings=settings, - path_manager=path_manager, - show_absorption=True, - show_initial_pressure=True, - show_reconstructed_data=True, - show_xz_only=True) + +def run_msot_invision_simulation(spacing: float | int = 0.5, path_manager=None, visualise: bool = True): + """ + + :param spacing: The simulation spacing between voxels + :param path_manager: the path manager to be used, typically sp.PathManager + :param visualise: If VISUALIZE is set to True, the reconstruction result will be plotted + :return: a run through of the example + """ + if path_manager is None: + path_manager = sp.PathManager() + SPEED_OF_SOUND = 1500 + XZ_DIM = 90 + Y_DIM = 40 + + def create_pipeline(_settings: sp.Settings): + return [ + sp.ModelBasedAdapter(settings), + sp.MCXAdapter(settings), + sp.KWaveAdapter(settings), + sp.FieldOfViewCropping(settings), + sp.TimeReversalAdapter(settings) + ] + + def get_device(): + pa_device = sp.InVision256TF(device_position_mm=np.asarray([XZ_DIM/2, Y_DIM/2, XZ_DIM/2])) + return pa_device + + def create_volume(): + inclusion_material = sp.Molecule(volume_fraction=1.0, + anisotropy_spectrum=sp.AnisotropySpectrumLibrary.CONSTANT_ANISOTROPY_ARBITRARY( + 0.9), + scattering_spectrum=sp.AnisotropySpectrumLibrary.CONSTANT_ANISOTROPY_ARBITRARY( + 100.0), + absorption_spectrum=sp.AnisotropySpectrumLibrary.CONSTANT_ANISOTROPY_ARBITRARY( + 4.0), + speed_of_sound=SPEED_OF_SOUND, + alpha_coefficient=1e-4, + density=1000, + gruneisen_parameter=1.0, + name="Inclusion") + + phantom_material = sp.Molecule(volume_fraction=1.0, + anisotropy_spectrum=sp.AnisotropySpectrumLibrary.CONSTANT_ANISOTROPY_ARBITRARY( + 0.9), + scattering_spectrum=sp.AnisotropySpectrumLibrary.CONSTANT_ANISOTROPY_ARBITRARY( + 100.0), + absorption_spectrum=sp.AnisotropySpectrumLibrary.CONSTANT_ANISOTROPY_ARBITRARY( + 0.05), + speed_of_sound=SPEED_OF_SOUND, + alpha_coefficient=1e-4, + density=1000, + gruneisen_parameter=1.0, + name="Phantom") + + heavy_water = sp.Molecule(volume_fraction=1.0, + anisotropy_spectrum=sp.AnisotropySpectrumLibrary.CONSTANT_ANISOTROPY_ARBITRARY(1.0), + scattering_spectrum=sp.ScatteringSpectrumLibrary.CONSTANT_SCATTERING_ARBITRARY(0.1), + absorption_spectrum=sp.AbsorptionSpectrumLibrary.CONSTANT_ABSORBER_ARBITRARY(1e-30), + speed_of_sound=SPEED_OF_SOUND, + alpha_coefficient=1e-4, + density=1000, + gruneisen_parameter=1.0, + name="background_water") + + background_dictionary = sp.Settings() + background_dictionary[Tags.MOLECULE_COMPOSITION] = (sp.MolecularCompositionGenerator() + .append(heavy_water) + .get_molecular_composition(segmentation_type=-1)) + background_dictionary[Tags.STRUCTURE_TYPE] = Tags.BACKGROUND + + phantom_material_dictionary = sp.Settings() + phantom_material_dictionary[Tags.PRIORITY] = 3 + phantom_material_dictionary[Tags.STRUCTURE_START_MM] = [31, 0, 38] + phantom_material_dictionary[Tags.STRUCTURE_X_EXTENT_MM] = 28 + phantom_material_dictionary[Tags.STRUCTURE_Y_EXTENT_MM] = 40 + phantom_material_dictionary[Tags.STRUCTURE_Z_EXTENT_MM] = 14 + phantom_material_dictionary[Tags.MOLECULE_COMPOSITION] = (sp.MolecularCompositionGenerator() + .append(phantom_material) + .get_molecular_composition(segmentation_type=0)) + phantom_material_dictionary[Tags.CONSIDER_PARTIAL_VOLUME] = False + phantom_material_dictionary[Tags.STRUCTURE_TYPE] = Tags.RECTANGULAR_CUBOID_STRUCTURE + + inclusion_1_dictionary = sp.Settings() + inclusion_1_dictionary[Tags.PRIORITY] = 8 + inclusion_1_dictionary[Tags.STRUCTURE_START_MM] = [38, 10, 40] + inclusion_1_dictionary[Tags.STRUCTURE_X_EXTENT_MM] = 2 + inclusion_1_dictionary[Tags.STRUCTURE_Y_EXTENT_MM] = 20 + inclusion_1_dictionary[Tags.STRUCTURE_Z_EXTENT_MM] = 10 + inclusion_1_dictionary[Tags.MOLECULE_COMPOSITION] = (sp.MolecularCompositionGenerator() + .append(inclusion_material) + .get_molecular_composition(segmentation_type=1)) + inclusion_1_dictionary[Tags.CONSIDER_PARTIAL_VOLUME] = False + inclusion_1_dictionary[Tags.STRUCTURE_TYPE] = Tags.RECTANGULAR_CUBOID_STRUCTURE + + inclusion_2_dictionary = sp.Settings() + inclusion_2_dictionary[Tags.PRIORITY] = 5 + inclusion_2_dictionary[Tags.STRUCTURE_START_MM] = [50, 0, 43] + inclusion_2_dictionary[Tags.STRUCTURE_END_MM] = [50, 40, 43] + inclusion_2_dictionary[Tags.STRUCTURE_RADIUS_MM] = 2 + inclusion_2_dictionary[Tags.MOLECULE_COMPOSITION] = (sp.MolecularCompositionGenerator() + .append(inclusion_material) + .get_molecular_composition(segmentation_type=2)) + inclusion_2_dictionary[Tags.CONSIDER_PARTIAL_VOLUME] = False + inclusion_2_dictionary[Tags.STRUCTURE_TYPE] = Tags.CIRCULAR_TUBULAR_STRUCTURE + + tissue_dict = sp.Settings() + tissue_dict[Tags.BACKGROUND] = background_dictionary + tissue_dict["phantom"] = phantom_material_dictionary + tissue_dict["inclusion_1"] = inclusion_1_dictionary + tissue_dict["inclusion_2"] = inclusion_2_dictionary + return { + Tags.STRUCTURES: tissue_dict, + Tags.SIMULATE_DEFORMED_LAYERS: False + } + + def get_settings(): + general_settings = { + # These parameters set the general properties of the simulated volume + Tags.RANDOM_SEED: 4711, + Tags.VOLUME_NAME: "InVision Simulation Example", + Tags.SIMULATION_PATH: path_manager.get_hdf5_file_save_path(), + Tags.SPACING_MM: spacing, + Tags.DIM_VOLUME_Z_MM: XZ_DIM, + Tags.DIM_VOLUME_X_MM: XZ_DIM, + Tags.DIM_VOLUME_Y_MM: Y_DIM, + Tags.VOLUME_CREATOR: Tags.VOLUME_CREATOR_VERSATILE, + Tags.GPU: True, + Tags.WAVELENGTHS: [700] + } + + volume_settings = create_volume() + + optical_settings = { + Tags.OPTICAL_MODEL_NUMBER_PHOTONS: 1e7, + Tags.OPTICAL_MODEL_BINARY_PATH: path_manager.get_mcx_binary_path(), + Tags.ILLUMINATION_TYPE: Tags.ILLUMINATION_TYPE_MSOT_INVISION, + Tags.LASER_PULSE_ENERGY_IN_MILLIJOULE: 50, + } + + acoustic_settings = { + Tags.ACOUSTIC_SIMULATION_3D: False, + Tags.ACOUSTIC_MODEL_BINARY_PATH: path_manager.get_matlab_binary_path(), + Tags.KWAVE_PROPERTY_ALPHA_POWER: 0.00, + Tags.KWAVE_PROPERTY_SENSOR_RECORD: "p", + Tags.KWAVE_PROPERTY_PMLInside: False, + Tags.KWAVE_PROPERTY_PMLSize: [31, 32], + Tags.KWAVE_PROPERTY_PMLAlpha: 1.5, + Tags.KWAVE_PROPERTY_PlotPML: False, + Tags.RECORDMOVIE: False, + Tags.MOVIENAME: "visualization_log", + Tags.ACOUSTIC_LOG_SCALE: True + } + + reconstruction_settings = { + Tags.RECONSTRUCTION_PERFORM_BANDPASS_FILTERING: False, + Tags.TUKEY_WINDOW_ALPHA: 0.5, + Tags.RECONSTRUCTION_BMODE_AFTER_RECONSTRUCTION: False, + Tags.RECONSTRUCTION_BMODE_METHOD: Tags.RECONSTRUCTION_BMODE_METHOD_HILBERT_TRANSFORM, + Tags.RECONSTRUCTION_APODIZATION_METHOD: Tags.RECONSTRUCTION_APODIZATION_HAMMING, + Tags.RECONSTRUCTION_MODE: Tags.RECONSTRUCTION_MODE_PRESSURE, + Tags.DATA_FIELD_SPEED_OF_SOUND: SPEED_OF_SOUND, + Tags.KWAVE_PROPERTY_SENSOR_RECORD: "p", + Tags.KWAVE_PROPERTY_PMLInside: False, + Tags.KWAVE_PROPERTY_PMLSize: [31, 32], + Tags.KWAVE_PROPERTY_PMLAlpha: 1.5, + Tags.KWAVE_PROPERTY_PlotPML: False, + Tags.RECORDMOVIE: False, + Tags.MOVIENAME: "visualization_log", + Tags.ACOUSTIC_LOG_SCALE: True, + Tags.ACOUSTIC_MODEL_BINARY_PATH: path_manager.get_matlab_binary_path(), + Tags.KWAVE_PROPERTY_ALPHA_POWER: 0.00, + Tags.SPACING_MM: 0.25, + } + + _settings = sp.Settings(general_settings) + _settings.set_volume_creation_settings(volume_settings) + _settings.set_optical_settings(optical_settings) + _settings.set_acoustic_settings(acoustic_settings) + _settings.set_reconstruction_settings(reconstruction_settings) + return _settings + + device = get_device() + settings = get_settings() + pipeline = create_pipeline(settings) + + sp.simulate(simulation_pipeline=pipeline, digital_device_twin=device, settings=settings) + + if visualise: + sp.visualise_data(settings=settings, + path_manager=path_manager, + show_absorption=True, + show_initial_pressure=True, + show_reconstructed_data=True, + show_xz_only=True) + + +if __name__ == "__main__": + parser = ArgumentParser(description='Run the msot invision simulation example') + parser.add_argument("--spacing", default=0.2, type=float, help='the voxel spacing in mm') + parser.add_argument("--path_manager", default=None, help='the path manager, None uses sp.PathManager') + parser.add_argument("--visualise", default=True, type=bool, help='whether to visualise the result') + config = parser.parse_args() + + run_msot_invision_simulation(spacing=config.spacing, path_manager=config.path_manager, visualise=config.visualise) diff --git a/simpa_examples/optical_and_acoustic_simulation.py b/simpa_examples/optical_and_acoustic_simulation.py index 68bb856c..34887450 100644 --- a/simpa_examples/optical_and_acoustic_simulation.py +++ b/simpa_examples/optical_and_acoustic_simulation.py @@ -5,198 +5,219 @@ from simpa import Tags import simpa as sp import numpy as np +from simpa.utils.profiling import profile +from argparse import ArgumentParser # FIXME temporary workaround for newest Intel architectures import os os.environ["KMP_DUPLICATE_LIB_OK"] = "TRUE" -VOLUME_TRANSDUCER_DIM_IN_MM = 75 -VOLUME_PLANAR_DIM_IN_MM = 20 -VOLUME_HEIGHT_IN_MM = 25 -SPACING = 0.2 -RANDOM_SEED = 4711 - # TODO: Please make sure that a valid path_config.env file is located in your home directory, or that you # point to the correct file in the PathManager(). -path_manager = sp.PathManager() - -# If VISUALIZE is set to True, the simulation result will be plotted -VISUALIZE = True -def create_example_tissue(): +@profile +def run_optical_and_acoustic_simulation(spacing: float | int = 0.2, path_manager=None, + visualise: bool = True): """ - This is a very simple example script of how to create a tissue definition. - It contains a muscular background, an epidermis layer on top of the muscles - and a blood vessel. + + :param spacing: The simulation spacing between voxels + :param path_manager: the path manager to be used, typically sp.PathManager + :param visualise: If VISUALIZE is set to True, the reconstruction result will be plotted + :return: a run through of the example """ - background_dictionary = sp.Settings() - background_dictionary[Tags.MOLECULE_COMPOSITION] = sp.TISSUE_LIBRARY.constant(1e-10, 1e-10, 1.0) - background_dictionary[Tags.STRUCTURE_TYPE] = Tags.BACKGROUND - - tissue_dict = sp.Settings() - tissue_dict[Tags.BACKGROUND] = background_dictionary - tissue_dict["muscle"] = sp.define_horizontal_layer_structure_settings(z_start_mm=0, thickness_mm=100, - molecular_composition=sp.TISSUE_LIBRARY.constant( - 0.05, 100, 0.9), - priority=1, - consider_partial_volume=True, - adhere_to_deformation=True) - tissue_dict["epidermis"] = sp.define_horizontal_layer_structure_settings(z_start_mm=1, thickness_mm=0.1, - molecular_composition=sp.TISSUE_LIBRARY.epidermis(), - priority=8, - consider_partial_volume=True, - adhere_to_deformation=True) - tissue_dict["vessel_1"] = sp.define_circular_tubular_structure_settings( - tube_start_mm=[VOLUME_TRANSDUCER_DIM_IN_MM/2 - 10, 0, 5], - tube_end_mm=[VOLUME_TRANSDUCER_DIM_IN_MM/2 - 10, VOLUME_PLANAR_DIM_IN_MM, 5], - molecular_composition=sp.TISSUE_LIBRARY.blood(), - radius_mm=2, priority=3, consider_partial_volume=True, - adhere_to_deformation=False - ) - tissue_dict["vessel_2"] = sp.define_circular_tubular_structure_settings( - tube_start_mm=[VOLUME_TRANSDUCER_DIM_IN_MM/2, 0, 10], - tube_end_mm=[VOLUME_TRANSDUCER_DIM_IN_MM/2, VOLUME_PLANAR_DIM_IN_MM, 10], - molecular_composition=sp.TISSUE_LIBRARY.blood(), - radius_mm=3, priority=3, consider_partial_volume=True, - adhere_to_deformation=False - ) - return tissue_dict - - -# Seed the numpy random configuration prior to creating the global_settings file in -# order to ensure that the same volume -# is generated with the same random seed every time. - -np.random.seed(RANDOM_SEED) -VOLUME_NAME = "CompletePipelineTestMSOT_"+str(RANDOM_SEED) - -general_settings = { - # These parameters set the general properties of the simulated volume - Tags.RANDOM_SEED: RANDOM_SEED, - Tags.VOLUME_NAME: "CompletePipelineExample_" + str(RANDOM_SEED), - Tags.SIMULATION_PATH: path_manager.get_hdf5_file_save_path(), - Tags.SPACING_MM: SPACING, - Tags.DIM_VOLUME_Z_MM: VOLUME_HEIGHT_IN_MM, - Tags.DIM_VOLUME_X_MM: VOLUME_TRANSDUCER_DIM_IN_MM, - Tags.DIM_VOLUME_Y_MM: VOLUME_PLANAR_DIM_IN_MM, - Tags.VOLUME_CREATOR: Tags.VOLUME_CREATOR_VERSATILE, - Tags.GPU: True, - Tags.WAVELENGTHS: [700, 800], - Tags.DO_FILE_COMPRESSION: True, - Tags.DO_IPASC_EXPORT: True -} -settings = sp.Settings(general_settings) -np.random.seed(RANDOM_SEED) - -settings.set_volume_creation_settings({ - Tags.STRUCTURES: create_example_tissue(), - Tags.SIMULATE_DEFORMED_LAYERS: True -}) - -settings.set_optical_settings({ - Tags.OPTICAL_MODEL_NUMBER_PHOTONS: 1e7, - Tags.OPTICAL_MODEL_BINARY_PATH: path_manager.get_mcx_binary_path(), - Tags.ILLUMINATION_TYPE: Tags.ILLUMINATION_TYPE_MSOT_ACUITY_ECHO, - Tags.LASER_PULSE_ENERGY_IN_MILLIJOULE: 50, - Tags.MCX_ASSUMED_ANISOTROPY: 0.9, -}) - -settings.set_acoustic_settings({ - Tags.ACOUSTIC_SIMULATION_3D: False, - Tags.ACOUSTIC_MODEL_BINARY_PATH: path_manager.get_matlab_binary_path(), - Tags.KWAVE_PROPERTY_ALPHA_POWER: 0.00, - Tags.KWAVE_PROPERTY_SENSOR_RECORD: "p", - Tags.KWAVE_PROPERTY_PMLInside: False, - Tags.KWAVE_PROPERTY_PMLSize: [31, 32], - Tags.KWAVE_PROPERTY_PMLAlpha: 1.5, - Tags.KWAVE_PROPERTY_PlotPML: False, - Tags.RECORDMOVIE: False, - Tags.MOVIENAME: "visualization_log", - Tags.ACOUSTIC_LOG_SCALE: True -}) - -settings.set_reconstruction_settings({ - Tags.RECONSTRUCTION_PERFORM_BANDPASS_FILTERING: False, - Tags.ACOUSTIC_MODEL_BINARY_PATH: path_manager.get_matlab_binary_path(), - Tags.ACOUSTIC_SIMULATION_3D: False, - Tags.KWAVE_PROPERTY_ALPHA_POWER: 0.00, - Tags.TUKEY_WINDOW_ALPHA: 0.5, - Tags.BANDPASS_CUTOFF_LOWPASS_IN_HZ: int(8e6), - Tags.BANDPASS_CUTOFF_HIGHPASS_IN_HZ: int(0.1e4), - Tags.RECONSTRUCTION_BMODE_AFTER_RECONSTRUCTION: False, - Tags.RECONSTRUCTION_BMODE_METHOD: Tags.RECONSTRUCTION_BMODE_METHOD_HILBERT_TRANSFORM, - Tags.RECONSTRUCTION_APODIZATION_METHOD: Tags.RECONSTRUCTION_APODIZATION_BOX, - Tags.RECONSTRUCTION_MODE: Tags.RECONSTRUCTION_MODE_PRESSURE, - Tags.KWAVE_PROPERTY_SENSOR_RECORD: "p", - Tags.KWAVE_PROPERTY_PMLInside: False, - Tags.KWAVE_PROPERTY_PMLSize: [31, 32], - Tags.KWAVE_PROPERTY_PMLAlpha: 1.5, - Tags.KWAVE_PROPERTY_PlotPML: False, - Tags.RECORDMOVIE: False, - Tags.MOVIENAME: "visualization_log", - Tags.ACOUSTIC_LOG_SCALE: True, - Tags.DATA_FIELD_SPEED_OF_SOUND: 1540, - Tags.DATA_FIELD_ALPHA_COEFF: 0.01, - Tags.DATA_FIELD_DENSITY: 1000, - Tags.SPACING_MM: SPACING -}) - -settings["noise_initial_pressure"] = { - Tags.NOISE_MEAN: 1, - Tags.NOISE_STD: 0.01, - Tags.NOISE_MODE: Tags.NOISE_MODE_MULTIPLICATIVE, - Tags.DATA_FIELD: Tags.DATA_FIELD_INITIAL_PRESSURE, - Tags.NOISE_NON_NEGATIVITY_CONSTRAINT: True -} - -settings["noise_time_series"] = { - Tags.NOISE_STD: 1, - Tags.NOISE_MODE: Tags.NOISE_MODE_ADDITIVE, - Tags.DATA_FIELD: Tags.DATA_FIELD_TIME_SERIES_DATA -} - -# TODO: For the device choice, uncomment the undesired device - -# device = sp.MSOTAcuityEcho(device_position_mm=np.array([VOLUME_TRANSDUCER_DIM_IN_MM/2, -# VOLUME_PLANAR_DIM_IN_MM/2, -# 0])) -# device.update_settings_for_use_of_model_based_volume_creator(settings) - -device = sp.PhotoacousticDevice(device_position_mm=np.array([VOLUME_TRANSDUCER_DIM_IN_MM/2, - VOLUME_PLANAR_DIM_IN_MM/2, - 0]), - field_of_view_extent_mm=np.asarray([-15, 15, 0, 0, 0, 20])) -device.set_detection_geometry(sp.LinearArrayDetectionGeometry(device_position_mm=device.device_position_mm, - pitch_mm=0.25, - number_detector_elements=100, - field_of_view_extent_mm=np.asarray([-15, 15, 0, 0, 0, 20]))) -print(device.get_detection_geometry().get_detector_element_positions_base_mm()) -device.add_illumination_geometry(sp.SlitIlluminationGeometry(slit_vector_mm=[100, 0, 0])) - - -SIMULATION_PIPELINE = [ - sp.ModelBasedVolumeCreationAdapter(settings), - sp.MCXAdapter(settings), - sp.GaussianNoise(settings, "noise_initial_pressure"), - sp.KWaveAdapter(settings), - sp.GaussianNoise(settings, "noise_time_series"), - sp.TimeReversalAdapter(settings), - sp.FieldOfViewCropping(settings) -] - -sp.simulate(SIMULATION_PIPELINE, settings, device) - -if Tags.WAVELENGTH in settings: - WAVELENGTH = settings[Tags.WAVELENGTH] -else: - WAVELENGTH = 700 - -if VISUALIZE: - sp.visualise_data(path_to_hdf5_file=settings[Tags.SIMPA_OUTPUT_PATH], - wavelength=WAVELENGTH, - show_time_series_data=True, - show_initial_pressure=True, - show_reconstructed_data=True, - log_scale=False, - show_xz_only=False) + if path_manager is None: + path_manager = sp.PathManager() + VOLUME_TRANSDUCER_DIM_IN_MM = 75 + VOLUME_PLANAR_DIM_IN_MM = 20 + VOLUME_HEIGHT_IN_MM = 25 + RANDOM_SEED = 4711 + + # If VISUALIZE is set to True, the simulation result will be plotted + VISUALIZE = True + + def create_example_tissue(): + """ + This is a very simple example script of how to create a tissue definition. + It contains a muscular background, an epidermis layer on top of the muscles + and a blood vessel. + """ + background_dictionary = sp.Settings() + background_dictionary[Tags.MOLECULE_COMPOSITION] = sp.TISSUE_LIBRARY.constant(1e-10, 1e-10, 1.0) + background_dictionary[Tags.STRUCTURE_TYPE] = Tags.BACKGROUND + + tissue_dict = sp.Settings() + tissue_dict[Tags.BACKGROUND] = background_dictionary + tissue_dict["muscle"] = sp.define_horizontal_layer_structure_settings(z_start_mm=0, thickness_mm=100, + molecular_composition=sp.TISSUE_LIBRARY.constant( + 0.05, 100, 0.9), + priority=1, + consider_partial_volume=True, + adhere_to_deformation=True) + tissue_dict["epidermis"] = sp.define_horizontal_layer_structure_settings(z_start_mm=1, thickness_mm=0.1, + molecular_composition=sp.TISSUE_LIBRARY.epidermis(), + priority=8, + consider_partial_volume=True, + adhere_to_deformation=True) + tissue_dict["vessel_1"] = sp.define_circular_tubular_structure_settings( + tube_start_mm=[VOLUME_TRANSDUCER_DIM_IN_MM/2 - 10, 0, 5], + tube_end_mm=[VOLUME_TRANSDUCER_DIM_IN_MM/2 - 10, VOLUME_PLANAR_DIM_IN_MM, 5], + molecular_composition=sp.TISSUE_LIBRARY.blood(), + radius_mm=2, priority=3, consider_partial_volume=True, + adhere_to_deformation=False + ) + tissue_dict["vessel_2"] = sp.define_circular_tubular_structure_settings( + tube_start_mm=[VOLUME_TRANSDUCER_DIM_IN_MM/2, 0, 10], + tube_end_mm=[VOLUME_TRANSDUCER_DIM_IN_MM/2, VOLUME_PLANAR_DIM_IN_MM, 10], + molecular_composition=sp.TISSUE_LIBRARY.blood(), + radius_mm=3, priority=3, consider_partial_volume=True, + adhere_to_deformation=False + ) + return tissue_dict + + # Seed the numpy random configuration prior to creating the global_settings file in + # order to ensure that the same volume + # is generated with the same random seed every time. + + np.random.seed(RANDOM_SEED) + VOLUME_NAME = "CompletePipelineTestMSOT_"+str(RANDOM_SEED) + + general_settings = { + # These parameters set the general properties of the simulated volume + Tags.RANDOM_SEED: RANDOM_SEED, + Tags.VOLUME_NAME: "CompletePipelineExample_" + str(RANDOM_SEED), + Tags.SIMULATION_PATH: path_manager.get_hdf5_file_save_path(), + Tags.SPACING_MM: spacing, + Tags.DIM_VOLUME_Z_MM: VOLUME_HEIGHT_IN_MM, + Tags.DIM_VOLUME_X_MM: VOLUME_TRANSDUCER_DIM_IN_MM, + Tags.DIM_VOLUME_Y_MM: VOLUME_PLANAR_DIM_IN_MM, + Tags.VOLUME_CREATOR: Tags.VOLUME_CREATOR_VERSATILE, + Tags.GPU: True, + Tags.WAVELENGTHS: [700, 800], + Tags.DO_FILE_COMPRESSION: True, + Tags.DO_IPASC_EXPORT: True + } + settings = sp.Settings(general_settings) + np.random.seed(RANDOM_SEED) + + settings.set_volume_creation_settings({ + Tags.STRUCTURES: create_example_tissue(), + Tags.SIMULATE_DEFORMED_LAYERS: True + }) + + settings.set_optical_settings({ + Tags.OPTICAL_MODEL_NUMBER_PHOTONS: 1e7, + Tags.OPTICAL_MODEL_BINARY_PATH: path_manager.get_mcx_binary_path(), + Tags.ILLUMINATION_TYPE: Tags.ILLUMINATION_TYPE_MSOT_ACUITY_ECHO, + Tags.LASER_PULSE_ENERGY_IN_MILLIJOULE: 50, + Tags.MCX_ASSUMED_ANISOTROPY: 0.9, + Tags.ADDITIONAL_FLAGS: ['--printgpu'] # to print MCX GPU information + }) + + settings.set_acoustic_settings({ + Tags.ACOUSTIC_SIMULATION_3D: False, + Tags.ACOUSTIC_MODEL_BINARY_PATH: path_manager.get_matlab_binary_path(), + Tags.KWAVE_PROPERTY_ALPHA_POWER: 0.00, + Tags.KWAVE_PROPERTY_SENSOR_RECORD: "p", + Tags.KWAVE_PROPERTY_PMLInside: False, + Tags.KWAVE_PROPERTY_PMLSize: [31, 32], + Tags.KWAVE_PROPERTY_PMLAlpha: 1.5, + Tags.KWAVE_PROPERTY_PlotPML: False, + Tags.RECORDMOVIE: False, + Tags.MOVIENAME: "visualization_log", + Tags.ACOUSTIC_LOG_SCALE: True + }) + + settings.set_reconstruction_settings({ + Tags.RECONSTRUCTION_PERFORM_BANDPASS_FILTERING: False, + Tags.ACOUSTIC_MODEL_BINARY_PATH: path_manager.get_matlab_binary_path(), + Tags.ACOUSTIC_SIMULATION_3D: False, + Tags.KWAVE_PROPERTY_ALPHA_POWER: 0.00, + Tags.TUKEY_WINDOW_ALPHA: 0.5, + Tags.BANDPASS_CUTOFF_LOWPASS_IN_HZ: int(8e6), + Tags.BANDPASS_CUTOFF_HIGHPASS_IN_HZ: int(0.1e4), + Tags.RECONSTRUCTION_BMODE_AFTER_RECONSTRUCTION: False, + Tags.RECONSTRUCTION_BMODE_METHOD: Tags.RECONSTRUCTION_BMODE_METHOD_HILBERT_TRANSFORM, + Tags.RECONSTRUCTION_APODIZATION_METHOD: Tags.RECONSTRUCTION_APODIZATION_BOX, + Tags.RECONSTRUCTION_MODE: Tags.RECONSTRUCTION_MODE_PRESSURE, + Tags.KWAVE_PROPERTY_SENSOR_RECORD: "p", + Tags.KWAVE_PROPERTY_PMLInside: False, + Tags.KWAVE_PROPERTY_PMLSize: [31, 32], + Tags.KWAVE_PROPERTY_PMLAlpha: 1.5, + Tags.KWAVE_PROPERTY_PlotPML: False, + Tags.RECORDMOVIE: False, + Tags.MOVIENAME: "visualization_log", + Tags.ACOUSTIC_LOG_SCALE: True, + Tags.DATA_FIELD_SPEED_OF_SOUND: 1540, + Tags.DATA_FIELD_ALPHA_COEFF: 0.01, + Tags.DATA_FIELD_DENSITY: 1000, + Tags.SPACING_MM: spacing + }) + + settings["noise_initial_pressure"] = { + Tags.NOISE_MEAN: 1, + Tags.NOISE_STD: 0.01, + Tags.NOISE_MODE: Tags.NOISE_MODE_MULTIPLICATIVE, + Tags.DATA_FIELD: Tags.DATA_FIELD_INITIAL_PRESSURE, + Tags.NOISE_NON_NEGATIVITY_CONSTRAINT: True + } + + settings["noise_time_series"] = { + Tags.NOISE_STD: 1, + Tags.NOISE_MODE: Tags.NOISE_MODE_ADDITIVE, + Tags.DATA_FIELD: Tags.DATA_FIELD_TIME_SERIES_DATA + } + + # TODO: For the device choice, uncomment the undesired device + + # device = sp.MSOTAcuityEcho(device_position_mm=np.array([VOLUME_TRANSDUCER_DIM_IN_MM/2, + # VOLUME_PLANAR_DIM_IN_MM/2, + # 0])) + # device.update_settings_for_use_of_model_based_volume_creator(settings) + + device = sp.PhotoacousticDevice(device_position_mm=np.array([VOLUME_TRANSDUCER_DIM_IN_MM/2, + VOLUME_PLANAR_DIM_IN_MM/2, + 0]), + field_of_view_extent_mm=np.asarray([-15, 15, 0, 0, 0, 20])) + device.set_detection_geometry(sp.LinearArrayDetectionGeometry(device_position_mm=device.device_position_mm, + pitch_mm=0.25, + number_detector_elements=100, + field_of_view_extent_mm=np.asarray([-15, 15, 0, 0, 0, 20]))) + device.add_illumination_geometry(sp.SlitIlluminationGeometry(slit_vector_mm=[100, 0, 0])) + + SIMULATION_PIPELINE = [ + sp.ModelBasedAdapter(settings), + sp.MCXAdapter(settings), + sp.GaussianNoise(settings, "noise_initial_pressure"), + sp.KWaveAdapter(settings), + sp.GaussianNoise(settings, "noise_time_series"), + sp.TimeReversalAdapter(settings), + sp.FieldOfViewCropping(settings) + ] + + sp.simulate(SIMULATION_PIPELINE, settings, device) + + if Tags.WAVELENGTH in settings: + WAVELENGTH = settings[Tags.WAVELENGTH] + else: + WAVELENGTH = 700 + + if visualise: + sp.visualise_data(path_to_hdf5_file=settings[Tags.SIMPA_OUTPUT_FILE_PATH], + wavelength=WAVELENGTH, + show_time_series_data=True, + show_initial_pressure=True, + show_reconstructed_data=True, + log_scale=False, + show_xz_only=False) + + +if __name__ == "__main__": + parser = ArgumentParser(description='Run the optical and acoustic simulation example') + parser.add_argument("--spacing", default=0.2, type=float, help='the voxel spacing in mm') + parser.add_argument("--path_manager", default=None, help='the path manager, None uses sp.PathManager') + parser.add_argument("--visualise", default=True, type=bool, help='whether to visualise the result') + config = parser.parse_args() + + run_optical_and_acoustic_simulation(spacing=config.spacing, path_manager=config.path_manager, + visualise=config.visualise) diff --git a/simpa_examples/path_config.env b/simpa_examples/path_config.env deleted file mode 100644 index 8514ce81..00000000 --- a/simpa_examples/path_config.env +++ /dev/null @@ -1,7 +0,0 @@ -# Example path_config file. Please define all required paths for your simulation here. -# Afterwards, either copy this file to your current working directory, to your home directory, -# or to the SIMPA base directry. - -SAVE_PATH= /workplace/data -MCX_BINARY_PATH=/workplace/mcx # On Linux systems, the .exe at the end must be omitted. -MATLAB_BINARY_PATH=/path/to/matlab.exe # On Linux systems, the .exe at the end must be omitted. \ No newline at end of file diff --git a/simpa_examples/path_config.env.example b/simpa_examples/path_config.env.example index 23b1bd16..0a9e3d41 100644 --- a/simpa_examples/path_config.env.example +++ b/simpa_examples/path_config.env.example @@ -2,6 +2,6 @@ # Afterwards, either copy this file to your current working directory, to your home directory, # or to the SIMPA base directory, and rename it to path_config.env -SIMPA_SAVE_PATH=/workplace/data # Path to a directory where all data will be stored. This path is always required. +SIMPA_SAVE_DIRECTORY=/workplace/data # Path to a directory where all data will be stored. This path is always required. MCX_BINARY_PATH=/workplace/mcx # On Linux systems, the .exe at the end must be omitted. This path is required if you plan to run optical simulations. MATLAB_BINARY_PATH=/path/to/matlab.exe # On Linux systems, the .exe at the end must be omitted. This path is required if you plan to run acoustic simulations. diff --git a/simpa_examples/perform_image_reconstruction.py b/simpa_examples/perform_image_reconstruction.py index fb710dac..2d1fa9b2 100644 --- a/simpa_examples/perform_image_reconstruction.py +++ b/simpa_examples/perform_image_reconstruction.py @@ -9,35 +9,52 @@ import simpa as sp from simpa import Tags +from simpa.utils.profiling import profile +from typing import Union # FIXME temporary workaround for newest Intel architectures os.environ["KMP_DUPLICATE_LIB_OK"] = "TRUE" path_manager = sp.PathManager() PATH = path_manager.get_hdf5_file_save_path() + "/CompletePipelineExample_4711.hdf5" -settings = sp.load_data_field(PATH, Tags.SETTINGS) -settings[Tags.WAVELENGTH] = settings[Tags.WAVELENGTHS][0] -settings.set_reconstruction_settings({ - Tags.RECONSTRUCTION_PERFORM_BANDPASS_FILTERING: False, - Tags.TUKEY_WINDOW_ALPHA: 0.5, - Tags.BANDPASS_CUTOFF_LOWPASS_IN_HZ: int(8e6), - Tags.BANDPASS_CUTOFF_HIGHPASS_IN_HZ: int(0.1e6), - Tags.RECONSTRUCTION_BMODE_METHOD: Tags.RECONSTRUCTION_BMODE_METHOD_HILBERT_TRANSFORM, - Tags.RECONSTRUCTION_APODIZATION_METHOD: Tags.RECONSTRUCTION_APODIZATION_BOX, - Tags.RECONSTRUCTION_MODE: Tags.RECONSTRUCTION_MODE_PRESSURE, - Tags.SPACING_MM: settings[Tags.SPACING_MM] -}) +def run_perform_image_reconstruction(SPACING: Union[int, float] = 0.5, path_manager=sp.PathManager(), visualise=True): + """ -# TODO use the correct device definition here -device = sp.load_data_field(PATH, Tags.DIGITAL_DEVICE) + :param SPACING: The simulation spacing between voxels + :param path_manager: the path manager to be used, typically sp.PathManager + :param visualise: If VISUALIZE is set to True, the reconstruction result will be plotted + :return: a run through of the example + """ + PATH = path_manager.get_hdf5_file_save_path() + "/CompletePipelineExample_4711.hdf5" + settings = sp.load_data_field(PATH, Tags.SETTINGS) + settings[Tags.WAVELENGTH] = settings[Tags.WAVELENGTHS][0] -sp.DelayAndSumAdapter(settings).run(device) + settings.set_reconstruction_settings({ + Tags.RECONSTRUCTION_PERFORM_BANDPASS_FILTERING: False, + Tags.TUKEY_WINDOW_ALPHA: 0.5, + Tags.BANDPASS_CUTOFF_LOWPASS_IN_HZ: int(8e6), + Tags.BANDPASS_CUTOFF_HIGHPASS_IN_HZ: int(0.1e6), + Tags.RECONSTRUCTION_BMODE_METHOD: Tags.RECONSTRUCTION_BMODE_METHOD_HILBERT_TRANSFORM, + Tags.RECONSTRUCTION_APODIZATION_METHOD: Tags.RECONSTRUCTION_APODIZATION_BOX, + Tags.RECONSTRUCTION_MODE: Tags.RECONSTRUCTION_MODE_PRESSURE, + Tags.SPACING_MM: settings[Tags.SPACING_MM] + }) -reconstructed_image = sp.load_data_field(PATH, Tags.DATA_FIELD_RECONSTRUCTED_DATA, settings[Tags.WAVELENGTH]) -reconstructed_image = np.squeeze(reconstructed_image) + # TODO use the correct device definition here + device = sp.load_data_field(PATH, Tags.DIGITAL_DEVICE) -sp.visualise_data(path_to_hdf5_file=PATH, - wavelength=settings[Tags.WAVELENGTH], - show_reconstructed_data=True, - show_xz_only=True) + sp.DelayAndSumAdapter(settings).run(device) + + reconstructed_image = sp.load_data_field(PATH, Tags.DATA_FIELD_RECONSTRUCTED_DATA, settings[Tags.WAVELENGTH]) + reconstructed_image = np.squeeze(reconstructed_image) + + if visualise: + sp.visualise_data(path_to_hdf5_file=PATH, + wavelength=settings[Tags.WAVELENGTH], + show_reconstructed_data=True, + show_xz_only=True) + + +if __name__ == "__main__": + run_perform_image_reconstruction(SPACING=0.5, path_manager=sp.PathManager(), visualise=True) diff --git a/simpa_examples/perform_iterative_qPAI_reconstruction.py b/simpa_examples/perform_iterative_qPAI_reconstruction.py index 90d4ec93..7a299733 100644 --- a/simpa_examples/perform_iterative_qPAI_reconstruction.py +++ b/simpa_examples/perform_iterative_qPAI_reconstruction.py @@ -11,6 +11,8 @@ import simpa as sp from simpa import Tags +from simpa.utils.profiling import profile +from argparse import ArgumentParser # FIXME temporary workaround for newest Intel architectures os.environ["KMP_DUPLICATE_LIB_OK"] = "TRUE" @@ -18,218 +20,234 @@ # TODO: Please make sure that a valid path_config.env file is located in your home directory, or that you # point to the correct file in the PathManager(). -path_manager = sp.PathManager() -VOLUME_TRANSDUCER_DIM_IN_MM = 30 -VOLUME_PLANAR_DIM_IN_MM = 30 -VOLUME_HEIGHT_IN_MM = 30 -SPACING = 0.2 -RANDOM_SEED = 471 -VOLUME_NAME = "MyqPAIReconstruction_" + str(RANDOM_SEED) -# If VISUALIZE is set to True, the reconstruction result will be plotted -VISUALIZE = True - - -def create_example_tissue(): +def run_perform_iterative_qPAI_reconstruction(spacing: float | int = 0.2, path_manager=None, + visualise: bool = True): """ - This is a very simple example script of how to create a tissue definition. - It contains a muscular background, an epidermis layer on top of the muscles - and a blood vessel. + + :param spacing: The simulation spacing between voxels + :param path_manager: the path manager to be used, typically sp.PathManager + :param visualise: If VISUALIZE is set to True, the reconstruction result will be plotted + :return: a run through of the example """ - background_dictionary = sp.Settings() - background_dictionary[Tags.MOLECULE_COMPOSITION] = sp.TISSUE_LIBRARY.constant(0.05, 30, 0.9) - background_dictionary[Tags.STRUCTURE_TYPE] = Tags.BACKGROUND - - epidermis_structure = sp.Settings() - epidermis_structure[Tags.PRIORITY] = 1 - epidermis_structure[Tags.STRUCTURE_START_MM] = [0, 0, 2] - epidermis_structure[Tags.STRUCTURE_END_MM] = [0, 0, 2.5] - epidermis_structure[Tags.MOLECULE_COMPOSITION] = sp.TISSUE_LIBRARY.constant(2.2, 100.0, 0.9) - epidermis_structure[Tags.CONSIDER_PARTIAL_VOLUME] = True - epidermis_structure[Tags.ADHERE_TO_DEFORMATION] = True - epidermis_structure[Tags.STRUCTURE_TYPE] = Tags.HORIZONTAL_LAYER_STRUCTURE - - vessel_structure_1 = sp.Settings() - vessel_structure_1[Tags.PRIORITY] = 2 - vessel_structure_1[Tags.STRUCTURE_START_MM] = [VOLUME_TRANSDUCER_DIM_IN_MM / 2.5, 0, - VOLUME_HEIGHT_IN_MM / 2] - vessel_structure_1[Tags.STRUCTURE_END_MM] = [VOLUME_TRANSDUCER_DIM_IN_MM / 2.5, - VOLUME_PLANAR_DIM_IN_MM, VOLUME_HEIGHT_IN_MM / 2] - vessel_structure_1[Tags.STRUCTURE_RADIUS_MM] = 1.75 - vessel_structure_1[Tags.STRUCTURE_ECCENTRICITY] = 0.85 - vessel_structure_1[Tags.MOLECULE_COMPOSITION] = sp.TISSUE_LIBRARY.constant(5.2, 100.0, 0.9) - vessel_structure_1[Tags.CONSIDER_PARTIAL_VOLUME] = True - vessel_structure_1[Tags.ADHERE_TO_DEFORMATION] = True - vessel_structure_1[Tags.STRUCTURE_TYPE] = Tags.ELLIPTICAL_TUBULAR_STRUCTURE - - vessel_structure_2 = sp.Settings() - vessel_structure_2[Tags.PRIORITY] = 3 - vessel_structure_2[Tags.STRUCTURE_START_MM] = [VOLUME_TRANSDUCER_DIM_IN_MM / 2, 0, - VOLUME_HEIGHT_IN_MM / 3] - vessel_structure_2[Tags.STRUCTURE_END_MM] = [VOLUME_TRANSDUCER_DIM_IN_MM / 2, - VOLUME_PLANAR_DIM_IN_MM, VOLUME_HEIGHT_IN_MM / 3] - vessel_structure_2[Tags.STRUCTURE_RADIUS_MM] = 0.75 - vessel_structure_2[Tags.MOLECULE_COMPOSITION] = sp.TISSUE_LIBRARY.constant(3.0, 100.0, 0.9) - vessel_structure_2[Tags.CONSIDER_PARTIAL_VOLUME] = True - vessel_structure_2[Tags.STRUCTURE_TYPE] = Tags.CIRCULAR_TUBULAR_STRUCTURE - - tissue_dict = sp.Settings() - tissue_dict[Tags.BACKGROUND] = background_dictionary - tissue_dict["epidermis"] = epidermis_structure - tissue_dict["vessel_1"] = vessel_structure_1 - tissue_dict["vessel_2"] = vessel_structure_2 - return tissue_dict - - -# set settings for volume creation, optical simulation and iterative qPAI method -np.random.seed(RANDOM_SEED) - -general_settings = { - # These parameters set the general properties of the simulated volume - Tags.RANDOM_SEED: RANDOM_SEED, - Tags.VOLUME_NAME: VOLUME_NAME, - Tags.SIMULATION_PATH: path_manager.get_hdf5_file_save_path(), - Tags.SPACING_MM: SPACING, - Tags.DIM_VOLUME_Z_MM: VOLUME_HEIGHT_IN_MM, - Tags.DIM_VOLUME_X_MM: VOLUME_TRANSDUCER_DIM_IN_MM, - Tags.DIM_VOLUME_Y_MM: VOLUME_PLANAR_DIM_IN_MM, - Tags.WAVELENGTHS: [700] -} - -settings = sp.Settings(general_settings) - -settings.set_volume_creation_settings({ - # These parameters set the properties for the volume creation - Tags.SIMULATE_DEFORMED_LAYERS: True, - Tags.STRUCTURES: create_example_tissue() -}) -settings.set_optical_settings({ - # These parameters set the properties for the optical Monte Carlo simulation - Tags.OPTICAL_MODEL_NUMBER_PHOTONS: 1e7, - Tags.OPTICAL_MODEL_BINARY_PATH: path_manager.get_mcx_binary_path(), - Tags.OPTICAL_MODEL: Tags.OPTICAL_MODEL_MCX, - Tags.LASER_PULSE_ENERGY_IN_MILLIJOULE: 50 -}) -settings["noise_model"] = { - Tags.NOISE_MEAN: 1.0, - Tags.NOISE_STD: 0.01, - Tags.NOISE_MODE: Tags.NOISE_MODE_MULTIPLICATIVE, - Tags.DATA_FIELD: Tags.DATA_FIELD_INITIAL_PRESSURE, - Tags.NOISE_NON_NEGATIVITY_CONSTRAINT: True -} -settings["iterative_qpai_reconstruction"] = { - # These parameters set the properties of the iterative reconstruction - Tags.DOWNSCALE_FACTOR: 0.75, - Tags.ITERATIVE_RECONSTRUCTION_CONSTANT_REGULARIZATION: False, - # the following tag has no effect, since the regularization is chosen to be SNR dependent, not constant - Tags.ITERATIVE_RECONSTRUCTION_REGULARIZATION_SIGMA: 0.01, - Tags.ITERATIVE_RECONSTRUCTION_MAX_ITERATION_NUMBER: 20, - # for this example, we are not interested in all absorption updates - Tags.ITERATIVE_RECONSTRUCTION_SAVE_INTERMEDIATE_RESULTS: False, - Tags.ITERATIVE_RECONSTRUCTION_STOPPING_LEVEL: 1e-3 -} - -# run pipeline including iterative qPAI method -pipeline = [ - sp.ModelBasedVolumeCreationAdapter(settings), - sp.MCXAdapter(settings), - sp.GaussianNoise(settings, "noise_model"), - sp.IterativeqPAI(settings, "iterative_qpai_reconstruction") -] - - -class CustomDevice(sp.PhotoacousticDevice): - - def __init__(self): - super(CustomDevice, self).__init__(device_position_mm=np.asarray([general_settings[Tags.DIM_VOLUME_X_MM] / 2, - general_settings[Tags.DIM_VOLUME_Y_MM] / 2, - 0])) - self.add_illumination_geometry(sp.DiskIlluminationGeometry(beam_radius_mm=20)) - - -device = CustomDevice() - -device.update_settings_for_use_of_model_based_volume_creator(settings) - -sp.simulate(pipeline, settings, device) - -# visualize reconstruction results -if VISUALIZE: - # get simulation output - data_path = path_manager.get_hdf5_file_save_path() + "/" + VOLUME_NAME + ".hdf5" - settings = sp.load_data_field(data_path, Tags.SETTINGS) - wavelength = settings[Tags.WAVELENGTHS][0] - - # get reconstruction result - absorption_reconstruction = sp.load_data_field(data_path, Tags.ITERATIVE_qPAI_RESULT, wavelength) - - # get ground truth absorption coefficients - absorption_gt = sp.load_data_field(data_path, Tags.DATA_FIELD_ABSORPTION_PER_CM, wavelength) - - # rescale ground truth to same dimension as reconstruction (necessary due to resampling in iterative algorithm) - scale = np.shape(absorption_reconstruction)[0] / np.shape(absorption_gt)[0] # same as Tags.DOWNSCALE_FACTOR - absorption_gt = zoom(absorption_gt, scale, order=1, mode="nearest") - - # compute reconstruction error - difference = absorption_gt - absorption_reconstruction - - median_error = np.median(difference) - q3, q1 = np.percentile(difference, [75, 25]) - iqr = q3 - q1 - - # visualize results - x_pos = int(np.shape(absorption_gt)[0] / 2) - y_pos = int(np.shape(absorption_gt)[1] / 2) - - if np.min(absorption_gt) > np.min(absorption_reconstruction): - cmin = np.min(absorption_reconstruction) - else: - cmin = np.min(absorption_gt) - - if np.max(absorption_gt) > np.max(absorption_reconstruction): - cmax = np.max(absorption_gt) - else: - cmax = np.max(absorption_reconstruction) - - results_x_z = [absorption_gt[:, y_pos, :], absorption_reconstruction[:, y_pos, :], difference[:, y_pos, :]] - results_y_z = [absorption_gt[x_pos, :, :], absorption_reconstruction[x_pos, :, :], difference[x_pos, :, :]] - - label = ["Absorption coefficients: ${\mu_a}^{gt}$", "Reconstruction: ${\mu_a}^{reconstr.}$", - "Difference: ${\mu_a}^{gt} - {\mu_a}^{reconstr.}$"] - - plt.figure(figsize=(20, 15)) - plt.subplots_adjust(hspace=0.5) - plt.suptitle("Iterative qPAI Reconstruction \n median error = " + str(np.round(median_error, 4)) + - "\n IQR = " + str(np.round(iqr, 4)), fontsize=10) - - for i, quantity in enumerate(results_x_z): - plt.subplot(2, len(results_x_z), i + 1) - if i == 0: - plt.ylabel("x-z", fontsize=10) - plt.title(label[i], fontsize=10) - plt.imshow(quantity.T) - plt.xticks(fontsize=6) - plt.yticks(fontsize=6) - plt.colorbar() - if i != 2: - plt.clim(cmin, cmax) + if path_manager is None: + path_manager = sp.PathManager() + VOLUME_TRANSDUCER_DIM_IN_MM = 30 + VOLUME_PLANAR_DIM_IN_MM = 30 + VOLUME_HEIGHT_IN_MM = 30 + RANDOM_SEED = 471 + VOLUME_NAME = "MyqPAIReconstruction_" + str(RANDOM_SEED) + + # If VISUALIZE is set to True, the reconstruction result will be plotted + + def create_example_tissue(): + """ + This is a very simple example script of how to create a tissue definition. + It contains a muscular background, an epidermis layer on top of the muscles + and a blood vessel. + """ + background_dictionary = sp.Settings() + background_dictionary[Tags.MOLECULE_COMPOSITION] = sp.TISSUE_LIBRARY.constant(0.05, 30, 0.9) + background_dictionary[Tags.STRUCTURE_TYPE] = Tags.BACKGROUND + + epidermis_structure = sp.Settings() + epidermis_structure[Tags.PRIORITY] = 1 + epidermis_structure[Tags.STRUCTURE_START_MM] = [0, 0, 2] + epidermis_structure[Tags.STRUCTURE_END_MM] = [0, 0, 2.5] + epidermis_structure[Tags.MOLECULE_COMPOSITION] = sp.TISSUE_LIBRARY.constant(2.2, 100.0, 0.9) + epidermis_structure[Tags.CONSIDER_PARTIAL_VOLUME] = True + epidermis_structure[Tags.ADHERE_TO_DEFORMATION] = True + epidermis_structure[Tags.STRUCTURE_TYPE] = Tags.HORIZONTAL_LAYER_STRUCTURE + + vessel_structure_1 = sp.Settings() + vessel_structure_1[Tags.PRIORITY] = 2 + vessel_structure_1[Tags.STRUCTURE_START_MM] = [VOLUME_TRANSDUCER_DIM_IN_MM / 2.5, 0, + VOLUME_HEIGHT_IN_MM / 2] + vessel_structure_1[Tags.STRUCTURE_END_MM] = [VOLUME_TRANSDUCER_DIM_IN_MM / 2.5, + VOLUME_PLANAR_DIM_IN_MM, VOLUME_HEIGHT_IN_MM / 2] + vessel_structure_1[Tags.STRUCTURE_RADIUS_MM] = 1.75 + vessel_structure_1[Tags.STRUCTURE_ECCENTRICITY] = 0.85 + vessel_structure_1[Tags.MOLECULE_COMPOSITION] = sp.TISSUE_LIBRARY.constant(5.2, 100.0, 0.9) + vessel_structure_1[Tags.CONSIDER_PARTIAL_VOLUME] = True + vessel_structure_1[Tags.ADHERE_TO_DEFORMATION] = True + vessel_structure_1[Tags.STRUCTURE_TYPE] = Tags.ELLIPTICAL_TUBULAR_STRUCTURE + + vessel_structure_2 = sp.Settings() + vessel_structure_2[Tags.PRIORITY] = 3 + vessel_structure_2[Tags.STRUCTURE_START_MM] = [VOLUME_TRANSDUCER_DIM_IN_MM / 2, 0, + VOLUME_HEIGHT_IN_MM / 3] + vessel_structure_2[Tags.STRUCTURE_END_MM] = [VOLUME_TRANSDUCER_DIM_IN_MM / 2, + VOLUME_PLANAR_DIM_IN_MM, VOLUME_HEIGHT_IN_MM / 3] + vessel_structure_2[Tags.STRUCTURE_RADIUS_MM] = 0.75 + vessel_structure_2[Tags.MOLECULE_COMPOSITION] = sp.TISSUE_LIBRARY.constant(3.0, 100.0, 0.9) + vessel_structure_2[Tags.CONSIDER_PARTIAL_VOLUME] = True + vessel_structure_2[Tags.STRUCTURE_TYPE] = Tags.CIRCULAR_TUBULAR_STRUCTURE + + tissue_dict = sp.Settings() + tissue_dict[Tags.BACKGROUND] = background_dictionary + tissue_dict["epidermis"] = epidermis_structure + tissue_dict["vessel_1"] = vessel_structure_1 + tissue_dict["vessel_2"] = vessel_structure_2 + return tissue_dict + + # set settings for volume creation, optical simulation and iterative qPAI method + np.random.seed(RANDOM_SEED) + + general_settings = { + # These parameters set the general properties of the simulated volume + Tags.RANDOM_SEED: RANDOM_SEED, + Tags.VOLUME_NAME: VOLUME_NAME, + Tags.SIMULATION_PATH: path_manager.get_hdf5_file_save_path(), + Tags.SPACING_MM: spacing, + Tags.DIM_VOLUME_Z_MM: VOLUME_HEIGHT_IN_MM, + Tags.DIM_VOLUME_X_MM: VOLUME_TRANSDUCER_DIM_IN_MM, + Tags.DIM_VOLUME_Y_MM: VOLUME_PLANAR_DIM_IN_MM, + Tags.WAVELENGTHS: [700] + } + + settings = sp.Settings(general_settings) + + settings.set_volume_creation_settings({ + # These parameters set the properties for the volume creation + Tags.SIMULATE_DEFORMED_LAYERS: True, + Tags.STRUCTURES: create_example_tissue() + }) + settings.set_optical_settings({ + # These parameters set the properties for the optical Monte Carlo simulation + Tags.OPTICAL_MODEL_NUMBER_PHOTONS: 1e7, + Tags.OPTICAL_MODEL_BINARY_PATH: path_manager.get_mcx_binary_path(), + Tags.OPTICAL_MODEL: Tags.OPTICAL_MODEL_MCX, + Tags.LASER_PULSE_ENERGY_IN_MILLIJOULE: 50 + }) + settings["noise_model"] = { + Tags.NOISE_MEAN: 1.0, + Tags.NOISE_STD: 0.01, + Tags.NOISE_MODE: Tags.NOISE_MODE_MULTIPLICATIVE, + Tags.DATA_FIELD: Tags.DATA_FIELD_INITIAL_PRESSURE, + Tags.NOISE_NON_NEGATIVITY_CONSTRAINT: True + } + settings["iterative_qpai_reconstruction"] = { + # These parameters set the properties of the iterative reconstruction + Tags.DOWNSCALE_FACTOR: 0.75, + Tags.ITERATIVE_RECONSTRUCTION_CONSTANT_REGULARIZATION: False, + # the following tag has no effect, since the regularization is chosen to be SNR dependent, not constant + Tags.ITERATIVE_RECONSTRUCTION_REGULARIZATION_SIGMA: 0.01, + Tags.ITERATIVE_RECONSTRUCTION_MAX_ITERATION_NUMBER: 20, + # for this example, we are not interested in all absorption updates + Tags.ITERATIVE_RECONSTRUCTION_SAVE_INTERMEDIATE_RESULTS: False, + Tags.ITERATIVE_RECONSTRUCTION_STOPPING_LEVEL: 1e-3 + } + + # run pipeline including iterative qPAI method + pipeline = [ + sp.ModelBasedAdapter(settings), + sp.MCXAdapter(settings), + sp.GaussianNoise(settings, "noise_model"), + sp.IterativeqPAI(settings, "iterative_qpai_reconstruction") + ] + + class CustomDevice(sp.PhotoacousticDevice): + + def __init__(self): + super(CustomDevice, self).__init__(device_position_mm=np.asarray([general_settings[Tags.DIM_VOLUME_X_MM] / 2, + general_settings[Tags.DIM_VOLUME_Y_MM] / 2, + 0])) + self.add_illumination_geometry(sp.DiskIlluminationGeometry(beam_radius_mm=20)) + + device = CustomDevice() + + device.update_settings_for_use_of_model_based_volume_creator(settings) + + sp.simulate(pipeline, settings, device) + + # visualize reconstruction results + if visualise: + # get simulation output + data_path = path_manager.get_hdf5_file_save_path() + "/" + VOLUME_NAME + ".hdf5" + settings = sp.load_data_field(data_path, Tags.SETTINGS) + wavelength = settings[Tags.WAVELENGTHS][0] + + # get reconstruction result + absorption_reconstruction = sp.load_data_field(data_path, Tags.ITERATIVE_qPAI_RESULT, wavelength) + + # get ground truth absorption coefficients + absorption_gt = sp.load_data_field(data_path, Tags.DATA_FIELD_ABSORPTION_PER_CM, wavelength) + + # rescale ground truth to same dimension as reconstruction (necessary due to resampling in iterative algorithm) + scale = np.shape(absorption_reconstruction)[0] / np.shape(absorption_gt)[0] # same as Tags.DOWNSCALE_FACTOR + absorption_gt = zoom(absorption_gt, scale, order=1, mode="nearest") + + # compute reconstruction error + difference = absorption_gt - absorption_reconstruction + + median_error = np.median(difference) + q3, q1 = np.percentile(difference, [75, 25]) + iqr = q3 - q1 + + # visualize results + x_pos = int(np.shape(absorption_gt)[0] / 2) + y_pos = int(np.shape(absorption_gt)[1] / 2) + + if np.min(absorption_gt) > np.min(absorption_reconstruction): + cmin = np.min(absorption_reconstruction) else: - plt.clim(np.min(difference), np.max(difference)) - - for i, quantity in enumerate(results_y_z): - plt.subplot(2, len(results_x_z), i + len(results_x_z) + 1) - if i == 0: - plt.ylabel("y-z", fontsize=10) - plt.title(label[i], fontsize=10) - plt.imshow(quantity.T) - plt.xticks(fontsize=6) - plt.yticks(fontsize=6) - plt.colorbar() - if i != 2: - plt.clim(cmin, cmax) - else: - plt.clim(np.min(difference), np.max(difference)) + cmin = np.min(absorption_gt) - plt.show() - plt.close() + if np.max(absorption_gt) > np.max(absorption_reconstruction): + cmax = np.max(absorption_gt) + else: + cmax = np.max(absorption_reconstruction) + + results_x_z = [absorption_gt[:, y_pos, :], absorption_reconstruction[:, y_pos, :], difference[:, y_pos, :]] + results_y_z = [absorption_gt[x_pos, :, :], absorption_reconstruction[x_pos, :, :], difference[x_pos, :, :]] + + label = ["Absorption coefficients: ${\mu_a}^{gt}$", "Reconstruction: ${\mu_a}^{reconstr.}$", + "Difference: ${\mu_a}^{gt} - {\mu_a}^{reconstr.}$"] + + plt.figure(figsize=(20, 15)) + plt.subplots_adjust(hspace=0.5) + plt.suptitle("Iterative qPAI Reconstruction \n median error = " + str(np.round(median_error, 4)) + + "\n IQR = " + str(np.round(iqr, 4)), fontsize=10) + + for i, quantity in enumerate(results_x_z): + plt.subplot(2, len(results_x_z), i + 1) + if i == 0: + plt.ylabel("x-z", fontsize=10) + plt.title(label[i], fontsize=10) + plt.imshow(quantity.T) + plt.xticks(fontsize=6) + plt.yticks(fontsize=6) + plt.colorbar() + if i != 2: + plt.clim(cmin, cmax) + else: + plt.clim(np.min(difference), np.max(difference)) + + for i, quantity in enumerate(results_y_z): + plt.subplot(2, len(results_x_z), i + len(results_x_z) + 1) + if i == 0: + plt.ylabel("y-z", fontsize=10) + plt.title(label[i], fontsize=10) + plt.imshow(quantity.T) + plt.xticks(fontsize=6) + plt.yticks(fontsize=6) + plt.colorbar() + if i != 2: + plt.clim(cmin, cmax) + else: + plt.clim(np.min(difference), np.max(difference)) + + plt.show() + plt.close() + + +if __name__ == "__main__": + parser = ArgumentParser(description='Run the iterative qPAI reconstruction example') + parser.add_argument("--spacing", default=0.2, type=float, help='the voxel spacing in mm') + parser.add_argument("--path_manager", default=None, help='the path manager, None uses sp.PathManager') + parser.add_argument("--visualise", default=True, type=bool, help='whether to visualise the result') + config = parser.parse_args() + + run_perform_iterative_qPAI_reconstruction(spacing=config.spacing, path_manager=config.path_manager, + visualise=config.visualise) diff --git a/simpa_examples/segmentation_loader.py b/simpa_examples/segmentation_loader.py index 09fe493e..a4d9cd39 100644 --- a/simpa_examples/segmentation_loader.py +++ b/simpa_examples/segmentation_loader.py @@ -7,89 +7,110 @@ import numpy as np from skimage.data import shepp_logan_phantom from scipy.ndimage import zoom +from skimage.transform import resize # FIXME temporary workaround for newest Intel architectures import os +from argparse import ArgumentParser +from simpa.utils.profiling import profile os.environ["KMP_DUPLICATE_LIB_OK"] = "TRUE" -# If VISUALIZE is set to True, the simulation result will be plotted -VISUALIZE = True - # TODO: Please make sure that a valid path_config.env file is located in your home directory, or that you # point to the correct file in the PathManager(). -path_manager = sp.PathManager() - -target_spacing = 1.0 - -label_mask = shepp_logan_phantom() - -label_mask = np.digitize(label_mask, bins=np.linspace(0.0, 1.0, 11), right=True) - -label_mask = np.reshape(label_mask, (400, 1, 400)) - -input_spacing = 0.2 -segmentation_volume_tiled = np.tile(label_mask, (1, 128, 1)) -segmentation_volume_mask = np.round(zoom(segmentation_volume_tiled, input_spacing/target_spacing, - order=0)).astype(int) - - -def segmentation_class_mapping(): - ret_dict = dict() - ret_dict[0] = sp.TISSUE_LIBRARY.heavy_water() - ret_dict[1] = sp.TISSUE_LIBRARY.blood() - ret_dict[2] = sp.TISSUE_LIBRARY.epidermis() - ret_dict[3] = sp.TISSUE_LIBRARY.muscle() - ret_dict[4] = sp.TISSUE_LIBRARY.mediprene() - ret_dict[5] = sp.TISSUE_LIBRARY.ultrasound_gel() - ret_dict[6] = sp.TISSUE_LIBRARY.heavy_water() - ret_dict[7] = (sp.MolecularCompositionGenerator() - .append(sp.MOLECULE_LIBRARY.oxyhemoglobin(0.01)) - .append(sp.MOLECULE_LIBRARY.deoxyhemoglobin(0.01)) - .append(sp.MOLECULE_LIBRARY.water(0.98)) - .get_molecular_composition(sp.SegmentationClasses.COUPLING_ARTIFACT)) - ret_dict[8] = sp.TISSUE_LIBRARY.heavy_water() - ret_dict[9] = sp.TISSUE_LIBRARY.heavy_water() - ret_dict[10] = sp.TISSUE_LIBRARY.heavy_water() - return ret_dict - - -settings = sp.Settings() -settings[Tags.SIMULATION_PATH] = path_manager.get_hdf5_file_save_path() -settings[Tags.VOLUME_NAME] = "SegmentationTest" -settings[Tags.RANDOM_SEED] = 1234 -settings[Tags.WAVELENGTHS] = [700] -settings[Tags.SPACING_MM] = target_spacing -settings[Tags.DIM_VOLUME_X_MM] = 400 / (target_spacing / input_spacing) -settings[Tags.DIM_VOLUME_Y_MM] = 128 / (target_spacing / input_spacing) -settings[Tags.DIM_VOLUME_Z_MM] = 400 / (target_spacing / input_spacing) - -settings.set_volume_creation_settings({ - Tags.INPUT_SEGMENTATION_VOLUME: segmentation_volume_mask, - Tags.SEGMENTATION_CLASS_MAPPING: segmentation_class_mapping(), - -}) - -settings.set_optical_settings({ - Tags.OPTICAL_MODEL_NUMBER_PHOTONS: 1e8, - Tags.OPTICAL_MODEL_BINARY_PATH: path_manager.get_mcx_binary_path(), - Tags.ILLUMINATION_TYPE: Tags.ILLUMINATION_TYPE_MSOT_ACUITY_ECHO, - Tags.LASER_PULSE_ENERGY_IN_MILLIJOULE: 50, -}) - -pipeline = [ - sp.SegmentationBasedVolumeCreationAdapter(settings), - sp.MCXAdapter(settings) -] - -sp.simulate(pipeline, settings, sp.RSOMExplorerP50(element_spacing_mm=1.0)) - -if Tags.WAVELENGTH in settings: - WAVELENGTH = settings[Tags.WAVELENGTH] -else: - WAVELENGTH = 700 - -if VISUALIZE: - sp.visualise_data(path_to_hdf5_file=path_manager.get_hdf5_file_save_path() + "/" + "SegmentationTest" + ".hdf5", - wavelength=WAVELENGTH, - show_initial_pressure=True, - show_segmentation_map=True) + + +@profile +def run_segmentation_loader(spacing: float | int = 1.0, input_spacing: float | int = 0.2, path_manager=None, + visualise: bool = True): + """ + + :param spacing: The simulation spacing between voxels in mm + :param input_spacing: The input spacing between voxels in mm + :param path_manager: the path manager to be used, typically sp.PathManager + :param visualise: If VISUALIZE is set to True, the reconstruction result will be plotted + :return: a run through of the example + """ + if path_manager is None: + path_manager = sp.PathManager() + + label_mask = shepp_logan_phantom() + + label_mask = np.digitize(label_mask, bins=np.linspace(0.0, 1.0, 11), right=True) + label_mask = label_mask[100:300, 100:300] + label_mask = np.reshape(label_mask, (label_mask.shape[0], 1, label_mask.shape[1])) + + segmentation_volume_tiled = np.tile(label_mask, (1, 128, 1)) + segmentation_volume_mask = sp.round_x5_away_from_zero(zoom(segmentation_volume_tiled, input_spacing/spacing, + order=0)).astype(int) + + def segmentation_class_mapping(): + ret_dict = dict() + ret_dict[0] = sp.TISSUE_LIBRARY.heavy_water() + ret_dict[1] = sp.TISSUE_LIBRARY.blood() + ret_dict[2] = sp.TISSUE_LIBRARY.epidermis() + ret_dict[3] = sp.TISSUE_LIBRARY.muscle() + ret_dict[4] = sp.TISSUE_LIBRARY.mediprene() + ret_dict[5] = sp.TISSUE_LIBRARY.ultrasound_gel() + ret_dict[6] = sp.TISSUE_LIBRARY.heavy_water() + ret_dict[7] = (sp.MolecularCompositionGenerator() + .append(sp.MOLECULE_LIBRARY.oxyhemoglobin(0.01)) + .append(sp.MOLECULE_LIBRARY.deoxyhemoglobin(0.01)) + .append(sp.MOLECULE_LIBRARY.water(0.98)) + .get_molecular_composition(sp.SegmentationClasses.COUPLING_ARTIFACT)) + ret_dict[8] = sp.TISSUE_LIBRARY.heavy_water() + ret_dict[9] = sp.TISSUE_LIBRARY.heavy_water() + ret_dict[10] = sp.TISSUE_LIBRARY.heavy_water() + return ret_dict + + settings = sp.Settings() + settings[Tags.SIMULATION_PATH] = path_manager.get_hdf5_file_save_path() + settings[Tags.VOLUME_NAME] = "SegmentationTest" + settings[Tags.RANDOM_SEED] = 1234 + settings[Tags.WAVELENGTHS] = [700, 800] + settings[Tags.SPACING_MM] = spacing + settings[Tags.DIM_VOLUME_X_MM] = segmentation_volume_mask.shape[0] * spacing + settings[Tags.DIM_VOLUME_Y_MM] = segmentation_volume_mask.shape[1] * spacing + settings[Tags.DIM_VOLUME_Z_MM] = segmentation_volume_mask.shape[2] * spacing + + settings.set_volume_creation_settings({ + Tags.INPUT_SEGMENTATION_VOLUME: segmentation_volume_mask, + Tags.SEGMENTATION_CLASS_MAPPING: segmentation_class_mapping(), + + }) + + settings.set_optical_settings({ + Tags.OPTICAL_MODEL_NUMBER_PHOTONS: 1e8, + Tags.OPTICAL_MODEL_BINARY_PATH: path_manager.get_mcx_binary_path(), + Tags.ILLUMINATION_TYPE: Tags.ILLUMINATION_TYPE_MSOT_ACUITY_ECHO, + Tags.LASER_PULSE_ENERGY_IN_MILLIJOULE: 50, + }) + + pipeline = [ + sp.SegmentationBasedAdapter(settings), + sp.MCXAdapter(settings) + ] + + sp.simulate(pipeline, settings, sp.RSOMExplorerP50(element_spacing_mm=1.0)) + + if Tags.WAVELENGTH in settings: + WAVELENGTH = settings[Tags.WAVELENGTH] + else: + WAVELENGTH = 700 + + if visualise: + sp.visualise_data(path_to_hdf5_file=path_manager.get_hdf5_file_save_path() + "/" + "SegmentationTest" + ".hdf5", + wavelength=WAVELENGTH, + show_initial_pressure=True, + show_segmentation_map=True) + + +if __name__ == "__main__": + parser = ArgumentParser(description='Run the segmentation loader example') + parser.add_argument("--spacing", default=1, type=float, help='the voxel spacing in mm') + parser.add_argument("--input_spacing", default=0.2, type=float, help='the input spacing in mm') + parser.add_argument("--path_manager", default=None, help='the path manager, None uses sp.PathManager') + parser.add_argument("--visualise", default=True, type=bool, help='whether to visualise the result') + config = parser.parse_args() + + run_segmentation_loader(spacing=config.spacing, input_spacing=config.input_spacing, + path_manager=config.path_manager, visualise=config.visualise) diff --git a/simpa_examples/three_vs_two_dimensional_simulation_example.py b/simpa_examples/three_vs_two_dimensional_simulation_example.py new file mode 100644 index 00000000..6df7845f --- /dev/null +++ b/simpa_examples/three_vs_two_dimensional_simulation_example.py @@ -0,0 +1,220 @@ +# SPDX-FileCopyrightText: 2021 Division of Intelligent Medical Systems, DKFZ +# SPDX-FileCopyrightText: 2021 Janek Groehl +# SPDX-License-Identifier: MIT + +# SPDX-FileCopyrightText: 2024 Division of Intelligent Medical Systems, DKFZ +# SPDX-FileCopyrightText: 2024 Janek Groehl +# SPDX-License-Identifier: MIT + +from simpa import Tags +import simpa as sp +import numpy as np +from simpa.utils.profiling import profile +from argparse import ArgumentParser +import matplotlib.pyplot as plt + +# FIXME temporary workaround for newest Intel architectures +import os +os.environ["KMP_DUPLICATE_LIB_OK"] = "TRUE" + +# TODO: Please make sure that a valid path_config.env file is located in your home directory, or that you +# point to the correct file in the PathManager(). + + +@profile +def run_three_vs_two_dimensional_simulation_example(spacing: float | int = 0.2, path_manager=None, + visualise: bool = True): + """ + + A run through of the 3D vs 2D example. In this example, the same simulation script is run twice, with the + only difference being the simulation run is either 2D or 3D. + + :param spacing: The simulation spacing between voxels + :param path_manager: the path manager to be used, typically sp.PathManager + :param visualise: If visualise is set to True, the result swill be plotted using matplotlib + """ + + def run_sim(run_3D: bool = True, path_manager=path_manager): + if path_manager is None: + path_manager = sp.PathManager() + VOLUME_TRANSDUCER_DIM_IN_MM = 75 + VOLUME_PLANAR_DIM_IN_MM = 20 + VOLUME_HEIGHT_IN_MM = 25 + RANDOM_SEED = 4711 + + # If VISUALIZE is set to True, the simulation result will be plotted + VISUALIZE = True + + def create_example_tissue(): + """ + This is a very simple example script of how to create a tissue definition. + It contains a muscular background, an epidermis layer on top of the muscles + and a blood vessel. + """ + background_dictionary = sp.Settings() + background_dictionary[Tags.MOLECULE_COMPOSITION] = sp.TISSUE_LIBRARY.constant(1e-10, 1e-10, 1.0) + background_dictionary[Tags.STRUCTURE_TYPE] = Tags.BACKGROUND + + tissue_dict = sp.Settings() + tissue_dict[Tags.BACKGROUND] = background_dictionary + tissue_dict["muscle"] = sp.define_horizontal_layer_structure_settings(z_start_mm=0, thickness_mm=100, + molecular_composition=sp.TISSUE_LIBRARY.constant( + 0.05, 100, 0.9), + priority=1, + consider_partial_volume=True, + adhere_to_deformation=True) + tissue_dict["epidermis"] = sp.define_horizontal_layer_structure_settings(z_start_mm=1, thickness_mm=0.1, + molecular_composition=sp.TISSUE_LIBRARY.epidermis(), + priority=8, + consider_partial_volume=True, + adhere_to_deformation=True) + tissue_dict["vessel_1"] = sp.define_circular_tubular_structure_settings( + tube_start_mm=[VOLUME_TRANSDUCER_DIM_IN_MM/2 - 10, 0, 5], + tube_end_mm=[VOLUME_TRANSDUCER_DIM_IN_MM/2 - 10, VOLUME_PLANAR_DIM_IN_MM, 5], + molecular_composition=sp.TISSUE_LIBRARY.blood(), + radius_mm=2, priority=3, consider_partial_volume=True, + adhere_to_deformation=False + ) + tissue_dict["vessel_2"] = sp.define_circular_tubular_structure_settings( + tube_start_mm=[VOLUME_TRANSDUCER_DIM_IN_MM/2, 0, 10], + tube_end_mm=[VOLUME_TRANSDUCER_DIM_IN_MM/2, VOLUME_PLANAR_DIM_IN_MM, 10], + molecular_composition=sp.TISSUE_LIBRARY.blood(), + radius_mm=3, priority=3, consider_partial_volume=True, + adhere_to_deformation=False + ) + return tissue_dict + + # Seed the numpy random configuration prior to creating the global_settings file in + # order to ensure that the same volume + # is generated with the same random seed every time. + + np.random.seed(RANDOM_SEED) + + general_settings = { + # These parameters set the general properties of the simulated volume + Tags.RANDOM_SEED: RANDOM_SEED, + Tags.VOLUME_NAME: f"2Dvs3D_3D{run_3D}_" + str(RANDOM_SEED), + Tags.SIMULATION_PATH: path_manager.get_hdf5_file_save_path(), + Tags.SPACING_MM: spacing, + Tags.DIM_VOLUME_Z_MM: VOLUME_HEIGHT_IN_MM, + Tags.DIM_VOLUME_X_MM: VOLUME_TRANSDUCER_DIM_IN_MM, + Tags.DIM_VOLUME_Y_MM: VOLUME_PLANAR_DIM_IN_MM, + Tags.VOLUME_CREATOR: Tags.VOLUME_CREATOR_VERSATILE, + Tags.GPU: True, + Tags.WAVELENGTHS: [800], + Tags.DO_FILE_COMPRESSION: True, + Tags.DO_IPASC_EXPORT: True + } + settings = sp.Settings(general_settings) + np.random.seed(RANDOM_SEED) + + settings.set_volume_creation_settings({ + Tags.STRUCTURES: create_example_tissue(), + Tags.SIMULATE_DEFORMED_LAYERS: True + }) + + settings.set_optical_settings({ + Tags.OPTICAL_MODEL_NUMBER_PHOTONS: 1e7, + Tags.OPTICAL_MODEL_BINARY_PATH: path_manager.get_mcx_binary_path(), + Tags.ILLUMINATION_TYPE: Tags.ILLUMINATION_TYPE_MSOT_ACUITY_ECHO, + Tags.LASER_PULSE_ENERGY_IN_MILLIJOULE: 50, + Tags.MCX_ASSUMED_ANISOTROPY: 0.9, + Tags.ADDITIONAL_FLAGS: ['--printgpu'] # to print MCX GPU information + }) + + settings.set_acoustic_settings({ + Tags.ACOUSTIC_SIMULATION_3D: run_3D, + Tags.ACOUSTIC_MODEL_BINARY_PATH: path_manager.get_matlab_binary_path(), + Tags.KWAVE_PROPERTY_ALPHA_POWER: 0.00, + Tags.KWAVE_PROPERTY_SENSOR_RECORD: "p", + Tags.KWAVE_PROPERTY_PMLInside: False, + Tags.KWAVE_PROPERTY_PMLSize: [31, 32], + Tags.KWAVE_PROPERTY_PMLAlpha: 1.5, + Tags.KWAVE_PROPERTY_PlotPML: False, + Tags.RECORDMOVIE: False, + Tags.MOVIENAME: "visualization_log", + Tags.ACOUSTIC_LOG_SCALE: True + }) + + settings.set_reconstruction_settings({ + Tags.RECONSTRUCTION_PERFORM_BANDPASS_FILTERING: False, + Tags.ACOUSTIC_MODEL_BINARY_PATH: path_manager.get_matlab_binary_path(), + Tags.ACOUSTIC_SIMULATION_3D: run_3D, + Tags.KWAVE_PROPERTY_ALPHA_POWER: 0.00, + Tags.TUKEY_WINDOW_ALPHA: 0.5, + Tags.BANDPASS_CUTOFF_LOWPASS_IN_HZ: int(8e6), + Tags.BANDPASS_CUTOFF_HIGHPASS_IN_HZ: int(0.1e4), + Tags.RECONSTRUCTION_BMODE_AFTER_RECONSTRUCTION: False, + Tags.RECONSTRUCTION_BMODE_METHOD: Tags.RECONSTRUCTION_BMODE_METHOD_HILBERT_TRANSFORM, + Tags.RECONSTRUCTION_APODIZATION_METHOD: Tags.RECONSTRUCTION_APODIZATION_BOX, + Tags.RECONSTRUCTION_MODE: Tags.RECONSTRUCTION_MODE_PRESSURE, + Tags.KWAVE_PROPERTY_SENSOR_RECORD: "p", + Tags.KWAVE_PROPERTY_PMLInside: False, + Tags.KWAVE_PROPERTY_PMLSize: [31, 32], + Tags.KWAVE_PROPERTY_PMLAlpha: 1.5, + Tags.KWAVE_PROPERTY_PlotPML: False, + Tags.RECORDMOVIE: False, + Tags.MOVIENAME: "visualization_log", + Tags.ACOUSTIC_LOG_SCALE: True, + Tags.DATA_FIELD_SPEED_OF_SOUND: 1540, + Tags.DATA_FIELD_ALPHA_COEFF: 0.01, + Tags.DATA_FIELD_DENSITY: 1000, + Tags.SPACING_MM: spacing + }) + + device = sp.PhotoacousticDevice(device_position_mm=np.array([VOLUME_TRANSDUCER_DIM_IN_MM/2, + VOLUME_PLANAR_DIM_IN_MM/2, + 0]), + field_of_view_extent_mm=np.asarray([-15, 15, 0, 0, 0, 20])) + device.set_detection_geometry(sp.LinearArrayDetectionGeometry(device_position_mm=device.device_position_mm, + pitch_mm=0.25, + number_detector_elements=100, + field_of_view_extent_mm=np.asarray([-15, 15, 0, 0, 0, 20]))) + device.add_illumination_geometry(sp.SlitIlluminationGeometry(slit_vector_mm=[100, 0, 0])) + + SIMULATION_PIPELINE = [ + sp.ModelBasedAdapter(settings), + sp.MCXAdapter(settings), + sp.KWaveAdapter(settings), + sp.TimeReversalAdapter(settings), + sp.FieldOfViewCropping(settings) + ] + + sp.simulate(SIMULATION_PIPELINE, settings, device) + + if Tags.WAVELENGTH in settings: + WAVELENGTH = settings[Tags.WAVELENGTH] + else: + WAVELENGTH = 800 + + return (sp.load_data_field(settings[Tags.SIMPA_OUTPUT_FILE_PATH], + sp.Tags.DATA_FIELD_TIME_SERIES_DATA, WAVELENGTH), + sp.load_data_field(settings[Tags.SIMPA_OUTPUT_FILE_PATH], + sp.Tags.DATA_FIELD_RECONSTRUCTED_DATA, WAVELENGTH)) + + two_d_time_series, two_d_recon = run_sim(False) + three_d_time_series, three_d_recon = run_sim(True) + + if visualise: + fig, (ax1, ax2, ax3) = plt.subplots(1, 3, layout="constrained", figsize=(12, 4)) + + ax1.imshow(two_d_recon.T) + ax1.set_title("2D Simulation") + ax2.imshow(three_d_recon.T) + ax2.set_title("3D Simulation") + ax3.plot(two_d_time_series[49], label="2D simulation") + ax3.plot(three_d_time_series[49], label="3D simulation") + plt.legend() + + plt.show() + + +if __name__ == "__main__": + parser = ArgumentParser(description='Run the optical and acoustic simulation example') + parser.add_argument("--spacing", default=0.25, type=float, help='the voxel spacing in mm') + parser.add_argument("--path_manager", default=None, help='the path manager, None uses sp.PathManager') + parser.add_argument("--visualise", default=True, type=bool, help='whether to visualise the result') + config = parser.parse_args() + + run_three_vs_two_dimensional_simulation_example(spacing=config.spacing, path_manager=config.path_manager, + visualise=config.visualise) diff --git a/simpa_tests/automatic_tests/device_tests/test_field_of_view.py b/simpa_tests/automatic_tests/device_tests/test_field_of_view.py index cc63680c..bbdbc99a 100644 --- a/simpa_tests/automatic_tests/device_tests/test_field_of_view.py +++ b/simpa_tests/automatic_tests/device_tests/test_field_of_view.py @@ -22,7 +22,7 @@ def setUp(self): def _test(self, field_of_view_extent_mm: list, spacing_in_mm: float, detection_geometry: DetectionGeometryBase): detection_geometry.field_of_view_extent_mm = field_of_view_extent_mm xdim, zdim, ydim, xdim_start, xdim_end, ydim_start, ydim_end, zdim_start, zdim_end = compute_image_dimensions( - detection_geometry, spacing_in_mm, self.logger) + detection_geometry.field_of_view_extent_mm, spacing_in_mm, self.logger) assert type(xdim) == int and type(ydim) == int and type(zdim) == int, "dimensions should be integers" assert xdim >= 1 and ydim >= 1 and zdim >= 1, "dimensions should be positive" @@ -76,10 +76,10 @@ def test_unsymmetric(self): xdim, zdim, ydim, xdim_start, xdim_end, ydim_start, ydim_end, zdim_start, zdim_end = image_dimensions assert zdim == 1, "With no FOV extend in z dimension only one slice should be created" - assert xdim == 249 + assert xdim == 250 assert ydim == 100 - self.assertAlmostEqual(xdim_start, -124.75) - self.assertAlmostEqual(xdim_end, 124.25) + self.assertAlmostEqual(xdim_start, -125.25) + self.assertAlmostEqual(xdim_end, 124.75) self.assertAlmostEqual(ydim_start, -60) self.assertAlmostEqual(ydim_end, 40) self.assertAlmostEqual(zdim_start, 0) @@ -101,3 +101,40 @@ def test_symmetric_with_odd_number_of_elements(self): self.assertAlmostEqual(ydim_end, 40) self.assertAlmostEqual(zdim_start, 0) self.assertAlmostEqual(zdim_end, 0) + + def test_with_changing_spacing(self): + """ + Tests different spacings with the same field of view. Dimensions and their start and end are expected to change + """ + spacings = [0.1, 0.2, 0.3, 0.4, 0.5] + expected_xdims = [100, 50, 33, 25, 20] + expected_ydims = [200, 100, 67, 50, 40] + expected_xdim_starts = [-50, -25, -16.5, -12.5, -10] + expected_xdim_ends = [50, 25, 16.5, 12.5, 10] + expected_ydim_starts = [-120, -60, -40.16666666, -30, -24] + expected_ydim_ends = [80, 40, 26.83333333, 20, 16] + + for i, spacing in enumerate(spacings): + image_dimensions = self._test([-5, 5, 0, 0, -12, 8], spacing, self.detection_geometry) + xdim, zdim, ydim, xdim_start, xdim_end, ydim_start, ydim_end, zdim_start, zdim_end = image_dimensions + + assert zdim == 1, "With no FOV extend in z dimension only one slice should be created" + assert xdim == expected_xdims[i] + assert ydim == expected_ydims[i] + self.assertAlmostEqual(xdim_start, expected_xdim_starts[i]) + self.assertAlmostEqual(xdim_end, expected_xdim_ends[i]) + self.assertAlmostEqual(ydim_start, expected_ydim_starts[i]) + self.assertAlmostEqual(ydim_end, expected_ydim_ends[i]) + self.assertAlmostEqual(zdim_start, 0) + self.assertAlmostEqual(zdim_end, 0) + + +if __name__ == '__main__': + test = TestFieldOfView() + test.setUp() + test.test_symmetric() + test.test_symmetric_with_small_spacing() + test.test_unsymmetric_with_small_spacing() + test.test_unsymmetric() + test.test_symmetric_with_odd_number_of_elements() + test.test_with_changing_spacing() diff --git a/simpa_tests/automatic_tests/test_IPASC_export.py b/simpa_tests/automatic_tests/test_IPASC_export.py index cc638ec7..1b555464 100644 --- a/simpa_tests/automatic_tests/test_IPASC_export.py +++ b/simpa_tests/automatic_tests/test_IPASC_export.py @@ -10,13 +10,13 @@ from simpa.core.device_digital_twins import RSOMExplorerP50 from simpa.utils import Tags, Settings from simpa_tests.test_utils import create_test_structure_parameters -from simpa import ModelBasedVolumeCreationAdapter -from simpa.core.simulation_modules.optical_simulation_module.optical_forward_model_test_adapter import \ - OpticalForwardModelTestAdapter -from simpa.core.simulation_modules.acoustic_forward_module.acoustic_forward_model_test_adapter import \ - AcousticForwardModelTestAdapter -from simpa.core.simulation_modules.reconstruction_module.reconstruction_module_test_adapter import \ - ReconstructionModuleTestAdapter +from simpa import ModelBasedAdapter +from simpa.core.simulation_modules.optical_module.optical_test_adapter import \ + OpticalTestAdapter +from simpa.core.simulation_modules.acoustic_module.acoustic_test_adapter import \ + AcousticTestAdapter +from simpa.core.simulation_modules.reconstruction_module.reconstruction_test_adapter import \ + ReconstructionTestAdapter from pacfish import load_data as load_ipasc from simpa.io_handling import load_hdf5 as load_simpa @@ -63,21 +63,21 @@ def setUp(self) -> None: }) self.acoustic_simulation_pipeline = [ - ModelBasedVolumeCreationAdapter(self.settings), - OpticalForwardModelTestAdapter(self.settings), - AcousticForwardModelTestAdapter(self.settings), + ModelBasedAdapter(self.settings), + OpticalTestAdapter(self.settings), + AcousticTestAdapter(self.settings), ] self.optical_simulation_pipeline = [ - ModelBasedVolumeCreationAdapter(self.settings), - OpticalForwardModelTestAdapter(self.settings) + ModelBasedAdapter(self.settings), + OpticalTestAdapter(self.settings) ] self.full_simulation_pipeline = [ - ModelBasedVolumeCreationAdapter(self.settings), - OpticalForwardModelTestAdapter(self.settings), - AcousticForwardModelTestAdapter(self.settings), - ReconstructionModuleTestAdapter(self.settings) + ModelBasedAdapter(self.settings), + OpticalTestAdapter(self.settings), + AcousticTestAdapter(self.settings), + ReconstructionTestAdapter(self.settings) ] self.device = RSOMExplorerP50(0.1, 12, 12) @@ -86,11 +86,11 @@ def setUp(self) -> None: self.expected_ipasc_output_path = None def clean_up(self): - print(f"Attempting to clean {self.settings[Tags.SIMPA_OUTPUT_PATH]}") - if (os.path.exists(self.settings[Tags.SIMPA_OUTPUT_PATH]) and - os.path.isfile(self.settings[Tags.SIMPA_OUTPUT_PATH])): + print(f"Attempting to clean {self.settings[Tags.SIMPA_OUTPUT_FILE_PATH]}") + if (os.path.exists(self.settings[Tags.SIMPA_OUTPUT_FILE_PATH]) and + os.path.isfile(self.settings[Tags.SIMPA_OUTPUT_FILE_PATH])): # Delete the created file - os.remove(self.settings[Tags.SIMPA_OUTPUT_PATH]) + os.remove(self.settings[Tags.SIMPA_OUTPUT_FILE_PATH]) print(f"Attempting to clean {self.expected_ipasc_output_path}") if (os.path.exists(self.expected_ipasc_output_path) and @@ -131,24 +131,24 @@ def assert_ipasc_file_binary_contents_is_matching_simpa_simulation(self, simpa_p def test_file_is_not_created_on_only_optical_simulation(self): simulate(self.optical_simulation_pipeline, self.settings, self.device) - self.expected_ipasc_output_path = self.settings[Tags.SIMPA_OUTPUT_PATH].replace(".hdf5", "_ipasc.hdf5") + self.expected_ipasc_output_path = self.settings[Tags.SIMPA_OUTPUT_FILE_PATH].replace(".hdf5", "_ipasc.hdf5") self.assertTrue(not os.path.exists(self.expected_ipasc_output_path)) self.clean_up() @expectedFailure def test_file_is_created_on_acoustic_simulation(self): simulate(self.acoustic_simulation_pipeline, self.settings, self.device) - self.expected_ipasc_output_path = self.settings[Tags.SIMPA_OUTPUT_PATH].replace(".hdf5", "_ipasc.hdf5") + self.expected_ipasc_output_path = self.settings[Tags.SIMPA_OUTPUT_FILE_PATH].replace(".hdf5", "_ipasc.hdf5") self.assertTrue(os.path.exists(self.expected_ipasc_output_path)) - self.assert_ipasc_file_binary_contents_is_matching_simpa_simulation(self.settings[Tags.SIMPA_OUTPUT_PATH], + self.assert_ipasc_file_binary_contents_is_matching_simpa_simulation(self.settings[Tags.SIMPA_OUTPUT_FILE_PATH], self.expected_ipasc_output_path) self.clean_up() @expectedFailure def test_file_is_created_on_full_simulation(self): simulate(self.full_simulation_pipeline, self.settings, self.device) - self.expected_ipasc_output_path = self.settings[Tags.SIMPA_OUTPUT_PATH].replace(".hdf5", "_ipasc.hdf5") + self.expected_ipasc_output_path = self.settings[Tags.SIMPA_OUTPUT_FILE_PATH].replace(".hdf5", "_ipasc.hdf5") self.assertTrue(os.path.exists(self.expected_ipasc_output_path)) - self.assert_ipasc_file_binary_contents_is_matching_simpa_simulation(self.settings[Tags.SIMPA_OUTPUT_PATH], + self.assert_ipasc_file_binary_contents_is_matching_simpa_simulation(self.settings[Tags.SIMPA_OUTPUT_FILE_PATH], self.expected_ipasc_output_path) self.clean_up() diff --git a/simpa_tests/automatic_tests/test_additional_flags.py b/simpa_tests/automatic_tests/test_additional_flags.py new file mode 100644 index 00000000..48968956 --- /dev/null +++ b/simpa_tests/automatic_tests/test_additional_flags.py @@ -0,0 +1,61 @@ +# SPDX-FileCopyrightText: 2021 Division of Intelligent Medical Systems, DKFZ +# SPDX-FileCopyrightText: 2021 Janek Groehl +# SPDX-License-Identifier: MIT + +import unittest + +from simpa import MCXReflectanceAdapter, MCXAdapter, KWaveAdapter, TimeReversalAdapter, Tags, Settings +from simpa.utils.matlab import generate_matlab_cmd + + +class TestAdditionalFlags(unittest.TestCase): + def setUp(self) -> None: + self.additional_flags = ('-l', '-a') + self.settings = Settings() + + def test_get_cmd_mcx_reflectance_adapter(self): + self.settings.set_optical_settings({ + Tags.OPTICAL_MODEL_BINARY_PATH: '.', + Tags.ADDITIONAL_FLAGS: self.additional_flags + }) + mcx_reflectance_adapter = MCXReflectanceAdapter(global_settings=self.settings) + cmd = mcx_reflectance_adapter.get_command() + for flag in self.additional_flags: + self.assertIn( + flag, cmd, f"{flag} was not in command returned by mcx reflectance adapter but was defined as additional flag") + + def test_get_cmd_mcx_adapter(self): + self.settings.set_optical_settings({ + Tags.OPTICAL_MODEL_BINARY_PATH: '.', + Tags.ADDITIONAL_FLAGS: self.additional_flags + }) + mcx_adapter = MCXAdapter(global_settings=self.settings) + cmd = mcx_adapter.get_command() + for flag in self.additional_flags: + self.assertIn( + flag, cmd, f"{flag} was not in command returned by mcx adapter but was defined as additional flag") + + def test_get_cmd_kwave_adapter(self): + self.settings.set_acoustic_settings({ + Tags.ADDITIONAL_FLAGS: self.additional_flags + }) + kwave_adapter = KWaveAdapter(global_settings=self.settings) + cmd = generate_matlab_cmd("./matlab.exe", "simulate_2D.m", "my_hdf5.mat", kwave_adapter.get_additional_flags()) + for flag in self.additional_flags: + self.assertIn( + flag, cmd, f"{flag} was not in command returned by kwave adapter but was defined as additional flag") + + def test_get_cmd_time_reversal_adapter(self): + self.settings.set_reconstruction_settings({ + Tags.ADDITIONAL_FLAGS: self.additional_flags + }) + time_reversal_adapter = TimeReversalAdapter(global_settings=self.settings) + cmd = generate_matlab_cmd("./matlab.exe", "time_reversal_2D.m", "my_hdf5.mat", + time_reversal_adapter.get_additional_flags()) + for flag in self.additional_flags: + self.assertIn( + flag, cmd, f"{flag} was not in command returned by time reversal adapter but was defined as additional flag") + + +if __name__ == '__main__': + unittest.main() diff --git a/simpa_tests/automatic_tests/test_calculation_utils.py b/simpa_tests/automatic_tests/test_calculation_utils.py index b070df1d..02c34f34 100644 --- a/simpa_tests/automatic_tests/test_calculation_utils.py +++ b/simpa_tests/automatic_tests/test_calculation_utils.py @@ -5,10 +5,11 @@ import unittest from simpa.utils import SegmentationClasses, MolecularCompositionGenerator from simpa.utils.libraries.molecule_library import MOLECULE_LIBRARY -from simpa.utils.calculate import calculate_oxygenation +from simpa.utils.calculate import calculate_oxygenation, calculate_bvf from simpa.utils.calculate import randomize_uniform from simpa.utils.calculate import calculate_gruneisen_parameter_from_temperature from simpa.utils.calculate import positive_gauss +from simpa.utils.calculate import round_x5_away_from_zero import numpy as np @@ -53,7 +54,7 @@ def test_oxygenation_calculation(self): assert abs(oxy_value) < 1e-5, ("oxy value was not 0.0 but " + str(oxy_value)) # RANDOM CASES - for i in range(100): + for _ in range(100): oxy = np.random.random() deoxy = np.random.random() mcg = MolecularCompositionGenerator() @@ -65,6 +66,55 @@ def test_oxygenation_calculation(self): assert abs(sO2_value - (oxy / (oxy + deoxy))) < 1e-10 + def test_bvf_calculation(self): + + # Neither oxy nor deoxy: + mcg = MolecularCompositionGenerator() + mcg.append(MOLECULE_LIBRARY.fat(1.0)) + bvf_value = calculate_bvf(mcg.get_molecular_composition(segmentation_type=SegmentationClasses.GENERIC)) + assert bvf_value == 0 + + mcg = MolecularCompositionGenerator() + mcg.append(MOLECULE_LIBRARY.fat(1.0)) + mcg.append(MOLECULE_LIBRARY.oxyhemoglobin(0.0)) + bvf_value = calculate_bvf(mcg.get_molecular_composition(segmentation_type=SegmentationClasses.GENERIC)) + assert bvf_value == 0 + + # Just oxyhemoglobin CASES: + mcg = MolecularCompositionGenerator() + mcg.append(MOLECULE_LIBRARY.oxyhemoglobin(1.0)) + oxy_hemo = mcg.get_molecular_composition(segmentation_type=SegmentationClasses.GENERIC) + bvf_value = calculate_bvf(oxy_hemo) + assert bvf_value == 1.0 + mcg.append(MOLECULE_LIBRARY.deoxyhemoglobin(0.0)) + oxy_hemo = mcg.get_molecular_composition(segmentation_type=SegmentationClasses.GENERIC) + bvf_value = calculate_bvf(oxy_hemo) + assert bvf_value == 1.0 + + # Just deoxyhemoglobin CASES: + mcg = MolecularCompositionGenerator() + mcg.append(MOLECULE_LIBRARY.deoxyhemoglobin(1.0)) + deoxy_hemo = mcg.get_molecular_composition(segmentation_type=SegmentationClasses.GENERIC) + bvf_value = calculate_bvf(deoxy_hemo) + assert bvf_value == 1.0 + mcg.append(MOLECULE_LIBRARY.oxyhemoglobin(0.0)) + deoxy_hemo = mcg.get_molecular_composition(segmentation_type=SegmentationClasses.GENERIC) + bvf_value = calculate_bvf(deoxy_hemo) + assert bvf_value == 1.0 + + # RANDOM CASES + for _ in range(100): + oxy = np.random.random() + deoxy = np.random.random() + fat = np.random.random() + sum_oxy_deoxy_fat = oxy + deoxy + fat + mcg = MolecularCompositionGenerator() + mcg.append(MOLECULE_LIBRARY.fat(fat/sum_oxy_deoxy_fat)) + mcg.append(MOLECULE_LIBRARY.deoxyhemoglobin(deoxy/sum_oxy_deoxy_fat)) + mcg.append(MOLECULE_LIBRARY.oxyhemoglobin(oxy/sum_oxy_deoxy_fat)) + bvf_value = calculate_bvf(mcg.get_molecular_composition(segmentation_type=SegmentationClasses.GENERIC)) + assert abs(bvf_value - (oxy+deoxy)/sum_oxy_deoxy_fat) < 1e-10 + def test_randomize(self): for _ in range(1000): lower = np.random.randint(0, 10000000) @@ -86,3 +136,26 @@ def test_positive_Gauss(self): std = np.random.rand(1)[0] random_value = positive_gauss(mean, std) assert random_value > float(0), "positive Gauss value outside the desired range and negative" + + def test_rounding_function(self): + assert round_x5_away_from_zero(0.5) == 1 + assert round_x5_away_from_zero(0.4) == 0 + assert round_x5_away_from_zero(0.6) == 1 + assert round_x5_away_from_zero(0.1) == 0 + assert round_x5_away_from_zero(0.9) == 1 + assert round_x5_away_from_zero(0.0) == 0 + assert round_x5_away_from_zero(1.0) == 1 + assert (round_x5_away_from_zero(np.arange(0, 15) + 0.5) == np.arange(0, 15) + 1).all() + assert (round_x5_away_from_zero(np.arange(0, 15) + 0.4) == np.arange(0, 15)).all() + assert (round_x5_away_from_zero(np.arange(0, 15) + 0.6) == np.arange(0, 15) + 1).all() + assert (round_x5_away_from_zero(np.arange(0, 15) + 0.1) == np.arange(0, 15)).all() + assert (round_x5_away_from_zero(np.arange(0, 15) + 0.9) == np.arange(0, 15) + 1).all() + assert (round_x5_away_from_zero(np.arange(0, 15) + 0.0) == np.arange(0, 15)).all() + assert (round_x5_away_from_zero(np.arange(0, 15) + 1.0) == np.arange(0, 15) + 1).all() + assert (round_x5_away_from_zero(np.arange(-15, 0) - 0.5) == np.arange(-15, 0) - 1).all() + assert (round_x5_away_from_zero(np.arange(-15, 0) - 0.4) == np.arange(-15, 0)).all() + assert (round_x5_away_from_zero(np.arange(-15, 0) - 0.6) == np.arange(-15, 0) - 1).all() + assert (round_x5_away_from_zero(np.arange(-15, 0) - 0.1) == np.arange(-15, 0)).all() + assert (round_x5_away_from_zero(np.arange(-15, 0) - 0.9) == np.arange(-15, 0) - 1).all() + assert (round_x5_away_from_zero(np.arange(-15, 0) - 0.0) == np.arange(-15, 0)).all() + assert (round_x5_away_from_zero(np.arange(-15, 0) - 1.0) == np.arange(-15, 0) - 1).all() diff --git a/simpa_tests/automatic_tests/test_create_a_volume.py b/simpa_tests/automatic_tests/test_create_a_volume.py index c67a8fc8..727a0752 100644 --- a/simpa_tests/automatic_tests/test_create_a_volume.py +++ b/simpa_tests/automatic_tests/test_create_a_volume.py @@ -8,7 +8,7 @@ from simpa.core.simulation import simulate import os from simpa_tests.test_utils import create_test_structure_parameters -from simpa import ModelBasedVolumeCreationAdapter +from simpa import ModelBasedAdapter from simpa.core.device_digital_twins import RSOMExplorerP50 @@ -32,11 +32,11 @@ def test_create_volume(self): settings.set_volume_creation_settings({Tags.STRUCTURES: create_test_structure_parameters()}) simulation_pipeline = [ - ModelBasedVolumeCreationAdapter(settings) + ModelBasedAdapter(settings) ] simulate(simulation_pipeline, settings, RSOMExplorerP50(0.1, 1, 1)) - if (os.path.exists(settings[Tags.SIMPA_OUTPUT_PATH]) and - os.path.isfile(settings[Tags.SIMPA_OUTPUT_PATH])): - os.remove(settings[Tags.SIMPA_OUTPUT_PATH]) + if (os.path.exists(settings[Tags.SIMPA_OUTPUT_FILE_PATH]) and + os.path.isfile(settings[Tags.SIMPA_OUTPUT_FILE_PATH])): + os.remove(settings[Tags.SIMPA_OUTPUT_FILE_PATH]) diff --git a/simpa_tests/automatic_tests/test_heterogeneity_generator.py b/simpa_tests/automatic_tests/test_heterogeneity_generator.py new file mode 100644 index 00000000..252e84fb --- /dev/null +++ b/simpa_tests/automatic_tests/test_heterogeneity_generator.py @@ -0,0 +1,223 @@ +# SPDX-FileCopyrightText: 2021 Division of Intelligent Medical Systems, DKFZ +# SPDX-FileCopyrightText: 2021 Janek Groehl +# SPDX-License-Identifier: MIT + +import unittest +import numpy as np +import simpa as sp +from simpa.utils import Tags + + +class TestHeterogeneityGenerator(unittest.TestCase): + + def setUp(self) -> None: + self.spacing = 1.0 + self.MIN = -4.0 + self.MAX = 8.0 + self.MEAN = -332.0 + self.STD = 78.0 + self.FULL_IMAGE = np.zeros((4, 8)) + self.FULL_IMAGE[:, 1:2] = 1 + self.PARTIAL_IMAGE = np.zeros((2, 2)) + self.PARTIAL_IMAGE[:, 1] = 1 + self.TEST_SETTINGS = sp.Settings({ + # These parameters set the general properties of the simulated volume + sp.Tags.SPACING_MM: self.spacing, + sp.Tags.DIM_VOLUME_Z_MM: 8, + sp.Tags.DIM_VOLUME_X_MM: 4, + sp.Tags.DIM_VOLUME_Y_MM: 7 + }) + dimx, dimy, dimz = self.TEST_SETTINGS.get_volume_dimensions_voxels() + self.HETEROGENEITY_GENERATORS = [ + sp.RandomHeterogeneity(dimx, dimy, dimz, spacing_mm=self.spacing), + sp.RandomHeterogeneity(dimx, dimy, dimz, spacing_mm=self.spacing, gaussian_blur_size_mm=3), + sp.BlobHeterogeneity(dimx, dimy, dimz, spacing_mm=self.spacing), + sp.ImageHeterogeneity(dimx, dimy, dimz, heterogeneity_image=self.FULL_IMAGE, spacing_mm=self.spacing, + image_pixel_spacing_mm=self.spacing), + sp.ImageHeterogeneity(dimx, dimy, dimz, heterogeneity_image=self.PARTIAL_IMAGE, + scaling_type=Tags.IMAGE_SCALING_CONSTANT, spacing_mm=self.spacing, constant=0.5), + sp.ImageHeterogeneity(dimx, dimy, dimz, heterogeneity_image=self.PARTIAL_IMAGE, + scaling_type=Tags.IMAGE_SCALING_STRETCH, spacing_mm=self.spacing), + sp.ImageHeterogeneity(dimx, dimy, dimz, heterogeneity_image=self.PARTIAL_IMAGE, + scaling_type=Tags.IMAGE_SCALING_SYMMETRIC, spacing_mm=self.spacing), + sp.ImageHeterogeneity(dimx, dimy, dimz, heterogeneity_image=self.PARTIAL_IMAGE, + scaling_type=Tags.IMAGE_SCALING_WRAP, spacing_mm=self.spacing), + sp.ImageHeterogeneity(dimx, dimy, dimz, heterogeneity_image=self.PARTIAL_IMAGE, + scaling_type=Tags.IMAGE_SCALING_EDGE, spacing_mm=self.spacing), + ] + self.HETEROGENEITY_GENERATORS_MIN_MAX = [ + sp.RandomHeterogeneity(dimx, dimy, dimz, spacing_mm=self.spacing, target_min=self.MIN, target_max=self.MAX), + sp.RandomHeterogeneity(dimx, dimy, dimz, spacing_mm=self.spacing, target_min=self.MIN, target_max=self.MAX, + gaussian_blur_size_mm=3), + sp.BlobHeterogeneity(dimx, dimy, dimz, spacing_mm=self.spacing, target_min=self.MIN, target_max=self.MAX), + sp.ImageHeterogeneity(dimx, dimy, dimz, heterogeneity_image=self.FULL_IMAGE, spacing_mm=self.spacing, + target_min=self.MIN, target_max=self.MAX), + sp.ImageHeterogeneity(dimx, dimy, dimz, heterogeneity_image=self.PARTIAL_IMAGE, + scaling_type=Tags.IMAGE_SCALING_CONSTANT, spacing_mm=self.spacing, constant=0.5, + target_min=self.MIN, target_max=self.MAX), + sp.ImageHeterogeneity(dimx, dimy, dimz, heterogeneity_image=self.PARTIAL_IMAGE, + scaling_type=Tags.IMAGE_SCALING_STRETCH, spacing_mm=self.spacing, + target_min=self.MIN, target_max=self.MAX), + sp.ImageHeterogeneity(dimx, dimy, dimz, heterogeneity_image=self.PARTIAL_IMAGE, + scaling_type=Tags.IMAGE_SCALING_SYMMETRIC, spacing_mm=self.spacing, + target_min=self.MIN, target_max=self.MAX), + sp.ImageHeterogeneity(dimx, dimy, dimz, heterogeneity_image=self.PARTIAL_IMAGE, + scaling_type=Tags.IMAGE_SCALING_WRAP, spacing_mm=self.spacing, + target_min=self.MIN, target_max=self.MAX), + sp.ImageHeterogeneity(dimx, dimy, dimz, heterogeneity_image=self.PARTIAL_IMAGE, + scaling_type=Tags.IMAGE_SCALING_EDGE, spacing_mm=self.spacing, + target_min=self.MIN, target_max=self.MAX), + ] + self.HETEROGENEITY_GENERATORS_MEAN_STD = [ + sp.RandomHeterogeneity(dimx, dimy, dimz, spacing_mm=self.spacing, + target_mean=self.MEAN, target_std=self.STD), + sp.RandomHeterogeneity(dimx, dimy, dimz, spacing_mm=self.spacing, target_mean=self.MEAN, target_std=self.STD, + gaussian_blur_size_mm=3), + sp.BlobHeterogeneity(dimx, dimy, dimz, spacing_mm=self.spacing, target_mean=self.MEAN, target_std=self.STD), + sp.ImageHeterogeneity(dimx, dimy, dimz, heterogeneity_image=self.PARTIAL_IMAGE, + scaling_type=Tags.IMAGE_SCALING_CONSTANT, spacing_mm=self.spacing, constant=0.5, + target_mean=self.MEAN, target_std=self.STD), + sp.ImageHeterogeneity(dimx, dimy, dimz, heterogeneity_image=self.PARTIAL_IMAGE, + scaling_type=Tags.IMAGE_SCALING_STRETCH, spacing_mm=self.spacing, + target_mean=self.MEAN, target_std=self.STD), + sp.ImageHeterogeneity(dimx, dimy, dimz, heterogeneity_image=self.PARTIAL_IMAGE, + scaling_type=Tags.IMAGE_SCALING_SYMMETRIC, spacing_mm=self.spacing, + target_mean=self.MEAN, target_std=self.STD), + sp.ImageHeterogeneity(dimx, dimy, dimz, heterogeneity_image=self.PARTIAL_IMAGE, + scaling_type=Tags.IMAGE_SCALING_WRAP, spacing_mm=self.spacing, + target_mean=self.MEAN, target_std=self.STD), + sp.ImageHeterogeneity(dimx, dimy, dimz, heterogeneity_image=self.PARTIAL_IMAGE, + scaling_type=Tags.IMAGE_SCALING_EDGE, spacing_mm=self.spacing, + target_mean=self.MEAN, target_std=self.STD), + ] + + def tearDown(self) -> None: + pass + + def assert_dimension_size(self, heterogeneity_generator, dimx, dimy, dimz): + random_map = heterogeneity_generator.get_map() + map_shape = np.shape(random_map) + self.assertAlmostEqual(dimx, map_shape[0]) + self.assertAlmostEqual(dimy, map_shape[1]) + self.assertAlmostEqual(dimz, map_shape[2]) + + def test_dimension_sizes(self): + dimx, dimy, dimz = self.TEST_SETTINGS.get_volume_dimensions_voxels() + for generator in self.HETEROGENEITY_GENERATORS: + self.assert_dimension_size(generator, dimx, dimy, dimz) + + def assert_min_max(self, heterogeneity_generator): + random_map = heterogeneity_generator.get_map() + self.assertAlmostEqual(np.min(random_map), self.MIN, 5) + self.assertAlmostEqual(np.max(random_map), self.MAX, 5) + + def test_min_max_bounds(self): + for generator in self.HETEROGENEITY_GENERATORS_MIN_MAX: + self.assert_min_max(generator) + + def assert_mean_std(self, heterogeneity_generator): + random_map = heterogeneity_generator.get_map() + self.assertAlmostEqual(np.mean(random_map), self.MEAN, 5) + self.assertAlmostEqual(np.std(random_map), self.STD, 5) + + def test_mean_std_bounds(self): + for generator in self.HETEROGENEITY_GENERATORS_MEAN_STD: + self.assert_mean_std(generator) + + +class TestImageScaling(unittest.TestCase): + """ + A set of tests for the ImageHeterogeneity class, designed to see if the scaling works. + """ + + def setUp(self): + self.spacing = 1.0 + self.MIN = -4.0 + self.MAX = 8.0 + self.PARTIAL_IMAGE = np.zeros((2, 2)) + self.PARTIAL_IMAGE[:, 1] = 1 + self.TOO_BIG_IMAGE = np.zeros((8, 8)) + self.TOO_BIG_IMAGE[:, :: 2] = 1 + self.TEST_SETTINGS = sp.Settings({ + # These parameters set the general properties of the simulated volume + sp.Tags.SPACING_MM: self.spacing, + sp.Tags.DIM_VOLUME_Z_MM: 8, + sp.Tags.DIM_VOLUME_X_MM: 4, + sp.Tags.DIM_VOLUME_Y_MM: 7 + }) + self.dimx, self.dimy, self.dimz = self.TEST_SETTINGS.get_volume_dimensions_voxels() + + def test_stretch(self): + """ + Test to see if the image can be stretched to fill th area, and then the volume. After stretched to fill the + area we should see the furthest two columns keep their values + :return: Assertion for if the image has been stretched + """ + stretched_image = sp.ImageHeterogeneity(self.dimx, self.dimy, self.dimz, heterogeneity_image=self.PARTIAL_IMAGE, + scaling_type=Tags.IMAGE_SCALING_STRETCH, spacing_mm=self.spacing).get_map() + end_equals_1 = np.all(stretched_image[:, :, 6:] == 1) + beginning_equals_0 = np.all(stretched_image[:, :, :1] == 0) + assert end_equals_1 and beginning_equals_0 + + def test_wrap(self): + """ + Test to see if the image can be replicated to fill th area, and then the volume. Even and odd columns will keep + their values + :return: Assertion for if the image has been wrapped to fill the volume + """ + wrapped_image = sp.ImageHeterogeneity(self.dimx, self.dimy, self.dimz, heterogeneity_image=self.PARTIAL_IMAGE, + scaling_type=Tags.IMAGE_SCALING_WRAP, spacing_mm=self.spacing).get_map() + even_equal_1 = np.all(wrapped_image[:, :, 1::2] == 1) + odd_equal_zero = np.all(wrapped_image[:, :, ::2] == 0) + assert even_equal_1 and odd_equal_zero + + def test_edge(self): + """ + Test to see if the image can fill the area by extending the edges, and then the volume. Should observe a line + of zeros and the rest ones. + :return: Assertion for if the image edges have filled the volume + """ + edge_image = sp.ImageHeterogeneity(self.dimx, self.dimy, self.dimz, heterogeneity_image=self.PARTIAL_IMAGE, + scaling_type=Tags.IMAGE_SCALING_EDGE, spacing_mm=self.spacing).get_map() + initially_zero = np.all(edge_image[:, :, 0] == 0) + rest_ones = np.all(edge_image[:, :, 1:] == 1) + assert initially_zero and rest_ones + + def test_constant(self): + """ + Test to see if the image can fill the area with a constant, and then the volume + :return: Assertion for if the image has been filled by a constant + """ + constant_image = sp.ImageHeterogeneity(self.dimx, self.dimy, self.dimz, heterogeneity_image=self.PARTIAL_IMAGE, + scaling_type=Tags.IMAGE_SCALING_CONSTANT, spacing_mm=self.spacing, + constant=0.5).get_map() + initial_area_same = np.all(constant_image[1:3, :, 0] == 0) and np.all(constant_image[1:3, :, 1] == 1) + rest_constant = np.all(constant_image[:, :, 2:] == 0.5) and np.all(constant_image[0, :, :] == 0.5) and \ + np.all(constant_image[3, :, :] == 0.5) + assert initial_area_same and rest_constant + + def test_symmetric(self): + """ + Test to see if the image can fill the area by symmetric reflections, and then the volume. See stripes following + two pixel 1 to 0 patterns + :return: Assertion for if the image has been reflected to fill the volume + """ + symmetric_image = sp.ImageHeterogeneity(self.dimx, self.dimy, self.dimz, heterogeneity_image=self.PARTIAL_IMAGE, + scaling_type=Tags.IMAGE_SCALING_SYMMETRIC, + spacing_mm=self.spacing).get_map() + ones_stripes_working = np.all(symmetric_image[:, :, 1:3] == 1) and np.all(symmetric_image[:, :, 5:7] == 1) + zeros_stripes_working = np.all(symmetric_image[:, :, 0] == 0) and np.all(symmetric_image[:, :, 3:5] == 0) and \ + np.all(symmetric_image[:, :, 7:] == 0) + assert ones_stripes_working and zeros_stripes_working + + def test_crop(self): + """ + Test to see if the image will crop to the desired area, thus leaving the same pattern but in a smaller shape + :return: Assertion for if the image has been cropped to the desired area + """ + crop_image = sp.ImageHeterogeneity(self.dimx, self.dimy, self.dimz, heterogeneity_image=self.TOO_BIG_IMAGE, + spacing_mm=self.spacing, image_pixel_spacing_mm=self.spacing).get_map() + odd_columns_equal_1 = np.all(crop_image[:, :, ::2] == 1) + even_columns_equal_0 = np.all(crop_image[:, :, 1::2] == 0) + size_is_right = np.all(crop_image[:, 1, :].shape == (self.dimx, self.dimz)) + assert odd_columns_equal_1 and even_columns_equal_0 and size_is_right diff --git a/simpa_tests/automatic_tests/test_linear_unmixing.py b/simpa_tests/automatic_tests/test_linear_unmixing.py index fe61e885..d9556f9b 100644 --- a/simpa_tests/automatic_tests/test_linear_unmixing.py +++ b/simpa_tests/automatic_tests/test_linear_unmixing.py @@ -50,7 +50,7 @@ def setUp(self): self.device = sp.PencilBeamIlluminationGeometry() # Run empty pipeline simulation to "fill" hdf5 file following usual procedure - pipeline = [sp.ModelBasedVolumeCreationAdapter(self.settings)] + pipeline = [sp.ModelBasedAdapter(self.settings)] sp.simulate(pipeline, self.settings, self.device) def test(self): @@ -78,11 +78,11 @@ def test(self): lu.run() # Load blood oxygen saturation - lu_results = sp.load_data_field(self.settings[Tags.SIMPA_OUTPUT_PATH], Tags.LINEAR_UNMIXING_RESULT) + lu_results = sp.load_data_field(self.settings[Tags.SIMPA_OUTPUT_FILE_PATH], Tags.LINEAR_UNMIXING_RESULT) self.assert_correct_so2_vales(lu_results["sO2"]) def assert_correct_so2_vales(self, estimates, tolerance=1e-7): - ground_truth = sp.load_data_field(self.settings[Tags.SIMPA_OUTPUT_PATH], Tags.DATA_FIELD_OXYGENATION) + ground_truth = sp.load_data_field(self.settings[Tags.SIMPA_OUTPUT_FILE_PATH], Tags.DATA_FIELD_OXYGENATION) msg = f"expected {estimates.reshape((-1,))[0]} but was {ground_truth.reshape((-1,))[0]}" self.logger.info(msg) self.assertTrue(np.allclose(estimates, ground_truth, atol=tolerance), msg=msg) @@ -113,7 +113,7 @@ def test_non_negative_least_squares(self): lu.run() # Load blood oxygen saturation - lu_results = sp.load_data_field(self.settings[Tags.SIMPA_OUTPUT_PATH], Tags.LINEAR_UNMIXING_RESULT) + lu_results = sp.load_data_field(self.settings[Tags.SIMPA_OUTPUT_FILE_PATH], Tags.LINEAR_UNMIXING_RESULT) self.assert_correct_so2_vales(lu_results["sO2"]) @expectedFailure @@ -164,7 +164,7 @@ def test_subset_of_2_wavelengths(self): lu.run() # assert correct estimates - lu_results = sp.load_data_field(self.settings[Tags.SIMPA_OUTPUT_PATH], Tags.LINEAR_UNMIXING_RESULT) + lu_results = sp.load_data_field(self.settings[Tags.SIMPA_OUTPUT_FILE_PATH], Tags.LINEAR_UNMIXING_RESULT) self.assert_correct_so2_vales(lu_results["sO2"]) def test_subset_of_3_wavelengths(self): @@ -189,7 +189,7 @@ def test_subset_of_3_wavelengths(self): # Run linear unmixing component lu = sp.LinearUnmixing(self.settings, "linear_unmixing") lu.run() - lu_results = sp.load_data_field(self.settings[Tags.SIMPA_OUTPUT_PATH], Tags.LINEAR_UNMIXING_RESULT) + lu_results = sp.load_data_field(self.settings[Tags.SIMPA_OUTPUT_FILE_PATH], Tags.LINEAR_UNMIXING_RESULT) self.assert_correct_so2_vales(lu_results["sO2"]) @expectedFailure @@ -248,12 +248,12 @@ def test_with_all_absorbers(self): # Run linear unmixing component lu = sp.LinearUnmixing(self.settings, "linear_unmixing") lu.run() - lu_results = sp.load_data_field(self.settings[Tags.SIMPA_OUTPUT_PATH], Tags.LINEAR_UNMIXING_RESULT) + lu_results = sp.load_data_field(self.settings[Tags.SIMPA_OUTPUT_FILE_PATH], Tags.LINEAR_UNMIXING_RESULT) self.assert_correct_so2_vales(lu_results["sO2"], tolerance=1e-2) def tearDown(self): # Clean up file after testing - if (os.path.exists(self.settings[Tags.SIMPA_OUTPUT_PATH]) and - os.path.isfile(self.settings[Tags.SIMPA_OUTPUT_PATH])): + if (os.path.exists(self.settings[Tags.SIMPA_OUTPUT_FILE_PATH]) and + os.path.isfile(self.settings[Tags.SIMPA_OUTPUT_FILE_PATH])): # Delete the created file - os.remove(self.settings[Tags.SIMPA_OUTPUT_PATH]) + os.remove(self.settings[Tags.SIMPA_OUTPUT_FILE_PATH]) diff --git a/simpa_tests/automatic_tests/test_noise_models.py b/simpa_tests/automatic_tests/test_noise_models.py index b23ac50d..d6912cd4 100644 --- a/simpa_tests/automatic_tests/test_noise_models.py +++ b/simpa_tests/automatic_tests/test_noise_models.py @@ -13,7 +13,7 @@ from simpa.core.simulation import simulate from simpa.utils import TISSUE_LIBRARY from simpa.io_handling import load_data_field -from simpa import ModelBasedVolumeCreationAdapter +from simpa import ModelBasedAdapter class TestNoiseModels(unittest.TestCase): @@ -63,14 +63,14 @@ def validate_noise_model_results(self, noise_model, noise_model_settings, settings["noise_model_settings"] = noise_model_settings simulation_pipeline = [ - ModelBasedVolumeCreationAdapter(settings), + ModelBasedAdapter(settings), noise_model(settings, "noise_model_settings") ] try: simulate(simulation_pipeline, settings, RSOMExplorerP50(0.1, 1, 1)) - absorption = load_data_field(file_path=settings[Tags.SIMPA_OUTPUT_PATH], + absorption = load_data_field(file_path=settings[Tags.SIMPA_OUTPUT_FILE_PATH], data_field=Tags.DATA_FIELD_ABSORPTION_PER_CM, wavelength=800) actual_mean = np.mean(absorption) @@ -82,10 +82,10 @@ def validate_noise_model_results(self, noise_model, noise_model_settings, np.abs(actual_std - expected_std) / expected_std < error_margin, f"The std was not as expected. Expected {expected_std} but was {actual_std}") finally: - if (os.path.exists(settings[Tags.SIMPA_OUTPUT_PATH]) and - os.path.isfile(settings[Tags.SIMPA_OUTPUT_PATH])): + if (os.path.exists(settings[Tags.SIMPA_OUTPUT_FILE_PATH]) and + os.path.isfile(settings[Tags.SIMPA_OUTPUT_FILE_PATH])): # Delete the created file - os.remove(settings[Tags.SIMPA_OUTPUT_PATH]) + os.remove(settings[Tags.SIMPA_OUTPUT_FILE_PATH]) def setUp(self): diff --git a/simpa_tests/automatic_tests/test_path_manager.py b/simpa_tests/automatic_tests/test_path_manager.py index 11f4e565..f7fbc61c 100644 --- a/simpa_tests/automatic_tests/test_path_manager.py +++ b/simpa_tests/automatic_tests/test_path_manager.py @@ -13,6 +13,7 @@ class TestPathManager(unittest.TestCase): + def setUp(self): self.path = '/path_config.env' self.save_path = "/workplace/data/" @@ -21,7 +22,7 @@ def setUp(self): self.file_content = (f"# Example path_config file. Please define all required paths for your simulation here.\n" f"# Afterwards, either copy this file to your current working directory, to your home directory,\n" f"# or to the SIMPA base directry.\n" - f"SIMPA_SAVE_PATH={self.save_path}\n" + f"SIMPA_SAVE_DIRECTORY={self.save_path}\n" f"MCX_BINARY_PATH={self.mcx_path}\n" f"MATLAB_BINARY_PATH={self.matlab_path}") self.home_file = str(Path.home()) + self.path @@ -33,16 +34,18 @@ def setUp(self): self.simpa_home_exists = os.path.exists(self.simpa_home) @unittest.expectedFailure - def test_variables_not_set(): - path_manager = PathManager() + @patch.dict(os.environ, {Tags.SIMPA_SAVE_DIRECTORY_VARNAME: None, + Tags.MCX_BINARY_PATH_VARNAME: None}) + def test_variables_not_set(self): + path_manager = PathManager("/path/to/nowhere/") _ = path_manager.get_mcx_binary_path() _ = path_manager.get_hdf5_file_save_path() _ = path_manager.get_matlab_binary_path() - @patch.dict(os.environ, {Tags.SIMPA_SAVE_PATH_VARNAME: "test_simpa_save_path", + @patch.dict(os.environ, {Tags.SIMPA_SAVE_DIRECTORY_VARNAME: "test_simpa_save_path", Tags.MCX_BINARY_PATH_VARNAME: "test_mcx_path"}) def test_instantiate_without_file(self): - path_manager = PathManager() + path_manager = PathManager("/path/to/nowhere/") self.assertEqual(path_manager.get_mcx_binary_path(), "test_mcx_path") self.assertEqual(path_manager.get_hdf5_file_save_path(), "test_simpa_save_path") diff --git a/simpa_tests/automatic_tests/test_pipeline.py b/simpa_tests/automatic_tests/test_pipeline.py index 1b290fe9..0e0213c8 100644 --- a/simpa_tests/automatic_tests/test_pipeline.py +++ b/simpa_tests/automatic_tests/test_pipeline.py @@ -9,11 +9,11 @@ import numpy as np from simpa_tests.test_utils import create_test_structure_parameters import os -from simpa import ModelBasedVolumeCreationAdapter -from simpa.core.simulation_modules.optical_simulation_module.optical_forward_model_test_adapter import \ - OpticalForwardModelTestAdapter -from simpa.core.simulation_modules.acoustic_forward_module.acoustic_forward_model_test_adapter import \ - AcousticForwardModelTestAdapter +from simpa import ModelBasedAdapter +from simpa.core.simulation_modules.optical_module.optical_test_adapter import \ + OpticalTestAdapter +from simpa.core.simulation_modules.acoustic_module.acoustic_test_adapter import \ + AcousticTestAdapter from simpa.core.device_digital_twins import RSOMExplorerP50 @@ -72,14 +72,14 @@ def test_pipeline(self): }) simulation_pipeline = [ - ModelBasedVolumeCreationAdapter(settings), - OpticalForwardModelTestAdapter(settings), - AcousticForwardModelTestAdapter(settings), + ModelBasedAdapter(settings), + OpticalTestAdapter(settings), + AcousticTestAdapter(settings), ] simulate(simulation_pipeline, settings, RSOMExplorerP50(0.1, 1, 1)) - if (os.path.exists(settings[Tags.SIMPA_OUTPUT_PATH]) and - os.path.isfile(settings[Tags.SIMPA_OUTPUT_PATH])): + if (os.path.exists(settings[Tags.SIMPA_OUTPUT_FILE_PATH]) and + os.path.isfile(settings[Tags.SIMPA_OUTPUT_FILE_PATH])): # Delete the created file - os.remove(settings[Tags.SIMPA_OUTPUT_PATH]) + os.remove(settings[Tags.SIMPA_OUTPUT_FILE_PATH]) diff --git a/simpa_tests/automatic_tests/test_processing_device.py b/simpa_tests/automatic_tests/test_processing_device.py index fa00b049..f77c74a4 100644 --- a/simpa_tests/automatic_tests/test_processing_device.py +++ b/simpa_tests/automatic_tests/test_processing_device.py @@ -2,7 +2,6 @@ # SPDX-FileCopyrightText: 2021 Janek Groehl # SPDX-License-Identifier: MIT -from simpa.log.file_logger import Logger from simpa.utils.tags import Tags from simpa.utils.settings import Settings from simpa.utils.processing_device import get_processing_device diff --git a/simpa_tests/automatic_tests/tissue_library/test_core_assumptions.py b/simpa_tests/automatic_tests/tissue_library/test_core_assumptions.py index 183d683a..049d6540 100644 --- a/simpa_tests/automatic_tests/tissue_library/test_core_assumptions.py +++ b/simpa_tests/automatic_tests/tissue_library/test_core_assumptions.py @@ -2,22 +2,63 @@ # SPDX-FileCopyrightText: 2021 Janek Groehl # SPDX-License-Identifier: MIT +import inspect import unittest -from simpa.utils import TISSUE_LIBRARY -from simpa.utils.libraries.tissue_library import TissueLibrary + +import numpy as np + +from simpa.utils import Tags, Settings, TISSUE_LIBRARY +from simpa.utils.calculate import calculate_bvf, calculate_oxygenation from simpa.utils.libraries.molecule_library import MolecularComposition -import inspect +from simpa.utils.libraries.tissue_library import TissueLibrary + +TEST_SETTINGS = Settings({ + # These parameters set the general properties of the simulated volume + Tags.SPACING_MM: 1, + Tags.DIM_VOLUME_Z_MM: 4, + Tags.DIM_VOLUME_X_MM: 2, + Tags.DIM_VOLUME_Y_MM: 7 +}) class TestCoreAssumptions(unittest.TestCase): def test_volume_fractions_sum_to_less_or_equal_one(self): for (method_name, method) in self.get_all_tissue_library_methods(): - total_volume_fraction = 0 - for molecule in method(TISSUE_LIBRARY): - total_volume_fraction += molecule.volume_fraction - self.assertAlmostEqual(total_volume_fraction, 1.0, 3, - f"Volume fraction not 1.0 +/- 0.001 for {method_name}") + molecular_composition = method(TISSUE_LIBRARY) + tissue_composition = molecular_composition.get_properties_for_wavelength(TEST_SETTINGS, 800) + total_volume_fraction = tissue_composition.volume_fraction + self.assertTrue((np.abs(total_volume_fraction-1.0) < 1e-3).all(), + f"Volume fraction not 1.0 +/- 0.001 for {method_name}") + + def test_bvf_and_oxygenation_consistency(self): + # blood_volume_fraction (bvf) and oxygenation of tissue classes defined + # as input have to be the same as the calculated ones + + def compare_input_with_calculations(test_tissue, oxy, bvf): + calculated_bvf = calculate_bvf(test_tissue) + calculated_sO2 = calculate_oxygenation(test_tissue) + if bvf < 1e-10: + assert calculated_sO2 is None + assert abs(bvf - calculated_bvf) < 1e-10 + else: + assert abs(oxy - calculated_sO2) < 1e-10 + assert abs(bvf - calculated_bvf) < 1e-10 + + # Test edge cases and a random one + oxy_values = [0., 0., 1., 1., np.random.random()] + bvf_values = [0., 1., 0., 1., np.random.random()] + for oxy in oxy_values: + # assert blood only with varying oxygenation_values + test_tissue = TISSUE_LIBRARY.blood(oxygenation=oxy) + compare_input_with_calculations(test_tissue, oxy, 1.) + # assert all other tissue classes with varying oxygenation- and bvf_values + for bvf in bvf_values: + for (_, method) in self.get_all_tissue_library_methods(): + args = inspect.getfullargspec(method).args + if "background_oxy" in args and "blood_volume_fraction" in args: + test_tissue = method(TISSUE_LIBRARY, background_oxy=oxy, blood_volume_fraction=bvf) + compare_input_with_calculations(test_tissue, oxy, bvf) @staticmethod def get_all_tissue_library_methods(): diff --git a/simpa_tests/manual_tests/__init__.py b/simpa_tests/manual_tests/__init__.py index 79a6a5b1..2ff337b0 100644 --- a/simpa_tests/manual_tests/__init__.py +++ b/simpa_tests/manual_tests/__init__.py @@ -222,7 +222,7 @@ def tear_down(self): def visualise_result(self, show_figure_on_screen=True, save_path=None): initial_pressure = load_data_field( - self.settings[Tags.SIMPA_OUTPUT_PATH], Tags.DATA_FIELD_INITIAL_PRESSURE, + self.settings[Tags.SIMPA_OUTPUT_FILE_PATH], Tags.DATA_FIELD_INITIAL_PRESSURE, wavelength=self.settings[Tags.WAVELENGTH]) plt.figure(figsize=(9, 3)) diff --git a/simpa_tests/manual_tests/acoustic_forward_models/KWaveAcousticForwardConvenienceFunction.py b/simpa_tests/manual_tests/acoustic_forward_models/KWaveAcousticForwardConvenienceFunction.py index a9157c33..de3e6f5d 100644 --- a/simpa_tests/manual_tests/acoustic_forward_models/KWaveAcousticForwardConvenienceFunction.py +++ b/simpa_tests/manual_tests/acoustic_forward_models/KWaveAcousticForwardConvenienceFunction.py @@ -5,9 +5,9 @@ from simpa.core.device_digital_twins import SlitIlluminationGeometry, LinearArrayDetectionGeometry, PhotoacousticDevice from simpa import perform_k_wave_acoustic_forward_simulation -from simpa.core.simulation_modules.reconstruction_module.reconstruction_module_delay_and_sum_adapter import \ +from simpa.core.simulation_modules.reconstruction_module.delay_and_sum_adapter import \ reconstruct_delay_and_sum_pytorch -from simpa import MCXAdapter, ModelBasedVolumeCreationAdapter, \ +from simpa import MCXAdapter, ModelBasedAdapter, \ GaussianNoise from simpa.utils import Tags, Settings, TISSUE_LIBRARY from simpa.core.simulation import simulate @@ -33,7 +33,7 @@ class KWaveAcousticForwardConvenienceFunction(ManualIntegrationTestClass): def setup(self): """ Runs a pipeline consisting of volume creation and optical simulation. The resulting hdf5 file of the - simple test volume is saved at SIMPA_SAVE_PATH location defined in the path_config.env file. + simple test volume is saved at SIMPA_SAVE_DIRECTORY location defined in the path_config.env file. """ self.path_manager = PathManager() @@ -88,13 +88,13 @@ def setup(self): # run pipeline including volume creation and optical mcx simulation self.pipeline = [ - ModelBasedVolumeCreationAdapter(self.settings), + ModelBasedAdapter(self.settings), MCXAdapter(self.settings), GaussianNoise(self.settings, "noise_model") ] def teardown(self): - os.remove(self.settings[Tags.SIMPA_OUTPUT_PATH]) + os.remove(self.settings[Tags.SIMPA_OUTPUT_FILE_PATH]) def perform_test(self): simulate(self.pipeline, self.settings, self.device) @@ -128,7 +128,7 @@ def test_convenience_function(self): self.reconstructed = reconstruct_delay_and_sum_pytorch( time_series_data.copy(), self.device.get_detection_geometry(), speed_of_sound_in_m_per_s=1540, - time_spacing_in_s=1/40_000_000_000, + time_spacing_in_s=1/40_000_000, sensor_spacing_in_mm=self.device.get_detection_geometry().pitch_mm, recon_mode=Tags.RECONSTRUCTION_MODE_PRESSURE, ) diff --git a/simpa_tests/manual_tests/acoustic_forward_models/MinimalKWaveTest.py b/simpa_tests/manual_tests/acoustic_forward_models/MinimalKWaveTest.py index 7f781d5b..0d0fd120 100644 --- a/simpa_tests/manual_tests/acoustic_forward_models/MinimalKWaveTest.py +++ b/simpa_tests/manual_tests/acoustic_forward_models/MinimalKWaveTest.py @@ -27,7 +27,7 @@ def setup(self): if os.path.exists(p0_path): self.initial_pressure = np.load(p0_path)["initial_pressure"] else: - self.initial_pressure = np.zeros((100, 100, 100)) + self.initial_pressure = np.zeros((100, 30, 100)) self.initial_pressure[50, :, 50] = 1 self.speed_of_sound = np.ones((100, 30, 100)) * self.SPEED_OF_SOUND self.density = np.ones((100, 30, 100)) * 1000 @@ -89,31 +89,31 @@ def perform_test(self): Tags.DATA_FIELD_ALPHA_COEFF: self.alpha } save_file_path = generate_dict_path(Tags.SIMULATION_PROPERTIES) - save_hdf5(acoustic_properties, self.settings[Tags.SIMPA_OUTPUT_PATH], save_file_path) + save_hdf5(acoustic_properties, self.settings[Tags.SIMPA_OUTPUT_FILE_PATH], save_file_path) optical_output = { Tags.DATA_FIELD_INITIAL_PRESSURE: {self.settings[Tags.WAVELENGTHS][0]: self.initial_pressure} } optical_output_path = generate_dict_path(Tags.OPTICAL_MODEL_OUTPUT_NAME) - save_hdf5(optical_output, self.settings[Tags.SIMPA_OUTPUT_PATH], optical_output_path) + save_hdf5(optical_output, self.settings[Tags.SIMPA_OUTPUT_FILE_PATH], optical_output_path) KWaveAdapter(self.settings).run(self.pa_device) DelayAndSumAdapter(self.settings).run(self.pa_device) - self.reconstructed_image_1000 = load_data_field(self.settings[Tags.SIMPA_OUTPUT_PATH], + self.reconstructed_image_1000 = load_data_field(self.settings[Tags.SIMPA_OUTPUT_FILE_PATH], data_field=Tags.DATA_FIELD_RECONSTRUCTED_DATA, wavelength=self.settings[Tags.WAVELENGTHS][0]) self.settings.get_reconstruction_settings()[Tags.DATA_FIELD_SPEED_OF_SOUND] = self.SPEED_OF_SOUND * 1.025 DelayAndSumAdapter(self.settings).run(self.pa_device) - self.reconstructed_image_1050 = load_data_field(self.settings[Tags.SIMPA_OUTPUT_PATH], + self.reconstructed_image_1050 = load_data_field(self.settings[Tags.SIMPA_OUTPUT_FILE_PATH], data_field=Tags.DATA_FIELD_RECONSTRUCTED_DATA, wavelength=self.settings[Tags.WAVELENGTHS][0]) self.settings.get_reconstruction_settings()[Tags.DATA_FIELD_SPEED_OF_SOUND] = self.SPEED_OF_SOUND * 0.975 DelayAndSumAdapter(self.settings).run(self.pa_device) - self.reconstructed_image_950 = load_data_field(self.settings[Tags.SIMPA_OUTPUT_PATH], + self.reconstructed_image_950 = load_data_field(self.settings[Tags.SIMPA_OUTPUT_FILE_PATH], data_field=Tags.DATA_FIELD_RECONSTRUCTED_DATA, wavelength=self.settings[Tags.WAVELENGTHS][0]) diff --git a/simpa_tests/manual_tests/digital_device_twins/SimulationWithMSOTInvision.py b/simpa_tests/manual_tests/digital_device_twins/SimulationWithMSOTInvision.py index 397d2da5..f3e32a56 100644 --- a/simpa_tests/manual_tests/digital_device_twins/SimulationWithMSOTInvision.py +++ b/simpa_tests/manual_tests/digital_device_twins/SimulationWithMSOTInvision.py @@ -12,7 +12,7 @@ from simpa.visualisation.matplotlib_data_visualisation import visualise_data import numpy as np from simpa.utils.path_manager import PathManager -from simpa import DelayAndSumAdapter, MCXAdapter, KWaveAdapter, ModelBasedVolumeCreationAdapter, FieldOfViewCropping +from simpa import DelayAndSumAdapter, MCXAdapter, KWaveAdapter, ModelBasedAdapter, FieldOfViewCropping from simpa.core.device_digital_twins import * from simpa_tests.manual_tests import ManualIntegrationTestClass import os @@ -26,7 +26,7 @@ def setup(self): def create_pipeline(self, _settings: Settings): return [ - ModelBasedVolumeCreationAdapter(_settings), + ModelBasedAdapter(_settings), MCXAdapter(_settings), KWaveAdapter(_settings), FieldOfViewCropping(_settings), @@ -194,7 +194,7 @@ def perform_test(self): simulate(simulation_pipeline=pipeline, digital_device_twin=device, settings=self.settings) def tear_down(self): - os.remove(self.settings[Tags.SIMPA_OUTPUT_PATH]) + os.remove(self.settings[Tags.SIMPA_OUTPUT_FILE_PATH]) def visualise_result(self, show_figure_on_screen=True, save_path=None): visualise_data(settings=self.settings, diff --git a/simpa_tests/manual_tests/digital_device_twins/VisualiseDevices.py b/simpa_tests/manual_tests/digital_device_twins/VisualiseDevices.py index b598ae69..02b4ebe9 100644 --- a/simpa_tests/manual_tests/digital_device_twins/VisualiseDevices.py +++ b/simpa_tests/manual_tests/digital_device_twins/VisualiseDevices.py @@ -19,12 +19,21 @@ def tear_down(self): pass def visualise_result(self, show_figure_on_screen=True, save_path=None): + if show_figure_on_screen: + figure_save_path = [None, None, None] + else: + if save_path is None: + save_path = "" + figure_save_path = [save_path + "device_visualisation_MSOT_Acuity.png", + save_path + "device_visualisation_MSOT_Invision.png", + save_path + "device_visualisation_RSOM_Explorer.png" + ] sp.visualise_device(sp.MSOTAcuityEcho(device_position_mm=np.asarray([50, 10, 0])), - save_path + "device_visualisation_MSOT_Acuity.png") + figure_save_path[0]) sp.visualise_device(sp.InVision256TF(device_position_mm=np.asarray([50, 10, 50])), - save_path + "device_visualisation_MSOT_Invision.png") + figure_save_path[1]) sp.visualise_device(sp.RSOMExplorerP50(device_position_mm=np.asarray([50, 10, 0])), - save_path + "device_visualisation_RSOM_Explorer.png") + figure_save_path[2]) if __name__ == "__main__": diff --git a/simpa_tests/manual_tests/executables/MATLABAdditionalFlags.py b/simpa_tests/manual_tests/executables/MATLABAdditionalFlags.py new file mode 100644 index 00000000..2c006ac5 --- /dev/null +++ b/simpa_tests/manual_tests/executables/MATLABAdditionalFlags.py @@ -0,0 +1,149 @@ +# SPDX-FileCopyrightText: 2021 Division of Intelligent Medical Systems, DKFZ +# SPDX-FileCopyrightText: 2021 Janek Groehl +# SPDX-License-Identifier: MIT + +import os +import numpy as np +from simpa import MCXAdapter, ModelBasedAdapter, simulate, KWaveAdapter +from simpa.core.device_digital_twins import PhotoacousticDevice, PencilBeamIlluminationGeometry, LinearArrayDetectionGeometry +from simpa.utils import Settings, Tags, TISSUE_LIBRARY, PathManager +from simpa_tests.manual_tests import ManualIntegrationTestClass + + +class MATLABAdditionalFlags(ManualIntegrationTestClass): + """ + Tests if using Tags.ADDITIONAL_FLAGS to set additional flags for MATLAB works in the KWaveAdapter. + """ + + def create_example_tissue(self): + """ + Creates a very simple example tissue with only background tissue. + """ + + background_dictionary = Settings() + background_dictionary[Tags.MOLECULE_COMPOSITION] = TISSUE_LIBRARY.constant(0.1, 100, 0.9) + background_dictionary[Tags.STRUCTURE_TYPE] = Tags.BACKGROUND + tissue_dict = Settings() + tissue_dict[Tags.BACKGROUND] = background_dictionary + + return tissue_dict + + def setup(self): + """ + Creates basic simulation settings and a simulation device. + """ + + path_manager = PathManager() + + self.settings = Settings({ + Tags.WAVELENGTHS: [800], + Tags.WAVELENGTH: 800, + Tags.VOLUME_NAME: "AdditionalFlagsTest", + Tags.SIMULATION_PATH: path_manager.get_hdf5_file_save_path(), + Tags.SPACING_MM: 1, + Tags.DIM_VOLUME_X_MM: 100, + Tags.DIM_VOLUME_Y_MM: 100, + Tags.DIM_VOLUME_Z_MM: 100, + Tags.RANDOM_SEED: 4711, + Tags.GPU: True + }) + + self.settings.set_volume_creation_settings({ + Tags.SIMULATE_DEFORMED_LAYERS: True, + Tags.STRUCTURES: self.create_example_tissue() + }) + self.settings.set_optical_settings({ + Tags.OPTICAL_MODEL_NUMBER_PHOTONS: 1e7, + Tags.OPTICAL_MODEL_BINARY_PATH: path_manager.get_mcx_binary_path(), + Tags.OPTICAL_MODEL: Tags.OPTICAL_MODEL_MCX, + Tags.ILLUMINATION_TYPE: Tags.ILLUMINATION_TYPE_PENCIL, + Tags.LASER_PULSE_ENERGY_IN_MILLIJOULE: 50, + Tags.MCX_ASSUMED_ANISOTROPY: 0.9 + }) + self.settings.set_acoustic_settings({ + Tags.ACOUSTIC_SIMULATION_3D: False, + Tags.ACOUSTIC_MODEL_BINARY_PATH: path_manager.get_matlab_binary_path(), + Tags.KWAVE_PROPERTY_ALPHA_POWER: 0.00, + Tags.KWAVE_PROPERTY_SENSOR_RECORD: "p", + Tags.KWAVE_PROPERTY_PMLInside: False, + Tags.KWAVE_PROPERTY_PMLSize: [31, 32], + Tags.KWAVE_PROPERTY_PMLAlpha: 1.5, + Tags.KWAVE_PROPERTY_PlotPML: False, + Tags.RECORDMOVIE: False, + Tags.MOVIENAME: "visualization_log", + Tags.ACOUSTIC_LOG_SCALE: True + }) + + self.device = PhotoacousticDevice(device_position_mm=np.asarray([self.settings[Tags.DIM_VOLUME_X_MM] / 2 - 0.5, + self.settings[Tags.DIM_VOLUME_Y_MM] / 2 - 0.5, + 0])) + self.device.add_illumination_geometry(PencilBeamIlluminationGeometry()) + self.device.set_detection_geometry(LinearArrayDetectionGeometry(device_position_mm=self.device.device_position_mm, + pitch_mm=0.25, + number_detector_elements=100, + field_of_view_extent_mm=np.asarray([-15, 15, 0, 0, 0, 20]))) + + output_name = f'{os.path.join(self.settings[Tags.SIMULATION_PATH], self.settings[Tags.VOLUME_NAME])}' + self.output_file_name = f'{output_name}.log' + + def run_simulation(self): + # run pipeline including volume creation and optical mcx simulation and acoustic matlab kwave simulation + pipeline = [ + ModelBasedAdapter(self.settings), + MCXAdapter(self.settings), + KWaveAdapter(self.settings) + ] + simulate(pipeline, self.settings, self.device) + + def test_execution_of_additional_flag(self): + """Tests if log file is created by setting additional parameters + + :raises FileNotFoundError: if log file does not exist at expected location + """ + + # perform cleaning before test + if os.path.exists(self.output_file_name): + os.remove(self.output_file_name) + + # run simulation + self.settings.get_acoustic_settings()[Tags.ADDITIONAL_FLAGS] = ['-logfile', self.output_file_name] + self.run_simulation() + + # checking if file exists afterwards + if not os.path.exists(self.output_file_name): + raise FileNotFoundError(f"Log file wasn't created at expected path {self.output_file_name}") + + def test_if_last_flag_is_used(self): + """Tests if log file is created with correct last given name by setting multiple additional parameters + + :raises FileNotFoundError: if correct log file does not exist at expected location + """ + + # perform cleaning before test + if os.path.exists(self.output_file_name): + os.remove(self.output_file_name) + + # run simulation + self.settings.get_acoustic_settings()[Tags.ADDITIONAL_FLAGS] = [ + '-logfile', 'temp_name', '-logfile', self.output_file_name] + self.run_simulation() + + # checking if file exists afterwards + if not os.path.exists(self.output_file_name): + raise FileNotFoundError( + f"Log file wasn't created with correct last given name at expected path {self.output_file_name}") + + def perform_test(self): + """ + Calls all individual tests of this class + """ + self.test_execution_of_additional_flag() + self.test_if_last_flag_is_used() + + def visualise_result(self, show_figure_on_screen=True, save_path=None): + pass # no figures are created that could be visualized + + +if __name__ == '__main__': + test = MATLABAdditionalFlags() + test.run_test(show_figure_on_screen=False) diff --git a/simpa_tests/manual_tests/executables/MCXAdditionalFlags.py b/simpa_tests/manual_tests/executables/MCXAdditionalFlags.py new file mode 100644 index 00000000..b9e1768b --- /dev/null +++ b/simpa_tests/manual_tests/executables/MCXAdditionalFlags.py @@ -0,0 +1,128 @@ +# SPDX-FileCopyrightText: 2021 Division of Intelligent Medical Systems, DKFZ +# SPDX-FileCopyrightText: 2021 Janek Groehl +# SPDX-License-Identifier: MIT + +import os +import numpy as np +from simpa import MCXAdapter, ModelBasedAdapter, simulate +from simpa.core.device_digital_twins import PhotoacousticDevice, PencilBeamIlluminationGeometry +from simpa.utils import Settings, Tags, TISSUE_LIBRARY, PathManager +from simpa_tests.manual_tests import ManualIntegrationTestClass + + +class MCXAdditionalFlags(ManualIntegrationTestClass): + + def create_example_tissue(self): + """ + Creates a very simple example tissue with only background tissue. + """ + + background_dictionary = Settings() + background_dictionary[Tags.MOLECULE_COMPOSITION] = TISSUE_LIBRARY.constant(0.1, 100, 0.9) + background_dictionary[Tags.STRUCTURE_TYPE] = Tags.BACKGROUND + tissue_dict = Settings() + tissue_dict[Tags.BACKGROUND] = background_dictionary + + return tissue_dict + + def setup(self): + """ + Creates basic simulation settings and a simulation device. + """ + + path_manager = PathManager() + + self.settings = Settings({ + Tags.WAVELENGTHS: [800], + Tags.WAVELENGTH: 800, + Tags.VOLUME_NAME: "AdditionalFlagsTest", + Tags.SIMULATION_PATH: path_manager.get_hdf5_file_save_path(), + Tags.SPACING_MM: 1, + Tags.DIM_VOLUME_X_MM: 100, + Tags.DIM_VOLUME_Y_MM: 100, + Tags.DIM_VOLUME_Z_MM: 100, + Tags.RANDOM_SEED: 4711 + }) + + self.settings.set_volume_creation_settings({ + Tags.SIMULATE_DEFORMED_LAYERS: True, + Tags.STRUCTURES: self.create_example_tissue() + }) + self.settings.set_optical_settings({ + Tags.OPTICAL_MODEL_NUMBER_PHOTONS: 1e7, + Tags.OPTICAL_MODEL_BINARY_PATH: path_manager.get_mcx_binary_path(), + Tags.OPTICAL_MODEL: Tags.OPTICAL_MODEL_MCX, + Tags.ILLUMINATION_TYPE: Tags.ILLUMINATION_TYPE_PENCIL, + Tags.LASER_PULSE_ENERGY_IN_MILLIJOULE: 50, + Tags.MCX_ASSUMED_ANISOTROPY: 0.9 + }) + + self.device = PhotoacousticDevice(device_position_mm=np.asarray([self.settings[Tags.DIM_VOLUME_X_MM] / 2 - 0.5, + self.settings[Tags.DIM_VOLUME_Y_MM] / 2 - 0.5, + 0])) + self.device.add_illumination_geometry(PencilBeamIlluminationGeometry()) + + self.output_name = f'{os.path.join(self.settings[Tags.SIMULATION_PATH], self.settings[Tags.VOLUME_NAME])}_output' + self.output_file_name = f'{self.output_name}.log' + + def run_simulation(self): + # run pipeline including volume creation and optical mcx simulation + pipeline = [ + ModelBasedAdapter(self.settings), + MCXAdapter(self.settings), + ] + simulate(pipeline, self.settings, self.device) + + def test_execution_of_additional_flag(self): + """Tests if log file is created by setting additional parameters + + :raises FileNotFoundError: if log file does not exist at expected location + """ + + # perform cleaning before test + if os.path.exists(self. output_file_name): + os.remove(self.output_file_name) + + # run simulation + self.settings.get_optical_settings()[Tags.ADDITIONAL_FLAGS] = ['-l', 1, '-s', self.output_name] + self.run_simulation() + + # checking if file exists afterwards + if not os.path.exists(self.output_file_name): + raise FileNotFoundError(f"Log file wasn't created at expected path {self.output_file_name}") + + def test_if_last_flag_is_used(self): + """Tests if log file is created with correct last given name by setting multiple additional parameters + + :raises FileNotFoundError: if correct log file does not exist at expected location + """ + output_name = f'{os.path.join(self.settings[Tags.SIMULATION_PATH], self.settings[Tags.VOLUME_NAME])}_output' + output_file_name = f'{output_name}.log' + + # perform cleaning before test + if os.path.exists(output_file_name): + os.remove(output_file_name) + + # run simulation + self.settings.get_optical_settings()[Tags.ADDITIONAL_FLAGS] = ['-l', 1, '-s', 'temp_name', '-s', output_name] + self.run_simulation() + + # checking if file exists afterwards + if not os.path.exists(output_file_name): + raise FileNotFoundError( + f"Log file wasn't created with correct last given name at expected path {output_file_name}") + + def perform_test(self): + """ + Calls all individual tests of this class + """ + self.test_execution_of_additional_flag() + self.test_if_last_flag_is_used() + + def visualise_result(self, show_figure_on_screen=True, save_path=None): + pass # no figures are created that could be visualized + + +if __name__ == '__main__': + test = MCXAdditionalFlags() + test.run_test(show_figure_on_screen=False) diff --git a/simpa_tests/manual_tests/generate_overview.py b/simpa_tests/manual_tests/generate_overview.py new file mode 100644 index 00000000..e99cf978 --- /dev/null +++ b/simpa_tests/manual_tests/generate_overview.py @@ -0,0 +1,379 @@ +# SPDX-FileCopyrightText: 2021 Division of Intelligent Medical Systems, DKFZ +# SPDX-FileCopyrightText: 2021 Janek Groehl +# SPDX-License-Identifier: MIT + +import ast +import glob +import importlib +import inspect +import logging +import os +import shutil +import sys +import zipfile +from importlib.metadata import version + +import pypandoc +import requests +from mdutils.mdutils import MdUtils + +from simpa.log import Logger + + +class GenerateOverview(): + """ + Runs all scripts automatically and takes the created images and compares it with reference images. + """ + + def __init__(self, verbose: bool = False, save_path: str = None): + self.verbosity = verbose + self.logger = Logger() + self.import_path = "simpa_tests.manual_tests" + # directory of this script, i.e ~/simpa/simpa_tests/manual_tests + self.current_dir = os.path.dirname(os.path.realpath(__file__)) + self.file_name = os.path.basename(__file__) + self.reference_figures_path = os.path.join(self.current_dir, "reference_figures/") + if save_path == None: + self.save_path = os.path.join(self.current_dir, "figures/") + else: + self.save_path = save_path + os.makedirs(self.save_path, exist_ok=True) + self.logger.debug(f"Created {self.save_path} directory") + self.md_name = 'manual_tests_overview' + self.mdFile = MdUtils(file_name=self.md_name, title='Overview of Manual Test Results') + self.set_style() + + # If you manually want to neglect a specific manual test enter the python script name here + self.scripts_to_neglect = [] + + def download_reference_images(self): + """ + Removes the current reference figures directory and downloads the latest references from nextcloud. + + :return: None + """ + ref_imgs_path = os.path.join(self.current_dir, "reference_figures") + if os.path.exists(ref_imgs_path): + # Remove the directory + shutil.rmtree(ref_imgs_path) + # nextcloud url with the reference images + self.nextcloud_url = "https://hub.dkfz.de/s/nsXKMGAaN6tPPsq" # shared "reference_figures" folder on nextcloud + # Specify the local directory to save the files + zip_filepath = os.path.join(self.current_dir, "downloaded.zip") + # Construct the download URL based on the public share link + download_url = self.nextcloud_url.replace('/s/', '/index.php/s/') + '/download' + # Send a GET request to download the file + self.logger.debug(f'Download folder with reference figures from nextcloud...') + response = requests.get(download_url) + if response.status_code == 200: + # Save the file + with open(zip_filepath, 'wb') as f: + f.write(response.content) + self.logger.debug(f'File downloaded successfully and stored at {zip_filepath}.') + else: + self.logger.critical(f'Failed to download file. Status code: {response.status_code}') + raise requests.exceptions.HTTPError(f'Failed to download file. Status code: {response.status_code}') + + # Open the zip file + with zipfile.ZipFile(zip_filepath, 'r') as zip_ref: + # Extract all the contents into the specified directory + zip_ref.extractall(self.current_dir) + + self.logger.debug(f'Files extracted to {self.current_dir}') + + # Remove the zip file after extraction + os.remove(zip_filepath) + self.logger.debug(f'{zip_filepath} removed successfully.') + + def clean_dir(self, dir): + """ + Removes scripts from the directory list that should not be run. + + :param dir: List of files or directories. + :type dir: list + + :return: None + """ + + # do not execute the following files in the manual_tests folder + to_be_ignored = ["__pycache__", "__init__.py", self.file_name, "test_data", "utils.py", + "manual_tests_overview.md", "manual_tests_overview.pdf", "manual_tests_overview.html", + "figures", "reference_figures", "path_config.env", "version.txt"] + + for name in to_be_ignored: + try: + dir.remove(name) + except ValueError: + pass + + def log_version(self): + """ + Logs the current 'simpa' version to a file and compares it with a reference version. + + :return: None + """ + self.simpa_version = version("simpa") + with open(os.path.join(self.save_path, "simpa_version.txt"), "w") as file: + file.write(self.simpa_version) + + ref_version_path = os.path.join(self.reference_figures_path, "simpa_version.txt") + try: + with open(ref_version_path, 'r') as file: + reference_sp_version = file.read() + self.mdFile.write(f""" +SIMPA versions:
\n\n + + + + + + + + + +
Reference simpa version:{reference_sp_version}
Your simpa version:{self.simpa_version}
+""") + if self.simpa_version != reference_sp_version: + self.logger.debug( + "Your simpa version does not match with the simpa version used for generating the reference figures") + except FileNotFoundError: + self.logger.warning(f"The reference simpa version file at {ref_version_path} was not found") + except IOError: + self.logger.warning(f"An error occurred while reading the file at {ref_version_path}") + + def run_manual_tests(self, run_tests: bool = True): + """ + runs all the scripts and creates md file with the results figures + + :param run_tests: if true scripts are executed + :type run_tests: bool + + :return: None + """ + self.logger.debug(f"Neglect the following files: {self.scripts_to_neglect}") + + directories = os.listdir(self.current_dir) + directories.sort() + self.clean_dir(directories) + + for dir_num, dir_ in enumerate(directories): + self.logger.debug(f"Enter dir: {dir_}") + dir_title = f"{dir_num+1}. " + dir_.replace("_", " ").capitalize() + self.mdFile.new_header(level=1, title=dir_title) + files = os.listdir(os.path.join(self.current_dir, dir_)) + files.sort() + self.clean_dir(files) + + # iterate through scripts + for file_num, file in enumerate(files): + self.logger.debug(f"Enter file: {file}") + test_save_path = os.path.join(self.save_path, file.split(".py")[0] + "/") + os.makedirs(test_save_path, exist_ok=True) + + if file in self.scripts_to_neglect: + self.logger.debug(f"{file} has bug or is not compatible and has to be neglected") + continue + + file_title = f"{dir_num+1}.{file_num+1} " + file.split(".py")[0] + self.mdFile.new_header(level=2, title=file_title) + + global_path = os.path.join(self.current_dir, dir_, file) + module_name = ".".join([self.import_path, dir_, file.split(".")[0]]) + + # execute all manual test scripts + try: + self.logger.debug(f"import module {module_name}") + module = importlib.import_module(module_name) + + # run all test classes of the current python source code + with open(global_path, 'r', encoding='utf-8') as source: + p = ast.parse(source.read()) + classes = [node.name for node in ast.walk(p) if isinstance(node, ast.ClassDef)] + for class_name in classes: + self.logger.debug(f"Run {class_name}") + + class_ = getattr(module, class_name) + + # write class documentation string in the markdown file + class_doc = inspect.getdoc(class_) + self.mdFile.write("- Description:
") + self.mdFile.write(str(class_doc)) + + # run the manual test + test_object = class_() + if run_tests: + if not self.verbosity: + self.deafen(test_object.run_test, show_figure_on_screen=False, + save_path=test_save_path) + else: + test_object.run_test(show_figure_on_screen=False, save_path=test_save_path) + except Exception as e: + self.logger.warning(f"Error Name: {type(e).__name__}") + self.logger.warning(f"Error Message: {e}") + self.mdFile.write( + f"\n- ERROR occured:
- Error: {type(e).__name__}
- Error message: {e}\n") + + # Write comparison of reference image and new generated image in markdown file + self.mdFile.write("\n- Comparison of reference and generated image:
\n") + try: + reference_folder = os.path.join(self.reference_figures_path, os.path.splitext(file)[0]) + ref_img_list = glob.glob(os.path.join(reference_folder, "*.png")) + if len(ref_img_list) == 0: + self.logger.warning("No reference image found") + ref_img_list.sort() + for ref_img_path in ref_img_list: + img_name = os.path.basename(ref_img_path) + img_path = os.path.join(test_save_path, img_name) + self.create_comparison_html_table(ref_img_path, img_path) + except: + self.mdFile.write("Could not load any figures.") + + # Create a table of contents + self.mdFile.new_table_of_contents(table_title='Contents', depth=2) + self.logger.debug(f"Saving md file in {os.getcwd()=}") + self.mdFile.create_md_file() + + # Helper Functions + def create_comparison_html_table(self, img1_path=None, img2_path=None): + """ + Creates an HTML table to compare two images, with optional size specification. + + :param img1_path: Path to the first image. + :type img1_path: str or None + :param img2_path: Path to the second image. + :type img2_path: str or None + + :return: None + """ + specify_size = False + if specify_size: + self.mdFile.write(f""" + + + + + + + + + +
ReferenceGenerated
+""") + else: + self.mdFile.write(f""" + + + + + + + + + +
ReferenceGenerated
+""") + + def set_style(self): + """ + Writes CSS styles to the Markdown file for image and header formatting, including zoom functionality. + + :return: None + """ + self.mdFile.write(""" + +""") + + def deafen(self, method, **kwargs): + """ + Suppresses output and logging temporarily while executing a specified method. + + :param method: The method to execute with suppressed output and logging. + :type method: callable + :param kwargs: Keyword arguments to pass to the method. + :type kwargs: dict + + :return: None + """ + + os.system("set -v") + self.logger._logger.setLevel(logging.CRITICAL) + real_stdout = sys.stdout + sys.stdout = open(os.devnull, "w") + method(**kwargs) + sys.stdout = real_stdout + self.logger._logger.setLevel(logging.DEBUG) + os.system("set +v") + + def create_html(self): + """ + Creates an HTML table to compare generated and reference figures. + + :return: None + """ + try: + self.logger.debug(f"Saving html file in {os.getcwd()=}") + with open(os.path.join(os.getcwd(), self.md_name+".html"), "w") as html_file: + text = pypandoc.convert_text(self.mdFile.get_md_text(), "html", format="md", + extra_args=['--markdown-headings=atx']) + updated_text = "" + for row in text.split("\n"): + if 'href="#' in row: + id1 = row.find("#") + id2 = row.find("-") + row = row.replace(row[id1+1:id2+1], "") + updated_text += (row+"\n") + elif self.reference_figures_path in row: + updated_text += '
\n\n
' + elif self.save_path in row: + updated_text += '
\n\n
' + else: + updated_text += (row+"\n") + html_file.write(updated_text) + # pypandoc.convert_file(self.md_name + ".md", 'html', outputfile=self.md_name + '.html') + except Exception as e: + self.logger.warning("Check installation of needed requirements (pypandoc, pypandoc_binary).") + + +if __name__ == '__main__': + automatic_manual_tests = GenerateOverview() + automatic_manual_tests.download_reference_images() + automatic_manual_tests.log_version() + automatic_manual_tests.run_manual_tests(run_tests=True) + automatic_manual_tests.create_html() diff --git a/simpa_tests/manual_tests/image_reconstruction/DelayAndSumReconstruction.py b/simpa_tests/manual_tests/image_reconstruction/DelayAndSumReconstruction.py index 0f29f65d..b73677f0 100644 --- a/simpa_tests/manual_tests/image_reconstruction/DelayAndSumReconstruction.py +++ b/simpa_tests/manual_tests/image_reconstruction/DelayAndSumReconstruction.py @@ -8,7 +8,7 @@ from simpa.io_handling import load_data_field from simpa.core.simulation import simulate from simpa import KWaveAdapter, MCXAdapter, \ - DelayAndSumAdapter, ModelBasedVolumeCreationAdapter, GaussianNoise + DelayAndSumAdapter, ModelBasedAdapter, GaussianNoise from simpa import reconstruct_delay_and_sum_pytorch from simpa_tests.manual_tests import ReconstructionAlgorithmTestBaseClass @@ -28,7 +28,7 @@ def test_reconstruction_of_simulation(self): self.device.update_settings_for_use_of_model_based_volume_creator(self.settings) SIMUATION_PIPELINE = [ - ModelBasedVolumeCreationAdapter(self.settings), + ModelBasedAdapter(self.settings), MCXAdapter(self.settings), GaussianNoise(self.settings, "noise_initial_pressure"), KWaveAdapter(self.settings), @@ -37,19 +37,19 @@ def test_reconstruction_of_simulation(self): simulate(SIMUATION_PIPELINE, self.settings, self.device) - self.reconstructed_image_pipeline = load_data_field(self.settings[Tags.SIMPA_OUTPUT_PATH], Tags.DATA_FIELD_RECONSTRUCTED_DATA, + self.reconstructed_image_pipeline = load_data_field(self.settings[Tags.SIMPA_OUTPUT_FILE_PATH], Tags.DATA_FIELD_RECONSTRUCTED_DATA, self.settings[Tags.WAVELENGTH]) def test_convenience_function(self): # Load simulated time series data - time_series_sensor_data = load_data_field(self.settings[Tags.SIMPA_OUTPUT_PATH], + time_series_sensor_data = load_data_field(self.settings[Tags.SIMPA_OUTPUT_FILE_PATH], Tags.DATA_FIELD_TIME_SERIES_DATA, self.settings[Tags.WAVELENGTH]) reconstruction_settings = self.settings.get_reconstruction_settings() # reconstruct via convenience function self.reconstructed_image_convenience = reconstruct_delay_and_sum_pytorch(time_series_sensor_data, self.device.get_detection_geometry(), reconstruction_settings[Tags.DATA_FIELD_SPEED_OF_SOUND], 1.0 / ( - self.device.get_detection_geometry().sampling_frequency_MHz * 1000), self.settings[Tags.SPACING_MM], reconstruction_settings[Tags.RECONSTRUCTION_MODE], reconstruction_settings[Tags.RECONSTRUCTION_APODIZATION_METHOD]) + self.device.get_detection_geometry().sampling_frequency_MHz * 1_000_000), self.settings[Tags.SPACING_MM], reconstruction_settings[Tags.RECONSTRUCTION_MODE], reconstruction_settings[Tags.RECONSTRUCTION_APODIZATION_METHOD]) # apply envelope detection method if set if reconstruction_settings[Tags.RECONSTRUCTION_BMODE_AFTER_RECONSTRUCTION]: diff --git a/simpa_tests/manual_tests/image_reconstruction/DelayMultiplyAndSumReconstruction.py b/simpa_tests/manual_tests/image_reconstruction/DelayMultiplyAndSumReconstruction.py index 7a0de0d5..66f4b84d 100644 --- a/simpa_tests/manual_tests/image_reconstruction/DelayMultiplyAndSumReconstruction.py +++ b/simpa_tests/manual_tests/image_reconstruction/DelayMultiplyAndSumReconstruction.py @@ -7,7 +7,7 @@ from simpa.io_handling import load_data_field from simpa.core.simulation import simulate from simpa import KWaveAdapter, MCXAdapter, \ - DelayMultiplyAndSumAdapter, ModelBasedVolumeCreationAdapter, GaussianNoise + DelayMultiplyAndSumAdapter, ModelBasedAdapter, GaussianNoise from simpa import reconstruct_delay_multiply_and_sum_pytorch from simpa_tests.manual_tests import ReconstructionAlgorithmTestBaseClass @@ -27,7 +27,7 @@ def test_reconstruction_of_simulation(self): self.device.update_settings_for_use_of_model_based_volume_creator(self.settings) SIMUATION_PIPELINE = [ - ModelBasedVolumeCreationAdapter(self.settings), + ModelBasedAdapter(self.settings), MCXAdapter(self.settings), GaussianNoise(self.settings, "noise_initial_pressure"), KWaveAdapter(self.settings), @@ -36,19 +36,19 @@ def test_reconstruction_of_simulation(self): simulate(SIMUATION_PIPELINE, self.settings, self.device) - self.reconstructed_image_pipeline = load_data_field(self.settings[Tags.SIMPA_OUTPUT_PATH], Tags.DATA_FIELD_RECONSTRUCTED_DATA, + self.reconstructed_image_pipeline = load_data_field(self.settings[Tags.SIMPA_OUTPUT_FILE_PATH], Tags.DATA_FIELD_RECONSTRUCTED_DATA, self.settings[Tags.WAVELENGTH]) def test_convenience_function(self): # Load simulated time series data - time_series_sensor_data = load_data_field(self.settings[Tags.SIMPA_OUTPUT_PATH], + time_series_sensor_data = load_data_field(self.settings[Tags.SIMPA_OUTPUT_FILE_PATH], Tags.DATA_FIELD_TIME_SERIES_DATA, self.settings[Tags.WAVELENGTH]) reconstruction_settings = self.settings.get_reconstruction_settings() # reconstruct via convenience function self.reconstructed_image_convenience = reconstruct_delay_multiply_and_sum_pytorch(time_series_sensor_data, self.device.get_detection_geometry(), reconstruction_settings[Tags.DATA_FIELD_SPEED_OF_SOUND], 1.0 / ( - self.device.get_detection_geometry().sampling_frequency_MHz * 1000), self.settings[Tags.SPACING_MM], reconstruction_settings[Tags.RECONSTRUCTION_MODE], reconstruction_settings[Tags.RECONSTRUCTION_APODIZATION_METHOD]) + self.device.get_detection_geometry().sampling_frequency_MHz * 1_000_000), self.settings[Tags.SPACING_MM], reconstruction_settings[Tags.RECONSTRUCTION_MODE], reconstruction_settings[Tags.RECONSTRUCTION_APODIZATION_METHOD]) # apply envelope detection method if set if reconstruction_settings[Tags.RECONSTRUCTION_BMODE_AFTER_RECONSTRUCTION]: diff --git a/simpa_tests/manual_tests/image_reconstruction/PointSourceReconstruction.py b/simpa_tests/manual_tests/image_reconstruction/PointSourceReconstruction.py index e59f35e3..83cc94b2 100644 --- a/simpa_tests/manual_tests/image_reconstruction/PointSourceReconstruction.py +++ b/simpa_tests/manual_tests/image_reconstruction/PointSourceReconstruction.py @@ -2,275 +2,323 @@ # SPDX-FileCopyrightText: 2021 Janek Groehl # SPDX-License-Identifier: MIT -from simpa.utils import Tags +# FIXME temporary workaround for newest Intel architectures +import os -from simpa.core.simulation import simulate -from simpa.utils.settings import Settings -from simpa.utils.libraries.molecule_library import Molecule, MolecularCompositionGenerator -from simpa.utils.libraries.spectrum_library import AbsorptionSpectrumLibrary, AnisotropySpectrumLibrary, \ - ScatteringSpectrumLibrary -from simpa.visualisation.matplotlib_data_visualisation import visualise_data import numpy as np -from simpa.utils.path_manager import PathManager -from simpa import DelayAndSumAdapter, MCXAdapter, KWaveAdapter, ModelBasedVolumeCreationAdapter, FieldOfViewCropping + +from simpa import (DelayAndSumAdapter, FieldOfViewCropping, KWaveAdapter, + MCXAdapter, ModelBasedAdapter) from simpa.core.device_digital_twins import * +from simpa.core.simulation import simulate from simpa.io_handling import load_data_field +from simpa.utils import Tags +from simpa.utils.libraries.molecule_library import ( + MolecularCompositionGenerator, Molecule) +from simpa.utils.libraries.spectrum_library import (AbsorptionSpectrumLibrary, + AnisotropySpectrumLibrary, + ScatteringSpectrumLibrary) +from simpa.utils.path_manager import PathManager +from simpa.utils.settings import Settings +from simpa.visualisation.matplotlib_data_visualisation import visualise_data +from simpa_tests.manual_tests import ReconstructionAlgorithmTestBaseClass -# FIXME temporary workaround for newest Intel architectures -import os os.environ["KMP_DUPLICATE_LIB_OK"] = "TRUE" -SPEED_OF_SOUND = 1470 -VOLUME_TRANSDUCER_DIM_IN_MM = 90 -VOLUME_PLANAR_DIM_IN_MM = 20 -VOLUME_HEIGHT_IN_MM = 90 -SPACING = 0.4 -RANDOM_SEED = 4711 -# TODO: Please make sure that a valid path_config.env file is located in your home directory, or that you -# point to the correct file in the PathManager(). -path_manager = PathManager() - -# If VISUALIZE is set to True, the simulation result will be plotted -VISUALIZE = True - - -def create_point_source(): +class PointSourceReconstruction(ReconstructionAlgorithmTestBaseClass): """ - This is a very simple example script of how to create a tissue definition. - It contains a muscular background, an epidermis layer on top of the muscles - and a blood vessel. + TODO + """ - background_molecule = Molecule( - volume_fraction=1.0, - absorption_spectrum=AbsorptionSpectrumLibrary.CONSTANT_ABSORBER_ARBITRARY(1e-10), - scattering_spectrum=ScatteringSpectrumLibrary.CONSTANT_SCATTERING_ARBITRARY(1e-10), - anisotropy_spectrum=AnisotropySpectrumLibrary.CONSTANT_ANISOTROPY_ARBITRARY(1.0), - speed_of_sound=SPEED_OF_SOUND - ) - background_dictionary = Settings() - background_dictionary[Tags.MOLECULE_COMPOSITION] = (MolecularCompositionGenerator(). - append(background_molecule). - get_molecular_composition(-1)) - background_dictionary[Tags.STRUCTURE_TYPE] = Tags.BACKGROUND - - vessel_molecule = Molecule( - volume_fraction=1.0, - absorption_spectrum=AbsorptionSpectrumLibrary.CONSTANT_ABSORBER_ARBITRARY(2), - scattering_spectrum=ScatteringSpectrumLibrary.CONSTANT_SCATTERING_ARBITRARY(100), - anisotropy_spectrum=AnisotropySpectrumLibrary.CONSTANT_ANISOTROPY_ARBITRARY(0.9), - speed_of_sound=SPEED_OF_SOUND+100, - density=1100 - ) - - vessel_1_dictionary = Settings() - vessel_1_dictionary[Tags.PRIORITY] = 3 - vessel_1_dictionary[Tags.STRUCTURE_START_MM] = [VOLUME_TRANSDUCER_DIM_IN_MM/2-10, 0, 35] - vessel_1_dictionary[Tags.STRUCTURE_END_MM] = [VOLUME_TRANSDUCER_DIM_IN_MM/2-10, VOLUME_PLANAR_DIM_IN_MM, 35] - vessel_1_dictionary[Tags.STRUCTURE_RADIUS_MM] = SPACING - vessel_1_dictionary[Tags.MOLECULE_COMPOSITION] = (MolecularCompositionGenerator(). - append(vessel_molecule). - get_molecular_composition(-1)) - vessel_1_dictionary[Tags.CONSIDER_PARTIAL_VOLUME] = True - vessel_1_dictionary[Tags.STRUCTURE_TYPE] = Tags.CIRCULAR_TUBULAR_STRUCTURE - - tissue_dict = Settings() - tissue_dict[Tags.BACKGROUND] = background_dictionary - tissue_dict["vessel_1"] = vessel_1_dictionary - return tissue_dict - - -# Seed the numpy random configuration prior to creating the global_settings file in -# order to ensure that the same volume -# is generated with the same random seed every time. - -np.random.seed(RANDOM_SEED) -VOLUME_NAME = "CompletePipelineTestMSOT_"+str(RANDOM_SEED) - -general_settings = { - # These parameters set the general properties of the simulated volume - Tags.RANDOM_SEED: RANDOM_SEED, - Tags.VOLUME_NAME: VOLUME_NAME, - Tags.SIMULATION_PATH: path_manager.get_hdf5_file_save_path(), - Tags.SPACING_MM: SPACING, - Tags.DIM_VOLUME_Z_MM: VOLUME_HEIGHT_IN_MM, - Tags.DIM_VOLUME_X_MM: VOLUME_TRANSDUCER_DIM_IN_MM, - Tags.DIM_VOLUME_Y_MM: VOLUME_PLANAR_DIM_IN_MM, - Tags.VOLUME_CREATOR: Tags.VOLUME_CREATOR_VERSATILE, - Tags.GPU: True, - - # The following parameters set the optical forward model - Tags.WAVELENGTHS: [700] -} -settings = Settings(general_settings) -np.random.seed(RANDOM_SEED) - -settings.set_volume_creation_settings({ - Tags.STRUCTURES: create_point_source(), - Tags.SIMULATE_DEFORMED_LAYERS: True -}) - -settings.set_optical_settings({ - Tags.OPTICAL_MODEL_NUMBER_PHOTONS: 1e7, - Tags.OPTICAL_MODEL_BINARY_PATH: path_manager.get_mcx_binary_path(), - Tags.ILLUMINATION_TYPE: Tags.ILLUMINATION_TYPE_MSOT_ACUITY_ECHO, - Tags.LASER_PULSE_ENERGY_IN_MILLIJOULE: 50, -}) - -settings.set_acoustic_settings({ - Tags.ACOUSTIC_SIMULATION_3D: False, - Tags.ACOUSTIC_MODEL_BINARY_PATH: path_manager.get_matlab_binary_path(), - Tags.KWAVE_PROPERTY_ALPHA_POWER: 0.00, - Tags.KWAVE_PROPERTY_SENSOR_RECORD: "p", - Tags.KWAVE_PROPERTY_PMLInside: False, - Tags.KWAVE_PROPERTY_PMLSize: [31, 32], - Tags.KWAVE_PROPERTY_PMLAlpha: 1.5, - Tags.KWAVE_PROPERTY_PlotPML: False, - Tags.RECORDMOVIE: False, - Tags.MOVIENAME: "visualization_log", - Tags.ACOUSTIC_LOG_SCALE: True -}) - -settings.set_reconstruction_settings({ - Tags.RECONSTRUCTION_PERFORM_BANDPASS_FILTERING: False, - Tags.ACOUSTIC_MODEL_BINARY_PATH: path_manager.get_matlab_binary_path(), - Tags.KWAVE_PROPERTY_ALPHA_POWER: 0.00, - Tags.TUKEY_WINDOW_ALPHA: 0.5, - Tags.BANDPASS_CUTOFF_LOWPASS_IN_HZ: int(8e6), - Tags.BANDPASS_CUTOFF_HIGHPASS_IN_HZ: int(0.1e4), - Tags.RECONSTRUCTION_BMODE_AFTER_RECONSTRUCTION: False, - Tags.RECONSTRUCTION_BMODE_METHOD: Tags.RECONSTRUCTION_BMODE_METHOD_HILBERT_TRANSFORM, - Tags.RECONSTRUCTION_APODIZATION_METHOD: Tags.RECONSTRUCTION_APODIZATION_BOX, - Tags.RECONSTRUCTION_MODE: Tags.RECONSTRUCTION_MODE_PRESSURE, - Tags.KWAVE_PROPERTY_SENSOR_RECORD: "p", - Tags.KWAVE_PROPERTY_PMLInside: False, - Tags.KWAVE_PROPERTY_PMLSize: [31, 32], - Tags.KWAVE_PROPERTY_PMLAlpha: 1.5, - Tags.KWAVE_PROPERTY_PlotPML: False, - Tags.RECORDMOVIE: False, - Tags.MOVIENAME: "visualization_log", - Tags.ACOUSTIC_LOG_SCALE: True, - Tags.DATA_FIELD_SPEED_OF_SOUND: SPEED_OF_SOUND, - Tags.SPACING_MM: SPACING -}) - -SIMUATION_PIPELINE = [ - ModelBasedVolumeCreationAdapter(settings), - MCXAdapter(settings), - KWaveAdapter(settings), - FieldOfViewCropping(settings), - DelayAndSumAdapter(settings) -] - - -def simulate_and_evaluate_with_device(_device): - print("Simulating for device:", _device) - simulate(SIMUATION_PIPELINE, settings, _device) - - if Tags.WAVELENGTH in settings: - wavelength = settings[Tags.WAVELENGTH] - else: - wavelength = 700 - - initial_pressure = load_data_field(path_manager.get_hdf5_file_save_path() + "/" + VOLUME_NAME + ".hdf5", - data_field=Tags.DATA_FIELD_INITIAL_PRESSURE, - wavelength=wavelength) - reconstruction = load_data_field(path_manager.get_hdf5_file_save_path() + "/" + VOLUME_NAME + ".hdf5", - data_field=Tags.DATA_FIELD_RECONSTRUCTED_DATA, - wavelength=wavelength) - - p0_idx = np.unravel_index(np.argmax(initial_pressure), np.shape(initial_pressure)) - re_idx = np.unravel_index(np.argmax(reconstruction), np.shape(reconstruction)) - - print("x/y in initial pressure map:", p0_idx) - print("x/y in reconstruction map:", re_idx) - distance = np.sqrt((re_idx[0] - p0_idx[0]) ** 2 + (re_idx[1] - p0_idx[1]) ** 2) - print("Distance:", distance) - - visualise_data(path_to_hdf5_file=path_manager.get_hdf5_file_save_path() + "/" + VOLUME_NAME + ".hdf5", - wavelength=wavelength, - show_time_series_data=True, - show_absorption=False, - show_reconstructed_data=True, - show_xz_only=True, - show_initial_pressure=True, - show_segmentation_map=False, - log_scale=False) - return distance - - -dist = list() - -# fov_e = np.asarray([-VOLUME_TRANSDUCER_DIM_IN_MM/2, VOLUME_TRANSDUCER_DIM_IN_MM/2, 0, 0, 0, VOLUME_HEIGHT_IN_MM]) -# device = PhotoacousticDevice(device_position_mm=np.array([VOLUME_TRANSDUCER_DIM_IN_MM/2, -# VOLUME_PLANAR_DIM_IN_MM/2, -# 0]), -# field_of_view_extent_mm=fov_e) -# device.set_detection_geometry(Random2DArrayDetectionGeometry(device_position_mm=device.device_position_mm, -# number_detector_elements=256, -# seed=1234, field_of_view_extent_mm=fov_e)) -# device.add_illumination_geometry(PencilBeamIlluminationGeometry()) -# dist.append(simulate_and_evaluate_with_device(device)) -# -# device = PhotoacousticDevice(device_position_mm=np.array([VOLUME_TRANSDUCER_DIM_IN_MM/2, -# VOLUME_PLANAR_DIM_IN_MM/2, -# 0]), -# field_of_view_extent_mm=fov_e) -# device.set_detection_geometry(Random3DArrayDetectionGeometry(device_position_mm=device.device_position_mm, -# number_detector_elements=256, -# seed=1234, field_of_view_extent_mm=fov_e)) -# device.add_illumination_geometry(PencilBeamIlluminationGeometry()) -# dist.append(simulate_and_evaluate_with_device(device)) - -dist.append(simulate_and_evaluate_with_device(MSOTAcuityEcho(device_position_mm=np.array([VOLUME_TRANSDUCER_DIM_IN_MM/2, - VOLUME_PLANAR_DIM_IN_MM/2, - 35]), - field_of_view_extent_mm=np.array([-(2 * np.sin(0.34 / 40 * 128) * 40) / 2, - (2 * np.sin(0.34 / - 40 * 128) * 40) / 2, - 0, 0, -25, 25])))) - -dist.append(simulate_and_evaluate_with_device(InVision256TF(device_position_mm=np.array([VOLUME_TRANSDUCER_DIM_IN_MM/2, - VOLUME_PLANAR_DIM_IN_MM/2, - VOLUME_HEIGHT_IN_MM/2])))) -device = PhotoacousticDevice(device_position_mm=np.array([VOLUME_TRANSDUCER_DIM_IN_MM/2, - VOLUME_PLANAR_DIM_IN_MM/2, - 30]), - field_of_view_extent_mm=np.asarray([-VOLUME_TRANSDUCER_DIM_IN_MM/2, - VOLUME_TRANSDUCER_DIM_IN_MM/2, - 0, 0, 0, VOLUME_HEIGHT_IN_MM])) -device.set_detection_geometry(LinearArrayDetectionGeometry(device_position_mm=device.device_position_mm, - pitch_mm=0.2, - number_detector_elements=256)) -device.add_illumination_geometry(PencilBeamIlluminationGeometry()) -dist.append(simulate_and_evaluate_with_device(device)) - -device = PhotoacousticDevice(device_position_mm=np.array([VOLUME_TRANSDUCER_DIM_IN_MM/2, - VOLUME_PLANAR_DIM_IN_MM/2, - 5]), - field_of_view_extent_mm=np.asarray([-VOLUME_TRANSDUCER_DIM_IN_MM / 2, - VOLUME_TRANSDUCER_DIM_IN_MM / 2, - 0, 0, 0, VOLUME_HEIGHT_IN_MM])) - -device.set_detection_geometry(LinearArrayDetectionGeometry(device_position_mm=device.device_position_mm, - pitch_mm=0.2, - number_detector_elements=256)) -device.add_illumination_geometry(PencilBeamIlluminationGeometry()) -dist.append(simulate_and_evaluate_with_device(device)) - -device = PhotoacousticDevice(device_position_mm=np.array([VOLUME_TRANSDUCER_DIM_IN_MM/2, - VOLUME_PLANAR_DIM_IN_MM/2, - 10]), - field_of_view_extent_mm=np.asarray([-VOLUME_TRANSDUCER_DIM_IN_MM / 2, - VOLUME_TRANSDUCER_DIM_IN_MM / 2, - 0, 0, 0, VOLUME_HEIGHT_IN_MM])) -device.set_detection_geometry(LinearArrayDetectionGeometry(device_position_mm=device.device_position_mm, - pitch_mm=0.2, - number_detector_elements=256)) -device.add_illumination_geometry(PencilBeamIlluminationGeometry()) -dist.append(simulate_and_evaluate_with_device(device)) -print("") -print("Results:") -print("______________") -for dis in dist: - print("Distance", dis) + + def __init__(self, speed_of_sound: float = 1470, volume_transducer_dim_in_mm: float = 90, + volume_planar_dim_in_mm: float = 20, volume_height_in_mm: float = 90, + spacing: float = 0.4): + + self.reconstructed_image_pipeline = None # TODO REMOVE + + self.SPEED_OF_SOUND = speed_of_sound + self.VOLUME_TRANSDUCER_DIM_IN_MM = volume_transducer_dim_in_mm + self.VOLUME_PLANAR_DIM_IN_MM = volume_planar_dim_in_mm + self.VOLUME_HEIGHT_IN_MM = volume_height_in_mm + self.SPACING = spacing + + self.RANDOM_SEED = 4711 + + def create_point_source(self): + """ + This is a very simple example script of how to create a tissue definition. + It contains a muscular background, an epidermis layer on top of the muscles + and a blood vessel. + """ + background_molecule = Molecule( + volume_fraction=1.0, + absorption_spectrum=AbsorptionSpectrumLibrary.CONSTANT_ABSORBER_ARBITRARY(1e-10), + scattering_spectrum=ScatteringSpectrumLibrary.CONSTANT_SCATTERING_ARBITRARY(1e-10), + anisotropy_spectrum=AnisotropySpectrumLibrary.CONSTANT_ANISOTROPY_ARBITRARY(1.0), + speed_of_sound=self.SPEED_OF_SOUND + ) + background_dictionary = Settings() + background_dictionary[Tags.MOLECULE_COMPOSITION] = (MolecularCompositionGenerator(). + append(background_molecule). + get_molecular_composition(-1)) + background_dictionary[Tags.STRUCTURE_TYPE] = Tags.BACKGROUND + + vessel_molecule = Molecule( + volume_fraction=1.0, + absorption_spectrum=AbsorptionSpectrumLibrary.CONSTANT_ABSORBER_ARBITRARY(2), + scattering_spectrum=ScatteringSpectrumLibrary.CONSTANT_SCATTERING_ARBITRARY(100), + anisotropy_spectrum=AnisotropySpectrumLibrary.CONSTANT_ANISOTROPY_ARBITRARY(0.9), + speed_of_sound=self.SPEED_OF_SOUND+100, + density=1100 + ) + + vessel_1_dictionary = Settings() + vessel_1_dictionary[Tags.PRIORITY] = 3 + vessel_1_dictionary[Tags.STRUCTURE_START_MM] = [self.VOLUME_TRANSDUCER_DIM_IN_MM/2-10, 0, 35] + vessel_1_dictionary[Tags.STRUCTURE_END_MM] = [ + self.VOLUME_TRANSDUCER_DIM_IN_MM/2-10, self.VOLUME_PLANAR_DIM_IN_MM, 35] + vessel_1_dictionary[Tags.STRUCTURE_RADIUS_MM] = self.SPACING + vessel_1_dictionary[Tags.MOLECULE_COMPOSITION] = (MolecularCompositionGenerator(). + append(vessel_molecule). + get_molecular_composition(-1)) + vessel_1_dictionary[Tags.CONSIDER_PARTIAL_VOLUME] = True + vessel_1_dictionary[Tags.STRUCTURE_TYPE] = Tags.CIRCULAR_TUBULAR_STRUCTURE + + tissue_dict = Settings() + tissue_dict[Tags.BACKGROUND] = background_dictionary + tissue_dict["vessel_1"] = vessel_1_dictionary + return tissue_dict + + def setup(self): + + # Note: Please make sure that a valid path_config.env file is located in your home directory, or that you + # point to the correct file in the PathManager(). + self.path_manager = PathManager() + + # Seed the numpy random configuration prior to creating the global_settings file in + # order to ensure that the same volume + # is generated with the same random seed every time. + + np.random.seed(self.RANDOM_SEED) + self.VOLUME_NAME = "CompletePipelineTestMSOT_"+str(self.RANDOM_SEED) + + general_settings = { + # These parameters set the general properties of the simulated volume + Tags.RANDOM_SEED: self.RANDOM_SEED, + Tags.VOLUME_NAME: self.VOLUME_NAME, + Tags.SIMULATION_PATH: self.path_manager.get_hdf5_file_save_path(), + Tags.SPACING_MM: self.SPACING, + Tags.DIM_VOLUME_Z_MM: self.VOLUME_HEIGHT_IN_MM, + Tags.DIM_VOLUME_X_MM: self.VOLUME_TRANSDUCER_DIM_IN_MM, + Tags.DIM_VOLUME_Y_MM: self.VOLUME_PLANAR_DIM_IN_MM, + Tags.VOLUME_CREATOR: Tags.VOLUME_CREATOR_VERSATILE, + Tags.GPU: True, + + # The following parameters set the optical forward model + Tags.WAVELENGTHS: [700] + } + settings = Settings(general_settings) + np.random.seed(self.RANDOM_SEED) + + settings.set_volume_creation_settings({ + Tags.STRUCTURES: self.create_point_source(), + Tags.SIMULATE_DEFORMED_LAYERS: True + }) + + settings.set_optical_settings({ + Tags.OPTICAL_MODEL_NUMBER_PHOTONS: 1e7, + Tags.OPTICAL_MODEL_BINARY_PATH: self.path_manager.get_mcx_binary_path(), + Tags.ILLUMINATION_TYPE: Tags.ILLUMINATION_TYPE_MSOT_ACUITY_ECHO, + Tags.LASER_PULSE_ENERGY_IN_MILLIJOULE: 50, + }) + + settings.set_acoustic_settings({ + Tags.ACOUSTIC_SIMULATION_3D: False, + Tags.ACOUSTIC_MODEL_BINARY_PATH: self.path_manager.get_matlab_binary_path(), + Tags.KWAVE_PROPERTY_ALPHA_POWER: 0.00, + Tags.KWAVE_PROPERTY_SENSOR_RECORD: "p", + Tags.KWAVE_PROPERTY_PMLInside: False, + Tags.KWAVE_PROPERTY_PMLSize: [31, 32], + Tags.KWAVE_PROPERTY_PMLAlpha: 1.5, + Tags.KWAVE_PROPERTY_PlotPML: False, + Tags.RECORDMOVIE: False, + Tags.MOVIENAME: "visualization_log", + Tags.ACOUSTIC_LOG_SCALE: True + }) + + settings.set_reconstruction_settings({ + Tags.RECONSTRUCTION_PERFORM_BANDPASS_FILTERING: False, + Tags.ACOUSTIC_MODEL_BINARY_PATH: self.path_manager.get_matlab_binary_path(), + Tags.KWAVE_PROPERTY_ALPHA_POWER: 0.00, + Tags.TUKEY_WINDOW_ALPHA: 0.5, + Tags.BANDPASS_CUTOFF_LOWPASS_IN_HZ: int(8e6), + Tags.BANDPASS_CUTOFF_HIGHPASS_IN_HZ: int(0.1e4), + Tags.RECONSTRUCTION_BMODE_AFTER_RECONSTRUCTION: False, + Tags.RECONSTRUCTION_BMODE_METHOD: Tags.RECONSTRUCTION_BMODE_METHOD_HILBERT_TRANSFORM, + Tags.RECONSTRUCTION_APODIZATION_METHOD: Tags.RECONSTRUCTION_APODIZATION_BOX, + Tags.RECONSTRUCTION_MODE: Tags.RECONSTRUCTION_MODE_PRESSURE, + Tags.KWAVE_PROPERTY_SENSOR_RECORD: "p", + Tags.KWAVE_PROPERTY_PMLInside: False, + Tags.KWAVE_PROPERTY_PMLSize: [31, 32], + Tags.KWAVE_PROPERTY_PMLAlpha: 1.5, + Tags.KWAVE_PROPERTY_PlotPML: False, + Tags.RECORDMOVIE: False, + Tags.MOVIENAME: "visualization_log", + Tags.ACOUSTIC_LOG_SCALE: True, + Tags.DATA_FIELD_SPEED_OF_SOUND: self.SPEED_OF_SOUND, + Tags.SPACING_MM: self.SPACING + }) + + self.settings = settings + + def simulate_and_evaluate_with_device(self, _device): + SIMULATION_PIPELINE = [ + ModelBasedAdapter(self.settings), + MCXAdapter(self.settings), + KWaveAdapter(self.settings), + FieldOfViewCropping(self.settings), + DelayAndSumAdapter(self.settings) + ] + + print("Simulating for device:", _device) + simulate(SIMULATION_PIPELINE, self.settings, _device) + + if Tags.WAVELENGTH in self.settings: + wavelength = self.settings[Tags.WAVELENGTH] + else: + wavelength = 700 + + initial_pressure = load_data_field(self.path_manager.get_hdf5_file_save_path() + "/" + self.VOLUME_NAME + ".hdf5", + data_field=Tags.DATA_FIELD_INITIAL_PRESSURE, + wavelength=wavelength) + reconstruction = load_data_field(self.path_manager.get_hdf5_file_save_path() + "/" + self.VOLUME_NAME + ".hdf5", + data_field=Tags.DATA_FIELD_RECONSTRUCTED_DATA, + wavelength=wavelength) + + p0_idx = np.unravel_index(np.argmax(initial_pressure), np.shape(initial_pressure)) + re_idx = np.unravel_index(np.argmax(reconstruction), np.shape(reconstruction)) + + print("x/y in initial pressure map:", p0_idx) + print("x/y in reconstruction map:", re_idx) + distance = np.sqrt((re_idx[0] - p0_idx[0]) ** 2 + (re_idx[1] - p0_idx[1]) ** 2) + print("Distance:", distance) + + if self.save_path is not None: + save_path = self.save_path + f"PointSourceReconstruction_{self.figure_number}.png" + else: + save_path = self.save_path + + visualise_data(path_to_hdf5_file=self.path_manager.get_hdf5_file_save_path() + "/" + self.VOLUME_NAME + ".hdf5", + wavelength=wavelength, + show_time_series_data=True, + show_absorption=False, + show_reconstructed_data=True, + show_xz_only=True, + show_initial_pressure=True, + show_segmentation_map=False, + log_scale=False, + save_path=save_path) + self.figure_number += 1 + return distance + + def test_reconstruction_of_simulation(self): + + dist = list() + + # fov_e = np.asarray([-self.VOLUME_TRANSDUCER_DIM_IN_MM/2, self.VOLUME_TRANSDUCER_DIM_IN_MM/2, 0, 0, 0, self.VOLUME_HEIGHT_IN_MM]) + # device = PhotoacousticDevice(device_position_mm=np.array([self.VOLUME_TRANSDUCER_DIM_IN_MM/2, + # self.VOLUME_PLANAR_DIM_IN_MM/2, + # 0]), + # field_of_view_extent_mm=fov_e) + # device.set_detection_geometry(Random2DArrayDetectionGeometry(device_position_mm=device.device_position_mm, + # number_detector_elements=256, + # seed=1234, field_of_view_extent_mm=fov_e)) + # device.add_illumination_geometry(PencilBeamIlluminationGeometry()) + # dist.append(self.simulate_and_evaluate_with_device(device)) + # + # device = PhotoacousticDevice(device_position_mm=np.array([self.VOLUME_TRANSDUCER_DIM_IN_MM/2, + # self.VOLUME_PLANAR_DIM_IN_MM/2, + # 0]), + # field_of_view_extent_mm=fov_e) + # device.set_detection_geometry(Random3DArrayDetectionGeometry(device_position_mm=device.device_position_mm, + # number_detector_elements=256, + # seed=1234, field_of_view_extent_mm=fov_e)) + # device.add_illumination_geometry(PencilBeamIlluminationGeometry()) + # dist.append(self.simulate_and_evaluate_with_device(device)) + + dist.append(self.simulate_and_evaluate_with_device(MSOTAcuityEcho(device_position_mm=np.array([self.VOLUME_TRANSDUCER_DIM_IN_MM/2, + self.VOLUME_PLANAR_DIM_IN_MM/2, + 35]), + field_of_view_extent_mm=np.array([-(2 * np.sin(0.34 / 40 * 128) * 40) / 2, + (2 * np.sin(0.34 / + 40 * 128) * 40) / 2, + 0, 0, -25, 25])))) + + dist.append(self.simulate_and_evaluate_with_device(InVision256TF(device_position_mm=np.array([self.VOLUME_TRANSDUCER_DIM_IN_MM/2, + self.VOLUME_PLANAR_DIM_IN_MM/2, + self.VOLUME_HEIGHT_IN_MM/2])))) + device = PhotoacousticDevice(device_position_mm=np.array([self.VOLUME_TRANSDUCER_DIM_IN_MM/2, + self.VOLUME_PLANAR_DIM_IN_MM/2, + 30]), + field_of_view_extent_mm=np.asarray([-self.VOLUME_TRANSDUCER_DIM_IN_MM/2, + self.VOLUME_TRANSDUCER_DIM_IN_MM/2, + 0, 0, 0, self.VOLUME_HEIGHT_IN_MM])) + device.set_detection_geometry(LinearArrayDetectionGeometry(device_position_mm=device.device_position_mm, + pitch_mm=0.2, + number_detector_elements=256)) + device.add_illumination_geometry(PencilBeamIlluminationGeometry(device_position_mm=device.device_position_mm)) + dist.append(self.simulate_and_evaluate_with_device(device)) + + device = PhotoacousticDevice(device_position_mm=np.array([self.VOLUME_TRANSDUCER_DIM_IN_MM/2, + self.VOLUME_PLANAR_DIM_IN_MM/2, + 5]), + field_of_view_extent_mm=np.asarray([-self.VOLUME_TRANSDUCER_DIM_IN_MM / 2, + self.VOLUME_TRANSDUCER_DIM_IN_MM / 2, + 0, 0, 0, self.VOLUME_HEIGHT_IN_MM])) + + device.set_detection_geometry(LinearArrayDetectionGeometry(device_position_mm=device.device_position_mm, + pitch_mm=0.2, + number_detector_elements=256)) + device.add_illumination_geometry(PencilBeamIlluminationGeometry()) + dist.append(self.simulate_and_evaluate_with_device(device)) + + device = PhotoacousticDevice(device_position_mm=np.array([self.VOLUME_TRANSDUCER_DIM_IN_MM/2, + self.VOLUME_PLANAR_DIM_IN_MM/2, + 10]), + field_of_view_extent_mm=np.asarray([-self.VOLUME_TRANSDUCER_DIM_IN_MM / 2, + self.VOLUME_TRANSDUCER_DIM_IN_MM / 2, + 0, 0, 0, self.VOLUME_HEIGHT_IN_MM])) + device.set_detection_geometry(LinearArrayDetectionGeometry(device_position_mm=device.device_position_mm, + pitch_mm=0.2, + number_detector_elements=256)) + device.add_illumination_geometry(PencilBeamIlluminationGeometry()) + dist.append(self.simulate_and_evaluate_with_device(device)) + print("") + print("Results:") + print("______________") + for dis in dist: + print("Distance", dis) + + def run_test(self, show_figure_on_screen=True, save_path=None): + + if save_path is None or not os.path.isdir(save_path): + save_path = "figures/" + if not os.path.exists(save_path): + os.mkdir(save_path) + + if show_figure_on_screen: + save_path = None + + self.save_path = save_path + self.figure_number = 0 + + self.setup() + self.perform_test() + self.tear_down() + + +if __name__ == '__main__': + test = PointSourceReconstruction() + test.run_test(show_figure_on_screen=True) diff --git a/simpa_tests/manual_tests/image_reconstruction/SignedDelayMultiplyAndSumReconstruction.py b/simpa_tests/manual_tests/image_reconstruction/SignedDelayMultiplyAndSumReconstruction.py index 754cdd01..00b0540b 100644 --- a/simpa_tests/manual_tests/image_reconstruction/SignedDelayMultiplyAndSumReconstruction.py +++ b/simpa_tests/manual_tests/image_reconstruction/SignedDelayMultiplyAndSumReconstruction.py @@ -7,7 +7,7 @@ from simpa.io_handling import load_data_field from simpa.core.simulation import simulate from simpa import KWaveAdapter, MCXAdapter, \ - SignedDelayMultiplyAndSumAdapter, ModelBasedVolumeCreationAdapter + SignedDelayMultiplyAndSumAdapter, ModelBasedAdapter from simpa.core.processing_components.monospectral.noise import GaussianNoise from simpa import reconstruct_signed_delay_multiply_and_sum_pytorch from simpa_tests.manual_tests import ReconstructionAlgorithmTestBaseClass @@ -28,7 +28,7 @@ def test_reconstruction_of_simulation(self): self.device.update_settings_for_use_of_model_based_volume_creator(self.settings) SIMUATION_PIPELINE = [ - ModelBasedVolumeCreationAdapter(self.settings), + ModelBasedAdapter(self.settings), MCXAdapter(self.settings), GaussianNoise(self.settings, "noise_initial_pressure"), KWaveAdapter(self.settings), @@ -37,19 +37,19 @@ def test_reconstruction_of_simulation(self): simulate(SIMUATION_PIPELINE, self.settings, self.device) - self.reconstructed_image_pipeline = load_data_field(self.settings[Tags.SIMPA_OUTPUT_PATH], Tags.DATA_FIELD_RECONSTRUCTED_DATA, + self.reconstructed_image_pipeline = load_data_field(self.settings[Tags.SIMPA_OUTPUT_FILE_PATH], Tags.DATA_FIELD_RECONSTRUCTED_DATA, self.settings[Tags.WAVELENGTH]) def test_convenience_function(self): # Load simulated time series data - time_series_sensor_data = load_data_field(self.settings[Tags.SIMPA_OUTPUT_PATH], + time_series_sensor_data = load_data_field(self.settings[Tags.SIMPA_OUTPUT_FILE_PATH], Tags.DATA_FIELD_TIME_SERIES_DATA, self.settings[Tags.WAVELENGTH]) reconstruction_settings = self.settings.get_reconstruction_settings() # reconstruct via convenience function self.reconstructed_image_convenience = reconstruct_signed_delay_multiply_and_sum_pytorch(time_series_sensor_data, self.device.get_detection_geometry(), reconstruction_settings[Tags.DATA_FIELD_SPEED_OF_SOUND], 1.0 / ( - self.device.get_detection_geometry().sampling_frequency_MHz * 1000), self.settings[Tags.SPACING_MM], reconstruction_settings[Tags.RECONSTRUCTION_MODE], reconstruction_settings[Tags.RECONSTRUCTION_APODIZATION_METHOD]) + self.device.get_detection_geometry().sampling_frequency_MHz * 1_000_000), self.settings[Tags.SPACING_MM], reconstruction_settings[Tags.RECONSTRUCTION_MODE], reconstruction_settings[Tags.RECONSTRUCTION_APODIZATION_METHOD]) # apply envelope detection method if set if reconstruction_settings[Tags.RECONSTRUCTION_BMODE_AFTER_RECONSTRUCTION]: diff --git a/simpa_tests/manual_tests/image_reconstruction/TimeReversalReconstruction.py b/simpa_tests/manual_tests/image_reconstruction/TimeReversalReconstruction.py index 72a8c49f..3e9353f0 100644 --- a/simpa_tests/manual_tests/image_reconstruction/TimeReversalReconstruction.py +++ b/simpa_tests/manual_tests/image_reconstruction/TimeReversalReconstruction.py @@ -7,8 +7,7 @@ from simpa.io_handling import load_data_field from simpa.core.simulation import simulate from simpa import KWaveAdapter, MCXAdapter, \ - TimeReversalAdapter, ModelBasedVolumeCreationAdapter, GaussianNoise -from simpa import reconstruct_delay_and_sum_pytorch + TimeReversalAdapter, ModelBasedAdapter, GaussianNoise from simpa_tests.manual_tests import ReconstructionAlgorithmTestBaseClass # FIXME temporary workaround for newest Intel architectures @@ -27,7 +26,7 @@ def test_reconstruction_of_simulation(self): self.device.update_settings_for_use_of_model_based_volume_creator(self.settings) SIMULATION_PIPELINE = [ - ModelBasedVolumeCreationAdapter(self.settings), + ModelBasedAdapter(self.settings), MCXAdapter(self.settings), GaussianNoise(self.settings, "noise_initial_pressure"), KWaveAdapter(self.settings), @@ -36,7 +35,7 @@ def test_reconstruction_of_simulation(self): simulate(SIMULATION_PIPELINE, self.settings, self.device) - self.reconstructed_image_pipeline = load_data_field(self.settings[Tags.SIMPA_OUTPUT_PATH], Tags.DATA_FIELD_RECONSTRUCTED_DATA, + self.reconstructed_image_pipeline = load_data_field(self.settings[Tags.SIMPA_OUTPUT_FILE_PATH], Tags.DATA_FIELD_RECONSTRUCTED_DATA, self.settings[Tags.WAVELENGTH]) def test_convenience_function(self): diff --git a/simpa_tests/manual_tests/optical_forward_models/AbsorptionAndScatteringWithInifinitesimalSlabExperiment.py b/simpa_tests/manual_tests/optical_forward_models/AbsorptionAndScatteringWithInifinitesimalSlabExperiment.py index 5ac62e06..3917e17d 100644 --- a/simpa_tests/manual_tests/optical_forward_models/AbsorptionAndScatteringWithInifinitesimalSlabExperiment.py +++ b/simpa_tests/manual_tests/optical_forward_models/AbsorptionAndScatteringWithInifinitesimalSlabExperiment.py @@ -28,7 +28,7 @@ import matplotlib.pyplot as plt import numpy as np -from simpa import MCXAdapter, ModelBasedVolumeCreationAdapter +from simpa import MCXAdapter, ModelBasedAdapter from simpa.core.device_digital_twins import PhotoacousticDevice, PencilBeamIlluminationGeometry from simpa.core.simulation import simulate from simpa.io_handling import load_data_field @@ -115,7 +115,7 @@ def setup(self): self.device.add_illumination_geometry(PencilBeamIlluminationGeometry()) def teardown(self): - os.remove(self.settings[Tags.SIMPA_OUTPUT_PATH]) + os.remove(self.settings[Tags.SIMPA_OUTPUT_FILE_PATH]) def test_both(self): """ @@ -208,20 +208,20 @@ def test_simulation(self, distance=10, expected_decay_ratio=np.e, scattering_val self.settings.get_optical_settings()[Tags.MCX_ASSUMED_ANISOTROPY] = anisotropy_value pipeline = [ - ModelBasedVolumeCreationAdapter(self.settings), + ModelBasedAdapter(self.settings), MCXAdapter(self.settings) ] simulate(pipeline, self.settings, self.device) # run_optical_forward_model(self.settings) - fluence = load_data_field(self.settings[Tags.SIMPA_OUTPUT_PATH], Tags.DATA_FIELD_FLUENCE, + fluence = load_data_field(self.settings[Tags.SIMPA_OUTPUT_FILE_PATH], Tags.DATA_FIELD_FLUENCE, self.settings[Tags.WAVELENGTH]) - absorption = load_data_field(self.settings[Tags.SIMPA_OUTPUT_PATH], Tags.DATA_FIELD_ABSORPTION_PER_CM, + absorption = load_data_field(self.settings[Tags.SIMPA_OUTPUT_FILE_PATH], Tags.DATA_FIELD_ABSORPTION_PER_CM, self.settings[Tags.WAVELENGTH]) - scattering = load_data_field(self.settings[Tags.SIMPA_OUTPUT_PATH], Tags.DATA_FIELD_SCATTERING_PER_CM, + scattering = load_data_field(self.settings[Tags.SIMPA_OUTPUT_FILE_PATH], Tags.DATA_FIELD_SCATTERING_PER_CM, self.settings[Tags.WAVELENGTH]) - anisotropy = load_data_field(self.settings[Tags.SIMPA_OUTPUT_PATH], Tags.DATA_FIELD_ANISOTROPY, + anisotropy = load_data_field(self.settings[Tags.SIMPA_OUTPUT_FILE_PATH], Tags.DATA_FIELD_ANISOTROPY, self.settings[Tags.WAVELENGTH]) early_point = int((self.z_dim / 2 - distance / 2) / self.settings[Tags.SPACING_MM]) diff --git a/simpa_tests/manual_tests/optical_forward_models/AbsorptionAndScatteringWithinHomogenousMedium.py b/simpa_tests/manual_tests/optical_forward_models/AbsorptionAndScatteringWithinHomogenousMedium.py index 806863bd..d3508b27 100644 --- a/simpa_tests/manual_tests/optical_forward_models/AbsorptionAndScatteringWithinHomogenousMedium.py +++ b/simpa_tests/manual_tests/optical_forward_models/AbsorptionAndScatteringWithinHomogenousMedium.py @@ -28,7 +28,7 @@ import matplotlib.pyplot as plt import numpy as np -from simpa import MCXAdapter, ModelBasedVolumeCreationAdapter +from simpa import MCXAdapter, ModelBasedAdapter from simpa.core.device_digital_twins import PhotoacousticDevice, PencilBeamIlluminationGeometry from simpa.core.simulation import simulate from simpa.io_handling import load_data_field @@ -38,7 +38,7 @@ os.environ["KMP_DUPLICATE_LIB_OK"] = "TRUE" -class TestAbsorptionAndScatteringWithInifinitesimalSlabExperiment(ManualIntegrationTestClass): +class TestAbsorptionAndScatteringWithinHomogeneousMedium(ManualIntegrationTestClass): def create_example_tissue(self, scattering_value=1e-30, absorption_value=1e-30, anisotropy_value=0.0): """ @@ -94,122 +94,122 @@ def setup(self): self.device.add_illumination_geometry(PencilBeamIlluminationGeometry()) def teardown(self): - os.remove(self.settings[Tags.SIMPA_OUTPUT_PATH]) + os.remove(self.settings[Tags.SIMPA_OUTPUT_FILE_PATH]) def test_low_scattering(self): """ Here, the slab is 10 mm long, mua and mus are both used with values of 0.05 mm^-1, so that mua+mus=0.1 mm^-1. We expect a decay ratio of e^1. """ - return self.test_simultion(absorption_value_1=0.01, - absorption_value_2=0.01, - scattering_value_1=1.0, - scattering_value_2=10.0, - anisotropy_value_1=0.0, - anisotropy_value_2=0.9, - title="Low Abs. Low Scat.") + return self.test_simulation(absorption_value_1=0.01, + absorption_value_2=0.01, + scattering_value_1=1.0, + scattering_value_2=10.0, + anisotropy_value_1=0.0, + anisotropy_value_2=0.9, + title="Low Abs. Low Scat.") def test_medium_scattering(self): """ Here, the slab is 10 mm long, mua and mus are both used with values of 0.05 mm^-1, so that mua+mus=0.1 mm^-1. We expect a decay ratio of e^1. """ - return self.test_simultion(absorption_value_1=0.01, - absorption_value_2=0.01, - scattering_value_1=10.0, - scattering_value_2=100.0, - anisotropy_value_1=0.0, - anisotropy_value_2=0.9, - title="Low Abs. Medium Scat.") + return self.test_simulation(absorption_value_1=0.01, + absorption_value_2=0.01, + scattering_value_1=10.0, + scattering_value_2=100.0, + anisotropy_value_1=0.0, + anisotropy_value_2=0.9, + title="Low Abs. Medium Scat.") def test_high_scattering_090(self): """ Here, the slab is 10 mm long, mua and mus are both used with values of 0.05 mm^-1, so that mua+mus=0.1 mm^-1. We expect a decay ratio of e^1. """ - return self.test_simultion(absorption_value_1=0.01, - absorption_value_2=0.01, - scattering_value_1=50.0, - scattering_value_2=500.0, - anisotropy_value_1=0.0, - anisotropy_value_2=0.9, - title="Anisotropy 0.9") + return self.test_simulation(absorption_value_1=0.01, + absorption_value_2=0.01, + scattering_value_1=50.0, + scattering_value_2=500.0, + anisotropy_value_1=0.0, + anisotropy_value_2=0.9, + title="Anisotropy 0.9") def simulate_perfect_result(self): """ Here, the slab is 10 mm long, mua and mus are both used with values of 0.05 mm^-1, so that mua+mus=0.1 mm^-1. We expect a decay ratio of e^1. """ - return self.test_simultion(absorption_value_1=0.01, - absorption_value_2=0.01, - scattering_value_1=50.0, - scattering_value_2=50.0, - anisotropy_value_1=0.0, - anisotropy_value_2=0.0, - title="Ideal Result") + return self.test_simulation(absorption_value_1=0.01, + absorption_value_2=0.01, + scattering_value_1=50.0, + scattering_value_2=50.0, + anisotropy_value_1=0.0, + anisotropy_value_2=0.0, + title="Ideal Result") def test_high_scattering_075(self): """ Here, the slab is 10 mm long, mua and mus are both used with values of 0.05 mm^-1, so that mua+mus=0.1 mm^-1. We expect a decay ratio of e^1. """ - return self.test_simultion(absorption_value_1=0.01, - absorption_value_2=0.01, - scattering_value_1=50.0, - scattering_value_2=200.0, - anisotropy_value_1=0.0, - anisotropy_value_2=0.75, - title="Anisotropy 0.75") + return self.test_simulation(absorption_value_1=0.01, + absorption_value_2=0.01, + scattering_value_1=50.0, + scattering_value_2=200.0, + anisotropy_value_1=0.0, + anisotropy_value_2=0.75, + title="Anisotropy 0.75") def test_high_scattering_025(self): """ Here, the slab is 10 mm long, mua and mus are both used with values of 0.05 mm^-1, so that mua+mus=0.1 mm^-1. We expect a decay ratio of e^1. """ - return self.test_simultion(absorption_value_1=0.01, - absorption_value_2=0.01, - scattering_value_1=50.0, - scattering_value_2=66.666666666666667, - anisotropy_value_1=0.0, - anisotropy_value_2=0.25, - title="Anisotropy 0.25") + return self.test_simulation(absorption_value_1=0.01, + absorption_value_2=0.01, + scattering_value_1=50.0, + scattering_value_2=66.666666666666667, + anisotropy_value_1=0.0, + anisotropy_value_2=0.25, + title="Anisotropy 0.25") def test_ignore_mcx_anisotropy_025(self): """ Here, the slab is 10 mm long, mua and mus are both used with values of 0.05 mm^-1, so that mua+mus=0.1 mm^-1. We expect a decay ratio of e^1. """ - return self.test_simultion(absorption_value_1=0.01, - absorption_value_2=0.01, - scattering_value_1=50.0, - scattering_value_2=66.666666666666667, - anisotropy_value_1=0.0, - anisotropy_value_2=0.25, - title="Ignore MCX Anisotropy 0.25", - use_mcx_anisotropy=False) + return self.test_simulation(absorption_value_1=0.01, + absorption_value_2=0.01, + scattering_value_1=50.0, + scattering_value_2=66.666666666666667, + anisotropy_value_1=0.0, + anisotropy_value_2=0.25, + title="Ignore MCX Anisotropy 0.25", + use_mcx_anisotropy=False) def test_ignore_mcx_anisotropy_075(self): """ Here, the slab is 10 mm long, mua and mus are both used with values of 0.05 mm^-1, so that mua+mus=0.1 mm^-1. We expect a decay ratio of e^1. """ - return self.test_simultion(absorption_value_1=0.01, - absorption_value_2=0.01, - scattering_value_1=50.0, - scattering_value_2=200.0, - anisotropy_value_1=0.0, - anisotropy_value_2=0.75, - title="Ignore MCX Anisotropy 0.75", - use_mcx_anisotropy=False) - - def test_simultion(self, scattering_value_1=1e-30, - absorption_value_1=1e-30, - anisotropy_value_1=1.0, - scattering_value_2=1e-30, - absorption_value_2=1e-30, - anisotropy_value_2=1.0, - title="Medium Abs. High Scat.", - use_mcx_anisotropy=True): + return self.test_simulation(absorption_value_1=0.01, + absorption_value_2=0.01, + scattering_value_1=50.0, + scattering_value_2=200.0, + anisotropy_value_1=0.0, + anisotropy_value_2=0.75, + title="Ignore MCX Anisotropy 0.75", + use_mcx_anisotropy=False) + + def test_simulation(self, scattering_value_1=1e-30, + absorption_value_1=1e-30, + anisotropy_value_1=1.0, + scattering_value_2=1e-30, + absorption_value_2=1e-30, + anisotropy_value_2=1.0, + title="Medium Abs. High Scat.", + use_mcx_anisotropy=True): # RUN SIMULATION 1 @@ -220,16 +220,19 @@ def test_simultion(self, scattering_value_1=1e-30, anisotropy_value=anisotropy_value_1) }) - self.settings.get_optical_settings()[Tags.MCX_ASSUMED_ANISOTROPY] = anisotropy_value_1 + if use_mcx_anisotropy: + self.settings.get_optical_settings()[Tags.MCX_ASSUMED_ANISOTROPY] = anisotropy_value_1 + else: + self.settings.get_optical_settings()[Tags.MCX_ASSUMED_ANISOTROPY] = anisotropy_value_2 pipeline = [ - ModelBasedVolumeCreationAdapter(self.settings), + ModelBasedAdapter(self.settings), MCXAdapter(self.settings) ] simulate(pipeline, self.settings, self.device) - fluence_1 = load_data_field(self.settings[Tags.SIMPA_OUTPUT_PATH], Tags.DATA_FIELD_FLUENCE, + fluence_1 = load_data_field(self.settings[Tags.SIMPA_OUTPUT_FILE_PATH], Tags.DATA_FIELD_FLUENCE, self.settings[Tags.WAVELENGTH]) # RUN SIMULATION 2 @@ -241,19 +244,16 @@ def test_simultion(self, scattering_value_1=1e-30, anisotropy_value=anisotropy_value_2) }) - if use_mcx_anisotropy: - self.settings.get_optical_settings()[Tags.MCX_ASSUMED_ANISOTROPY] = anisotropy_value_2 - else: - self.settings.get_optical_settings()[Tags.MCX_ASSUMED_ANISOTROPY] = 0.9 + self.settings.get_optical_settings()[Tags.MCX_ASSUMED_ANISOTROPY] = anisotropy_value_2 pipeline = [ - ModelBasedVolumeCreationAdapter(self.settings), + ModelBasedAdapter(self.settings), MCXAdapter(self.settings) ] simulate(pipeline, self.settings, self.device) - fluence_2 = load_data_field(self.settings[Tags.SIMPA_OUTPUT_PATH], Tags.DATA_FIELD_FLUENCE, + fluence_2 = load_data_field(self.settings[Tags.SIMPA_OUTPUT_FILE_PATH], Tags.DATA_FIELD_FLUENCE, self.settings[Tags.WAVELENGTH]) illuminator_point = int((self.xy_dim / 2) / self.settings[Tags.SPACING_MM]) - 1 @@ -309,5 +309,5 @@ def perform_test(self): if __name__ == '__main__': - test = TestAbsorptionAndScatteringWithInifinitesimalSlabExperiment() + test = TestAbsorptionAndScatteringWithinHomogeneousMedium() test.run_test(show_figure_on_screen=False) diff --git a/simpa_tests/manual_tests/optical_forward_models/CompareMCXResultsWithDiffusionTheory.py b/simpa_tests/manual_tests/optical_forward_models/CompareMCXResultsWithDiffusionTheory.py index 898fd1f6..e86b5a2a 100644 --- a/simpa_tests/manual_tests/optical_forward_models/CompareMCXResultsWithDiffusionTheory.py +++ b/simpa_tests/manual_tests/optical_forward_models/CompareMCXResultsWithDiffusionTheory.py @@ -4,7 +4,7 @@ from simpa.utils import Tags, PathManager, Settings, TISSUE_LIBRARY from simpa.core.simulation import simulate -from simpa import ModelBasedVolumeCreationAdapter, MCXAdapter +from simpa import ModelBasedAdapter, MCXAdapter from simpa.core.device_digital_twins import PhotoacousticDevice, PencilBeamIlluminationGeometry from simpa.io_handling import load_data_field import numpy as np @@ -130,7 +130,7 @@ def run_simulation(self, distance, spacing): # run pipeline including volume creation and optical mcx simulation pipeline = [ - ModelBasedVolumeCreationAdapter(self.settings), + ModelBasedAdapter(self.settings), MCXAdapter(self.settings), ] simulate(pipeline, self.settings, self.device) @@ -145,7 +145,7 @@ def perform_test(self): self.results.append(self.test_spacing_long()) def assertDiffusionTheory(self, distance, spacing): - fluence = load_data_field(self.settings[Tags.SIMPA_OUTPUT_PATH], Tags.DATA_FIELD_FLUENCE, + fluence = load_data_field(self.settings[Tags.SIMPA_OUTPUT_FILE_PATH], Tags.DATA_FIELD_FLUENCE, self.settings[Tags.WAVELENGTH]) number_of_measurements = np.arange(0, int(distance/self.settings[Tags.SPACING_MM]), 1) measurement_distances = number_of_measurements * self.settings[Tags.SPACING_MM] diff --git a/simpa_tests/manual_tests/optical_forward_models/ComputeDiffuseReflectance.py b/simpa_tests/manual_tests/optical_forward_models/ComputeDiffuseReflectance.py index e8e77855..a748bd55 100644 --- a/simpa_tests/manual_tests/optical_forward_models/ComputeDiffuseReflectance.py +++ b/simpa_tests/manual_tests/optical_forward_models/ComputeDiffuseReflectance.py @@ -4,7 +4,7 @@ from simpa.utils import Tags, PathManager, Settings, TISSUE_LIBRARY from simpa.core.simulation import simulate -from simpa import ModelBasedVolumeCreationAdapter, MCXAdapterReflectance +from simpa import ModelBasedAdapter, MCXReflectanceAdapter from simpa.core.device_digital_twins import PhotoacousticDevice, PencilBeamIlluminationGeometry from simpa.io_handling import load_data_field import numpy as np @@ -136,8 +136,8 @@ def run_simulation(self, distance, spacing): # run pipeline including volume creation and optical mcx simulation pipeline = [ - ModelBasedVolumeCreationAdapter(self.settings), - MCXAdapterReflectance(self.settings), + ModelBasedAdapter(self.settings), + MCXReflectanceAdapter(self.settings), ] simulate(pipeline, self.settings, self.device) @@ -151,11 +151,11 @@ def perform_test(self): self.results.append(self.test_spacing_long()) def assertDiffusionTheory(self, distance, spacing): - fluence = load_data_field(self.settings[Tags.SIMPA_OUTPUT_PATH], Tags.DATA_FIELD_FLUENCE, + fluence = load_data_field(self.settings[Tags.SIMPA_OUTPUT_FILE_PATH], Tags.DATA_FIELD_FLUENCE, self.settings[Tags.WAVELENGTH]) - ref = load_data_field(self.settings[Tags.SIMPA_OUTPUT_PATH], Tags.DATA_FIELD_DIFFUSE_REFLECTANCE, + ref = load_data_field(self.settings[Tags.SIMPA_OUTPUT_FILE_PATH], Tags.DATA_FIELD_DIFFUSE_REFLECTANCE, self.settings[Tags.WAVELENGTH]) - ref_pos = load_data_field(self.settings[Tags.SIMPA_OUTPUT_PATH], Tags.DATA_FIELD_DIFFUSE_REFLECTANCE_POS, + ref_pos = load_data_field(self.settings[Tags.SIMPA_OUTPUT_FILE_PATH], Tags.DATA_FIELD_DIFFUSE_REFLECTANCE_POS, self.settings[Tags.WAVELENGTH]) reflectance = np.zeros((ref_pos[:, 0].max() + 1, ref_pos[:, 1].max() + 1)) reflectance[ref_pos[:, 0], ref_pos[:, 1], ...] = ref @@ -203,7 +203,7 @@ def visualise_result(self, show_figure_on_screen=True, save_path=None): else: if save_path is None: save_path = "" - plt.savefig(save_path + f"diffusion_theory_{idx}.png") + plt.savefig(save_path + f"diffusion_theory_reflectance_{idx}.png") plt.close() diff --git a/simpa_tests/manual_tests/processing_components/QPAIReconstruction.py b/simpa_tests/manual_tests/processing_components/QPAIReconstruction.py index c262bcbb..d82f99f4 100644 --- a/simpa_tests/manual_tests/processing_components/QPAIReconstruction.py +++ b/simpa_tests/manual_tests/processing_components/QPAIReconstruction.py @@ -7,7 +7,7 @@ from simpa_tests.manual_tests import ManualIntegrationTestClass from simpa.core.device_digital_twins import RSOMExplorerP50 from simpa.core.processing_components.monospectral.iterative_qPAI_algorithm import IterativeqPAI -from simpa import MCXAdapter, ModelBasedVolumeCreationAdapter, \ +from simpa import MCXAdapter, ModelBasedAdapter, \ GaussianNoise from simpa.utils import Tags, Settings, TISSUE_LIBRARY from simpa.core.simulation import simulate @@ -30,7 +30,7 @@ class TestqPAIReconstruction(ManualIntegrationTestClass): def setup(self): """ Runs a pipeline consisting of volume creation and optical simulation. The resulting hdf5 file of the - simple test volume is saved at SIMPA_SAVE_PATH location defined in the path_config.env file. + simple test volume is saved at SIMPA_SAVE_DIRECTORY location defined in the path_config.env file. """ self.path_manager = PathManager() @@ -85,7 +85,7 @@ def setup(self): # run pipeline including volume creation and optical mcx simulation pipeline = [ - ModelBasedVolumeCreationAdapter(self.settings), + ModelBasedAdapter(self.settings), MCXAdapter(self.settings), GaussianNoise(self.settings, "noise_model") ] @@ -110,7 +110,7 @@ def perform_test(self): self.settings["iterative_qpai_reconstruction"] = component_settings self.wavelength = self.settings[Tags.WAVELENGTH] - absorption_gt = load_data_field(self.settings[Tags.SIMPA_OUTPUT_PATH], Tags.DATA_FIELD_ABSORPTION_PER_CM, + absorption_gt = load_data_field(self.settings[Tags.SIMPA_OUTPUT_FILE_PATH], Tags.DATA_FIELD_ABSORPTION_PER_CM, self.wavelength) # if the initial pressure is resampled the ground truth has to be resampled to allow for comparison diff --git a/simpa_tests/manual_tests/processing_components/TestLinearUnmixingVisual.py b/simpa_tests/manual_tests/processing_components/TestLinearUnmixingVisual.py index 38896d64..22241b61 100644 --- a/simpa_tests/manual_tests/processing_components/TestLinearUnmixingVisual.py +++ b/simpa_tests/manual_tests/processing_components/TestLinearUnmixingVisual.py @@ -65,7 +65,7 @@ def setup(self): # Run simulation pipeline for all wavelengths in Tag.WAVELENGTHS self.pipeline = [ - sp.ModelBasedVolumeCreationAdapter(self.settings) + sp.ModelBasedAdapter(self.settings) ] def perform_test(self): @@ -82,22 +82,22 @@ def perform_test(self): self.logger.info("Testing linear unmixing...") # Load blood oxygen saturation - self.lu_results = sp.load_data_field(self.settings[Tags.SIMPA_OUTPUT_PATH], Tags.LINEAR_UNMIXING_RESULT) + self.lu_results = sp.load_data_field(self.settings[Tags.SIMPA_OUTPUT_FILE_PATH], Tags.LINEAR_UNMIXING_RESULT) self.sO2 = self.lu_results["sO2"] # Load reference absorption for the first wavelength - self.mua = sp.load_data_field(self.settings[Tags.SIMPA_OUTPUT_PATH], Tags.DATA_FIELD_ABSORPTION_PER_CM, + self.mua = sp.load_data_field(self.settings[Tags.SIMPA_OUTPUT_FILE_PATH], Tags.DATA_FIELD_ABSORPTION_PER_CM, wavelength=self.VISUAL_WAVELENGTHS[0]) def tear_down(self): # clean up file after testing - os.remove(self.settings[Tags.SIMPA_OUTPUT_PATH]) + os.remove(self.settings[Tags.SIMPA_OUTPUT_FILE_PATH]) def visualise_result(self, show_figure_on_screen=True, save_path=None): # Visualize linear unmixing result # The shape of the linear unmixing result should take after the reference absorption - ground_truth_sO2 = sp.load_data_field(self.settings[Tags.SIMPA_OUTPUT_PATH], Tags.DATA_FIELD_OXYGENATION) + ground_truth_sO2 = sp.load_data_field(self.settings[Tags.SIMPA_OUTPUT_FILE_PATH], Tags.DATA_FIELD_OXYGENATION) y_dim = int(self.mua.shape[1] / 2) plt.figure(figsize=(9, 3)) diff --git a/simpa_tests/manual_tests/test_with_experimental_measurements/ReproduceDISMeasurements.py b/simpa_tests/manual_tests/test_with_experimental_measurements/ReproduceDISMeasurements.py index f36f1fe2..73e0fd78 100644 --- a/simpa_tests/manual_tests/test_with_experimental_measurements/ReproduceDISMeasurements.py +++ b/simpa_tests/manual_tests/test_with_experimental_measurements/ReproduceDISMeasurements.py @@ -24,7 +24,7 @@ from simpa.core.device_digital_twins import * import numpy as np from simpa.visualisation.matplotlib_data_visualisation import visualise_data -from simpa import ModelBasedVolumeCreationAdapter, MCXAdapter +from simpa import ModelBasedAdapter, MCXAdapter from simpa_tests.manual_tests.test_with_experimental_measurements.utils import read_reference_spectra, read_rxt_file from simpa_tests.manual_tests import ManualIntegrationTestClass import inspect @@ -40,7 +40,7 @@ class TestDoubleIntegratingSphereSimulation(ManualIntegrationTestClass): def tear_down(self): - os.remove(self.settings[Tags.SIMPA_OUTPUT_PATH]) + os.remove(self.settings[Tags.SIMPA_OUTPUT_FILE_PATH]) def setup(self): @@ -177,7 +177,7 @@ def create_measurement_setup(sample_tickness_mm): }) self.pipeline = [ - ModelBasedVolumeCreationAdapter(self.settings), + ModelBasedAdapter(self.settings), MCXAdapter(self.settings), ] @@ -226,7 +226,6 @@ def visualise_result(self, show_figure_on_screen=True, save_path=None): else: if save_path is None: save_path = "" - save_path = save_path + "DIS_measurement_simulation_a.png" visualise_data(path_to_hdf5_file=self.path_manager.get_hdf5_file_save_path() + "/" + self.VOLUME_NAME + ".hdf5", wavelength=800, @@ -234,7 +233,7 @@ def visualise_result(self, show_figure_on_screen=True, save_path=None): show_absorption=True, show_fluence=True, log_scale=True, - save_path=save_path) + save_path=save_path + "DIS_measurement_simulation_a.png") measured_transmittance = np.asarray([self.transmittance_spectrum.get_value_for_wavelength(wl) for wl in self.settings[Tags.WAVELENGTHS]]) diff --git a/simpa_tests/manual_tests/volume_creation/SegmentationLoader.py b/simpa_tests/manual_tests/volume_creation/SegmentationLoader.py index 780d92a7..559b9436 100644 --- a/simpa_tests/manual_tests/volume_creation/SegmentationLoader.py +++ b/simpa_tests/manual_tests/volume_creation/SegmentationLoader.py @@ -24,8 +24,8 @@ def setup(self): label_mask = np.reshape(label_mask, (400, 1, 400)) input_spacing = 0.2 segmentation_volume_tiled = np.tile(label_mask, (1, 128, 1)) - segmentation_volume_mask = np.round(zoom(segmentation_volume_tiled, input_spacing/target_spacing, - order=0)).astype(int) + segmentation_volume_mask = sp.round_x5_away_from_zero(zoom(segmentation_volume_tiled, input_spacing/target_spacing, + order=0)).astype(int) def segmentation_class_mapping(): ret_dict = dict() @@ -72,7 +72,7 @@ def segmentation_class_mapping(): }) self.pipeline = [ - sp.SegmentationBasedVolumeCreationAdapter(self.settings), + sp.SegmentationBasedAdapter(self.settings), sp.MCXAdapter(self.settings) ] @@ -83,7 +83,7 @@ def perform_test(self): device_position_mm=np.asarray([20, 10, 0]))) def tear_down(self): - os.remove(self.settings[Tags.SIMPA_OUTPUT_PATH]) + os.remove(self.settings[Tags.SIMPA_OUTPUT_FILE_PATH]) def visualise_result(self, show_figure_on_screen=True, save_path=None): diff --git a/simpa_tests/test_utils/tissue_composition_tests.py b/simpa_tests/test_utils/tissue_composition_tests.py index daa9f067..7adc8ca0 100644 --- a/simpa_tests/test_utils/tissue_composition_tests.py +++ b/simpa_tests/test_utils/tissue_composition_tests.py @@ -2,7 +2,7 @@ # SPDX-FileCopyrightText: 2021 Janek Groehl # SPDX-License-Identifier: MIT -from simpa.utils import Tags, SegmentationClasses, calculate_gruneisen_parameter_from_temperature +from simpa.utils import Tags, Settings, SegmentationClasses, calculate_gruneisen_parameter_from_temperature from simpa.utils.libraries.molecule_library import MolecularComposition from simpa.utils.tissue_properties import TissueProperties from simpa.utils.constants import property_tags @@ -11,6 +11,14 @@ import matplotlib.patches as patches import matplotlib.pyplot as plt +TEST_SETTINGS = Settings({ + # These parameters set the general properties of the simulated volume + Tags.SPACING_MM: 1, + Tags.DIM_VOLUME_Z_MM: 1, + Tags.DIM_VOLUME_X_MM: 1, + Tags.DIM_VOLUME_Y_MM: 1 +}) + def validate_expected_values_dictionary(expected_values: dict): @@ -41,8 +49,9 @@ def compare_molecular_composition_against_expected_values(molecular_composition: num_subplots = len(property_tags) for wavelength in expected_values.keys(): - molecular_composition.update_internal_properties() - composition_properties = molecular_composition.get_properties_for_wavelength(wavelength=wavelength) + molecular_composition.update_internal_properties(TEST_SETTINGS) + composition_properties = molecular_composition.get_properties_for_wavelength( + TEST_SETTINGS, wavelength=wavelength) expected_properties = expected_values[wavelength] if visualise_values: @@ -92,68 +101,74 @@ def get_epidermis_reference_dictionary(): """ reference_dict = dict() - values450nm = TissueProperties() + values450nm = TissueProperties(TEST_SETTINGS) values450nm[Tags.DATA_FIELD_ABSORPTION_PER_CM] = 13.5 values450nm[Tags.DATA_FIELD_SCATTERING_PER_CM] = 121.6 values450nm[Tags.DATA_FIELD_ANISOTROPY] = 0.728 values450nm[Tags.DATA_FIELD_GRUNEISEN_PARAMETER] = calculate_gruneisen_parameter_from_temperature(37.0) values450nm[Tags.DATA_FIELD_SEGMENTATION] = SegmentationClasses.EPIDERMIS values450nm[Tags.DATA_FIELD_OXYGENATION] = None + values450nm[Tags.DATA_FIELD_BLOOD_VOLUME_FRACTION] = None values450nm[Tags.DATA_FIELD_DENSITY] = 1109 values450nm[Tags.DATA_FIELD_SPEED_OF_SOUND] = 1624.0 values450nm[Tags.DATA_FIELD_ALPHA_COEFF] = 0.35 - values500nm = TissueProperties() + values500nm = TissueProperties(TEST_SETTINGS) values500nm[Tags.DATA_FIELD_ABSORPTION_PER_CM] = 9.77 values500nm[Tags.DATA_FIELD_SCATTERING_PER_CM] = 93.01 values500nm[Tags.DATA_FIELD_ANISOTROPY] = 0.745 values500nm[Tags.DATA_FIELD_GRUNEISEN_PARAMETER] = calculate_gruneisen_parameter_from_temperature(37.0) values500nm[Tags.DATA_FIELD_SEGMENTATION] = SegmentationClasses.EPIDERMIS values500nm[Tags.DATA_FIELD_OXYGENATION] = None + values500nm[Tags.DATA_FIELD_BLOOD_VOLUME_FRACTION] = None values500nm[Tags.DATA_FIELD_DENSITY] = 1109 values500nm[Tags.DATA_FIELD_SPEED_OF_SOUND] = 1624.0 values500nm[Tags.DATA_FIELD_ALPHA_COEFF] = 0.35 - values550nm = TissueProperties() + values550nm = TissueProperties(TEST_SETTINGS) values550nm[Tags.DATA_FIELD_ABSORPTION_PER_CM] = 6.85 values550nm[Tags.DATA_FIELD_SCATTERING_PER_CM] = 74.7 values550nm[Tags.DATA_FIELD_ANISOTROPY] = 0.759 values550nm[Tags.DATA_FIELD_GRUNEISEN_PARAMETER] = calculate_gruneisen_parameter_from_temperature(37.0) values550nm[Tags.DATA_FIELD_SEGMENTATION] = SegmentationClasses.EPIDERMIS values550nm[Tags.DATA_FIELD_OXYGENATION] = None + values550nm[Tags.DATA_FIELD_BLOOD_VOLUME_FRACTION] = None values550nm[Tags.DATA_FIELD_DENSITY] = 1109 values550nm[Tags.DATA_FIELD_SPEED_OF_SOUND] = 1624.0 values550nm[Tags.DATA_FIELD_ALPHA_COEFF] = 0.35 - values600nm = TissueProperties() + values600nm = TissueProperties(TEST_SETTINGS) values600nm[Tags.DATA_FIELD_ABSORPTION_PER_CM] = 5.22 values600nm[Tags.DATA_FIELD_SCATTERING_PER_CM] = 63.76 values600nm[Tags.DATA_FIELD_ANISOTROPY] = 0.774 values600nm[Tags.DATA_FIELD_GRUNEISEN_PARAMETER] = calculate_gruneisen_parameter_from_temperature(37.0) values600nm[Tags.DATA_FIELD_SEGMENTATION] = SegmentationClasses.EPIDERMIS values600nm[Tags.DATA_FIELD_OXYGENATION] = None + values600nm[Tags.DATA_FIELD_BLOOD_VOLUME_FRACTION] = None values600nm[Tags.DATA_FIELD_DENSITY] = 1109 values600nm[Tags.DATA_FIELD_SPEED_OF_SOUND] = 1624.0 values600nm[Tags.DATA_FIELD_ALPHA_COEFF] = 0.35 - values650nm = TissueProperties() + values650nm = TissueProperties(TEST_SETTINGS) values650nm[Tags.DATA_FIELD_ABSORPTION_PER_CM] = 3.68 values650nm[Tags.DATA_FIELD_SCATTERING_PER_CM] = 55.48 values650nm[Tags.DATA_FIELD_ANISOTROPY] = 0.7887 values650nm[Tags.DATA_FIELD_GRUNEISEN_PARAMETER] = calculate_gruneisen_parameter_from_temperature(37.0) values650nm[Tags.DATA_FIELD_SEGMENTATION] = SegmentationClasses.EPIDERMIS values650nm[Tags.DATA_FIELD_OXYGENATION] = None + values650nm[Tags.DATA_FIELD_BLOOD_VOLUME_FRACTION] = None values650nm[Tags.DATA_FIELD_DENSITY] = 1109 values650nm[Tags.DATA_FIELD_SPEED_OF_SOUND] = 1624.0 values650nm[Tags.DATA_FIELD_ALPHA_COEFF] = 0.35 - values700nm = TissueProperties() + values700nm = TissueProperties(TEST_SETTINGS) values700nm[Tags.DATA_FIELD_ABSORPTION_PER_CM] = 3.07 values700nm[Tags.DATA_FIELD_SCATTERING_PER_CM] = 54.66 values700nm[Tags.DATA_FIELD_ANISOTROPY] = 0.804 values700nm[Tags.DATA_FIELD_GRUNEISEN_PARAMETER] = calculate_gruneisen_parameter_from_temperature(37.0) values700nm[Tags.DATA_FIELD_SEGMENTATION] = SegmentationClasses.EPIDERMIS values700nm[Tags.DATA_FIELD_OXYGENATION] = None + values700nm[Tags.DATA_FIELD_BLOOD_VOLUME_FRACTION] = None values700nm[Tags.DATA_FIELD_DENSITY] = 1109 values700nm[Tags.DATA_FIELD_SPEED_OF_SOUND] = 1624.0 values700nm[Tags.DATA_FIELD_ALPHA_COEFF] = 0.35 @@ -189,123 +204,134 @@ def get_dermis_reference_dictionary(): """ reference_dict = dict() - values450nm = TissueProperties() + values450nm = TissueProperties(TEST_SETTINGS) values450nm[Tags.DATA_FIELD_ABSORPTION_PER_CM] = 2.105749981 values450nm[Tags.DATA_FIELD_SCATTERING_PER_CM] = 244.6 values450nm[Tags.DATA_FIELD_ANISOTROPY] = 0.715 values450nm[Tags.DATA_FIELD_GRUNEISEN_PARAMETER] = calculate_gruneisen_parameter_from_temperature(37.0) values450nm[Tags.DATA_FIELD_SEGMENTATION] = SegmentationClasses.DERMIS values450nm[Tags.DATA_FIELD_OXYGENATION] = 0.5 + values450nm[Tags.DATA_FIELD_BLOOD_VOLUME_FRACTION] = None values450nm[Tags.DATA_FIELD_DENSITY] = 1109 values450nm[Tags.DATA_FIELD_SPEED_OF_SOUND] = 1624 values450nm[Tags.DATA_FIELD_ALPHA_COEFF] = 0.35 - values500nm = TissueProperties() + values500nm = TissueProperties(TEST_SETTINGS) values500nm[Tags.DATA_FIELD_ABSORPTION_PER_CM] = 0.924812913 values500nm[Tags.DATA_FIELD_SCATTERING_PER_CM] = 175.0 values500nm[Tags.DATA_FIELD_ANISOTROPY] = 0.715 values500nm[Tags.DATA_FIELD_GRUNEISEN_PARAMETER] = calculate_gruneisen_parameter_from_temperature(37.0) values500nm[Tags.DATA_FIELD_SEGMENTATION] = SegmentationClasses.DERMIS values500nm[Tags.DATA_FIELD_OXYGENATION] = 0.5 + values500nm[Tags.DATA_FIELD_BLOOD_VOLUME_FRACTION] = None values500nm[Tags.DATA_FIELD_DENSITY] = 1109 values500nm[Tags.DATA_FIELD_SPEED_OF_SOUND] = 1624 values500nm[Tags.DATA_FIELD_ALPHA_COEFF] = 0.35 - values550nm = TissueProperties() + values550nm = TissueProperties(TEST_SETTINGS) values550nm[Tags.DATA_FIELD_ABSORPTION_PER_CM] = 0.974386604 values550nm[Tags.DATA_FIELD_SCATTERING_PER_CM] = 131.1 values550nm[Tags.DATA_FIELD_ANISOTROPY] = 0.715 values550nm[Tags.DATA_FIELD_GRUNEISEN_PARAMETER] = calculate_gruneisen_parameter_from_temperature(37.0) values550nm[Tags.DATA_FIELD_SEGMENTATION] = SegmentationClasses.DERMIS values550nm[Tags.DATA_FIELD_OXYGENATION] = 0.5 + values550nm[Tags.DATA_FIELD_BLOOD_VOLUME_FRACTION] = None values550nm[Tags.DATA_FIELD_DENSITY] = 1109 values550nm[Tags.DATA_FIELD_SPEED_OF_SOUND] = 1624 values550nm[Tags.DATA_FIELD_ALPHA_COEFF] = 0.35 - values600nm = TissueProperties() + values600nm = TissueProperties(TEST_SETTINGS) values600nm[Tags.DATA_FIELD_ABSORPTION_PER_CM] = 0.440476363 values600nm[Tags.DATA_FIELD_SCATTERING_PER_CM] = 101.9 values600nm[Tags.DATA_FIELD_ANISOTROPY] = 0.715 values600nm[Tags.DATA_FIELD_GRUNEISEN_PARAMETER] = calculate_gruneisen_parameter_from_temperature(37.0) values600nm[Tags.DATA_FIELD_SEGMENTATION] = SegmentationClasses.DERMIS values600nm[Tags.DATA_FIELD_OXYGENATION] = 0.5 + values600nm[Tags.DATA_FIELD_BLOOD_VOLUME_FRACTION] = None values600nm[Tags.DATA_FIELD_DENSITY] = 1109 values600nm[Tags.DATA_FIELD_SPEED_OF_SOUND] = 1624 values600nm[Tags.DATA_FIELD_ALPHA_COEFF] = 0.35 - values650nm = TissueProperties() + values650nm = TissueProperties(TEST_SETTINGS) values650nm[Tags.DATA_FIELD_ABSORPTION_PER_CM] = 0.313052704 values650nm[Tags.DATA_FIELD_SCATTERING_PER_CM] = 81.7 values650nm[Tags.DATA_FIELD_ANISOTROPY] = 0.715 values650nm[Tags.DATA_FIELD_GRUNEISEN_PARAMETER] = calculate_gruneisen_parameter_from_temperature(37.0) values650nm[Tags.DATA_FIELD_SEGMENTATION] = SegmentationClasses.DERMIS values650nm[Tags.DATA_FIELD_OXYGENATION] = 0.5 + values650nm[Tags.DATA_FIELD_BLOOD_VOLUME_FRACTION] = None values650nm[Tags.DATA_FIELD_DENSITY] = 1109 values650nm[Tags.DATA_FIELD_SPEED_OF_SOUND] = 1624 values650nm[Tags.DATA_FIELD_ALPHA_COEFF] = 0.35 - values700nm = TissueProperties() + values700nm = TissueProperties(TEST_SETTINGS) values700nm[Tags.DATA_FIELD_ABSORPTION_PER_CM] = 0.277003236 values700nm[Tags.DATA_FIELD_SCATTERING_PER_CM] = 67.1 values700nm[Tags.DATA_FIELD_ANISOTROPY] = 0.715 values700nm[Tags.DATA_FIELD_GRUNEISEN_PARAMETER] = calculate_gruneisen_parameter_from_temperature(37.0) values700nm[Tags.DATA_FIELD_SEGMENTATION] = SegmentationClasses.DERMIS values700nm[Tags.DATA_FIELD_OXYGENATION] = 0.5 + values700nm[Tags.DATA_FIELD_BLOOD_VOLUME_FRACTION] = None values700nm[Tags.DATA_FIELD_DENSITY] = 1109 values700nm[Tags.DATA_FIELD_SPEED_OF_SOUND] = 1624 values700nm[Tags.DATA_FIELD_ALPHA_COEFF] = 0.35 - values750nm = TissueProperties() + values750nm = TissueProperties(TEST_SETTINGS) values750nm[Tags.DATA_FIELD_ABSORPTION_PER_CM] = 0.264286111 values750nm[Tags.DATA_FIELD_SCATTERING_PER_CM] = 56.3 values750nm[Tags.DATA_FIELD_ANISOTROPY] = 0.715 values750nm[Tags.DATA_FIELD_GRUNEISEN_PARAMETER] = calculate_gruneisen_parameter_from_temperature(37.0) values750nm[Tags.DATA_FIELD_SEGMENTATION] = SegmentationClasses.DERMIS values750nm[Tags.DATA_FIELD_OXYGENATION] = 0.5 + values750nm[Tags.DATA_FIELD_BLOOD_VOLUME_FRACTION] = None values750nm[Tags.DATA_FIELD_DENSITY] = 1109 values750nm[Tags.DATA_FIELD_SPEED_OF_SOUND] = 1624 values750nm[Tags.DATA_FIELD_ALPHA_COEFF] = 0.35 - values800nm = TissueProperties() + values800nm = TissueProperties(TEST_SETTINGS) values800nm[Tags.DATA_FIELD_ABSORPTION_PER_CM] = 0.256933531 values800nm[Tags.DATA_FIELD_SCATTERING_PER_CM] = 48.1 values800nm[Tags.DATA_FIELD_ANISOTROPY] = 0.715 values800nm[Tags.DATA_FIELD_GRUNEISEN_PARAMETER] = calculate_gruneisen_parameter_from_temperature(37.0) values800nm[Tags.DATA_FIELD_SEGMENTATION] = SegmentationClasses.DERMIS values800nm[Tags.DATA_FIELD_OXYGENATION] = 0.5 + values800nm[Tags.DATA_FIELD_BLOOD_VOLUME_FRACTION] = None values800nm[Tags.DATA_FIELD_DENSITY] = 1109 values800nm[Tags.DATA_FIELD_SPEED_OF_SOUND] = 1624 values800nm[Tags.DATA_FIELD_ALPHA_COEFF] = 0.35 - values850nm = TissueProperties() + values850nm = TissueProperties(TEST_SETTINGS) values850nm[Tags.DATA_FIELD_ABSORPTION_PER_CM] = 0.255224508 values850nm[Tags.DATA_FIELD_SCATTERING_PER_CM] = 41.8 values850nm[Tags.DATA_FIELD_ANISOTROPY] = 0.715 values850nm[Tags.DATA_FIELD_GRUNEISEN_PARAMETER] = calculate_gruneisen_parameter_from_temperature(37.0) values850nm[Tags.DATA_FIELD_SEGMENTATION] = SegmentationClasses.DERMIS values850nm[Tags.DATA_FIELD_OXYGENATION] = 0.5 + values850nm[Tags.DATA_FIELD_BLOOD_VOLUME_FRACTION] = None values850nm[Tags.DATA_FIELD_DENSITY] = 1109 values850nm[Tags.DATA_FIELD_SPEED_OF_SOUND] = 1624 values850nm[Tags.DATA_FIELD_ALPHA_COEFF] = 0.35 - values900nm = TissueProperties() + values900nm = TissueProperties(TEST_SETTINGS) values900nm[Tags.DATA_FIELD_ABSORPTION_PER_CM] = 0.254198591 values900nm[Tags.DATA_FIELD_SCATTERING_PER_CM] = 36.7 values900nm[Tags.DATA_FIELD_ANISOTROPY] = 0.715 values900nm[Tags.DATA_FIELD_GRUNEISEN_PARAMETER] = calculate_gruneisen_parameter_from_temperature(37.0) values900nm[Tags.DATA_FIELD_SEGMENTATION] = SegmentationClasses.DERMIS values900nm[Tags.DATA_FIELD_OXYGENATION] = 0.5 + values900nm[Tags.DATA_FIELD_BLOOD_VOLUME_FRACTION] = None values900nm[Tags.DATA_FIELD_DENSITY] = 1109 values900nm[Tags.DATA_FIELD_SPEED_OF_SOUND] = 1624 values900nm[Tags.DATA_FIELD_ALPHA_COEFF] = 0.35 - values950nm = TissueProperties() + values950nm = TissueProperties(TEST_SETTINGS) values950nm[Tags.DATA_FIELD_ABSORPTION_PER_CM] = 0.254522563 values950nm[Tags.DATA_FIELD_SCATTERING_PER_CM] = 32.6 values950nm[Tags.DATA_FIELD_ANISOTROPY] = 0.715 values950nm[Tags.DATA_FIELD_GRUNEISEN_PARAMETER] = calculate_gruneisen_parameter_from_temperature(37.0) values950nm[Tags.DATA_FIELD_SEGMENTATION] = SegmentationClasses.DERMIS values950nm[Tags.DATA_FIELD_OXYGENATION] = 0.5 + values950nm[Tags.DATA_FIELD_BLOOD_VOLUME_FRACTION] = None values950nm[Tags.DATA_FIELD_DENSITY] = 1109 values950nm[Tags.DATA_FIELD_SPEED_OF_SOUND] = 1624 values950nm[Tags.DATA_FIELD_ALPHA_COEFF] = 0.35 @@ -342,79 +368,86 @@ def get_muscle_reference_dictionary(): """ reference_dict = dict() - values650nm = TissueProperties() + values650nm = TissueProperties(TEST_SETTINGS) values650nm[Tags.DATA_FIELD_ABSORPTION_PER_CM] = 1.04 values650nm[Tags.DATA_FIELD_SCATTERING_PER_CM] = 87.5 values650nm[Tags.DATA_FIELD_ANISOTROPY] = 0.9 values650nm[Tags.DATA_FIELD_GRUNEISEN_PARAMETER] = calculate_gruneisen_parameter_from_temperature(37.0) values650nm[Tags.DATA_FIELD_SEGMENTATION] = SegmentationClasses.MUSCLE values650nm[Tags.DATA_FIELD_OXYGENATION] = 0.175 + values650nm[Tags.DATA_FIELD_BLOOD_VOLUME_FRACTION] = None values650nm[Tags.DATA_FIELD_DENSITY] = 1090.4 values650nm[Tags.DATA_FIELD_SPEED_OF_SOUND] = 1588.4 values650nm[Tags.DATA_FIELD_ALPHA_COEFF] = 1.09 - values700nm = TissueProperties() + values700nm = TissueProperties(TEST_SETTINGS) values700nm[Tags.DATA_FIELD_ABSORPTION_PER_CM] = 0.48 values700nm[Tags.DATA_FIELD_SCATTERING_PER_CM] = 81.8 values700nm[Tags.DATA_FIELD_ANISOTROPY] = 0.9 values700nm[Tags.DATA_FIELD_GRUNEISEN_PARAMETER] = calculate_gruneisen_parameter_from_temperature(37.0) values700nm[Tags.DATA_FIELD_SEGMENTATION] = SegmentationClasses.MUSCLE values700nm[Tags.DATA_FIELD_OXYGENATION] = 0.175 + values700nm[Tags.DATA_FIELD_BLOOD_VOLUME_FRACTION] = None values700nm[Tags.DATA_FIELD_DENSITY] = 1090.4 values700nm[Tags.DATA_FIELD_SPEED_OF_SOUND] = 1588.4 values700nm[Tags.DATA_FIELD_ALPHA_COEFF] = 1.09 - values750nm = TissueProperties() + values750nm = TissueProperties(TEST_SETTINGS) values750nm[Tags.DATA_FIELD_ABSORPTION_PER_CM] = 0.41 values750nm[Tags.DATA_FIELD_SCATTERING_PER_CM] = 77.1 values750nm[Tags.DATA_FIELD_ANISOTROPY] = 0.9 values750nm[Tags.DATA_FIELD_GRUNEISEN_PARAMETER] = calculate_gruneisen_parameter_from_temperature(37.0) values750nm[Tags.DATA_FIELD_SEGMENTATION] = SegmentationClasses.MUSCLE values750nm[Tags.DATA_FIELD_OXYGENATION] = 0.175 + values750nm[Tags.DATA_FIELD_BLOOD_VOLUME_FRACTION] = None values750nm[Tags.DATA_FIELD_DENSITY] = 1090.4 values750nm[Tags.DATA_FIELD_SPEED_OF_SOUND] = 1588.4 values750nm[Tags.DATA_FIELD_ALPHA_COEFF] = 1.09 - values800nm = TissueProperties() + values800nm = TissueProperties(TEST_SETTINGS) values800nm[Tags.DATA_FIELD_ABSORPTION_PER_CM] = 0.28 values800nm[Tags.DATA_FIELD_SCATTERING_PER_CM] = 70.4 values800nm[Tags.DATA_FIELD_ANISOTROPY] = 0.9 values800nm[Tags.DATA_FIELD_GRUNEISEN_PARAMETER] = calculate_gruneisen_parameter_from_temperature(37.0) values800nm[Tags.DATA_FIELD_SEGMENTATION] = SegmentationClasses.MUSCLE values800nm[Tags.DATA_FIELD_OXYGENATION] = 0.175 + values800nm[Tags.DATA_FIELD_BLOOD_VOLUME_FRACTION] = None values800nm[Tags.DATA_FIELD_DENSITY] = 1090.4 values800nm[Tags.DATA_FIELD_SPEED_OF_SOUND] = 1588.4 values800nm[Tags.DATA_FIELD_ALPHA_COEFF] = 1.09 - values850nm = TissueProperties() + values850nm = TissueProperties(TEST_SETTINGS) values850nm[Tags.DATA_FIELD_ABSORPTION_PER_CM] = 0.3 values850nm[Tags.DATA_FIELD_SCATTERING_PER_CM] = 66.7 values850nm[Tags.DATA_FIELD_ANISOTROPY] = 0.9 values850nm[Tags.DATA_FIELD_GRUNEISEN_PARAMETER] = calculate_gruneisen_parameter_from_temperature(37.0) values850nm[Tags.DATA_FIELD_SEGMENTATION] = SegmentationClasses.MUSCLE values850nm[Tags.DATA_FIELD_OXYGENATION] = 0.175 + values850nm[Tags.DATA_FIELD_BLOOD_VOLUME_FRACTION] = None values850nm[Tags.DATA_FIELD_DENSITY] = 1090.4 values850nm[Tags.DATA_FIELD_SPEED_OF_SOUND] = 1588.4 values850nm[Tags.DATA_FIELD_ALPHA_COEFF] = 1.09 - values900nm = TissueProperties() + values900nm = TissueProperties(TEST_SETTINGS) values900nm[Tags.DATA_FIELD_ABSORPTION_PER_CM] = 0.32 values900nm[Tags.DATA_FIELD_SCATTERING_PER_CM] = 62.1 values900nm[Tags.DATA_FIELD_ANISOTROPY] = 0.9 values900nm[Tags.DATA_FIELD_GRUNEISEN_PARAMETER] = calculate_gruneisen_parameter_from_temperature(37.0) values900nm[Tags.DATA_FIELD_SEGMENTATION] = SegmentationClasses.MUSCLE values900nm[Tags.DATA_FIELD_OXYGENATION] = 0.175 + values900nm[Tags.DATA_FIELD_BLOOD_VOLUME_FRACTION] = None values900nm[Tags.DATA_FIELD_DENSITY] = 1090.4 values900nm[Tags.DATA_FIELD_SPEED_OF_SOUND] = 1588.4 values900nm[Tags.DATA_FIELD_ALPHA_COEFF] = 1.09 - values950nm = TissueProperties() + values950nm = TissueProperties(TEST_SETTINGS) values950nm[Tags.DATA_FIELD_ABSORPTION_PER_CM] = 0.46 values950nm[Tags.DATA_FIELD_SCATTERING_PER_CM] = 59.0 values950nm[Tags.DATA_FIELD_ANISOTROPY] = 0.9 values950nm[Tags.DATA_FIELD_GRUNEISEN_PARAMETER] = calculate_gruneisen_parameter_from_temperature(37.0) values950nm[Tags.DATA_FIELD_SEGMENTATION] = SegmentationClasses.MUSCLE values950nm[Tags.DATA_FIELD_OXYGENATION] = 0.175 + values950nm[Tags.DATA_FIELD_BLOOD_VOLUME_FRACTION] = None values950nm[Tags.DATA_FIELD_DENSITY] = 1090.4 values950nm[Tags.DATA_FIELD_SPEED_OF_SOUND] = 1588.4 values950nm[Tags.DATA_FIELD_ALPHA_COEFF] = 1.09 @@ -451,123 +484,134 @@ def get_fully_oxygenated_blood_reference_dictionary(only_use_NIR_values=False): """ reference_dict = dict() - values450nm = TissueProperties() + values450nm = TissueProperties(TEST_SETTINGS) values450nm[Tags.DATA_FIELD_ABSORPTION_PER_CM] = 336 values450nm[Tags.DATA_FIELD_SCATTERING_PER_CM] = 772 values450nm[Tags.DATA_FIELD_ANISOTROPY] = 0.9447 values450nm[Tags.DATA_FIELD_GRUNEISEN_PARAMETER] = calculate_gruneisen_parameter_from_temperature(37.0) values450nm[Tags.DATA_FIELD_SEGMENTATION] = SegmentationClasses.BLOOD values450nm[Tags.DATA_FIELD_OXYGENATION] = 1.0 + values450nm[Tags.DATA_FIELD_BLOOD_VOLUME_FRACTION] = 1.0 values450nm[Tags.DATA_FIELD_DENSITY] = 1049.75 values450nm[Tags.DATA_FIELD_SPEED_OF_SOUND] = 1578.2 values450nm[Tags.DATA_FIELD_ALPHA_COEFF] = 0.2 - values500nm = TissueProperties() + values500nm = TissueProperties(TEST_SETTINGS) values500nm[Tags.DATA_FIELD_ABSORPTION_PER_CM] = 112 values500nm[Tags.DATA_FIELD_SCATTERING_PER_CM] = 868.3 values500nm[Tags.DATA_FIELD_ANISOTROPY] = 0.9761 values500nm[Tags.DATA_FIELD_GRUNEISEN_PARAMETER] = calculate_gruneisen_parameter_from_temperature(37.0) values500nm[Tags.DATA_FIELD_SEGMENTATION] = SegmentationClasses.BLOOD values500nm[Tags.DATA_FIELD_OXYGENATION] = 1.0 + values500nm[Tags.DATA_FIELD_BLOOD_VOLUME_FRACTION] = 1.0 values500nm[Tags.DATA_FIELD_DENSITY] = 1049.75 values500nm[Tags.DATA_FIELD_SPEED_OF_SOUND] = 1578.2 values500nm[Tags.DATA_FIELD_ALPHA_COEFF] = 0.2 - values550nm = TissueProperties() + values550nm = TissueProperties(TEST_SETTINGS) values550nm[Tags.DATA_FIELD_ABSORPTION_PER_CM] = 230 values550nm[Tags.DATA_FIELD_SCATTERING_PER_CM] = 714.9 values550nm[Tags.DATA_FIELD_ANISOTROPY] = 0.9642 values550nm[Tags.DATA_FIELD_GRUNEISEN_PARAMETER] = calculate_gruneisen_parameter_from_temperature(37.0) values550nm[Tags.DATA_FIELD_SEGMENTATION] = SegmentationClasses.BLOOD values550nm[Tags.DATA_FIELD_OXYGENATION] = 1.0 + values550nm[Tags.DATA_FIELD_BLOOD_VOLUME_FRACTION] = 1.0 values550nm[Tags.DATA_FIELD_DENSITY] = 1049.75 values550nm[Tags.DATA_FIELD_SPEED_OF_SOUND] = 1578.2 values550nm[Tags.DATA_FIELD_ALPHA_COEFF] = 0.2 - values600nm = TissueProperties() + values600nm = TissueProperties(TEST_SETTINGS) values600nm[Tags.DATA_FIELD_ABSORPTION_PER_CM] = 17 values600nm[Tags.DATA_FIELD_SCATTERING_PER_CM] = 868.8 values600nm[Tags.DATA_FIELD_ANISOTROPY] = 0.9794 values600nm[Tags.DATA_FIELD_GRUNEISEN_PARAMETER] = calculate_gruneisen_parameter_from_temperature(37.0) values600nm[Tags.DATA_FIELD_SEGMENTATION] = SegmentationClasses.BLOOD values600nm[Tags.DATA_FIELD_OXYGENATION] = 1.0 + values600nm[Tags.DATA_FIELD_BLOOD_VOLUME_FRACTION] = 1.0 values600nm[Tags.DATA_FIELD_DENSITY] = 1049.75 values600nm[Tags.DATA_FIELD_SPEED_OF_SOUND] = 1578.2 values600nm[Tags.DATA_FIELD_ALPHA_COEFF] = 0.2 - values650nm = TissueProperties() + values650nm = TissueProperties(TEST_SETTINGS) values650nm[Tags.DATA_FIELD_ABSORPTION_PER_CM] = 2 values650nm[Tags.DATA_FIELD_SCATTERING_PER_CM] = 880.1 values650nm[Tags.DATA_FIELD_ANISOTROPY] = 0.9825 values650nm[Tags.DATA_FIELD_GRUNEISEN_PARAMETER] = calculate_gruneisen_parameter_from_temperature(37.0) values650nm[Tags.DATA_FIELD_SEGMENTATION] = SegmentationClasses.BLOOD values650nm[Tags.DATA_FIELD_OXYGENATION] = 1.0 + values650nm[Tags.DATA_FIELD_BLOOD_VOLUME_FRACTION] = 1.0 values650nm[Tags.DATA_FIELD_DENSITY] = 1049.75 values650nm[Tags.DATA_FIELD_SPEED_OF_SOUND] = 1578.2 values650nm[Tags.DATA_FIELD_ALPHA_COEFF] = 0.2 - values700nm = TissueProperties() + values700nm = TissueProperties(TEST_SETTINGS) values700nm[Tags.DATA_FIELD_ABSORPTION_PER_CM] = 1.6 values700nm[Tags.DATA_FIELD_SCATTERING_PER_CM] = 857.0 values700nm[Tags.DATA_FIELD_ANISOTROPY] = 0.9836 values700nm[Tags.DATA_FIELD_GRUNEISEN_PARAMETER] = calculate_gruneisen_parameter_from_temperature(37.0) values700nm[Tags.DATA_FIELD_SEGMENTATION] = SegmentationClasses.BLOOD values700nm[Tags.DATA_FIELD_OXYGENATION] = 1.0 + values700nm[Tags.DATA_FIELD_BLOOD_VOLUME_FRACTION] = 1.0 values700nm[Tags.DATA_FIELD_DENSITY] = 1049.75 values700nm[Tags.DATA_FIELD_SPEED_OF_SOUND] = 1578.2 values700nm[Tags.DATA_FIELD_ALPHA_COEFF] = 0.2 - values750nm = TissueProperties() + values750nm = TissueProperties(TEST_SETTINGS) values750nm[Tags.DATA_FIELD_ABSORPTION_PER_CM] = 2.8 values750nm[Tags.DATA_FIELD_SCATTERING_PER_CM] = 802.2 values750nm[Tags.DATA_FIELD_ANISOTROPY] = 0.9837 values750nm[Tags.DATA_FIELD_GRUNEISEN_PARAMETER] = calculate_gruneisen_parameter_from_temperature(37.0) values750nm[Tags.DATA_FIELD_SEGMENTATION] = SegmentationClasses.BLOOD values750nm[Tags.DATA_FIELD_OXYGENATION] = 1.0 + values750nm[Tags.DATA_FIELD_BLOOD_VOLUME_FRACTION] = 1.0 values750nm[Tags.DATA_FIELD_DENSITY] = 1049.75 values750nm[Tags.DATA_FIELD_SPEED_OF_SOUND] = 1578.2 values750nm[Tags.DATA_FIELD_ALPHA_COEFF] = 0.2 - values800nm = TissueProperties() + values800nm = TissueProperties(TEST_SETTINGS) values800nm[Tags.DATA_FIELD_ABSORPTION_PER_CM] = 4.4 values800nm[Tags.DATA_FIELD_SCATTERING_PER_CM] = 767.3 values800nm[Tags.DATA_FIELD_ANISOTROPY] = 0.9833 values800nm[Tags.DATA_FIELD_GRUNEISEN_PARAMETER] = calculate_gruneisen_parameter_from_temperature(37.0) values800nm[Tags.DATA_FIELD_SEGMENTATION] = SegmentationClasses.BLOOD values800nm[Tags.DATA_FIELD_OXYGENATION] = 1.0 + values800nm[Tags.DATA_FIELD_BLOOD_VOLUME_FRACTION] = 1.0 values800nm[Tags.DATA_FIELD_DENSITY] = 1049.75 values800nm[Tags.DATA_FIELD_SPEED_OF_SOUND] = 1578.2 values800nm[Tags.DATA_FIELD_ALPHA_COEFF] = 0.2 - values850nm = TissueProperties() + values850nm = TissueProperties(TEST_SETTINGS) values850nm[Tags.DATA_FIELD_ABSORPTION_PER_CM] = 5.7 values850nm[Tags.DATA_FIELD_SCATTERING_PER_CM] = 742.0 values850nm[Tags.DATA_FIELD_ANISOTROPY] = 0.9832 values850nm[Tags.DATA_FIELD_GRUNEISEN_PARAMETER] = calculate_gruneisen_parameter_from_temperature(37.0) values850nm[Tags.DATA_FIELD_SEGMENTATION] = SegmentationClasses.BLOOD values850nm[Tags.DATA_FIELD_OXYGENATION] = 1.0 + values850nm[Tags.DATA_FIELD_BLOOD_VOLUME_FRACTION] = 1.0 values850nm[Tags.DATA_FIELD_DENSITY] = 1049.75 values850nm[Tags.DATA_FIELD_SPEED_OF_SOUND] = 1578.2 values850nm[Tags.DATA_FIELD_ALPHA_COEFF] = 0.2 - values900nm = TissueProperties() + values900nm = TissueProperties(TEST_SETTINGS) values900nm[Tags.DATA_FIELD_ABSORPTION_PER_CM] = 6.4 values900nm[Tags.DATA_FIELD_SCATTERING_PER_CM] = 688.6 values900nm[Tags.DATA_FIELD_ANISOTROPY] = 0.9824 values900nm[Tags.DATA_FIELD_GRUNEISEN_PARAMETER] = calculate_gruneisen_parameter_from_temperature(37.0) values900nm[Tags.DATA_FIELD_SEGMENTATION] = SegmentationClasses.BLOOD values900nm[Tags.DATA_FIELD_OXYGENATION] = 1.0 + values900nm[Tags.DATA_FIELD_BLOOD_VOLUME_FRACTION] = 1.0 values900nm[Tags.DATA_FIELD_DENSITY] = 1049.75 values900nm[Tags.DATA_FIELD_SPEED_OF_SOUND] = 1578.2 values900nm[Tags.DATA_FIELD_ALPHA_COEFF] = 0.2 - values950nm = TissueProperties() + values950nm = TissueProperties(TEST_SETTINGS) values950nm[Tags.DATA_FIELD_ABSORPTION_PER_CM] = 6.4 values950nm[Tags.DATA_FIELD_SCATTERING_PER_CM] = 652.1 values950nm[Tags.DATA_FIELD_ANISOTROPY] = 0.9808 values950nm[Tags.DATA_FIELD_GRUNEISEN_PARAMETER] = calculate_gruneisen_parameter_from_temperature(37.0) values950nm[Tags.DATA_FIELD_SEGMENTATION] = SegmentationClasses.BLOOD values950nm[Tags.DATA_FIELD_OXYGENATION] = 1.0 + values950nm[Tags.DATA_FIELD_BLOOD_VOLUME_FRACTION] = 1.0 values950nm[Tags.DATA_FIELD_DENSITY] = 1049.75 values950nm[Tags.DATA_FIELD_SPEED_OF_SOUND] = 1578.2 values950nm[Tags.DATA_FIELD_ALPHA_COEFF] = 0.2 @@ -609,123 +653,134 @@ def get_fully_deoxygenated_blood_reference_dictionary(only_use_NIR_values=False) """ reference_dict = dict() - values450nm = TissueProperties() + values450nm = TissueProperties(TEST_SETTINGS) values450nm[Tags.DATA_FIELD_ABSORPTION_PER_CM] = 553 values450nm[Tags.DATA_FIELD_SCATTERING_PER_CM] = 772 values450nm[Tags.DATA_FIELD_ANISOTROPY] = 0.9447 values450nm[Tags.DATA_FIELD_GRUNEISEN_PARAMETER] = calculate_gruneisen_parameter_from_temperature(37.0) values450nm[Tags.DATA_FIELD_SEGMENTATION] = SegmentationClasses.BLOOD values450nm[Tags.DATA_FIELD_OXYGENATION] = 0.0 + values450nm[Tags.DATA_FIELD_BLOOD_VOLUME_FRACTION] = 1.0 values450nm[Tags.DATA_FIELD_DENSITY] = 1049.75 values450nm[Tags.DATA_FIELD_SPEED_OF_SOUND] = 1578.2 values450nm[Tags.DATA_FIELD_ALPHA_COEFF] = 0.2 - values500nm = TissueProperties() + values500nm = TissueProperties(TEST_SETTINGS) values500nm[Tags.DATA_FIELD_ABSORPTION_PER_CM] = 112 values500nm[Tags.DATA_FIELD_SCATTERING_PER_CM] = 868.3 values500nm[Tags.DATA_FIELD_ANISOTROPY] = 0.9761 values500nm[Tags.DATA_FIELD_GRUNEISEN_PARAMETER] = calculate_gruneisen_parameter_from_temperature(37.0) values500nm[Tags.DATA_FIELD_SEGMENTATION] = SegmentationClasses.BLOOD values500nm[Tags.DATA_FIELD_OXYGENATION] = 0.0 + values500nm[Tags.DATA_FIELD_BLOOD_VOLUME_FRACTION] = 1.0 values500nm[Tags.DATA_FIELD_DENSITY] = 1049.75 values500nm[Tags.DATA_FIELD_SPEED_OF_SOUND] = 1578.2 values500nm[Tags.DATA_FIELD_ALPHA_COEFF] = 0.2 - values550nm = TissueProperties() + values550nm = TissueProperties(TEST_SETTINGS) values550nm[Tags.DATA_FIELD_ABSORPTION_PER_CM] = 286 values550nm[Tags.DATA_FIELD_SCATTERING_PER_CM] = 714.9 values550nm[Tags.DATA_FIELD_ANISOTROPY] = 0.9642 values550nm[Tags.DATA_FIELD_GRUNEISEN_PARAMETER] = calculate_gruneisen_parameter_from_temperature(37.0) values550nm[Tags.DATA_FIELD_SEGMENTATION] = SegmentationClasses.BLOOD values550nm[Tags.DATA_FIELD_OXYGENATION] = 0.0 + values550nm[Tags.DATA_FIELD_BLOOD_VOLUME_FRACTION] = 1.0 values550nm[Tags.DATA_FIELD_DENSITY] = 1049.75 values550nm[Tags.DATA_FIELD_SPEED_OF_SOUND] = 1578.2 values550nm[Tags.DATA_FIELD_ALPHA_COEFF] = 0.2 - values600nm = TissueProperties() + values600nm = TissueProperties(TEST_SETTINGS) values600nm[Tags.DATA_FIELD_ABSORPTION_PER_CM] = 79 values600nm[Tags.DATA_FIELD_SCATTERING_PER_CM] = 868.8 values600nm[Tags.DATA_FIELD_ANISOTROPY] = 0.9794 values600nm[Tags.DATA_FIELD_GRUNEISEN_PARAMETER] = calculate_gruneisen_parameter_from_temperature(37.0) values600nm[Tags.DATA_FIELD_SEGMENTATION] = SegmentationClasses.BLOOD values600nm[Tags.DATA_FIELD_OXYGENATION] = 0.0 + values600nm[Tags.DATA_FIELD_BLOOD_VOLUME_FRACTION] = 1.0 values600nm[Tags.DATA_FIELD_DENSITY] = 1049.75 values600nm[Tags.DATA_FIELD_SPEED_OF_SOUND] = 1578.2 values600nm[Tags.DATA_FIELD_ALPHA_COEFF] = 0.2 - values650nm = TissueProperties() + values650nm = TissueProperties(TEST_SETTINGS) values650nm[Tags.DATA_FIELD_ABSORPTION_PER_CM] = 20.1 values650nm[Tags.DATA_FIELD_SCATTERING_PER_CM] = 880.1 values650nm[Tags.DATA_FIELD_ANISOTROPY] = 0.9825 values650nm[Tags.DATA_FIELD_GRUNEISEN_PARAMETER] = calculate_gruneisen_parameter_from_temperature(37.0) values650nm[Tags.DATA_FIELD_SEGMENTATION] = SegmentationClasses.BLOOD values650nm[Tags.DATA_FIELD_OXYGENATION] = 0.0 + values650nm[Tags.DATA_FIELD_BLOOD_VOLUME_FRACTION] = 1.0 values650nm[Tags.DATA_FIELD_DENSITY] = 1049.75 values650nm[Tags.DATA_FIELD_SPEED_OF_SOUND] = 1578.2 values650nm[Tags.DATA_FIELD_ALPHA_COEFF] = 0.2 - values700nm = TissueProperties() + values700nm = TissueProperties(TEST_SETTINGS) values700nm[Tags.DATA_FIELD_ABSORPTION_PER_CM] = 9.6 values700nm[Tags.DATA_FIELD_SCATTERING_PER_CM] = 857.0 values700nm[Tags.DATA_FIELD_ANISOTROPY] = 0.9836 values700nm[Tags.DATA_FIELD_GRUNEISEN_PARAMETER] = calculate_gruneisen_parameter_from_temperature(37.0) values700nm[Tags.DATA_FIELD_SEGMENTATION] = SegmentationClasses.BLOOD values700nm[Tags.DATA_FIELD_OXYGENATION] = 0.0 + values700nm[Tags.DATA_FIELD_BLOOD_VOLUME_FRACTION] = 1.0 values700nm[Tags.DATA_FIELD_DENSITY] = 1049.75 values700nm[Tags.DATA_FIELD_SPEED_OF_SOUND] = 1578.2 values700nm[Tags.DATA_FIELD_ALPHA_COEFF] = 0.2 - values750nm = TissueProperties() + values750nm = TissueProperties(TEST_SETTINGS) values750nm[Tags.DATA_FIELD_ABSORPTION_PER_CM] = 7.5 values750nm[Tags.DATA_FIELD_SCATTERING_PER_CM] = 802.2 values750nm[Tags.DATA_FIELD_ANISOTROPY] = 0.9837 values750nm[Tags.DATA_FIELD_GRUNEISEN_PARAMETER] = calculate_gruneisen_parameter_from_temperature(37.0) values750nm[Tags.DATA_FIELD_SEGMENTATION] = SegmentationClasses.BLOOD values750nm[Tags.DATA_FIELD_OXYGENATION] = 0.0 + values750nm[Tags.DATA_FIELD_BLOOD_VOLUME_FRACTION] = 1.0 values750nm[Tags.DATA_FIELD_DENSITY] = 1049.75 values750nm[Tags.DATA_FIELD_SPEED_OF_SOUND] = 1578.2 values750nm[Tags.DATA_FIELD_ALPHA_COEFF] = 0.2 - values800nm = TissueProperties() + values800nm = TissueProperties(TEST_SETTINGS) values800nm[Tags.DATA_FIELD_ABSORPTION_PER_CM] = 4.1 values800nm[Tags.DATA_FIELD_SCATTERING_PER_CM] = 767.3 values800nm[Tags.DATA_FIELD_ANISOTROPY] = 0.9833 values800nm[Tags.DATA_FIELD_GRUNEISEN_PARAMETER] = calculate_gruneisen_parameter_from_temperature(37.0) values800nm[Tags.DATA_FIELD_SEGMENTATION] = SegmentationClasses.BLOOD values800nm[Tags.DATA_FIELD_OXYGENATION] = 0.0 + values800nm[Tags.DATA_FIELD_BLOOD_VOLUME_FRACTION] = 1.0 values800nm[Tags.DATA_FIELD_DENSITY] = 1049.75 values800nm[Tags.DATA_FIELD_SPEED_OF_SOUND] = 1578.2 values800nm[Tags.DATA_FIELD_ALPHA_COEFF] = 0.2 - values850nm = TissueProperties() + values850nm = TissueProperties(TEST_SETTINGS) values850nm[Tags.DATA_FIELD_ABSORPTION_PER_CM] = 3.7 values850nm[Tags.DATA_FIELD_SCATTERING_PER_CM] = 742.0 values850nm[Tags.DATA_FIELD_ANISOTROPY] = 0.9832 values850nm[Tags.DATA_FIELD_GRUNEISEN_PARAMETER] = calculate_gruneisen_parameter_from_temperature(37.0) values850nm[Tags.DATA_FIELD_SEGMENTATION] = SegmentationClasses.BLOOD values850nm[Tags.DATA_FIELD_OXYGENATION] = 0.0 + values850nm[Tags.DATA_FIELD_BLOOD_VOLUME_FRACTION] = 1.0 values850nm[Tags.DATA_FIELD_DENSITY] = 1049.75 values850nm[Tags.DATA_FIELD_SPEED_OF_SOUND] = 1578.2 values850nm[Tags.DATA_FIELD_ALPHA_COEFF] = 0.2 - values900nm = TissueProperties() + values900nm = TissueProperties(TEST_SETTINGS) values900nm[Tags.DATA_FIELD_ABSORPTION_PER_CM] = 4.1 values900nm[Tags.DATA_FIELD_SCATTERING_PER_CM] = 688.6 values900nm[Tags.DATA_FIELD_ANISOTROPY] = 0.9824 values900nm[Tags.DATA_FIELD_GRUNEISEN_PARAMETER] = calculate_gruneisen_parameter_from_temperature(37.0) values900nm[Tags.DATA_FIELD_SEGMENTATION] = SegmentationClasses.BLOOD values900nm[Tags.DATA_FIELD_OXYGENATION] = 0.0 + values900nm[Tags.DATA_FIELD_BLOOD_VOLUME_FRACTION] = 1.0 values900nm[Tags.DATA_FIELD_DENSITY] = 1049.75 values900nm[Tags.DATA_FIELD_SPEED_OF_SOUND] = 1578.2 values900nm[Tags.DATA_FIELD_ALPHA_COEFF] = 0.2 - values950nm = TissueProperties() + values950nm = TissueProperties(TEST_SETTINGS) values950nm[Tags.DATA_FIELD_ABSORPTION_PER_CM] = 3.2 values950nm[Tags.DATA_FIELD_SCATTERING_PER_CM] = 652.1 values950nm[Tags.DATA_FIELD_ANISOTROPY] = 0.9808 values950nm[Tags.DATA_FIELD_GRUNEISEN_PARAMETER] = calculate_gruneisen_parameter_from_temperature(37.0) values950nm[Tags.DATA_FIELD_SEGMENTATION] = SegmentationClasses.BLOOD values950nm[Tags.DATA_FIELD_OXYGENATION] = 0.0 + values950nm[Tags.DATA_FIELD_BLOOD_VOLUME_FRACTION] = 1.0 values950nm[Tags.DATA_FIELD_DENSITY] = 1049.75 values950nm[Tags.DATA_FIELD_SPEED_OF_SOUND] = 1578.2 values950nm[Tags.DATA_FIELD_ALPHA_COEFF] = 0.2 @@ -753,123 +808,134 @@ def get_lymph_node_reference_dictionary(only_use_NIR_values=False): """ reference_dict = dict() - values450nm = TissueProperties() + values450nm = TissueProperties(TEST_SETTINGS) values450nm[Tags.DATA_FIELD_ABSORPTION_PER_CM] = None values450nm[Tags.DATA_FIELD_SCATTERING_PER_CM] = None values450nm[Tags.DATA_FIELD_ANISOTROPY] = None values450nm[Tags.DATA_FIELD_GRUNEISEN_PARAMETER] = calculate_gruneisen_parameter_from_temperature(37.0) values450nm[Tags.DATA_FIELD_SEGMENTATION] = SegmentationClasses.LYMPH_NODE values450nm[Tags.DATA_FIELD_OXYGENATION] = 0.73 + values450nm[Tags.DATA_FIELD_BLOOD_VOLUME_FRACTION] = 0.14 values450nm[Tags.DATA_FIELD_DENSITY] = 1035 values450nm[Tags.DATA_FIELD_SPEED_OF_SOUND] = 1586 values450nm[Tags.DATA_FIELD_ALPHA_COEFF] = 2.50 - values500nm = TissueProperties() + values500nm = TissueProperties(TEST_SETTINGS) values500nm[Tags.DATA_FIELD_ABSORPTION_PER_CM] = None values500nm[Tags.DATA_FIELD_SCATTERING_PER_CM] = None values500nm[Tags.DATA_FIELD_ANISOTROPY] = None values500nm[Tags.DATA_FIELD_GRUNEISEN_PARAMETER] = calculate_gruneisen_parameter_from_temperature(37.0) values500nm[Tags.DATA_FIELD_SEGMENTATION] = SegmentationClasses.LYMPH_NODE values500nm[Tags.DATA_FIELD_OXYGENATION] = 0.73 + values500nm[Tags.DATA_FIELD_BLOOD_VOLUME_FRACTION] = 0.14 values500nm[Tags.DATA_FIELD_DENSITY] = 1035 values500nm[Tags.DATA_FIELD_SPEED_OF_SOUND] = 1586 values500nm[Tags.DATA_FIELD_ALPHA_COEFF] = 2.50 - values550nm = TissueProperties() + values550nm = TissueProperties(TEST_SETTINGS) values550nm[Tags.DATA_FIELD_ABSORPTION_PER_CM] = None values550nm[Tags.DATA_FIELD_SCATTERING_PER_CM] = None values550nm[Tags.DATA_FIELD_ANISOTROPY] = None values550nm[Tags.DATA_FIELD_GRUNEISEN_PARAMETER] = calculate_gruneisen_parameter_from_temperature(37.0) values550nm[Tags.DATA_FIELD_SEGMENTATION] = SegmentationClasses.LYMPH_NODE values550nm[Tags.DATA_FIELD_OXYGENATION] = 0.73 + values550nm[Tags.DATA_FIELD_BLOOD_VOLUME_FRACTION] = 0.14 values550nm[Tags.DATA_FIELD_DENSITY] = 1035 values550nm[Tags.DATA_FIELD_SPEED_OF_SOUND] = 1586 values550nm[Tags.DATA_FIELD_ALPHA_COEFF] = 2.50 - values600nm = TissueProperties() + values600nm = TissueProperties(TEST_SETTINGS) values600nm[Tags.DATA_FIELD_ABSORPTION_PER_CM] = None values600nm[Tags.DATA_FIELD_SCATTERING_PER_CM] = None values600nm[Tags.DATA_FIELD_ANISOTROPY] = None values600nm[Tags.DATA_FIELD_GRUNEISEN_PARAMETER] = calculate_gruneisen_parameter_from_temperature(37.0) values600nm[Tags.DATA_FIELD_SEGMENTATION] = SegmentationClasses.LYMPH_NODE values600nm[Tags.DATA_FIELD_OXYGENATION] = 0.73 + values600nm[Tags.DATA_FIELD_BLOOD_VOLUME_FRACTION] = 0.14 values600nm[Tags.DATA_FIELD_DENSITY] = 1035 values600nm[Tags.DATA_FIELD_SPEED_OF_SOUND] = 1586 values600nm[Tags.DATA_FIELD_ALPHA_COEFF] = 2.50 - values650nm = TissueProperties() + values650nm = TissueProperties(TEST_SETTINGS) values650nm[Tags.DATA_FIELD_ABSORPTION_PER_CM] = None values650nm[Tags.DATA_FIELD_SCATTERING_PER_CM] = None values650nm[Tags.DATA_FIELD_ANISOTROPY] = None values650nm[Tags.DATA_FIELD_GRUNEISEN_PARAMETER] = calculate_gruneisen_parameter_from_temperature(37.0) values650nm[Tags.DATA_FIELD_SEGMENTATION] = SegmentationClasses.LYMPH_NODE values650nm[Tags.DATA_FIELD_OXYGENATION] = 0.73 + values650nm[Tags.DATA_FIELD_BLOOD_VOLUME_FRACTION] = 0.14 values650nm[Tags.DATA_FIELD_DENSITY] = 1035 values650nm[Tags.DATA_FIELD_SPEED_OF_SOUND] = 1586 values650nm[Tags.DATA_FIELD_ALPHA_COEFF] = 2.50 - values700nm = TissueProperties() + values700nm = TissueProperties(TEST_SETTINGS) values700nm[Tags.DATA_FIELD_ABSORPTION_PER_CM] = None values700nm[Tags.DATA_FIELD_SCATTERING_PER_CM] = None values700nm[Tags.DATA_FIELD_ANISOTROPY] = None values700nm[Tags.DATA_FIELD_GRUNEISEN_PARAMETER] = calculate_gruneisen_parameter_from_temperature(37.0) values700nm[Tags.DATA_FIELD_SEGMENTATION] = SegmentationClasses.LYMPH_NODE values700nm[Tags.DATA_FIELD_OXYGENATION] = 0.73 + values700nm[Tags.DATA_FIELD_BLOOD_VOLUME_FRACTION] = 0.14 values700nm[Tags.DATA_FIELD_DENSITY] = 1035 values700nm[Tags.DATA_FIELD_SPEED_OF_SOUND] = 1586 values700nm[Tags.DATA_FIELD_ALPHA_COEFF] = 2.50 - values750nm = TissueProperties() + values750nm = TissueProperties(TEST_SETTINGS) values750nm[Tags.DATA_FIELD_ABSORPTION_PER_CM] = None values750nm[Tags.DATA_FIELD_SCATTERING_PER_CM] = None values750nm[Tags.DATA_FIELD_ANISOTROPY] = None values750nm[Tags.DATA_FIELD_GRUNEISEN_PARAMETER] = calculate_gruneisen_parameter_from_temperature(37.0) values750nm[Tags.DATA_FIELD_SEGMENTATION] = SegmentationClasses.LYMPH_NODE values750nm[Tags.DATA_FIELD_OXYGENATION] = 0.73 + values750nm[Tags.DATA_FIELD_BLOOD_VOLUME_FRACTION] = 0.14 values750nm[Tags.DATA_FIELD_DENSITY] = 1035 values750nm[Tags.DATA_FIELD_SPEED_OF_SOUND] = 1586 values750nm[Tags.DATA_FIELD_ALPHA_COEFF] = 2.50 - values800nm = TissueProperties() + values800nm = TissueProperties(TEST_SETTINGS) values800nm[Tags.DATA_FIELD_ABSORPTION_PER_CM] = None values800nm[Tags.DATA_FIELD_SCATTERING_PER_CM] = None values800nm[Tags.DATA_FIELD_ANISOTROPY] = None values800nm[Tags.DATA_FIELD_GRUNEISEN_PARAMETER] = calculate_gruneisen_parameter_from_temperature(37.0) values800nm[Tags.DATA_FIELD_SEGMENTATION] = SegmentationClasses.LYMPH_NODE values800nm[Tags.DATA_FIELD_OXYGENATION] = 0.73 + values800nm[Tags.DATA_FIELD_BLOOD_VOLUME_FRACTION] = 0.14 values800nm[Tags.DATA_FIELD_DENSITY] = 1035 values800nm[Tags.DATA_FIELD_SPEED_OF_SOUND] = 1586 values800nm[Tags.DATA_FIELD_ALPHA_COEFF] = 2.50 - values850nm = TissueProperties() + values850nm = TissueProperties(TEST_SETTINGS) values850nm[Tags.DATA_FIELD_ABSORPTION_PER_CM] = None values850nm[Tags.DATA_FIELD_SCATTERING_PER_CM] = None values850nm[Tags.DATA_FIELD_ANISOTROPY] = None values850nm[Tags.DATA_FIELD_GRUNEISEN_PARAMETER] = calculate_gruneisen_parameter_from_temperature(37.0) values850nm[Tags.DATA_FIELD_SEGMENTATION] = SegmentationClasses.LYMPH_NODE values850nm[Tags.DATA_FIELD_OXYGENATION] = 0.73 + values850nm[Tags.DATA_FIELD_BLOOD_VOLUME_FRACTION] = 0.14 values850nm[Tags.DATA_FIELD_DENSITY] = 1035 values850nm[Tags.DATA_FIELD_SPEED_OF_SOUND] = 1586 values850nm[Tags.DATA_FIELD_ALPHA_COEFF] = 2.50 - values900nm = TissueProperties() + values900nm = TissueProperties(TEST_SETTINGS) values900nm[Tags.DATA_FIELD_ABSORPTION_PER_CM] = None values900nm[Tags.DATA_FIELD_SCATTERING_PER_CM] = None values900nm[Tags.DATA_FIELD_ANISOTROPY] = None values900nm[Tags.DATA_FIELD_GRUNEISEN_PARAMETER] = calculate_gruneisen_parameter_from_temperature(37.0) values900nm[Tags.DATA_FIELD_SEGMENTATION] = SegmentationClasses.LYMPH_NODE values900nm[Tags.DATA_FIELD_OXYGENATION] = 0.73 + values900nm[Tags.DATA_FIELD_BLOOD_VOLUME_FRACTION] = 0.14 values900nm[Tags.DATA_FIELD_DENSITY] = 1035 values900nm[Tags.DATA_FIELD_SPEED_OF_SOUND] = 1586 values900nm[Tags.DATA_FIELD_ALPHA_COEFF] = 2.50 - values950nm = TissueProperties() + values950nm = TissueProperties(TEST_SETTINGS) values950nm[Tags.DATA_FIELD_ABSORPTION_PER_CM] = None values950nm[Tags.DATA_FIELD_SCATTERING_PER_CM] = None values950nm[Tags.DATA_FIELD_ANISOTROPY] = None values950nm[Tags.DATA_FIELD_GRUNEISEN_PARAMETER] = calculate_gruneisen_parameter_from_temperature(37.0) values950nm[Tags.DATA_FIELD_SEGMENTATION] = SegmentationClasses.LYMPH_NODE values950nm[Tags.DATA_FIELD_OXYGENATION] = 0.73 + values950nm[Tags.DATA_FIELD_BLOOD_VOLUME_FRACTION] = 0.14 values950nm[Tags.DATA_FIELD_DENSITY] = 1035 values950nm[Tags.DATA_FIELD_SPEED_OF_SOUND] = 1586 values950nm[Tags.DATA_FIELD_ALPHA_COEFF] = 2.50 From b0fce1c7eb7271ca600ed77073f9e0dc215a7a6f Mon Sep 17 00:00:00 2001 From: v496a Date: Tue, 5 Nov 2024 14:40:24 +0100 Subject: [PATCH 14/34] Some test fixes --- .../optical_module/mcx_reflectance_adapter.py | 9 +++-- simpa/utils/libraries/molecule_library.py | 2 ++ simpa/utils/libraries/spectrum_library.py | 2 +- .../tissue_library/test_tissue_library.py | 33 +++++++++++++------ .../test_utils/tissue_composition_tests.py | 31 ++++++++++++++--- 5 files changed, 60 insertions(+), 17 deletions(-) diff --git a/simpa/core/simulation_modules/optical_module/mcx_reflectance_adapter.py b/simpa/core/simulation_modules/optical_module/mcx_reflectance_adapter.py index d32d1a9b..005b1171 100644 --- a/simpa/core/simulation_modules/optical_module/mcx_reflectance_adapter.py +++ b/simpa/core/simulation_modules/optical_module/mcx_reflectance_adapter.py @@ -7,8 +7,9 @@ import struct import jdata import os -from typing import List, Tuple, Dict, Union +from typing import Tuple, Dict, Union +from simpa.core.simulation_modules.optical_module.volume_boundary_condition import MCXVolumeBoundaryCondition from simpa.utils import Tags, Settings from simpa.core.simulation_modules.optical_module.mcx_adapter import MCXAdapter from simpa.core.device_digital_twins import IlluminationGeometryBase, PhotoacousticDevice @@ -40,7 +41,11 @@ def __init__(self, global_settings: Settings): super(MCXReflectanceAdapter, self).__init__(global_settings=global_settings) self.mcx_photon_data_file = None self.padded = None - self.volume_boundary_condition_str = global_settings[Tags.VOLUME_BOUNDARY_BONDITION] + if Tags.VOLUME_BOUNDARY_BONDITION in global_settings: + self.volume_boundary_condition_str = global_settings[Tags.VOLUME_BOUNDARY_BONDITION] + else: + self.volume_boundary_condition_str = MCXVolumeBoundaryCondition.DEFAULT.value + self.mcx_output_suffixes = {'mcx_volumetric_data_file': '.jnii', 'mcx_photon_data_file': '_detp.jdat'} diff --git a/simpa/utils/libraries/molecule_library.py b/simpa/utils/libraries/molecule_library.py index eaad1359..cd1bc0d2 100644 --- a/simpa/utils/libraries/molecule_library.py +++ b/simpa/utils/libraries/molecule_library.py @@ -519,6 +519,8 @@ def dermal_scatterer(volume_fraction: (float, torch.Tensor) = 1.0) -> Molecule: :param volume_fraction: The volume fraction of the molecule, defaults to 1.0 :return: A Molecule object representing a dermal scatterer """ + + # Todo: Add refractive index. return Molecule(name="dermal_scatterer", absorption_spectrum=AbsorptionSpectrumLibrary().get_spectrum_by_name("Skin_Baseline"), volume_fraction=volume_fraction, diff --git a/simpa/utils/libraries/spectrum_library.py b/simpa/utils/libraries/spectrum_library.py index 9689fbee..dcf5eeb2 100644 --- a/simpa/utils/libraries/spectrum_library.py +++ b/simpa/utils/libraries/spectrum_library.py @@ -73,7 +73,7 @@ def get_value_for_wavelength(self, wavelength: int) -> float: if wavelength < self.min_wavelength or wavelength > self.max_wavelength: raise ValueError(f"The given wavelength ({wavelength}) is not within the range of the spectrum " f"({self.min_wavelength} - {self.max_wavelength})") - return self.values_interp[wavelength-self.min_wavelength] + return self.values_interp[wavelength - self.min_wavelength] def __eq__(self, other): """ diff --git a/simpa_tests/automatic_tests/tissue_library/test_tissue_library.py b/simpa_tests/automatic_tests/tissue_library/test_tissue_library.py index b36a734b..f3b698e5 100644 --- a/simpa_tests/automatic_tests/tissue_library/test_tissue_library.py +++ b/simpa_tests/automatic_tests/tissue_library/test_tissue_library.py @@ -13,20 +13,23 @@ def test_if_optical_parameter_spectra_are_provided_correct_tissue_definition_is_ mua_sample = np.linspace(1e-6, 1e-5, wavelengths_sample.shape[0]) mus_sample = np.linspace(1e-5, 5e-6, wavelengths_sample.shape[0]) g_sample = np.linspace(0.8, 0.9, wavelengths_sample.shape[0]) + n_sample = np.linspace(1.4, 0.8, wavelengths_sample.shape[0]) mua_spectrum = Spectrum("Mua", wavelengths_sample, mua_sample) mus_spectrum = Spectrum("Mus", wavelengths_sample, mus_sample) g_spectrum = Spectrum("g", wavelengths_sample, g_sample) + n_spectrum = Spectrum("n", wavelengths_sample, n_sample) - actual_tissue = TISSUE_LIBRARY.generic_tissue(mua_spectrum, mus_spectrum, g_spectrum) + actual_tissue = TISSUE_LIBRARY.generic_tissue(mua_spectrum, mus_spectrum, g_spectrum, n_spectrum) assert actual_tissue.segmentation_type == SegmentationClasses.GENERIC assert len(actual_tissue) == 1 molecule: Molecule = actual_tissue[0] assert molecule.volume_fraction == 1.0 - assert molecule.spectrum == mua_spectrum + assert molecule.absorption_spectrum == mua_spectrum assert molecule.scattering_spectrum == mus_spectrum assert molecule.anisotropy_spectrum == g_spectrum + assert molecule.refractive_index == n_spectrum def test_if_generic_tissue_is_called_with_invalid_arguments_error_is_raised(): @@ -34,47 +37,57 @@ def test_if_generic_tissue_is_called_with_invalid_arguments_error_is_raised(): mua_sample = np.linspace(1e-6, 1e-5, wavelengths_sample.shape[0]) mus_sample = np.linspace(1e-5, 5e-6, wavelengths_sample.shape[0]) g_sample = np.linspace(0.8, 0.9, wavelengths_sample.shape[0]) + n_sample = np.linspace(1.4, 0.8, wavelengths_sample.shape[0]) mua_spectrum = Spectrum("Mua", wavelengths_sample, mua_sample) mus_spectrum = Spectrum("Mus", wavelengths_sample, mus_sample) g_spectrum = Spectrum("g", wavelengths_sample, g_sample) + n_spectrum = Spectrum("n", wavelengths_sample, n_sample) + with pytest.raises(AssertionError): + TISSUE_LIBRARY.generic_tissue(None, mus_spectrum, g_spectrum, n_spectrum) with pytest.raises(AssertionError): - TISSUE_LIBRARY.generic_tissue(None, mus_spectrum, g_spectrum) + TISSUE_LIBRARY.generic_tissue(mua_spectrum, None, g_spectrum, n_spectrum) with pytest.raises(AssertionError): - TISSUE_LIBRARY.generic_tissue(mua_spectrum, None, g_spectrum) + TISSUE_LIBRARY.generic_tissue(mua_spectrum, mus_spectrum, None, n_spectrum) with pytest.raises(AssertionError): - TISSUE_LIBRARY.generic_tissue(mua_spectrum, mus_spectrum, None) + TISSUE_LIBRARY.generic_tissue(mua_spectrum, mus_spectrum, g_spectrum, None) def test_if_optical_parameter_spectra_are_provided_correct_tissue_definition_is_returned_from_constant(): mua_sample = 1e-5 mus_sample = 3e-6 g_sample = 0.85 + n_sample = 1.13 - actual_tissue = TISSUE_LIBRARY.constant(mua_sample, mus_sample, g_sample) + actual_tissue = TISSUE_LIBRARY.constant(mua_sample, mus_sample, g_sample, n_sample) assert actual_tissue.segmentation_type == SegmentationClasses.GENERIC assert len(actual_tissue) == 1 molecule: Molecule = actual_tissue[0] assert molecule.volume_fraction == 1.0 - assert (molecule.spectrum.values == mua_sample).all() + assert (molecule.absorption_spectrum.values == mua_sample).all() assert (molecule.scattering_spectrum.values == mus_sample).all() assert (molecule.anisotropy_spectrum.values == g_sample).all() + assert (molecule.refractive_index.values == n_sample).all() def test_if_constant_is_called_with_invalid_arguments_error_is_raised(): mua_sample = 1e-5 mus_sample = 3e-6 g_sample = 0.85 + n_sample = 1.13 + + with pytest.raises(TypeError): + TISSUE_LIBRARY.constant(None, mus_sample, g_sample, n_sample) with pytest.raises(TypeError): - TISSUE_LIBRARY.constant(None, mus_sample, g_sample) + TISSUE_LIBRARY.constant(mua_sample, None, g_sample, n_sample) with pytest.raises(TypeError): - TISSUE_LIBRARY.constant(mua_sample, None, g_sample) + TISSUE_LIBRARY.constant(mua_sample, mus_sample, None, n_sample) with pytest.raises(TypeError): - TISSUE_LIBRARY.constant(mua_sample, mus_sample, None) + TISSUE_LIBRARY.constant(mua_sample, mus_sample, g_sample, None) diff --git a/simpa_tests/test_utils/tissue_composition_tests.py b/simpa_tests/test_utils/tissue_composition_tests.py index 7adc8ca0..9e8d080f 100644 --- a/simpa_tests/test_utils/tissue_composition_tests.py +++ b/simpa_tests/test_utils/tissue_composition_tests.py @@ -495,6 +495,7 @@ def get_fully_oxygenated_blood_reference_dictionary(only_use_NIR_values=False): values450nm[Tags.DATA_FIELD_DENSITY] = 1049.75 values450nm[Tags.DATA_FIELD_SPEED_OF_SOUND] = 1578.2 values450nm[Tags.DATA_FIELD_ALPHA_COEFF] = 0.2 + values450nm[Tags.DATA_FIELD_REFRACTIVE_INDEX] = 1.373 values500nm = TissueProperties(TEST_SETTINGS) values500nm[Tags.DATA_FIELD_ABSORPTION_PER_CM] = 112 @@ -507,6 +508,7 @@ def get_fully_oxygenated_blood_reference_dictionary(only_use_NIR_values=False): values500nm[Tags.DATA_FIELD_DENSITY] = 1049.75 values500nm[Tags.DATA_FIELD_SPEED_OF_SOUND] = 1578.2 values500nm[Tags.DATA_FIELD_ALPHA_COEFF] = 0.2 + values500nm[Tags.DATA_FIELD_REFRACTIVE_INDEX] = 1.371 values550nm = TissueProperties(TEST_SETTINGS) values550nm[Tags.DATA_FIELD_ABSORPTION_PER_CM] = 230 @@ -519,6 +521,7 @@ def get_fully_oxygenated_blood_reference_dictionary(only_use_NIR_values=False): values550nm[Tags.DATA_FIELD_DENSITY] = 1049.75 values550nm[Tags.DATA_FIELD_SPEED_OF_SOUND] = 1578.2 values550nm[Tags.DATA_FIELD_ALPHA_COEFF] = 0.2 + values550nm[Tags.DATA_FIELD_REFRACTIVE_INDEX] = 1.370 values600nm = TissueProperties(TEST_SETTINGS) values600nm[Tags.DATA_FIELD_ABSORPTION_PER_CM] = 17 @@ -531,6 +534,7 @@ def get_fully_oxygenated_blood_reference_dictionary(only_use_NIR_values=False): values600nm[Tags.DATA_FIELD_DENSITY] = 1049.75 values600nm[Tags.DATA_FIELD_SPEED_OF_SOUND] = 1578.2 values600nm[Tags.DATA_FIELD_ALPHA_COEFF] = 0.2 + values600nm[Tags.DATA_FIELD_REFRACTIVE_INDEX] = 1.369 values650nm = TissueProperties(TEST_SETTINGS) values650nm[Tags.DATA_FIELD_ABSORPTION_PER_CM] = 2 @@ -543,6 +547,7 @@ def get_fully_oxygenated_blood_reference_dictionary(only_use_NIR_values=False): values650nm[Tags.DATA_FIELD_DENSITY] = 1049.75 values650nm[Tags.DATA_FIELD_SPEED_OF_SOUND] = 1578.2 values650nm[Tags.DATA_FIELD_ALPHA_COEFF] = 0.2 + values650nm[Tags.DATA_FIELD_REFRACTIVE_INDEX] = 1.368 values700nm = TissueProperties(TEST_SETTINGS) values700nm[Tags.DATA_FIELD_ABSORPTION_PER_CM] = 1.6 @@ -555,6 +560,7 @@ def get_fully_oxygenated_blood_reference_dictionary(only_use_NIR_values=False): values700nm[Tags.DATA_FIELD_DENSITY] = 1049.75 values700nm[Tags.DATA_FIELD_SPEED_OF_SOUND] = 1578.2 values700nm[Tags.DATA_FIELD_ALPHA_COEFF] = 0.2 + values700nm[Tags.DATA_FIELD_REFRACTIVE_INDEX] = 1.367 values750nm = TissueProperties(TEST_SETTINGS) values750nm[Tags.DATA_FIELD_ABSORPTION_PER_CM] = 2.8 @@ -567,6 +573,7 @@ def get_fully_oxygenated_blood_reference_dictionary(only_use_NIR_values=False): values750nm[Tags.DATA_FIELD_DENSITY] = 1049.75 values750nm[Tags.DATA_FIELD_SPEED_OF_SOUND] = 1578.2 values750nm[Tags.DATA_FIELD_ALPHA_COEFF] = 0.2 + values750nm[Tags.DATA_FIELD_REFRACTIVE_INDEX] = 1.367 values800nm = TissueProperties(TEST_SETTINGS) values800nm[Tags.DATA_FIELD_ABSORPTION_PER_CM] = 4.4 @@ -579,6 +586,7 @@ def get_fully_oxygenated_blood_reference_dictionary(only_use_NIR_values=False): values800nm[Tags.DATA_FIELD_DENSITY] = 1049.75 values800nm[Tags.DATA_FIELD_SPEED_OF_SOUND] = 1578.2 values800nm[Tags.DATA_FIELD_ALPHA_COEFF] = 0.2 + values800nm[Tags.DATA_FIELD_REFRACTIVE_INDEX] = 1.366 values850nm = TissueProperties(TEST_SETTINGS) values850nm[Tags.DATA_FIELD_ABSORPTION_PER_CM] = 5.7 @@ -591,6 +599,7 @@ def get_fully_oxygenated_blood_reference_dictionary(only_use_NIR_values=False): values850nm[Tags.DATA_FIELD_DENSITY] = 1049.75 values850nm[Tags.DATA_FIELD_SPEED_OF_SOUND] = 1578.2 values850nm[Tags.DATA_FIELD_ALPHA_COEFF] = 0.2 + values850nm[Tags.DATA_FIELD_REFRACTIVE_INDEX] = 1.365 values900nm = TissueProperties(TEST_SETTINGS) values900nm[Tags.DATA_FIELD_ABSORPTION_PER_CM] = 6.4 @@ -603,6 +612,7 @@ def get_fully_oxygenated_blood_reference_dictionary(only_use_NIR_values=False): values900nm[Tags.DATA_FIELD_DENSITY] = 1049.75 values900nm[Tags.DATA_FIELD_SPEED_OF_SOUND] = 1578.2 values900nm[Tags.DATA_FIELD_ALPHA_COEFF] = 0.2 + values900nm[Tags.DATA_FIELD_REFRACTIVE_INDEX] = None values950nm = TissueProperties(TEST_SETTINGS) values950nm[Tags.DATA_FIELD_ABSORPTION_PER_CM] = 6.4 @@ -615,6 +625,7 @@ def get_fully_oxygenated_blood_reference_dictionary(only_use_NIR_values=False): values950nm[Tags.DATA_FIELD_DENSITY] = 1049.75 values950nm[Tags.DATA_FIELD_SPEED_OF_SOUND] = 1578.2 values950nm[Tags.DATA_FIELD_ALPHA_COEFF] = 0.2 + values950nm[Tags.DATA_FIELD_REFRACTIVE_INDEX] = None if not only_use_NIR_values: reference_dict[450] = values450nm @@ -626,8 +637,8 @@ def get_fully_oxygenated_blood_reference_dictionary(only_use_NIR_values=False): reference_dict[750] = values750nm reference_dict[800] = values800nm reference_dict[850] = values850nm - reference_dict[900] = values900nm - reference_dict[950] = values950nm + # reference_dict[900] = values900nm # FIXME: Find refractive index values for 900 and 950 nm. + # reference_dict[950] = values950nm return reference_dict @@ -664,6 +675,8 @@ def get_fully_deoxygenated_blood_reference_dictionary(only_use_NIR_values=False) values450nm[Tags.DATA_FIELD_DENSITY] = 1049.75 values450nm[Tags.DATA_FIELD_SPEED_OF_SOUND] = 1578.2 values450nm[Tags.DATA_FIELD_ALPHA_COEFF] = 0.2 + values450nm[Tags.DATA_FIELD_REFRACTIVE_INDEX] = 0.2 + values450nm[Tags.DATA_FIELD_REFRACTIVE_INDEX] = 1.374 values500nm = TissueProperties(TEST_SETTINGS) values500nm[Tags.DATA_FIELD_ABSORPTION_PER_CM] = 112 @@ -676,6 +689,7 @@ def get_fully_deoxygenated_blood_reference_dictionary(only_use_NIR_values=False) values500nm[Tags.DATA_FIELD_DENSITY] = 1049.75 values500nm[Tags.DATA_FIELD_SPEED_OF_SOUND] = 1578.2 values500nm[Tags.DATA_FIELD_ALPHA_COEFF] = 0.2 + values500nm[Tags.DATA_FIELD_REFRACTIVE_INDEX] = 1.368 values550nm = TissueProperties(TEST_SETTINGS) values550nm[Tags.DATA_FIELD_ABSORPTION_PER_CM] = 286 @@ -688,6 +702,7 @@ def get_fully_deoxygenated_blood_reference_dictionary(only_use_NIR_values=False) values550nm[Tags.DATA_FIELD_DENSITY] = 1049.75 values550nm[Tags.DATA_FIELD_SPEED_OF_SOUND] = 1578.2 values550nm[Tags.DATA_FIELD_ALPHA_COEFF] = 0.2 + values550nm[Tags.DATA_FIELD_REFRACTIVE_INDEX] = 1.366 values600nm = TissueProperties(TEST_SETTINGS) values600nm[Tags.DATA_FIELD_ABSORPTION_PER_CM] = 79 @@ -700,6 +715,7 @@ def get_fully_deoxygenated_blood_reference_dictionary(only_use_NIR_values=False) values600nm[Tags.DATA_FIELD_DENSITY] = 1049.75 values600nm[Tags.DATA_FIELD_SPEED_OF_SOUND] = 1578.2 values600nm[Tags.DATA_FIELD_ALPHA_COEFF] = 0.2 + values600nm[Tags.DATA_FIELD_REFRACTIVE_INDEX] = 1.364 values650nm = TissueProperties(TEST_SETTINGS) values650nm[Tags.DATA_FIELD_ABSORPTION_PER_CM] = 20.1 @@ -712,6 +728,7 @@ def get_fully_deoxygenated_blood_reference_dictionary(only_use_NIR_values=False) values650nm[Tags.DATA_FIELD_DENSITY] = 1049.75 values650nm[Tags.DATA_FIELD_SPEED_OF_SOUND] = 1578.2 values650nm[Tags.DATA_FIELD_ALPHA_COEFF] = 0.2 + values650nm[Tags.DATA_FIELD_REFRACTIVE_INDEX] = 1.362 values700nm = TissueProperties(TEST_SETTINGS) values700nm[Tags.DATA_FIELD_ABSORPTION_PER_CM] = 9.6 @@ -724,6 +741,7 @@ def get_fully_deoxygenated_blood_reference_dictionary(only_use_NIR_values=False) values700nm[Tags.DATA_FIELD_DENSITY] = 1049.75 values700nm[Tags.DATA_FIELD_SPEED_OF_SOUND] = 1578.2 values700nm[Tags.DATA_FIELD_ALPHA_COEFF] = 0.2 + values700nm[Tags.DATA_FIELD_REFRACTIVE_INDEX] = 1.361 values750nm = TissueProperties(TEST_SETTINGS) values750nm[Tags.DATA_FIELD_ABSORPTION_PER_CM] = 7.5 @@ -736,6 +754,7 @@ def get_fully_deoxygenated_blood_reference_dictionary(only_use_NIR_values=False) values750nm[Tags.DATA_FIELD_DENSITY] = 1049.75 values750nm[Tags.DATA_FIELD_SPEED_OF_SOUND] = 1578.2 values750nm[Tags.DATA_FIELD_ALPHA_COEFF] = 0.2 + values750nm[Tags.DATA_FIELD_REFRACTIVE_INDEX] = 1.360 values800nm = TissueProperties(TEST_SETTINGS) values800nm[Tags.DATA_FIELD_ABSORPTION_PER_CM] = 4.1 @@ -748,6 +767,7 @@ def get_fully_deoxygenated_blood_reference_dictionary(only_use_NIR_values=False) values800nm[Tags.DATA_FIELD_DENSITY] = 1049.75 values800nm[Tags.DATA_FIELD_SPEED_OF_SOUND] = 1578.2 values800nm[Tags.DATA_FIELD_ALPHA_COEFF] = 0.2 + values800nm[Tags.DATA_FIELD_REFRACTIVE_INDEX] = 1.358 values850nm = TissueProperties(TEST_SETTINGS) values850nm[Tags.DATA_FIELD_ABSORPTION_PER_CM] = 3.7 @@ -760,6 +780,7 @@ def get_fully_deoxygenated_blood_reference_dictionary(only_use_NIR_values=False) values850nm[Tags.DATA_FIELD_DENSITY] = 1049.75 values850nm[Tags.DATA_FIELD_SPEED_OF_SOUND] = 1578.2 values850nm[Tags.DATA_FIELD_ALPHA_COEFF] = 0.2 + values850nm[Tags.DATA_FIELD_REFRACTIVE_INDEX] = 1.357 values900nm = TissueProperties(TEST_SETTINGS) values900nm[Tags.DATA_FIELD_ABSORPTION_PER_CM] = 4.1 @@ -772,6 +793,7 @@ def get_fully_deoxygenated_blood_reference_dictionary(only_use_NIR_values=False) values900nm[Tags.DATA_FIELD_DENSITY] = 1049.75 values900nm[Tags.DATA_FIELD_SPEED_OF_SOUND] = 1578.2 values900nm[Tags.DATA_FIELD_ALPHA_COEFF] = 0.2 + values900nm[Tags.DATA_FIELD_REFRACTIVE_INDEX] = None values950nm = TissueProperties(TEST_SETTINGS) values950nm[Tags.DATA_FIELD_ABSORPTION_PER_CM] = 3.2 @@ -784,6 +806,7 @@ def get_fully_deoxygenated_blood_reference_dictionary(only_use_NIR_values=False) values950nm[Tags.DATA_FIELD_DENSITY] = 1049.75 values950nm[Tags.DATA_FIELD_SPEED_OF_SOUND] = 1578.2 values950nm[Tags.DATA_FIELD_ALPHA_COEFF] = 0.2 + values950nm[Tags.DATA_FIELD_REFRACTIVE_INDEX] = None if not only_use_NIR_values: reference_dict[450] = values450nm @@ -795,8 +818,8 @@ def get_fully_deoxygenated_blood_reference_dictionary(only_use_NIR_values=False) reference_dict[750] = values750nm reference_dict[800] = values800nm reference_dict[850] = values850nm - reference_dict[900] = values900nm - reference_dict[950] = values950nm + # reference_dict[900] = values900nm # FIXME: Find refractive index values for 900 and 950 nm. + # reference_dict[950] = values950nm return reference_dict From 03b3e2b5997d139c1b6c686a2c6d2d65b6f5b86d Mon Sep 17 00:00:00 2001 From: v496a Date: Thu, 7 Nov 2024 10:22:50 +0100 Subject: [PATCH 15/34] + -b 1 --- simpa/core/simulation_modules/optical_module/mcx_adapter.py | 1 - .../optical_module/mcx_reflectance_adapter.py | 2 ++ 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/simpa/core/simulation_modules/optical_module/mcx_adapter.py b/simpa/core/simulation_modules/optical_module/mcx_adapter.py index 9314aed5..65b969c1 100644 --- a/simpa/core/simulation_modules/optical_module/mcx_adapter.py +++ b/simpa/core/simulation_modules/optical_module/mcx_adapter.py @@ -65,7 +65,6 @@ def forward_model(self, settings_dict = self.get_mcx_settings(illumination_geometry=illumination_geometry) - print(settings_dict) self.generate_mcx_json_input(settings_dict=settings_dict) # run the simulation cmd = self.get_command() diff --git a/simpa/core/simulation_modules/optical_module/mcx_reflectance_adapter.py b/simpa/core/simulation_modules/optical_module/mcx_reflectance_adapter.py index 005b1171..6b2cc3dd 100644 --- a/simpa/core/simulation_modules/optical_module/mcx_reflectance_adapter.py +++ b/simpa/core/simulation_modules/optical_module/mcx_reflectance_adapter.py @@ -105,6 +105,8 @@ def get_command(self) -> typing.List: cmd.append(self.mcx_json_config_file) cmd.append("-O") cmd.append("F") + cmd.append("-b") + cmd.append("1") # use 'C' order array format for binary input file cmd.append("-a") cmd.append("1") From 3e0c1fdb1e2a1a49d87a6e164617f173b24797e3 Mon Sep 17 00:00:00 2001 From: v496a Date: Wed, 20 Nov 2024 10:09:59 +0100 Subject: [PATCH 16/34] Added RefractiveIndexSpectrumLibrary to init for easier imports --- simpa/utils/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/simpa/utils/__init__.py b/simpa/utils/__init__.py index c8484709..7359d37b 100644 --- a/simpa/utils/__init__.py +++ b/simpa/utils/__init__.py @@ -18,6 +18,7 @@ from .libraries.spectrum_library import Spectrum from .libraries.spectrum_library import view_saved_spectra from .libraries.spectrum_library import AnisotropySpectrumLibrary +from .libraries.spectrum_library import RefractiveIndexSpectrumLibrary from .libraries.spectrum_library import ScatteringSpectrumLibrary from .libraries.spectrum_library import get_simpa_internal_absorption_spectra_by_names From 181c5b3416cfe8f7192ad81f48806c8e2a3d2149 Mon Sep 17 00:00:00 2001 From: v496a Date: Wed, 26 Mar 2025 10:15:01 +0100 Subject: [PATCH 17/34] + First reduced scattering formula --- simpa/utils/libraries/spectrum_library.py | 27 ++++++++++++++++++----- 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/simpa/utils/libraries/spectrum_library.py b/simpa/utils/libraries/spectrum_library.py index dcf5eeb2..3fa34d81 100644 --- a/simpa/utils/libraries/spectrum_library.py +++ b/simpa/utils/libraries/spectrum_library.py @@ -238,18 +238,33 @@ def scattering_from_rayleigh_and_mie_theory(name: str, mus_at_500_nm: float = 1. fraction_rayleigh_scattering: float = 0.0, mie_power_law_coefficient: float = 0.0) -> Spectrum: """ - Creates a scattering spectrum based on Rayleigh and Mie scattering theory. + Creates a reduced scattering spectrum based on Rayleigh and Mie scattering theory. :param name: The name of the spectrum. - :param mus_at_500_nm: Scattering coefficient at 500 nm. + :param mus_at_500_nm: Reduced scattering coefficient at 500 nm. :param fraction_rayleigh_scattering: Fraction of Rayleigh scattering. :param mie_power_law_coefficient: Power law coefficient for Mie scattering. :return: A Spectrum instance based on Rayleigh and Mie scattering theory. """ - wavelengths = np.arange(450, 1001, 1) - scattering = (mus_at_500_nm * (fraction_rayleigh_scattering * (wavelengths / 500) ** 1e-4 + - (1 - fraction_rayleigh_scattering) * (wavelengths / 500) ** -mie_power_law_coefficient)) - return Spectrum(name, wavelengths, scattering) + wavelengths = np.arange(400, 1301, 1) + reduced_scattering = (mus_at_500_nm * (fraction_rayleigh_scattering * (wavelengths / 500) ** 1e-4 + + (1 - fraction_rayleigh_scattering) * (wavelengths / 500) ** -mie_power_law_coefficient)) + return Spectrum(name, wavelengths, reduced_scattering) + + @staticmethod + def scattering_from_scattering_power(name: str, mus_at_500_nm: float, scattering_power: float) -> Spectrum: + """ + Creates a reduced scattering spectrum based on the first reduced scattering formula of the paper. + + :param name: The name of the spectrum. + :param mus_at_500_nm: Reduced scattering coefficient at 500 nm. Corresponds to a in the formula. + :param scattering_power: The scattering power. Corresponds to b in the formula. + + :return: The reduced scattering coefficients by wavelengths as a Spectrum instance. + """ + wavelengths = np.arange(400, 1301, 1) + reduced_scattering = mus_at_500_nm * np.power(wavelengths / 500, -scattering_power) + return Spectrum(name, wavelengths, reduced_scattering) class AbsorptionSpectrumLibrary(SpectraLibrary): From 5887f5e598e9d4a69c8219dc03d74b6cd4095c4e Mon Sep 17 00:00:00 2001 From: v496a Date: Wed, 26 Mar 2025 10:27:24 +0100 Subject: [PATCH 18/34] Converted all tensors used in spectrum class to numpy --- simpa/utils/libraries/spectrum_library.py | 26 +++++++++++------------ 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/simpa/utils/libraries/spectrum_library.py b/simpa/utils/libraries/spectrum_library.py index 3fa34d81..d9395d78 100644 --- a/simpa/utils/libraries/spectrum_library.py +++ b/simpa/utils/libraries/spectrum_library.py @@ -35,22 +35,22 @@ def __init__(self, spectrum_name: str, wavelengths: np.ndarray, values: np.ndarr :raises ValueError: If the shape of wavelengths does not match the shape of values. """ - if isinstance(values, np.ndarray): - values = torch.from_numpy(values) - wavelengths = torch.from_numpy(wavelengths) + assert isinstance(wavelengths, np.ndarray), type(wavelengths) + assert isinstance(values, np.ndarray), type(values) + self.spectrum_name = spectrum_name self.wavelengths = wavelengths - self.max_wavelength = int(torch.floor(torch.max(wavelengths))) - self.min_wavelength = int(torch.ceil(torch.min(wavelengths))) + self.max_wavelength = int(np.floor(np.max(wavelengths))) + self.min_wavelength = int(np.ceil(np.min(wavelengths))) self.values = values - if torch.Tensor.size(wavelengths) != torch.Tensor.size(values): + if wavelengths.shape != values.shape: raise ValueError("The shape of the wavelengths and the values did not match: " + - str(torch.Tensor.size(wavelengths)) + " vs " + str(torch.Tensor.size(values))) + str(wavelengths.shape) + " vs " + str(values.shape)) - new_wavelengths = torch.arange(self.min_wavelength, self.max_wavelength+1, 1) - new_absorptions_function = interpolate.interp1d(self.wavelengths, self.values) - self.values_interp = new_absorptions_function(new_wavelengths) + new_wavelengths = np.arange(self.min_wavelength, self.max_wavelength + 1, 1) + self.values_by_wavelength_function = interpolate.interp1d(self.wavelengths, self.values) + self.values_interp = self.values_by_wavelength_function(new_wavelengths) def get_value_over_wavelength(self) -> np.ndarray: """ @@ -60,7 +60,7 @@ def get_value_over_wavelength(self) -> np.ndarray: """ return np.asarray([self.wavelengths, self.values]) - def get_value_for_wavelength(self, wavelength: int) -> float: + def get_value_for_wavelength(self, wavelength: int | np.ndarray) -> float: """ Retrieves the interpolated value for a given wavelength within the spectrum range. @@ -70,10 +70,10 @@ def get_value_for_wavelength(self, wavelength: int) -> float: :return: the best matching linearly interpolated values for the given wavelength. :raises ValueError: if the given wavelength is not within the range of the spectrum. """ - if wavelength < self.min_wavelength or wavelength > self.max_wavelength: + if np.min(wavelength) < self.min_wavelength or np.max(wavelength) > self.max_wavelength: raise ValueError(f"The given wavelength ({wavelength}) is not within the range of the spectrum " f"({self.min_wavelength} - {self.max_wavelength})") - return self.values_interp[wavelength - self.min_wavelength] + return self.values_by_wavelength_function(wavelength) def __eq__(self, other): """ From 90c1310790ff03bde844d598d3ddcc2030207bc7 Mon Sep 17 00:00:00 2001 From: v496a Date: Wed, 26 Mar 2025 11:14:48 +0100 Subject: [PATCH 19/34] Fixes in spectrum library --- simpa/utils/libraries/spectrum_library.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/simpa/utils/libraries/spectrum_library.py b/simpa/utils/libraries/spectrum_library.py index d9395d78..9184ef8b 100644 --- a/simpa/utils/libraries/spectrum_library.py +++ b/simpa/utils/libraries/spectrum_library.py @@ -49,8 +49,8 @@ def __init__(self, spectrum_name: str, wavelengths: np.ndarray, values: np.ndarr str(wavelengths.shape) + " vs " + str(values.shape)) new_wavelengths = np.arange(self.min_wavelength, self.max_wavelength + 1, 1) - self.values_by_wavelength_function = interpolate.interp1d(self.wavelengths, self.values) - self.values_interp = self.values_by_wavelength_function(new_wavelengths) + values_by_wavelength_function = interpolate.interp1d(self.wavelengths, self.values) + self.values_interp = values_by_wavelength_function(new_wavelengths) def get_value_over_wavelength(self) -> np.ndarray: """ @@ -73,7 +73,7 @@ def get_value_for_wavelength(self, wavelength: int | np.ndarray) -> float: if np.min(wavelength) < self.min_wavelength or np.max(wavelength) > self.max_wavelength: raise ValueError(f"The given wavelength ({wavelength}) is not within the range of the spectrum " f"({self.min_wavelength} - {self.max_wavelength})") - return self.values_by_wavelength_function(wavelength) + return self.values_interp[wavelength - self.min_wavelength] def __eq__(self, other): """ From 2d3d7191e32f3d7fee7d129136660a00638f6fc5 Mon Sep 17 00:00:00 2001 From: v496a Date: Wed, 26 Mar 2025 11:34:42 +0100 Subject: [PATCH 20/34] Fixed second formula --- simpa/utils/libraries/spectrum_library.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/simpa/utils/libraries/spectrum_library.py b/simpa/utils/libraries/spectrum_library.py index 9184ef8b..97a300aa 100644 --- a/simpa/utils/libraries/spectrum_library.py +++ b/simpa/utils/libraries/spectrum_library.py @@ -247,7 +247,7 @@ def scattering_from_rayleigh_and_mie_theory(name: str, mus_at_500_nm: float = 1. :return: A Spectrum instance based on Rayleigh and Mie scattering theory. """ wavelengths = np.arange(400, 1301, 1) - reduced_scattering = (mus_at_500_nm * (fraction_rayleigh_scattering * (wavelengths / 500) ** 1e-4 + + reduced_scattering = (mus_at_500_nm * (fraction_rayleigh_scattering * (wavelengths / 500) ** -4 + (1 - fraction_rayleigh_scattering) * (wavelengths / 500) ** -mie_power_law_coefficient)) return Spectrum(name, wavelengths, reduced_scattering) From ff13b76bae80dddf60ed9bbc7cbc6a1f00477b62 Mon Sep 17 00:00:00 2001 From: v496a Date: Tue, 15 Apr 2025 16:58:20 +0200 Subject: [PATCH 21/34] + Photon position and direction saved on exit --- .../optical_module/mcx_reflectance_adapter.py | 16 +++++++--------- .../optical_module/volume_boundary_condition.py | 2 ++ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/simpa/core/simulation_modules/optical_module/mcx_reflectance_adapter.py b/simpa/core/simulation_modules/optical_module/mcx_reflectance_adapter.py index 6b2cc3dd..f4120f00 100644 --- a/simpa/core/simulation_modules/optical_module/mcx_reflectance_adapter.py +++ b/simpa/core/simulation_modules/optical_module/mcx_reflectance_adapter.py @@ -112,13 +112,16 @@ def get_command(self) -> typing.List: cmd.append("1") cmd.append("-F") cmd.append("jnii") - + cmd.append("--bc") + cmd.append(self.volume_boundary_condition_str) + cmd.append("-H") + cmd.append( + f"{int(self.component_settings[Tags.OPTICAL_MODEL_NUMBER_PHOTONS])}" + ) if ( Tags.COMPUTE_PHOTON_DIRECTION_AT_EXIT in self.component_settings and self.component_settings[Tags.COMPUTE_PHOTON_DIRECTION_AT_EXIT] ): - # FIXME - raise NotImplementedError("Does not work with volume boundary condition") cmd.append("--savedetflag") cmd.append("XV") @@ -126,13 +129,8 @@ def get_command(self) -> typing.List: Tags.COMPUTE_DIFFUSE_REFLECTANCE in self.component_settings and self.component_settings[Tags.COMPUTE_DIFFUSE_REFLECTANCE] ): - cmd.append("-H") - cmd.append( - f"{int(self.component_settings[Tags.OPTICAL_MODEL_NUMBER_PHOTONS])}" - ) - cmd.append("--bc") # save photon exit position and direction - cmd.append(self.volume_boundary_condition_str) cmd.append("--saveref") + cmd += self.get_additional_flags() return cmd diff --git a/simpa/core/simulation_modules/optical_module/volume_boundary_condition.py b/simpa/core/simulation_modules/optical_module/volume_boundary_condition.py index 3955fd1e..c5b2d715 100644 --- a/simpa/core/simulation_modules/optical_module/volume_boundary_condition.py +++ b/simpa/core/simulation_modules/optical_module/volume_boundary_condition.py @@ -19,6 +19,8 @@ class MCXVolumeBoundaryCondition(Enum): """The default behavior.""" MIRROR_REFLECTION = "mm_mm_000000" """The photons are totally reflected as if the volume faces are mirrors.""" + MIRROR_REFLECTION_WITH_DETECTION = "mm_mm_000001" + """The photons are totally reflected as if the volume faces are mirrors. The z-axis plane will also serve as a detector.""" CYCLIC = "cc_cc_000000" """The photons reenter from the opposite volume face.""" ABSORB = "aa_aa_000000" From 7484a7811c171e6f6d8d495e36a7b1cb1886c89f Mon Sep 17 00:00:00 2001 From: v496a Date: Fri, 25 Apr 2025 10:48:40 +0200 Subject: [PATCH 22/34] Adding russian roulette cmd arg for faster MCX simulations --- .../optical_module/mcx_reflectance_adapter.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/simpa/core/simulation_modules/optical_module/mcx_reflectance_adapter.py b/simpa/core/simulation_modules/optical_module/mcx_reflectance_adapter.py index f4120f00..9a490377 100644 --- a/simpa/core/simulation_modules/optical_module/mcx_reflectance_adapter.py +++ b/simpa/core/simulation_modules/optical_module/mcx_reflectance_adapter.py @@ -112,6 +112,8 @@ def get_command(self) -> typing.List: cmd.append("1") cmd.append("-F") cmd.append("jnii") + cmd.append("-e") + cmd.append(str(1e-3)) cmd.append("--bc") cmd.append(self.volume_boundary_condition_str) cmd.append("-H") @@ -312,8 +314,8 @@ def run_forward_model(self, def _append_results(results, reflectance, reflectance_position, - photon_position, - photon_direction): + photon_position: list[np.ndarray], + photon_direction: list[np.ndarray]): if Tags.DATA_FIELD_DIFFUSE_REFLECTANCE in results: reflectance.append(results[Tags.DATA_FIELD_DIFFUSE_REFLECTANCE]) reflectance_position.append(results[Tags.DATA_FIELD_DIFFUSE_REFLECTANCE_POS]) From aa4790d32270fa6d657721d4539f141f3160791d Mon Sep 17 00:00:00 2001 From: v496a Date: Wed, 7 May 2025 09:15:47 +0200 Subject: [PATCH 23/34] Focal length in rectangle illumination --- .../illumination_geometries/rectangle_illumination.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/simpa/core/device_digital_twins/illumination_geometries/rectangle_illumination.py b/simpa/core/device_digital_twins/illumination_geometries/rectangle_illumination.py index 69bd3588..fa0c26d6 100644 --- a/simpa/core/device_digital_twins/illumination_geometries/rectangle_illumination.py +++ b/simpa/core/device_digital_twins/illumination_geometries/rectangle_illumination.py @@ -24,6 +24,7 @@ class RectangleIlluminationGeometry(IlluminationGeometryBase): def __init__(self, length_mm: int = 10, width_mm: int = 10, + focal_length_in_mm: float | str | None = None, device_position_mm: typing.Optional[np.ndarray] = None, source_direction_vector: typing.Optional[np.ndarray] = None, field_of_view_extent_mm: typing.Optional[np.ndarray] = None): @@ -51,6 +52,10 @@ def __init__(self, assert length_mm > 0 assert width_mm > 0 + if isinstance(focal_length_in_mm, str): + assert focal_length_in_mm == "_NaN_" or focal_length_in_mm == "-_Inf_", f"{focal_length_in_mm} is not supported yet" + + self.focal_length_in_mm = focal_length_in_mm self.length_mm = length_mm self.width_mm = width_mm @@ -74,6 +79,11 @@ def get_mcx_illuminator_definition(self, global_settings: Settings) -> dict: source_direction = list(self.normalized_source_direction_vector) + if self.focal_length_in_mm is not None: + param4 = self.focal_length_in_mm if isinstance( + self.focal_length_in_mm, str) else self.focal_length_in_mm / spacing + source_direction.append(param4) + source_param1 = [np.rint(self.width_mm / spacing) + 1, 0, 0] source_param2 = [0, np.rint(self.length_mm / spacing) + 1, 0] From d33105a54d6bcd7ea2dccf577e686d60334431d1 Mon Sep 17 00:00:00 2001 From: v496a Date: Wed, 7 May 2025 09:17:01 +0200 Subject: [PATCH 24/34] + Detector when capturing photons individually --- .../optical_module/mcx_reflectance_adapter.py | 29 +++++++++++++++++-- simpa/utils/tags.py | 12 +++++++- 2 files changed, 38 insertions(+), 3 deletions(-) diff --git a/simpa/core/simulation_modules/optical_module/mcx_reflectance_adapter.py b/simpa/core/simulation_modules/optical_module/mcx_reflectance_adapter.py index 9a490377..60da5aac 100644 --- a/simpa/core/simulation_modules/optical_module/mcx_reflectance_adapter.py +++ b/simpa/core/simulation_modules/optical_module/mcx_reflectance_adapter.py @@ -41,8 +41,8 @@ def __init__(self, global_settings: Settings): super(MCXReflectanceAdapter, self).__init__(global_settings=global_settings) self.mcx_photon_data_file = None self.padded = None - if Tags.VOLUME_BOUNDARY_BONDITION in global_settings: - self.volume_boundary_condition_str = global_settings[Tags.VOLUME_BOUNDARY_BONDITION] + if Tags.VOLUME_BOUNDARY_CONDITION in global_settings: + self.volume_boundary_condition_str = global_settings[Tags.VOLUME_BOUNDARY_CONDITION] else: self.volume_boundary_condition_str = MCXVolumeBoundaryCondition.DEFAULT.value @@ -94,6 +94,31 @@ def forward_model(self, self.remove_mcx_output() return results + def get_mcx_settings(self, + illumination_geometry: IlluminationGeometryBase, + **kwargs) -> Dict: + settings_dict = super().get_mcx_settings(illumination_geometry=illumination_geometry, **kwargs) + uses_photon_exit_data = Tags.COMPUTE_PHOTON_DIRECTION_AT_EXIT in self.component_settings and self.component_settings[ + Tags.COMPUTE_PHOTON_DIRECTION_AT_EXIT] + + if uses_photon_exit_data: + if Tags.MCX_DETECTOR in self.global_settings: + settings_dict["Optode"]["Detector"] = self.global_settings[Tags.MCX_DETECTOR] + else: + # For some reason, the simulation gets slower the larger the detector is + width = self.global_settings[Tags.DIM_VOLUME_X_MM] / self.global_settings[Tags.SPACING_MM] + height = self.global_settings[Tags.DIM_VOLUME_Y_MM] / self.global_settings[Tags.SPACING_MM] + position = [width / 2 + 1, height / 2 + 1, 0.0] + radius = np.sqrt(width ** 2 + height ** 2) / 2 + settings_dict["Optode"]["Detector"] = [ + { + "Pos": position, + "R": radius + } + ] + + return settings_dict + def get_command(self) -> typing.List: """Generates list of commands to be parse to MCX in a subprocess. diff --git a/simpa/utils/tags.py b/simpa/utils/tags.py index 2d8c80c0..54bfadef 100644 --- a/simpa/utils/tags.py +++ b/simpa/utils/tags.py @@ -1454,11 +1454,21 @@ class Tags: Usage: simpa.core.simulation_modules.optical_simulation_module.optical_forward_model_mcx_reflectance_adapter """ - VOLUME_BOUNDARY_BONDITION = "volume_boundary_condition" + VOLUME_BOUNDARY_CONDITION = "volume_boundary_condition" """ FIXME """ + MCX_DETECTOR = ("mcx_detector", dict) + """ + The detector property list used in mcx for capturing exiting photons.\n + Only use it if you want to capture photon exit properties (COMPUTE_PHOTON_DIRECTION_AT_EXIT is enabled). + + Example usage is: + + settings[Tags.MCX_DETECTOR] = [{"Pos": [30.0, 30.0, 0.0], "R": 45.0}] + """ + COMPUTE_PHOTON_DIRECTION_AT_EXIT = "save_dir_at_exit" """ Flag that indicates if the direction of photons when they exit the volume should be stored From e32392528ed7e80bf3eb8bfb5915de87d4854b07 Mon Sep 17 00:00:00 2001 From: RecurvedBow Date: Thu, 15 May 2025 15:13:51 +0200 Subject: [PATCH 25/34] + Settings for optical lens modeling --- .../optical_module/mcx_adapter.py | 3 +- simpa/utils/settings.py | 18 +++++++++++ simpa/utils/tags.py | 31 +++++++++++++++++++ 3 files changed, 51 insertions(+), 1 deletion(-) diff --git a/simpa/core/simulation_modules/optical_module/mcx_adapter.py b/simpa/core/simulation_modules/optical_module/mcx_adapter.py index 65b969c1..991e0d50 100644 --- a/simpa/core/simulation_modules/optical_module/mcx_adapter.py +++ b/simpa/core/simulation_modules/optical_module/mcx_adapter.py @@ -117,6 +117,7 @@ def get_mcx_settings(self, self.frames = int(time / dt) source = illumination_geometry.get_mcx_illuminator_definition(self.global_settings) + mcx_length_unit = self.global_settings[Tags.SPACING_MM] if Tags.TRUE_SPACING_MM not in self.global_settings else self.global_settings[Tags.TRUE_SPACING_MM] settings_dict = { "Session": { "ID": mcx_volumetric_data_file, @@ -134,7 +135,7 @@ def get_mcx_settings(self, }, "Domain": { "OriginType": 0, - "LengthUnit": self.global_settings[Tags.SPACING_MM], + "LengthUnit": mcx_length_unit, "Media": [ { "mua": 0, diff --git a/simpa/utils/settings.py b/simpa/utils/settings.py index 447ff48d..b59a4c95 100644 --- a/simpa/utils/settings.py +++ b/simpa/utils/settings.py @@ -113,6 +113,24 @@ def set_optical_settings(self, optical_settings: dict): """ self[Tags.OPTICAL_MODEL_SETTINGS] = Settings(optical_settings) + def get_optical_camera_settings(self): + """" + Returns the settings for the optical forward model that are saved in this settings dictionary + """ + optical_camera_settings = self[Tags.OPTICAL_CAMERA_SETTINGS] + if isinstance(optical_camera_settings, Settings): + return optical_camera_settings + else: + return Settings(optical_camera_settings) + + def set_optical_camera_settings(self, optical_camera_settings: dict): + """ + Replaces the currently stored optical settings with the given dictionary + + :param optical_settings: a dictionary containing the optical settings + """ + self[Tags.OPTICAL_CAMERA_SETTINGS] = Settings(optical_camera_settings) + def get_volume_creation_settings(self): """" Returns the settings for the optical forward model that are saved in this settings dictionary diff --git a/simpa/utils/tags.py b/simpa/utils/tags.py index 54bfadef..81d9d01d 100644 --- a/simpa/utils/tags.py +++ b/simpa/utils/tags.py @@ -370,6 +370,31 @@ class Tags: Usage: module optical_simulation_module """ + OPTICAL_CAMERA_SETTINGS = ("optical_camera_settings", dict) + """ + Optical camera settings + """ + + OPTICAL_CAMERA_OBJECT_TO_LENS_DISTANCE = ("optical_camera_object_to_lens_distance", float | list) + """ + The distance between the object and the camera lens. + """ + + OPTICAL_CAMERA_LENS_TO_SENSOR_DISTANCE = ("optical_camera_lens_to_sensor_distance", float | list) + """ + The distance between the camera lens and the sensor. + """ + + OPTICAL_CAMERA_FOCAL_LENGTH = ("optical_camera_focal_length", float | list) + """ + The camera lens focal length. + """ + + OPTICAL_CAMERA_F_NUMBER = ("optical_camera_f_number", float | list) + """ + The camera f number. + """ + LASER_PULSE_ENERGY_IN_MILLIJOULE = ("laser_pulse_energy_in_millijoule", (int, np.integer, float, list, range, tuple, np.ndarray)) """ @@ -949,6 +974,12 @@ class Tags: Usage: SIMPA package """ + TRUE_SPACING_MM = ("true_voxel_spacing_mm", Number) + """ + Isotropic extent of one voxels in mm used in MCX. If not set, Tags.SPACING_MM will be used instead.\n + Usage: MCXReflectanceAdapter + """ + DIM_VOLUME_X_MM = ("volume_x_dim_mm", Number) """ Extent of the x-axis of the generated volume.\n From f4d651f034b2402353ca3c00a9a6f8c600104b83 Mon Sep 17 00:00:00 2001 From: RecurvedBow Date: Tue, 20 May 2025 10:29:19 +0200 Subject: [PATCH 26/34] + MCX camera settings --- .../optical_module/mcx_reflectance_adapter.py | 10 ++++++++ simpa/utils/settings.py | 18 +++++++++++++ simpa/utils/tags.py | 25 +++++++++++++++++++ 3 files changed, 53 insertions(+) diff --git a/simpa/core/simulation_modules/optical_module/mcx_reflectance_adapter.py b/simpa/core/simulation_modules/optical_module/mcx_reflectance_adapter.py index 60da5aac..9cb12090 100644 --- a/simpa/core/simulation_modules/optical_module/mcx_reflectance_adapter.py +++ b/simpa/core/simulation_modules/optical_module/mcx_reflectance_adapter.py @@ -100,6 +100,7 @@ def get_mcx_settings(self, settings_dict = super().get_mcx_settings(illumination_geometry=illumination_geometry, **kwargs) uses_photon_exit_data = Tags.COMPUTE_PHOTON_DIRECTION_AT_EXIT in self.component_settings and self.component_settings[ Tags.COMPUTE_PHOTON_DIRECTION_AT_EXIT] + contains_camera_settings = Tags.MCX_CAMERA_SETTINGS in self.global_settings if uses_photon_exit_data: if Tags.MCX_DETECTOR in self.global_settings: @@ -117,6 +118,15 @@ def get_mcx_settings(self, } ] + if contains_camera_settings: + camera_settings = self.global_settings[Tags.MCX_CAMERA_SETTINGS] + settings_dict["Camera"] = { + "ObjectDistance": camera_settings[Tags.MCX_OBJECT_DISTANCE], + "ProjectionDistance": camera_settings[Tags.MCX_PROJECTION_DISTANCE], + "FocalLength": camera_settings[Tags.MCX_FOCAL_LENGTH], + "ApertureRadius": camera_settings[Tags.MCX_APERTURE_RADIUS], + } + return settings_dict def get_command(self) -> typing.List: diff --git a/simpa/utils/settings.py b/simpa/utils/settings.py index b59a4c95..6b85c4b6 100644 --- a/simpa/utils/settings.py +++ b/simpa/utils/settings.py @@ -131,6 +131,24 @@ def set_optical_camera_settings(self, optical_camera_settings: dict): """ self[Tags.OPTICAL_CAMERA_SETTINGS] = Settings(optical_camera_settings) + def get_mcx_camera_settings(self): + """" + Returns the camera settings for MCX that are saved in this settings dictionary + """ + mcx_camera_settings = self[Tags.MCX_CAMERA_SETTINGS] + if isinstance(mcx_camera_settings, Settings): + return mcx_camera_settings + else: + return Settings(mcx_camera_settings) + + def set_mcx_camera_settings(self, mcx_camera_settings: dict): + """ + Replaces the currently stored optical settings with the given dictionary + + :param optical_settings: a dictionary containing the optical settings + """ + self[Tags.MCX_CAMERA_SETTINGS] = Settings(mcx_camera_settings) + def get_volume_creation_settings(self): """" Returns the settings for the optical forward model that are saved in this settings dictionary diff --git a/simpa/utils/tags.py b/simpa/utils/tags.py index 81d9d01d..a27d39ae 100644 --- a/simpa/utils/tags.py +++ b/simpa/utils/tags.py @@ -395,6 +395,31 @@ class Tags: The camera f number. """ + MCX_CAMERA_SETTINGS = ("mcx_camera_settings", dict) + """ + MCX camera settings + """ + + MCX_OBJECT_DISTANCE = ("mcx_object_distance", float) + """ + The distance between the object and the camera lens. + """ + + MCX_PROJECTION_DISTANCE = ("mcx_projection_distance", float) + """ + The distance between the object and the camera lens. + """ + + MCX_FOCAL_LENGTH = ("mcx_focal_length", float) + """ + The distance between the object and the camera lens. + """ + + MCX_APERTURE_RADIUS = ("mcx_aperture_radius", float) + """ + The distance between the object and the camera lens. + """ + LASER_PULSE_ENERGY_IN_MILLIJOULE = ("laser_pulse_energy_in_millijoule", (int, np.integer, float, list, range, tuple, np.ndarray)) """ From 076b86336451ec6665ade8a1ff9267814cbd7506 Mon Sep 17 00:00:00 2001 From: RecurvedBow Date: Wed, 21 May 2025 14:12:52 +0200 Subject: [PATCH 27/34] MCX camera intensity can now be used without saving individual photon data --- .../optical_module/mcx_reflectance_adapter.py | 4 +++- .../optical_module/volume_boundary_condition.py | 2 -- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/simpa/core/simulation_modules/optical_module/mcx_reflectance_adapter.py b/simpa/core/simulation_modules/optical_module/mcx_reflectance_adapter.py index 9cb12090..e5bb0eaf 100644 --- a/simpa/core/simulation_modules/optical_module/mcx_reflectance_adapter.py +++ b/simpa/core/simulation_modules/optical_module/mcx_reflectance_adapter.py @@ -240,7 +240,9 @@ def pre_process_volumes(self, **kwargs) -> Tuple: check_padding = (Tags.COMPUTE_DIFFUSE_REFLECTANCE in self.component_settings and self.component_settings[Tags.COMPUTE_DIFFUSE_REFLECTANCE]) or \ (Tags.COMPUTE_PHOTON_DIRECTION_AT_EXIT in self.component_settings and - self.component_settings[Tags.COMPUTE_PHOTON_DIRECTION_AT_EXIT]) + self.component_settings[Tags.COMPUTE_PHOTON_DIRECTION_AT_EXIT]) or \ + Tags.MCX_CAMERA_SETTINGS in self.global_settings + # check that all volumes on first layer along z have only 0 values if np.any([np.any(a[:, :, 0] != 0)] for a in arrays) and check_padding: results = tuple(np.pad(a, ((0, 0), (0, 0), (1, 0)), "constant", constant_values=0) for a in arrays) diff --git a/simpa/core/simulation_modules/optical_module/volume_boundary_condition.py b/simpa/core/simulation_modules/optical_module/volume_boundary_condition.py index c5b2d715..3955fd1e 100644 --- a/simpa/core/simulation_modules/optical_module/volume_boundary_condition.py +++ b/simpa/core/simulation_modules/optical_module/volume_boundary_condition.py @@ -19,8 +19,6 @@ class MCXVolumeBoundaryCondition(Enum): """The default behavior.""" MIRROR_REFLECTION = "mm_mm_000000" """The photons are totally reflected as if the volume faces are mirrors.""" - MIRROR_REFLECTION_WITH_DETECTION = "mm_mm_000001" - """The photons are totally reflected as if the volume faces are mirrors. The z-axis plane will also serve as a detector.""" CYCLIC = "cc_cc_000000" """The photons reenter from the opposite volume face.""" ABSORB = "aa_aa_000000" From bb353ac688abdaf943d26d8dcbe99ae4e7dbccb1 Mon Sep 17 00:00:00 2001 From: RecurvedBow Date: Wed, 21 May 2025 15:33:06 +0200 Subject: [PATCH 28/34] Save camera intensity from MCX into HDF simulation output --- .../optical_module/mcx_reflectance_adapter.py | 30 ++++++++++++++----- simpa/utils/constants.py | 3 +- simpa/utils/dict_path_manager.py | 2 +- simpa/utils/tags.py | 6 ++++ 4 files changed, 32 insertions(+), 9 deletions(-) diff --git a/simpa/core/simulation_modules/optical_module/mcx_reflectance_adapter.py b/simpa/core/simulation_modules/optical_module/mcx_reflectance_adapter.py index e5bb0eaf..b5b0220b 100644 --- a/simpa/core/simulation_modules/optical_module/mcx_reflectance_adapter.py +++ b/simpa/core/simulation_modules/optical_module/mcx_reflectance_adapter.py @@ -1,6 +1,7 @@ # SPDX-FileCopyrightText: 2021 Division of Intelligent Medical Systems, DKFZ # SPDX-FileCopyrightText: 2021 Janek Groehl # SPDX-License-Identifier: MIT +import pathlib import typing import numpy as np @@ -66,8 +67,6 @@ def forward_model(self, :param anisotropy: array containing the anisotropy of the volume defined by `absorption_cm` and `scattering_cm` :param refractive_index: array containing the refractive index of the volume defined by `absorption_cm` and `scattering_cm` :param illumination_geometry: and instance of `IlluminationGeometryBase` defining the illumination geometry - :param probe_position_mm: position of a probe in `mm` units. This is parsed to - `illumination_geometry.get_mcx_illuminator_definition` :return: `Settings` containing the results of optical simulations, the keys in this dictionary-like object depend on the Tags defined in `self.component_settings` """ @@ -196,6 +195,11 @@ def read_mcx_output(self, **kwargs) -> Dict: self.component_settings[Tags.COMPUTE_DIFFUSE_REFLECTANCE]: results[Tags.DATA_FIELD_DIFFUSE_REFLECTANCE] = ref results[Tags.DATA_FIELD_DIFFUSE_REFLECTANCE_POS] = ref_pos + if Tags.MCX_CAMERA_SETTINGS in self.global_settings and self.global_settings[Tags.MCX_CAMERA_SETTINGS]: + cam_intensity_file_path = pathlib.Path(self.mcx_volumetric_data_file).with_suffix(".bin") + cam_intensity = np.fromfile(cam_intensity_file_path, dtype=np.float32) + results[Tags.DATA_FIELD_CAMERA_INTENSITY] = cam_intensity + if Tags.COMPUTE_PHOTON_DIRECTION_AT_EXIT in self.component_settings and \ self.component_settings[Tags.COMPUTE_PHOTON_DIRECTION_AT_EXIT]: content = jdata.load(self.mcx_photon_data_file) @@ -296,6 +300,8 @@ def run_forward_model(self, reflectance_position = [] photon_position = [] photon_direction = [] + camera_intensity = [] + if isinstance(_device, list): # per convention this list has at least two elements results = self.forward_model(absorption_cm=absorption, @@ -307,7 +313,9 @@ def run_forward_model(self, reflectance=reflectance, reflectance_position=reflectance_position, photon_position=photon_position, - photon_direction=photon_direction) + photon_direction=photon_direction, + camera_intensity=camera_intensity) + fluence = results[Tags.DATA_FIELD_FLUENCE] for idx in range(1, len(_device)): # we already looked at the 0th element, so go from 1 to n-1 @@ -320,9 +328,10 @@ def run_forward_model(self, reflectance=reflectance, reflectance_position=reflectance_position, photon_position=photon_position, - photon_direction=photon_direction) - fluence += results[Tags.DATA_FIELD_FLUENCE] + photon_direction=photon_direction, + camera_intensity=camera_intensity) + fluence += results[Tags.DATA_FIELD_FLUENCE] fluence = fluence / len(_device) else: @@ -335,7 +344,9 @@ def run_forward_model(self, reflectance=reflectance, reflectance_position=reflectance_position, photon_position=photon_position, - photon_direction=photon_direction) + photon_direction=photon_direction, + camera_intensity=camera_intensity) + fluence = results[Tags.DATA_FIELD_FLUENCE] aggregated_results = dict() aggregated_results[Tags.DATA_FIELD_FLUENCE] = fluence @@ -345,6 +356,8 @@ def run_forward_model(self, if photon_position: aggregated_results[Tags.DATA_FIELD_PHOTON_EXIT_POS] = np.concatenate(photon_position, axis=0) aggregated_results[Tags.DATA_FIELD_PHOTON_EXIT_DIR] = np.concatenate(photon_direction, axis=0) + if camera_intensity: + aggregated_results[Tags.DATA_FIELD_CAMERA_INTENSITY] = np.concatenate(camera_intensity, axis=0) return aggregated_results @staticmethod @@ -352,10 +365,13 @@ def _append_results(results, reflectance, reflectance_position, photon_position: list[np.ndarray], - photon_direction: list[np.ndarray]): + photon_direction: list[np.ndarray], + camera_intensity: list[np.ndarray]): if Tags.DATA_FIELD_DIFFUSE_REFLECTANCE in results: reflectance.append(results[Tags.DATA_FIELD_DIFFUSE_REFLECTANCE]) reflectance_position.append(results[Tags.DATA_FIELD_DIFFUSE_REFLECTANCE_POS]) if Tags.DATA_FIELD_PHOTON_EXIT_POS in results: photon_position.append(results[Tags.DATA_FIELD_PHOTON_EXIT_POS]) photon_direction.append(results[Tags.DATA_FIELD_PHOTON_EXIT_DIR]) + if Tags.DATA_FIELD_CAMERA_INTENSITY in results: + camera_intensity.append(results[Tags.DATA_FIELD_CAMERA_INTENSITY]) diff --git a/simpa/utils/constants.py b/simpa/utils/constants.py index ac073b25..f49f4ef8 100644 --- a/simpa/utils/constants.py +++ b/simpa/utils/constants.py @@ -60,7 +60,8 @@ class SegmentationClasses: Tags.DATA_FIELD_DIFFUSE_REFLECTANCE, Tags.DATA_FIELD_DIFFUSE_REFLECTANCE_POS, Tags.DATA_FIELD_PHOTON_EXIT_POS, - Tags.DATA_FIELD_PHOTON_EXIT_DIR] + Tags.DATA_FIELD_PHOTON_EXIT_DIR, + Tags.DATA_FIELD_CAMERA_INTENSITY] simulation_output_fields = [Tags.OPTICAL_MODEL_OUTPUT_NAME, Tags.SIMULATION_PROPERTIES] diff --git a/simpa/utils/dict_path_manager.py b/simpa/utils/dict_path_manager.py index f83e6dcf..a828b287 100644 --- a/simpa/utils/dict_path_manager.py +++ b/simpa/utils/dict_path_manager.py @@ -35,7 +35,7 @@ def generate_dict_path(data_field, wavelength: (int, float) = None) -> str: elif data_field in simulation_output: if data_field in [Tags.DATA_FIELD_FLUENCE, Tags.DATA_FIELD_INITIAL_PRESSURE, Tags.OPTICAL_MODEL_UNITS, Tags.DATA_FIELD_DIFFUSE_REFLECTANCE, Tags.DATA_FIELD_DIFFUSE_REFLECTANCE_POS, - Tags.DATA_FIELD_PHOTON_EXIT_POS, Tags.DATA_FIELD_PHOTON_EXIT_DIR]: + Tags.DATA_FIELD_PHOTON_EXIT_POS, Tags.DATA_FIELD_PHOTON_EXIT_DIR, Tags.DATA_FIELD_CAMERA_INTENSITY]: dict_path = "/" + Tags.SIMULATIONS + "/" + Tags.OPTICAL_MODEL_OUTPUT_NAME + "/" + data_field + wl else: dict_path = "/" + Tags.SIMULATIONS + "/" + data_field + wl diff --git a/simpa/utils/tags.py b/simpa/utils/tags.py index a27d39ae..47a60abb 100644 --- a/simpa/utils/tags.py +++ b/simpa/utils/tags.py @@ -1537,6 +1537,12 @@ class Tags: Usage: simpa.core.simulation_modules.optical_simulation_module.optical_forward_model_mcx_reflectance_adapter """ + DATA_FIELD_CAMERA_INTENSITY = "camera_intensity" + """ + Identifier for the raw camera intensity returned by MCX + Usage: simpa.core.simulation_modules.optical_simulation_module.optical_forward_model_mcx_reflectance_adapter + """ + DATA_FIELD_DIFFUSE_REFLECTANCE_POS = "diffuse_reflectance_pos" """ Identified for the position within the volumes where the diffuse reflectance was originally stored, interface to From 63e5b0db09f89427641a34a3766275e3156ba829 Mon Sep 17 00:00:00 2001 From: RecurvedBow Date: Thu, 22 May 2025 15:25:28 +0200 Subject: [PATCH 29/34] Fast MCX reflectance adapter to skip HDF saving --- simpa/__init__.py | 2 +- .../optical_module/mcx_reflectance_adapter.py | 98 +++++++++++++++++++ 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/simpa/__init__.py b/simpa/__init__.py index c653bfe1..ff6a18d2 100644 --- a/simpa/__init__.py +++ b/simpa/__init__.py @@ -19,7 +19,7 @@ from .core.simulation_modules.optical_module.mcx_adapter import \ MCXAdapter from .core.simulation_modules.optical_module.mcx_reflectance_adapter import \ - MCXReflectanceAdapter + MCXReflectanceAdapter, FastMCXReflectanceAdapter from .core.simulation_modules.acoustic_module.k_wave_adapter import \ KWaveAdapter from .core.simulation_modules.reconstruction_module.delay_and_sum_adapter import \ diff --git a/simpa/core/simulation_modules/optical_module/mcx_reflectance_adapter.py b/simpa/core/simulation_modules/optical_module/mcx_reflectance_adapter.py index b5b0220b..104e71de 100644 --- a/simpa/core/simulation_modules/optical_module/mcx_reflectance_adapter.py +++ b/simpa/core/simulation_modules/optical_module/mcx_reflectance_adapter.py @@ -10,6 +10,8 @@ import os from typing import Tuple, Dict, Union +from simpa.io_handling.io_hdf5 import load_data_field + from simpa.core.simulation_modules.optical_module.volume_boundary_condition import MCXVolumeBoundaryCondition from simpa.utils import Tags, Settings from simpa.core.simulation_modules.optical_module.mcx_adapter import MCXAdapter @@ -375,3 +377,99 @@ def _append_results(results, photon_direction.append(results[Tags.DATA_FIELD_PHOTON_EXIT_DIR]) if Tags.DATA_FIELD_CAMERA_INTENSITY in results: camera_intensity.append(results[Tags.DATA_FIELD_CAMERA_INTENSITY]) + + +class FastMCXReflectanceAdapter(MCXReflectanceAdapter): + """ + Same functionality of MCXReflectanceAdapter, but saves the simulation output through an event instead + of saving it in the HDF output file. + + Note: Does not include fluence. + """ + + def __init__(self, global_settings: Settings): + """ + initializes MCX-specific configuration and clean-up instances + + :param global_settings: global settings used during simulations + """ + super().__init__(global_settings=global_settings) + + self.simulation_finished_event_listeners = [] + + def read_mcx_output(self, **kwargs) -> Dict: + """ + reads the temporary output generated with MCX + + :param kwargs: dummy, used for class inheritance compatibility + :return: `Settings` instance containing the MCX output + """ + results = dict() + if os.path.isfile(self.mcx_volumetric_data_file) and self.mcx_volumetric_data_file.endswith( + self.mcx_output_suffixes['mcx_volumetric_data_file']): + if Tags.COMPUTE_DIFFUSE_REFLECTANCE in self.component_settings and \ + self.component_settings[Tags.COMPUTE_DIFFUSE_REFLECTANCE]: + content = jdata.load(self.mcx_volumetric_data_file) + fluence = content['NIFTIData'] + + if fluence.ndim > 3: + # remove the 1 or 2 (for mcx >= v2024.1) additional dimensions of size 1 if present to obtain a 3d array + fluence = fluence.reshape(fluence.shape[0], fluence.shape[1], -1) + + ref, ref_pos, fluence = self.extract_reflectance_from_fluence(fluence=fluence) + fluence = self.post_process_volumes(**{'arrays': (fluence,)})[0] + fluence *= 100 # Convert from J/mm^2 to J/cm^2 + results[Tags.DATA_FIELD_DIFFUSE_REFLECTANCE] = ref + results[Tags.DATA_FIELD_DIFFUSE_REFLECTANCE_POS] = ref_pos + else: + raise FileNotFoundError( + f"Could not find .jnii file for {self.mcx_volumetric_data_file}, something went wrong!") + + if Tags.MCX_CAMERA_SETTINGS in self.global_settings and self.global_settings[Tags.MCX_CAMERA_SETTINGS]: + cam_intensity_file_path = pathlib.Path(self.mcx_volumetric_data_file).with_suffix(".bin") + cam_intensity = np.fromfile(cam_intensity_file_path, dtype=np.float32) + results[Tags.DATA_FIELD_CAMERA_INTENSITY] = cam_intensity + + if Tags.COMPUTE_PHOTON_DIRECTION_AT_EXIT in self.component_settings and \ + self.component_settings[Tags.COMPUTE_PHOTON_DIRECTION_AT_EXIT]: + content = jdata.load(self.mcx_photon_data_file) + photon_pos = content['MCXData']['PhotonData']['p'] + photon_dir = content['MCXData']['PhotonData']['v'] + results[Tags.DATA_FIELD_PHOTON_EXIT_POS] = photon_pos + results[Tags.DATA_FIELD_PHOTON_EXIT_DIR] = photon_dir + return results + + def run(self, device: Union[IlluminationGeometryBase, PhotoacousticDevice]) -> None: + """ + runs optical simulations. Volumes are first loaded from HDF5 file and parsed to `self.forward_model`, the output + is aggregated in case multiple illuminations are defined by `device` and stored in the same HDF5 file. + + :param device: Illumination or Photoacoustic device that defines the illumination geometry + :return: None + """ + assert Tags.IGNORE_QA_ASSERTIONS not in self.global_settings, "Not implemented" + assert Tags.LASER_PULSE_ENERGY_IN_MILLIJOULE not in self.component_settings, "Not implemented" + + self.logger.info("Simulating the optical forward process...") + + file_path = self.global_settings[Tags.SIMPA_OUTPUT_FILE_PATH] + wl = str(self.global_settings[Tags.WAVELENGTH]) + + absorption = load_data_field(file_path, Tags.DATA_FIELD_ABSORPTION_PER_CM, wl) + scattering = load_data_field(file_path, Tags.DATA_FIELD_SCATTERING_PER_CM, wl) + anisotropy = load_data_field(file_path, Tags.DATA_FIELD_ANISOTROPY, wl) + refractive_index = load_data_field(file_path, Tags.DATA_FIELD_REFRACTIVE_INDEX, wl) + + assert isinstance(device, IlluminationGeometryBase), "Not implemented" + + results = self.forward_model(absorption_cm=absorption, + scattering_cm=scattering, + anisotropy=anisotropy, + refractive_index=refractive_index, + illumination_geometry=device) + + assert Tags.DATA_FIELD_FLUENCE not in results + self.logger.info("Simulating the optical forward process...[Done]") + + for event_listener in self.simulation_finished_event_listeners: + event_listener(self.global_settings[Tags.WAVELENGTH], results) From 8e313ed6dd28f568e93b2d8d40df90db0ffc5f79 Mon Sep 17 00:00:00 2001 From: RecurvedBow Date: Fri, 23 May 2025 16:57:25 +0200 Subject: [PATCH 30/34] MCX output file from MCX camera settings now deleted after extracting simulation results --- .../simulation_modules/optical_module/mcx_reflectance_adapter.py | 1 + 1 file changed, 1 insertion(+) diff --git a/simpa/core/simulation_modules/optical_module/mcx_reflectance_adapter.py b/simpa/core/simulation_modules/optical_module/mcx_reflectance_adapter.py index 104e71de..0f7c9cdc 100644 --- a/simpa/core/simulation_modules/optical_module/mcx_reflectance_adapter.py +++ b/simpa/core/simulation_modules/optical_module/mcx_reflectance_adapter.py @@ -50,6 +50,7 @@ def __init__(self, global_settings: Settings): self.volume_boundary_condition_str = MCXVolumeBoundaryCondition.DEFAULT.value self.mcx_output_suffixes = {'mcx_volumetric_data_file': '.jnii', + 'mcx_volumetric_data_file_camera': '.bin', 'mcx_photon_data_file': '_detp.jdat'} def forward_model(self, From bac06592ad9c9240299714fe1b8cd634fe1ed742 Mon Sep 17 00:00:00 2001 From: RecurvedBow Date: Tue, 15 Jul 2025 17:38:23 +0200 Subject: [PATCH 31/34] Included MMC definition for planar/rectangle illumination --- .../illumination_geometry_base.py | 14 +++++++++++++ .../rectangle_illumination.py | 20 +++++++++++++++++-- 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/simpa/core/device_digital_twins/illumination_geometries/illumination_geometry_base.py b/simpa/core/device_digital_twins/illumination_geometries/illumination_geometry_base.py index 6c5780bf..23dd6197 100644 --- a/simpa/core/device_digital_twins/illumination_geometries/illumination_geometry_base.py +++ b/simpa/core/device_digital_twins/illumination_geometries/illumination_geometry_base.py @@ -51,6 +51,20 @@ def get_mcx_illuminator_definition(self, global_settings) -> dict: """ pass + @abstractmethod + def get_mmc_illuminator_definition(self, global_settings) -> dict: + """ + IMPORTANT: This method creates a dictionary that contains tags as they are expected for the + MMC simulation tool to represent the illumination geometry of this device. + + :param global_settings: The global_settings instance containing the simulation instructions. + :type global_settings: Settings + + :return: Dictionary that includes all parameters needed for mcx. + :rtype: dict + """ + pass + def check_settings_prerequisites(self, global_settings) -> bool: return True diff --git a/simpa/core/device_digital_twins/illumination_geometries/rectangle_illumination.py b/simpa/core/device_digital_twins/illumination_geometries/rectangle_illumination.py index fa0c26d6..4d897d5b 100644 --- a/simpa/core/device_digital_twins/illumination_geometries/rectangle_illumination.py +++ b/simpa/core/device_digital_twins/illumination_geometries/rectangle_illumination.py @@ -53,7 +53,9 @@ def __init__(self, assert width_mm > 0 if isinstance(focal_length_in_mm, str): - assert focal_length_in_mm == "_NaN_" or focal_length_in_mm == "-_Inf_", f"{focal_length_in_mm} is not supported yet" + assert ((focal_length_in_mm == "_NaN_" + or focal_length_in_mm == "-_Inf_") + or focal_length_in_mm == "_Inf_"), f"{focal_length_in_mm} is not supported yet" self.focal_length_in_mm = focal_length_in_mm self.length_mm = length_mm @@ -73,7 +75,7 @@ def get_mcx_illuminator_definition(self, global_settings: Settings) -> dict: spacing = global_settings[Tags.SPACING_MM] - device_position = list(np.rint(self.device_position_mm / spacing)) + device_position = list(self.device_position_mm / spacing) self.logger.debug(device_position) @@ -98,6 +100,20 @@ def get_mcx_illuminator_definition(self, global_settings: Settings) -> dict: "Param2": source_param2 } + def get_mmc_illuminator_definition(self, global_settings: Settings) -> dict: + """ + Returns the illumination parameters for MMC simulations. + + :param global_settings: The global settings. + + :return: The illumination parameters as a dictionary. + """ + + mcx_illuminator_definition = self.get_mcx_illuminator_definition(global_settings) + mcx_illuminator_definition["Param1"] += [0.0] + mcx_illuminator_definition["Param2"] += [0.0] + return mcx_illuminator_definition + def serialize(self) -> dict: """ Serializes the object into a dictionary. From 4d222154f1351bb7e4f1020a0568e152a1b78843 Mon Sep 17 00:00:00 2001 From: RecurvedBow Date: Fri, 18 Jul 2025 11:46:26 +0200 Subject: [PATCH 32/34] Added tags for repeated MMC simulation with same settings, MMC camera modeling (prototype) --- simpa/utils/tags.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/simpa/utils/tags.py b/simpa/utils/tags.py index 47a60abb..c980ee6b 100644 --- a/simpa/utils/tags.py +++ b/simpa/utils/tags.py @@ -420,6 +420,27 @@ class Tags: The distance between the object and the camera lens. """ + MMC_IMAGE_HEIGHT = ("mmc_image_height", int) + """ + The image height. Currently only used in MMC. + """ + + MMC_IMAGE_WIDTH = ("mmc_image_width", int) + """ + The image width. Currently only used in MMC. + """ + + MMC_PIXEL_PITCH = ("mmc_pixel_pitch", float) + """ + The pixel pitch. Currently only used in MMC. + """ + + MMC_RERUNS = ("mmc_reruns", int) + """ + How often to rerun MMC simulations to effectively simulate with much more photons. + Currently only used in MMCReflectanceAdapter. + """ + LASER_PULSE_ENERGY_IN_MILLIJOULE = ("laser_pulse_energy_in_millijoule", (int, np.integer, float, list, range, tuple, np.ndarray)) """ From b2900958eabe133a89b95cae9b8a54b1ef0a62d8 Mon Sep 17 00:00:00 2001 From: RecurvedBow Date: Wed, 8 Oct 2025 14:15:38 +0200 Subject: [PATCH 33/34] + MCX photon backtracking --- .../rectangle_illumination.py | 4 ++-- .../optical_module/mcx_adapter.py | 23 +++++++++++++++---- .../optical_module/mcx_reflectance_adapter.py | 22 ++++++++++++------ simpa/utils/settings.py | 20 +++++++++++++++- simpa/utils/tags.py | 17 +++++++++++++- 5 files changed, 71 insertions(+), 15 deletions(-) diff --git a/simpa/core/device_digital_twins/illumination_geometries/rectangle_illumination.py b/simpa/core/device_digital_twins/illumination_geometries/rectangle_illumination.py index 4d897d5b..457e7db4 100644 --- a/simpa/core/device_digital_twins/illumination_geometries/rectangle_illumination.py +++ b/simpa/core/device_digital_twins/illumination_geometries/rectangle_illumination.py @@ -86,8 +86,8 @@ def get_mcx_illuminator_definition(self, global_settings: Settings) -> dict: self.focal_length_in_mm, str) else self.focal_length_in_mm / spacing source_direction.append(param4) - source_param1 = [np.rint(self.width_mm / spacing) + 1, 0, 0] - source_param2 = [0, np.rint(self.length_mm / spacing) + 1, 0] + source_param1 = [self.width_mm / spacing + 1, 0., 0.] + source_param2 = [0., self.length_mm / spacing + 1, 0.] # If Pos=[10, 12, 0], Param1=[10, 0, 0], Param2=[0, 20, 0], # then illumination covers: x in [10, 20], y in [12, 32] diff --git a/simpa/core/simulation_modules/optical_module/mcx_adapter.py b/simpa/core/simulation_modules/optical_module/mcx_adapter.py index 991e0d50..a9d1200b 100644 --- a/simpa/core/simulation_modules/optical_module/mcx_adapter.py +++ b/simpa/core/simulation_modules/optical_module/mcx_adapter.py @@ -205,10 +205,11 @@ def generate_mcx_bin_input(self, :param refractive_index: Refractive index :return: None """ - absorption_mm, scattering_mm, anisotropy, refractive_index = self.pre_process_volumes(**{'absorption_cm': absorption_cm, - 'scattering_cm': scattering_cm, - 'anisotropy': anisotropy, - 'refractive_index': refractive_index}) + absorption_mm = self.pre_process_volume(absorption_cm, is_mua_or_mus=True) + scattering_mm = self.pre_process_volume(scattering_cm, is_mua_or_mus=True) + anisotropy = self.pre_process_volume(anisotropy, is_mua_or_mus=False) + refractive_index = self.pre_process_volume(refractive_index, is_mua_or_mus=False) + # stack arrays to give array with shape (nx,ny,nz,4) - where the 4 floats correspond to mua/mus/g/n op_array = np.stack([absorption_mm, scattering_mm, anisotropy, refractive_index], axis=-1, dtype=np.float32) [self.nx, self.ny, self.nz, _] = np.shape(op_array) @@ -245,6 +246,20 @@ def remove_mcx_output(self) -> None: if os.path.isfile(f): os.remove(f) + def pre_process_volume(self, volume: np.ndarray, is_mua_or_mus: bool) -> np.ndarray: + """ + pre-process volumes before running simulations with MCX. The volumes are transformed to `mm` units + + :param kwargs: dictionary containing at least the keys `scattering_cm, absorption_cm, anisotropy, refractive_index` + :return: `Tuple` of volumes after transformation + """ + if is_mua_or_mus: + volume /= 10 # Conversion from 1/cm to 1/mm (required by MCX) + volume[volume == 0] = np.nan + + # No preprocessing is done on anisotropy and refractive index + return volume + def pre_process_volumes(self, **kwargs) -> Tuple: """ pre-process volumes before running simulations with MCX. The volumes are transformed to `mm` units diff --git a/simpa/core/simulation_modules/optical_module/mcx_reflectance_adapter.py b/simpa/core/simulation_modules/optical_module/mcx_reflectance_adapter.py index 0f7c9cdc..da66805f 100644 --- a/simpa/core/simulation_modules/optical_module/mcx_reflectance_adapter.py +++ b/simpa/core/simulation_modules/optical_module/mcx_reflectance_adapter.py @@ -103,6 +103,8 @@ def get_mcx_settings(self, uses_photon_exit_data = Tags.COMPUTE_PHOTON_DIRECTION_AT_EXIT in self.component_settings and self.component_settings[ Tags.COMPUTE_PHOTON_DIRECTION_AT_EXIT] contains_camera_settings = Tags.MCX_CAMERA_SETTINGS in self.global_settings + contains_backtrack_settings = Tags.MCX_BACKTRACK_SETTINGS in self.global_settings + assert not contains_camera_settings or not contains_backtrack_settings if uses_photon_exit_data: if Tags.MCX_DETECTOR in self.global_settings: @@ -126,9 +128,16 @@ def get_mcx_settings(self, "ObjectDistance": camera_settings[Tags.MCX_OBJECT_DISTANCE], "ProjectionDistance": camera_settings[Tags.MCX_PROJECTION_DISTANCE], "FocalLength": camera_settings[Tags.MCX_FOCAL_LENGTH], - "ApertureRadius": camera_settings[Tags.MCX_APERTURE_RADIUS], + "ApertureRadius": camera_settings[Tags.MCX_APERTURE_RADIUS] + } + elif contains_backtrack_settings: + backtrack_settings = self.global_settings.get_mcx_backtrack_settings() + settings_dict["Backtrack"] = { + "ObjectDistance": backtrack_settings[Tags.MCX_OBJECT_DISTANCE], + "IdealDistance": backtrack_settings[Tags.MCX_IDEAL_DISTANCE], + "ApertureRadius": backtrack_settings[Tags.MCX_APERTURE_RADIUS], + "TrueApertureRadius": backtrack_settings[Tags.MCX_TRUE_APERTURE_RADIUS] } - return settings_dict def get_command(self) -> typing.List: @@ -198,7 +207,8 @@ def read_mcx_output(self, **kwargs) -> Dict: self.component_settings[Tags.COMPUTE_DIFFUSE_REFLECTANCE]: results[Tags.DATA_FIELD_DIFFUSE_REFLECTANCE] = ref results[Tags.DATA_FIELD_DIFFUSE_REFLECTANCE_POS] = ref_pos - if Tags.MCX_CAMERA_SETTINGS in self.global_settings and self.global_settings[Tags.MCX_CAMERA_SETTINGS]: + + if (Tags.MCX_CAMERA_SETTINGS in self.global_settings) or (Tags.MCX_BACKTRACK_SETTINGS in self.global_settings): cam_intensity_file_path = pathlib.Path(self.mcx_volumetric_data_file).with_suffix(".bin") cam_intensity = np.fromfile(cam_intensity_file_path, dtype=np.float32) results[Tags.DATA_FIELD_CAMERA_INTENSITY] = cam_intensity @@ -417,16 +427,14 @@ def read_mcx_output(self, **kwargs) -> Dict: # remove the 1 or 2 (for mcx >= v2024.1) additional dimensions of size 1 if present to obtain a 3d array fluence = fluence.reshape(fluence.shape[0], fluence.shape[1], -1) - ref, ref_pos, fluence = self.extract_reflectance_from_fluence(fluence=fluence) - fluence = self.post_process_volumes(**{'arrays': (fluence,)})[0] - fluence *= 100 # Convert from J/mm^2 to J/cm^2 + ref, ref_pos, _ = self.extract_reflectance_from_fluence(fluence=fluence) results[Tags.DATA_FIELD_DIFFUSE_REFLECTANCE] = ref results[Tags.DATA_FIELD_DIFFUSE_REFLECTANCE_POS] = ref_pos else: raise FileNotFoundError( f"Could not find .jnii file for {self.mcx_volumetric_data_file}, something went wrong!") - if Tags.MCX_CAMERA_SETTINGS in self.global_settings and self.global_settings[Tags.MCX_CAMERA_SETTINGS]: + if (Tags.MCX_CAMERA_SETTINGS in self.global_settings) or (Tags.MCX_BACKTRACK_SETTINGS in self.global_settings): cam_intensity_file_path = pathlib.Path(self.mcx_volumetric_data_file).with_suffix(".bin") cam_intensity = np.fromfile(cam_intensity_file_path, dtype=np.float32) results[Tags.DATA_FIELD_CAMERA_INTENSITY] = cam_intensity diff --git a/simpa/utils/settings.py b/simpa/utils/settings.py index 6b85c4b6..6f61e799 100644 --- a/simpa/utils/settings.py +++ b/simpa/utils/settings.py @@ -145,10 +145,28 @@ def set_mcx_camera_settings(self, mcx_camera_settings: dict): """ Replaces the currently stored optical settings with the given dictionary - :param optical_settings: a dictionary containing the optical settings + :param mcx_camera_settings: a dictionary containing the optical settings """ self[Tags.MCX_CAMERA_SETTINGS] = Settings(mcx_camera_settings) + def get_mcx_backtrack_settings(self): + """" + Returns the camera settings for MCX that are saved in this settings dictionary + """ + mcx_backtrack_settings = self[Tags.MCX_BACKTRACK_SETTINGS] + if isinstance(mcx_backtrack_settings, Settings): + return mcx_backtrack_settings + else: + return Settings(mcx_backtrack_settings) + + def set_mcx_backtrack_settings(self, mcx_backtrack_settings: dict): + """ + Replaces the currently stored optical settings with the given dictionary + + :param mcx_backtrack_settings: a dictionary containing the optical settings + """ + self[Tags.MCX_BACKTRACK_SETTINGS] = Settings(mcx_backtrack_settings) + def get_volume_creation_settings(self): """" Returns the settings for the optical forward model that are saved in this settings dictionary diff --git a/simpa/utils/tags.py b/simpa/utils/tags.py index c980ee6b..342a2670 100644 --- a/simpa/utils/tags.py +++ b/simpa/utils/tags.py @@ -400,6 +400,11 @@ class Tags: MCX camera settings """ + MCX_BACKTRACK_SETTINGS = ("mcx_backtrack_settings", dict) + """ + MCX backtrack settings + """ + MCX_OBJECT_DISTANCE = ("mcx_object_distance", float) """ The distance between the object and the camera lens. @@ -410,6 +415,11 @@ class Tags: The distance between the object and the camera lens. """ + MCX_IDEAL_DISTANCE = ("mcx_ideal_distance", float) + """ + The ideal distance between the object and the camera lens for perfect focus. Only required in backtrack mode. + """ + MCX_FOCAL_LENGTH = ("mcx_focal_length", float) """ The distance between the object and the camera lens. @@ -417,7 +427,12 @@ class Tags: MCX_APERTURE_RADIUS = ("mcx_aperture_radius", float) """ - The distance between the object and the camera lens. + The aperture radius. + """ + + MCX_TRUE_APERTURE_RADIUS = ("mcx_true_aperture_radius", float) + """ + The true aperture radius. Only required in backtrack mode which uses a larger aperture radius as the radius. """ MMC_IMAGE_HEIGHT = ("mmc_image_height", int) From 884cc2f2eaa3159349b5fba0a09bbf3e00e51636 Mon Sep 17 00:00:00 2001 From: RecurvedBow Date: Wed, 19 Nov 2025 15:11:08 +0100 Subject: [PATCH 34/34] Fixes to rectangle_illumination.py --- .../illumination_geometries/rectangle_illumination.py | 5 +++-- .../optical_module/mcx_reflectance_adapter.py | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/simpa/core/device_digital_twins/illumination_geometries/rectangle_illumination.py b/simpa/core/device_digital_twins/illumination_geometries/rectangle_illumination.py index 457e7db4..a0c83c23 100644 --- a/simpa/core/device_digital_twins/illumination_geometries/rectangle_illumination.py +++ b/simpa/core/device_digital_twins/illumination_geometries/rectangle_illumination.py @@ -86,8 +86,9 @@ def get_mcx_illuminator_definition(self, global_settings: Settings) -> dict: self.focal_length_in_mm, str) else self.focal_length_in_mm / spacing source_direction.append(param4) - source_param1 = [self.width_mm / spacing + 1, 0., 0.] - source_param2 = [0., self.length_mm / spacing + 1, 0.] + source_param1 = [self.width_mm / spacing + 2, 0., 0.] + # Legit ka warum +2 but only then the whole area is illuminated correctly + source_param2 = [0., self.length_mm / spacing + 2, 0.] # If Pos=[10, 12, 0], Param1=[10, 0, 0], Param2=[0, 20, 0], # then illumination covers: x in [10, 20], y in [12, 32] diff --git a/simpa/core/simulation_modules/optical_module/mcx_reflectance_adapter.py b/simpa/core/simulation_modules/optical_module/mcx_reflectance_adapter.py index da66805f..3ae140dd 100644 --- a/simpa/core/simulation_modules/optical_module/mcx_reflectance_adapter.py +++ b/simpa/core/simulation_modules/optical_module/mcx_reflectance_adapter.py @@ -159,7 +159,7 @@ def get_command(self) -> typing.List: cmd.append("-F") cmd.append("jnii") cmd.append("-e") - cmd.append(str(1e-3)) + cmd.append(str(1e-2)) cmd.append("--bc") cmd.append(self.volume_boundary_condition_str) cmd.append("-H")