Skip to content
Merged
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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ ds = gpgi.load(

With CIC and TSC deposition, particles contribute to cells neighbouring the one
that contains them. For particles that live in the outermost layer of the
domain, this means some of their contribution is lost. This behaviour
domain, this means some of their contribution is lost. This behavior
corresponds to the default `'open'` boundary condition, but `gpgi` has builtin
support for more conservative boundary conditions.

Expand Down
27 changes: 19 additions & 8 deletions src/gpgi/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from __future__ import annotations

import enum
import math
import sys
import warnings
from abc import ABC, abstractmethod
Expand Down Expand Up @@ -229,22 +230,32 @@ def _validate_coordinates(self) -> None:
coord_dtype = self._get_safe_datatype(coord)
dt = coord_dtype.type
xmin, xmax = (dt(_) for _ in _AXES_LIMITS[axis])
if (cmin := dt(np.min(coord))) < xmin:
if (cmin := dt(np.min(coord))) < xmin or not math.isfinite(cmin):
if math.isfinite(xmin):
hint = f"minimal value allowed is {xmin}"
else:
assert xmin == -float("inf")
hint = "value must be finite"
raise ValueError(
f"Invalid coordinate data for axis {axis!r} {cmin} "
f"(minimal allowed value is {xmin})"
f"Invalid coordinate data for axis {axis!r} {cmin} ({hint})"
)
if (cmax := dt(np.max(coord))) > xmax:
if (cmax := dt(np.max(coord))) > xmax or not math.isfinite(cmax):
if math.isfinite(xmax):
hint = f"maximal value allowed is {xmax}"
else:
assert xmax == float("inf")
hint = "value must be finite"
raise ValueError(
f"Invalid coordinate data for axis {axis!r} {cmax} "
f"(maximal allowed value is {xmax})"
f"Invalid coordinate data for axis {axis!r} {cmax} ({hint})"
)

self.coordinates[axis] = coord.astype(coord_dtype, copy=False)

def _get_safe_datatype(self, reference: np.ndarray | None = None) -> np.dtype:
# int32 and int64 are fragile because they cannot represent "+/-inf",
# which we use as default box boundaries, e.g. for cartesian datasets
# which in gpgi 1.0 were used as default box boundaries, e.g. for
# cartesian datasets.
# This behavior is preserved for backward compatibility.
if reference is None:
reference = self.coordinates[self.axes[0]]
dt = reference.dtype
Expand Down Expand Up @@ -680,7 +691,7 @@ def deposit(

if self.grid.size == 1:
warnings.warn(
"Depositing on a single-cell grid is undefined behaviour",
"Depositing on a single-cell grid is undefined behavior",
stacklevel=2,
)
if self.particles.count == 0:
Expand Down
37 changes: 36 additions & 1 deletion tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,41 @@ def test_unsorted_cell_edges():
gpgi.load(geometry="cartesian", grid={"cell_edges": {"x": np.array([1, 0])}})


@pytest.mark.parametrize(
"side, out_of_bounds_value",
[
("min", -float("inf")),
("max", float("inf")),
],
)
def test_infinite_box_edges(side, out_of_bounds_value):
if side == "min":
xlim = np.array([-np.inf, 1.0])
elif side == "max":
xlim = np.array([-0.1, +np.inf])
with pytest.raises(
ValueError,
match=(
f"Invalid coordinate data for axis 'x' {out_of_bounds_value} "
rf"\(value must be finite\)"
),
):
gpgi.load(geometry="cartesian", grid={"cell_edges": {"x": xlim}})


def test_missing_grid():
with pytest.raises(
TypeError, match=r"load\(\) missing 1 required keyword-only argument: 'grid'"
):
gpgi.load(
geometry="cartesian",
particles={
"coordinates": {"x": np.arange(10, dtype="float64")},
"fields": {"mass": np.ones(10, dtype="float64")},
},
)


@pytest.mark.parametrize(
"geometry, cell_edges, coords, axis, side, limit",
[
Expand Down Expand Up @@ -237,7 +272,7 @@ def test_load_invalid_particles_coordinates(
ValueError,
match=(
f"Invalid coordinate data for axis {axis!r} {c} "
rf"\({side}imal allowed value is {limit}\)"
rf"\({side}imal value allowed is {limit}\)"
),
):
gpgi.load(
Expand Down
25 changes: 6 additions & 19 deletions tests/test_deposit.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,37 +47,24 @@ def sample_2D_dataset():
)


def test_single_cell_grid():
# TODO: drop support for this use case (infinite boundaries)
@pytest.mark.parametrize("method", ["ngp", "cic", "tsc"])
def test_single_cell_grid(method):
ds = gpgi.load(
geometry="cartesian",
grid={
"cell_edges": {
"x": np.array([-np.inf, np.inf]),
"x": np.array([-1.0, 1.0]),
},
},
particles={
"coordinates": {"x": np.arange(10, dtype="float64")},
"coordinates": {"x": 0.1 * np.arange(10, dtype="float64") - 0.5},
"fields": {"mass": np.ones(10, dtype="float64")},
},
)
with pytest.warns(
UserWarning, match="Depositing on a single-cell grid is undefined behaviour"
):
ds.deposit("mass", method="ngp")


def test_missing_grid():
with pytest.raises(
TypeError, match=r"load\(\) missing 1 required keyword-only argument: 'grid'"
UserWarning, match="Depositing on a single-cell grid is undefined behavior"
):
gpgi.load(
geometry="cartesian",
particles={
"coordinates": {"x": np.arange(10, dtype="float64")},
"fields": {"mass": np.ones(10, dtype="float64")},
},
)
ds.deposit("mass", method=method)


def test_missing_particles():
Expand Down