simind-python-connector 2.0.1__py3-none-any.whl → 2.0.2__py3-none-any.whl

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.
@@ -14,7 +14,7 @@ for _dist_name in ("simind-python-connector", __name__):
14
14
  except _meta.PackageNotFoundError:
15
15
  continue
16
16
  else: # editable / source checkout
17
- __version__ = "2.0.1"
17
+ __version__ = "2.0.2"
18
18
 
19
19
 
20
20
  def __getattr__(name: str) -> Any:
@@ -853,8 +853,8 @@ class SimindPythonConnector(BaseConnector):
853
853
  ):
854
854
  warnings.warn(
855
855
  "SIMIND v9.0 on Linux can abort in SCATTWIN or silently return "
856
- "corrupted projections. Use SIMIND v8, the macOS v9 build or "
857
- "the PENETRATE scoring routine.",
856
+ "corrupted projections. Use SIMIND v8; v9 support is "
857
+ "experimental and unvalidated in this release (planned for 2.1.0).",
858
858
  RuntimeWarning,
859
859
  stacklevel=2,
860
860
  )
@@ -23,9 +23,13 @@ class ScatterCorrection:
23
23
  output, which holds phantom scatter only.
24
24
 
25
25
  With measured, this is Dewaraja et al. 2017's pixel-wise model: the
26
- scatter fraction scatter / total of SIMIND's run, times the measured data.
27
- It does not depend on the projector's scalar scale. With smoothing,
28
- smoothing_fwhm_bins=3.0 and smooth_across_views=True match the paper.
26
+ measured data times the scatter-to-total ratio of SIMIND's run. It does not
27
+ depend on the projector's scalar scale. With smoothing, SIMIND's scatter
28
+ and total are smoothed with the same Gaussian before the ratio, and the
29
+ scaled result is smoothed again. smoothing_fwhm_bins=3.0 and
30
+ smooth_across_views=True match the paper. The final smoothing is retained
31
+ because the measured noise enters the estimate. The ratio is zero only
32
+ where the smoothed total is not positive; signed scatter is not clipped.
29
33
  smooth_across_views wraps around the view axis, so it assumes the views
30
34
  cover 360 degrees.
31
35
  """
@@ -79,6 +83,26 @@ class ScatterCorrection:
79
83
  f"scatter shape {scatter_array.shape}"
80
84
  )
81
85
  total_array = total.as_array()
86
+ if total_array.shape != scatter_array.shape:
87
+ raise ValueError(
88
+ f"total shape {total_array.shape} does not match "
89
+ f"scatter shape {scatter_array.shape}"
90
+ )
91
+ # Smooth the MC scatter and its total before forming the fraction. In
92
+ # the fixed-primary, noisy-scatter control used to check this order,
93
+ # pre-smoothing reduced the ratio noise in the summed estimate. The
94
+ # direction and size of a noisy ratio's error depend on the covariance
95
+ # between numerator and denominator, so this is an observation about
96
+ # that control, not a general theorem about Monte Carlo ratios, and it
97
+ # does not account for the unresolved pixel-wise drift. The final
98
+ # smoothing in estimate() is retained.
99
+ if self.smoothing_fwhm_bins is not None:
100
+ scatter_array = _smooth_array(
101
+ scatter_array, self.smoothing_fwhm_bins, self.smooth_across_views
102
+ )
103
+ total_array = _smooth_array(
104
+ total_array, self.smoothing_fwhm_bins, self.smooth_across_views
105
+ )
82
106
  fraction = np.divide(
83
107
  scatter_array,
84
108
  total_array,
@@ -90,27 +114,45 @@ class ScatterCorrection:
90
114
  return out
91
115
 
92
116
 
93
- def _smooth_views(data: Any, fwhm_bins: float, across_views: bool = False) -> Any:
94
- """Gaussian-smooth each view along its axial and bin axes, keeping the total.
117
+ def _smooth_array(
118
+ array: Any, fwhm_bins: float, across_views: bool = False
119
+ ) -> np.ndarray:
120
+ """Gaussian-smooth a projection array, keeping its total.
95
121
 
96
- With across_views, also smooth along the view axis, wrapping round.
122
+ The total is restored by rescaling only when the input sum stands clear
123
+ of the rounding floor of that sum, so a zero-sum signed field keeps its
124
+ smoothed values instead of being multiplied by an arbitrary factor.
97
125
  """
