Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 24 additions & 8 deletions src/aspire/source/image.py
Original file line number Diff line number Diff line change
Expand Up @@ -417,17 +417,31 @@ def filter_indices(self, indices):

@property
def offsets(self):
return np.atleast_2d(
self.get_metadata(
["_rlnOriginX", "_rlnOriginY"],
default_value=np.array(0.0, dtype=self.dtype),
"""
Get pixel offsets.
"""
# Use pixel_size = 1 for sources agnostic to pixel_size.
px_sz = self.pixel_size or 1.0
return (
np.atleast_2d(
self.get_metadata(
["_rlnOriginXAngst", "_rlnOriginYAngst"],
default_value=np.array(0.0, dtype=self.dtype),
)
)
/ px_sz
)

@offsets.setter
def offsets(self, values):
"""
Set angstrom valued offsets from pixel offset values.
"""
# Use pixel_size = 1 for sources agnostic to pixel_size.
px_sz = self.pixel_size or 1.0
return self.set_metadata(
["_rlnOriginX", "_rlnOriginY"], np.array(values, dtype=self.dtype)
["_rlnOriginXAngst", "_rlnOriginYAngst"],
np.array(values * px_sz, dtype=self.dtype),
)

@property
Expand Down Expand Up @@ -782,9 +796,11 @@ def downsample(self, L, zero_nyquist=True, legacy=False):

ds_factor = self.L / L
self.unique_filters = [f.scale(ds_factor) for f in self.unique_filters]
self.offsets /= ds_factor
if self.pixel_size is not None:
self.pixel_size *= ds_factor
else:
# For sources agnostic to pixel size, offsets must be explicitly scaled.
self.offsets /= ds_factor

self.L = L

Expand Down Expand Up @@ -1657,8 +1673,8 @@ def _reset_orientation(self):
"_rlnAngleRot",
"_rlnAngleTilt",
"_rlnAnglePsi",
"_rlnOriginX",
"_rlnOriginY",
"_rlnOriginXAngst",
"_rlnOriginYAngst",
]
for key in rot_keys:
if self.has_metadata(key):
Expand Down
9 changes: 9 additions & 0 deletions src/aspire/source/relion.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,15 @@ def __init__(
pixel_size = 1.0
self.pixel_size = float(pixel_size)

# Ensure Relion >= 3.1 convention for offsets
offset_keys = ["_rlnOriginX", "_rlnOriginY"]
if self.has_metadata(offset_keys):
# The setter will store offsets as _rlnOriginX(Y)Angst in metadata
self.offsets = np.atleast_2d(self.get_metadata(offset_keys))
# Remove old convention from metadata
for key in offset_keys:
del self._metadata[key]

# CTF estimation parameters coming from Relion
CTF_params = [
"_rlnVoltage",
Expand Down
2 changes: 2 additions & 0 deletions src/aspire/utils/relion_interop.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@
"_rlnGroupNumber": str,
"_rlnOriginX": float,
"_rlnOriginY": float,
"_rlnOriginXAngst": float,
"_rlnOriginYAngst": float,
"_rlnAngleRot": float,
"_rlnAngleTilt": float,
"_rlnAnglePsi": float,
Expand Down
68 changes: 68 additions & 0 deletions tests/test_relion_source.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import pytest

from aspire.source import RelionSource
from aspire.utils import RelionStarFile
from aspire.volume import SymmetryGroup

from .test_starfile_stack import StarFileTestCase
Expand Down Expand Up @@ -97,3 +98,70 @@ def test_pixel_size(caplog):
src = RelionSource(starfile_no_pix_size)
assert msg in caplog.text
np.testing.assert_equal(src.pixel_size, 1.0)


def test_offsets_conversion():
"""
Check that offset convention gets converted to Relion >= 3.1 convention.
"""
starfile = os.path.join(DATA_DIR, "sample_particles_relion30.star")

# Extract pixel valued offsets from starfile prior to source instantiation.
metadata = RelionStarFile(starfile).get_merged_data_block()
pixel_offsets = np.column_stack((metadata["_rlnOriginX"], metadata["_rlnOriginY"]))

# Create Relion Source and extract offsets from metadata using updated field names.
src = RelionSource(starfile)
angst_offsets = src.get_metadata(["_rlnOriginXAngst", "_rlnOriginYAngst"])

# Check that old convention offset fields have been removed.
assert "_rlnOriginX" not in src._metadata
assert "_rlnOriginY" not in src._metadata

# Check that offsets in metadata match up to pixel/angstrom conversion.
np.testing.assert_allclose(angst_offsets / src.pixel_size, pixel_offsets)

# src.offsets should still return pixel valued offsets.
np.testing.assert_allclose(src.offsets, pixel_offsets)


def test_offsets():
"""
Check that offsets are loaded properly with starfile field _rlnOriginX(Y)Angst.
"""
# This starfile has offsets stored with angstrom values as _rlnOriginX(Y)Angst.
starfile = os.path.join(DATA_DIR, "sample_particles_relion31.star")

# Create a RelionSource
src = RelionSource(starfile)

# Check offsets are angstrom valued in metadata and correspond to src.offsets.
angst_offsets = src.get_metadata(["_rlnOriginXAngst", "_rlnOriginYAngst"])
np.testing.assert_allclose(src.offsets * src.pixel_size, angst_offsets)


def test_offsets_save(tmp_path):
"""
Test that saving a RelionSource that was loaded with pixel offsets
saves with angstrom valued offsets.
"""
# Starfile with pixel offsets.
starfile = os.path.join(DATA_DIR, "sample_particles_relion30.star")

# Extract pixel valued offsets from starfile prior to source instantiation.
metadata = RelionStarFile(starfile).get_merged_data_block()
pixel_offsets = np.column_stack((metadata["_rlnOriginX"], metadata["_rlnOriginY"]))

# Create and RelionSource and save to starfile.
src = RelionSource(starfile)
save_path = tmp_path / "test_file.star"
src.save(save_path)

# Saved starfile should have angstrom valued offsets.
metadata = RelionStarFile(save_path).get_merged_data_block()
angst_offsets = np.column_stack(
(metadata["_rlnOriginXAngst"], metadata["_rlnOriginYAngst"])
)

# Check saved offsets match original up to pixel_size scaling.
np.testing.assert_allclose(angst_offsets / src.pixel_size, pixel_offsets)