Skip to content

Commit

Permalink
add covering grid to timeseries object
Browse files Browse the repository at this point in the history
  • Loading branch information
chrishavlin committed Apr 3, 2024
1 parent 79e24f7 commit 5b7b239
Show file tree
Hide file tree
Showing 4 changed files with 109 additions and 47 deletions.
6 changes: 3 additions & 3 deletions docs/examples/ytnapari_scene_04_timeseries.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@
"\n",
"One difference between `yt-napari` and `yt` proper is that when sampling a time series, you first specify a selection object **independently** from a dataset object to define the extents and field of selection. That selection is then applied across all specified timesteps.\n",
"\n",
"The currently available selection objects are a `Slice` or 3D gridded `Region`. The arguments follow the same convention as a usual `yt` dataset selection object (i.e., `ds.slice`, `ds.region`) for specifying the geometric bounds of the selection with the additional constraint that you must specify a single field and the resolution you want to sample at:"
"The currently available selection objects are a 2D `Slice` or 3D gridded region, either a `Region` of a `CoveringGrid`. The arguments follow the same convention as a usual `yt` dataset selection object (i.e., `ds.slice`, `ds.region`, `ds.covering_grid`) for specifying the geometric bounds of the selection with the additional constraint that you must specify a single field and the resolution you want to sample at:"
]
},
{
Expand Down Expand Up @@ -238,7 +238,7 @@
"id": "edd2babf-5aae-4d2f-8079-96a68b594b22",
"metadata": {},
"source": [
"Once you create a `Slice` or `Region`, you can pass that to `add_to_viewer` and it will be used to sample each timestep specified. \n",
"Once you create a `Slice`, `Region` or `CoveringGrid`, you can pass that to `add_to_viewer` and it will be used to sample each timestep specified. \n",
"\n",
"## Slices through a timeseries\n",
"\n",
Expand Down Expand Up @@ -1131,7 +1131,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.9.16"
"version": "3.11.7"
}
},
"nbformat": 4,
Expand Down
15 changes: 10 additions & 5 deletions src/yt_napari/_model_ingestor.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,15 @@ def _get_covering_grid(
return frb, dims


def _get_region_frb(ds, LE, RE, res):
frb = ds.r[
LE[0] : RE[0] : complex(0, res[0]), # noqa: E203
LE[1] : RE[1] : complex(0, res[1]), # noqa: E203
LE[2] : RE[2] : complex(0, res[2]), # noqa: E203
]
return frb


class LayerDomain:
# container for domain info for a single layer
# left_edge, right_edge, resolution, n_d are all self explanatory.
Expand Down Expand Up @@ -471,11 +480,7 @@ def _load_3D_regions(

if isinstance(sel, Region):
res = sel.resolution
frb = ds.r[
LE[0] : RE[0] : complex(0, res[0]), # noqa: E203
LE[1] : RE[1] : complex(0, res[1]), # noqa: E203
LE[2] : RE[2] : complex(0, res[2]), # noqa: E203
]
frb = _get_region_frb(ds, LE, RE, res)
elif isinstance(sel, CoveringGrid):
frb, dims = _get_covering_grid(ds, LE, RE, sel.level, sel.num_ghost_zones)
res = dims
Expand Down
7 changes: 7 additions & 0 deletions src/yt_napari/_tests/test_timeseries.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,13 @@ def test_region(yt_ds_0):
assert np.all(np.log10(data4) == data)


def test_covering_grid(yt_ds_0):
cg = ts.CoveringGrid(_field)
data = cg.sample_ds(yt_ds_0)
# sampled at level 0 for full domain, so should get out the base dimensions
assert data.shape == tuple(yt_ds_0.domain_dimensions)


def test_slice(yt_ds_0):
sample_res = (20, 20)
slc = ts.Slice(_field, "x", resolution=sample_res)
Expand Down
128 changes: 89 additions & 39 deletions src/yt_napari/timeseries.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,26 +47,9 @@ def _validate_unit_tuple(val):
return None, None


class Region(_Selection):
class _RegionBase(_Selection, abc.ABC):
"""
A 3D rectangular selection through a domain.
Parameters
----------
field: (str, str)
a yt field present in all timeseries to load.
left_edge: unyt_array or (ndarray, str)
(optional) a 3-element unyt_array defining the left edge of the region,
defaults to the domain left_edge of each active timestep.
right_edge: unyt_array or (ndarray, str)
(optional) a 3-element unyt_array defining the right edge of the region,
defaults to the domain right_edge of each active timestep.
resolution: (int, int, int)
(optional) 3-element tuple defining the resolution to sample at. Default
is (400, 400, 400).
take_log: bool
(optional) If True, take the log10 of the sampled field. Defaults to the
default behavior for the field in the dataset.
Base class for 3D box-like regions
"""

nd = 3
Expand All @@ -76,13 +59,11 @@ def __init__(
field: Tuple[str, str],
left_edge: Optional[Union[unyt_array, Tuple[np.ndarray, str]]] = None,
right_edge: Optional[Union[unyt_array, Tuple[np.ndarray, str]]] = None,
resolution: Optional[Tuple[int, int, int]] = (400, 400, 400),
take_log: Optional[bool] = None,
):
super().__init__(field, take_log=take_log)
self.left_edge = left_edge
self.right_edge = right_edge
self.resolution = resolution
self._le, self._le_units = self._validate_unit_tuple(left_edge)
self._re, self._re_units = self._validate_unit_tuple(right_edge)

Expand All @@ -102,6 +83,60 @@ def _calc_aspect_ratio(self, LE, RE):
wid = RE - LE
self._aspect_ratio = wid / wid[0]

def _get_edges(self, ds):
if self.left_edge is None:
LE = ds.domain_left_edge
elif self._le is not None:
LE = ds.arr(self._le, self._le_units)
else:
LE = self.left_edge

if self.right_edge is None:
RE = ds.domain_right_edge
elif self._re is not None:
RE = ds.arr(self._re, self._re_units)
else:
RE = self.right_edge
return LE, RE


class Region(_RegionBase):
"""
A 3D rectangular selection through a domain.
Parameters
----------
field: (str, str)
a yt field present in all timeseries to load.
left_edge: unyt_array or (ndarray, str)
(optional) a 3-element unyt_array defining the left edge of the region,
defaults to the domain left_edge of each active timestep.
right_edge: unyt_array or (ndarray, str)
(optional) a 3-element unyt_array defining the right edge of the region,
defaults to the domain right_edge of each active timestep.
resolution: (int, int, int)
(optional) 3-element tuple defining the resolution to sample at. Default
is (400, 400, 400).
take_log: bool
(optional) If True, take the log10 of the sampled field. Defaults to the
default behavior for the field in the dataset.
"""

nd = 3

def __init__(
self,
field: Tuple[str, str],
left_edge: Optional[Union[unyt_array, Tuple[np.ndarray, str]]] = None,
right_edge: Optional[Union[unyt_array, Tuple[np.ndarray, str]]] = None,
resolution: Optional[Tuple[int, int, int]] = (400, 400, 400),
take_log: Optional[bool] = None,
):
super().__init__(
field, left_edge=left_edge, right_edge=right_edge, take_log=take_log
)
self.resolution = resolution

def sample_ds(self, ds):
"""
return a fixed resolution sample of a field in a yt dataset.
Expand All @@ -128,31 +163,46 @@ def sample_ds(self, ds):
This is equivalent to `ds.r[...,...,..][field]`, but is a useful
abstraction for applying the same selection to a series of datasets.
"""
if self.left_edge is None:
LE = ds.domain_left_edge
elif self._le is not None:
LE = ds.arr(self._le, self._le_units)
else:
LE = self.left_edge

if self.right_edge is None:
RE = ds.domain_right_edge
elif self._re is not None:
RE = ds.arr(self._re, self._re_units)
else:
RE = self.right_edge
LE, RE = self._get_edges(ds)

res = self.resolution
if self._aspect_ratio is None:
self._calc_aspect_ratio(LE, RE)

# create the fixed resolution buffer
frb = ds.r[
LE[0] : RE[0] : complex(0, res[0]), # noqa: E203
LE[1] : RE[1] : complex(0, res[1]), # noqa: E203
LE[2] : RE[2] : complex(0, res[2]), # noqa: E203
]
frb = _mi._get_region_frb(ds, LE, RE, res)

data = frb[self.field]
return self._finalize_array(ds, data)


class CoveringGrid(_RegionBase):

def __init__(
self,
field: Tuple[str, str],
left_edge: Optional[Union[unyt_array, Tuple[np.ndarray, str]]] = None,
right_edge: Optional[Union[unyt_array, Tuple[np.ndarray, str]]] = None,
level: Optional[int] = 0,
num_ghost_zones: Optional[int] = 0,
take_log: Optional[bool] = None,
):

super().__init__(
field, left_edge=left_edge, right_edge=right_edge, take_log=take_log
)
self.level = level
self.num_ghost_zones = num_ghost_zones

def sample_ds(self, ds):
LE, RE = self._get_edges(ds)

if self._aspect_ratio is None:
self._calc_aspect_ratio(LE, RE)

frb, _ = _mi._get_covering_grid(
ds, LE, RE, self.level, self.num_ghost_zones, test_dims=None
)
data = frb[self.field]
return self._finalize_array(ds, data)

Expand Down

0 comments on commit 5b7b239

Please sign in to comment.