simind-python-connector 1.0.1__tar.gz → 1.1.0__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.0.1/simind_python_connector.egg-info → simind_python_connector-1.1.0}/PKG-INFO +1 -1
- {simind_python_connector-1.0.1 → simind_python_connector-1.1.0}/docs/changelog.rst +70 -0
- {simind_python_connector-1.0.1 → simind_python_connector-1.1.0}/docs/conf.py +1 -1
- {simind_python_connector-1.0.1 → simind_python_connector-1.1.0/docs}/examples/07A_stir_adaptor_osem.py +4 -0
- {simind_python_connector-1.0.1 → simind_python_connector-1.1.0/docs}/examples/07B_sirf_adaptor_osem.py +4 -0
- {simind_python_connector-1.0.1 → simind_python_connector-1.1.0}/docs/examples/07C_pytomography_adaptor_osem.py +4 -0
- {simind_python_connector-1.0.1 → simind_python_connector-1.1.0}/docs/geometry.rst +28 -4
- {simind_python_connector-1.0.1/docs → simind_python_connector-1.1.0}/examples/07A_stir_adaptor_osem.py +4 -0
- {simind_python_connector-1.0.1/docs → simind_python_connector-1.1.0}/examples/07B_sirf_adaptor_osem.py +4 -0
- {simind_python_connector-1.0.1 → simind_python_connector-1.1.0}/examples/07C_pytomography_adaptor_osem.py +4 -0
- {simind_python_connector-1.0.1 → simind_python_connector-1.1.0}/pyproject.toml +1 -1
- {simind_python_connector-1.0.1 → simind_python_connector-1.1.0}/simind_python_connector/__init__.py +1 -1
- {simind_python_connector-1.0.1 → simind_python_connector-1.1.0}/simind_python_connector/backends/__init__.py +7 -0
- {simind_python_connector-1.0.1 → simind_python_connector-1.1.0}/simind_python_connector/builders/acquisition_builder.py +77 -29
- {simind_python_connector-1.0.1 → simind_python_connector-1.1.0}/simind_python_connector/builders/image_builder.py +18 -6
- simind_python_connector-1.1.0/simind_python_connector/connectors/__init__.py +43 -0
- {simind_python_connector-1.0.1 → simind_python_connector-1.1.0}/simind_python_connector/connectors/_spacing.py +13 -7
- {simind_python_connector-1.0.1 → simind_python_connector-1.1.0}/simind_python_connector/connectors/python_connector.py +146 -27
- {simind_python_connector-1.0.1 → simind_python_connector-1.1.0}/simind_python_connector/connectors/pytomography_adaptor.py +5 -0
- {simind_python_connector-1.0.1 → simind_python_connector-1.1.0}/simind_python_connector/connectors/sirf_adaptor.py +3 -0
- {simind_python_connector-1.0.1 → simind_python_connector-1.1.0}/simind_python_connector/connectors/stir_adaptor.py +3 -0
- {simind_python_connector-1.0.1 → simind_python_connector-1.1.0}/simind_python_connector/converters/attenuation.py +3 -1
- {simind_python_connector-1.0.1 → simind_python_connector-1.1.0}/simind_python_connector/converters/simind_to_stir.py +12 -10
- {simind_python_connector-1.0.1 → simind_python_connector-1.1.0}/simind_python_connector/core/config.py +208 -66
- {simind_python_connector-1.0.1 → simind_python_connector-1.1.0}/simind_python_connector/core/executor.py +36 -7
- {simind_python_connector-1.0.1 → simind_python_connector-1.1.0}/simind_python_connector/utils/import_helpers.py +4 -0
- {simind_python_connector-1.0.1 → simind_python_connector-1.1.0}/simind_python_connector/utils/interfile_numpy.py +18 -8
- {simind_python_connector-1.0.1 → simind_python_connector-1.1.0}/simind_python_connector/utils/interfile_parser.py +4 -2
- {simind_python_connector-1.0.1 → simind_python_connector-1.1.0}/simind_python_connector/utils/simind_utils.py +50 -11
- {simind_python_connector-1.0.1 → simind_python_connector-1.1.0}/simind_python_connector/utils/stir_utils.py +7 -2
- {simind_python_connector-1.0.1 → simind_python_connector-1.1.0/simind_python_connector.egg-info}/PKG-INFO +1 -1
- {simind_python_connector-1.0.1 → simind_python_connector-1.1.0}/simind_python_connector.egg-info/SOURCES.txt +7 -0
- {simind_python_connector-1.0.1 → simind_python_connector-1.1.0}/tests/test_acquisition_builder_unit.py +77 -2
- simind_python_connector-1.1.0/tests/test_attenuation_utils.py +116 -0
- {simind_python_connector-1.0.1 → simind_python_connector-1.1.0}/tests/test_backends.py +78 -0
- simind_python_connector-1.1.0/tests/test_connector_backend_separation.py +118 -0
- simind_python_connector-1.1.0/tests/test_container_scripts.py +290 -0
- simind_python_connector-1.1.0/tests/test_dicom_builder.py +156 -0
- simind_python_connector-1.1.0/tests/test_executor.py +217 -0
- {simind_python_connector-1.0.1 → simind_python_connector-1.1.0}/tests/test_image_builder_unit.py +23 -0
- {simind_python_connector-1.0.1 → simind_python_connector-1.1.0}/tests/test_interfile_numpy.py +128 -4
- simind_python_connector-1.1.0/tests/test_legacy_surface_policy.py +20 -0
- simind_python_connector-1.1.0/tests/test_marker_policy.py +117 -0
- {simind_python_connector-1.0.1 → simind_python_connector-1.1.0}/tests/test_native_adaptors.py +108 -12
- simind_python_connector-1.1.0/tests/test_non_square_slices_simind.py +64 -0
- simind_python_connector-1.1.0/tests/test_packaging_resources.py +113 -0
- {simind_python_connector-1.0.1 → simind_python_connector-1.1.0}/tests/test_python_connector.py +318 -0
- {simind_python_connector-1.0.1 → simind_python_connector-1.1.0}/tests/test_pytomography_adaptor.py +85 -0
- {simind_python_connector-1.0.1 → simind_python_connector-1.1.0}/tests/test_schneider_density.py +3 -0
- {simind_python_connector-1.0.1 → simind_python_connector-1.1.0}/tests/test_simind_to_stir_converter.py +41 -9
- simind_python_connector-1.1.0/tests/test_simulation_config.py +208 -0
- simind_python_connector-1.1.0/tests/test_utils_small.py +96 -0
- simind_python_connector-1.0.1/simind_python_connector/connectors/__init__.py +0 -24
- simind_python_connector-1.0.1/tests/test_connector_backend_separation.py +0 -42
- simind_python_connector-1.0.1/tests/test_marker_policy.py +0 -72
- simind_python_connector-1.0.1/tests/test_simulation_config.py +0 -89
- simind_python_connector-1.0.1/tests/test_utils_small.py +0 -53
- {simind_python_connector-1.0.1 → simind_python_connector-1.1.0}/LICENSE +0 -0
- {simind_python_connector-1.0.1 → simind_python_connector-1.1.0}/MANIFEST.in +0 -0
- {simind_python_connector-1.0.1 → simind_python_connector-1.1.0}/README.md +0 -0
- {simind_python_connector-1.0.1 → simind_python_connector-1.1.0}/docs/api.rst +0 -0
- {simind_python_connector-1.0.1 → simind_python_connector-1.1.0}/docs/backends.rst +0 -0
- {simind_python_connector-1.0.1 → simind_python_connector-1.1.0}/docs/connector_modernization_plan.rst +0 -0
- {simind_python_connector-1.0.1 → simind_python_connector-1.1.0}/docs/contributing.rst +0 -0
- {simind_python_connector-1.0.1 → simind_python_connector-1.1.0}/docs/examples/01_basic_simulation.py +0 -0
- {simind_python_connector-1.0.1 → simind_python_connector-1.1.0}/docs/examples/02_runtime_switch_comparison.py +0 -0
- {simind_python_connector-1.0.1 → simind_python_connector-1.1.0}/docs/examples/03_multi_window.py +0 -0
- {simind_python_connector-1.0.1 → simind_python_connector-1.1.0}/docs/examples/04_custom_config.py +0 -0
- {simind_python_connector-1.0.1 → simind_python_connector-1.1.0}/docs/examples/05_scattwin_vs_penetrate_comparison.py +0 -0
- {simind_python_connector-1.0.1 → simind_python_connector-1.1.0}/docs/examples/06_schneider_density_conversion.py +0 -0
- {simind_python_connector-1.0.1 → simind_python_connector-1.1.0}/docs/examples/08A_stir_adaptor_from_dicom.py +0 -0
- {simind_python_connector-1.0.1 → simind_python_connector-1.1.0}/docs/examples/08B_sirf_adaptor_from_dicom.py +0 -0
- {simind_python_connector-1.0.1 → simind_python_connector-1.1.0}/docs/examples/08C_pytomography_adaptor_from_dicom.py +0 -0
- {simind_python_connector-1.0.1 → simind_python_connector-1.1.0}/docs/examples/_python_connector_helpers.py +0 -0
- {simind_python_connector-1.0.1 → simind_python_connector-1.1.0}/docs/examples.rst +0 -0
- {simind_python_connector-1.0.1 → simind_python_connector-1.1.0}/docs/index.rst +0 -0
- {simind_python_connector-1.0.1 → simind_python_connector-1.1.0}/docs/installation.rst +0 -0
- {simind_python_connector-1.0.1 → simind_python_connector-1.1.0}/docs/intro.rst +0 -0
- {simind_python_connector-1.0.1 → simind_python_connector-1.1.0}/docs/testing.rst +0 -0
- {simind_python_connector-1.0.1 → simind_python_connector-1.1.0}/docs/usage.rst +0 -0
- {simind_python_connector-1.0.1 → simind_python_connector-1.1.0}/examples/01_basic_simulation.py +0 -0
- {simind_python_connector-1.0.1 → simind_python_connector-1.1.0}/examples/02_runtime_switch_comparison.py +0 -0
- {simind_python_connector-1.0.1 → simind_python_connector-1.1.0}/examples/03_multi_window.py +0 -0
- {simind_python_connector-1.0.1 → simind_python_connector-1.1.0}/examples/04_custom_config.py +0 -0
- {simind_python_connector-1.0.1 → simind_python_connector-1.1.0}/examples/05_scattwin_vs_penetrate_comparison.py +0 -0
- {simind_python_connector-1.0.1 → simind_python_connector-1.1.0}/examples/06_schneider_density_conversion.py +0 -0
- {simind_python_connector-1.0.1 → simind_python_connector-1.1.0}/examples/08A_stir_adaptor_from_dicom.py +0 -0
- {simind_python_connector-1.0.1 → simind_python_connector-1.1.0}/examples/08B_sirf_adaptor_from_dicom.py +0 -0
- {simind_python_connector-1.0.1 → simind_python_connector-1.1.0}/examples/08C_pytomography_adaptor_from_dicom.py +0 -0
- {simind_python_connector-1.0.1 → simind_python_connector-1.1.0}/examples/_python_connector_helpers.py +0 -0
- {simind_python_connector-1.0.1 → simind_python_connector-1.1.0}/requirements-dev.txt +0 -0
- {simind_python_connector-1.0.1 → simind_python_connector-1.1.0}/requirements.txt +0 -0
- {simind_python_connector-1.0.1 → simind_python_connector-1.1.0}/setup.cfg +0 -0
- {simind_python_connector-1.0.1 → simind_python_connector-1.1.0}/simind_python_connector/backends/base.py +0 -0
- {simind_python_connector-1.0.1 → simind_python_connector-1.1.0}/simind_python_connector/backends/sirf_backend.py +0 -0
- {simind_python_connector-1.0.1 → simind_python_connector-1.1.0}/simind_python_connector/backends/stir_backend.py +0 -0
- {simind_python_connector-1.0.1 → simind_python_connector-1.1.0}/simind_python_connector/builders/__init__.py +0 -0
- {simind_python_connector-1.0.1 → simind_python_connector-1.1.0}/simind_python_connector/configs/AnyScan.yaml +0 -0
- {simind_python_connector-1.0.1 → simind_python_connector-1.1.0}/simind_python_connector/configs/Discovery670.yaml +0 -0
- {simind_python_connector-1.0.1 → simind_python_connector-1.1.0}/simind_python_connector/configs/Example.yaml +0 -0
- {simind_python_connector-1.0.1 → simind_python_connector-1.1.0}/simind_python_connector/configs/MLD001_SCAN0.yaml +0 -0
- {simind_python_connector-1.0.1 → simind_python_connector-1.1.0}/simind_python_connector/configs/__init__.py +0 -0
- {simind_python_connector-1.0.1 → simind_python_connector-1.1.0}/simind_python_connector/configs/input.smc +0 -0
- {simind_python_connector-1.0.1 → simind_python_connector-1.1.0}/simind_python_connector/connectors/base.py +0 -0
- {simind_python_connector-1.0.1 → simind_python_connector-1.1.0}/simind_python_connector/converters/__init__.py +0 -0
- {simind_python_connector-1.0.1 → simind_python_connector-1.1.0}/simind_python_connector/converters/dicom_to_stir.py +0 -0
- {simind_python_connector-1.0.1 → simind_python_connector-1.1.0}/simind_python_connector/core/__init__.py +0 -0
- {simind_python_connector-1.0.1 → simind_python_connector-1.1.0}/simind_python_connector/core/types.py +0 -0
- {simind_python_connector-1.0.1 → simind_python_connector-1.1.0}/simind_python_connector/data/Schneider2000.json +0 -0
- {simind_python_connector-1.0.1 → simind_python_connector-1.1.0}/simind_python_connector/data/__init__.py +0 -0
- {simind_python_connector-1.0.1 → simind_python_connector-1.1.0}/simind_python_connector/data/bone.atn +0 -0
- {simind_python_connector-1.0.1 → simind_python_connector-1.1.0}/simind_python_connector/data/h2o.atn +0 -0
- {simind_python_connector-1.0.1 → simind_python_connector-1.1.0}/simind_python_connector/utils/__init__.py +0 -0
- {simind_python_connector-1.0.1 → simind_python_connector-1.1.0}/simind_python_connector/utils/backend_access.py +0 -0
- {simind_python_connector-1.0.1 → simind_python_connector-1.1.0}/simind_python_connector/utils/io_utils.py +0 -0
- {simind_python_connector-1.0.1 → simind_python_connector-1.1.0}/simind_python_connector/utils/sirf_stir_utils.py +0 -0
- {simind_python_connector-1.0.1 → simind_python_connector-1.1.0}/simind_python_connector.egg-info/dependency_links.txt +0 -0
- {simind_python_connector-1.0.1 → simind_python_connector-1.1.0}/simind_python_connector.egg-info/requires.txt +0 -0
- {simind_python_connector-1.0.1 → simind_python_connector-1.1.0}/simind_python_connector.egg-info/top_level.txt +0 -0
- {simind_python_connector-1.0.1 → simind_python_connector-1.1.0}/tests/__init__.py +0 -0
- {simind_python_connector-1.0.1 → simind_python_connector-1.1.0}/tests/conftest.py +0 -0
- {simind_python_connector-1.0.1 → simind_python_connector-1.1.0}/tests/test_arithmetic_operations.py +0 -0
- {simind_python_connector-1.0.1 → simind_python_connector-1.1.0}/tests/test_connectors_base.py +0 -0
- {simind_python_connector-1.0.1 → simind_python_connector-1.1.0}/tests/test_container_library_isolation.py +0 -0
- {simind_python_connector-1.0.1 → simind_python_connector-1.1.0}/tests/test_example_config_profile.py +0 -0
- {simind_python_connector-1.0.1 → simind_python_connector-1.1.0}/tests/test_examples_backend_separation.py +0 -0
- {simind_python_connector-1.0.1 → simind_python_connector-1.1.0}/tests/test_geometry_isolation_forward_projection.py +0 -0
- {simind_python_connector-1.0.1 → simind_python_connector-1.1.0}/tests/test_integration.py +0 -0
- {simind_python_connector-1.0.1 → simind_python_connector-1.1.0}/tests/test_interfile_parser.py +0 -0
- {simind_python_connector-1.0.1 → simind_python_connector-1.1.0}/tests/test_osem_geometry_diagnostics.py +0 -0
- {simind_python_connector-1.0.1 → simind_python_connector-1.1.0}/tests/test_package_identity.py +0 -0
- {simind_python_connector-1.0.1 → simind_python_connector-1.1.0}/tests/test_sirf_stir_utils.py +0 -0
- {simind_python_connector-1.0.1 → simind_python_connector-1.1.0}/tests/test_stir_utils_backend_choice_unit.py +0 -0
- {simind_python_connector-1.0.1 → simind_python_connector-1.1.0}/tests/test_utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: simind-python-connector
|
|
3
|
-
Version: 1.0
|
|
3
|
+
Version: 1.1.0
|
|
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>
|
|
@@ -7,6 +7,76 @@ 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.0 - 2026-10-11
|
|
11
|
+
------------------
|
|
12
|
+
|
|
13
|
+
Replaces 2.0.0 and 3.0.0, which were published by mistake and have been
|
|
14
|
+
withdrawn.
|
|
15
|
+
|
|
16
|
+
Breaking Changes
|
|
17
|
+
~~~~~~~~~~~~~~~~
|
|
18
|
+
|
|
19
|
+
- The projection bin size and matrix (Index 28, 76, 77) come from the
|
|
20
|
+
configuration. ``configure_voxel_phantom`` no longer sets them from the
|
|
21
|
+
phantom image, which made the projections square whenever the image slices
|
|
22
|
+
were square and took the number of axial rows from the image height.
|
|
23
|
+
- ``configure_voxel_phantom`` keeps the configured scoring routine (Index 84)
|
|
24
|
+
unless ``scoring_routine`` is given.
|
|
25
|
+
- ``STIRSPECTAcquisitionDataBuilder`` no longer flips projection data along the
|
|
26
|
+
last axis.
|
|
27
|
+
- Runtime-operator switches passed to ``run()`` apply to that run only. They
|
|
28
|
+
are no longer kept on the connector for later runs.
|
|
29
|
+
- Each run deletes earlier outputs that share the output prefix before SIMIND
|
|
30
|
+
starts.
|
|
31
|
+
- The STIR backend needs ``stirextra`` as well as ``stir``.
|
|
32
|
+
- STIR/SIRF adaptors take the z voxel spacing from a different axis (see
|
|
33
|
+
Fixed). Results change for anisotropic voxels.
|
|
34
|
+
- Invalid inputs now raise ``ValueError``: empty, non-finite or negative source
|
|
35
|
+
and attenuation maps, array shapes that do not match the header, and
|
|
36
|
+
``output_prefix`` values that contain a path. Energy-window validation raises
|
|
37
|
+
``ValueError`` instead of ``AssertionError``.
|
|
38
|
+
|
|
39
|
+
Added
|
|
40
|
+
~~~~~
|
|
41
|
+
|
|
42
|
+
- ``run()`` warns when a voxel phantom is wider or longer than the projection
|
|
43
|
+
field of view.
|
|
44
|
+
|
|
45
|
+
Fixed
|
|
46
|
+
~~~~~
|
|
47
|
+
|
|
48
|
+
- The packaged ``configs/input.smc`` is now included in the wheel. SMC parsing
|
|
49
|
+
handles fixed-width fields and checks section counts.
|
|
50
|
+
- STIR/SIRF adaptors read the z voxel spacing from the correct axis:
|
|
51
|
+
``voxel_sizes()`` is ``(z, y, x)``, and raw ``get_grid_spacing()`` is
|
|
52
|
+
``(unused, z, y, x)``.
|
|
53
|
+
- Voxel-map dimension indices 78, 79, 81 and 82 are set correctly.
|
|
54
|
+
- The executor resolves ``SIMIND_BIN`` and accepts executable paths that
|
|
55
|
+
contain spaces. The connector no longer changes the process working directory.
|
|
56
|
+
- Runtime switches apply to one run only. Output prefixes cannot escape the
|
|
57
|
+
output directory. Stale outputs are removed, and ``.win``, ``.smi`` and
|
|
58
|
+
``.dmi`` inputs are kept.
|
|
59
|
+
- Energy-window validation raises ``ValueError`` instead of failing an
|
|
60
|
+
``assert``.
|
|
61
|
+
- The Interfile loader honours data offsets, strips quoted values and rejects
|
|
62
|
+
truncated or oversized payloads. The SIMIND-to-STIR converter keeps data-file
|
|
63
|
+
lines and applies the radius scale factor.
|
|
64
|
+
- Attenuation utilities handle the ``rho*1000`` selector and STIR ``#`` header
|
|
65
|
+
keys, and reject unknown attenuation types.
|
|
66
|
+
- Image and acquisition builders validate array shapes, split multi-energy data
|
|
67
|
+
deterministically and no longer flip images. The DICOM builder handles missing
|
|
68
|
+
timing information and 2D pixel data.
|
|
69
|
+
- Backend adaptors are imported lazily, so the core package imports without
|
|
70
|
+
STIR, SIRF or PyTomography.
|
|
71
|
+
|
|
72
|
+
Removed
|
|
73
|
+
~~~~~~~
|
|
74
|
+
|
|
75
|
+
- The legacy ``scripts/simulation.py``.
|
|
76
|
+
- The "non-square matrix" warning from
|
|
77
|
+
``SimulationConfig.validate_parameters``; SIMIND supports non-square
|
|
78
|
+
projections.
|
|
79
|
+
|
|
10
80
|
1.0.1 - 2026-04-16
|
|
11
81
|
------------------
|
|
12
82
|
|
|
@@ -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.0
|
|
16
|
+
release = "1.1.0"
|
|
17
17
|
for dist_name in ("simind-python-connector",):
|
|
18
18
|
try:
|
|
19
19
|
release = importlib_metadata.version(dist_name)
|
|
@@ -188,6 +188,10 @@ def main() -> None:
|
|
|
188
188
|
adaptor.add_config_value(1, 140.0)
|
|
189
189
|
adaptor.add_config_value(19, 2)
|
|
190
190
|
adaptor.add_config_value(53, 0)
|
|
191
|
+
# Projections on the reconstruction grid: 32 x 32 bins of 4 mm (Index 28 in cm).
|
|
192
|
+
adaptor.add_config_value(28, 0.4)
|
|
193
|
+
adaptor.add_config_value(76, 32)
|
|
194
|
+
adaptor.add_config_value(77, 32)
|
|
191
195
|
adaptor.run()
|
|
192
196
|
|
|
193
197
|
projection = adaptor.get_total_output(window=1)
|
|
@@ -170,6 +170,10 @@ def main() -> None:
|
|
|
170
170
|
adaptor.add_config_value(1, 140.0)
|
|
171
171
|
adaptor.add_config_value(19, 2)
|
|
172
172
|
adaptor.add_config_value(53, 0)
|
|
173
|
+
# Projections on the reconstruction grid: 32 x 32 bins of 4 mm (Index 28 in cm).
|
|
174
|
+
adaptor.add_config_value(28, 0.4)
|
|
175
|
+
adaptor.add_config_value(76, 32)
|
|
176
|
+
adaptor.add_config_value(77, 32)
|
|
173
177
|
adaptor.run()
|
|
174
178
|
|
|
175
179
|
projection = adaptor.get_total_output(window=1)
|
|
@@ -144,6 +144,10 @@ def main() -> None:
|
|
|
144
144
|
adaptor.add_config_value(1, 140.0)
|
|
145
145
|
adaptor.add_config_value(19, 2)
|
|
146
146
|
adaptor.add_config_value(53, 0)
|
|
147
|
+
# Projections on the reconstruction grid: 32 x 32 bins of 4 mm (Index 28 in cm).
|
|
148
|
+
adaptor.add_config_value(28, 0.4)
|
|
149
|
+
adaptor.add_config_value(76, 32)
|
|
150
|
+
adaptor.add_config_value(77, 32)
|
|
147
151
|
adaptor.run()
|
|
148
152
|
|
|
149
153
|
projection_tensor = adaptor.get_total_output(window=1).to(dtype=torch.float32)
|
|
@@ -35,10 +35,32 @@ 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 adaptor voxel size extraction expects z-spacing in mm.
|
|
39
|
-
``voxel_sizes()
|
|
40
|
-
|
|
41
|
-
|
|
38
|
+
- STIR/SIRF adaptor voxel size extraction expects z-spacing in mm. Images
|
|
39
|
+
report ``voxel_sizes()`` as ``(z, y, x)``, so the **first** element is the z
|
|
40
|
+
spacing. Raw ``get_grid_spacing()`` sequences follow the STIR coordinate
|
|
41
|
+
layout ``(unused, z, y, x)`` and use index 1; coordinate objects with a
|
|
42
|
+
``z`` accessor use that accessor directly.
|
|
43
|
+
|
|
44
|
+
Projection Size
|
|
45
|
+
---------------
|
|
46
|
+
|
|
47
|
+
The projection bin size and matrix come from the configuration: Index 28 (bin
|
|
48
|
+
size in cm), Index 76 (bins) and Index 77 (axial rows). Configuring a voxel
|
|
49
|
+
phantom does not change them, so set them to cover the phantom; ``run()`` warns
|
|
50
|
+
when the phantom is wider or longer than the projection field of view. The
|
|
51
|
+
matrix does not have to be square.
|
|
52
|
+
|
|
53
|
+
Some projectors need the projections sampled like the image. STIR's SPECTUB
|
|
54
|
+
needs equal axial sampling, so the OSEM examples set Index 28 to the voxel size.
|
|
55
|
+
|
|
56
|
+
Voxel Phantom Slices
|
|
57
|
+
--------------------
|
|
58
|
+
|
|
59
|
+
Image slices do not have to be square. The columns (x, Index 78 and 79) and
|
|
60
|
+
rows (y, Index 81 and 82) of the source and density maps are set separately,
|
|
61
|
+
and a SIMIND test checks that non-square slices give the same projections as
|
|
62
|
+
the same slices padded to square. The voxels themselves must be square in-plane:
|
|
63
|
+
SIMIND takes a single voxel side (Index 31 and the ``PX`` switch).
|
|
42
64
|
|
|
43
65
|
Example Configuration Guardrails
|
|
44
66
|
--------------------------------
|
|
@@ -50,6 +72,8 @@ are reproducible:
|
|
|
50
72
|
- ``config[29]=24`` for projection count.
|
|
51
73
|
- ``config[53]=0`` to keep collimator modeling geometric-only in these tests.
|
|
52
74
|
- ``config[19]=2`` to keep a consistent mapping used by current examples.
|
|
75
|
+
- ``config[28]=0.4``, ``config[76]=32`` and ``config[77]=32`` to put the
|
|
76
|
+
projections on the 4 mm, 32-voxel reconstruction grid.
|
|
53
77
|
|
|
54
78
|
Debug Checklist
|
|
55
79
|
---------------
|
|
@@ -188,6 +188,10 @@ def main() -> None:
|
|
|
188
188
|
adaptor.add_config_value(1, 140.0)
|
|
189
189
|
adaptor.add_config_value(19, 2)
|
|
190
190
|
adaptor.add_config_value(53, 0)
|
|
191
|
+
# Projections on the reconstruction grid: 32 x 32 bins of 4 mm (Index 28 in cm).
|
|
192
|
+
adaptor.add_config_value(28, 0.4)
|
|
193
|
+
adaptor.add_config_value(76, 32)
|
|
194
|
+
adaptor.add_config_value(77, 32)
|
|
191
195
|
adaptor.run()
|
|
192
196
|
|
|
193
197
|
projection = adaptor.get_total_output(window=1)
|
|
@@ -170,6 +170,10 @@ def main() -> None:
|
|
|
170
170
|
adaptor.add_config_value(1, 140.0)
|
|
171
171
|
adaptor.add_config_value(19, 2)
|
|
172
172
|
adaptor.add_config_value(53, 0)
|
|
173
|
+
# Projections on the reconstruction grid: 32 x 32 bins of 4 mm (Index 28 in cm).
|
|
174
|
+
adaptor.add_config_value(28, 0.4)
|
|
175
|
+
adaptor.add_config_value(76, 32)
|
|
176
|
+
adaptor.add_config_value(77, 32)
|
|
173
177
|
adaptor.run()
|
|
174
178
|
|
|
175
179
|
projection = adaptor.get_total_output(window=1)
|
|
@@ -144,6 +144,10 @@ def main() -> None:
|
|
|
144
144
|
adaptor.add_config_value(1, 140.0)
|
|
145
145
|
adaptor.add_config_value(19, 2)
|
|
146
146
|
adaptor.add_config_value(53, 0)
|
|
147
|
+
# Projections on the reconstruction grid: 32 x 32 bins of 4 mm (Index 28 in cm).
|
|
148
|
+
adaptor.add_config_value(28, 0.4)
|
|
149
|
+
adaptor.add_config_value(76, 32)
|
|
150
|
+
adaptor.add_config_value(77, 32)
|
|
147
151
|
adaptor.run()
|
|
148
152
|
|
|
149
153
|
projection_tensor = adaptor.get_total_output(window=1).to(dtype=torch.float32)
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "simind-python-connector"
|
|
7
|
-
version = "1.0
|
|
7
|
+
version = "1.1.0"
|
|
8
8
|
description = "Python SIMIND Monte Carlo connector with STIR/SIRF/PyTomography adaptors"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.9"
|
|
@@ -19,6 +19,7 @@ Usage:
|
|
|
19
19
|
|
|
20
20
|
import importlib
|
|
21
21
|
import logging
|
|
22
|
+
import os
|
|
22
23
|
from typing import TYPE_CHECKING, Any, Literal, Optional, Union
|
|
23
24
|
|
|
24
25
|
from .base import AcquisitionDataInterface, ImageDataInterface
|
|
@@ -233,6 +234,9 @@ def create_image_data(
|
|
|
233
234
|
if isinstance(filepath_or_object, ImageDataInterface):
|
|
234
235
|
return filepath_or_object
|
|
235
236
|
|
|
237
|
+
if isinstance(filepath_or_object, os.PathLike):
|
|
238
|
+
filepath_or_object = os.fspath(filepath_or_object)
|
|
239
|
+
|
|
236
240
|
backend_hint = detect_image_backend(filepath_or_object)
|
|
237
241
|
if backend_hint is None:
|
|
238
242
|
backend_hint = detect_backend_from_interface(filepath_or_object)
|
|
@@ -314,6 +318,9 @@ def create_acquisition_data(
|
|
|
314
318
|
if isinstance(filepath_or_object, AcquisitionDataInterface):
|
|
315
319
|
return filepath_or_object
|
|
316
320
|
|
|
321
|
+
if isinstance(filepath_or_object, os.PathLike):
|
|
322
|
+
filepath_or_object = os.fspath(filepath_or_object)
|
|
323
|
+
|
|
317
324
|
backend_hint = detect_acquisition_backend(filepath_or_object)
|
|
318
325
|
if backend_hint is None:
|
|
319
326
|
backend_hint = detect_backend_from_interface(filepath_or_object)
|
|
@@ -97,6 +97,19 @@ class STIRSPECTAcquisitionDataBuilder:
|
|
|
97
97
|
else:
|
|
98
98
|
self.pixel_array = np.array(self.pixel_array, dtype=np.float32)
|
|
99
99
|
|
|
100
|
+
expected_shape = (
|
|
101
|
+
1,
|
|
102
|
+
matrix_size_1,
|
|
103
|
+
num_projections,
|
|
104
|
+
matrix_size_2,
|
|
105
|
+
)
|
|
106
|
+
if self.pixel_array.shape != expected_shape:
|
|
107
|
+
raise ValueError(
|
|
108
|
+
f"pixel array shape {self.pixel_array.shape} does not match "
|
|
109
|
+
f"header dimensions {expected_shape} "
|
|
110
|
+
"(tof, bin, view, axial)"
|
|
111
|
+
)
|
|
112
|
+
|
|
100
113
|
def _write(base_path: Path, cleanup: bool) -> AcquisitionData:
|
|
101
114
|
header_path = base_path.with_suffix(".hs")
|
|
102
115
|
raw_file_path = base_path.with_suffix(".s")
|
|
@@ -112,9 +125,8 @@ class STIRSPECTAcquisitionDataBuilder:
|
|
|
112
125
|
|
|
113
126
|
acqdata = self._load_acquisition(str(header_path))
|
|
114
127
|
|
|
115
|
-
flipped = np.flip(self.pixel_array, axis=-1)
|
|
116
128
|
acqdata = acqdata.clone()
|
|
117
|
-
acqdata.fill(
|
|
129
|
+
acqdata.fill(self.pixel_array)
|
|
118
130
|
acqdata.write(str(header_path))
|
|
119
131
|
|
|
120
132
|
if cleanup:
|
|
@@ -194,27 +206,50 @@ class STIRSPECTAcquisitionDataBuilder:
|
|
|
194
206
|
acqdata = self.build(output_path=output_path_base)
|
|
195
207
|
return [acqdata]
|
|
196
208
|
|
|
209
|
+
if self.pixel_array is None:
|
|
210
|
+
raise ValueError(
|
|
211
|
+
"build_multi_energy requires pixel_array to be set "
|
|
212
|
+
"(use update_header_from_dicom or set it directly)"
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
num_windows = len(self.energy_windows)
|
|
216
|
+
total_projections = int(self.header.get("!number of projections", 1))
|
|
217
|
+
if self.pixel_array.ndim != 4 or (
|
|
218
|
+
self.pixel_array.shape[2] != total_projections
|
|
219
|
+
):
|
|
220
|
+
raise ValueError(
|
|
221
|
+
f"pixel array shape {self.pixel_array.shape} does not match "
|
|
222
|
+
f"declared {total_projections} projections"
|
|
223
|
+
)
|
|
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
|
+
|
|
197
231
|
# number of projections needs dividing by number of energy windows
|
|
198
|
-
|
|
199
|
-
num_projections //= len(self.energy_windows)
|
|
200
|
-
self.header["!number of projections"] = str(num_projections)
|
|
232
|
+
self.header["!number of projections"] = str(projections_per_window)
|
|
201
233
|
|
|
202
234
|
# split pixel_array into energy windows along 3rd axis
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
235
|
+
original_pixel_array = self.pixel_array
|
|
236
|
+
try:
|
|
237
|
+
pixel_array_list = np.array_split(original_pixel_array, num_windows, axis=2)
|
|
238
|
+
|
|
239
|
+
acqdata_list = []
|
|
240
|
+
for idx, ew in enumerate(self.energy_windows):
|
|
241
|
+
# Update header for this energy window.
|
|
242
|
+
self.header["energy window lower level[1]"] = ew["lower"]
|
|
243
|
+
self.header["energy window upper level[1]"] = ew["upper"]
|
|
244
|
+
suffix = f"_ew{idx + 1}"
|
|
245
|
+
output_path = output_path_base + suffix
|
|
246
|
+
self.pixel_array = pixel_array_list[idx]
|
|
247
|
+
acqdata = self.build(output_path=output_path)
|
|
248
|
+
acqdata_list.append(acqdata)
|
|
249
|
+
return acqdata_list
|
|
250
|
+
finally:
|
|
251
|
+
self.pixel_array = original_pixel_array
|
|
252
|
+
self.header["!number of projections"] = str(total_projections)
|
|
218
253
|
|
|
219
254
|
def update_header_from_dicom(self, dicom_filepath):
|
|
220
255
|
"""
|
|
@@ -303,6 +338,8 @@ class STIRSPECTAcquisitionDataBuilder:
|
|
|
303
338
|
)
|
|
304
339
|
|
|
305
340
|
# Rotation Information Sequence processing
|
|
341
|
+
num_frames = ds.get("NumberOfFrames", None)
|
|
342
|
+
time_per_projection = None
|
|
306
343
|
try:
|
|
307
344
|
if (0x0054, 0x0052) in ds:
|
|
308
345
|
rot_seq = ds[(0x0054, 0x0052)].value
|
|
@@ -322,16 +359,17 @@ class STIRSPECTAcquisitionDataBuilder:
|
|
|
322
359
|
rot_item[(0x0018, 0x1242)].value / 1000
|
|
323
360
|
)
|
|
324
361
|
|
|
325
|
-
if num_frames is not None:
|
|
362
|
+
if time_per_projection is not None and num_frames is not None:
|
|
326
363
|
self.header["number of time frames"] = str(1)
|
|
327
364
|
self.header["!image duration (sec)[1]"] = str(
|
|
328
365
|
int(
|
|
329
366
|
np.round(
|
|
330
|
-
float(time_per_projection) * float(num_frames),
|
|
367
|
+
float(time_per_projection) * float(num_frames),
|
|
368
|
+
0,
|
|
331
369
|
)
|
|
332
370
|
)
|
|
333
371
|
)
|
|
334
|
-
|
|
372
|
+
elif time_per_projection is not None:
|
|
335
373
|
self.header["!time per projection (sec)[1]"] = (
|
|
336
374
|
time_per_projection
|
|
337
375
|
)
|
|
@@ -516,11 +554,21 @@ class STIRSPECTAcquisitionDataBuilder:
|
|
|
516
554
|
except AttributeError:
|
|
517
555
|
warnings.warn("StudyDescription not found in DICOM.")
|
|
518
556
|
try:
|
|
519
|
-
|
|
520
|
-
print(self.pixel_array.shape)
|
|
521
|
-
self.pixel_array = np.transpose(self.pixel_array, (2, 0, 1))
|
|
522
|
-
# rotate the image by 90 degrees cW in axis 1
|
|
523
|
-
self.pixel_array = np.rot90(self.pixel_array, 3, axes=(0, 2))
|
|
524
|
-
self.pixel_array = np.expand_dims(self.pixel_array, axis=0)
|
|
557
|
+
raw_pixel_array = ds.pixel_array
|
|
525
558
|
except AttributeError:
|
|
526
559
|
warnings.warn("Pixel data not found in DICOM.")
|
|
560
|
+
return
|
|
561
|
+
|
|
562
|
+
raw_pixel_array = np.asarray(raw_pixel_array)
|
|
563
|
+
if raw_pixel_array.ndim == 2:
|
|
564
|
+
# Single frame: promote to (frames=1, rows, columns) so the
|
|
565
|
+
# transpose below sees the same layout as multiframe data.
|
|
566
|
+
raw_pixel_array = raw_pixel_array[np.newaxis, :, :]
|
|
567
|
+
elif raw_pixel_array.ndim != 3:
|
|
568
|
+
raise ValueError(
|
|
569
|
+
f"pixel data must be 2D or 3D, got {raw_pixel_array.ndim}D"
|
|
570
|
+
)
|
|
571
|
+
self.pixel_array = np.transpose(raw_pixel_array, (2, 0, 1))
|
|
572
|
+
# rotate the image by 90 degrees cW in axis 1
|
|
573
|
+
self.pixel_array = np.rot90(self.pixel_array, 3, axes=(0, 2))
|
|
574
|
+
self.pixel_array = np.expand_dims(self.pixel_array, axis=0)
|
|
@@ -79,12 +79,24 @@ class STIRSPECTImageDataBuilder:
|
|
|
79
79
|
|
|
80
80
|
def _resolve_data_array(self) -> np.ndarray:
|
|
81
81
|
if self.pixel_array is not None:
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
82
|
+
data = np.asarray(self.pixel_array, dtype=np.float32)
|
|
83
|
+
else:
|
|
84
|
+
dim_x = int(self.header["!matrix size [1]"])
|
|
85
|
+
dim_y = int(self.header["!matrix size [2]"])
|
|
86
|
+
dim_z = int(self.header["!matrix size [3]"])
|
|
87
|
+
data = np.zeros((dim_z, dim_y, dim_x), dtype=np.float32)
|
|
88
|
+
|
|
89
|
+
expected_shape = (
|
|
90
|
+
int(self.header["!matrix size [3]"]),
|
|
91
|
+
int(self.header["!matrix size [2]"]),
|
|
92
|
+
int(self.header["!matrix size [1]"]),
|
|
93
|
+
)
|
|
94
|
+
if data.shape != expected_shape:
|
|
95
|
+
raise ValueError(
|
|
96
|
+
f"pixel array shape {data.shape} does not match header "
|
|
97
|
+
f"dimensions {expected_shape} (z, y, x)"
|
|
98
|
+
)
|
|
99
|
+
return data
|
|
88
100
|
|
|
89
101
|
def build(self, output_path: Optional[str | Path] = None):
|
|
90
102
|
"""
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Connector/adaptor APIs.
|
|
2
|
+
|
|
3
|
+
Backend-specific adaptors are exposed lazily so that importing this package
|
|
4
|
+
never imports SIRF, STIR, or PyTomography.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import importlib
|
|
8
|
+
|
|
9
|
+
from .base import BaseConnector
|
|
10
|
+
from .python_connector import (
|
|
11
|
+
NumpyConnector,
|
|
12
|
+
ProjectionResult,
|
|
13
|
+
RuntimeOperator,
|
|
14
|
+
SimindPythonConnector,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
_LAZY_ADAPTORS = {
|
|
19
|
+
"PyTomographySimindAdaptor": "pytomography_adaptor",
|
|
20
|
+
"SirfSimindAdaptor": "sirf_adaptor",
|
|
21
|
+
"StirSimindAdaptor": "stir_adaptor",
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def __getattr__(name):
|
|
26
|
+
if name in _LAZY_ADAPTORS:
|
|
27
|
+
module = importlib.import_module(f".{_LAZY_ADAPTORS[name]}", __name__)
|
|
28
|
+
obj = getattr(module, name)
|
|
29
|
+
globals()[name] = obj
|
|
30
|
+
return obj
|
|
31
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
__all__ = [
|
|
35
|
+
"BaseConnector",
|
|
36
|
+
"NumpyConnector",
|
|
37
|
+
"PyTomographySimindAdaptor",
|
|
38
|
+
"ProjectionResult",
|
|
39
|
+
"RuntimeOperator",
|
|
40
|
+
"SirfSimindAdaptor",
|
|
41
|
+
"SimindPythonConnector",
|
|
42
|
+
"StirSimindAdaptor",
|
|
43
|
+
]
|
|
@@ -17,9 +17,11 @@ def _voxel_size_from_spacing(spacing: Any) -> float | None:
|
|
|
17
17
|
|
|
18
18
|
if values:
|
|
19
19
|
if len(values) >= 4:
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
# Raw STIR-style coordinate sequence: (unused, z, y, x)
|
|
21
|
+
return float(values[1])
|
|
22
|
+
if len(values) == 3:
|
|
23
|
+
# Public (z, y, x) convention
|
|
24
|
+
return float(values[0])
|
|
23
25
|
|
|
24
26
|
if hasattr(spacing, "z"):
|
|
25
27
|
try:
|
|
@@ -28,13 +30,13 @@ def _voxel_size_from_spacing(spacing: Any) -> float | None:
|
|
|
28
30
|
pass
|
|
29
31
|
|
|
30
32
|
if hasattr(spacing, "at"):
|
|
31
|
-
for index in (
|
|
33
|
+
for index in (1, 2, 3):
|
|
32
34
|
try:
|
|
33
35
|
return float(spacing.at(index))
|
|
34
36
|
except Exception:
|
|
35
37
|
continue
|
|
36
38
|
|
|
37
|
-
for index in (
|
|
39
|
+
for index in (1, 2, 3):
|
|
38
40
|
try:
|
|
39
41
|
return float(spacing[index])
|
|
40
42
|
except Exception:
|
|
@@ -44,12 +46,16 @@ def _voxel_size_from_spacing(spacing: Any) -> float | None:
|
|
|
44
46
|
|
|
45
47
|
|
|
46
48
|
def extract_voxel_size_mm(image: Any, backend_name: str) -> float:
|
|
47
|
-
"""Extract z voxel spacing in mm from backend image metadata.
|
|
49
|
+
"""Extract z voxel spacing in mm from backend image metadata.
|
|
50
|
+
|
|
51
|
+
Backend images report ``voxel_sizes()`` in ``(z, y, x)`` order, so the
|
|
52
|
+
first element is the z spacing.
|
|
53
|
+
"""
|
|
48
54
|
if hasattr(image, "voxel_sizes"):
|
|
49
55
|
voxel_sizes = image.voxel_sizes()
|
|
50
56
|
try:
|
|
51
57
|
if len(voxel_sizes) >= 3:
|
|
52
|
-
return float(voxel_sizes[
|
|
58
|
+
return float(voxel_sizes[0])
|
|
53
59
|
except Exception:
|
|
54
60
|
pass
|
|
55
61
|
|