Skip to content

Commit dd26822

Browse files
Jammy2211claude
authored andcommitted
test: imaging sparse invalid-slot parity and nojax helpers (#643)
- sparse_triplets_from zeroes a -1 index and unused slots with non-zero weights (numpy and JAX), and its triplets scatter to mapping_matrix_from; - InversionImagingSparse on a mapper with -1 indices, a negative index in a used slot and weighted unused slots matches the mapping inversion (data_vector, curvature_matrix, mapped reconstructed data) on numpy and JAX backends; - the mapper triplets and both imaging scatter-add helpers run with jax, jaxlib and nufftax blocked (subprocess import hook). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
1 parent 3e53e0a commit dd26822

2 files changed

Lines changed: 406 additions & 0 deletions

File tree

‎test_autoarray/inversion/inversion/imaging/test_imaging.py‎

Lines changed: 264 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
import importlib.util
2+
import os
3+
import subprocess
4+
import sys
5+
import textwrap
6+
17
import autoarray as aa
28

39
from autoarray.inversion.inversion.imaging.inversion_imaging_util import (
@@ -299,3 +305,261 @@ def counted(self, *args, **kwargs):
299305
inversion.mapped_reconstructed_data
300306

301307
assert len(calls) == 1
308+
309+
310+
requires_jax = pytest.mark.skipif(
311+
importlib.util.find_spec("jax") is None,
312+
reason="the imaging sparse operator is built with jax",
313+
)
314+
315+
316+
def _mapper_with_invalid_slots_from(mapper):
317+
"""
318+
Returns a copy of `mapper` whose stencil table carries every kind of entry
319+
`mapping_matrix_from` drops, all with non-zero weights:
320+
321+
- an unused slot (slot >= pix_sizes) padded with index -1 (odd sub-pixels of a Delaunay row);
322+
- a negative index inside a used slot (even sub-pixels, whose pix_size is bumped over a -1 slot);
323+
- an extra unused slot holding a real source pixel index (every sub-pixel).
324+
325+
The dense mapping matrix is unchanged by these entries, so a correct sparse path must be too.
326+
"""
327+
pix_indexes = np.asarray(mapper.pix_indexes_for_sub_slim_index)
328+
pix_weights = np.asarray(mapper.pix_weights_for_sub_slim_index, dtype=float)
329+
pix_sizes = np.asarray(mapper.pix_sizes_for_sub_slim_index).copy()
330+
331+
M_sub, P = pix_indexes.shape
332+
333+
unused = np.arange(P)[None, :] >= pix_sizes[:, None]
334+
335+
assert unused.any()
336+
assert (pix_indexes[unused] == -1).all()
337+
338+
pix_weights = np.where(unused, 0.7, pix_weights)
339+
340+
bump = (np.arange(M_sub) % 2 == 0) & (pix_sizes < P)
341+
pix_sizes[bump] += 1
342+
343+
pix_indexes = np.concatenate(
344+
[pix_indexes, (np.arange(M_sub) % mapper.params)[:, None]], axis=1
345+
)
346+
pix_weights = np.concatenate([pix_weights, np.full((M_sub, 1), 0.9)], axis=1)
347+
348+
class MapperWithInvalidSlots(type(mapper)):
349+
@property
350+
def pix_indexes_for_sub_slim_index(self):
351+
return pix_indexes
352+
353+
@property
354+
def pix_weights_for_sub_slim_index(self):
355+
return pix_weights
356+
357+
@property
358+
def pix_sizes_for_sub_slim_index(self):
359+
return pix_sizes
360+
361+
corrupted = MapperWithInvalidSlots(
362+
interpolator=mapper.interpolator,
363+
regularization=mapper.regularization,
364+
image_plane_mesh_grid=mapper.image_plane_mesh_grid,
365+
)
366+
367+
assert (corrupted.pix_indexes_for_sub_slim_index < 0).any()
368+
assert np.allclose(corrupted.mapping_matrix, mapper.mapping_matrix)
369+
370+
return corrupted
371+
372+
373+
@requires_jax
374+
def test__sparse__mapper_with_invalid_stencil_slots__matches_mapping_inversion(
375+
masked_imaging_7x7, delaunay_mapper_9_3x3
376+
):
377+
mapper = _mapper_with_invalid_slots_from(delaunay_mapper_9_3x3)
378+
379+
settings = aa.Settings(use_positive_only_solver=False)
380+
381+
inversion_sparse = aa.Inversion(
382+
dataset=masked_imaging_7x7.apply_sparse_operator(),
383+
linear_obj_list=[mapper],
384+
settings=settings,
385+
)
386+
inversion_mapping = aa.Inversion(
387+
dataset=masked_imaging_7x7,
388+
linear_obj_list=[mapper],
389+
settings=settings,
390+
)
391+
392+
assert isinstance(inversion_sparse, InversionImagingSparse)
393+
394+
assert np.asarray(inversion_sparse.data_vector) == pytest.approx(
395+
np.asarray(inversion_mapping.data_vector), 1.0e-6
396+
)
397+
assert np.asarray(inversion_sparse.curvature_matrix) == pytest.approx(
398+
np.asarray(inversion_mapping.curvature_matrix), 1.0e-6
399+
)
400+
401+
reconstruction = np.asarray(inversion_sparse.reconstruction)
402+
403+
assert reconstruction == pytest.approx(
404+
np.asarray(inversion_mapping.reconstruction), 1.0e-4
405+
)
406+
407+
mapped = np.asarray(inversion_sparse.mapped_reconstructed_data_dict[mapper].array)
408+
409+
assert mapped == pytest.approx(
410+
np.asarray(mapper.mapping_matrix) @ reconstruction, 1.0e-8
411+
)
412+
assert mapped == pytest.approx(
413+
np.asarray(inversion_mapping.mapped_reconstructed_data_dict[mapper].array),
414+
1.0e-4,
415+
)
416+
assert np.asarray(
417+
inversion_sparse.mapped_reconstructed_operated_data_dict[mapper].array
418+
) == pytest.approx(
419+
np.asarray(
420+
inversion_mapping.mapped_reconstructed_operated_data_dict[mapper].array
421+
),
422+
1.0e-4,
423+
)
424+
425+
426+
@requires_jax
427+
def test__sparse__mapper_with_invalid_stencil_slots__matches_mapping_inversion__jax(
428+
masked_imaging_7x7, delaunay_mapper_9_3x3
429+
):
430+
import jax
431+
432+
jax.config.update("jax_enable_x64", True)
433+
434+
import jax.numpy as jnp
435+
436+
mapper = _mapper_with_invalid_slots_from(delaunay_mapper_9_3x3)
437+
mapper_jax = type(mapper)(
438+
interpolator=mapper.interpolator,
439+
regularization=mapper.regularization,
440+
image_plane_mesh_grid=mapper.image_plane_mesh_grid,
441+
xp=jnp,
442+
)
443+
444+
settings = aa.Settings(use_positive_only_solver=False)
445+
446+
inversion_sparse = aa.Inversion(
447+
dataset=masked_imaging_7x7.apply_sparse_operator(),
448+
linear_obj_list=[mapper_jax],
449+
settings=settings,
450+
xp=jnp,
451+
)
452+
inversion_mapping = aa.Inversion(
453+
dataset=masked_imaging_7x7,
454+
linear_obj_list=[mapper],
455+
settings=settings,
456+
)
457+
458+
assert isinstance(inversion_sparse, InversionImagingSparse)
459+
460+
assert np.asarray(inversion_sparse.data_vector) == pytest.approx(
461+
np.asarray(inversion_mapping.data_vector), 1.0e-6
462+
)
463+
assert np.asarray(inversion_sparse.curvature_matrix) == pytest.approx(
464+
np.asarray(inversion_mapping.curvature_matrix), 1.0e-6
465+
)
466+
467+
reconstruction = np.asarray(inversion_sparse.reconstruction)
468+
469+
assert np.asarray(
470+
inversion_sparse.mapped_reconstructed_data_dict[mapper_jax].array
471+
) == pytest.approx(np.asarray(mapper.mapping_matrix) @ reconstruction, 1.0e-8)
472+
473+
474+
def test__sparse_helpers__import_and_run_with_jax_blocked():
475+
"""
476+
The mapper triplets and the two imaging scatter-add helpers (`data_vector_via_psf_weighted_data_from`,
477+
`mapped_reconstructed_operated_data_via_sparse_operator_from`) must work in the `unittest-nojax`
478+
leg. JAX is in `sys.modules` for the rest of a normal test session, so a subprocess hides `jax`,
479+
`jaxlib` and `nufftax` from the import system and checks them against the dense mapping matrix
480+
on a Delaunay mapper, whose unused stencil slots carry `-1` indices.
481+
482+
The `ImagingSparseOperator` itself (FFT precompute and curvature blocks) is JAX-only, so the full
483+
`InversionImagingSparse` is not run here.
484+
"""
485+
script = textwrap.dedent(
486+
"""
487+
import importlib.machinery
488+
import sys
489+
490+
_find_spec = importlib.machinery.PathFinder.find_spec.__func__
491+
492+
493+
def _find_spec_blocking_jax(cls, name, path=None, target=None):
494+
if name.split(".")[0] in ("jax", "jaxlib", "nufftax"):
495+
return None
496+
return _find_spec(cls, name, path, target)
497+
498+
499+
importlib.machinery.PathFinder.find_spec = classmethod(_find_spec_blocking_jax)
500+
501+
# Guard the guard: if the block stopped working this test would pass vacuously.
502+
try:
503+
import jax
504+
except ModuleNotFoundError:
505+
pass
506+
else:
507+
raise AssertionError("jax was not blocked")
508+
509+
import numpy as np
510+
511+
import autoarray as aa
512+
from autoarray import fixtures
513+
from autoarray.inversion.inversion.imaging import inversion_imaging_util
514+
515+
mapper = fixtures.make_delaunay_mapper_9_3x3()
516+
mask = fixtures.make_mask_2d_7x7()
517+
518+
assert (np.asarray(mapper.pix_indexes_for_sub_slim_index) < 0).any()
519+
520+
mapping_matrix = np.asarray(mapper.mapping_matrix)
521+
522+
rows, cols, vals = mapper.sparse_triplets_data
523+
524+
rng = np.random.default_rng(seed=1)
525+
psf_weighted_data = rng.normal(size=mapping_matrix.shape[0])
526+
527+
data_vector = inversion_imaging_util.data_vector_via_psf_weighted_data_from(
528+
psf_weighted_data=psf_weighted_data,
529+
rows=rows,
530+
cols=cols,
531+
vals=vals,
532+
S=mapper.params,
533+
xp=np,
534+
)
535+
536+
assert np.allclose(data_vector, mapping_matrix.T @ psf_weighted_data)
537+
538+
rows, cols, vals = mapper.sparse_triplets_curvature
539+
540+
reconstruction = rng.normal(size=mapper.params)
541+
542+
mapped = inversion_imaging_util.mapped_reconstructed_operated_data_via_sparse_operator_from(
543+
reconstruction=reconstruction,
544+
rows=rows,
545+
cols=cols,
546+
vals=vals,
547+
fft_index_for_masked_pixel=mask.fft_index_for_masked_pixel,
548+
data_shape=mask.shape_native,
549+
xp=np,
550+
)
551+
552+
assert np.allclose(mapped, mapping_matrix @ reconstruction)
553+
554+
assert not any(
555+
name.split(".")[0] in ("jax", "jaxlib", "nufftax") for name in sys.modules
556+
)
557+
"""
558+
)
559+
560+
environment = dict(os.environ)
561+
environment["PYAUTO_DISABLE_JAX"] = "1"
562+
563+
subprocess.run(
564+
[sys.executable, "-c", script], check=True, env=environment, timeout=600
565+
)

0 commit comments

Comments
 (0)