Skip to content

Commit

Permalink
Merge pull request #256 from neutrinoceros/api/mv_types.py
Browse files Browse the repository at this point in the history
API: clearly define public/private APIs
  • Loading branch information
neutrinoceros authored Sep 2, 2024
2 parents 008b4b8 + 0e7c6f3 commit 22d98ee
Show file tree
Hide file tree
Showing 7 changed files with 179 additions and 125 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ More refined methods are also available.

*new in gpgi 0.12.0*
User-defined alternative methods may be provided to `Dataset.deposit` as `method=my_func`.
Their signature need to be compatible with `gpgi.types.DepositionMethodT`.
Their signature need to be compatible with `gpgi.typing.DepositionMethodT` or `gpgi.typing.DepositionMethodWithMetadataT` .

### Supported geometries
| geometry name | axes order |
Expand Down
73 changes: 10 additions & 63 deletions src/gpgi/__init__.py
Original file line number Diff line number Diff line change
@@ -1,69 +1,16 @@
"""gpgi: Fast particle deposition at post-processing time."""

from importlib.util import find_spec
from typing import Any, Literal, cast

from ._typing import FieldMap, GridDict, ParticleSetDict
from .types import Dataset, Geometry, Grid, ParticleSet
from ._data_types import Dataset, Geometry, Grid, ParticleSet
from ._load import load

_IS_PY_LIB = find_spec("gpgi._lib").origin.endswith(".py") # type: ignore [union-attr]


def load(
*,
geometry: Literal["cartesian", "polar", "cylindrical", "spherical", "equatorial"],
grid: GridDict,
particles: ParticleSetDict | None = None,
metadata: dict[str, Any] | None = None,
) -> Dataset:
r"""
Load a Dataset.
Parameters
----------
geometry: Literal["cartesian", "polar", "cylindrical", "spherical", "equatorial"]
This flag is used for validation of axis names, order and domain limits.
grid: dict[str, FieldMap]
A dictionary representing the grid coordinates as 1D arrays of cell left edges,
and on-grid fields as ND arrays (fields are assumed to be defined on cell
centers)
__all__ = [
"load",
"Dataset",
"Geometry",
"Grid",
"ParticleSet",
]

particles: dict[str, FieldMap] (optional)
A dictionary representing particle coordinates and associated fields as 1D
arrays
metadata: dict[str, Any] (optional)
A dictionary representing arbitrary additional data, that will be attached to
the returned Dataset as an attribute (namely, ds.metadata). This special
attribute is accessible from boundary condition methods as the argument of the
same name.
"""
try:
_geometry = Geometry(geometry)
except ValueError:
raise ValueError(
f"unknown geometry {geometry!r}, expected any of {tuple(_.value for _ in Geometry)}"
) from None

if "cell_edges" not in grid:
raise ValueError("grid dictionary missing required key 'cell_edges'")
_grid = Grid(
_geometry,
cell_edges=cast(FieldMap, grid["cell_edges"]),
fields=grid.get("fields"),
)

_particles: ParticleSet | None = None
if particles is not None:
if "coordinates" not in particles:
raise ValueError("particles dictionary missing required key 'coordinates'")
_particles = ParticleSet(
_geometry,
coordinates=cast(FieldMap, particles["coordinates"]),
fields=particles.get("fields"),
)

return Dataset(
geometry=_geometry, grid=_grid, particles=_particles, metadata=metadata
)
_IS_PY_LIB = find_spec("gpgi._lib").origin.endswith(".py") # type: ignore [union-attr]
2 changes: 1 addition & 1 deletion src/gpgi/_boundaries.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from typing import TYPE_CHECKING, Any, Literal, cast

if TYPE_CHECKING:
from ._typing import RealArray
from gpgi._typing import RealArray

