Skip to content

Test suite consolidation: fewer test IDs, asserts that can fail, and CLAUDE.md test rules - #307

Merged
arthurmccray merged 19 commits into
electronmicroscopy:devfrom
cophus:test-cleanup
Oct 20, 2026
Merged

arthurmccray merged 19 commits into
electronmicroscopy:devfrom
cophus:test-cleanup

Conversation

@cophus

@cophus cophus commented Oct 18, 2026

Copy link
Copy Markdown
Contributor

The quantem test suite grew from approximately 2k lines in June to 22k lines in October, mostly from tests added during Claude Code sessions. This PR consolidates the suite following the approach @TomaSusi used for abTEM in abTEM/abTEM#589: we removed parametrizations that do not change the code path, merged duplicate tests, rewrote tests that could not fail so that they check computed values, and shared expensive inputs between tests. We also added a CLAUDE.md with rules for writing tests, so that future sessions extend existing tests instead of adding new ones. Each commit message lists every removed or merged test and the test that still covers that behavior.

Results

dev this PR
Test IDs 1,460 1,055
Test lines 22,419 20,561
Serial run time 366 s 275 s
Run time with 4 workers approximately 122 s approximately 97 s
Tests Test IDs Serial run time
Direct ptychography 539 → 342 276 → 200 s
Other diffractive imaging 136 → 98 26 → 16 s
Datastructures 134 → 81 3.2 → 1.5 s
Visualization 88 → 31 < 1 s
Tomography 127 → 104 < 1 s
Utils, core, spectroscopy and config 106 → 69 < 1 s

We measured all run times on an Apple silicon laptop, where repeated runs with 4 workers varied by approximately 10 s. The diffraction, imaging and ML tests are unchanged (see below).

Changes

Over-parametrization. Many tests were parametrized over values that take the same code path, for example kernel name aliases, both direct ptychography classes for code in their shared base class, both store="zip" and store="dir" for every serialization test, and a 4 × 6 grid of upsampling factors and shifts. We kept one case per code path, and where several inexpensive cases share one path, we loop over them inside one test and name the failing case in the assert message.

Duplicate tests. The Fourier and montage direct ptychography files contained copies of the tests for their shared helper functions and 17 separate save and load round trips, which are now 5. The INR and tensor decomposition tests contained identical copies of approximately 140 lines of helper functions, which now live in diffractive_imaging/conftest.py.

Tests that could not fail. The normalization stretch tests asserted 0 <= f(x) <= 1 for inputs that each stretch first clips to [0, 1], and the histogram tests asserted ~isnan(h[h > 0]). The tomography logger tests checked for an events file that SummaryWriter.__init__ always writes, the pure-phase double-mask test used a constant phase that gives zero whether the mask is applied once or twice, and the GPU origin-fit test compared two parallax images that were identically zero. These tests now check computed values.

Run time. The direct ptychography tests now share module-scoped fixtures for the Fourier and montage reconstruction pair, the drift frames and the tilt series acquisitions. The sparse event tests use a mean count of 4 instead of 40, which reduces each from_vector build from 2.2 s to 0.2 s and leaves the largest dense-to-sparse error at 1.3e-5, below the 1e-4 tolerance. The target-residency tests in test_ptychography.py previously rebuilt a 64⁴ simulated dataset for each of 9 test IDs, and now use a single 8 × 8 × 16 × 16 dataset.