98
126
  from scipy.ndimage import gaussian_filter
99
127
 
100
- array = np.asarray(data.as_array(), dtype=np.float64)
101
- sigma = [0.0] * array.ndim
102
- mode = ["reflect"] * array.ndim
128
+ values = np.asarray(array, dtype=np.float64)
129
+ sigma = [0.0] * values.ndim
130
+ mode = ["reflect"] * values.ndim
103
131
  # Projection arrays end in (axial, view, bin); SIRF may add a TOF axis first.
104
132
  sigma[-3] = sigma[-1] = fwhm_bins / (2.0 * np.sqrt(2.0 * np.log(2.0)))
105
133
  if across_views:
106
134
  sigma[-2] = sigma[-1]
107
135
  mode[-2] = "wrap"
108
- smoothed = gaussian_filter(array, sigma, mode=mode)
109
- total = smoothed.sum()
110
- if total != 0.0:
111
- smoothed *= array.sum() / total
136
+ smoothed = gaussian_filter(values, sigma, mode=mode)
137
+ # Keep the original total, but only when it stands clear of the rounding
138
+ # floor of the sum. A cancelling signed field sums to about zero, and so
139
+ # does its smoothed version: their ratio is then an arbitrary factor (and
140
+ # zero erases the field), so such a field is left exactly as smoothed.
141
+ original_sum = values.sum()
142
+ if abs(original_sum) > 1e-9 * np.abs(values).sum():
143
+ total = smoothed.sum()
144
+ if total != 0.0:
145
+ smoothed *= original_sum / total
146
+ return smoothed.astype(np.float32)
147
+
148
+
149
+ def _smooth_views(data: Any, fwhm_bins: float, across_views: bool = False) -> Any:
150
+ """Gaussian-smooth each view along its axial and bin axes, keeping the total.
151
+
152
+ With across_views, also smooth along the view axis, wrapping round.
153
+ """
112
154
  out = data.clone()
113
- out.fill(smoothed.astype(np.float32))
155
+ out.fill(_smooth_array(data.as_array(), fwhm_bins, across_views))
114
156
  return out
115
157
 
116
158
 
