diff --git a/.github/workflows/lint_and_test.yml b/.github/workflows/lint_and_test.yml index 0dadaba..b5abd07 100644 --- a/.github/workflows/lint_and_test.yml +++ b/.github/workflows/lint_and_test.yml @@ -114,9 +114,8 @@ jobs: pip install -e swiftgalaxy/ - name: Test with pytest including coverage report run: | - cd swiftgalaxy - pytest --cov=./ --cov-report=xml + pytest --cov --cov-branch --cov-report=xml swiftgalaxy/tests - name: Upload coverage to Codecov - uses: codecov/codecov-action@v4 + uses: codecov/codecov-action@v5 env: - CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} \ No newline at end of file + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} diff --git a/codecov.yml b/codecov.yml index de84b28..394f544 100644 --- a/codecov.yml +++ b/codecov.yml @@ -5,6 +5,9 @@ coverage: target: 100% threshold: 10% ignore: - - "tests" - - "docs" - - "examples" \ No newline at end of file + - "^tests" + - "^docs" + - "^examples" + - "^SOAP" +fixes: + - "^swiftgalaxy/::" diff --git a/swiftgalaxy/halo_catalogues.py b/swiftgalaxy/halo_catalogues.py index 56d480a..f8dc134 100644 --- a/swiftgalaxy/halo_catalogues.py +++ b/swiftgalaxy/halo_catalogues.py @@ -420,7 +420,6 @@ def _load(self) -> None: Derived classes shoud put any non-trivial i/o operations needed at initialization here. Method will be called during ``__init__``. """ - pass @abstractmethod def _generate_spatial_mask(self, snapshot_filename: str) -> SWIFTMask: @@ -441,7 +440,6 @@ def _generate_spatial_mask(self, snapshot_filename: str) -> SWIFTMask: out : :class:`~swiftsimio.masks.SWIFTMask` The spatial mask to select particles in the region of interest. """ - pass @abstractmethod def _generate_bound_only_mask(self, sg: "SWIFTGalaxy") -> MaskCollection: @@ -467,7 +465,6 @@ def _generate_bound_only_mask(self, sg: "SWIFTGalaxy") -> MaskCollection: The mask object that selects bound particles from the spatially-masked set of particles. """ - pass @abstractmethod def _get_preload_fields(self, sg: "SWIFTGalaxy") -> Set[str]: @@ -493,7 +490,6 @@ def _get_preload_fields(self, sg: "SWIFTGalaxy") -> Set[str]: A set specifying the data that need to be read as strings, such as ``{"gas.particle_ids", ...}``. """ - pass @property @abstractmethod @@ -511,7 +507,6 @@ def centre(self) -> cosmo_array: out : :class:`~swiftsimio.objects.cosmo_array` The coordinate centre(s) of the object(s) of interest. """ - pass @property @abstractmethod @@ -529,7 +524,6 @@ def velocity_centre(self) -> cosmo_array: out : :class:`~swiftsimio.objects.cosmo_array` The velocity centre(s) of the object(s) of interest. """ - pass @property @abstractmethod @@ -546,7 +540,6 @@ def _region_centre(self) -> cosmo_array: out : :class:`~swiftsimio.objects.cosmo_array` The coordinates of the centres of the spatial mask regions. """ - pass @property @abstractmethod @@ -565,7 +558,6 @@ def _region_aperture(self) -> cosmo_array: The half-length of the bounding box to use to construct the spatial mask regions. """ - pass class SOAP(_HaloCatalogue): diff --git a/swiftgalaxy/reader.py b/swiftgalaxy/reader.py index d05b023..6b92e57 100644 --- a/swiftgalaxy/reader.py +++ b/swiftgalaxy/reader.py @@ -103,7 +103,7 @@ def _apply_translation(coords: cosmo_array, offset: cosmo_array) -> cosmo_array: offset = offset.to_comoving() elif hasattr(offset, "comoving") and not coords.comoving: offset = offset.to_physical() - elif not hasattr(offset, "comoving"): + else: # not hasattr(offset, "comoving") msg = ( "Translation assumed to be in comoving (not physical) coordinates." if coords.comoving @@ -848,9 +848,7 @@ def _mask_dataset(self, mask: slice) -> None: else: if self._swiftgalaxy._spatial_mask is None: # get a count of particles in the box - num_part = self._particle_dataset.metadata.num_part[ - particle_metadata.particle_type - ] + num_part = getattr(self.metadata, f"n_{particle_metadata.group_name}") else: # get a count of particles in the spatial mask region num_part = np.sum( @@ -1029,20 +1027,16 @@ def spherical_coordinates(self) -> _CoordinateHelper: a**0, scale_factor=r.cosmo_factor.scale_factor ), ) - if self.cylindrical_coordinates is not None: + if self._cylindrical_coordinates is not None: phi = self.cylindrical_coordinates.phi else: - phi = cosmo_array( + phi = ( np.arctan2( self.cartesian_coordinates.y, self.cartesian_coordinates.x - ), - units=unyt.rad, - comoving=r.comoving, - cosmo_factor=cosmo_factor( - a**0, scale_factor=r.cosmo_factor.scale_factor - ), - ) - phi[phi < 0] = phi[phi < 0] + 2 * np.pi * unyt.rad + ) + * unyt.rad + ) # arctan2 returns dimensionless + phi[phi < 0] += 2 * np.pi * np.ones_like(phi)[phi < 0] self._spherical_coordinates = dict(_r=r, _theta=theta, _phi=phi) return _CoordinateHelper( self._spherical_coordinates, @@ -1125,10 +1119,13 @@ def spherical_velocities(self) -> _CoordinateHelper: + _sin_t * _sin_p * self.cartesian_velocities.y - _cos_t * self.cartesian_velocities.z ) - v_p = ( - -_sin_p * self.cartesian_velocities.x - + _cos_p * self.cartesian_velocities.y - ) + if self._cylindrical_velocities is not None: + v_p = self.cylindrical_velocities.phi + else: + v_p = ( + -_sin_p * self.cartesian_velocities.x + + _cos_p * self.cartesian_velocities.y + ) self._spherical_velocities = dict(_v_r=v_r, _v_t=v_t, _v_p=v_p) return _CoordinateHelper( self._spherical_velocities, @@ -1195,19 +1192,13 @@ def cylindrical_coordinates(self) -> _CoordinateHelper: if self._spherical_coordinates is not None: phi = self.spherical_coordinates.phi else: - # np.where returns ndarray - phi = np.arctan2( - self.cartesian_coordinates.y, self.cartesian_coordinates.x - ).view(np.ndarray) - phi = np.where(phi < 0, phi + 2 * np.pi, phi) - phi = cosmo_array( - phi, - units=unyt.rad, - comoving=rho.comoving, - cosmo_factor=cosmo_factor( - a**0, scale_factor=rho.cosmo_factor.scale_factor - ), - ) + phi = ( + np.arctan2( + self.cartesian_coordinates.y, self.cartesian_coordinates.x + ) + * unyt.rad + ) # arctan2 returns dimensionless + phi[phi < 0] += 2 * np.pi * np.ones_like(phi)[phi < 0] z = self.cartesian_coordinates.z self._cylindrical_coordinates = dict(_rho=rho, _phi=phi, _z=z) return _CoordinateHelper( @@ -1531,7 +1522,7 @@ def __init__( self._velocity_like_transform = np.eye(4) if self.halo_catalogue is None: # in server mode we don't have a halo_catalogue yet - pass + self._spatial_mask = getattr(self, "_spatial_mask", None) elif self.halo_catalogue._user_spatial_offsets is not None: self._spatial_mask = self.halo_catalogue._get_user_spatial_mask( self.snapshot_filename @@ -2022,13 +2013,18 @@ def _transform(self, transform4: cosmo_array, boost: bool = False) -> None: if boost else self.transforms_like_coordinates ) + transform_units = ( + self.metadata.units.length / self.metadata.units.time + if boost + else self.metadata.units.length + ) for particle_name in self.metadata.present_group_names: dataset = getattr(self, particle_name)._particle_dataset for field_name in transformable: field_data = getattr(dataset, f"_{field_name}") if field_data is not None: field_data = _apply_4transform( - field_data, transform4.to_value(), transform4.units + field_data, transform4, transform_units ) setattr(dataset, f"_{field_name}", field_data) if boost: diff --git a/tests/test_coordinate_transformations.py b/tests/test_coordinate_transformations.py index b2af997..1679e04 100644 --- a/tests/test_coordinate_transformations.py +++ b/tests/test_coordinate_transformations.py @@ -4,8 +4,15 @@ from unyt.testing import assert_allclose_units from swiftsimio.objects import cosmo_array, cosmo_factor, a, cosmo_quantity from scipy.spatial.transform import Rotation -from toysnap import present_particle_types, toysnap_filename, ToyHF +from toysnap import ( + present_particle_types, + toysnap_filename, + ToyHF, + create_toysnap, + remove_toysnap, +) from swiftgalaxy import SWIFTGalaxy +from swiftgalaxy.reader import _apply_translation, _apply_4transform reltol = 1.01 # allow some wiggle room for floating point roundoff abstol_c = cosmo_quantity( @@ -362,6 +369,18 @@ def test_translate(self, sg, particle_name, coordinate_name, before_load): xyz = getattr(getattr(sg, particle_name), f"{coordinate_name}") assert_allclose_units(xyz_before + translation, xyz, rtol=1.0e-4, atol=abstol_c) + def test_translate_warn_comoving_missing(self, sg): + """ + If the translation does not have comoving information issue a warning. + """ + translation = u.unyt_array( + [1, 1, 1], + units=u.Mpc, + ) + msg = "Translation assumed to be in comoving" + with pytest.warns(RuntimeWarning, match=msg): + sg.translate(translation) + @pytest.mark.parametrize("velocity_name", ("velocities", "extra_velocities")) @pytest.mark.parametrize("particle_name", present_particle_types.values()) @pytest.mark.parametrize("before_load", (True, False)) @@ -424,6 +443,64 @@ def test_box_wrap(self, sg, particle_name, coordinate_name): xyz = getattr(getattr(sg, particle_name), f"{coordinate_name}") assert_allclose_units(xyz_before, xyz, rtol=1.0e-4, atol=abstol_c) + @pytest.mark.parametrize("coordinate_name", ("coordinates", "extra_coordinates")) + @pytest.mark.parametrize("particle_name", present_particle_types.values()) + @pytest.mark.parametrize("before_load", (True, False)) + def test_transform(self, sg, particle_name, coordinate_name, before_load): + """ + Check that affine transformation works. + """ + xyz_before = getattr(getattr(sg, particle_name), f"{coordinate_name}") + if before_load: + setattr( + getattr(sg, particle_name)._particle_dataset, + f"_{coordinate_name}", + None, + ) + translation = cosmo_array( + [1, 1, 1], + units=u.Mpc, + comoving=True, + cosmo_factor=cosmo_factor(a**1, scale_factor=1.0), + ) + transform = np.eye(4) + transform[:3, :3] = rot + transform[3, :3] = translation.to_comoving_value(u.Mpc) + sg._transform(transform) + xyz = getattr(getattr(sg, particle_name), f"{coordinate_name}") + assert_allclose_units( + xyz_before.dot(rot) + translation, xyz, rtol=1.0e-4, atol=abstol_c + ) + + @pytest.mark.parametrize("coordinate_name", ("velocities", "extra_velocities")) + @pytest.mark.parametrize("particle_name", present_particle_types.values()) + @pytest.mark.parametrize("before_load", (True, False)) + def test_transform_velocity(self, sg, particle_name, coordinate_name, before_load): + """ + Check that affine transformation works. + """ + xyz_before = getattr(getattr(sg, particle_name), f"{coordinate_name}") + if before_load: + setattr( + getattr(sg, particle_name)._particle_dataset, + f"_{coordinate_name}", + None, + ) + translation = cosmo_array( + [100, 100, 100], + units=u.km / u.s, + comoving=True, + cosmo_factor=cosmo_factor(a**0, scale_factor=1.0), + ) + transform = np.eye(4) + transform[:3, :3] = rot + transform[3, :3] = translation.to_comoving_value(u.km / u.s) + sg._transform(transform, boost=True) + xyz = getattr(getattr(sg, particle_name), f"{coordinate_name}") + assert_allclose_units( + xyz_before.dot(rot) + translation, xyz, rtol=1.0e-4, atol=abstol_v + ) + class TestSequentialTransformations: @pytest.mark.parametrize("before_load", (True, False)) @@ -533,6 +610,27 @@ def test_auto_recentering_with_copied_coordinate_frame(self, sg): toysnap_filename, ToyHF(), auto_recentre=True, coordinate_frame_from=sg ) + def test_invalid_coordinate_frame_from(self, sg): + """ + Check that we get an error if coordinate_frame_from has mismatched internal units. + """ + new_time_unit = u.s + assert sg.metadata.units.time != new_time_unit + sg.metadata.units.time = new_time_unit + try: + create_toysnap() + with pytest.raises( + ValueError, match="Internal units \\(length and time\\) of" + ): + SWIFTGalaxy( + toysnap_filename, + ToyHF(), + coordinate_frame_from=sg, + auto_recentre=False, + ) + finally: + remove_toysnap() + def test_copied_coordinate_transform(self, sg): """ Check that a SWIFTGalaxy initialised to copy the coordinate frame @@ -572,3 +670,115 @@ def test_copied_velocity_transform(self, sg): assert_allclose_units( sg.gas.velocities, sg2.gas.velocities, rtol=1.0e-4, atol=abstol_v ) + + +class TestApplyTranslation: + + @pytest.mark.parametrize("comoving", [True, False]) + def test_comoving_physical_conversion(self, comoving): + """ + The _apply_translation function should convert the offset to + match the coordinates. + """ + coords = cosmo_array( + [[1, 2, 3], [4, 5, 6]], + units=u.Mpc, + comoving=comoving, + cosmo_factor=cosmo_factor(a**1, scale_factor=1.0), + ) + offset = cosmo_array( + [1, 1, 1], + units=u.Mpc, + comoving=True, + cosmo_factor=cosmo_factor(a**1, scale_factor=1.0), + ) + result = _apply_translation(coords, offset) + assert result.comoving == comoving + + @pytest.mark.parametrize("comoving", [True, False]) + def test_warn_comoving_missing(self, comoving): + """ + If the offset does not have comoving information issue a warning. + """ + coords = cosmo_array( + [[1, 2, 3], [4, 5, 6]], + units=u.Mpc, + comoving=comoving, + cosmo_factor=cosmo_factor(a**1, scale_factor=1.0), + ) + offset = u.unyt_array( + [1, 1, 1], + units=u.Mpc, + ) + msg = ( + "Translation assumed to be in comoving" + if comoving + else "Translation assumed to be in physical" + ) + with pytest.warns(RuntimeWarning, match=msg): + with pytest.warns( + RuntimeWarning, match="Mixing arguments with and without" + ): + result = _apply_translation(coords, offset) + assert result.comoving == comoving + + +class TestApply4Transform: + + @pytest.mark.parametrize("comoving", [True, False]) + def test_comoving_physical_conversion(self, comoving): + """ + The _apply_4transform function should return comoving if input + was comoving, physical otherwise. + """ + coords = cosmo_array( + [[1, 2, 3], [4, 5, 6]], + units=u.Mpc, + comoving=comoving, + cosmo_factor=cosmo_factor(a**1, scale_factor=1.0), + ) + transform = np.eye(4) # identity 4transform + result = _apply_4transform(coords, transform, transform_units=u.Mpc) + assert result.comoving == comoving + + +class TestCoordinateProperties: + + def test_centre(self, sg): + """ + Check the centre attribute. + """ + new_centre = cosmo_array( + [1, 2, 3], + units=u.Mpc, + comoving=True, + cosmo_factor=cosmo_factor(a**1, scale_factor=1.0), + ) + sg.recentre(new_centre) + assert_allclose_units( + sg.halo_catalogue.centre + new_centre, + sg.centre, + ) + + def test_velocity_centre(self, sg): + """ + Check the velocity_centre attribute. + """ + new_centre = cosmo_array( + [100, 200, 300], + units=u.km / u.s, + comoving=True, + cosmo_factor=cosmo_factor(a**0, scale_factor=1.0), + ) + sg.recentre_velocity(new_centre) + assert_allclose_units( + sg.halo_catalogue.velocity_centre + new_centre, + sg.velocity_centre, + ) + + def test_rotation(self, sg): + """ + Check the rotation attribute. + """ + sg.rotate(Rotation.from_matrix(rot)) + assert np.allclose(sg.rotation.as_matrix(), rot) diff --git a/tests/test_copy.py b/tests/test_copy.py index e75b306..ac9179f 100644 --- a/tests/test_copy.py +++ b/tests/test_copy.py @@ -1,6 +1,9 @@ -from copy import deepcopy +import numpy as np +from copy import copy, deepcopy +import pytest import unyt as u from unyt.testing import assert_allclose_units +from swiftgalaxy.masks import MaskCollection abstol_m = 1e2 * u.solMass reltol_m = 1.0e-4 @@ -9,13 +12,34 @@ class TestCopySWIFTGalaxy: - def test_deepcopy_sg(self, sg): + def test_copy_sg(self, sg): """ - Test that datasets get copied on deep copy. + Test that dataset arrays don't get copied on shallow copy. """ # lazy load a dataset and a named column sg.gas.masses sg.gas.hydrogen_ionization_fractions.neutral + sg_copy = copy(sg) + # check private attribute to not trigger lazy loading + assert sg_copy.gas._particle_dataset._masses is None + assert ( + sg_copy.gas.hydrogen_ionization_fractions._named_column_dataset._neutral + is None + ) + + @pytest.mark.parametrize("derived_coords_initialized", [True, False]) + def test_deepcopy_sg(self, sg, derived_coords_initialized): + """ + Test that dataset arrays get copied on deep copy. + """ + # lazy load a dataset and a named column + sg.gas.masses + sg.gas.hydrogen_ionization_fractions.neutral + if derived_coords_initialized: + sg.gas.spherical_coordinates + sg.gas.spherical_velocities + sg.gas.cylindrical_coordinates + sg.gas.cylindrical_velocities sg_copy = deepcopy(sg) # check private attribute to not trigger lazy loading assert_allclose_units( @@ -30,3 +54,131 @@ def test_deepcopy_sg(self, sg): rtol=reltol_nd, atol=abstol_nd, ) + if derived_coords_initialized: + assert_allclose_units( + sg.gas.spherical_coordinates.r, + sg_copy.gas._spherical_coordinates["_r"], + ) + assert_allclose_units( + sg.gas.spherical_velocities.r, + sg_copy.gas._spherical_velocities["_v_r"], + ) + assert_allclose_units( + sg.gas.cylindrical_coordinates.rho, + sg_copy.gas._cylindrical_coordinates["_rho"], + ) + assert_allclose_units( + sg.gas.cylindrical_velocities.rho, + sg_copy.gas._cylindrical_velocities["_v_rho"], + ) + else: + assert sg_copy.gas._spherical_coordinates is None + assert sg_copy.gas._spherical_velocities is None + assert sg_copy.gas._cylindrical_coordinates is None + assert sg_copy.gas._cylindrical_velocities is None + + +class TestCopyDataset: + def test_copy_dataset(self, sg): + """ + Test that arrays don't get copied on shallow copy. + """ + # lazy load a dataset and a named column + sg.gas.masses + sg.gas.hydrogen_ionization_fractions.neutral + ds_copy = copy(sg.gas) + # check private attribute to not trigger lazy loading + assert ds_copy._particle_dataset._masses is None + assert ( + ds_copy.hydrogen_ionization_fractions._named_column_dataset._neutral is None + ) + + def test_deepcopy_dataset(self, sg): + """ + Test that arrays get copied on deep copy. + """ + # lazy load a dataset and a named column + sg.gas.masses + sg.gas.hydrogen_ionization_fractions.neutral + ds_copy = deepcopy(sg.gas) + # check private attribute to not trigger lazy loading + assert_allclose_units( + sg.gas.masses, + ds_copy._particle_dataset._masses, + rtol=reltol_m, + atol=abstol_m, + ) + assert_allclose_units( + sg.gas.hydrogen_ionization_fractions.neutral, + ds_copy.hydrogen_ionization_fractions._named_column_dataset._neutral, + rtol=reltol_nd, + atol=abstol_nd, + ) + + +class TestCopyNamedColumns: + def test_copy_namedcolumn(self, sg): + """ + Test that columns don't get copied on shallow copy. + """ + # lazy load a named column + sg.gas.hydrogen_ionization_fractions.neutral + nc_copy = copy(sg.gas.hydrogen_ionization_fractions) + # check private attribute to not trigger lazy loading + assert nc_copy._named_column_dataset._neutral is None + + def test_deepcopy_namedcolumn(self, sg): + """ + Test that columns get copied on deep copy. + """ + # lazy load a named column + sg.gas.hydrogen_ionization_fractions.neutral + nc_copy = deepcopy(sg.gas.hydrogen_ionization_fractions) + # check private attribute to not trigger lazy loading + assert_allclose_units( + sg.gas.hydrogen_ionization_fractions.neutral, + nc_copy._named_column_dataset._neutral, + rtol=reltol_nd, + atol=abstol_nd, + ) + + +class TestCopyMaskCollection: + def test_copy_mask_collection(self): + """ + Test that masks get copied. + """ + mc = MaskCollection( + gas=np.ones(100, dtype=bool), + dark_matter=np.s_[:20], + stars=None, + black_holes=np.arange(3), + ) + mc_copy = copy(mc) + assert set(mc_copy.__dict__.keys()) == set(mc.__dict__.keys()) + for k in ("gas", "dark_matter", "stars", "black_holes"): + comparison = getattr(mc, k) == getattr(mc_copy, k) + if type(comparison) is bool: + assert comparison + else: + assert all(comparison) + + def test_deepcopy_mask_collection(self): + """ + Test that masks get copied along with values. Since the object isn't + really "deep", shallow copy and deepcopy have the same expectation. + """ + mc = MaskCollection( + gas=np.ones(100, dtype=bool), + dark_matter=np.s_[:20], + stars=None, + black_holes=np.arange(3), + ) + mc_copy = deepcopy(mc) + assert set(mc_copy.__dict__.keys()) == set(mc.__dict__.keys()) + for k in ("gas", "dark_matter", "stars", "black_holes"): + comparison = getattr(mc, k) == getattr(mc_copy, k) + if type(comparison) is bool: + assert comparison + else: + assert all(comparison) diff --git a/tests/test_creation.py b/tests/test_creation.py index b9d3b88..24928bc 100644 --- a/tests/test_creation.py +++ b/tests/test_creation.py @@ -1,3 +1,12 @@ +""" +Tests checking that we can create objects, if these fail something +fundamental has gone wrong. +""" + +from swiftgalaxy import SWIFTGalaxy +from toysnap import create_toysnap, remove_toysnap, toysnap_filename, n_g_1 + + class TestSWIFTGalaxyCreation: def test_sg_creation(self, sg): """ @@ -66,6 +75,41 @@ def test_tab_completion(self, sg): assert prop in dir(sg.gas.hydrogen_ionization_fractions) # and check something that we inherited: assert "generate_empty_properties" in dir(sg.gas) + # finally, check that we didn't lazy-load everything! + assert sg.gas._particle_dataset._coordinates is None + + def test_mask_preloaded_namedcolumn(self): + """ + If namedcolumn data was loaded during evaluation of a mask, it needs to be masked + during initialization. + """ + from toysnap import ToyHF + + def load_namedcolumn(method): + + def wrapper(self, sg): + sg.gas.hydrogen_ionization_fractions.neutral + return method(self, sg) + + return wrapper + + # decorate the mask evaluation to load an (unused) namedcolumn + ToyHF._generate_bound_only_mask = load_namedcolumn( + ToyHF._generate_bound_only_mask + ) + + try: + create_toysnap() + sg = SWIFTGalaxy(toysnap_filename, ToyHF()) + # confirm that we loaded a namedcolumn during initialization: + assert ( + sg.gas.hydrogen_ionization_fractions._internal_dataset._neutral + is not None + ) + # confirm that it got masked: + assert sg.gas.hydrogen_ionization_fractions.neutral.size == n_g_1 + finally: + remove_toysnap() class TestSWIFTGalaxiesCreation: @@ -95,3 +139,15 @@ def test_sgs_caesar_creation(self, sgs_caesar): def test_sgs_sa_creation(self, sgs_sa): pass # fixture created SWIFTGalaxy with Standalone interface + + +class TestDeletion: + + def test_dataset_deleter(self, sg): + """ + Check that we can delete a dataset's array. + """ + sg.gas.coordinates # lazy-load some data + assert sg.gas._internal_dataset._coordinates is not None + del sg.gas.coordinates + assert sg.gas._internal_dataset._coordinates is None diff --git a/tests/test_derived_coordinates.py b/tests/test_derived_coordinates.py index 31b920d..bac88b1 100644 --- a/tests/test_derived_coordinates.py +++ b/tests/test_derived_coordinates.py @@ -206,6 +206,17 @@ def test_spherical_velocity_phi(self, sg, particle_name, alias): spherical_v_phi, v_phi_from_cartesian, rtol=1.0e-4, atol=abstol_v ) + @pytest.mark.parametrize("ctype", ["coordinates", "velocities"]) + def test_copy_from_cylindrical(self, sg, ctype): + """ + Check that copying the azimuth from cylindrical if already evaluated works. + """ + getattr(sg.gas, f"cylindrical_{ctype}").phi # trigger evaluation + assert ( + getattr(sg.gas, f"spherical_{ctype}").phi + is getattr(sg.gas, f"cylindrical_{ctype}").phi + ) + class TestCylindricalCoordinates: @pytest.mark.parametrize("particle_name", present_particle_types.values()) @@ -329,6 +340,17 @@ def test_cylindrical_velocity_z(self, sg, particle_name, alias): cylindrical_v_z, v_z_from_cartesian, rtol=1.0e-4, atol=abstol_v ) + @pytest.mark.parametrize("ctype", ["coordinates", "velocities"]) + def test_copy_from_spherical(self, sg, ctype): + """ + Check that copying the azimuth from spherical if already evaluated works. + """ + getattr(sg.gas, f"spherical_{ctype}").phi # trigger evaluation + assert ( + getattr(sg.gas, f"cylindrical_{ctype}").phi + is getattr(sg.gas, f"spherical_{ctype}").phi + ) + class TestInteractionWithCoordinateTransformations: @pytest.mark.parametrize("coordinate_type", ("coordinates", "velocities")) diff --git a/tests/test_masking.py b/tests/test_masking.py index 4570dfc..3b492f7 100644 --- a/tests/test_masking.py +++ b/tests/test_masking.py @@ -1,8 +1,21 @@ +""" +Tests for applying masks to swiftgalaxy, datasets and named columns. +""" + import pytest import numpy as np from unyt.testing import assert_allclose_units from toysnap import present_particle_types -from swiftgalaxy import MaskCollection +from swiftgalaxy import SWIFTGalaxy, MaskCollection +from toysnap import ( + create_toysnap, + remove_toysnap, + toysnap_filename, + n_g_all, + n_dm_all, + n_s_all, + n_bh_all, +) abstol_nd = 1.0e-4 reltol_nd = 1.0e-4 @@ -72,6 +85,41 @@ def test_namedcolumn_masked(self, sg, before_load): neutral_before[mask], neutral, rtol=reltol_nd, atol=abstol_nd ) + def test_mask_without_spatial_mask(self): + """ + Check that if we have no masks we read everything in the box (and warn about it). + Then that we can still apply an extra mask, and a second one (there's specific + logic for applying two consecutively). + """ + try: + create_toysnap() + sg = SWIFTGalaxy( + toysnap_filename, + None, # no halo_catalogue is easiest way to get no mask + transforms_like_coordinates={"coordinates", "extra_coordinates"}, + transforms_like_velocities={"velocities", "extra_velocities"}, + ) + # check that extra mask is blank for all particle types: + assert sg._extra_mask.gas is None + assert sg._extra_mask.dark_matter is None + assert sg._extra_mask.stars is None + assert sg._extra_mask.black_holes is None + # check that cell mask is blank for all particle types: + assert sg._spatial_mask is None + # check that we read all the particles: + assert sg.gas.masses.size == n_g_all + assert sg.dark_matter.masses.size == n_dm_all + assert sg.stars.masses.size == n_s_all + assert sg.black_holes.masses.size == n_bh_all + # now apply an extra mask + sg.mask_particles(MaskCollection(gas=np.s_[:1000])) + assert sg.gas.masses.size == 1000 + # and the second consecutive one + sg.mask_particles(MaskCollection(gas=np.s_[:100])) + assert sg.gas.masses.size == 100 + finally: + remove_toysnap() + class TestMaskingParticleDatasets: @pytest.mark.parametrize("particle_name", present_particle_types.values()) diff --git a/tests/test_str.py b/tests/test_str.py new file mode 100644 index 0000000..108c299 --- /dev/null +++ b/tests/test_str.py @@ -0,0 +1,31 @@ +""" +Tests of string representations of objects. +""" + + +class TestStr: + + def test_coordinate_helper_str(self, sg): + """ + Check that we get a sensible string representation of a coordinate helper. + """ + string = str(sg.gas.spherical_coordinates) + assert "Available coordinates:" in string + assert "radius" in string + assert "azimuth" in string + assert repr(sg.gas.spherical_coordinates) == string + + def test_namedcolumn_fullname(self, sg): + """ + Check that the _fullname of a namedcolumns matches the dataset + namedcolumns + name. + """ + assert ( + sg.gas.hydrogen_ionization_fractions._fullname + == "gas.hydrogen_ionization_fractions" + ) + + def test_sg_string(self, sg): + string = str(sg) + assert "SWIFTGalaxy at" in string + assert repr(sg) == string diff --git a/tests/toysnap.py b/tests/toysnap.py index f7d2606..27bf10e 100644 --- a/tests/toysnap.py +++ b/tests/toysnap.py @@ -43,8 +43,10 @@ n_dm_b = n_dm_all - n_dm_1 - n_dm_2 n_s_1 = 5000 n_s_2 = 5000 +n_s_all = n_s_1 + n_s_2 n_bh_1 = 1 n_bh_2 = 1 +n_bh_all = n_bh_1 + n_bh_2 m_g = 1e3 * u.msun T_g = 1e4 * u.K m_dm = 1e4 * u.msun