Skip to content

Conversation

@j-c-c
Copy link
Collaborator

@j-c-c j-c-c commented Nov 3, 2025

Save ImageSource's with Relion >= 3.1 convention for starfiles, with separate data_optics and data_particle blocks.

Some other additions in this PR are:

  • Always store amplitudes in doubles
  • Rename _rlnAmplitude metadata field to _aspireAmplitude, since _rlnAmplitude is not a valid Relion field name
  • Save ImageSource mrcs files with pixel/voxel size in header. Some Relion command line tools extract pixel size (angpix) from here.
  • Save with optics block required fields _rlnImageSize and _rlnImageDimensionality
  • Testing for added features
  • Loading new file format in Relion was verified using several Relion CLI tools with Relion version 5.0.1

@j-c-c j-c-c self-assigned this Nov 3, 2025
@j-c-c j-c-c added cleanup extern Relating to external changes labels Nov 3, 2025
@j-c-c j-c-c requested a review from garrettwrong November 4, 2025 14:59
Copy link
Collaborator

@garrettwrong garrettwrong left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice, thanks. Couple minor things noted.

Can you add a test that covers slicing the source? I don't think arbitrary slicing will mess up your logic, but it might in the future. Better to add test now while it works :D.

If you haven't let's make sure you can convert some real STAR files that we use. (Read them in and write them out in a re-usable way, ideally with the user doing nothing...). If we can't, that is going to be a problem and I'm not sure we could take the code yet. This may be fine already, so don't interpret it as a code criticism 😇 .

Can we document in a tutorial an example that shows generating a realistic Simulation, saving with this new format, and loading with Relion? (This can be a non-running, display only, example). I see your testing that we can load one back ourselves, but, we're probably not the target application...

Thanks!

optics_block["_rlnOpticsGroup"] = []
optics_block["_rlnOpticsGroupName"] = []
for field in optics_value_fields:
optics_block[field] = []
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is good. Try using a defaultdict(list) for optics_block and see if that reduces some of this code.

Fact check me, but I believe all Python dicts are now naturally ordered for versions of Python we support.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, that's cleaner. Thanks.

Yep, you're correct about dicts being ordered.

if "_rlnImageSize" not in metadata:
metadata["_rlnImageSize"] = np.full(self.n, self.L, dtype=int)

if "_rlnImageDimensionality" not in metadata:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what in the world

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

😅

metadata = RelionStarFile(self.filepath).get_merged_data_block()

# Promote legacy _rlnAmplitude column to the ASPIRE-specific name.
if "_rlnAmplitude" in metadata and "_aspireAmplitude" not in metadata:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I appreciate you are trying to make old files forward compatible. Nice idea, but perhaps too dangerous. Consider if Relion decides tomorrow they want to use _rlnAmplitude ...

How much stuff is actually going to break if we do not do this...?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not going to break anything. I just had it in for compatibility with old files. Removed.

# of certain key fields used in the codebase,
# which are originally read from Relion STAR files.
relion_metadata_fields = {
"_aspireAmplitude": float,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wasn't the whole point that _aspireAmplitude doesn't belong in relion_metadata_fields 😇 ?...

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oops. Removed.

np.testing.assert_equal(src.amplitudes.dtype, dtype)

# offsets are always stored as doubles
# offsets and amplitudes are always stored as doubles
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good idea, thanks

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep!

sim.save(starpath, overwrite=True)

star = RelionStarFile(str(starpath))
assert star.relion_version == "3.1"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just curious, are we exactly making 3.1 only, or some later version or ? What versions are possible?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's really 3.1 up to the current version. The format has stayed the same, with the added optics block, since 3.1.

np.testing.assert_array_equal(optics["_rlnImageSize"], np.full(kv_ct, res))
np.testing.assert_array_equal(optics["_rlnImageDimensionality"], np.full(kv_ct, 2))

# Due to Simulation random indexing, voltages will be unordered
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think that is true. You should have the order of filters in the simulation. I think this ordering would follow from that one if I understand correctly.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe "unordered" is the wrong word here. The optics["_rlnVoltage"])'s will be in the order they were encountered during save. That will not necessarily be the same ascending order the voltages were in when we created the ctf filters (which get applied randomly to the images).



@pytest.mark.parametrize("batch_size", [1, 6])
def test_simulation_save_sets_voxel_size(tmp_path, batch_size):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice, thanks

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

😇

@codecov
Copy link

codecov bot commented Nov 5, 2025

Codecov Report

❌ Patch coverage is 98.33333% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 90.68%. Comparing base (34f82da) to head (5cb9557).

Files with missing lines Patch % Lines
src/aspire/source/image.py 98.14% 1 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff             @@
##           develop    #1337      +/-   ##
===========================================
+ Coverage    90.65%   90.68%   +0.02%     
===========================================
  Files          134      134              
  Lines        14431    14487      +56     
===========================================
+ Hits         13082    13137      +55     
- Misses        1349     1350       +1     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@j-c-c j-c-c force-pushed the save_optics_group branch from 934a2cd to a3d6381 Compare November 14, 2025 13:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

cleanup extern Relating to external changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants