From 4c777020838fd5ef0bd226e17a0443d5e852560b Mon Sep 17 00:00:00 2001 From: Garrett Wright Date: Fri, 10 Jul 2026 14:45:18 -0400 Subject: [PATCH 01/11] 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 89e930e09..371b7ead4 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, @@ -19,6 +20,7 @@ ScaledFilter, ZeroFilter, ) +from aspire.source import ArrayImageSource, Simulation from aspire.utils import utest_tolerance logger = logging.getLogger(__name__) @@ -562,3 +564,66 @@ def test_batching_eval(): ref_eval = filter_stack[i].evaluate_grid(L, pixel_size=px) # compare singleton evaluation with __getitem__ from stack np.testing.assert_allclose(stack_eval[i], ref_eval) + +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 94fdcfba210cd47906ea085e0c3c59f1e2455c9b Mon Sep 17 00:00:00 2001 From: Garrett Wright Date: Fri, 10 Jul 2026 14:46:22 -0400 Subject: [PATCH 02/11] 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 371b7ead4..529755a3f 100644 --- a/tests/test_filters.py +++ b/tests/test_filters.py @@ -594,17 +594,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 """ @@ -622,7 +619,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 5b00d683d2b2a12fa4728bf56c5fe0aa572b0a42 Mon Sep 17 00:00:00 2001 From: Garrett Wright Date: Fri, 10 Jul 2026 14:50:16 -0400 Subject: [PATCH 03/11] 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 92152ae90..6582f3478 100644 --- a/src/aspire/operators/filters.py +++ b/src/aspire/operators/filters.py @@ -888,6 +888,12 @@ def __eq__(self, other): return np.all(self._ctf_params() == other._ctf_params()) + 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 6406a8ca6fddff7823c2aa41721c06faaa7a6d3c Mon Sep 17 00:00:00 2001 From: Garrett Wright Date: Fri, 10 Jul 2026 14:59:53 -0400 Subject: [PATCH 04/11] 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 ac642914a..5be2f5664 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 dc7d696ec7352724254d53854be2bad375a127b8 Mon Sep 17 00:00:00 2001 From: Garrett Wright Date: Mon, 13 Jul 2026 09:37:16 -0400 Subject: [PATCH 05/11] 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 b2da4806f..bb9fe0eca 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 74ceee83871443749fffd840e55f32c204e46269 Mon Sep 17 00:00:00 2001 From: Garrett Wright Date: Mon, 13 Jul 2026 10:29:55 -0400 Subject: [PATCH 06/11] 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 bb9fe0eca..c20fe29fb 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 ad972838ef133fe81903d2a91ca9460200e4bfc6 Mon Sep 17 00:00:00 2001 From: Garrett Wright Date: Mon, 13 Jul 2026 11:50:10 -0400 Subject: [PATCH 07/11] 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 529755a3f..6d3c7582a 100644 --- a/tests/test_filters.py +++ b/tests/test_filters.py @@ -146,11 +146,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 0015e98ea182286de9af8cfdf9e6584d5cebcdc3 Mon Sep 17 00:00:00 2001 From: Garrett Wright Date: Mon, 13 Jul 2026 11:53:27 -0400 Subject: [PATCH 08/11] 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 5be2f5664..8a56bdf8b 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 7c1336686..8aeee149a 100644 --- a/src/aspire/source/simulation.py +++ b/src/aspire/source/simulation.py @@ -316,7 +316,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) @@ -327,9 +326,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 93b4aa1d6379d7641750ef3735bb40474d5befd7 Mon Sep 17 00:00:00 2001 From: Garrett Wright Date: Mon, 13 Jul 2026 11:53:50 -0400 Subject: [PATCH 09/11] 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 dd8141a13..848f97387 100644 --- a/src/aspire/source/image.py +++ b/src/aspire/source/image.py @@ -773,12 +773,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 f395416fc34f7c2a6a5b47b2bb8278e08139f79c Mon Sep 17 00:00:00 2001 From: Garrett Wright Date: Mon, 13 Jul 2026 11:55:50 -0400 Subject: [PATCH 10/11] tox --- tests/test_filters.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/tests/test_filters.py b/tests/test_filters.py index 6d3c7582a..a122ec52f 100644 --- a/tests/test_filters.py +++ b/tests/test_filters.py @@ -26,6 +26,7 @@ logger = logging.getLogger(__name__) DATA_DIR = os.path.join(os.path.dirname(__file__), "saved_test_data") +SEED = 707 class SimTestCase(TestCase): @@ -575,13 +576,13 @@ def test_batching_eval(): # compare singleton evaluation with __getitem__ from stack np.testing.assert_allclose(stack_eval[i], ref_eval) + 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 = [ @@ -596,7 +597,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[:]) @@ -617,7 +618,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( @@ -625,11 +625,10 @@ def test_downsample_cache(): vols=vol, offsets=0, amplitudes=1, - seed=707, + seed=SEED, ) sim_ds = src.downsample(K).images[:] sim_dsc = src.downsample(K).cache().images[:] np.testing.assert_allclose(sim_dsc, sim_ds) - From 822bc1792902224d5c783e2fc502c369dd5f46e8 Mon Sep 17 00:00:00 2001 From: Garrett Wright Date: Tue, 28 Jul 2026 10:32:57 -0400 Subject: [PATCH 11/11] minor rebase conflict --- tests/test_filters.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/tests/test_filters.py b/tests/test_filters.py index a122ec52f..4f3c063e2 100644 --- a/tests/test_filters.py +++ b/tests/test_filters.py @@ -585,9 +585,7 @@ def testCTFdownsample(): K = 179 # simulation pixel downsampled angs = np.linspace(0, 2 * np.pi, n) - filter_stack = [ - CTFFilter(defocus_u=10000, defocus_v=15000, defocus_ang=ang) for ang in angs - ] + filter_stack = CTFFilter(defocus_u=10000, defocus_v=15000, defocus_ang=angs) vol = emdb_2660().astype(np.float64) sim = Simulation( @@ -595,13 +593,13 @@ def testCTFdownsample(): vols=vol, offsets=0, amplitudes=1, - unique_filters=filter_stack, + filter_stack=filter_stack, filter_indices=np.arange(n), seed=SEED, ) # Reduce possibility of simulation generation code interacting with the test. src = ArrayImageSource(sim.images[:]) - src.unique_filters = sim.unique_filters + src.filter_stack = sim.filter_stack src.filter_indices = sim.filter_indices sim_pf_ds = src.phase_flip().downsample(K).images[:]