voidspace 0.1.4__tar.gz → 0.1.6__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.
Files changed (29) hide show
  1. {voidspace-0.1.4/src/voidspace.egg-info → voidspace-0.1.6}/PKG-INFO +1 -1
  2. {voidspace-0.1.4 → voidspace-0.1.6}/pyproject.toml +1 -1
  3. {voidspace-0.1.4 → voidspace-0.1.6}/src/voidspace/__init__.py +1 -1
  4. {voidspace-0.1.4 → voidspace-0.1.6}/src/voidspace/workflows.py +8 -2
  5. {voidspace-0.1.4 → voidspace-0.1.6/src/voidspace.egg-info}/PKG-INFO +1 -1
  6. {voidspace-0.1.4 → voidspace-0.1.6}/src/voidspace.egg-info/SOURCES.txt +1 -0
  7. voidspace-0.1.6/tests/test_version.py +11 -0
  8. {voidspace-0.1.4 → voidspace-0.1.6}/tests/test_workflows.py +41 -0
  9. {voidspace-0.1.4 → voidspace-0.1.6}/LICENSE +0 -0
  10. {voidspace-0.1.4 → voidspace-0.1.6}/README.md +0 -0
  11. {voidspace-0.1.4 → voidspace-0.1.6}/setup.cfg +0 -0
  12. {voidspace-0.1.4 → voidspace-0.1.6}/src/voidspace/change.py +0 -0
  13. {voidspace-0.1.4 → voidspace-0.1.6}/src/voidspace/cli.py +0 -0
  14. {voidspace-0.1.4 → voidspace-0.1.6}/src/voidspace/io.py +0 -0
  15. {voidspace-0.1.4 → voidspace-0.1.6}/src/voidspace/metrics.py +0 -0
  16. {voidspace-0.1.4 → voidspace-0.1.6}/src/voidspace/models.py +0 -0
  17. {voidspace-0.1.4 → voidspace-0.1.6}/src/voidspace/morphology.py +0 -0
  18. {voidspace-0.1.4 → voidspace-0.1.6}/src/voidspace/segment.py +0 -0
  19. {voidspace-0.1.4 → voidspace-0.1.6}/src/voidspace.egg-info/dependency_links.txt +0 -0
  20. {voidspace-0.1.4 → voidspace-0.1.6}/src/voidspace.egg-info/entry_points.txt +0 -0
  21. {voidspace-0.1.4 → voidspace-0.1.6}/src/voidspace.egg-info/requires.txt +0 -0
  22. {voidspace-0.1.4 → voidspace-0.1.6}/src/voidspace.egg-info/top_level.txt +0 -0
  23. {voidspace-0.1.4 → voidspace-0.1.6}/tests/test_aim_io.py +0 -0
  24. {voidspace-0.1.4 → voidspace-0.1.6}/tests/test_change.py +0 -0
  25. {voidspace-0.1.4 → voidspace-0.1.6}/tests/test_cli.py +0 -0
  26. {voidspace-0.1.4 → voidspace-0.1.6}/tests/test_metrics.py +0 -0
  27. {voidspace-0.1.4 → voidspace-0.1.6}/tests/test_models.py +0 -0
  28. {voidspace-0.1.4 → voidspace-0.1.6}/tests/test_morphology.py +0 -0
  29. {voidspace-0.1.4 → voidspace-0.1.6}/tests/test_segment.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: voidspace
3
- Version: 0.1.4
3
+ Version: 0.1.6
4
4
  Summary: Spacing-aware voidspace analysis for HR-pQCT segmentations
5
5
  Author: Matthias Walle
6
6
  License-Expression: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "voidspace"
