From 8dd1f77b1031c47b41e35d8f98e69844b6c187e6 Mon Sep 17 00:00:00 2001 From: Garrett Wright Date: Fri, 10 Jul 2026 14:45:18 -0400 Subject: [PATCH 01/10] add ds->pf pf->ds ds->cache->pf test --- tests/test_filters.py | 65 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/tests/test_filters.py b/tests/test_filters.py index b96bc6dff3..002fc23422 100644 --- a/tests/test_filters.py +++ b/tests/test_filters.py @@ -5,6 +5,7 @@ import numpy as np import pytest +from aspire.downloader import emdb_2660 from aspire.operators import ( ArrayFilter, CTFFilter, @@ -16,6 +17,7 @@ ScaledFilter, ZeroFilter, ) +from aspire.source import ArrayImageSource, Simulation from aspire.utils import utest_tolerance DATA_DIR = os.path.join(os.path.dirname(__file__), "saved_test_data") @@ -245,3 +247,66 @@ def test_ctf_reference(): # Test match all significant digits above np.testing.assert_allclose(h, ref_h, atol=5e-5) + + +def testCTFdownsample(): + """ + Compare CTF Phaseflip -> Downsample vs Downsample -> Phaseflip + """ + n = 10 # number of filters in stack + K = 179 # simulation pixel downsampled + SEED = 707 + + angs = np.linspace(0, 2 * np.pi, n) + filter_stack = [ + CTFFilter(defocus_u=10000, defocus_v=15000, defocus_ang=ang) for ang in angs + ] + + vol = emdb_2660().astype(np.float64) + sim = Simulation( + n=n, + vols=vol, + offsets=0, + amplitudes=1, + unique_filters=filter_stack, + filter_indices=np.arange(n), + seed=707, + ) + # Reduce possibility of simulation generation code interacting with the test. + src = ArrayImageSource(sim.images[:]) + src.unique_filters = sim.unique_filters + src.filter_indices = sim.filter_indices + + sim_pf_ds = src.phase_flip().downsample(K).images[:] + print("----------------------------------") + sim_ds_pf = src.downsample(K).phase_flip().images[:] + print("2----------------------------------") + + np.testing.assert_allclose(sim_ds_pf, sim_pf_ds) + + sim_dsc_pf = src.downsample(K).cache().phase_flip().images[:] + np.testing.assert_allclose(sim_dsc_pf, sim_pf_ds) + + +def testdownsamplecache(): + """ + Compare Downsample Cache vs Downsample + """ + n = 10 # number of filters in stack + K = 179 # simulation pixel downsampled + SEED = 707 + + vol = emdb_2660().astype(np.float64) + src = Simulation( + n=n, + vols=vol, + offsets=0, + amplitudes=1, + seed=707, + ) + + sim_ds = src.downsample(K).images[:] + print("----------------------------------") + sim_dsc = src.downsample(K).cache().images[:] + + np.testing.assert_allclose(sim_dsc, sim_ds) From a799a8a5e07d089c6286480520cfc3cf445bc917 Mon Sep 17 00:00:00 2001 From: Garrett Wright Date: Fri, 10 Jul 2026 14:46:22 -0400 Subject: [PATCH 02/10] add ds->pf pf->ds ds->cache->pf test --- tests/test_filters.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/tests/test_filters.py b/tests/test_filters.py index 002fc23422..7dc367365d 100644 --- a/tests/test_filters.py +++ b/tests/test_filters.py @@ -278,17 +278,14 @@ def testCTFdownsample(): src.filter_indices = sim.filter_indices sim_pf_ds = src.phase_flip().downsample(K).images[:] - print("----------------------------------") sim_ds_pf = src.downsample(K).phase_flip().images[:] - print("2----------------------------------") - np.testing.assert_allclose(sim_ds_pf, sim_pf_ds) sim_dsc_pf = src.downsample(K).cache().phase_flip().images[:] np.testing.assert_allclose(sim_dsc_pf, sim_pf_ds) -def testdownsamplecache(): +def test_downsample_cache(): """ Compare Downsample Cache vs Downsample """ @@ -306,7 +303,6 @@ def testdownsamplecache(): ) sim_ds = src.downsample(K).images[:] - print("----------------------------------") sim_dsc = src.downsample(K).cache().images[:] np.testing.assert_allclose(sim_dsc, sim_ds) From 3643192aaebdf539d274cbd6702476d3a46e2c45 Mon Sep 17 00:00:00 2001 From: Garrett Wright Date: Fri, 10 Jul 2026 14:50:16 -0400 Subject: [PATCH 03/10] custom scale for CTFFilter --- src/aspire/operators/filters.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/aspire/operators/filters.py b/src/aspire/operators/filters.py index 3c3f362fc3..083ae41d0a 100644 --- a/src/aspire/operators/filters.py +++ b/src/aspire/operators/filters.py @@ -505,6 +505,12 @@ def _evaluate(self, omega, **kwargs): return h + def scale(self, c=1): + """ + Override internal scaling for CTFFilter because they are passed pixel size explicitly. + """ + return self + class RadialCTFFilter(CTFFilter): def __init__(self, voltage=200, defocus=15000, Cs=2.26, alpha=0.07, B=0): From 23f1fc77d9dde96900d0e4b5ebfbfb7b0f6e0d4c Mon Sep 17 00:00:00 2001 From: Garrett Wright Date: Fri, 10 Jul 2026 14:59:53 -0400 Subject: [PATCH 04/10] fix bug in downsample --- src/aspire/image/xform.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/aspire/image/xform.py b/src/aspire/image/xform.py index 20a2e62c5c..4ef1512151 100644 --- a/src/aspire/image/xform.py +++ b/src/aspire/image/xform.py @@ -226,9 +226,9 @@ def _forward(self, im, indices): centered_fft=self.centered_fft, ) - # pixel_size has already been adjusted in the ImageSource and passed - # to `im`, so we instantiate the new Image with im.pixel_size. - return Image(im_ds, pixel_size=im.pixel_size).stack_reshape( + # pixel_size needs to be maintained correctly + scale = im.resolution / self.resolution + return Image(im_ds, pixel_size=im.pixel_size * scale).stack_reshape( original_stack_shape ) From 69f03516c42dfa4d3263dca1dafb5ec1c2c392f4 Mon Sep 17 00:00:00 2001 From: Garrett Wright Date: Mon, 13 Jul 2026 09:37:16 -0400 Subject: [PATCH 05/10] add assert to test msg --- tests/test_downsample.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_downsample.py b/tests/test_downsample.py index 80a6f1e69a..0a27ebe539 100644 --- a/tests/test_downsample.py +++ b/tests/test_downsample.py @@ -98,7 +98,7 @@ def test_downsample_2d_case(L, L_ds): assert checkCenterPoint(imgs_org, imgs_ds) # Confirm default `pixel_size` assert np.allclose(imgs_org.pixel_size, 1.0) - assert np.allclose(imgs_ds.pixel_size, imgs_org.pixel_size * (L / L_ds)) + assert np.allclose(imgs_ds.pixel_size, imgs_org.pixel_size * (L / L_ds)), f"{imgs_ds.pixel_size}, {imgs_org.pixel_size*(L / L_ds)}" @pytest.mark.parametrize("L", [65, 66]) From 2066235303c7ab4aec31d452771dd7a61a3ca1f5 Mon Sep 17 00:00:00 2001 From: Garrett Wright Date: Mon, 13 Jul 2026 10:29:55 -0400 Subject: [PATCH 06/10] add assert to test msg --- tests/test_downsample.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/test_downsample.py b/tests/test_downsample.py index 0a27ebe539..8d801cf58f 100644 --- a/tests/test_downsample.py +++ b/tests/test_downsample.py @@ -98,7 +98,9 @@ def test_downsample_2d_case(L, L_ds): assert checkCenterPoint(imgs_org, imgs_ds) # Confirm default `pixel_size` assert np.allclose(imgs_org.pixel_size, 1.0) - assert np.allclose(imgs_ds.pixel_size, imgs_org.pixel_size * (L / L_ds)), f"{imgs_ds.pixel_size}, {imgs_org.pixel_size*(L / L_ds)}" + assert np.allclose( + imgs_ds.pixel_size, imgs_org.pixel_size * (L / L_ds) + ), f"{imgs_ds.pixel_size}, {imgs_org.pixel_size*(L / L_ds)}" @pytest.mark.parametrize("L", [65, 66]) From 0004ed3095d602b2289221838b7baf7d2db31192 Mon Sep 17 00:00:00 2001 From: Garrett Wright Date: Mon, 13 Jul 2026 11:50:10 -0400 Subject: [PATCH 07/10] update ctf filter test --- tests/test_filters.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/tests/test_filters.py b/tests/test_filters.py index 7dc367365d..df06d2e73f 100644 --- a/tests/test_filters.py +++ b/tests/test_filters.py @@ -141,11 +141,21 @@ def testCTFScale(self): result1 = filt.evaluate(self.omega, **self.filter_eval_kwargs) scale_value = 2.5 filt = filt.scale(scale_value) - # scaling a CTFFilter scales the pixel size which cancels out - # a corresponding scaling in omega - result2 = filt.evaluate(self.omega * scale_value, **self.filter_eval_kwargs) + # Scaling a CTFFilter is a no op; as of v15.0 scaling controlled by the pixel size. + result2 = filt.evaluate(self.omega, **self.filter_eval_kwargs) self.assertTrue(np.allclose(result1, result2, atol=utest_tolerance(self.dtype))) + # However, we can still test scaling pixel_size against scaling omega grid + px_sz = self.filter_eval_kwargs["pixel_size"] + # Scaling a CTFFilter pixel size should match a corresponding scaling in omega. + result3 = filt.evaluate( + self.omega / scale_value, pixel_size=px_sz + ) # scale omega + result4 = filt.evaluate( + self.omega, pixel_size=px_sz * scale_value + ) # scale pixel size + self.assertTrue(np.allclose(result4, result3, atol=utest_tolerance(self.dtype))) + DTYPES = [np.float32, np.float64] EPS = [None, 0.01] From dde9f61c66731104e26bfbecfe1861216c961286 Mon Sep 17 00:00:00 2001 From: Garrett Wright Date: Mon, 13 Jul 2026 11:53:27 -0400 Subject: [PATCH 08/10] try to fix actual big this time --- src/aspire/image/xform.py | 11 +---------- src/aspire/source/simulation.py | 4 ---- 2 files changed, 1 insertion(+), 14 deletions(-) diff --git a/src/aspire/image/xform.py b/src/aspire/image/xform.py index 4ef1512151..93b40228a2 100644 --- a/src/aspire/image/xform.py +++ b/src/aspire/image/xform.py @@ -217,21 +217,12 @@ def __init__(self, resolution, zero_nyquist=True, centered_fft=True): super().__init__() def _forward(self, im, indices): - original_stack_shape = im.stack_shape - data = im.stack_reshape(-1)._data - im_ds = Image._downsample( - data, + return im.downsample( self.resolution, zero_nyquist=self.zero_nyquist, centered_fft=self.centered_fft, ) - # pixel_size needs to be maintained correctly - scale = im.resolution / self.resolution - return Image(im_ds, pixel_size=im.pixel_size * scale).stack_reshape( - original_stack_shape - ) - def _adjoint(self, im, indices): # TODO: Implement up-sampling with zero-padding raise NotImplementedError("Adjoint of downsampling not implemented yet.") diff --git a/src/aspire/source/simulation.py b/src/aspire/source/simulation.py index b5de869a67..982d7b2bb6 100644 --- a/src/aspire/source/simulation.py +++ b/src/aspire/source/simulation.py @@ -320,7 +320,6 @@ def _images(self, indices, clean_images=False): if not clean_images and self.noise_adder is not None: im = self.noise_adder.forward(im, indices=indices) - # scaling pixel_size in source, scaling filter, and scaling in IMage.downsample in conflict... # Finally, apply transforms to resulting Image return self.generation_pipeline.forward(im, indices) @@ -331,9 +330,6 @@ def _apply_sim_filters(self, im, indices): self.filter_indices[indices], ) - # Assign correct pixel_size - im.pixel_size = self.pixel_size - return im def vol_coords(self, mean_vol=None, eig_vols=None): From d33834f9d1c454fee2bd34f8b3468a8d58a1504a Mon Sep 17 00:00:00 2001 From: Garrett Wright Date: Mon, 13 Jul 2026 11:53:50 -0400 Subject: [PATCH 09/10] remove old conversion code, no longer needed --- src/aspire/source/image.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/aspire/source/image.py b/src/aspire/source/image.py index 0660f402c5..64b12ad1ab 100644 --- a/src/aspire/source/image.py +++ b/src/aspire/source/image.py @@ -776,12 +776,6 @@ def _apply_filters( :param filters: A list of `Filter` objects :param indices: A list of indices indicating the corresponding filter in `filters` """ - if not isinstance(im_orig, Image): - logger.warning( - f"_apply_filters() passed {type(im_orig)} instead of Image instance" - ) - # for now just convert it - im_orig = Image(im_orig, pixel_size=self.pixel_size) im = im_orig.copy() From 8bd4fbb8ce4218f6c51e9577f6196c9f5efadea3 Mon Sep 17 00:00:00 2001 From: Garrett Wright Date: Mon, 13 Jul 2026 11:55:50 -0400 Subject: [PATCH 10/10] tox --- tests/test_filters.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/tests/test_filters.py b/tests/test_filters.py index df06d2e73f..e91bd768eb 100644 --- a/tests/test_filters.py +++ b/tests/test_filters.py @@ -21,6 +21,7 @@ from aspire.utils import utest_tolerance DATA_DIR = os.path.join(os.path.dirname(__file__), "saved_test_data") +SEED = 707 class SimTestCase(TestCase): @@ -265,7 +266,6 @@ def testCTFdownsample(): """ n = 10 # number of filters in stack K = 179 # simulation pixel downsampled - SEED = 707 angs = np.linspace(0, 2 * np.pi, n) filter_stack = [ @@ -280,7 +280,7 @@ def testCTFdownsample(): amplitudes=1, unique_filters=filter_stack, filter_indices=np.arange(n), - seed=707, + seed=SEED, ) # Reduce possibility of simulation generation code interacting with the test. src = ArrayImageSource(sim.images[:]) @@ -301,7 +301,6 @@ def test_downsample_cache(): """ n = 10 # number of filters in stack K = 179 # simulation pixel downsampled - SEED = 707 vol = emdb_2660().astype(np.float64) src = Simulation( @@ -309,7 +308,7 @@ def test_downsample_cache(): vols=vol, offsets=0, amplitudes=1, - seed=707, + seed=SEED, ) sim_ds = src.downsample(K).images[:]