BoundaryRecipeT = Callable[
[
Expand Down
102 changes: 43 additions & 59 deletions src/gpgi/types.py → src/gpgi/_data_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,12 @@
from textwrap import indent
from threading import Lock
from time import monotonic_ns
from typing import TYPE_CHECKING, Any, Literal, Protocol, Self, assert_never, cast
from typing import TYPE_CHECKING, Literal, assert_never, cast

import numpy as np

from ._boundaries import BoundaryRegistry
from ._lib import (
from gpgi._boundaries import BoundaryRegistry
from gpgi._lib import (
_deposit_cic_1D,
_deposit_cic_2D,
_deposit_cic_3D,
Expand All @@ -31,14 +31,18 @@
_deposit_tsc_3D,
_index_particles,
)
from gpgi._typing import FieldMap, Name
from gpgi.typing import DepositionMethodT, DepositionMethodWithMetadataT

if sys.version_info >= (3, 13):
LockType = Lock
else:
from _thread import LockType

if TYPE_CHECKING:
from ._typing import FieldMap, HCIArray, Name, RealArray
from typing import Any, Self

from gpgi._typing import FieldMap, HCIArray, Name, RealArray

BoundarySpec = tuple[tuple[str, str, str], ...]

Expand All @@ -63,40 +67,6 @@ class DepositionMethod(enum.Enum):
}


class DepositionMethodT(Protocol):
def __call__( # noqa D102
self,
cell_edges_x1: RealArray,
cell_edges_x2: RealArray,
cell_edges_x3: RealArray,
particles_x1: RealArray,
particles_x2: RealArray,
particles_x3: RealArray,
field: RealArray,
weight_field: RealArray,
hci: HCIArray,
out: RealArray,
) -> None: ...


class DepositionMethodWithMetadataT(Protocol):
def __call__( # noqa D102
self,
cell_edges_x1: RealArray,
cell_edges_x2: RealArray,
cell_edges_x3: RealArray,
particles_x1: RealArray,
particles_x2: RealArray,
particles_x3: RealArray,
field: RealArray,
weight_field: RealArray,
hci: HCIArray,
out: RealArray,
*,
metadata: dict[str, Any],
) -> None: ...


_BUILTIN_METHODS: dict[DepositionMethod, list[DepositionMethodT]] = {
DepositionMethod.NEAREST_GRID_POINT: [
_deposit_ngp_1D,
Expand Down Expand Up @@ -271,6 +241,7 @@ def _get_safe_datatype(self, reference: np.ndarray | None = None) -> np.dtype:
class Grid(_CoordinateValidatorMixin):
def __init__(
self,
*,
geometry: Geometry,
cell_edges: FieldMap,
fields: FieldMap | None = None,
Expand All @@ -280,13 +251,13 @@ def __init__(
Parameters
----------
geometry: gpgi.types.Geometry
geometry (keyword-only): gpgi.Geometry
cell_edges: gpgi.types.FieldMap
cell_edges (keyword-only): gpgi.typing.FieldMap
Left cell edges in each direction as 1D arrays, including the right edge
of the rightmost cell.
fields: gpgi.types.FieldMap (optional)
fields (keyword-only, optional): gpgi.typing.FieldMap
"""
self.geometry = geometry
self.coordinates = cell_edges
Expand Down Expand Up @@ -381,10 +352,23 @@ def cell_volumes(self) -> RealArray:
class ParticleSet(_CoordinateValidatorMixin):
def __init__(
self,
*,
geometry: Geometry,
coordinates: FieldMap,
fields: FieldMap | None = None,
) -> None:
r"""
Define a ParticleSet from point positions and data fields.
Parameters
----------
geometry (keyword-only): gpgi.Geometry
coordinates (keyword-only): gpgi.typing.FieldMap
Particle positions in each direction as 1D arrays.
fields (keyword-only, optional): gpgi.typing.FieldMap
"""
self.geometry = geometry
self.coordinates = coordinates

Expand Down Expand Up @@ -437,18 +421,18 @@ def __init__(
Parameters
----------
geometry: gpgi.types.Geometry
geometry (keyword-only): gpgi.Geometry
An enum member that represents the geometry.
grid: gpgi.types.Grid (optional)
grid (keyword-only): gpgi.Grid
particles: gpgi.types.ParticleSet (optional)
particles(keyword-only, optional): gpgi.ParticleSet
metadata: dict[str, Any] (optional)
A dictionary representing arbitrary additional data, that will be attached to
the returned Dataset as an attribute (namely, ds.metadata). This special
attribute is accessible from boundary condition methods as the argument of the
same name.
metadata (keyword-only, optional): dict[str, Any]
A dictionary representing arbitrary additional data, that will be attached
to the returned Dataset as an attribute (namely, ds.metadata). This special
attribute is accessible from boundary condition methods as the argument of
the same name.
.. versionadded: 0.4.0
"""
Expand Down Expand Up @@ -623,22 +607,22 @@ def is_sorted(self, *, axes: tuple[int, ...] | None = None) -> bool:
Parameters
----------
axes: tuple[int, ...]
axes (keyword-only, optional): tuple[int, ...]
specify in which order axes should be used for sorting.
"""
sort_key = self._get_sort_key(axes or self._default_sort_axes)
hci = self.host_cell_index
return bool(np.all(hci == hci[sort_key]))

def sorted(self, axes: tuple[int, ...] | None = None) -> Self:
def sorted(self, *, axes: tuple[int, ...] | None = None) -> Self:
r"""
Return a copy of this dataset with particles sorted by host cell index.
.. versionadded: 0.14.0
Parameters
----------
axes: tuple[int, ...]
axes (keyword-only, optional): tuple[int, ...]
specify in which order axes should be used for sorting.
"""
sort_key = self._get_sort_key(axes or self._default_sort_axes)
Expand All @@ -647,7 +631,7 @@ def sorted(self, axes: tuple[int, ...] | None = None) -> Self:
geometry=self.geometry,
grid=self.grid,
particles=ParticleSet(
self.geometry,
geometry=self.geometry,
coordinates={
name: arr[sort_key]
for name, arr in deepcopy(self.particles.coordinates).items()
Expand Down Expand Up @@ -696,16 +680,16 @@ def deposit(
.. versionchanged:: 0.12.0
Added support for user-defined functions.
verbose (keyword only): bool (default False)
if True, print execution time for hot loops (indexing and deposition)
verbose (keyword only, optional): bool (default False)
if True, print execution time for hot loops (indexing and deposition)
return_ghost_padded_array (keyword only): bool (default False)
return_ghost_padded_array (keyword only, optional): bool (default False)
if True, return the complete deposition array, including one extra
cell layer per direction and per side. This option is meant as a
debugging tool for methods that leak some particle data outside the
active domain (cic and tsc).
weight_field (keyword only): str
weight_field (keyword only, optional): str
label of another field to use as weights. Let u be the field to
deposit and w be the weight field. Let u' and w' be their equivalent
on-grid descriptions. u' is obtained as
Expand All @@ -718,7 +702,7 @@ def deposit(
.. versionadded: 0.7.0
boundaries and weight_field_boundaries (keyword only): dict
boundaries and weight_field_boundaries (keyword only, optional): dict
Maps from axis names (str) to boundary recipe keys (str, str)
representing left/right boundaries. By default all axes will use
'open' boundaries on both sides. Specifying boundaries for all axes
Expand All @@ -732,7 +716,7 @@ def deposit(
.. versionadded: 0.5.0
lock (keyword only): 'per-instance' (default), None, or threading.Lock
lock (keyword only, optional): 'per-instance' (default), None, or threading.Lock
Fine tune performance for multi-threaded applications: define a
locking strategy around the deposition hotloop.
- 'per-instance': allow multiple Dataset instances to run deposition
Expand Down
71 changes: 71 additions & 0 deletions src/gpgi/_load.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
from __future__ import annotations

from typing import TYPE_CHECKING, cast

from gpgi._data_types import Dataset, Geometry, Grid, ParticleSet
from gpgi._typing import FieldMap

if TYPE_CHECKING:
from typing import Any, Literal

from gpgi._typing import GridDict, ParticleSetDict


def load(
*,
geometry: Literal["cartesian", "polar", "cylindrical", "spherical", "equatorial"],
grid: GridDict,
particles: ParticleSetDict | None = None,
metadata: dict[str, Any] | None = None,
) -> Dataset:
r"""
Load a Dataset.
Parameters
----------
geometry: Literal["cartesian", "polar", "cylindrical", "spherical", "equatorial"]
This flag is used for validation of axis names, order and domain limits.
grid: dict[str, FieldMap]
A dictionary representing the grid coordinates as 1D arrays of cell left edges,
and on-grid fields as ND arrays (fields are assumed to be defined on cell
centers)
particles: dict[str, FieldMap] (optional)
A dictionary representing particle coordinates and associated fields as 1D
arrays
metadata: dict[str, Any] (optional)
A dictionary representing arbitrary additional data, that will be attached to
the returned Dataset as an attribute (namely, ds.metadata). This special
attribute is accessible from boundary condition methods as the argument of the
same name.
"""
try:
_geometry = Geometry(geometry)
except ValueError:
raise ValueError(
f"unknown geometry {geometry!r}, expected any of {tuple(_.value for _ in Geometry)}"
) from None

if "cell_edges" not in grid:
raise ValueError("grid dictionary missing required key 'cell_edges'")
_grid = Grid(
geometry=_geometry,
cell_edges=cast(FieldMap, grid["cell_edges"]),
fields=grid.get("fields"),
)

_particles: ParticleSet | None = None
if particles is not None:
if "coordinates" not in particles:
raise ValueError("particles dictionary missing required key 'coordinates'")
_particles = ParticleSet(
geometry=_geometry,
coordinates=cast(FieldMap, particles["coordinates"]),
fields=particles.get("fields"),
)

return Dataset(
geometry=_geometry, grid=_grid, particles=_particles, metadata=metadata
)
Loading

0 comments on commit 22d98ee

Please sign in to comment.