7
- version = "0.1.4"
7
+ version = "0.1.6"
8
8
  description = "Spacing-aware voidspace analysis for HR-pQCT segmentations"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -12,7 +12,7 @@ from voidspace.models import (
12
12
  from voidspace.segment import segment_voidspace
13
13
  from voidspace.workflows import analyze_maps, compare, intersect_masks, run_case
14
14
 
15
- __version__ = "0.1.4"
15
+ __version__ = "0.1.6"
16
16
 
17
17
  __all__ = [
18
18
  "VoidspaceChangeMasks",
@@ -69,8 +69,14 @@ def intersect_masks(
69
69
  intersection, spacing, reference = read_mask(paths[0])
70
70
  for path in paths[1:]:
71
71
  mask, mask_spacing, mask_reference = read_mask(path)
72
- if mask_spacing != spacing:
73
- raise ValueError("all masks must have the same spacing")
72
+ # AIM header quantization and NIfTI float storage can differ below 1 nm.
73
+ # Tolerate that rounding, but do not silently mix scan resolutions.
74
+ if not np.allclose(mask_spacing, spacing, rtol=0, atol=1e-6):
75
+ raise ValueError(
76
+ f"mask spacing mismatch (z, y, x; mm): {paths[0]} has {spacing}; "
77
+ f"{path} has {mask_spacing}. Expected the matching native-space "
78
+ "common region at the same scan resolution."
79
+ )
74
80
  mask = _array_on_reference_grid(mask, mask_reference, reference)
75
81
  if mask.shape != intersection.shape:
76
82
  raise ValueError("all masks must overlap the reference image space")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: voidspace
3
- Version: 0.1.4
3
+ Version: 0.1.6
4
4
  Summary: Spacing-aware voidspace analysis for HR-pQCT segmentations
5
5
  Author: Matthias Walle
6
6
  License-Expression: MIT
@@ -23,4 +23,5 @@ tests/test_metrics.py
23
23
  tests/test_models.py
24
24
  tests/test_morphology.py
25
25
  tests/test_segment.py
26
+ tests/test_version.py
26
27
  tests/test_workflows.py
@@ -0,0 +1,11 @@
1
+ from pathlib import Path
2
+ import tomllib
3
+
4
+ import voidspace
5
+
6
+
7
+ def test_public_version_matches_package_version():
8
+ project = tomllib.loads(
9
+ (Path(__file__).resolve().parents[1] / "pyproject.toml").read_text()
10
+ )["project"]
11
+ assert voidspace.__version__ == project["version"]
@@ -116,6 +116,47 @@ def test_intersect_masks_writes_boolean_intersection(tmp_path):
116
116
  assert np.array_equal(result, full & common)
117
117
 
118
118
 
119
+ @pytest.mark.parametrize("shift", [0.0, 0.0607])
120
+ @pytest.mark.parametrize("source_spacing, common_spacing", [
121
+ ((0.0607,) * 3, (float(np.float32(0.0607)),) * 3),
122
+ ((0.06069900095462799, 0.06069900095462799, 0.06069599837064743),
123
+ (0.06069965288043022, 0.06069965288043022, 0.06069643050432205)),
124
+ ])
125
+ def test_intersect_masks_accepts_header_rounding_and_preserves_physical_placement(
126
+ tmp_path, shift, source_spacing, common_spacing,
127
+ ):
128
+ full = sitk.GetImageFromArray(np.ones((5, 5, 5), dtype=np.uint8))
129
+ full.SetSpacing(source_spacing)
130
+ sitk.WriteImage(full, str(tmp_path / "full.mha"))
131
+ common = sitk.GetImageFromArray(np.ones((3, 3, 3), dtype=np.uint8))
132
+ common.SetSpacing(common_spacing)
133
+ common.SetOrigin((shift,) * 3)
134
+ sitk.WriteImage(common, str(tmp_path / "common.nii.gz"))
135
+
136
+ output = intersect_masks(
137
+ [tmp_path / "full.mha", tmp_path / "common.nii.gz"],
138
+ output_path=tmp_path / "intersection.nii.gz",
139
+ )
140
+ image = sitk.ReadImage(str(output))
141
+ expected = np.zeros((5, 5, 5), dtype=bool)
142
+ start = 0 if shift == 0 else 1
143
+ expected[start:start + 3, start:start + 3, start:start + 3] = True
144
+ assert np.array_equal(sitk.GetArrayFromImage(image).astype(bool), expected)
145
+ assert np.allclose(image.GetSpacing(), source_spacing, rtol=0, atol=1e-8)
146
+
147
+
148
+ def test_intersect_masks_reports_genuinely_different_resolution(tmp_path):
149
+ for name, spacing in [("full", 0.0607), ("common", 0.082)]:
150
+ image = sitk.GetImageFromArray(np.ones((3, 3, 3), dtype=np.uint8))
151
+ image.SetSpacing((spacing,) * 3)
152
+ sitk.WriteImage(image, str(tmp_path / f"{name}.mha"))
153
+ with pytest.raises(ValueError, match="spacing.*full.*common"):
154
+ intersect_masks(
155
+ [tmp_path / "full.mha", tmp_path / "common.mha"],
156
+ output_path=tmp_path / "intersection.nii.gz",
157
+ )
158
+
159
+
119
160
  def test_intersect_masks_resamples_cropped_mask_to_reference_grid(tmp_path):
120
161
  full = np.ones((5, 5, 5), dtype=bool)
121
162
  common = np.ones((3, 3, 3), dtype=bool)
File without changes
File without changes
File without changes
File without changes
File without changes