@@ -28,7 +28,8 @@ def run_osem_with_corrections(
28
28
  Each pass builds a fresh model, objective and reconstructor, continuing
29
29
  from the preceding pass's image. initial_image is cloned and not modified.
30
30
  updater.update_now is called between passes at completed subiterations;
31
- updater.schedule is not used.
31
+ updater.schedule is not used. Intermediate subiteration images are not
32
+ written to disk; only the returned image is produced.
32
33
  """
33
34
  # Each pass rebuilds a reconstructor that restarts at subset 0, so a
34
35
  # remainder would use some subsets more often and others never.
@@ -51,6 +52,9 @@ def run_osem_with_corrections(
51
52
  reconstructor.set_objective_function(objective)
52
53
  reconstructor.set_num_subsets(num_subsets)
53
54
  reconstructor.set_num_subiterations(subiterations_per_update)
55
+ # STIR otherwise writes one Interfile image per subiteration into the
56
+ # process working directory. Callers only need the returned image.
57
+ reconstructor.disable_output()
54
58
  reconstructor.set_input(acquisition_data)
55
59
  reconstructor.set_up(image)
56
60
  reconstructor.reconstruct(image)
@@ -0,0 +1,153 @@
1
+ """Small projection-metric helpers used by the worked examples.
2
+
3
+ They are deliberately plain NumPy: the examples need a shared count scale, a
4
+ repeatability measure, a triple-energy-window estimate and a quadrature check,
5
+ not a reconstruction framework.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import numpy as np
11
+
12
+ from simind_python_connector.core.types import MAX_SOURCE
13
+
14
+
15
+ def projection_view0(projection: np.ndarray) -> np.ndarray:
16
+ """Return the first view of a 3-D or 4-D projection array."""
17
+ if projection.ndim == 4:
18
+ return projection[0, :, 0, :]
19
+ if projection.ndim == 3:
20
+ return projection[0, :, :]
21
+ raise ValueError(
22
+ f"Expected projection with 3 or 4 dimensions, got shape {projection.shape}"
23
+ )
24
+
25
+
26
+ def require_output_keys(outputs: dict[str, object], *keys: str) -> dict[str, object]:
27
+ """Return only the named outputs, or say which ones the run did not write."""
28
+ missing = [key for key in keys if key not in outputs]
29
+ if missing:
30
+ raise KeyError(
31
+ f"SIMIND did not write {', '.join(missing)}; it wrote "
32
+ f"{', '.join(sorted(outputs))}"
33
+ )
34
+ return {key: outputs[key] for key in keys}
35
+
36
+
37
+ def source_weights(source: np.ndarray, quantization_scale: float) -> np.ndarray:
38
+ """Return the integer photon-history weights the connector writes."""
39
+ source = np.asarray(source, dtype=float)
40
+ if source.size == 0:
41
+ raise ValueError("source must not be empty")
42
+ if not 0.0 < quantization_scale <= 1.0:
43
+ raise ValueError("quantization_scale must be in (0, 1]")
44
+ maximum = float(source.max())
45
+ if maximum == 0.0:
46
+ return np.zeros(source.shape, dtype=np.int64)
47
+ return np.clip(
48
+ np.round(source / maximum * (MAX_SOURCE * quantization_scale)),
49
+ 0,
50
+ MAX_SOURCE,
51
+ ).astype(np.int64)
52
+
53
+
54
+ def source_weight_sum(source: np.ndarray, quantization_scale: float) -> int:
55
+ """Number of photon histories per projection before the ``NN`` multiplier."""
56
+ return int(source_weights(source, quantization_scale).sum())
57
+
58
+
59
+ def zero_history_fraction(source: np.ndarray, quantization_scale: float) -> float:
60
+ """Fraction of the source weight that integer rounding sends to zero."""
61
+ source = np.asarray(source, dtype=float)
62
+ total = float(source.sum())
63
+ if total == 0.0:
64
+ return 0.0
65
+ weights = source_weights(source, quantization_scale)
66
+ return float(source[weights == 0].sum() / total)
67
+
68
+
69
+ def symmetric_limits(*images: np.ndarray) -> tuple[float, float]:
70
+ """A zero-centred colour scale that fits every image."""
71
+ if not images:
72
+ raise ValueError("symmetric_limits needs at least one image")
73
+ limit = max(float(np.max(np.abs(image))) for image in images)
74
+ if limit == 0.0:
75
+ return 0.0, 0.0
76
+ return -limit, limit
77
+
78
+
79
+ def normalised_rms_difference(a: np.ndarray, b: np.ndarray) -> float:
80
+ """RMS difference between two images, relative to their mean magnitude."""
81
+ a = np.asarray(a, dtype=float)
82
+ b = np.asarray(b, dtype=float)
83
+ if a.shape != b.shape:
84
+ raise ValueError(f"shape mismatch: {a.shape} and {b.shape}")
85
+ scale = float(np.mean(np.abs(a) + np.abs(b)) / 2.0)
86
+ if scale == 0.0:
87
+ return 0.0
88
+ return float(np.sqrt(np.mean((a - b) ** 2)) / scale)
89
+
90
+
91
+ def tew_scatter_estimate(
92
+ lower: np.ndarray,
93
+ upper: np.ndarray,
94
+ *,
95
+ lower_width_kev: float,
96
+ photopeak_width_kev: float,
97
+ upper_width_kev: float,
98
+ ) -> np.ndarray:
99
+ """Triple-energy-window scatter estimate for one photopeak.
100
+
101
+ Each side window is scaled to the photopeak width and half of the total is
102
+ assigned to the peak, which assumes the scatter spectrum is linear across
103
+ the three windows. That is an approximation, not a measurement.
104
+ """
105
+ widths = (lower_width_kev, photopeak_width_kev, upper_width_kev)
106
+ if any(width <= 0 for width in widths):
107
+ raise ValueError(f"window widths must be positive, got {widths}")
108
+ return np.asarray(lower) * (
109
+ photopeak_width_kev / (2.0 * lower_width_kev)
110
+ ) + np.asarray(upper) * (photopeak_width_kev / (2.0 * upper_width_kev))
111
+
112
+
113
+ def tew_correction(
114
+ photopeak: np.ndarray,
115
+ lower: np.ndarray,
116
+ upper: np.ndarray,
117
+ *,
118
+ lower_width_kev: float,
119
+ photopeak_width_kev: float,
120
+ upper_width_kev: float,
121
+ ) -> tuple[np.ndarray, np.ndarray, float, float]:
122
+ """Return (scatter estimate, corrected, clipped count fraction, clipped pixels).
123
+
124
+ The corrected image is the photopeak minus the estimate, clipped at zero.
125
+ The count fraction is the part of the photopeak that clipping removed;
126
+ the pixel fraction is the part of the image that the estimate exceeded.
127
+ """
128
+ photopeak = np.asarray(photopeak)
129
+ estimate = tew_scatter_estimate(
130
+ lower,
131
+ upper,
132
+ lower_width_kev=lower_width_kev,
133
+ photopeak_width_kev=photopeak_width_kev,
134
+ upper_width_kev=upper_width_kev,
135
+ )
136
+ unclipped = photopeak - estimate
137
+ corrected = np.clip(unclipped, a_min=0.0, a_max=None)
138
+ peak_total = float(np.clip(photopeak, 0.0, None).sum())
139
+ clipped_counts = float((corrected - unclipped).sum())
140
+ clipped_count_fraction = clipped_counts / peak_total if peak_total else 0.0
141
+ clipped_pixel_fraction = float(np.mean(unclipped < 0.0))
142
+ return estimate, corrected, clipped_count_fraction, clipped_pixel_fraction
143
+
144
+
145
+ def count_preservation_error(
146
+ outputs: dict[str, object], total_key: str, *component_keys: str
147
+ ) -> float:
148
+ """Relative difference between a total and the sum of its components."""
149
+ total = float(outputs[total_key].projection.sum())
150
+ components = sum(float(outputs[key].projection.sum()) for key in component_keys)
151
+ if total == 0.0:
152
+ return 0.0 if components == 0.0 else float("inf")
153
+ return (components - total) / total
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: simind-python-connector
3
- Version: 2.0.1
3
+ Version: 2.0.2
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>
@@ -33,8 +33,8 @@ Requires-Dist: scipy; extra == "dev"
33
33
  Requires-Dist: ruff; extra == "dev"
34
34
  Requires-Dist: sphinx; extra == "dev"
35
35
  Requires-Dist: sphinx-rtd-theme; extra == "dev"
36
+ Requires-Dist: Pillow; extra == "dev"
36
37
  Requires-Dist: sphinx-autodoc-typehints; extra == "dev"
37
- Requires-Dist: sphinx-gallery<0.23,>=0.22; extra == "dev"
38
38
  Requires-Dist: pooch>=1.8; extra == "dev"
39
39
  Requires-Dist: myst-parser; extra == "dev"
40
40
  Requires-Dist: pre-commit; extra == "dev"
@@ -58,15 +58,12 @@ SIMIND project and Lund University.
58
58
 
59
59
  ## Start here
60
60
 
61
- See the [workflow tutorial](docs/tutorial.rst) for an ordered path through a
62
- core simulation, known-activity NEMA phantom, reconstruction, scatter
63
- correction, and measured-data calibration. The [gallery](docs/auto_examples/index.rst)
64
- has every tutorial with its figures and printed output. Rendered documentation,
65
- including the gallery, is built from the tagged repository; source distributions
66
- contain the example sources rather than the generated gallery. The
67
- [documentation](https://simind-python-connector.readthedocs.io/) explains
68
- installation, geometry, normalisation, backend options, and the
69
- [observed SIMIND output-component mapping](docs/simind_validation.md).
61
+ The [workflow tutorial](docs/tutorial.rst) walks through a core simulation,
62
+ known-activity NEMA phantom, reconstruction, scatter correction, and
63
+ measured-data calibration; the [example pages](docs/example_pages/index.rst)
64
+ collect the tutorials and figures. The
65
+ [documentation](https://simind-python-connector.readthedocs.io/) covers
66
+ installation, geometry, normalisation and backends.
70
67
 
71
68
  Install the package and example plotting dependencies:
72
69
 
@@ -79,15 +76,15 @@ Select a SIMIND executable and its matching data directory. The binary and
79
76
  `simind` on `PATH`; set `SMC_DIR` when required by your installation.
80
77
 
81
78
  ```bash
82
- export SIMIND_BIN=/path/to/simind-v9/simind
83
- export PATH=/path/to/simind-v9:$PATH
84
- export SMC_DIR=/path/to/simind-v9/smc_dir/
85
- export SIMIND_VERSION=9
79
+ export SIMIND_BIN=/path/to/simind-v8/simind
80
+ export PATH=/path/to/simind-v8:$PATH
81
+ export SMC_DIR=/path/to/simind-v8/smc_dir/
82
+ export SIMIND_VERSION=8
86
83
  ```
87
84
 
88
- Select version `8` or `9` explicitly. The connector uses this choice to place
89
- voxel source and density maps in the same physical coordinates; the default
90
- is version 8. A constructor's `simind_version` overrides `SIMIND_VERSION`.
85
+ SIMIND v8 is the supported engine. A constructor's `simind_version` overrides
86
+ `SIMIND_VERSION`; v9 selection is experimental. See
87
+ [installation](docs/installation.rst).
91
88
 
92
89
  Use the installed connector directly:
93
90
 
@@ -96,7 +93,7 @@ import numpy as np
96
93
  from simind_python_connector import SimindPythonConnector, configs
97
94
 
98
95
  connector = SimindPythonConnector(
99
- configs.get("Example.yaml"), "output/basic", "case01", simind_version=9
96
+ configs.get("Example.yaml"), "output/basic", "case01", simind_version=8
100
97
  )
101
98
  source = np.zeros((32, 32, 32), dtype=np.float32)
102
99
  source[12:20, 12:20, 12:20] = 1
@@ -107,42 +104,26 @@ outputs = connector.run()
107
104
  print(outputs["tot_w1"].projection.shape)
108
105
  ```
109
106
 
110
- Run a complete core simulation example from a source checkout:
111
-
112
- ```bash
113
- python examples/getting_started/plot_01_basic_simulation.py
114
- ```
115
-
116
107
  The core connector does not require STIR, SIRF, or PyTomography. Install only
117
- the optional backend used by your workflow; see
118
- [installation](docs/installation.rst). The NEMA examples need the optional
119
- phantomgen package; the install command is in the tutorial.
108
+ the optional backend your workflow uses; the NEMA examples also need
109
+ phantomgen. See [installation](docs/installation.rst) and the tutorial.
120
110
 
121
111
  ## Example map
122
112
 
123
- - `examples/getting_started/plot_01`–`plot_06`: core NumPy simulations, windows, configurations, scoring,
124
- and density conversion.
125
- - `examples/backends/plot_07a` / `07b` / `07c`: STIR / SIRF / PyTomography adaptors.
126
- - `examples/backends/plot_08a` / `08b` / `08c`: DICOM setup for STIR / SIRF / PyTomography.
127
- - `examples/phantoms/plot_09` / `plot_10`: the SIMIND analytic phantom library,
128
- and a known-activity NEMA/IEC simulation reconstructed with SIRF OSEM.
129
- - `examples/mc_scatter/plot_11`–`plot_13`: Monte Carlo scatter in SIRF OSEM,
130
- scatter normalisation, and residual correction.
131
- - `examples/cil/plot_14` / `plot_15`: CIL scatter correction, and an
132
- RDP-regularised SVRG reconstruction.
133
- - `examples/real_data/plot_16a` / `16b` / `16c`: CT-to-SPECT conversion with
134
- STIR / SIRF / PyTomography for the measured Y-90 phantom.
135
- - `examples/real_data/plot_17`: the Y-90 effective attenuation energy with SIRF.
136
-
137
- The real-data pages are rendered case studies on restricted Y-90 input, not
138
- reproducible tutorials. See [datasets](docs/datasets.rst) for the availability
139
- statement.
140
-
141
- Lower `quantization_scale`, fewer projections, or a mostly-zero source map can
142
- make smoke runs faster. These settings reduce history statistics; they do not
143
- set projection counts. For quantitative output, use known activity and
144
- acquisition time or normalise against a measured or analytic reference. Keep
145
- `NN >= 1`.
113
+ - `examples/getting_started/` and `examples/phantoms/`: core simulations,
114
+ windows, configurations, scoring, density conversion and phantoms.
115
+ - `examples/backends/`: STIR, SIRF and PyTomography adaptors, plus the DICOM
116
+ projection recipe in `docs/example_pages/dicom_recipe.rst`.
117
+ - `examples/mc_scatter/` and `examples/cil/`: Monte Carlo scatter and
118
+ residual correction, including CIL.
119
+ - `examples/real_data/` and `docs/example_pages/y90_*.rst`: restricted Y-90
120
+ CT conversion and effective attenuation energy, from data that are not
121
+ redistributed; see [datasets](docs/datasets.rst).
122
+
123
+ Lower `quantization_scale`, fewer projections, or a mostly-zero source map make
124
+ smoke runs faster without changing projection counts. For quantitative output,
125
+ use known activity and acquisition time or normalise against a measured or
126
+ analytic reference. Keep `NN >= 1`.
146
127
 
147
128
  ## License
148
129
 
@@ -1,4 +1,4 @@
1
- simind_python_connector/__init__.py,sha256=Byn5tXEI9VX7TGkMKvxJ0wbkDxeXtj8hgijKtJLLNsk,1730
1
+ simind_python_connector/__init__.py,sha256=ISwXiFuVtdtlhjmW38_xOGAXKqmByMDxAEGdTYKmzFs,1730
2
2
  simind_python_connector/datasets.py,sha256=sOocwL2sHvjn03qaYEZXk--r9Nr92G8bWBVWMUIgSxc,2228
3
3
  simind_python_connector/normalisation.py,sha256=9cLibos1BYCTUSuanHVUTmzjaqP9N0lL-Of8Y7GEpB0,3527
4
4
  simind_python_connector/phantoms.py,sha256=IU12ET2bPzMtLo3LNXVl98HQHkIXgyNFtGnQ8JlXr0E,13513
@@ -15,7 +15,7 @@ simind_python_connector/configs/input.smc,sha256=oAY2JpwS6IBWZxM-rHdtqWNF4shrZTQ
15
15
  simind_python_connector/connectors/__init__.py,sha256=_pw86l4iOGONER1JJvKHFafj6bXBzHwSzo_D1GHSLkM,959
16
16
  simind_python_connector/connectors/_native_adaptor.py,sha256=TgQCInvYWcSKXaXyIZrAdUlauVi8BZl0ChT5JbzlVWc,19685
17
17
  simind_python_connector/connectors/base.py,sha256=XGGF7W7b5mr3yb2efTDOqvXXc6b8Y35KVhAMGHScLBI,1249
18
- simind_python_connector/connectors/python_connector.py,sha256=0bBPk-fPiV8wWEQ08c8XqRmAo4vSqd7qU_0NuzcWBgQ,58790
18
+ simind_python_connector/connectors/python_connector.py,sha256=Bi63nkmOphI_yEexmtK_bjKV6n04ZuImbinyhKoNhv8,58817
19
19
  simind_python_connector/connectors/pytomography_adaptor.py,sha256=L-v5wk3904svETRbg53oWpC28tyRLruGWODlZR50ytE,13521
20
20
  simind_python_connector/connectors/sirf_adaptor.py,sha256=xgAcbLtKTsDBzZPZdmnv_xYEZ4WRtQiH5ua7FBnSR8E,1274
21
21
  simind_python_connector/connectors/stir_adaptor.py,sha256=umvI8z8i0ims82-o2rvmFH4SfKGVFxaHn9cHOra5eOw,1439
@@ -32,16 +32,17 @@ simind_python_connector/data/bone.atn,sha256=ROadKIOQPqJ7HGAxl43UnN_YjZ8keyTIV8H
32
32
  simind_python_connector/data/h2o.atn,sha256=UplHE9XSSxcsx_cji-5ZtRXcYQYgeyVMzAp_FMOS3cg,1860
33
33
  simind_python_connector/recon/__init__.py,sha256=T5TnLOsAbrWv6GNcwrARKJLswAl3KzwVAkkVU-JvF3U,1010
34
34
  simind_python_connector/recon/cil.py,sha256=TuemfbLBTUYj1v-odl1RXOCnGCIozyp2lxFnfOsoJ5w,7000
35
- simind_python_connector/recon/corrections.py,sha256=8qHvFqGSdG4npafC0OLTogUHpY51svW07c6eaJBYscQ,5518
35
+ simind_python_connector/recon/corrections.py,sha256=B_QgDPFqWbwqsY77OQwiKN5HHHGflhpf7YczuFT3HAM,7813
36
36
  simind_python_connector/recon/diagnostics.py,sha256=BbTavxVEP2U7yA5zCuDonnqA92RarO_4L1gUt4gykN4,1588
37
- simind_python_connector/recon/osem.py,sha256=GRBcUUdBacGiUyDiEyeEbK0k7A955YeGOG5xUpGXxiM,2385
37
+ simind_python_connector/recon/osem.py,sha256=LGCXWW2OtczOAws2U53sd_OIeWzLVa03MQZg5OvQcDo,2676
38
38
  simind_python_connector/recon/projector.py,sha256=emCDxcciKL4a96e5Lgd9g3U4g5cvWb3hWygpGEWcHa8,4960
39
39
  simind_python_connector/recon/updates.py,sha256=IeseWjD052Jt5G312gyMPnkkG2PQaZMGonA2ymzJ6_M,2914
40
40
  simind_python_connector/utils/__init__.py,sha256=lcqGs6Fg36If1zyeUph0vMC_bQCwbFJFRw76RGcAio8,2112
41
41
  simind_python_connector/utils/interfile.py,sha256=toasBMC63qRh6HkSgmUQWrCvcYUyZK2tUf4WAqtdIkI,24665
42
+ simind_python_connector/utils/projection_metrics.py,sha256=xzVXJ_KweIGUwsjfeke2S_27W1L3VCt5qVtqB_9ZXZ8,5786
42
43
  simind_python_connector/utils/simind_utils.py,sha256=cIfpF5LQdFLZAWsqso0mYLGBPkIUB3H9YQdgt9UDb-o,3815
43
- simind_python_connector-2.0.1.dist-info/licenses/LICENSE,sha256=cCWAxo3f4AsMBTc9HmCzz4RiWcEW1m5hsTXSlMzKikE,10874
44
- simind_python_connector-2.0.1.dist-info/METADATA,sha256=grEm5DnnaUmeNMbLAKIzGSHbkxnt9pq1ZFALLIO0Qe4,6535
45
- simind_python_connector-2.0.1.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
46
- simind_python_connector-2.0.1.dist-info/top_level.txt,sha256=9JxsbTd91-eq8ppiYYspHX1tpNz1Mk81LC9pJ53SkWA,24
47
- simind_python_connector-2.0.1.dist-info/RECORD,,
44
+ simind_python_connector-2.0.2.dist-info/licenses/LICENSE,sha256=cCWAxo3f4AsMBTc9HmCzz4RiWcEW1m5hsTXSlMzKikE,10874
45
+ simind_python_connector-2.0.2.dist-info/METADATA,sha256=BHvC9nSM-10jtGkQ6W23kNbK9ZQ4tcRjmzBtjzYQcWc,5449
46
+ simind_python_connector-2.0.2.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
47
+ simind_python_connector-2.0.2.dist-info/top_level.txt,sha256=9JxsbTd91-eq8ppiYYspHX1tpNz1Mk81LC9pJ53SkWA,24
48
+ simind_python_connector-2.0.2.dist-info/RECORD,,