CI scheduling. The CI workflow now runs pytest with --dist loadscope, which assigns each test class (or each module's free functions) to a single xdist worker, so that each module- and class-scoped fixture is built once instead of once per worker.

Device leak. On Apple silicon, test_config_set_device left the global device set to mps, so later tests in the same worker ran on MPS. The test now restores the original device.

CLAUDE.md. The new CLAUDE.md asks Claude Code sessions to extend an existing test before adding a new one, to test each code path once, to write asserts that can fail, and to use the smallest problem that exercises the code path. .gitignore previously excluded CLAUDE.md, so we replaced that entry with CLAUDE.local.md, which keeps personal notes out of the repository.

Remaining work

To avoid merge conflicts, we did not modify test files that open PRs also modify. These include tests/diffraction/, which #305 modifies; we audited it and expect a reduction from 256 to approximately 206 test IDs and from 56 s to approximately 32 s once #305 merges. The others are test_optimizermixin.py and four tomography test files (#246), test_vector.py (#273) and test_lattice.py (#236). Approximately 50 of the 64 test IDs in test_optimizermixin.py check dataclass default values and can be removed after #246 merges.

Other issues found

CI does not run the 31 tests marked @pytest.mark.slow, because the workflow does not pass --runslow. These include test_tomography_cpu_recon.py, whose docstring states that it exists to cover the learned reconstruction loop in CI. We should either run the slow tests in a separate scheduled CI job, or remove the slow marker from the tests that CI must cover.

tomography_opt.py:29 reads the pose learning rate from DEFAULT_LRS["pose"], but dataset_models.py:162 defines the key as "pose_lr", so the code always uses the fallback value, which is currently equal to the intended value of 5e-2.

Testing

The full suite passes both serially and with 4 workers (1,023 passed, 32 skipped). We ran the direct ptychography tests three times with 4 workers to check for flaky tests. test_multi_gpu.py requires two CUDA devices, so we linted it but did not run it.

🤖 Generated with Claude Code

cophus and others added 19 commits October 18, 2026 16:32
test_dataset.py (30 -> 16 IDs):
- Drop the array/name/signal_units getter-setter and repr/str tests; the
  origin/sampling/units setters and their length validation stay, as one loop.
- test_mean/max/min only checked types and shapes; replaced by one test that
  compares each reduction (all axes, both axes, axis 0) against numpy.
- The four 4D/single-axis crop tests become one table that checks shape and
  origin, both out of place and in place; test_crop is kept.
- test_bin and TestBinMetadata merged (shape, sampling, origin, in place).
- Down/upsample resample tests merged into one table with their own center
  tolerances; odd/even and API-error tests kept.
- Removed the unused 3D fixtures.

test_dataset4dstem.py (15 -> 8 IDs):
- The six dp_mean/max/median property and get_* tests checked only shapes;
  one loop now checks each against numpy and checks the attach/cache contract.
- getitem 2D/3D merged; virtual image now checks the summed values.
- Deleted test_show: its monkeypatch targeted visualization.show_2d, which
  dataset2d had already imported, so it rendered a real figure and only
  checked for a 2-tuple. Dataset4d.show is a one-line forward.

test_dataset3d_show.py (25 -> 3 IDs):
- Parametrized cases become loops that name the failing kwargs. Duplicate
  branches dropped (ncols/max -1, step 2). The long dataset is 25 frames
  instead of 100, so max=None draws 25 subplots, not 100; the default
  max=20 truncation is still exercised.

test_autoserialize.py (16 -> 6 IDs):
- store=zip/dir only changes the container, so only the Dummy test loops
  over both stores; the other tests use zip.
- Nested roundtrip and nested skip merged (same class, two saves).
- test_torch_tensor_roundtrip removed; test_torch_tensor_and_optimizer
  checks the same tensor. Its optimizer check compared state_dict keys,
  which are always {state, param_groups}; it now checks the restored lr.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
test_custom_normalizations.py (37 -> 9 IDs):
- Intervals/stretches clip to [0, 1], so the "0 <= result <= 1" checks on
  the log/inverse-log/asinh/sinh stretches and on every CustomNormalization
  variant could not fail (the 1..10 input clipped to all ones). Replaced by:
  one loop asserting f(0)=0, f(1)=1, monotonicity, inverse round trip, the
  documented log/inverse-log formulas and asinh/sinh symmetry; and one
  CustomNormalization table checking exact values for the default, manual,
  centered, power, logarithmic and asinh variants.
- Interval limit tests become one table; ManualInterval.__call__ now checks
  every value. Linear/power exact checks kept in one test.
- Invalid-parameter tests become one loop.
- NormalizationConfig default/custom tests deleted (dataclass fields).
- _resolve_normalization: the preset test only checked isinstance; now one
  test checks explicit configs for None, the vmin/vmax and quantile kwargs,
  dict, two presets and pass-through; invalid preset/type merged.
- Removed the unused outlier fixture.

test_visualization_utils.py (31 -> 12 IDs):
- The three "custom origin/sampling/statistic" histogram tests asserted
  ~isnan(hist[hist > 0]), which cannot fail, and the others only the output
  shape. One table now places known points and checks bin values for sum,
  mean, origin and sampling; the column-vector test checks equality with the
  1D call and the mismatch error.
- array_to_rgba: cmap path compared against matplotlib; angle path checks
  black at zero amplitude, lightness order, hue change and the chroma boost.
  combine_arrays_to_rgba checks black/no-signal, distinct hues and boost.
- ScalebarConfig default/custom tests deleted; _resolve_scalebar's six
  tests become one, adding the sampling-kwarg path.
- estimate_scalebar_length checks the actual lengths and the non-uniform
  sampling error instead of isinstance/positivity.
- Colorbar tests check the norm and the arg ticks; the chroma-boost variant
  only asserted "is not None" and now checks the colormap changes.
- turbo_black: fade_len was never reflected in any assertion; one loop now
  checks N, the black start and the unfaded tail.

test_visualization.py (20 -> 10 IDs):
- isinstance(fig, Figure) could not fail with figax passed in. Smoke cases
  for real/complex/cmap/chroma (and the combined variants) become loops
  asserting one image each; scalebar/colorbar/title checks kept in one call.
- New value check: manual norm + viridis is what imshow receives.
- Grid normalization tests merged; pyplot figures closed.
- Deleted three commented-out tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
test_imaging_utils.py (37 -> 7 IDs): the 4 x 6 upsample/shift product, the
6 torch-parity shifts and the 3 center-tap factors all exercise one code
path each. They are now loops inside the same tests, with every case kept
and named in the assertion message.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On Apple silicon the MPS branch left the global device set to "mps", so every
later test in the same worker that reads config.get_device() ran on MPS. The
test now restores the device it started with in a finally block.

Also:
- test_check_key_val_known_key_silent and ..._nested_key_silent merged into
  one loop over both keys.
- test_version_exists removed; test_version_is_string implies it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- test_eds_background.py: the four invalid-input parametrize cases become a
  loop in one test (6 -> 3 IDs), each case kept.
- test_xeds_fitting.py: test_loss_and_tv_helpers_are_finite only checked
  finiteness; it now checks the exact MSE, Poisson and in-plane TV values
  for the same inputs, and the unknown-loss error.
- test_serialize_bundle.py: the three reserved-name cases become a loop
  (6 -> 4 IDs).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
test_logger_tomography.py (5 -> 3 IDs): SummaryWriter writes the events file
in __init__, so "an events file exists" could not fail, and
test_log_iter_without_val_loss asserted nothing. The scalar tests are one
test that reads the events back and checks every tag, value and step
(including that loss/val is skipped when val_loss is None); the image test
checks the logged image tags.

test_tomography_opt.py (16 -> 11 IDs):
- test_setter_getter_roundtrip and test_current_lrs folded into
  test_set_optimizers_builds_object_and_pose (same state, all assertions).
- test_scheduler_setter_getter folded into test_set_schedulers_builds.
- The step/zero_grad once-per-call regressions become one loop.
- _get_default_lr checked only isinstance(float); it now checks the object
  and pose values, which also pins the routing, and the unknown-key error.

test_tomography.py (15 -> 9 IDs):
- The conventional factory and empty-loss-accessor tests merged.
- The three constraints-setter tests and the two wrong-type setter tests
  merged.
- test_to_updates_device deleted (moves cpu to cpu).
- TestInrFactory::test_from_models_builds deleted (isinstance only; every
  INR test here and in test_tomography_opt.py builds through from_models).
- Both plot_losses tests asserted nothing; they now check the plotted
  losses (and learning rates) and close the figure.

test_radon.py (19 -> 9 IDs):
- Shape tests merged into one radon and one iradon test.
- The named-filter parametrize only checked shape/finiteness; one loop now
  checks each window lies between 0 and the ramp and attenuates Nyquist,
  and that the ramp peaks there. Odd-size and unknown-name errors merged.
- The explicit- and default-theta round trips are one loop.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The constants, probe, smooth phase, dataset simulation, ptychography
builder and shift-tolerant correlation were copied verbatim into
test_object_inr.py and test_object_tensor_decomp.py. They now live once, as
an INR_-prefixed block appended to diffractive_imaging/conftest.py (nothing
above it changed; the prefix avoids the direct-ptychography N/Q_PROBE/...
names). The importlib concern in the old docstring does not apply: other
modules here already import from .conftest.

test_object_inr.py (23 -> 21 IDs):
- test_multislice_z_coordinates and test_single_slice_z_is_zero merged.
- test_autograd_only_backward_raises and
  test_complex_obj_type_not_supported merged into one test.

test_object_tensor_decomp.py (27 -> 20 IDs). ObjectTensorDecomp overrides
only its factories, name, model, optimizer grouping and pretrain, so these
re-tested ObjectINR code and are removed (covered in test_object_inr.py):
- test_off_object_is_vacuum (ObjectINR.forward masking)
- test_multislice_z_coordinates (ObjectINR._z_coords); the 3-slice K-Planes
  forward shape is now part of test_forward_shape_dtype_and_vacuum
- test_autograd_only_backward_raises (ObjectINR.backward)
- TestImplicitSync (same assertion as the INR test)
- slow test_multislice_runs (INR slow multislice test plus the unit forward)
Also: test_is_implicit_and_name folded into the forward test, and the two
KPlanesHead tests are one loop over use_hybrid_mlp.

Both files pass with --runslow (thresholds unchanged).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ests

test_ptychography.py (18 -> 4 IDs):
- TestTargetResidency ran on the 64^4 white-noise fixture (4096 simulated
  64x64 patterns, preprocessed with rotation/CoM plots), rebuilt for each
  of 9 IDs, about 0.8 s each, to test a string property. It now uses an
  8x8x16x16 random dataset; default/valid/invalid-setter checks are one
  test with the six invalid values looped and named; the save/load check
  is kept.
- TestAspectRatioRotatedFOV: the positions-inside-object and obj_cropped
  checks built the same rotated 32x64 scan separately; one test now builds
  it once per transpose and runs both checks (4 builds -> 2).
- Slow TestPtychographySaveLoadRoundtrip: the function-scoped trained
  fixture reran the 50-iteration 64^4 reconstruction for each of 5 tests.
  One test now reconstructs once and checks the reloaded losses,
  positions and object (exact), the strict 0.3 loss threshold, and the
  continue-training behaviour (about 24 s once instead of five times).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… case

test_constraints.py (24 -> 12 IDs):
- test_mask_applied_once[pure_phase] used a constant phase, which the
  recentering zeroes, so the expected 0 held whether the FOV mask was
  applied once or twice. It now uses a zero-mean +-0.7 checkerboard: once
  gives |phase| 0.35, twice would give 0.175.
- TestParseDict: the four successful parses are one test, the unknown and
  missing-name errors another.
- TestTypoCatching: the dict and add_constraint paths are one test.
- TestRoundtrip: the object/probe/dataset dataclass identity checks are a
  loop; the dict form is kept.
- TestReconstructKwargs: test_dataclass_leaf_applied folded into
  test_mixed_dataclass_and_dict_leaves (the object dataclass leaf now also
  sets tv_weight_z=1.5 and checks it); the partial dict update is kept.
- TestPurePhaseRealValued: the three storage-dtype tests and the
  hard-constraints-stay-real test are one loop; the phase-wrap warning
  presence/absence pair is another.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
TestMultiGPUDeviceRestoration, TestMultiGPULossTracking,
TestMultiGPULRTracking and TestMultiGPUOptimizerState each built a fresh
model and ran the same reset / continue / reset spawn sequence (9 spawns).
One test now runs a single sequence (4 spawns): the first call checks the
device list, CUDA placement, loss and lr history and Adam state; then a
continuation without device= (the device must persist), a continuation
with device=, and a reset, checking that the loss and lr histories extend
and reset. The single-GPU and DIP tests are unchanged. 6 -> 3 IDs.

This test requires two CUDA devices, so it was linted but not run.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Removed (cannot fail, or implied by another test):
- test_one_slice_is_the_single_plane: num_slices=1 is the default, so both calls were
  identical.
- test_no_per_pixel_stack_is_kept, test_variance_loss_has_been_removed: tombstones for
  removed attributes.
- test_astigmatism_breaks_the_radial_symmetry: covered by
  test_the_shift_fit_returns_the_twin_nearest_its_reference, which recovers C12 from the
  same shifts.
- test_the_two_classes_agree: rms_gradient_loss lives on the base class, and prlx equality
  is pinned by the montage TestFourierEquivalence.
- test_a_spec_built_anywhere_is_accepted_everywhere: implied by the `is` identity in
  test_every_import_path_is_the_same_class.
- test_the_noise_variance (Wiener): asserted the stored value equals the one passed.
- test_a_finer_scan_sampling_refines_the_grid: implied by
  test_a_finer_scan_sampling_recovers_it.
- test_upsampling_preserves_the_field_of_view[1,2,3]: same base _obj_sampling as the
  montage TestVisualization sampling test.
- TestDatasetVariants: the prlx residual minimum is covered by
  test_is_a_fraction_minimized_at_the_true_defocus.
- test_upsampling_an_irregular_regrid_warns: the warning is required by
  test_upsampling_factor_degrades_on_an_irregular_scan and
  test_a_finer_scan_sampling_recovers_it; its lattice_rms check moved into
  test_no_unfolding_warning_without_an_irregular_regrid.
- test_twisted_layers_separate[honeycomb_scene], test_odd_aberrations_..._sign[C23]: same
  code paths as the square and C21 cases; twisted_honeycomb_cell is dropped from conftest.

Merged (same branch or same setup):
- test_aliases_resolve[4], test_the_matched_filter_has_been_removed[2],
  test_unknown_kernel_raises -> test_kernel_names.
- test_is_a_fraction[4], test_is_minimized_at_the_true_defocus[3] ->
  test_is_a_fraction_minimized_at_the_true_defocus; the two "is None before
  reconstructing" tests -> test_is_none_before_reconstructing.
- test_grid_search, test_grid_search_with_the_rms_gradient,
  test_grid_search_accepts_a_callable, test_recorded_losses_are_the_requested_objective
  -> test_grid_search[residual, rms_gradient, callable].
- test_unknown_losses_are_rejected[3] -> one loop on the shared reconstruction.
- Six round-trip tests (TestSerialization x2, TestFromDataset3d, TestComplexObject,
  TestSectioning, TestPartialCoherence) -> TestSerialization: one gridded reconstruction
  carrying a complex sectioned object, partial coherence and a stored fit, plus one
  ungridded reconstruction.
- TestReconstructAllPermutations (4) and test_permutations_leave_out_only_icom -> one
  table.
- test_lattice_positions_reproduce_from_dataset4d[4 kernels]: regridding does not depend
  on the kernel, so the pair is built once and the kernels looped.
- The four no-warning tests of TestFromDataset3d -> two.
- test_lattice_positions_unfold_through_the_ungridded_path -> an absolute check in
  test_upsampling_factor_degrades_on_an_irregular_scan, which computes the same image.
- test_sections_are_single_plane_reconstructions[5 kernels] and
  test_a_complex_object_has_amplitude_sections -> one complex obf case:
  _reconstruct_sections is kernel-generic apart from the iCoM refusal, which keeps its own
  test.
- TestSectioning::test_aliases[4], test_rejects_an_unknown_mode -> test_mode_names.
- test_kernels_without_a_joint_solve_raise[3] and the identical ssb call in TestSectioning
  -> one test matching both halves of the message.
- test_only_the_normal_equation_kernels_take_it[3], TestWienerKernel's and WDD's ARD
  refusals -> one case in TestArdPrior::test_rejects_bad_arguments (one branch of
  _validate_prior).
- The empirical-probe reproductions (complex ssb/obf, joint obf, and the pure-phase cases
  of the montage file) -> TestEmpiricalProbe, sharing one analytic/empirical pair.
- The two visualize tests, test_obj_is_the_complex_transmission and
  test_pure_phase_has_no_amplitude, test_no_aberrations_means_no_shift into
  test_pure_defocus_matches_the_analytic_shift.
- Cheap parametrizations sharing one object become loops naming the case:
  test_one_mode_is_the_coherent_probe, test_a_double_precision_probe_..., test_one_plane,
  test_slice_thicknesses_are_validated, test_a_finite_source_leaves_obf_...,
  test_rejects_a_negative_spread.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Removed (cannot fail, or exercise no library code):
- TestBandLimit::test_deconvolution_kernels_are_untouched[ssb,obf]: band_limit only
  reaches _resolve_q_lowpass, which returns the cutoff unchanged for ssb/obf either way,
  so both runs were the same computation. test_an_explicit_cutoff_below_the_band_limit_
  still_wins asserts that branch directly.
- test_the_icom_kernel_is_the_ricom_kernel (numpy-only identity),
  test_photon_wavelength_is_not_the_electron_one (core util and literal arithmetic),
  test_nearest_is_a_roll_of_the_bright_field_images (rounding identity),
  test_interpolation_defaults_to_nearest (identical call).
- isfinite-only or "not allclose" checks: test_accepts_the_deconvolution_kernels
  (covered by TestRealSpaceKernels::test_converges_to_the_fourier_kernel),
  test_icom_runs_in_either_mode (test_icom_matches_the_fourier_class,
  test_truncated_icom_is_ricom, TestSparseEvents::test_icom_matches_dense),
  test_scattered_positions_reconstruct (test_scattered_positions_match_dense),
  test_position_axis_order_is_row_col (test_matches_from_dataset4d,
  test_scan_origin_recovers_the_input_positions), test_rotation_changes_the_reconstruction
  (replaced by an exact quarter-turn case in test_matches_parallax_kernel).
- test_fft_mode_beats_a_truncated_stencil (implied by test_fft_mode_is_exact and the
  convergence test), test_torch_size_attributes_round_trip_as_tuples (never round-tripped),
  test_is_analytic_reports_which_path_is_live, test_fft_mode_leaves_no_stencil_info,
  test_dense_and_sparse_are_mutually_exclusive.
- TestIntegerShiftConstruction: its integrality check is now a precondition inside
  test_matches_parallax_kernel.
- Copies of the Fourier file's from_dataset3d tests, which go through the same
  build_vbf_stack_from_dataset3d: auto scan sampling, Dataset2d positions, wrong units,
  mismatched count.
- test_grid_search_recovers_the_seeded_defocus: same base driver as the Fourier
  test_grid_search; the montage objective is covered by
  test_residual_loss_is_minimized_at_the_true_defocus.
- TestComplexAndMultislice::test_parallax_rejects_complex: same
  _validate_reconstruction_mode as the Fourier test.
- The Fourier cases of TestFourierProbe::test_empirical_probe_reproduces_the_analytic_one
  (now TestEmpiricalProbe in the Fourier file) and the montage case of
  test_prlx_runs_on_a_modelled_empirical_probe (test_fitted_coefficients_drive_parallax).
- test_semiangle_cutoff_becomes_optional: its reconstruction moved into
  test_an_empirical_probe_reads_its_extent_off_the_bf_mask.

Merged:
- test_matches_parallax_kernel[pixel_shift x U], test_matches_with_phase_flip,
  test_matches_with_butterworth_filters -> one loop on a shared pair (pixel_shift reaches
  no other path), plus the quarter turn.
- Sampling and field-of-view tests over both classes (scalebar[6], resets, before
  reconstructing, fov, wrapped canvas, wrap default, zero scan origin) ->
  test_scalebar_follows_the_upsampling; padded canvas at U=2 only.
- TestWavelength and TestSemiangleCutoff: the checks live in the base class, so one class;
  six parametrized refusals -> two loops.
- Eight round-trip tests -> TestSerialization::test_round_trip (an ungridded montage with
  scan offset, defocus gradient and a stored fit, and an empirical-probe montage),
  TestSparseEvents::test_survives_a_round_trip, and the wavelength round trip.
- TestDefocusGradient: the two rate tests, the delta tests, the update/leave defocus
  fits and the three defocus_map argument tests each become one; the gradient sweeps
  keep one oblique gradient (plus the zero control for the fit).
- TestRealSpaceKernels: the residual-undefined tests and the auto-radius tests become one
  each. TestConvolutionModes: conv2d[2x2] and the mode-resolution tests become loops.
- TestSparseEvents: the wrap-nearest case was the same call as
  test_deconvolution_stencils_match_dense[ssb]; the pad-bilinear case reuses the class
  pair through reconstruct(boundary="pad"), which is bit-identical to building a pad pair;
  the complex sparse tests move here onto the class pair. The counts are scaled to a mean
  of 4 rather than 40, which leaves every dense/sparse error unchanged (largest 1.3e-5
  against 1e-4) and cuts each from_vector build from 2.2 s to 0.2 s.
- TestFrameDrift: the drift is estimated once per class; the four rejection tests share
  one set of frames.
- The three stencil refusals of TestComplexAndMultislice -> one test.
- A module fixture shares the Fourier/montage pair among the tests that only reconstruct.

GPU:
- TestAccelerators moves to test_direct_ptychography_gpu.py and compares every route with
  the CPU, residual included; the ungridded origin fit now has aberrations, since with
  none the parallax image was identically zero. test_padded_canvas_shape_is_device_
  independent is the pad case of that loop.
- test_sections_match_the_cpu[3 kernels] -> obf: sectioning is a CPU-side loop over the
  single-plane kernels, each compared in test_every_kernel_matches_the_cpu.
- conftest: ACCELERATORS is unused; tilted_layers_4dstem replaces the layered-dataset
  helpers duplicated in the GPU and tilt-series files.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- test_one_untilted_acquisition_is_its_own_reconstruction[3]: each case simulated and
  built its own acquisition; the three cases now loop over one.
- test_upsampling asserted only the shape and finiteness on the five-tilt series. It now
  runs on two noise-free acquisitions and checks the upsampled slices against the
  coarse ones on the coarse grid.
- test_from_tilt_series_builds_each_acquisition: per-dataset keyword forwarding and the
  returned type are asserted by test_survives_a_round_trip.
- test_rejects_bad_series[3], test_rejects_another_scan_grid,
  test_only_the_normal_equation_kernels_combine[4] (a single `kernel not in _KERNELS`
  branch) and test_a_tilt_series_cannot_be_sectioned -> test_rejects_what_it_cannot_combine.
- The noise-free acquisitions are a module fixture shared by test_upsampling, the
  rejections and test_survives_a_round_trip_before_reconstructing; the layered dataset
  comes from conftest's tilted_layers_4dstem.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
test_scalebar_follows_the_upsampling loops over U = 2 and 3 on one pair, so the odd
factor the removed U=3 parametrizations covered is still checked for both classes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The test suite grew from approximately 2k lines in June to 22k lines in
October, mostly from tests added during Claude Code sessions. CLAUDE.md
asks future sessions to extend existing tests before adding new ones, to
test each code path once, to write asserts that can fail, and to use the
smallest problem that exercises the code path.

.gitignore previously excluded CLAUDE.md, so we replaced that entry with
CLAUDE.local.md, which keeps personal notes out of the repository.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With the default `--dist load`, each xdist worker rebuilds every module-
and class-scoped fixture it uses. `--dist loadscope` assigns each test
class (or each module's free functions) to a single worker, so the shared
direct ptychography fixtures are built once. A local run with 4 workers
dropped from approximately 118 s to 97 s.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
TomographyDatasetBase declared DEFAULT_LRS = {"pose_lr": 5e-2}, while
TomographyOpt._get_default_lr reads DEFAULT_LRS.get("pose", 5e-2), so the
declared value was never used and the fallback, which happens to be equal,
always was. The key is now "pose", the plain key every other DEFAULT_LRS
uses.

test_get_default_lr asserted 5e-2, which the fallback also gave, so it could
not fail. It now compares each key with the model's own DEFAULT_LRS, and
fails with a KeyError before this fix.

dataset_models.py already differs from ruff format in lines this commit does
not touch, so the formatter hook was skipped to keep the diff to the fix.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
test_tomography_cpu_recon.py exists to cover the learned reconstruction loop
of Tomography.reconstruct on CPU-only CI, but it was marked slow, which CI
skips. It takes under a second, so the slow marker is removed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
No CI job ran the tests marked slow. They now run with --runslow on pull
requests into main, once per release, on every OS in the release matrix.
Locally they add about 70 s with 4 workers (19 passed, 6 skipped without
CUDA). CLAUDE.md's rule for the slow marker says so.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@gvarnavi

Copy link
Copy Markdown
Collaborator

lgtmc (looks good to my Claude) 😅

@gvarnavi gvarnavi closed this Oct 19, 2026
@gvarnavi gvarnavi reopened this Oct 19, 2026
@arthurmccray

Copy link
Copy Markdown
Collaborator

Okay confirmed that this works for multi-gpus. The only issues I found were in diffraction and other areas you deliberately didn't modify, so I don't think there's anything else to add for now.

@arthurmccray
arthurmccray merged commit 04afe33 into electronmicroscopy:dev Oct 20, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants