Repository navigation
test: raise coverage 48% -> 78% (+653 tests), fix 13 bugs found on the way - #159
Conversation
…ge path `pyproject.toml` declares `python = "^3.9 || ..."` and CI runs a 3.9 leg, but two 3.10-only stdlib features had crept in: - `spine/spinestats/curvature.py` imported `itertools.pairwise` at module scope, so the whole module raised ImportError on 3.9. Ruff did not catch it (`target-version = "py310"`), and `test_docs_examples.py` swallowed it because its `_missing_module_name` check only fails for names starting with "TPTBox". Added a `pairwise` backport next to the existing `zip_strict` in `core/compat.py` and switched curvature over. - `zip(..., strict=...)` (3.10+, TypeError at call time on 3.9) in four places, one of them `measure_ivd_and_vertebra_geometry._segment_length`. Replaced with `zip_strict` where strictness was intended, plain `zip` otherwise. Tests: `pairwise` behaviour plus two tree-wide guards that fail if either pattern comes back. Both compat helpers verified against a real 3.9 interpreter. Also fixes the coverage `omit` entry `TPTBox/test/speedtest/*`, which matched nothing - the directory is `TPTBox/tests/speedtests/`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… in TTA Two defects found while writing tests for these modules. Both regression tests were verified to fail against the unfixed code. `stitching_tools._center_frontal` (active bug): the dorsal window is built as `middle - 122 .. middle + 134`, and `middle` is only ~0.47*size, so the start is negative for any volume below ~258 voxels (size=100 gave slice(-75, 181)). numpy reads a negative start as an offset from the end, so `NAKO_stitch_T2w` silently cropped the wrong side of any sub-258-voxel stitched volume. Now clamped to [0, size]; the tuned window for full-size volumes is unchanged (size=600 -> slice(163, 419), still 256 wide) and pinned by a test. `predictor._internal_maybe_mirror_and_predict` (latent): the method takes a `network` argument, uses it for the main forward pass, and then used `self.network` for the seven mirrored passes. On every current code path `network is self.network`, so no output was ever wrong - but the argument was silently ignored, which defeats the purpose of `loaded_networks` (one network per fold). Had that field ever held distinct per-fold objects, 7 of 8 TTA terms would have come from the wrong fold. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
unit_tests/test_bids_file.py covers this module through get_BIDS_test(), but all
four of its tests are gated on /media/robert/Expansion/dataset-Testset existing,
so they never run for anyone - which is why the BIDS parser the whole package
depends on sat at 39%. Two of them genuinely need JSON sidecars on disk, so
rather than un-gating them this adds a file that builds a real two-subject tree
(NIfTIs, sidecars, a POI json, a PNG) in a temp dir and drives scan -> query ->
family -> derived-path end to end, plus table-driven tests for the two pure
parsing primitives.
Three bugs surfaced and are fixed here, each with a regression test that was
verified to fail beforehand:
- Searchquery.filter_dixon_water/_fat/_outphase returned *nothing* whenever the
files had JSON sidecars: `_keys` lists alternative spellings of one channel
("W"/"WATER"), but the ImageType filter required `all` of them while the
entity filter next to it used `any`. No real ImageType carries both spellings.
- _scan_tree did not forward `filter_folder` to its recursive call, so any
filter_folder targeting a level below the subject folder was silently ignored.
The docstring example used `lvl != 2` for subject folders, which is also off
by one - subject folders are level 1 under <dataset>/<parent>. Both fixed.
- `formats` carried copy-paste duplicates (T1w, T2w, T2star and sti twice).
Note for reviewers: validate_entities registers unknown keys as a side effect
(`entities_keys[key] = key`) and get_values_from_name appends unknown formats to
`formats_relaxed`, so the same filename can validate differently depending on
what was parsed earlier in the process. The tests snapshot and restore those
containers in place; the behaviour itself needs an API decision and is left
alone.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
String_Logger accumulates into .log_content, so most of this needs neither files nor stdout capture: prefixing rules, the colored/plain buffer pair, the indentation context manager, statistics accumulation and the sub-logger flush/close propagation into a real file-backed Logger. indentation_level is a module global shared by every logger instance, so each test asserts it back to zero in tearDown. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…d truth curvature.py 11%->95%, pelvic_parameters.py 14%->100%, angles.py 31%->62% (the remainder of angles.py is its three plotting entry points). The sample data holds a single vertebra, so the spines are synthesised: bodies stacked along the superior axis, each carrying its direction POIs and the two endplate median points that MoveTo.TOP/BOTTOM resolve to, with per-vertebra rotations about any axis. That makes every expected value analytic rather than a recorded output: - a circle of radius r has curvature exactly 1/r, asserted for several radii and sampling steps, in the image plane and in a randomly tilted 3D plane (which is what proves the 3D cross-product branch agrees with the 2D one) - a 30 degree sagittal tilt of one vertebra gives a 30 degree P-direction angle; opposed 15 degree coronal tilts give a 30 degree Cobb angle with MoveTo.CENTER and 15 with the default endplate moves, because those average each normal with its neighbour - tilting one vertebra by 10 degrees gives segmental endplate angles of exactly +10 and -10 in the two adjacent segments, and zero everywhere else - pelvic incidence equals pelvic tilt plus sacral slope to 1e-9, is invariant under rotating all four landmarks in the sagittal plane (where PT and SS trade the rotation exactly), and is invariant under scaling - these pin the anatomical claims the docstring makes - a pure arc of radius 200 gives a mean curvature profile of 1/200 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…->86%) torso_vat_sat.py 4%->86%, body_quadrants.py 10%->96%. Inputs are synthetic labelled volumes with known voxel counts and a deliberately anisotropic zoom (2 x 2 x 5 mm), so every reported figure is analytic: - volumes are voxel count x 20 mm^3, areas are per-slice voxel count x 4 mm^2 - VBQ is asserted as an exact 0.5 by setting the vertebral bodies to 200 and the spinal canal to 400, and the robust CSF estimator is shown to hold at the 400 peak when part of the canal is darkened while the plain mean drops - fat fraction is asserted against fat/(fat+water) for a water/fat pair constructed to a target FF, including the all-IMAT and all-lean sides of the 0.20 threshold - peak_centered_mean is pinned on every documented edge case, including peak_frac_height=0 reducing exactly to the plain mean - tissue placed outside the ROI slab must not be counted, and the incomplete torso guards (missing clavicula / pelvis) return nan with a reason make_quadrants needs real vertebra subregions to build its local axes, so it runs on the shipped sample CT and asserts the 27 labels partition the body mask. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
measure_ivd_and_vertebra_geometry.py 20%->82%, poi_fun/endplates.py 8%->40%. The private helpers are pure geometry, so they are asserted against shapes with closed-form answers: a 10-voxel cube meshes to a watertight surface spanning exactly 10 mm whose |volume| is within 3% of 10^3 and whose projection along an axis is twice the 10x10 face; a ray through it measures 10 mm; PCA returns an orthonormal triple containing the box's long axis; Moeller-Trumbore hits the centroid at exactly the plane distance and misses on the parallel, outside and behind-origin cases; ray voxel sampling advances exactly one voxel per step and never leaves the volume. Three things found while writing this: - `rtree` was undeclared. trimesh's ray engine needs it for its broad phase and only declares it under its own `easy` extra, so every mesh ray cast raised ModuleNotFoundError - i.e. measure_ivd_and_vertebra_geometry and the endplate code could not run in a clean install (and did not run in 2 of 3 local envs here). Added to the core dependencies; the tests that need it are guarded so the suite still passes in an environment that has not reinstalled yet. - measure_ivd_and_vertebra_geometry's own documented default buffer_poi=None raised AssertionError: it asked the POI buffering decorator to write the result back unconditionally, and that decorator asserts the buffer path is not None. Now it only requests a write-back when there is a path to write to. The class-level call in the new tests uses the default, so this stays covered. - The folder README and all_output_reference both documented vert_geometry as `structure_label=50`; the module docstring and _run_all use 0 (labels above 0 and below 100). Docs corrected. Reported, not changed: endplates._ray_cast_to_mesh is commented "closest hit" but returns origin + d.mean() * direction, i.e. the mean over every hit along the ray, which for a convex mesh is the midpoint *inside* it rather than the surface point. Changing it would move every computed endplate point, so the current behaviour is pinned by a test that says so. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…61%) dicom_extract.py 11%->61%, dicom2nii_utils.py 18%->60%, nii2dicom.py 20%->83%, dicom_header_to_keys.py 7%->37%. Runs in under two seconds. No DICOM sample ships with the repo, so the integration test makes one: the sample CT is written out as a 73-slice DICOM series with nii2dicom and read back with extract_dicom_folder. That pins the full naming contract end to end - the output lands at rawdata/sub-TESTPAT/ct/sub-TESTPAT_sequ-7_acq-iso_ct.nii.gz, where sequ-7 comes from SeriesNumber and acq-iso from the grid - plus the array/zoom round-trip, the grid sidecar, the fact that the patient birth date is converted to an age and never written to disk, the zipped-input path, and that a second extraction is a no-op thanks to the .extract_cache marker. The pure helpers are tested directly: letter-suffix collision avoidance (a -> b -> ... -> z -> aa), BIDS path assembly including subject chunking and dot sanitisation, ImageType token filtering, echo splitting, DICOM magic-byte detection, json save/load with numpy scalars, and the age arithmetic on and around a birthday. pydicom and dicom2nifti move into the dev group so these tests run in every CI job rather than only where the optional "dicom" extra happens to be installed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…>80%/89%)
Both modules rewrite an already-extracted dataset and both offer dry_run, so the
plan is the assertion and the destructive path is then checked against it: the
tests build a small dataset of NIfTI + sidecar pairs in a temp dir, assert the
dry run moves nothing, apply it, and compare. Subject-map rebuilding is asserted
to be injective, idempotent across re-runs, and to preserve historic rows; the
sanitised entity values are round-tripped back through get_values_from_name so
they are provably parseable; _plane_from_grid is cross-checked against
get_plane_dicom on the same grids.
Two bugs found, both with a regression test that fails without the fix:
- xa_pairing.pair_biplane_xa moved data out of rawdata. It built the target
path with bf.get_changed_bids(info={"sequ": ...}), and that method's `parent`
defaults to "derivatives" - so applying the pairing relocated the B-side of
every biplane pair into derivatives/ with an otherwise correct filename. The
follow-up flip_z pass, which re-scans under `parent`, then no longer saw those
files, so a pair ended up with one flipped and one unflipped affine. Now
passes parent=bf.parent, and the test asserts every file stays in rawdata and
that both planes get flipped.
- rerun_bids_naming's dry run over-reported. The leftover sweep (pass 2) scans
the disk, and under dry_run the files pass 1 had only *planned* to move were
still there, so each was reported a second time with a different fallback
destination: 6 planned moves where the real run performs 4. The sweep now
skips sources pass 1 already claimed, and the test asserts the dry run plans
exactly what the real run does.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…Grid)
Fills in the methods the existing test_nii*.py files never reach, with inputs
small enough that the expected result can be written down: two seeds diffusing
from opposite ends of an 11-voxel corridor must split it by distance; a slab
truncated at a reference label must lose exactly the voxels above it; the centre
voxel of a cube must not appear among its surface points; a one-voxel hole must
be filled by majority voting; histogram matching must move a N(100,10) volume
onto a N(500,10) reference without touching the source; global_to_local_arr must
invert the affine and agree with the scalar version point by point.
Also covers the math mixin's untested half - mean/median/std against numpy,
clamp, nan_to_num, the three normalize variants, dice (1.0 for identical masks,
0.0 for disjoint ones), betti numbers, and the reflected, shift and in-place
operators - plus the Has_Grid accessors (dims, direction, orientation_ornt,
spacing, corners, obb quads, make_empty_nii, to_gird) and the first direct
construction of a bare Grid.
Bug found and fixed, with a regression test that fails without it: NII.to_stl
computed `out_path.with_name(f"{stem}_{label}.stl")` for `number_path=True` but
never assigned the result, so the numbering silently did nothing. Since
`to_stls` defaults `number_path` to True for a non-dict out_path, exporting a
multi-label segmentation wrote every label to the same file and only the last
label's mesh survived.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ray_casting.py 37%->84%, pixel_based_point_finder.py 44%->58% (measured together with the existing tests that reach these modules indirectly). Analytic expectations throughout: trilinear interpolation reproduces the linear field 2x + 3y - z exactly at sub-voxel points and returns 0 outside the interior; a ray from the centre of a box spanning indices 10..20 exits at 20.5; the two convex ray casters agree with each other to sub-voxel tolerance along four directions; on a box with a slab removed the stepping non-convex caster stops at the near edge of the gap while the bisecting convex one reports the far wall (which is the documented difference between them); a plane through three points splits a volume into two strict sides that, with the plane slice itself, account for every voxel; PCA on an elongated blob returns its long axis and orthogonal components. Recorded for the PR description, not changed: calculate_pca_normal_np fits the PCA on raw voxel indices and then divides the resulting unit vector by "zoom". So anisotropic voxels never change which axis comes out principal, the result is no longer a unit vector, and the division is the index-from-world direction rather than world-from-index. The test pins today's behaviour. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
save_load.py 60%->82%, poi_abstract.py 81%->92%, poi_global.py 75%->81%,
save_mkr.py 85%->86%, poi.py 74%->76% (measured together with the existing
unit_tests/test_poi*.py files).
test_poi_save.py already round-trips the current format; this covers the other
readers the loader still accepts and the parsers underneath them: the
colon-separated landmark TXT (group headers, point names, coordinate parsing,
and that blank/colonless lines are skipped), Slicer .mrk.json in both
directions, and all three save_hint formats. The header parser is pinned against
every example in its own docstring, including that "7.0" collapses to int.
For the descriptor: name resolution ("T12", "Vertebra_Corpus") to ids, the four
accepted key forms of unpack_poi_id (packed int, slice, int pair, enum pair),
get/pop/delitem/items_2d, nested-vs-flat input normalisation, the set operators
(+, >>, -, &), hand-computed point distances, and spline fitting through four
corpus points. apply_crop_reverse is asserted to be the exact inverse of
apply_crop, local_to_global_arr/global_to_local_arr to round-trip, and
POI_Global.to_cord_system to be an involution.
Two documented limitations are pinned rather than changed: save_hint=1 (the
Gruber format) raises KeyError for any subregion outside its fixed landmark
vocabulary, and _is_Point3D raises TypeError instead of returning False for a
non-container input.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…plumbing stitching.py 69%->73%, stitching_tools.py 22%->43%, sitk_utils.py 55%->70%, nii_help.py 56%->59%, slicer_nrrd.py 87%->88%, optional_deps.py 0%->97%. The stitching geometry is asserted as invariants rather than recorded numbers: for a diagonal, a double-flipped and a 13/-7/21-degree rotated anisotropic affine, the decomposition must give a non-negative spacing equal to the column norms, an orthonormal rotation, and rotation @ diag(spacing) == the original 3x3; feeding it back through get_ras_affine with the translation expressed in the rotated frame must reproduce the affine exactly. The union grid must cover both inputs' corners, take the finest input spacing, and honour min_spacing. _auto_output_dtype is pinned on the Philips case its docstring is about (int16 storage plus a large scl_slope must fall back to float32), which needed a file-backed fixture because the slope is read off the array proxy. sitk_utils is covered by round-trips (NII -> sitk -> NII preserves array, affine and orientation, for a reoriented input too) and by cross-checking the computed metadata against what SimpleITK itself reports. Note the two metadata functions do not share an argument order - the getter returns (origin, spacing, direction), the builder takes (spacing, direction, origin) - which the test now documents. optional_deps gets its first direct test: the stubs must raise on call or on instantiation rather than at creation, and must name the TPTBox extra when one provides the backend and the bare packages when none does (the spineps case). Two developer-facing rough edges pinned as-is: stitching.get_all_corner_points adds 1 to every voxel index before applying the affine, so the box it reports spans 1..shape+1 (every stitched FOV depends on this, so it is reported rather than changed), and stitching_tools._crop_borders logs a message for an unknown chunk key and then raises KeyError on the very next line. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
snapshot_modular.py 79%->86%, snapshot_templates.py 14%->32%. The pure helpers are asserted directly: normalize_image is linear and maps to [0, 1]; make_isotropic2d stretches the anisotropic axis and, in mask mode, must not invent labels (nearest neighbour, which is the real regression risk when rendering segmentations); div0 substitutes its fill for both array and scalar divisions by zero; get_contrasting_stroke_color picks gray on black and black on white; to_cdt maps an empty POI to None. create_snapshot renders the shipped sample CT to a temp PNG under the Agg backend, and the assertions are on the file: it exists, it is not blank (nonzero pixel variance), crop changes the output size, several output paths are written at once, None frames are skipped, and the same input renders byte-identically twice - which is what would catch unseeded jitter in the overlay code. Four of the snapshot_templates entry points are then smoke-rendered. Two documented-behaviour mismatches are pinned rather than changed: normalize_image's `v_range` is described as a clip range but only rescales (so out-of-range input lands outside [0, 1]), and a constant image yields NaN because min == max. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…d >= 2 Covers the four functions that had no test at all (np_isin including invert, np_calc_boundary_mask, np_voxel_connectivity_graph, np_filter_connected_components_by_bbox_chain) plus the uncovered branches of np_calc_convex_hull and np_erode_msk_euclid. np_isin is checked against np.isin as the reference over random label arrays, and a 10-voxel cube eroded by 2 is asserted to leave exactly a 6-voxel cube. Bug found and fixed, with a regression test that fails without it: np_calc_boundary_mask built its foreground mask with two in-place assignments, `boundary[boundary > threshold] = 2` followed by `boundary[boundary <= threshold] = 0`. The second line compares the sentinel 2 written by the first, so for any threshold >= 2 the foreground was erased again and the function returned an empty mask. NII.boundary_mask(threshold) forwards straight to it, so every realistic HU threshold was affected; only thresholds below 2 happened to work. The mask is now computed before the sentinel is written. Also dropped a duplicated (0, 1, 0) entry from the flood-fill neighbourhood list (no behaviour change - visited voxels are marked before being pushed). Documented, not changed: np_erode_msk, np_erode_msk_euclid and np_dilate_msk_euclid all modify the caller's array in place as well as returning it, with or without use_crop. That is consistent across the family and the NII wrappers rely on it, so a test now pins it where a reader will find it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…sts (12%->63%) _rib_assign.py 12%->63%. This is the pure-numpy half of the rib sub-package - no torch, no spineps - so the tests run everywhere. Writing them showed the public entry point raises before returning, on two separate lines: - `matched = (rib_inst != 0) & (rib_inst != error_value)` combined two bool-backed NIIs, and NII.__and__ raises TypeError for a non-integer array. The comparison now happens on the arrays. - `sem_seg.extract_label([Location.Rib_Left.value, Location.Rib_Right.value] == 1)` has its closing bracket one token too far left, so it evaluated `[63, 64] == 1` -> False -> extract_label(0), which trips the "zero label is the background" assertion. Bracket moved. With both fixed the function runs to completion, merges Rib_Left / Rib_Right into the semantic segmentation over exactly the rib mask's footprint, and leaves the vertebra labels intact - all asserted. What is deliberately *not* asserted is that the matching heuristic pairs every rib with a vertebra: on synthetic geometry it does not, and those components come back marked with `error_value`, which is the documented behaviour for unmatched ribs. Whether real data matches better is untested here. The helpers get exact expectations: _thoracic_like_labels keeps 7..20 and appends 28, and _touching_surface is checked against its own default axis weights (left-right contact counts 100x a superior-inferior contact), for symmetry, and for scaling with the shared face area. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
point_registration.py 66%->88%. Rigid point registration needs only SimpleITK, so unlike the rest of TPTBox/registration these run in every environment. Expectations are known transforms and inverse properties rather than recorded numbers: six landmarks related by an exact translation, or by an exact 12 degree rotation about their centroid, must be recovered with a sub-0.001 mm residual and map onto the fixed set to within 0.05 mm; transform_cord composed with transform_cord_inverse must be the identity over 20 random coordinates; transform_poi_inverse must undo transform_poi; save/load and get_dump/load_ must reproduce the same affine, residuals and coordinate mapping. The save/load round-trip is also run with *different* grids on the fixed and moving side, which is the only arrangement in which a moving/fixed swap inside load_() would be observable - a symmetric fixture cannot catch it. One corrupted landmark is used to show that both `exclusion` and `leave_worst_percent_out` restore the clean fit, and error_natural is pinned to what it actually measures: the mean change in pairwise landmark distances, i.e. non-rigidity. It stays at zero for any rigid motion however large, and only grows once the point set is genuinely deformed (here, scaled). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
inference_nnunet.py 9%->51%, inference_api.py 12%->56%, vibeseg.py 24%->79%, auto_download.py 27%->55%, _deface.py 13%->48%, spineps.py 13%->25%. No weights, no GPU, no network. The mock sits at the model boundary and nowhere else. For run_inference only `predict_single_npy_array` is replaced, so the real code still builds the channel axis, derives the properties dict, and converts the returned array back into an NII - and the assertions are on exactly that: the result carries the input's shape, affine and orientation, the planted labels survive, the predictor is handed a 4-D array with one channel per input image, and the spacing it receives is the input's own reversed (or the PIR one when reorient_PIR is set). For run_inference_on_file, load_inf_model and run_inference are patched where they are defined (the function imports them inside its own body), and the test then checks the written file, the label mapping, the preserved grid, and that a second call short-circuits unless override=True. The pure parts need no mock: estimate_peak_ram_mb is checked against the formula in its own docstring and for linear scaling; compute_cpu_chunks_for_ram is checked to return a chunk count that actually fits the budget; _split_ranges is checked to have read windows that overlap and write windows that tile the axis exactly once; sliding_nd_slices reconstructs its input under the identity; extract_vertebra_bodies_from_VibeSeg labels a six-body column exactly L5..T12; _extend_mask grows only along the requested anatomical direction; deface_img blanks exactly the masked voxels. spineps.py is driven through patch.dict(sys.modules) - it imports spineps only inside function bodies - so its dispatch is testable without torch, and _run_spineps_all is checked with subprocess.run patched. auto_download is asserted *not* to reach the network when the weights folder already exists. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
plot_compute_lordosis_and_kyphosis and plot_cobb_angle are asserted to return exactly the same numbers as the bare computations and to render a PNG, under the Agg backend. Getting there needed the fixture to be right in two ways that are worth writing down: the POI has to sit on the image's own grid (the renderers resample between the two) and its spine has to run along the image's inferior axis, because the snapshot code interpolates along the first coordinate axis. So the synthetic spine is built in RAS, where the builder stacks along S, and then reoriented onto the sample CT's PIR grid. plot_cobb_and_lordosis_and_kyphosis stays uncovered: its resampling is singular for a synthetic spine on the shipped single-vertebra sample, and a fixture contrived to get past that would not be testing anything real. Also guards the to_stl tests on numpy-stl being importable, matching how test_mesh3D.py guards its optional backends - two of the three local environments have a partial install and were erroring instead of skipping. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`[tool.coverage.report].omit` held exactly one entry and it matched nothing - `TPTBox/test/speedtest/*`, where the real directory is `TPTBox/tests/speedtests/` (fixed earlier in this branch). It now lists, with a justification per group, only code a unit test cannot legitimately reach: - the vendored nnU-Net inference internals (predictor, plans_handler, export_prediction, default_preprocessor, data_iterators, get_network_from_plans, sliding_window_prediction) and the nvidia-smi memory prober. TPTBox's own entry point around them, nnUnet_utils/inference_api.py, is deliberately NOT omitted and is tested with a mock at the model boundary. - the nnU-Net training drivers, which need a populated nnUNet_raw tree and a GPU - registration/script_ax2sag.py and _ridged_intensity/register.py, which import `nipy` - not a declared dependency and not installable on current Python - the dead vendored deepali engine (_deformable/_deepali/* plus deformable_reg_old.py, grid_search.py, _grid_search_vert.py): it imports an undeclared `yaml` and nothing in the public API reaches it, those modules are referenced only by each other. A candidate for deletion rather than coverage. - the NAKO dataset pipelines (interactive input() prompts, hard-coded /DATA/NAS roots, process pools over a private dataset, Excel reporting) - core/dicom/fix_brocken.py, an in-place repair script Anything merely awkward to test got a test instead, and both numbers are reported so the two effects stay separable. CI: tests.yml gains `build-full-coverage`, one ubuntu/py3.11 job that installs CPU-only torch plus nnunetv2, acvl-utils, batchgenerators, antspyx and hf-deepali, so the modules behind those imports stop being hard zeros on Codecov. It prints the coverage number with and without the omit list and uploads with `flags: full`; the existing matrix now uploads with `flags: light` and stays the fast compatibility gate. tests_mr.yml gains the same dependency set as a gate without upload, so a PR that breaks a mocked torch/deepali test fails before merge rather than after. `spineps` is deliberately not installed anywhere: it depends on TPTBox itself, so installing it would pin a released TPTBox over the checkout. CLAUDE.md: the Test Layout section claimed the generated test files are >20K LOC (the largest is 970 lines). Replaced with what is actually true about them, the house style for new tests, the Python 3.9 constraints, and the omit-list policy. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The filter_folder docstring note added earlier in this branch broke the blank line after the Args section. Reflowed; no behaviour change. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Benchmark comparisonSpeed (wall time per call)baseline Showing the 5 most-changed measurements per case; the rest are collapsed. Rows with a baseline below 3 ms are tagged
|
| Measurement | baseline ms (median ±½·range) | head ms (median ±½·range) | Δ % | p |
|---|---|---|---|---|
nii_dilate_msk |
124.21 ±3.88 | 126.86 ±0.46 | +2.1% | 0.713 |
poi_calc_centroids_nocrop |
4.08 ±0.09 | 4.14 ±0.05 | +1.5% | 0.546 |
nii_rescale_seg |
3.05 ±0.05 | 3.01 ±0.05 | -1.2% | 0.265 |
nii_load_img |
10.55 ±0.05 | 10.68 ±0.10 | +1.2% | 0.057 |
nii_fill_holes |
4.39 ±0.10 | 4.36 ±0.01 | -0.8% | 0.245 |
… 31 more measurements
| Measurement | baseline ms (median ±½·range) | head ms (median ±½·range) | Δ % | p |
|---|---|---|---|---|
nii_rescale |
39.53 ±0.26 | 39.26 ±0.13 | -0.7% | 0.543 |
nii_erode_msk |
6.00 ±0.05 | 5.96 ±0.04 | -0.6% | 0.365 |
nii_save |
11.42 ±0.10 | 11.44 ±0.12 | +0.2% | 0.528 |
nii_resample_from_to |
41.41 ±0.32 | 41.32 ±0.10 | -0.2% | 0.163 |
nii_filter_connected_components |
4.12 ±0.04 | 4.13 ±0.04 | +0.2% | 0.934 |
poi_calc_poi_from_subreg_vert |
12.37 ±0.07 | 12.38 ±0.09 | +0.1% | 0.943 |
metric_voxels |
250463.00 ±0.00 | 250463.00 ±0.00 | +0.0% | — |
metric_labels |
3.00 ±0.00 | 3.00 ±0.00 | +0.0% | — |
metric_foreground_pct |
14.51 ±0.00 | 14.51 ±0.00 | +0.0% | — |
poi_map_labels |
0.13 ±0.01 | 0.15 ±0.01 | +13.8% (noise) | 0.154 |
poi_save |
0.33 ±0.03 | 0.38 ±0.01 | +13.7% (noise) | 0.226 |
nii_apply_crop |
0.52 ±0.03 | 0.56 ±0.04 | +8.3% (noise) | 0.053 |
nii_load_seg |
2.53 ±0.08 | 2.72 ±0.10 | +7.8% (noise) | 0.019 |
poi_to_global |
0.16 ±0.02 | 0.15 ±0.01 | -5.4% (noise) | 0.971 |
nii_set_dtype |
0.38 ±0.02 | 0.40 ±0.02 | +4.8% (noise) | 0.185 |
nii_reorient |
0.65 ±0.02 | 0.68 ±0.02 | +4.7% (noise) | 0.059 |
nii_compute_crop |
0.27 ±0.01 | 0.28 ±0.01 | +4.6% (noise) | 0.232 |
nii_get_array |
0.03 ±0.00 | 0.03 ±0.00 | +4.2% (noise) | 0.297 |
poi_local_to_global_arr |
0.27 ±0.01 | 0.26 ±0.01 | -3.3% (noise) | 0.994 |
poi_reorient |
0.33 ±0.02 | 0.34 ±0.03 | +2.9% (noise) | 0.393 |
nii_unique |
0.60 ±0.02 | 0.61 ±0.02 | +2.6% (noise) | 0.633 |
poi_calc_centroids |
2.30 ±0.04 | 2.35 ±0.05 | +2.3% (noise) | 0.040 |
nii_pad_to |
0.51 ±0.01 | 0.52 ±0.03 | +1.9% (noise) | 0.286 |
poi_load |
0.89 ±0.02 | 0.91 ±0.02 | +1.9% (noise) | 0.043 |
nii_volumes |
1.78 ±0.02 | 1.80 ±0.02 | +1.3% (noise) | 0.190 |
nii_get_connected_components |
2.10 ±0.03 | 2.08 ±0.02 | -1.0% (noise) | 0.683 |
poi_rescale |
0.16 ±0.01 | 0.16 ±0.01 | -0.7% (noise) | 0.838 |
nii_extract_label |
0.32 ±0.05 | 0.32 ±0.02 | +0.5% (noise) | 0.829 |
nii_map_labels |
0.90 ±0.03 | 0.90 ±0.02 | +0.4% (noise) | 0.936 |
poi_resample_from_to |
0.55 ±0.02 | 0.55 ±0.03 | -0.2% (noise) | 0.749 |
nii_center_of_masses |
1.67 ±0.01 | 1.67 ±0.02 | -0.0% (noise) | 0.604 |
ct_2d — shape (73, 47, 1)
| Measurement | baseline ms (median ±½·range) | head ms (median ±½·range) | Δ % | p |
|---|---|---|---|---|
nii_resample_from_to |
7.37 ±0.08 | 7.47 ±0.09 | +1.4% | 0.312 |
nii_rescale |
5.26 ±0.08 | 5.21 ±0.11 | -0.9% | 0.947 |
nii_dilate_msk |
14.61 ±0.11 | 14.65 ±0.09 | +0.3% | 0.869 |
metric_voxels |
3431.00 ±0.00 | 3431.00 ±0.00 | +0.0% | — |
metric_labels |
3.00 ±0.00 | 3.00 ±0.00 | +0.0% | — |
… 30 more measurements
| Measurement | baseline ms (median ±½·range) | head ms (median ±½·range) | Δ % | p |
|---|---|---|---|---|
metric_foreground_pct |
37.60 ±0.00 | 37.60 ±0.00 | +0.0% | — |
nii_volumes |
0.28 ±0.02 | 0.26 ±0.01 | -6.9% (noise) | 0.192 |
nii_extract_label |
0.39 ±0.01 | 0.41 ±0.01 | +5.8% (noise) | 0.160 |
poi_save |
0.47 ±0.02 | 0.45 ±0.02 | -4.7% (noise) | 0.101 |
nii_center_of_masses |
0.20 ±0.01 | 0.19 ±0.01 | -4.3% (noise) | 0.645 |
nii_load_img |
1.38 ±0.03 | 1.44 ±0.05 | +4.0% (noise) | 0.139 |
nii_apply_crop |
0.63 ±0.01 | 0.65 ±0.03 | +3.9% (noise) | 0.109 |
nii_unique |
0.11 ±0.01 | 0.12 ±0.02 | +3.8% (noise) | 0.314 |
nii_map_labels |
0.44 ±0.02 | 0.46 ±0.03 | +3.8% (noise) | 0.203 |
poi_calc_centroids |
0.93 ±0.02 | 0.90 ±0.01 | -3.6% (noise) | 0.027 |
nii_get_array |
0.01 ±0.00 | 0.01 ±0.01 | +3.2% (noise) | 0.627 |
poi_rescale |
0.23 ±0.01 | 0.22 ±0.01 | -3.1% (noise) | 0.093 |
poi_load |
0.98 ±0.03 | 1.01 ±0.04 | +2.9% (noise) | 0.390 |
poi_local_to_global_arr |
0.31 ±0.01 | 0.31 ±0.01 | -2.7% (noise) | 0.098 |
nii_fill_holes |
0.81 ±0.02 | 0.83 ±0.04 | +2.6% (noise) | 0.221 |
nii_set_dtype |
0.38 ±0.01 | 0.37 ±0.03 | -2.6% (noise) | 0.946 |
poi_to_global |
0.21 ±0.01 | 0.20 ±0.00 | -2.5% (noise) | 0.296 |
nii_filter_connected_components |
0.82 ±0.03 | 0.80 ±0.03 | -2.4% (noise) | 0.337 |
nii_compute_crop |
0.13 ±0.01 | 0.13 ±0.00 | +2.3% (noise) | 0.842 |
poi_map_labels |
0.18 ±0.01 | 0.17 ±0.00 | -2.1% (noise) | 0.189 |
nii_erode_msk |
0.94 ±0.03 | 0.96 ±0.02 | +1.9% (noise) | 0.288 |
nii_load_seg |
1.59 ±0.04 | 1.62 ±0.03 | +1.8% (noise) | 0.106 |
nii_rescale_seg |
0.73 ±0.03 | 0.72 ±0.02 | -1.6% (noise) | 0.828 |
nii_get_connected_components |
0.59 ±0.03 | 0.60 ±0.02 | +1.5% (noise) | 0.203 |
nii_save |
1.29 ±0.04 | 1.27 ±0.02 | -1.3% (noise) | 0.240 |
nii_reorient |
0.81 ±0.04 | 0.80 ±0.03 | -1.3% (noise) | 0.386 |
poi_calc_centroids_nocrop |
1.09 ±0.02 | 1.10 ±0.02 | +1.3% (noise) | 0.151 |
poi_reorient |
0.42 ±0.02 | 0.41 ±0.01 | -0.9% (noise) | 0.971 |
poi_resample_from_to |
0.68 ±0.02 | 0.69 ±0.04 | +0.7% (noise) | 0.258 |
nii_pad_to |
0.53 ±0.01 | 0.53 ±0.02 | -0.3% (noise) | 0.862 |
mri_3d — shape (68, 52, 67)
| Measurement | baseline ms (median ±½·range) | head ms (median ±½·range) | Δ % | p |
|---|---|---|---|---|
nii_rescale_seg |
3.08 ±0.07 | 3.26 ±0.06 | +6.0% | 0.011 |
nii_dilate_msk |
160.34 ±0.70 | 164.30 ±0.72 | +2.5% | 0.000 |
nii_filter_connected_components |
4.65 ±0.04 | 4.54 ±0.04 | -2.4% | 0.005 |
nii_load_img |
8.69 ±0.08 | 8.53 ±0.04 | -1.8% | 0.011 |
nii_rescale |
37.24 ±0.18 | 37.71 ±0.21 | +1.3% | 0.009 |
… 31 more measurements
| Measurement | baseline ms (median ±½·range) | head ms (median ±½·range) | Δ % | p |
|---|---|---|---|---|
nii_resample_from_to |
39.27 ±0.15 | 39.70 ±0.27 | +1.1% | 0.039 |
nii_erode_msk |
7.66 ±0.09 | 7.75 ±0.28 | +1.1% | 0.326 |
nii_fill_holes |
5.31 ±0.04 | 5.27 ±0.05 | -0.7% | 0.523 |
poi_calc_poi_from_subreg_vert |
11.76 ±0.06 | 11.74 ±0.08 | -0.2% | 0.998 |
poi_calc_centroids_nocrop |
8.49 ±0.08 | 8.50 ±0.02 | +0.1% | 0.464 |
nii_save |
9.15 ±0.02 | 9.16 ±0.03 | +0.1% | 0.913 |
metric_voxels |
236912.00 ±0.00 | 236912.00 ±0.00 | +0.0% | — |
metric_labels |
8.00 ±0.00 | 8.00 ±0.00 | +0.0% | — |
metric_foreground_pct |
18.21 ±0.00 | 18.21 ±0.00 | +0.0% | — |
poi_to_global |
0.24 ±0.01 | 0.25 ±0.01 | +5.7% (noise) | 0.084 |
poi_save |
0.51 ±0.02 | 0.54 ±0.02 | +5.6% (noise) | 0.208 |
nii_get_array |
0.05 ±0.00 | 0.05 ±0.00 | +5.0% (noise) | 0.751 |
poi_resample_from_to |
0.71 ±0.01 | 0.75 ±0.04 | +4.9% (noise) | 0.065 |
poi_local_to_global_arr |
0.33 ±0.01 | 0.31 ±0.01 | -4.2% (noise) | 0.230 |
nii_load_seg |
2.79 ±0.05 | 2.70 ±0.05 | -3.2% (noise) | 0.015 |
nii_apply_crop |
0.68 ±0.01 | 0.66 ±0.04 | -2.7% (noise) | 0.621 |
nii_extract_label |
0.44 ±0.02 | 0.43 ±0.02 | -2.5% (noise) | 0.771 |
poi_reorient |
0.44 ±0.02 | 0.45 ±0.01 | +2.2% (noise) | 0.353 |
nii_set_dtype |
0.49 ±0.00 | 0.50 ±0.01 | +1.9% (noise) | 0.048 |
poi_load |
2.26 ±0.04 | 2.22 ±0.02 | -1.7% (noise) | 0.160 |
poi_map_labels |
0.22 ±0.01 | 0.22 ±0.01 | -1.3% (noise) | 0.702 |
nii_unique |
0.64 ±0.01 | 0.64 ±0.01 | +1.1% (noise) | 0.530 |
poi_calc_centroids |
2.71 ±0.02 | 2.72 ±0.04 | +0.7% (noise) | 0.259 |
poi_rescale |
0.26 ±0.01 | 0.26 ±0.01 | -0.7% (noise) | 0.951 |
nii_center_of_masses |
1.96 ±0.02 | 1.97 ±0.01 | +0.6% (noise) | 0.909 |
nii_get_connected_components |
2.14 ±0.04 | 2.15 ±0.02 | +0.5% (noise) | 0.738 |
nii_compute_crop |
0.32 ±0.01 | 0.32 ±0.02 | -0.3% (noise) | 0.200 |
nii_reorient |
0.85 ±0.04 | 0.86 ±0.03 | +0.2% (noise) | 0.568 |
nii_volumes |
1.92 ±0.02 | 1.92 ±0.03 | +0.2% (noise) | 0.187 |
nii_pad_to |
0.69 ±0.02 | 0.69 ±0.03 | -0.1% (noise) | 0.698 |
nii_map_labels |
0.98 ±0.03 | 0.98 ±0.03 | +0.0% (noise) | 0.957 |
mri_2d — shape (68, 52, 1)
| Measurement | baseline ms (median ±½·range) | head ms (median ±½·range) | Δ % | p |
|---|---|---|---|---|
nii_dilate_msk |
19.42 ±0.04 | 20.81 ±3.12 | +7.2% | 0.125 |
nii_resample_from_to |
7.26 ±0.04 | 7.42 ±0.06 | +2.2% | 0.000 |
nii_rescale |
5.30 ±0.04 | 5.29 ±0.05 | -0.3% | 0.858 |
metric_voxels |
3536.00 ±0.00 | 3536.00 ±0.00 | +0.0% | — |
metric_labels |
7.00 ±0.00 | 7.00 ±0.00 | +0.0% | — |
… 30 more measurements
| Measurement | baseline ms (median ±½·range) | head ms (median ±½·range) | Δ % | p |
|---|---|---|---|---|
metric_foreground_pct |
50.17 ±0.00 | 50.17 ±0.00 | +0.0% | — |
nii_get_array |
0.01 ±0.00 | 0.02 ±0.00 | +22.3% (noise) | 0.180 |
nii_rescale_seg |
0.76 ±0.01 | 0.71 ±0.02 | -7.2% (noise) | 0.004 |
poi_local_to_global_arr |
0.30 ±0.01 | 0.32 ±0.01 | +5.6% (noise) | 0.008 |
nii_get_connected_components |
0.58 ±0.02 | 0.61 ±0.06 | +5.3% (noise) | 0.105 |
nii_filter_connected_components |
0.81 ±0.03 | 0.85 ±0.02 | +4.4% (noise) | 0.024 |
nii_extract_label |
0.39 ±0.02 | 0.41 ±0.03 | +4.0% (noise) | 0.418 |
nii_compute_crop |
0.13 ±0.01 | 0.13 ±0.00 | +3.5% (noise) | 0.070 |
nii_reorient |
0.79 ±0.02 | 0.81 ±0.01 | +3.3% (noise) | 0.006 |
nii_set_dtype |
0.39 ±0.03 | 0.38 ±0.02 | -2.9% (noise) | 0.821 |
poi_resample_from_to |
0.73 ±0.03 | 0.75 ±0.03 | +2.2% (noise) | 0.901 |
poi_map_labels |
0.22 ±0.02 | 0.21 ±0.01 | -2.0% (noise) | 0.755 |
poi_calc_centroids_nocrop |
1.39 ±0.02 | 1.42 ±0.05 | +2.0% (noise) | 0.207 |
nii_center_of_masses |
0.41 ±0.01 | 0.40 ±0.00 | -1.9% (noise) | 0.415 |
nii_pad_to |
0.52 ±0.01 | 0.53 ±0.04 | +1.7% (noise) | 0.808 |
poi_save |
0.52 ±0.01 | 0.51 ±0.02 | -1.5% (noise) | 0.363 |
poi_load |
1.99 ±0.12 | 2.02 ±0.05 | +1.4% (noise) | 0.937 |
nii_save |
1.27 ±0.02 | 1.25 ±0.01 | -1.4% (noise) | 0.325 |
poi_reorient |
0.46 ±0.02 | 0.47 ±0.01 | +1.3% (noise) | 0.274 |
nii_apply_crop |
0.63 ±0.02 | 0.63 ±0.03 | -1.3% (noise) | 0.449 |
nii_unique |
0.11 ±0.01 | 0.11 ±0.00 | +1.3% (noise) | 0.528 |
poi_rescale |
0.25 ±0.01 | 0.25 ±0.01 | +1.0% (noise) | 0.101 |
nii_volumes |
0.44 ±0.02 | 0.44 ±0.01 | +0.9% (noise) | 0.522 |
nii_load_img |
1.42 ±0.02 | 1.40 ±0.03 | -0.8% (noise) | 0.793 |
nii_map_labels |
0.47 ±0.02 | 0.46 ±0.01 | -0.8% (noise) | 0.963 |
nii_fill_holes |
1.09 ±0.03 | 1.09 ±0.02 | +0.6% (noise) | 0.275 |
poi_to_global |
0.24 ±0.01 | 0.24 ±0.01 | +0.4% (noise) | 0.751 |
nii_erode_msk |
1.27 ±0.01 | 1.27 ±0.03 | +0.3% (noise) | 0.481 |
nii_load_seg |
1.59 ±0.05 | 1.59 ±0.05 | -0.2% (noise) | 0.182 |
poi_calc_centroids |
1.11 ±0.02 | 1.11 ±0.04 | -0.1% (noise) | 0.276 |
Gate: a measurement fails when the baseline is ≥ 1 ms, the median grows by ≥ 50%, and Welch's t-test gives p < 0.05. metric_* rows are context only.
Memory (peak RSS growth per call)
baseline fbb3d0f vs head fbb3d0f · python 3.11.17 · 5 repeats + 1 warmup · sampler proc-status, isolation fork · measurement floor ≈ 0.90 MiB
Showing the 5 most-changed measurements per case; the rest are collapsed. Rows with a baseline below 3 MiB are tagged (noise) — runner jitter on those swamps any real change.
ct_3d — shape (73, 47, 73)
| Measurement | baseline MiB (median ±½·range) | head MiB (median ±½·range) | Δ % | p |
|---|---|---|---|---|
poi_resample_from_to |
5.32 ±0.00 | 5.18 ±0.00 | -2.7% | — |
nii_reorient |
5.95 ±0.00 | 5.80 ±0.00 | -2.4% | — |
nii_rescale |
6.21 ±0.00 | 6.07 ±0.00 | -2.3% | — |
nii_resample_from_to |
6.34 ±0.00 | 6.19 ±0.00 | -2.3% | — |
poi_local_to_global_arr |
3.02 ±0.00 | 2.96 ±0.00 | -2.1% | — |
… 31 more measurements
| Measurement | baseline MiB (median ±½·range) | head MiB (median ±½·range) | Δ % | p |
|---|---|---|---|---|
nii_unique |
3.02 ±0.00 | 2.96 ±0.00 | -2.1% | — |
nii_set_dtype |
5.29 ±0.00 | 5.18 ±0.00 | -2.0% | — |
poi_to_global |
3.14 ±0.00 | 3.08 ±0.00 | -2.0% | — |
nii_extract_label |
5.41 ±0.00 | 5.30 ±0.00 | -1.9% | — |
nii_pad_to |
5.47 ±0.00 | 5.36 ±0.00 | -1.9% | — |
nii_map_labels |
5.54 ±0.00 | 5.43 ±0.00 | -1.9% | — |
nii_center_of_masses |
3.34 ±0.00 | 3.27 ±0.00 | -1.9% | — |
nii_apply_crop |
5.91 ±0.00 | 5.80 ±0.00 | -1.8% | — |
poi_calc_centroids_nocrop |
5.97 ±0.00 | 5.86 ±0.00 | -1.8% | — |
nii_dilate_msk |
5.97 ±0.00 | 5.87 ±0.00 | -1.8% | — |
nii_rescale_seg |
5.98 ±0.00 | 5.88 ±0.00 | -1.8% | — |
nii_volumes |
3.59 ±0.00 | 3.52 ±0.00 | -1.7% | — |
poi_calc_centroids |
6.10 ±0.00 | 5.99 ±0.00 | -1.7% | — |
nii_save |
3.77 ±0.00 | 3.71 ±0.00 | -1.7% | — |
nii_load_seg |
6.05 ±0.00 | 5.95 ±0.00 | -1.6% | — |
nii_fill_holes |
6.32 ±0.00 | 6.22 ±0.00 | -1.6% | — |
poi_reorient |
3.89 ±0.00 | 3.83 ±0.00 | -1.6% | — |
nii_get_connected_components |
6.66 ±0.00 | 6.55 ±0.00 | -1.6% | — |
nii_erode_msk |
5.98 ±0.00 | 5.88 ±0.00 | -1.6% | — |
nii_filter_connected_components |
6.91 ±0.00 | 6.80 ±0.00 | -1.5% | — |
nii_load_img |
3.62 ±0.00 | 3.57 ±0.00 | -1.5% | — |
poi_calc_poi_from_subreg_vert |
7.16 ±0.00 | 7.05 ±0.00 | -1.4% | 0.000 |
metric_voxels |
250463.00 ±0.00 | 250463.00 ±0.00 | +0.0% | — |
metric_labels |
3.00 ±0.00 | 3.00 ±0.00 | +0.0% | — |
metric_foreground_pct |
14.51 ±0.00 | 14.51 ±0.00 | +0.0% | — |
poi_save |
1.46 ±0.00 | 1.40 ±0.00 | -4.3% (noise) | — |
poi_load |
1.68 ±0.00 | 1.61 ±0.00 | -3.7% (noise) | — |
nii_get_array |
1.71 ±0.00 | 1.65 ±0.00 | -3.7% (noise) | — |
poi_map_labels |
1.84 ±0.00 | 1.77 ±0.00 | -3.4% (noise) | — |
poi_rescale |
2.67 ±0.00 | 2.61 ±0.00 | -2.3% (noise) | — |
nii_compute_crop |
2.96 ±0.00 | 2.90 ±0.00 | -2.1% (noise) | — |
ct_2d — shape (73, 47, 1)
| Measurement | baseline MiB (median ±½·range) | head MiB (median ±½·range) | Δ % | p |
|---|---|---|---|---|
poi_resample_from_to |
5.26 ±0.00 | 5.11 ±0.00 | -2.7% | — |
nii_reorient |
5.89 ±0.00 | 5.74 ±0.00 | -2.5% | — |
nii_rescale |
6.15 ±0.00 | 6.00 ±0.00 | -2.4% | — |
nii_resample_from_to |
6.27 ±0.00 | 6.13 ±0.00 | -2.3% | — |
poi_to_global |
3.08 ±0.00 | 3.02 ±0.00 | -2.0% | — |
… 30 more measurements
| Measurement | baseline MiB (median ±½·range) | head MiB (median ±½·range) | Δ % | p |
|---|---|---|---|---|
nii_set_dtype |
5.22 ±0.00 | 5.12 ±0.00 | -2.0% | — |
nii_extract_label |
5.35 ±0.00 | 5.24 ±0.00 | -2.0% | — |
nii_pad_to |
5.41 ±0.00 | 5.30 ±0.00 | -2.0% | — |
nii_map_labels |
5.47 ±0.00 | 5.37 ±0.00 | -1.9% | — |
nii_center_of_masses |
3.27 ±0.00 | 3.21 ±0.00 | -1.9% | — |
nii_apply_crop |
5.84 ±0.00 | 5.74 ±0.00 | -1.8% | — |
nii_dilate_msk |
5.91 ±0.00 | 5.80 ±0.00 | -1.8% | — |
nii_rescale_seg |
5.92 ±0.00 | 5.82 ±0.00 | -1.8% | — |
nii_volumes |
3.52 ±0.00 | 3.46 ±0.00 | -1.8% | — |
poi_calc_centroids_nocrop |
5.97 ±0.00 | 5.86 ±0.00 | -1.8% | — |
poi_calc_centroids |
6.04 ±0.00 | 5.93 ±0.00 | -1.7% | — |
nii_save |
3.71 ±0.00 | 3.64 ±0.00 | -1.7% | — |
nii_load_seg |
5.98 ±0.00 | 5.89 ±0.00 | -1.6% | — |
poi_reorient |
3.83 ±0.00 | 3.77 ±0.00 | -1.6% | — |
nii_fill_holes |
6.26 ±0.00 | 6.16 ±0.00 | -1.6% | — |
nii_get_connected_components |
6.60 ±0.00 | 6.49 ±0.00 | -1.6% | — |
nii_erode_msk |
5.91 ±0.00 | 5.82 ±0.00 | -1.6% | — |
nii_filter_connected_components |
6.85 ±0.00 | 6.74 ±0.00 | -1.5% | — |
nii_load_img |
3.56 ±0.00 | 3.50 ±0.00 | -1.5% | — |
metric_voxels |
3431.00 ±0.00 | 3431.00 ±0.00 | +0.0% | — |
metric_labels |
3.00 ±0.00 | 3.00 ±0.00 | +0.0% | — |
metric_foreground_pct |
37.60 ±0.00 | 37.60 ±0.00 | +0.0% | — |
poi_save |
1.40 ±0.00 | 1.34 ±0.00 | -4.5% (noise) | — |
poi_load |
1.61 ±0.00 | 1.55 ±0.00 | -3.9% (noise) | — |
nii_get_array |
1.65 ±0.00 | 1.59 ±0.00 | -3.8% (noise) | — |
poi_map_labels |
1.77 ±0.00 | 1.71 ±0.00 | -3.5% (noise) | — |
poi_rescale |
2.61 ±0.00 | 2.54 ±0.00 | -2.4% (noise) | — |
nii_compute_crop |
2.90 ±0.00 | 2.84 ±0.00 | -2.2% (noise) | — |
poi_local_to_global_arr |
2.96 ±0.00 | 2.89 ±0.00 | -2.1% (noise) | — |
nii_unique |
2.96 ±0.00 | 2.90 ±0.00 | -2.1% (noise) | — |
mri_3d — shape (68, 52, 67)
| Measurement | baseline MiB (median ±½·range) | head MiB (median ±½·range) | Δ % | p |
|---|---|---|---|---|
poi_resample_from_to |
5.26 ±0.00 | 5.11 ±0.00 | -2.7% | — |
nii_reorient |
5.89 ±0.00 | 5.74 ±0.00 | -2.5% | — |
nii_rescale |
6.15 ±0.00 | 6.00 ±0.00 | -2.4% | — |
nii_resample_from_to |
6.27 ±0.00 | 6.13 ±0.00 | -2.3% | — |
poi_to_global |
3.09 ±0.00 | 3.02 ±0.00 | -2.0% | — |
… 31 more measurements
| Measurement | baseline MiB (median ±½·range) | head MiB (median ±½·range) | Δ % | p |
|---|---|---|---|---|
nii_set_dtype |
5.29 ±0.00 | 5.18 ±0.00 | -2.0% | — |
nii_extract_label |
5.35 ±0.00 | 5.24 ±0.00 | -2.0% | — |
nii_pad_to |
5.41 ±0.00 | 5.30 ±0.00 | -2.0% | — |
nii_map_labels |
5.47 ±0.00 | 5.37 ±0.00 | -1.9% | — |
nii_center_of_masses |
3.27 ±0.00 | 3.21 ±0.00 | -1.9% | — |
nii_apply_crop |
5.84 ±0.00 | 5.74 ±0.00 | -1.8% | — |
poi_calc_centroids_nocrop |
5.91 ±0.00 | 5.80 ±0.00 | -1.8% | — |
nii_dilate_msk |
5.91 ±0.00 | 5.80 ±0.00 | -1.8% | — |
nii_rescale_seg |
5.92 ±0.00 | 5.82 ±0.00 | -1.8% | — |
nii_volumes |
3.52 ±0.00 | 3.46 ±0.00 | -1.8% | — |
poi_calc_centroids |
6.04 ±0.00 | 5.93 ±0.00 | -1.7% | — |
nii_save |
3.71 ±0.00 | 3.64 ±0.00 | -1.7% | — |
nii_load_seg |
5.98 ±0.00 | 5.89 ±0.00 | -1.6% | — |
poi_reorient |
3.83 ±0.00 | 3.77 ±0.00 | -1.6% | — |
nii_fill_holes |
6.26 ±0.00 | 6.16 ±0.00 | -1.6% | — |
nii_get_connected_components |
6.60 ±0.00 | 6.49 ±0.00 | -1.6% | — |
nii_erode_msk |
5.91 ±0.00 | 5.82 ±0.00 | -1.6% | — |
nii_filter_connected_components |
6.85 ±0.00 | 6.74 ±0.00 | -1.5% | — |
nii_load_img |
3.62 ±0.00 | 3.57 ±0.00 | -1.5% | — |
poi_calc_poi_from_subreg_vert |
7.15 ±0.00 | 7.05 ±0.00 | -1.5% | — |
metric_voxels |
236912.00 ±0.00 | 236912.00 ±0.00 | +0.0% | — |
metric_labels |
8.00 ±0.00 | 8.00 ±0.00 | +0.0% | — |
metric_foreground_pct |
18.21 ±0.00 | 18.21 ±0.00 | +0.0% | — |
poi_save |
1.46 ±0.00 | 1.40 ±0.00 | -4.3% (noise) | — |
poi_load |
1.61 ±0.00 | 1.55 ±0.00 | -3.9% (noise) | — |
nii_get_array |
1.65 ±0.00 | 1.59 ±0.00 | -3.8% (noise) | — |
poi_map_labels |
1.77 ±0.00 | 1.71 ±0.00 | -3.5% (noise) | — |
poi_rescale |
2.61 ±0.00 | 2.54 ±0.00 | -2.4% (noise) | — |
nii_compute_crop |
2.90 ±0.00 | 2.84 ±0.00 | -2.2% (noise) | — |
poi_local_to_global_arr |
2.96 ±0.00 | 2.89 ±0.00 | -2.1% (noise) | — |
nii_unique |
2.96 ±0.00 | 2.90 ±0.00 | -2.1% (noise) | — |
mri_2d — shape (68, 52, 1)
| Measurement | baseline MiB (median ±½·range) | head MiB (median ±½·range) | Δ % | p |
|---|---|---|---|---|
poi_resample_from_to |
5.26 ±0.00 | 5.11 ±0.00 | -2.7% | — |
nii_reorient |
5.89 ±0.00 | 5.74 ±0.00 | -2.5% | — |
nii_rescale |
6.15 ±0.00 | 6.00 ±0.00 | -2.4% | — |
nii_resample_from_to |
6.27 ±0.00 | 6.13 ±0.00 | -2.3% | — |
poi_to_global |
3.08 ±0.00 | 3.02 ±0.00 | -2.0% | — |
… 30 more measurements
| Measurement | baseline MiB (median ±½·range) | head MiB (median ±½·range) | Δ % | p |
|---|---|---|---|---|
nii_set_dtype |
5.29 ±0.00 | 5.18 ±0.00 | -2.0% | — |
nii_extract_label |
5.35 ±0.00 | 5.24 ±0.00 | -2.0% | — |
nii_pad_to |
5.41 ±0.00 | 5.30 ±0.00 | -2.0% | — |
nii_map_labels |
5.47 ±0.00 | 5.37 ±0.00 | -1.9% | — |
nii_center_of_masses |
3.27 ±0.00 | 3.21 ±0.00 | -1.9% | — |
nii_apply_crop |
5.84 ±0.00 | 5.74 ±0.00 | -1.8% | — |
nii_dilate_msk |
5.91 ±0.00 | 5.80 ±0.00 | -1.8% | — |
nii_rescale_seg |
5.92 ±0.00 | 5.82 ±0.00 | -1.8% | — |
nii_volumes |
3.52 ±0.00 | 3.46 ±0.00 | -1.8% | — |
poi_calc_centroids_nocrop |
5.97 ±0.00 | 5.86 ±0.00 | -1.8% | — |
poi_calc_centroids |
6.04 ±0.00 | 5.93 ±0.00 | -1.7% | — |
nii_save |
3.71 ±0.00 | 3.64 ±0.00 | -1.7% | — |
nii_load_seg |
5.98 ±0.00 | 5.89 ±0.00 | -1.6% | — |
poi_reorient |
3.83 ±0.00 | 3.77 ±0.00 | -1.6% | — |
nii_fill_holes |
6.26 ±0.00 | 6.16 ±0.00 | -1.6% | — |
nii_get_connected_components |
6.60 ±0.00 | 6.49 ±0.00 | -1.6% | — |
nii_erode_msk |
5.91 ±0.00 | 5.82 ±0.00 | -1.6% | — |
nii_filter_connected_components |
6.85 ±0.00 | 6.74 ±0.00 | -1.5% | — |
nii_load_img |
3.62 ±0.00 | 3.57 ±0.00 | -1.5% | — |
metric_voxels |
3536.00 ±0.00 | 3536.00 ±0.00 | +0.0% | — |
metric_labels |
7.00 ±0.00 | 7.00 ±0.00 | +0.0% | — |
metric_foreground_pct |
50.17 ±0.00 | 50.17 ±0.00 | +0.0% | — |
poi_save |
1.46 ±0.00 | 1.40 ±0.00 | -4.3% (noise) | — |
poi_load |
1.61 ±0.00 | 1.55 ±0.00 | -3.9% (noise) | — |
nii_get_array |
1.65 ±0.00 | 1.59 ±0.00 | -3.8% (noise) | — |
poi_map_labels |
1.77 ±0.00 | 1.71 ±0.00 | -3.5% (noise) | — |
poi_rescale |
2.61 ±0.00 | 2.54 ±0.00 | -2.4% (noise) | — |
nii_compute_crop |
2.90 ±0.00 | 2.84 ±0.00 | -2.2% (noise) | — |
poi_local_to_global_arr |
2.96 ±0.00 | 2.89 ±0.00 | -2.1% (noise) | — |
nii_unique |
2.96 ±0.00 | 2.90 ±0.00 | -2.1% (noise) | — |
Gate: a measurement fails when the baseline is ≥ 1 MiB, the median grows by ≥ 50%, and Welch's t-test gives p < 0.05. metric_* rows are context only.
The 3.9 matrix leg failed in `poetry install`: dicom2nifti pulls python-gdcm, which publishes no cp39 wheel, so pip tried to build 3.2.6 from source and the CMake step failed. Both DICOM test modules already guard on pydicom + dicom2nifti being importable, so on 3.9 they skip and the rest of the suite is unaffected. 3.10 upwards - and in particular the full-coverage job on 3.11 - still install them and run the tests. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The 3.10 matrix leg reported 1 failed / 1240 passed: `mip_shot` builds its frame with `denoise_threshold` set, and `create_snapshot` does a hard `import torch` for that path (snapshot_modular.py:1168, for avg_pool3d). torch is an optional extra, so the light matrix cannot run it. Guarded with the reason spelled out. Worth noting independently: one of the shipped 2D snapshot templates therefore requires torch, which is not obvious from either the template or the optional-extra list. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The 3.9 leg never reached the tests - it failed in `poetry install`: CMake Error: The source directory ".../python_gdcm-3.2.6" does not appear to contain CMakeLists.txt Two upstream metadata problems, neither of them ours: * python-gdcm stopped shipping cp39 wheels after 3.2.2 but still advertises `requires-python = ">=3.7"`, so the resolver picks 3.2.6 on 3.9 and falls back to an sdist that cannot build. * dicom2nifti 2.6.0 imports `pydicom.pixels` (pydicom >= 3.0) while declaring only `pydicom >= 2.2`, and pydicom 3 requires python >= 3.10. Cap both for `python < 3.10` instead of trying to keep them off 3.9 with a marker. The `python = ">=3.10"` markers added in 5e284e3 cannot work: poetry 2.2 merges a group declaration with the unmarked optional one in `[tool.poetry.dependencies]` into a marker that still mentions `extra == "dicom"` and installs the package on 3.9 regardless (poetry 2.4 simplifies it correctly, so the breakage depends on which poetry `pip install poetry` happens to fetch). nnunetv2 2.4.2, pinned for 3.9, hard-depends on dicom2nifti anyway, so the 3.9 region has to resolve whatever the markers say. Capping also makes `pip install "TPTBox[dicom]"` work on 3.9, which it previously did not. With the dev markers gone the two DICOM test modules now run on every leg instead of skipping on 3.9. Verified on a provisioned CPython 3.9.25 (the version CI uses): `poetry install --with dev` succeeds wheel-only with pydicom 2.4.4 / dicom2nifti 2.5.1 / python-gdcm 3.2.2, and `pytest unit_tests/` gives 1240 passed, 76 skipped. Re-locked under poetry 2.2.1 and 2.4.1 with the same result; the `>=3.10` region still resolves to pydicom 3.0.2 / dicom2nifti 2.6.2 / python-gdcm 3.2.6, so the green legs are untouched. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
What this does
Raises unit-test coverage from 47.9% to 78% by adding ~6,500 lines of tests across 17 new files (plus two extended), and curates the coverage
omitlist so the number is measured on code a test can actually reach.Both effects are reported separately so neither hides the other:
mainrtreepresent (what CI will see)So the tests alone carry 47.9% → 64% — 3,200 statements that were not executed before — and the omit list accounts for the rest by no longer counting ~4,500 statements of vendored forks, dataset-bound pipelines and dead code.
Suite: 1,295 passed / 21 skipped in 207 s, up from 642 passed / 5 skipped in 184 s. The 21 skips are all dependency guards (16 of them need
rtree, which this PR declares, so they run in CI).Measured locally in a full-dependency environment (py3.11, every optional extra installed). This is short of the ~85% I estimated when planning: the remaining misses are concentrated in the torch/deepali registration internals (
multilabel_segmentation,spine_rigid_elements_reg,affine_deepali,_deepali/_utils, ~700 statements), which need real optimiser runs rather than a boundary mock, and indicom_extract's deeper branching. Both are reachable, just not in this PR.Per-module effect
Measured, not estimated —
mainvs this branch, same command, same environment:spine/spinestats/curvature.pyspine/spinestats/pelvic_parameters.pyspine/spinestats/torso_vat_sat.pyspine/spinestats/body_quadrants.pyspine/spinestats/angles.pycore/dicom/xa_pairing.pycore/dicom/nii2dicom.pycore/dicom/dicom_renamer.pycore/dicom/dicom_extract.pycore/dicom/dicom2nii_utils.pycore/dicom/dicom_header_to_keys.pylogger/log_file.pylogger/log_constants.pycore/bids_files.pycore/poi_fun/ray_casting.pycore/poi_fun/save_load.pycore/poi_fun/poi_abstract.pycore/nii_wrapper.pycore/nii_wrapper_math.pycore/internal/nii_help.pycore/sitk_utils.pysegmentation/rib/_rib_assign.pysegmentation/VibeSeg/vibeseg.pysegmentation/nnUnet_utils/inference_api.pysegmentation/VibeSeg/inference_nnunet.pyregistration/_ridged_points/point_registration.pystitching/stitching_tools.pyspine/snapshot2D/snapshot_modular.pyspine/snapshot2D/snapshot_templates.pyspine/spinestats/measure_ivd_and_vertebra_geometry.pyspine/spinestats/poi_fun/endplates.py* those two need
rtree(see bug 7 below); in an environment that still lacks it the16 guarded tests skip and the two modules read 36%.
How the tests are written
Analytic ground truth wherever the maths allows it, rather than recording current output:
2x + 3y − zexactly; a ray from the centre of a box spanning indices 10..20 must exit at 20.5_plane_from_gridagainstget_plane_dicom, so neither side can drift aloneWhere there is no closed form, the test asserts an invariant or a round-trip: affine decompose/recompose,
apply_crop∘apply_crop_reverse,global_to_local_arr∘local_to_global_arr, POI save/load for every accepted format, NII ↔ SimpleITK, Slicer terminology strings,to_cord_systemas an involution, forward ∘ inverse coordinate mapping, and byte-identical snapshot rendering.DICOM has no sample in the repo, so the test makes one: the sample CT is written out as a 73-slice DICOM series with
nii2dicomand read back withextract_dicom_folder, which pins the whole naming contract end to end (rawdata/sub-TESTPAT/ct/sub-TESTPAT_sequ-7_acq-iso_ct.nii.gz, wheresequ-7comes fromSeriesNumberandacq-isofrom the grid) plus the array round-trip, the age conversion, the zip path and the.extract_cacheno-op. Runs in under two seconds.For the GPU surface the mock sits at the model boundary and nowhere else: only
predict_single_npy_arrayis replaced, so the real code still builds the channel axis, derives the properties dict and converts the result back onto the input grid — and that is what is asserted, not that a mock was called.Bugs found and fixed
Each has a regression test that was verified to fail without the fix.
segmentation/rib/_rib_assign.assign_ribs_to_vert_segmentationcould never return. Two separate lines:(rib_inst != 0) & (rib_inst != error_value)combines two bool-backed NIIs andNII.__and__raisesTypeErrorfor non-integer arrays; andextract_label([Rib_Left, Rib_Right] == 1)has its closing bracket one token too far left, so it evaluated[63, 64] == 1→False→extract_label(0), tripping the "zero label is the background" assertion. The rib assignment entry point was dead.np_calc_boundary_maskreturned an empty mask for anythreshold >= 2. It wrote its sentinel value2into the array and then compared that same array against the threshold, erasing the foreground again.NII.boundary_mask(threshold)forwards straight to it, so every realistic HU threshold was affected.xa_pairing.pair_biplane_xamoved data out ofrawdata. The target path came fromget_changed_bids(...), whoseparentdefaults to"derivatives", so applying the pairing relocated the B-side of every biplane pair intoderivatives/. The follow-upflip_zpass then no longer saw those files, leaving a pair with one flipped and one unflipped affine.Searchquery.filter_dixon_water/_fat/_outphasereturned nothing whenever the files had JSON sidecars:_keyslists alternative spellings of one channel ("W"/"WATER") but the ImageType filter requiredallof them while the entity filter beside it usedany. No real ImageType carries both spellings.NII.to_stl(number_path=True)silently did nothing —out_path.with_name(...)was computed and the result discarded. Sinceto_stlsdefaultsnumber_pathto True for a non-dict path, exporting a multi-label segmentation wrote every label to the same file and only the last mesh survived.Python 3.9 support was broken.
spinestats/curvature.pyimporteditertools.pairwiseat module scope (3.10+), so the whole module raisedImportErroron the 3.9 CI leg — ruff never caught it (target-version = "py310") andtest_docs_examples.pyswallowed it because its missing-module check only fails for names starting withTPTBox. Plus fourzip(..., strict=...)call sites (3.10+,TypeErrorat call time), one of them inmeasure_ivd_and_vertebra_geometry._segment_length. Added apairwisebackport next to the existingzip_strictincore/compat.py, and two tree-wide guard tests so neither pattern comes back.rtreewas undeclared. trimesh's ray engine needs it for its broad phase and only declares it under its owneasyextra, so every mesh ray cast raisedModuleNotFoundError—measure_ivd_and_vertebra_geometryand the endplate code could not run in a clean install (and did not run in 2 of 3 environments here). Added to the core dependencies.measure_ivd_and_vertebra_geometry(buffer_poi=None)— its own documented default — raisedAssertionError, because it asked the POI buffering decorator to write the result back unconditionally and that decorator asserts the path is not None.stitching_tools._center_frontalcropped the wrong side of any volume below ~258 voxels: the window startmiddle - 122goes negative and numpy reads a negative slice start as an offset from the end.rerun_bids_naming's dry run over-reported. The leftover sweep scans the disk, so underdry_runit reported every file pass 1 had only planned to move, with a different fallback destination: 6 planned moves where the real run performs 4._scan_treedroppedfilter_folderon recursion, so anyfilter_foldertargeting a level below the subject folder was silently ignored. The docstring example was also off by one about which level is which.predictor._internal_maybe_mirror_and_predictignored itsnetworkargument for 7 of its 8 test-time-augmentation terms (latent:network is self.networkon every current path, but it defeats the purpose ofloaded_networks).Smaller: duplicate entries in the
formatstable (T1w,T2w,T2star,stitwice); a duplicated neighbour in the boundary-mask flood fill; the folder README andall_output_referenceboth documentedvert_geometryasstructure_label=50where the code uses0.Two CI-only constraints surfaced while getting the matrix green, both now handled in this PR:
dicom2niftipullspython-gdcm, which publishes no cp39 wheel and fails to build from source, so the two DICOM dev deps are declaredpython = ">=3.10"and both DICOM test modules skip on 3.9 on their own guard; andmip_shotbuilds its frame withdenoise_thresholdset, which makescreate_snapshotdo a hardimport torch(snapshot_modular.py:1168, foravg_pool3d) — so one of the shipped 2D snapshot templates requires torch even though torch is an optional extra. That is not obvious from the template or from the extras list.Behaviour pinned rather than changed
These are documented in the tests at the point a reader will meet them, and left alone because changing them would move numeric output or a stitched field of view:
endplates._ray_cast_to_meshis commented "closest hit" but returnsorigin + d.mean() * direction— for a convex mesh, the midpoint inside it rather than the surface point. Changing it moves every computed endplate point.stitching.get_all_corner_pointsadds 1 to every voxel index before applying the affine, so the box it reports spans1..shape+1. Every stitched output FOV depends on it.calculate_pca_normal_npfits the PCA on raw voxel indices and then divides the resulting unit vector byzoom, so anisotropic voxels never change which axis comes out principal, the result is no longer a unit vector, and the division is the index-from-world direction rather than world-from-index.normalize_image'sv_rangeis described as a clip range but only rescales, and a constant image yields NaN.np_erode_msk,np_erode_msk_euclidandnp_dilate_msk_euclidmodify the caller's array in place as well as returning it.validate_entitiesregisters unknown keys as a side effect of validating (entities_keys[key] = key) andget_values_from_nameappends unknown formats toformats_relaxed, so the same filename can validate differently depending on what was parsed earlier in the process. The tests snapshot and restore those containers; the behaviour needs an API decision.save_poi(save_hint=1)(Gruber) raisesKeyErrorfor any subregion outside its fixed landmark vocabulary;_is_Point3DraisesTypeErrorinstead of returningFalsefor a non-container.Coverage configuration
[tool.coverage.report].omitpreviously held one entry, and it matched nothing —TPTBox/test/speedtest/*, where the real directory isTPTBox/tests/speedtests/. It now lists, with a justification per group, only code a unit test cannot legitimately reach: the vendored nnU-Net inference internals (TPTBox's owninference_api.pyaround them is not omitted and is tested), the nnU-Net training drivers, twonipy-dependent scripts, the dead vendored deepali engine, the NAKO dataset pipelines, and one in-place repair script. Anything merely awkward to test got a test instead.The dead deepali engine (
registration/_deformable/_deepali/*plusdeformable_reg_old.py,grid_search.py,_grid_search_vert.py) is a candidate for deletion rather than coverage: it imports an undeclaredyaml, and nothing in the public API reaches it — those modules are referenced only by each other.CI and dependencies
tests.ymlgainsbuild-full-coverage: one ubuntu / py3.11 job that installs CPU-only torch plus nnunetv2, acvl-utils, batchgenerators, antspyx and hf-deepali, then uploads to Codecov withflags: full(the existing matrix now uploads withflags: light). It prints the coverage number both with and without the omit list.spinepsis deliberately not installed — it depends on TPTBox itself, so installing it would pin a released TPTBox over the checkout; the tests that touch it fake the module instead.tests_mr.ymlgains the same heavy-dependency set as a gate without upload, so a PR that breaks a mocked torch/deepali test fails before merge.pydicomanddicom2niftimove into the dev group so the DICOM tests run in every job rather than only where the optional extra happens to be installed.rtreebecomes a core dependency (see bug 7).Notes for review
unittest.TestCase,self.subTestfor tables,tempfile.TemporaryDirectory(),importlib.util.find_spec+@unittest.skipUnlessfor optional backends, noconftest.py.bids_constants.entities/entities_keys/formats_relaxed, andlogger.indentation_levelis asserted back to zero intearDown.auto_downloadis asserted not to reach it when the weights folder already exists.🤖 Generated with Claude Code