diff --git a/docs/examples/ytnapari_scene_04_timeseries.ipynb b/docs/examples/ytnapari_scene_04_timeseries.ipynb index 5c03daa..5501708 100644 --- a/docs/examples/ytnapari_scene_04_timeseries.ipynb +++ b/docs/examples/ytnapari_scene_04_timeseries.ipynb @@ -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:" ] }, { @@ -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", @@ -1131,7 +1131,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.9.16" + "version": "3.11.7" } }, "nbformat": 4, diff --git a/src/yt_napari/_model_ingestor.py b/src/yt_napari/_model_ingestor.py index d3ca871..06d7ec5 100644 --- a/src/yt_napari/_model_ingestor.py +++ b/src/yt_napari/_model_ingestor.py @@ -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. @@ -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 diff --git a/src/yt_napari/_tests/test_timeseries.py b/src/yt_napari/_tests/test_timeseries.py index ab4b5e0..a00810a 100644 --- a/src/yt_napari/_tests/test_timeseries.py +++ b/src/yt_napari/_tests/test_timeseries.py @@ -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) diff --git a/src/yt_napari/timeseries.py b/src/yt_napari/timeseries.py index f4e7f80..1c8765b 100644 --- a/src/yt_napari/timeseries.py +++ b/src/yt_napari/timeseries.py @@ -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 @@ -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) @@ -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. @@ -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)