simind-python-connector 1.1.0__tar.gz → 1.1.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {simind_python_connector-1.1.0/simind_python_connector.egg-info → simind_python_connector-1.1.1}/PKG-INFO +1 -1
- {simind_python_connector-1.1.0 → simind_python_connector-1.1.1}/docs/backends.rst +1 -1
- {simind_python_connector-1.1.0 → simind_python_connector-1.1.1}/docs/changelog.rst +56 -0
- {simind_python_connector-1.1.0 → simind_python_connector-1.1.1}/docs/conf.py +1 -1
- {simind_python_connector-1.1.0 → simind_python_connector-1.1.1}/docs/examples/07C_pytomography_adaptor_osem.py +7 -15
- {simind_python_connector-1.1.0 → simind_python_connector-1.1.1/docs}/examples/_python_connector_helpers.py +3 -2
- {simind_python_connector-1.1.0 → simind_python_connector-1.1.1}/docs/geometry.rst +6 -7
- {simind_python_connector-1.1.0 → simind_python_connector-1.1.1}/examples/07C_pytomography_adaptor_osem.py +7 -15
- {simind_python_connector-1.1.0/docs → simind_python_connector-1.1.1}/examples/_python_connector_helpers.py +3 -2
- {simind_python_connector-1.1.0 → simind_python_connector-1.1.1}/pyproject.toml +2 -2
- {simind_python_connector-1.1.0 → simind_python_connector-1.1.1}/simind_python_connector/__init__.py +1 -1
- {simind_python_connector-1.1.0 → simind_python_connector-1.1.1}/simind_python_connector/backends/sirf_backend.py +3 -3
- {simind_python_connector-1.1.0 → simind_python_connector-1.1.1}/simind_python_connector/builders/acquisition_builder.py +51 -24
- {simind_python_connector-1.1.0 → simind_python_connector-1.1.1}/simind_python_connector/builders/image_builder.py +3 -0
- simind_python_connector-1.1.1/simind_python_connector/connectors/_spacing.py +83 -0
- {simind_python_connector-1.1.0 → simind_python_connector-1.1.1}/simind_python_connector/connectors/python_connector.py +30 -24
- {simind_python_connector-1.1.0 → simind_python_connector-1.1.1}/simind_python_connector/connectors/pytomography_adaptor.py +17 -24
- {simind_python_connector-1.1.0 → simind_python_connector-1.1.1}/simind_python_connector/connectors/sirf_adaptor.py +12 -2
- {simind_python_connector-1.1.0 → simind_python_connector-1.1.1}/simind_python_connector/connectors/stir_adaptor.py +12 -2
- {simind_python_connector-1.1.0 → simind_python_connector-1.1.1}/simind_python_connector/converters/attenuation.py +12 -14
- {simind_python_connector-1.1.0 → simind_python_connector-1.1.1}/simind_python_connector/converters/simind_to_stir.py +26 -17
- {simind_python_connector-1.1.0 → simind_python_connector-1.1.1}/simind_python_connector/core/config.py +41 -0
- {simind_python_connector-1.1.0 → simind_python_connector-1.1.1}/simind_python_connector/core/executor.py +13 -2
- {simind_python_connector-1.1.0 → simind_python_connector-1.1.1}/simind_python_connector/utils/interfile_parser.py +11 -2
- {simind_python_connector-1.1.0 → simind_python_connector-1.1.1}/simind_python_connector/utils/stir_utils.py +4 -4
- {simind_python_connector-1.1.0 → simind_python_connector-1.1.1/simind_python_connector.egg-info}/PKG-INFO +1 -1
- {simind_python_connector-1.1.0 → simind_python_connector-1.1.1}/tests/test_acquisition_builder_unit.py +35 -6
- {simind_python_connector-1.1.0 → simind_python_connector-1.1.1}/tests/test_attenuation_utils.py +14 -4
- {simind_python_connector-1.1.0 → simind_python_connector-1.1.1}/tests/test_backends.py +31 -0
- {simind_python_connector-1.1.0 → simind_python_connector-1.1.1}/tests/test_dicom_builder.py +16 -3
- {simind_python_connector-1.1.0 → simind_python_connector-1.1.1}/tests/test_example_config_profile.py +26 -0
- {simind_python_connector-1.1.0 → simind_python_connector-1.1.1}/tests/test_executor.py +33 -4
- {simind_python_connector-1.1.0 → simind_python_connector-1.1.1}/tests/test_image_builder_unit.py +30 -0
- {simind_python_connector-1.1.0 → simind_python_connector-1.1.1}/tests/test_interfile_parser.py +19 -0
- {simind_python_connector-1.1.0 → simind_python_connector-1.1.1}/tests/test_native_adaptors.py +125 -93
- {simind_python_connector-1.1.0 → simind_python_connector-1.1.1}/tests/test_python_connector.py +69 -9
- {simind_python_connector-1.1.0 → simind_python_connector-1.1.1}/tests/test_pytomography_adaptor.py +123 -8
- {simind_python_connector-1.1.0 → simind_python_connector-1.1.1}/tests/test_schneider_density.py +5 -0
- {simind_python_connector-1.1.0 → simind_python_connector-1.1.1}/tests/test_simind_to_stir_converter.py +41 -0
- {simind_python_connector-1.1.0 → simind_python_connector-1.1.1}/tests/test_simulation_config.py +13 -0
- {simind_python_connector-1.1.0 → simind_python_connector-1.1.1}/tests/test_stir_utils_backend_choice_unit.py +1 -1
- {simind_python_connector-1.1.0 → simind_python_connector-1.1.1}/tests/test_utils.py +3 -8
- simind_python_connector-1.1.0/simind_python_connector/connectors/_spacing.py +0 -75
- {simind_python_connector-1.1.0 → simind_python_connector-1.1.1}/LICENSE +0 -0
- {simind_python_connector-1.1.0 → simind_python_connector-1.1.1}/MANIFEST.in +0 -0
- {simind_python_connector-1.1.0 → simind_python_connector-1.1.1}/README.md +0 -0
- {simind_python_connector-1.1.0 → simind_python_connector-1.1.1}/docs/api.rst +0 -0
- {simind_python_connector-1.1.0 → simind_python_connector-1.1.1}/docs/connector_modernization_plan.rst +0 -0
- {simind_python_connector-1.1.0 → simind_python_connector-1.1.1}/docs/contributing.rst +0 -0
- {simind_python_connector-1.1.0 → simind_python_connector-1.1.1}/docs/examples/01_basic_simulation.py +0 -0
- {simind_python_connector-1.1.0 → simind_python_connector-1.1.1}/docs/examples/02_runtime_switch_comparison.py +0 -0
- {simind_python_connector-1.1.0 → simind_python_connector-1.1.1}/docs/examples/03_multi_window.py +0 -0
- {simind_python_connector-1.1.0 → simind_python_connector-1.1.1}/docs/examples/04_custom_config.py +0 -0
- {simind_python_connector-1.1.0 → simind_python_connector-1.1.1}/docs/examples/05_scattwin_vs_penetrate_comparison.py +0 -0
- {simind_python_connector-1.1.0 → simind_python_connector-1.1.1}/docs/examples/06_schneider_density_conversion.py +0 -0
- {simind_python_connector-1.1.0 → simind_python_connector-1.1.1}/docs/examples/07A_stir_adaptor_osem.py +0 -0
- {simind_python_connector-1.1.0 → simind_python_connector-1.1.1}/docs/examples/07B_sirf_adaptor_osem.py +0 -0
- {simind_python_connector-1.1.0 → simind_python_connector-1.1.1}/docs/examples/08A_stir_adaptor_from_dicom.py +0 -0
- {simind_python_connector-1.1.0 → simind_python_connector-1.1.1}/docs/examples/08B_sirf_adaptor_from_dicom.py +0 -0
- {simind_python_connector-1.1.0 → simind_python_connector-1.1.1}/docs/examples/08C_pytomography_adaptor_from_dicom.py +0 -0
- {simind_python_connector-1.1.0 → simind_python_connector-1.1.1}/docs/examples.rst +0 -0
- {simind_python_connector-1.1.0 → simind_python_connector-1.1.1}/docs/index.rst +0 -0
- {simind_python_connector-1.1.0 → simind_python_connector-1.1.1}/docs/installation.rst +0 -0
- {simind_python_connector-1.1.0 → simind_python_connector-1.1.1}/docs/intro.rst +0 -0
- {simind_python_connector-1.1.0 → simind_python_connector-1.1.1}/docs/testing.rst +0 -0
- {simind_python_connector-1.1.0 → simind_python_connector-1.1.1}/docs/usage.rst +0 -0
- {simind_python_connector-1.1.0 → simind_python_connector-1.1.1}/examples/01_basic_simulation.py +0 -0
- {simind_python_connector-1.1.0 → simind_python_connector-1.1.1}/examples/02_runtime_switch_comparison.py +0 -0
- {simind_python_connector-1.1.0 → simind_python_connector-1.1.1}/examples/03_multi_window.py +0 -0
- {simind_python_connector-1.1.0 → simind_python_connector-1.1.1}/examples/04_custom_config.py +0 -0
- {simind_python_connector-1.1.0 → simind_python_connector-1.1.1}/examples/05_scattwin_vs_penetrate_comparison.py +0 -0
- {simind_python_connector-1.1.0 → simind_python_connector-1.1.1}/examples/06_schneider_density_conversion.py +0 -0
- {simind_python_connector-1.1.0 → simind_python_connector-1.1.1}/examples/07A_stir_adaptor_osem.py +0 -0
- {simind_python_connector-1.1.0 → simind_python_connector-1.1.1}/examples/07B_sirf_adaptor_osem.py +0 -0
- {simind_python_connector-1.1.0 → simind_python_connector-1.1.1}/examples/08A_stir_adaptor_from_dicom.py +0 -0
- {simind_python_connector-1.1.0 → simind_python_connector-1.1.1}/examples/08B_sirf_adaptor_from_dicom.py +0 -0
- {simind_python_connector-1.1.0 → simind_python_connector-1.1.1}/examples/08C_pytomography_adaptor_from_dicom.py +0 -0
- {simind_python_connector-1.1.0 → simind_python_connector-1.1.1}/requirements-dev.txt +0 -0
- {simind_python_connector-1.1.0 → simind_python_connector-1.1.1}/requirements.txt +0 -0
- {simind_python_connector-1.1.0 → simind_python_connector-1.1.1}/setup.cfg +0 -0
- {simind_python_connector-1.1.0 → simind_python_connector-1.1.1}/simind_python_connector/backends/__init__.py +0 -0
- {simind_python_connector-1.1.0 → simind_python_connector-1.1.1}/simind_python_connector/backends/base.py +0 -0
- {simind_python_connector-1.1.0 → simind_python_connector-1.1.1}/simind_python_connector/backends/stir_backend.py +0 -0
- {simind_python_connector-1.1.0 → simind_python_connector-1.1.1}/simind_python_connector/builders/__init__.py +0 -0
- {simind_python_connector-1.1.0 → simind_python_connector-1.1.1}/simind_python_connector/configs/AnyScan.yaml +0 -0
- {simind_python_connector-1.1.0 → simind_python_connector-1.1.1}/simind_python_connector/configs/Discovery670.yaml +0 -0
- {simind_python_connector-1.1.0 → simind_python_connector-1.1.1}/simind_python_connector/configs/Example.yaml +0 -0
- {simind_python_connector-1.1.0 → simind_python_connector-1.1.1}/simind_python_connector/configs/MLD001_SCAN0.yaml +0 -0
- {simind_python_connector-1.1.0 → simind_python_connector-1.1.1}/simind_python_connector/configs/__init__.py +0 -0
- {simind_python_connector-1.1.0 → simind_python_connector-1.1.1}/simind_python_connector/configs/input.smc +0 -0
- {simind_python_connector-1.1.0 → simind_python_connector-1.1.1}/simind_python_connector/connectors/__init__.py +0 -0
- {simind_python_connector-1.1.0 → simind_python_connector-1.1.1}/simind_python_connector/connectors/base.py +0 -0
- {simind_python_connector-1.1.0 → simind_python_connector-1.1.1}/simind_python_connector/converters/__init__.py +0 -0
- {simind_python_connector-1.1.0 → simind_python_connector-1.1.1}/simind_python_connector/converters/dicom_to_stir.py +0 -0
- {simind_python_connector-1.1.0 → simind_python_connector-1.1.1}/simind_python_connector/core/__init__.py +0 -0
- {simind_python_connector-1.1.0 → simind_python_connector-1.1.1}/simind_python_connector/core/types.py +0 -0
- {simind_python_connector-1.1.0 → simind_python_connector-1.1.1}/simind_python_connector/data/Schneider2000.json +0 -0
- {simind_python_connector-1.1.0 → simind_python_connector-1.1.1}/simind_python_connector/data/__init__.py +0 -0
- {simind_python_connector-1.1.0 → simind_python_connector-1.1.1}/simind_python_connector/data/bone.atn +0 -0
- {simind_python_connector-1.1.0 → simind_python_connector-1.1.1}/simind_python_connector/data/h2o.atn +0 -0
- {simind_python_connector-1.1.0 → simind_python_connector-1.1.1}/simind_python_connector/utils/__init__.py +0 -0
- {simind_python_connector-1.1.0 → simind_python_connector-1.1.1}/simind_python_connector/utils/backend_access.py +0 -0
- {simind_python_connector-1.1.0 → simind_python_connector-1.1.1}/simind_python_connector/utils/import_helpers.py +0 -0
- {simind_python_connector-1.1.0 → simind_python_connector-1.1.1}/simind_python_connector/utils/interfile_numpy.py +0 -0
- {simind_python_connector-1.1.0 → simind_python_connector-1.1.1}/simind_python_connector/utils/io_utils.py +0 -0
- {simind_python_connector-1.1.0 → simind_python_connector-1.1.1}/simind_python_connector/utils/simind_utils.py +0 -0
- {simind_python_connector-1.1.0 → simind_python_connector-1.1.1}/simind_python_connector/utils/sirf_stir_utils.py +0 -0
- {simind_python_connector-1.1.0 → simind_python_connector-1.1.1}/simind_python_connector.egg-info/SOURCES.txt +0 -0
- {simind_python_connector-1.1.0 → simind_python_connector-1.1.1}/simind_python_connector.egg-info/dependency_links.txt +0 -0
- {simind_python_connector-1.1.0 → simind_python_connector-1.1.1}/simind_python_connector.egg-info/requires.txt +0 -0
- {simind_python_connector-1.1.0 → simind_python_connector-1.1.1}/simind_python_connector.egg-info/top_level.txt +0 -0
- {simind_python_connector-1.1.0 → simind_python_connector-1.1.1}/tests/__init__.py +0 -0
- {simind_python_connector-1.1.0 → simind_python_connector-1.1.1}/tests/conftest.py +0 -0
- {simind_python_connector-1.1.0 → simind_python_connector-1.1.1}/tests/test_arithmetic_operations.py +0 -0
- {simind_python_connector-1.1.0 → simind_python_connector-1.1.1}/tests/test_connector_backend_separation.py +0 -0
- {simind_python_connector-1.1.0 → simind_python_connector-1.1.1}/tests/test_connectors_base.py +0 -0
- {simind_python_connector-1.1.0 → simind_python_connector-1.1.1}/tests/test_container_library_isolation.py +0 -0
- {simind_python_connector-1.1.0 → simind_python_connector-1.1.1}/tests/test_container_scripts.py +0 -0
- {simind_python_connector-1.1.0 → simind_python_connector-1.1.1}/tests/test_examples_backend_separation.py +0 -0
- {simind_python_connector-1.1.0 → simind_python_connector-1.1.1}/tests/test_geometry_isolation_forward_projection.py +0 -0
- {simind_python_connector-1.1.0 → simind_python_connector-1.1.1}/tests/test_integration.py +0 -0
- {simind_python_connector-1.1.0 → simind_python_connector-1.1.1}/tests/test_interfile_numpy.py +0 -0
- {simind_python_connector-1.1.0 → simind_python_connector-1.1.1}/tests/test_legacy_surface_policy.py +0 -0
- {simind_python_connector-1.1.0 → simind_python_connector-1.1.1}/tests/test_marker_policy.py +0 -0
- {simind_python_connector-1.1.0 → simind_python_connector-1.1.1}/tests/test_non_square_slices_simind.py +0 -0
- {simind_python_connector-1.1.0 → simind_python_connector-1.1.1}/tests/test_osem_geometry_diagnostics.py +0 -0
- {simind_python_connector-1.1.0 → simind_python_connector-1.1.1}/tests/test_package_identity.py +0 -0
- {simind_python_connector-1.1.0 → simind_python_connector-1.1.1}/tests/test_packaging_resources.py +0 -0
- {simind_python_connector-1.1.0 → simind_python_connector-1.1.1}/tests/test_sirf_stir_utils.py +0 -0
- {simind_python_connector-1.1.0 → simind_python_connector-1.1.1}/tests/test_utils_small.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: simind-python-connector
|
|
3
|
-
Version: 1.1.
|
|
3
|
+
Version: 1.1.1
|
|
4
4
|
Summary: Python SIMIND Monte Carlo connector with STIR/SIRF/PyTomography adaptors
|
|
5
5
|
Author-email: Sam Porter <sam.porter.18@ucl.ac.uk>, Rebecca Gillen <rebecca.gillen.18@ucl.ac.uk>, "Efstathios (Stahis) Varzakis" <efstathios.varzakis.22@ucl.ac.uk>
|
|
6
6
|
Maintainer-email: Sam Porter <sam.porter.18@ucl.ac.uk>
|
|
@@ -16,7 +16,7 @@ Dependency Matrix
|
|
|
16
16
|
* - ``SimindPythonConnector``
|
|
17
17
|
- SIMIND executable on ``PATH`` (no SIRF/STIR/PyTomography requirement)
|
|
18
18
|
* - ``StirSimindAdaptor``
|
|
19
|
-
- STIR Python (``stir``)
|
|
19
|
+
- STIR Python (``stir`` and ``stirextra``)
|
|
20
20
|
* - ``SirfSimindAdaptor``
|
|
21
21
|
- SIRF (``sirf.STIR``)
|
|
22
22
|
* - ``PyTomographySimindAdaptor``
|
|
@@ -7,6 +7,62 @@ All notable changes to this project are documented in this file.
|
|
|
7
7
|
|
|
8
8
|
The format is based on `Keep a Changelog <https://keepachangelog.com/en/1.0.0/>`_, and this project adheres to `Semantic Versioning <https://semver.org/spec/v2.0.0.html>`_.
|
|
9
9
|
|
|
10
|
+
1.1.1 - 2026-10-11
|
|
11
|
+
------------------
|
|
12
|
+
|
|
13
|
+
Fixed
|
|
14
|
+
~~~~~
|
|
15
|
+
|
|
16
|
+
- A negative Index 1 (energies from the isotope file) turned the mu map into
|
|
17
|
+
near-vacuum; it is now converted at ``abs(Index 1)``.
|
|
18
|
+
- Acquisition arrays use ``(tof, axial, view, bin)``, and DICOM matrix sizes and
|
|
19
|
+
pixel spacing are no longer swapped, so non-square projections build
|
|
20
|
+
correctly. ``create_stir_acqdata`` takes ``(bins, axial)`` and uses the same
|
|
21
|
+
layout.
|
|
22
|
+
- The PyTomography adaptor reverses axial slices to match PyTomography's SIMIND
|
|
23
|
+
reader; reconstructions were flipped along z.
|
|
24
|
+
- The STIR/SIRF adaptors raise a ``ValueError`` for non-cubic voxels instead of
|
|
25
|
+
using the z spacing for every axis.
|
|
26
|
+
- The adaptors default ``scoring_routine`` to None, which keeps the
|
|
27
|
+
configuration's Index 84; they used to overwrite it with SCATTWIN.
|
|
28
|
+
- The geometry docs give the projection axis orders correctly.
|
|
29
|
+
- PyTomography PENETRATE and fallback outputs use the reader's
|
|
30
|
+
``(view, bin, axial)`` order, and PyTomography scatter orders are validated
|
|
31
|
+
like the core's.
|
|
32
|
+
- The STIR/SIRF adaptors reject a mu map whose voxel size differs from the
|
|
33
|
+
source's.
|
|
34
|
+
- The legacy SIRF attenuation loader reverses the voxel sizes with the
|
|
35
|
+
dimensions.
|
|
36
|
+
- The SIRF wrapper's ``maximum()`` and ``sapyb()`` update the wrapped object.
|
|
37
|
+
- Piecewise Schneider conversion gives HU outside the table the nearest
|
|
38
|
+
segment's density, so metal above 4001 HU no longer becomes air.
|
|
39
|
+
- MPI runs use ``$SIMIND_MPI_BIN``, else ``simind_mpi`` next to the serial
|
|
40
|
+
binary, with ``/MP`` and without a stray ``-p``.
|
|
41
|
+
- An output header that cannot be read raises instead of being skipped.
|
|
42
|
+
- YAML export keeps unnamed parameters and flags.
|
|
43
|
+
- New header keys are written before ``!END OF INTERFILE``, and ``set()``
|
|
44
|
+
changes the last duplicate key, the one readers use.
|
|
45
|
+
- A header converted into another directory points at its data by absolute
|
|
46
|
+
path, and an orbit file named like an ignored key keeps its radii.
|
|
47
|
+
- DICOM frames are split by ``EnergyWindowVector`` when present; each output
|
|
48
|
+
declares one window and its share of the duration.
|
|
49
|
+
- ``run()`` sets Flags 5 and 14, which SIMIND needs to write readable headers,
|
|
50
|
+
so config-defined phantom runs work.
|
|
51
|
+
- Example 07C no longer drops attenuation and PSF modelling silently, and the
|
|
52
|
+
examples accept ``SIMIND_BIN``.
|
|
53
|
+
- Importing the converter no longer configures root logging.
|
|
54
|
+
- setuptools 77.0.3 or newer is required to build.
|
|
55
|
+
|
|
56
|
+
Changed
|
|
57
|
+
~~~~~~~
|
|
58
|
+
|
|
59
|
+
- ``get_attenuation_coefficient`` takes keV, as documented.
|
|
60
|
+
- ``NN`` must be an integer >= 1, switches reject None and bool values,
|
|
61
|
+
``quantization_scale`` must be in (0, 1], and ``output_prefix`` must match
|
|
62
|
+
``[a-z0-9][a-z0-9_-]*`` (SIMIND lowercases file names).
|
|
63
|
+
- Publishing waits for the tests and checks the tag against the version; tests
|
|
64
|
+
also run on ``release/**`` branches.
|
|
65
|
+
|
|
10
66
|
1.1.0 - 2026-10-11
|
|
11
67
|
------------------
|
|
12
68
|
|
|
@@ -13,7 +13,7 @@ sys.path.insert(0, os.path.abspath(".."))
|
|
|
13
13
|
project = "simind-python-connector"
|
|
14
14
|
copyright = f"{datetime.now().year}, Sam Porter, Efstathios Varzakis"
|
|
15
15
|
author = "Sam Porter, Efstathios Varzakis"
|
|
16
|
-
release = "1.1.
|
|
16
|
+
release = "1.1.1"
|
|
17
17
|
for dist_name in ("simind-python-connector",):
|
|
18
18
|
try:
|
|
19
19
|
release = importlib_metadata.version(dist_name)
|
|
@@ -90,21 +90,13 @@ def _build_system_matrix(
|
|
|
90
90
|
) -> SPECTSystemMatrix:
|
|
91
91
|
object_meta, proj_meta = pytomo_simind.get_metadata(str(projection_header))
|
|
92
92
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
except Exception:
|
|
101
|
-
pass
|
|
102
|
-
|
|
103
|
-
try:
|
|
104
|
-
psf_meta = pytomo_simind.get_psfmeta_from_header(str(projection_header))
|
|
105
|
-
obj2obj_transforms.append(SPECTPSFTransform(psf_meta=psf_meta))
|
|
106
|
-
except Exception:
|
|
107
|
-
pass
|
|
93
|
+
psf_meta = pytomo_simind.get_psfmeta_from_header(str(projection_header))
|
|
94
|
+
obj2obj_transforms = [
|
|
95
|
+
SPECTAttenuationTransform(
|
|
96
|
+
attenuation_map=attenuation_map_xyz.to(dtype=torch.float32).contiguous()
|
|
97
|
+
),
|
|
98
|
+
SPECTPSFTransform(psf_meta=psf_meta),
|
|
99
|
+
]
|
|
108
100
|
|
|
109
101
|
return SPECTSystemMatrix(
|
|
110
102
|
obj2obj_transforms=obj2obj_transforms,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import os
|
|
3
4
|
import shutil
|
|
4
5
|
|
|
5
6
|
import numpy as np
|
|
@@ -8,9 +9,9 @@ from simind_python_connector import SimindPythonConnector
|
|
|
8
9
|
|
|
9
10
|
|
|
10
11
|
def require_simind() -> None:
|
|
11
|
-
if shutil.which("simind") is None:
|
|
12
|
+
if shutil.which(os.environ.get("SIMIND_BIN") or "simind") is None:
|
|
12
13
|
raise RuntimeError(
|
|
13
|
-
"SIMIND executable not found
|
|
14
|
+
"SIMIND executable not found ($SIMIND_BIN or simind on PATH). "
|
|
14
15
|
"Install SIMIND before running this example."
|
|
15
16
|
)
|
|
16
17
|
|
|
@@ -9,9 +9,9 @@ At-a-Glance Axis Conventions
|
|
|
9
9
|
|
|
10
10
|
- STIR/SIRF image arrays are handled as ``(z, y, x)``.
|
|
11
11
|
- STIR/SIRF projection arrays are often 4D with a singleton TOF axis:
|
|
12
|
-
``(tof,
|
|
12
|
+
``(tof, axial, view, bin)``. For these, **view 0** is ``arr[0, :, 0, :]``.
|
|
13
13
|
- Raw Interfile projection loading in ``interfile_numpy`` is exposed as
|
|
14
|
-
``(
|
|
14
|
+
``(view, axial, bin)`` when no singleton TOF axis is present.
|
|
15
15
|
- ``PyTomographySimindAdaptor`` public object-space tensors are ``(x, y, z)``.
|
|
16
16
|
- PyTomography object space is ``(x, y, z)`` (``Lx, Ly, Lz``).
|
|
17
17
|
- PyTomography SIMIND projections from ``pytomography.io.SPECT.simind`` are
|
|
@@ -35,11 +35,10 @@ Units
|
|
|
35
35
|
- STIR/SIRF image geometry is typically expressed in **mm**.
|
|
36
36
|
- Connectors handle conversion internally via voxel-size settings (for example,
|
|
37
37
|
runtime switch ``PX`` is set in cm for SIMIND).
|
|
38
|
-
- STIR/SIRF
|
|
39
|
-
report ``voxel_sizes()`` as
|
|
40
|
-
|
|
41
|
-
layout ``(unused, z, y, x)
|
|
42
|
-
``z`` accessor use that accessor directly.
|
|
38
|
+
- STIR/SIRF adaptors read the voxel spacing in mm and need cubic voxels; other
|
|
39
|
+
images raise a ``ValueError``. Images report ``voxel_sizes()`` as
|
|
40
|
+
``(z, y, x)``, and raw ``get_grid_spacing()`` sequences follow the STIR
|
|
41
|
+
coordinate layout ``(unused, z, y, x)``.
|
|
43
42
|
|
|
44
43
|
Projection Size
|
|
45
44
|
---------------
|
|
@@ -90,21 +90,13 @@ def _build_system_matrix(
|
|
|
90
90
|
) -> SPECTSystemMatrix:
|
|
91
91
|
object_meta, proj_meta = pytomo_simind.get_metadata(str(projection_header))
|
|
92
92
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
except Exception:
|
|
101
|
-
pass
|
|
102
|
-
|
|
103
|
-
try:
|
|
104
|
-
psf_meta = pytomo_simind.get_psfmeta_from_header(str(projection_header))
|
|
105
|
-
obj2obj_transforms.append(SPECTPSFTransform(psf_meta=psf_meta))
|
|
106
|
-
except Exception:
|
|
107
|
-
pass
|
|
93
|
+
psf_meta = pytomo_simind.get_psfmeta_from_header(str(projection_header))
|
|
94
|
+
obj2obj_transforms = [
|
|
95
|
+
SPECTAttenuationTransform(
|
|
96
|
+
attenuation_map=attenuation_map_xyz.to(dtype=torch.float32).contiguous()
|
|
97
|
+
),
|
|
98
|
+
SPECTPSFTransform(psf_meta=psf_meta),
|
|
99
|
+
]
|
|
108
100
|
|
|
109
101
|
return SPECTSystemMatrix(
|
|
110
102
|
obj2obj_transforms=obj2obj_transforms,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import os
|
|
3
4
|
import shutil
|
|
4
5
|
|
|
5
6
|
import numpy as np
|
|
@@ -8,9 +9,9 @@ from simind_python_connector import SimindPythonConnector
|
|
|
8
9
|
|
|
9
10
|
|
|
10
11
|
def require_simind() -> None:
|
|
11
|
-
if shutil.which("simind") is None:
|
|
12
|
+
if shutil.which(os.environ.get("SIMIND_BIN") or "simind") is None:
|
|
12
13
|
raise RuntimeError(
|
|
13
|
-
"SIMIND executable not found
|
|
14
|
+
"SIMIND executable not found ($SIMIND_BIN or simind on PATH). "
|
|
14
15
|
"Install SIMIND before running this example."
|
|
15
16
|
)
|
|
16
17
|
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
[build-system]
|
|
2
|
-
requires = ["setuptools>=
|
|
2
|
+
requires = ["setuptools>=77.0.3", "wheel"]
|
|
3
3
|
build-backend = "setuptools.build_meta"
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "simind-python-connector"
|
|
7
|
-
version = "1.1.
|
|
7
|
+
version = "1.1.1"
|
|
8
8
|
description = "Python SIMIND Monte Carlo connector with STIR/SIRF/PyTomography adaptors"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.9"
|
|
@@ -88,7 +88,7 @@ class SirfImageData(ImageDataInterface):
|
|
|
88
88
|
|
|
89
89
|
def maximum(self, value: float) -> None:
|
|
90
90
|
"""Apply element-wise maximum."""
|
|
91
|
-
self._obj.maximum(value)
|
|
91
|
+
self._obj.maximum(value, out=self._obj)
|
|
92
92
|
|
|
93
93
|
@property
|
|
94
94
|
def native_object(self):
|
|
@@ -150,7 +150,7 @@ class SirfImageData(ImageDataInterface):
|
|
|
150
150
|
SirfImageData: self after modification (for chaining)
|
|
151
151
|
"""
|
|
152
152
|
y_obj = y._obj if isinstance(y, SirfImageData) else y
|
|
153
|
-
self._obj.sapyb(a, y_obj, b)
|
|
153
|
+
self._obj.sapyb(a, y_obj, b, out=self._obj)
|
|
154
154
|
return self
|
|
155
155
|
|
|
156
156
|
|
|
@@ -305,5 +305,5 @@ class SirfAcquisitionData(AcquisitionDataInterface):
|
|
|
305
305
|
SirfAcquisitionData: self after modification (for chaining)
|
|
306
306
|
"""
|
|
307
307
|
y_obj = y._obj if isinstance(y, SirfAcquisitionData) else y
|
|
308
|
-
self._obj.sapyb(a, y_obj, b)
|
|
308
|
+
self._obj.sapyb(a, y_obj, b, out=self._obj)
|
|
309
309
|
return self
|
|
@@ -92,22 +92,22 @@ class STIRSPECTAcquisitionDataBuilder:
|
|
|
92
92
|
num_projections = int(self.header.get("!number of projections", 1))
|
|
93
93
|
if self.pixel_array is None:
|
|
94
94
|
self.pixel_array = np.zeros(
|
|
95
|
-
(1,
|
|
95
|
+
(1, matrix_size_2, num_projections, matrix_size_1), dtype=np.float32
|
|
96
96
|
)
|
|
97
97
|
else:
|
|
98
98
|
self.pixel_array = np.array(self.pixel_array, dtype=np.float32)
|
|
99
99
|
|
|
100
100
|
expected_shape = (
|
|
101
101
|
1,
|
|
102
|
-
matrix_size_1,
|
|
103
|
-
num_projections,
|
|
104
102
|
matrix_size_2,
|
|
103
|
+
num_projections,
|
|
104
|
+
matrix_size_1,
|
|
105
105
|
)
|
|
106
106
|
if self.pixel_array.shape != expected_shape:
|
|
107
107
|
raise ValueError(
|
|
108
108
|
f"pixel array shape {self.pixel_array.shape} does not match "
|
|
109
109
|
f"header dimensions {expected_shape} "
|
|
110
|
-
"(tof,
|
|
110
|
+
"(tof, axial, view, bin)"
|
|
111
111
|
)
|
|
112
112
|
|
|
113
113
|
def _write(base_path: Path, cleanup: bool) -> AcquisitionData:
|
|
@@ -115,6 +115,8 @@ class STIRSPECTAcquisitionDataBuilder:
|
|
|
115
115
|
raw_file_path = base_path.with_suffix(".s")
|
|
116
116
|
|
|
117
117
|
self.header["name of data file"] = raw_file_path.name
|
|
118
|
+
# Keys added since an earlier build must come before the terminator.
|
|
119
|
+
self.header.pop("!END OF INTERFILE", None)
|
|
118
120
|
self.header["!END OF INTERFILE"] = ""
|
|
119
121
|
|
|
120
122
|
with open(header_path, "w") as f:
|
|
@@ -221,35 +223,54 @@ class STIRSPECTAcquisitionDataBuilder:
|
|
|
221
223
|
f"pixel array shape {self.pixel_array.shape} does not match "
|
|
222
224
|
f"declared {total_projections} projections"
|
|
223
225
|
)
|
|
224
|
-
if total_projections % num_windows != 0:
|
|
225
|
-
raise ValueError(
|
|
226
|
-
f"Number of projections ({total_projections}) must be divisible "
|
|
227
|
-
f"by the number of energy windows ({num_windows})"
|
|
228
|
-
)
|
|
229
|
-
projections_per_window = total_projections // num_windows
|
|
230
226
|
|
|
231
|
-
|
|
232
|
-
|
|
227
|
+
window_vector = getattr(self, "energy_window_vector", None)
|
|
228
|
+
if window_vector is not None:
|
|
229
|
+
window_vector = np.asarray(window_vector)
|
|
230
|
+
if len(window_vector) != total_projections:
|
|
231
|
+
raise ValueError(
|
|
232
|
+
f"EnergyWindowVector has {len(window_vector)} entries for "
|
|
233
|
+
f"{total_projections} frames"
|
|
234
|
+
)
|
|
235
|
+
frames_per_window = [
|
|
236
|
+
np.flatnonzero(window_vector == idx + 1) for idx in range(num_windows)
|
|
237
|
+
]
|
|
238
|
+
else:
|
|
239
|
+
if total_projections % num_windows != 0:
|
|
240
|
+
raise ValueError(
|
|
241
|
+
f"Number of projections ({total_projections}) must be divisible "
|
|
242
|
+
f"by the number of energy windows ({num_windows})"
|
|
243
|
+
)
|
|
244
|
+
# Without a window vector, assume one contiguous block per window.
|
|
245
|
+
frames_per_window = np.array_split(
|
|
246
|
+
np.arange(total_projections), num_windows
|
|
247
|
+
)
|
|
233
248
|
|
|
234
|
-
# split pixel_array into energy windows along 3rd axis
|
|
235
249
|
original_pixel_array = self.pixel_array
|
|
250
|
+
original_header = dict(self.header)
|
|
251
|
+
duration = original_header.get("!image duration (sec)[1]")
|
|
236
252
|
try:
|
|
237
|
-
pixel_array_list = np.array_split(original_pixel_array, num_windows, axis=2)
|
|
238
|
-
|
|
239
253
|
acqdata_list = []
|
|
240
254
|
for idx, ew in enumerate(self.energy_windows):
|
|
241
|
-
|
|
255
|
+
frames = frames_per_window[idx]
|
|
256
|
+
self.header["!number of projections"] = str(len(frames))
|
|
257
|
+
self.header["!number of energy windows"] = "1"
|
|
242
258
|
self.header["energy window lower level[1]"] = ew["lower"]
|
|
243
259
|
self.header["energy window upper level[1]"] = ew["upper"]
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
260
|
+
if duration is not None:
|
|
261
|
+
# Windows are acquired simultaneously, so each output
|
|
262
|
+
# covers only its own frames.
|
|
263
|
+
self.header["!image duration (sec)[1]"] = (
|
|
264
|
+
f"{float(duration) * len(frames) / total_projections:g}"
|
|
265
|
+
)
|
|
266
|
+
output_path = output_path_base + f"_ew{idx + 1}"
|
|
267
|
+
self.pixel_array = original_pixel_array[:, :, frames, :]
|
|
247
268
|
acqdata = self.build(output_path=output_path)
|
|
248
269
|
acqdata_list.append(acqdata)
|
|
249
270
|
return acqdata_list
|
|
250
271
|
finally:
|
|
251
272
|
self.pixel_array = original_pixel_array
|
|
252
|
-
self.header
|
|
273
|
+
self.header = original_header
|
|
253
274
|
|
|
254
275
|
def update_header_from_dicom(self, dicom_filepath):
|
|
255
276
|
"""
|
|
@@ -273,16 +294,18 @@ class STIRSPECTAcquisitionDataBuilder:
|
|
|
273
294
|
"Modality not found in DICOM. Retaining default '!imaging modality'."
|
|
274
295
|
)
|
|
275
296
|
try:
|
|
276
|
-
self.header["!matrix size [1]"] = str(ds.
|
|
277
|
-
self.header["!matrix size [2]"] = str(ds.
|
|
297
|
+
self.header["!matrix size [1]"] = str(ds.Columns)
|
|
298
|
+
self.header["!matrix size [2]"] = str(ds.Rows)
|
|
278
299
|
except AttributeError:
|
|
279
300
|
warnings.warn(
|
|
280
301
|
"Rows/Columns not found in DICOM. Retaining default matrix sizes."
|
|
281
302
|
)
|
|
282
303
|
try:
|
|
283
304
|
pixel_spacing = ds.PixelSpacing
|
|
284
|
-
|
|
285
|
-
|
|
305
|
+
# DICOM PixelSpacing is [row spacing, column spacing]; Interfile
|
|
306
|
+
# [1] is the column/bin axis and [2] the row/axial axis.
|
|
307
|
+
self.header["scaling factor (mm/pixel) [1]"] = str(pixel_spacing[1])
|
|
308
|
+
self.header["scaling factor (mm/pixel) [2]"] = str(pixel_spacing[0])
|
|
286
309
|
except AttributeError:
|
|
287
310
|
warnings.warn(
|
|
288
311
|
"PixelSpacing not found in DICOM. Retaining default scaling factors."
|
|
@@ -303,6 +326,10 @@ class STIRSPECTAcquisitionDataBuilder:
|
|
|
303
326
|
try:
|
|
304
327
|
ewi_seq_tag = (0x0054, 0x0012)
|
|
305
328
|
self.energy_windows = [] # list to hold each energy window's info
|
|
329
|
+
vector = ds.get("EnergyWindowVector")
|
|
330
|
+
self.energy_window_vector = (
|
|
331
|
+
[int(v) for v in np.atleast_1d(vector)] if vector is not None else None
|
|
332
|
+
)
|
|
306
333
|
if ewi_seq_tag in ds:
|
|
307
334
|
ewi_seq = ds[ewi_seq_tag].value
|
|
308
335
|
for ewi_item in ewi_seq:
|
|
@@ -111,6 +111,9 @@ class STIRSPECTImageDataBuilder:
|
|
|
111
111
|
header_path = base_path.with_suffix(".hv")
|
|
112
112
|
raw_path = base_path.with_suffix(".v")
|
|
113
113
|
self.header["!name of data file"] = raw_path.name
|
|
114
|
+
# Keys added after construction must come before the terminator.
|
|
115
|
+
self.header.pop("!END OF INTERFILE", None)
|
|
116
|
+
self.header["!END OF INTERFILE"] = ""
|
|
114
117
|
|
|
115
118
|
with open(header_path, "w") as f:
|
|
116
119
|
line = 0
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""Helpers for extracting voxel spacing from SIRF/STIR image objects."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Optional
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _call_or_value(value: Any) -> Any:
|
|
9
|
+
return value() if callable(value) else value
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _spacing_zyx(spacing: Any) -> Optional[tuple[float, float, float]]:
|
|
13
|
+
try:
|
|
14
|
+
values = tuple(float(v) for v in spacing)
|
|
15
|
+
except Exception:
|
|
16
|
+
values = ()
|
|
17
|
+
|
|
18
|
+
if len(values) >= 4:
|
|
19
|
+
# Raw STIR-style coordinate sequence: (unused, z, y, x)
|
|
20
|
+
return values[1], values[2], values[3]
|
|
21
|
+
if len(values) == 3:
|
|
22
|
+
# Public (z, y, x) convention
|
|
23
|
+
return values[0], values[1], values[2]
|
|
24
|
+
|
|
25
|
+
if all(hasattr(spacing, axis) for axis in ("z", "y", "x")):
|
|
26
|
+
try:
|
|
27
|
+
z, y, x = (float(_call_or_value(getattr(spacing, axis))) for axis in "zyx")
|
|
28
|
+
return z, y, x
|
|
29
|
+
except Exception:
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
# Non-iterable STIR coordinates index (z, y, x) from 1.
|
|
33
|
+
for accessor in (
|
|
34
|
+
getattr(spacing, "at", None),
|
|
35
|
+
getattr(spacing, "__getitem__", None),
|
|
36
|
+
):
|
|
37
|
+
if accessor is None:
|
|
38
|
+
continue
|
|
39
|
+
try:
|
|
40
|
+
z, y, x = (float(accessor(index)) for index in (1, 2, 3))
|
|
41
|
+
return z, y, x
|
|
42
|
+
except Exception:
|
|
43
|
+
continue
|
|
44
|
+
|
|
45
|
+
return None
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def extract_voxel_size_mm(image: Any, backend_name: str) -> float:
|
|
49
|
+
"""Return the voxel size in mm from backend image metadata.
|
|
50
|
+
|
|
51
|
+
Backend images report spacing in ``(z, y, x)`` order. The connector takes a
|
|
52
|
+
single voxel size, so the voxels must be cubic.
|
|
53
|
+
"""
|
|
54
|
+
zyx = None
|
|
55
|
+
if hasattr(image, "voxel_sizes"):
|
|
56
|
+
try:
|
|
57
|
+
voxel_sizes = tuple(float(v) for v in image.voxel_sizes())
|
|
58
|
+
if len(voxel_sizes) >= 3:
|
|
59
|
+
zyx = voxel_sizes[:3]
|
|
60
|
+
except Exception:
|
|
61
|
+
pass
|
|
62
|
+
|
|
63
|
+
if zyx is None and hasattr(image, "get_grid_spacing"):
|
|
64
|
+
zyx = _spacing_zyx(image.get_grid_spacing())
|
|
65
|
+
if zyx is None:
|
|
66
|
+
raise ValueError(
|
|
67
|
+
f"Unable to read voxel spacing from {backend_name} get_grid_spacing()."
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
if zyx is None:
|
|
71
|
+
raise ValueError(
|
|
72
|
+
f"{backend_name} source object must expose voxel_sizes() or "
|
|
73
|
+
"get_grid_spacing()."
|
|
74
|
+
)
|
|
75
|
+
if max(zyx) - min(zyx) > 1e-3:
|
|
76
|
+
raise ValueError(
|
|
77
|
+
f"{backend_name} images must have cubic voxels; got (z, y, x) = "
|
|
78
|
+
f"{tuple(zyx)} mm"
|
|
79
|
+
)
|
|
80
|
+
return float(zyx[0])
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
__all__ = ["extract_voxel_size_mm"]
|
|
@@ -8,6 +8,7 @@ import contextlib
|
|
|
8
8
|
import logging
|
|
9
9
|
import math
|
|
10
10
|
import os
|
|
11
|
+
import re
|
|
11
12
|
import shutil
|
|
12
13
|
from dataclasses import dataclass, field
|
|
13
14
|
from pathlib import Path
|
|
@@ -68,8 +69,11 @@ class SimindPythonConnector(BaseConnector):
|
|
|
68
69
|
self.output_dir.mkdir(parents=True, exist_ok=True)
|
|
69
70
|
self.output_prefix = output_prefix
|
|
70
71
|
self.quantization_scale = float(quantization_scale)
|
|
71
|
-
|
|
72
|
-
|
|
72
|
+
# Above 1 the source would be clipped at MAX_SOURCE, changing its shape.
|
|
73
|
+
if not 0 < self.quantization_scale <= 1:
|
|
74
|
+
raise ValueError(
|
|
75
|
+
f"quantization_scale must be in (0, 1], got {quantization_scale!r}"
|
|
76
|
+
)
|
|
73
77
|
|
|
74
78
|
self.config = self._initialize_config(config_source)
|
|
75
79
|
self.runtime_switches = RuntimeSwitches()
|
|
@@ -82,16 +86,18 @@ class SimindPythonConnector(BaseConnector):
|
|
|
82
86
|
|
|
83
87
|
@staticmethod
|
|
84
88
|
def _validate_output_prefix(prefix: str) -> None:
|
|
85
|
-
"""
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
or Path(prefix).is_absolute()
|
|
89
|
+
"""Accept only prefixes SIMIND keeps unchanged in its file names.
|
|
90
|
+
|
|
91
|
+
SIMIND lowercases file names on Unix, a dot would be taken for a file
|
|
92
|
+
extension, and glob characters would break output discovery.
|
|
93
|
+
"""
|
|
94
|
+
if not isinstance(prefix, str) or not re.fullmatch(
|
|
95
|
+
r"[a-z0-9][a-z0-9_-]*", prefix
|
|
93
96
|
):
|
|
94
|
-
raise ValueError(
|
|
97
|
+
raise ValueError(
|
|
98
|
+
"output_prefix must contain only lowercase letters, digits, '_' "
|
|
99
|
+
f"and '-', got {prefix!r}"
|
|
100
|
+
)
|
|
95
101
|
|
|
96
102
|
@staticmethod
|
|
97
103
|
def _initialize_config(config_source: ConfigSource) -> SimulationConfig:
|
|
@@ -136,7 +142,9 @@ class SimindPythonConnector(BaseConnector):
|
|
|
136
142
|
|
|
137
143
|
The projection pixel size and matrix (Index 28, 76, 77) come from the
|
|
138
144
|
configuration, not from the phantom. scoring_routine overrides Index 84;
|
|
139
|
-
None keeps the configuration value.
|
|
145
|
+
None keeps the configuration value. The connector reads SPECT Interfile
|
|
146
|
+
output, so this sets Flag 5 (SPECT) and Flag 14 (Interfile headers).
|
|
147
|
+
The mu map is taken to be at abs(Index 1) keV.
|
|
140
148
|
|
|
141
149
|
Returns:
|
|
142
150
|
Tuple of (source_file_path, density_file_path).
|
|
@@ -222,7 +230,9 @@ class SimindPythonConnector(BaseConnector):
|
|
|
222
230
|
cfg.set_data_file(6, src_prefix)
|
|
223
231
|
|
|
224
232
|
if cfg.get_flag(11):
|
|
225
|
-
|
|
233
|
+
# A negative Index 1 asks SIMIND to read photon energies from the
|
|
234
|
+
# isotope file; its absolute value is still the window energy.
|
|
235
|
+
photon_energy = abs(float(cfg.get_value("photon_energy")))
|
|
226
236
|
density = attenuation_to_density(mu_map_array, photon_energy) * 1000.0
|
|
227
237
|
else:
|
|
228
238
|
density = np.zeros_like(mu_map_array)
|
|
@@ -290,6 +300,10 @@ class SimindPythonConnector(BaseConnector):
|
|
|
290
300
|
fov_length_mm,
|
|
291
301
|
)
|
|
292
302
|
|
|
303
|
+
# Outputs are read from SPECT Interfile headers (.h00), which SIMIND
|
|
304
|
+
# writes only with Flag 5 (SPECT) and Flag 14 (Interfile) set.
|
|
305
|
+
self.config.set_flag(5, True)
|
|
306
|
+
self.config.set_flag(14, True)
|
|
293
307
|
config_path = self.output_dir / self.output_prefix
|
|
294
308
|
self.config.save_file(config_path)
|
|
295
309
|
|
|
@@ -415,12 +429,9 @@ class SimindPythonConnector(BaseConnector):
|
|
|
415
429
|
try:
|
|
416
430
|
interfile = load_interfile_array(header_path)
|
|
417
431
|
except Exception as exc:
|
|
418
|
-
|
|
419
|
-
"
|
|
420
|
-
|
|
421
|
-
exc,
|
|
422
|
-
)
|
|
423
|
-
continue
|
|
432
|
+
raise RuntimeError(
|
|
433
|
+
f"SIMIND output {header_path} could not be read: {exc}"
|
|
434
|
+
) from exc
|
|
424
435
|
|
|
425
436
|
key = self._extract_output_key(header_path)
|
|
426
437
|
outputs[key] = ProjectionResult(
|
|
@@ -430,11 +441,6 @@ class SimindPythonConnector(BaseConnector):
|
|
|
430
441
|
metadata=interfile.metadata,
|
|
431
442
|
)
|
|
432
443
|
|
|
433
|
-
if not outputs:
|
|
434
|
-
raise RuntimeError(
|
|
435
|
-
f"No valid outputs were parsed from headers in {self.output_dir}"
|
|
436
|
-
)
|
|
437
|
-
|
|
438
444
|
return outputs
|
|
439
445
|
|
|
440
446
|
def _extract_output_key(self, header_path: Path) -> str:
|