Skip to content

Add bound-only mask accessor compatible with current particle selection#93

Open
diegod-01 wants to merge 9 commits into
SWIFTSIM:mainfrom
diegod-01:main
Open

Add bound-only mask accessor compatible with current particle selection#93
diegod-01 wants to merge 9 commits into
SWIFTSIM:mainfrom
diegod-01:main

Conversation

@diegod-01
Copy link
Copy Markdown

This PR adds a new SWIFTGalaxy method to retrieve a bound-only mask without applying it immediately.

What is included:
• Adds get_bound_only_mask(relative_to_current=True).
• Preserves lazy-loading and lazy-mask behavior.
• By default, returns a mask aligned with the currently selected particles (after spatial mask and any existing extra masks), so it is ready to apply directly.
• Supports relative_to_current=False to return the mask in post-spatial-mask index space.
• Raises a clear error when no halo catalogue is attached (probably superfluous).

– Tests –
Added masking tests for:
• error behavior without a halo catalogue,
• lazy behavior (no eager data loading),
• compatibility with currently masked particle arrays,
• default relative_to_current behavior.
Local checks passed for masking and halo mask-compatibility test subsets.

@kyleaoman kyleaoman added the enhancement New feature or request label Apr 10, 2026
@kyleaoman
Copy link
Copy Markdown
Member

kyleaoman commented Apr 10, 2026

An initial look revealed a case that breaks:

import unyt as u
from swiftsimio import cosmo_quantity
from swiftgalaxy import SWIFTGalaxy, SOAP, MaskCollection
from swiftgalaxy.demo_data import generated_examples

soap = SOAP(generated_examples.soap, soap_index=0, extra_mask=None)
sg = SWIFTGalaxy(generated_examples.virtual_snapshot, soap)
mask = MaskCollection(
    gas=sg.gas.spherical_coordinates.r
    < cosmo_quantity(
        5, u.kpc, comoving=True, scale_factor=sg.metadata.scale_factor, scale_exponent=1
    )
)
sg.mask_particles(mask)
bound_mask = sg.get_bound_only_mask()
bound_mask.dark_matter.mask  # ok
bound_mask.gas.mask  # IndexError

In general it looks like there are some issues around keeping the loaded data and the masks in the right state (which masks are applied when). Another breaking case:

import unyt as u
from swiftsimio import cosmo_quantity
from swiftgalaxy import SWIFTGalaxy, SOAP, MaskCollection
from swiftgalaxy.demo_data import generated_examples

soap = SOAP(generated_examples.soap, soap_index=0, extra_mask=None)
sg = SWIFTGalaxy(generated_examples.virtual_snapshot, soap)
mask = MaskCollection(
    gas=sg.gas.spherical_coordinates.r
    < cosmo_quantity(
        5, u.kpc, comoving=True, scale_factor=sg.metadata.scale_factor, scale_exponent=1
    )
)
sg.mask_particles(mask)
bound_mask = sg.get_bound_only_mask(relative_to_current=False)
assert sg.gas.group_nr_bound.size == sg.gas.coordinates.size // 3

Here the coordinates reflect the current masked state of the SWIFTGalaxy but the group_nr_bound only have the spatial mask.

@diegod-01
Copy link
Copy Markdown
Author

Had to refactor the code quite a lot. I tried using the combine_with method of the LazyMask class which required removing the references to the internal datasets _particle_dataset._group_nr_bound or _particle_dataset._particle_ids; good luck not breaking everything for anyone brave to try...

I ended up choosing this solution as it does not require a copy of the reader instance or refactoring the entire codebase. Should also pass the lint/ruff/flake8/numpydoc/sphinx-lint/mypy tests as far as I know. Can be ugly-looking, but I couldn't think of anything better.

Let me know!

@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 15, 2026

Codecov Report

❌ Patch coverage is 97.22222% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 99.95%. Comparing base (05655c4) to head (9128c14).

Files with missing lines Patch % Lines
swiftgalaxy/demo_data.py 92.30% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@             Coverage Diff             @@
##              main      #93      +/-   ##
===========================================
- Coverage   100.00%   99.95%   -0.05%     
===========================================
  Files            7        7              
  Lines         2319     2341      +22     
  Branches       261      269       +8     
===========================================
+ Hits          2319     2340      +21     
- Partials         0        1       +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.

@kyleaoman kyleaoman self-requested a review May 5, 2026 10:40
Copy link
Copy Markdown
Member

@kyleaoman kyleaoman left a comment

Choose a reason for hiding this comment

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

Made specific suggestions to resolve the test coverage. Once you push an addition to the docs I'm happy to merge.

Comment thread swiftgalaxy/demo_data.py
dtype=int,
),
][self._mask_index]
if load_masked:
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
if load_masked:
else:
raise ValueError("Unknown particle type.")
if load_masked:

The coverage analysis is complaining that there's an if/elif chain here where the case where none of the items is triggered is never encountered. This could technically be resolved by making the last elif into an else but that leads to weird behaviour if something not in ("gas", "dark_matter", "stars", "black_holes") is passed, so this seems safer. Means that something in the test suite has to trigger this error.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested such a test below, surprisingly difficult to trigger!

Comment thread tests/test_masking.py
assert got_mask.dtype == bool
assert got_mask.dtype == bool
assert got_mask.all()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
def test_invalid_particle_type(self, sg):
"""Check that an unknown particle type in masking is refused."""
sg.metadata.num_part[2] = 1 # claim that there's a "boundary" particle
with pytest.raises(ValueError, match="Unknown particle type."):
sg.halo_catalogue._generate_bound_only_mask(sg).boundary.mask

(Not sure I've got the line spacing right, might need to run ruff format.)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants