diff --git a/src/aspire/image/xform.py b/src/aspire/image/xform.py index 20a2e62c5c..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 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( - 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/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): 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() 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): diff --git a/tests/test_downsample.py b/tests/test_downsample.py index 80a6f1e69a..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)) + 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]) diff --git a/tests/test_filters.py b/tests/test_filters.py index b96bc6dff3..e91bd768eb 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,9 +17,11 @@ 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") +SEED = 707 class SimTestCase(TestCase): @@ -139,11 +142,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] @@ -245,3 +258,60 @@ 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 + + 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=SEED, + ) + # 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[:] + sim_ds_pf = src.downsample(K).phase_flip().images[:] + 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 test_downsample_cache(): + """ + Compare Downsample Cache vs Downsample + """ + n = 10 # number of filters in stack + K = 179 # simulation pixel downsampled + + vol = emdb_2660().astype(np.float64) + src = Simulation( + n=n, + vols=vol, + offsets=0, + amplitudes=1, + seed=SEED, + ) + + sim_ds = src.downsample(K).images[:] + sim_dsc = src.downsample(K).cache().images[:] + + np.testing.assert_allclose(sim_dsc, sim_ds)