Skip to content

Commit 284be6a

Browse files
authored
Merge pull request #5 from entity-toolkit/0.6.0rc
Support for plotting with ghost cells and multidomain
2 parents 414b56a + 43afb02 commit 284be6a

File tree

12 files changed

+400
-107
lines changed

12 files changed

+400
-107
lines changed

README.md

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ data.particles # < dict[int : xr.Dataset]
2727
data.spectra # < xr.Dataset
2828
```
2929

30+
> If using Jupyter notebook, you can quickly preview the loaded metadata by simply running a cell with just `data` in it (or in regular python, by doing `print(data)`).
31+
3032
#### Examples
3133

3234
Plot a field (in cartesian space) at a specific time (or output step):
@@ -96,7 +98,42 @@ def plot(t, data):
9698
data.makeMovie(plot)
9799
```
98100

99-
> If using Jupyter notebook, you can quickly preview the loaded metadata by simply running a cell with just `data` in it (or in regular python, by doing `print(data)`).
101+
You may also access the movie-making functionality directly in case you want to use it for other things:
102+
103+
```python
104+
import nt2.export as nt2e
105+
106+
def plot(t):
107+
...
108+
109+
# this will be the array of `t`-s passed to `plot`
110+
# |
111+
# V
112+
nt2e.makeFrames(plot, np.arange(100), "myAnim")
113+
nt2e.makeMovie(
114+
input="myAnim/", output="myAnim.mp4", number=5, overwrite=True
115+
)
116+
117+
# or combined together
118+
nt2e.makeFramesAndMovie(
119+
name="myAnim", plot=plot, times=np.arange(100)
120+
)
121+
```
122+
123+
#### Plots for debugging
124+
125+
If the simulation also outputs the ghost cells, `nt2py` will interpret the fields differently, and instead of reading the physical coordinates, will build the coordinates based on the number of cells (including ghost cells). In particular, instead of, e.g., `data.fields.x` it will contain `data.fields.i1`. The data will also contain information about the meshblock decomposition. For instance, if you have `Nx` meshblocks in the `x` direction, each having `nx` cells, the coordinates `data.fields.i1` will go from `0` to `nx * NX + 2 * NGHOSTS * Nx`.
126+
127+
You can overplot both the coordinate grid as well as the active zones of the meshblocks using the following:
128+
129+
```python
130+
ax = plt.gca()
131+
data.fields.Ex.isel(t=ti).plot(ax=ax)
132+
data.plotGrid(ax=ax)
133+
data.plotDomains(ax=ax)
134+
```
135+
136+
> Keep in mind, that by default `Entity` converts all quantities to tetrad basis (or contravariant in GR) and interpolates to cell centers before outputting (except for the ghost cells). So when doing plots for debugging, make sure to also set `as_is = true` (together with `ghosts = true`) in the `[output.debug]` section of the `toml` input file. This will ensure the fields are being output as is, with no conversion or interpolation. This does not apply to particle moments, which are never stored in the code and are computed only during the output.
100137
101138
### Dashboard
102139

@@ -119,4 +156,7 @@ This will output the port where the dashboard server is running, e.g., `Dashboar
119156

120157
- [ ] Unit tests
121158
- [ ] Plugins for other simulation data formats
159+
- [ ] Support for multiple runs
160+
- [ ] Interactive regime (`hvplot`, `bokeh`, `panel`)
161+
- [x] Ghost cells support
122162
- [x] Usage examples

dist/nt2py-0.6.0-py3-none-any.whl

27.9 KB
Binary file not shown.

dist/nt2py-0.6.0.tar.gz

23.9 KB
Binary file not shown.

nt2/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
__version__ = "0.5.3"
1+
__version__ = "0.6.0"
22

33
from nt2.data import Data as Data
44
from nt2.dashboard import Dashboard as Dashboard

nt2/containers/container.py

Lines changed: 40 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,6 @@ class Container:
2828
2929
Kwargs
3030
------
31-
single_file : bool, optional
32-
Whether the data is stored in a single file. Default is False.
33-
3431
pickle : bool, optional
3532
Whether to use pickle for reading the data. Default is True.
3633
@@ -67,17 +64,15 @@ class Container:
6764
6865
"""
6966

70-
def __init__(
71-
self, path, single_file=False, pickle=True, greek=False, dask_props={}
72-
):
67+
def __init__(self, path, pickle=True, greek=False, dask_props={}):
7368
super(Container, self).__init__()
7469

70+
self.path = path
7571
self.configs: dict[str, Any] = {
76-
"single_file": single_file,
72+
"single_file": self.path.endswith(".h5"),
7773
"use_pickle": pickle,
7874
"use_greek": greek,
7975
}
80-
self.path = path
8176
self.metadata = {}
8277
self.mesh = None
8378
if self.configs["single_file"]:
@@ -95,6 +90,8 @@ def __init__(
9590

9691
self.attrs = _read_attribs_SingleFile(self.master_file)
9792

93+
assert self.master_file is not None, "Master file not found"
94+
9895
self.configs["ngh"] = int(self.master_file.attrs.get("NGhosts", 0))
9996
self.configs["layout"] = (
10097
"right" if self.master_file.attrs.get("LayoutRight", 1) == 1 else "left"
@@ -106,16 +103,31 @@ def __init__(
106103
if self.configs["coordinates"] == "qsph":
107104
self.configs["coordinates"] = "sph"
108105

106+
if self.isDebug():
107+
self.configs["coordinates"] = "cart"
108+
109109
if not self.configs["single_file"]:
110110
self.master_file.close()
111111
self.master_file = None
112112

113+
def isDebug(self):
114+
return self.configs["ngh"] > 0
115+
113116
def __del__(self):
114117
if self.master_file is not None:
115118
self.master_file.close()
116119

117120
def plotGrid(self, ax, **kwargs):
118-
from matplotlib import patches
121+
try:
122+
assert self.mesh is not None, "Mesh not found"
123+
except AttributeError:
124+
raise AttributeError("Mesh not found")
125+
except AssertionError:
126+
raise AssertionError("Mesh not found")
127+
128+
assert len(self.mesh["xc"]) == 2, "Data must be 2D for plotGrid to work"
129+
130+
from matplotlib import patches as mpatches
119131

120132
xlim, ylim = ax.get_xlim(), ax.get_ylim()
121133
options = {
@@ -125,15 +137,23 @@ def plotGrid(self, ax, **kwargs):
125137
}
126138
options.update(kwargs)
127139

140+
x1_emin, x2_emin = list(self.mesh["xe_min"].keys())
141+
x1_emax, x2_emax = list(self.mesh["xe_max"].keys())
142+
x1_e = list(self.mesh["xe_min"][x1_emin][1]) + [
143+
self.mesh["xe_max"][x1_emax][1][-1]
144+
]
145+
x2_e = list(self.mesh["xe_min"][x2_emin][1]) + [
146+
self.mesh["xe_max"][x2_emax][1][-1]
147+
]
128148
if self.configs["coordinates"] == "cart":
129-
for x in self.attrs["X1"]:
130-
ax.plot([x, x], [self.attrs["X2Min"], self.attrs["X2Max"]], **options)
131-
for y in self.attrs["X2"]:
132-
ax.plot([self.attrs["X1Min"], self.attrs["X1Max"]], [y, y], **options)
149+
for x1 in x1_e:
150+
ax.plot([x1, x1], [x2_e[0], x2_e[-1]], **options)
151+
for x2 in x2_e:
152+
ax.plot([x1_e[0], x1_e[-1]], [x2, x2], **options)
133153
else:
134-
for r in self.attrs["X1"]:
154+
for r in x1_e:
135155
ax.add_patch(
136-
patches.Arc(
156+
mpatches.Arc(
137157
(0, 0),
138158
2 * r,
139159
2 * r,
@@ -143,15 +163,15 @@ def plotGrid(self, ax, **kwargs):
143163
**options,
144164
)
145165
)
146-
for th in self.attrs["X2"]:
166+
for th in x2_e:
147167
ax.plot(
148168
[
149-
self.attrs["X1Min"] * np.sin(th),
150-
self.attrs["X1Max"] * np.sin(th),
169+
x1_e[0] * np.sin(th),
170+
x1_e[-1] * np.sin(th),
151171
],
152172
[
153-
self.attrs["X1Min"] * np.cos(th),
154-
self.attrs["X1Max"] * np.cos(th),
173+
x1_e[0] * np.cos(th),
174+
x1_e[-1] * np.cos(th),
155175
],
156176
**options,
157177
)

nt2/containers/fields.py

Lines changed: 127 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@
66
from nt2.containers.utils import (
77
_read_category_metadata,
88
_read_coordinates,
9+
_preload_domain_shapes,
910
_preload_field,
11+
_preload_field_with_ghosts,
1012
)
1113

1214
from nt2.plotters.polar import (
@@ -103,9 +105,12 @@ def __init__(self, **kwargs):
103105
False, "f", self.fields_files
104106
)
105107

106-
coords = list(CoordinateDict[self.configs["coordinates"]].values())[::-1][
107-
-self.configs["dimension"] :
108-
]
108+
if not self.isDebug():
109+
coords = list(CoordinateDict[self.configs["coordinates"]].values())[::-1][
110+
-self.configs["dimension"] :
111+
]
112+
else:
113+
coords = ["i3", "i2", "i1"][-self.configs["dimension"] :]
109114

110115
if self.configs["single_file"]:
111116
assert self.master_file is not None, "Master file not found"
@@ -116,30 +121,74 @@ def __init__(self, **kwargs):
116121
self._fields = xr.Dataset()
117122

118123
if "fields" in self.metadata and len(self.metadata["fields"]["outsteps"]) > 0:
119-
for k in self.metadata["fields"]["quantities"]:
120-
name, dset = _preload_field(
121-
single_file=self.configs["single_file"],
122-
k=k,
123-
dim=self.configs["dimension"],
124-
ngh=self.configs["ngh"],
125-
outsteps=self.metadata["fields"]["outsteps"],
126-
times=self.metadata["fields"]["times"],
127-
steps=self.metadata["fields"]["steps"],
128-
coords=coords,
129-
xc_coords=self.mesh["x_c"],
130-
xe_min_coords=self.mesh["x_emin"],
131-
xe_max_coords=self.mesh["x_emax"],
132-
coord_replacements=list(
133-
CoordinateDict[self.configs["coordinates"]].items()
134-
),
135-
field_replacements=list(QuantityDict.items()),
136-
layout=self.configs["layout"],
137-
file=(
138-
self.master_file
139-
if self.configs["single_file"] and self.master_file is not None
140-
else self.fields_files
141-
),
124+
self.domains = xr.Dataset()
125+
for i in range(self.configs["dimension"]):
126+
self.domains[f"x{i+1}"], self.domains[f"sx{i+1}"] = (
127+
_preload_domain_shapes(
128+
single_file=self.configs["single_file"],
129+
k=f"N{i+1}l",
130+
outsteps=self.metadata["fields"]["outsteps"],
131+
times=self.metadata["fields"]["times"],
132+
steps=self.metadata["fields"]["steps"],
133+
file=(
134+
self.master_file
135+
if self.configs["single_file"]
136+
and self.master_file is not None
137+
else self.fields_files
138+
),
139+
)
142140
)
141+
142+
for k in self.metadata["fields"]["quantities"]:
143+
if not self.isDebug():
144+
name, dset = _preload_field(
145+
single_file=self.configs["single_file"],
146+
k=k,
147+
outsteps=self.metadata["fields"]["outsteps"],
148+
times=self.metadata["fields"]["times"],
149+
steps=self.metadata["fields"]["steps"],
150+
coords=coords,
151+
xc_coords=self.mesh["xc"],
152+
xe_min_coords=self.mesh["xe_min"],
153+
xe_max_coords=self.mesh["xe_max"],
154+
coord_replacements=list(
155+
CoordinateDict[self.configs["coordinates"]].items()
156+
),
157+
field_replacements=list(QuantityDict.items()),
158+
layout=self.configs["layout"],
159+
file=(
160+
self.master_file
161+
if self.configs["single_file"]
162+
and self.master_file is not None
163+
else self.fields_files
164+
),
165+
)
166+
else:
167+
(
168+
name,
169+
dset,
170+
self.mesh["xc"],
171+
self.mesh["xe_min"],
172+
self.mesh["xe_max"],
173+
) = _preload_field_with_ghosts(
174+
single_file=self.configs["single_file"],
175+
k=k,
176+
outsteps=self.metadata["fields"]["outsteps"],
177+
times=self.metadata["fields"]["times"],
178+
steps=self.metadata["fields"]["steps"],
179+
coords=coords,
180+
coord_replacements=list(
181+
CoordinateDict[self.configs["coordinates"]].items()
182+
),
183+
field_replacements=list(QuantityDict.items()),
184+
layout=self.configs["layout"],
185+
file=(
186+
self.master_file
187+
if self.configs["single_file"]
188+
and self.master_file is not None
189+
else self.fields_files
190+
),
191+
)
143192
self.fields[name] = dset
144193

145194
@property
@@ -190,3 +239,55 @@ def compactify(lst):
190239
string += "Fields: empty\n"
191240

192241
return string
242+
243+
def plotDomains(self, ax, ti=None, t=None, **kwargs):
244+
if self.domains is None:
245+
raise AttributeError("Domains not found")
246+
247+
assert len(self.domains.data_vars) == 4, "Data must be 2D for plotGrid to work"
248+
249+
import matplotlib.patches as mpatches
250+
251+
ngh = self.configs["ngh"]
252+
253+
xlim, ylim = ax.get_xlim(), ax.get_ylim()
254+
options = {
255+
"lw": 2,
256+
"color": "r",
257+
"ls": "-",
258+
}
259+
options.update(kwargs)
260+
261+
for dom in self.domains.dom:
262+
selection = self.domains.sel(dom=dom)
263+
if ti is not None:
264+
selection = selection.sel(t=ti)
265+
elif t is not None:
266+
selection = selection.sel(t=t, method="nearest")
267+
else:
268+
selection = selection.isel(t=0)
269+
270+
x1c, sx1 = selection.x1.values[()], selection.sx1.values[()]
271+
x2c, sx2 = selection.x2.values[()], selection.sx2.values[()]
272+
273+
# add rectangle
274+
ax.add_patch(
275+
mpatches.Rectangle(
276+
(x1c + ngh, x2c + ngh),
277+
sx1 - 2 * ngh,
278+
sx2 - 2 * ngh,
279+
fill=None,
280+
**options,
281+
)
282+
)
283+
284+
# ax.plot(
285+
# self.domains[x1][j],
286+
# self.domains[x2][j],
287+
# **options,
288+
# )
289+
# ax.plot(
290+
# self.domains[x1_e][j],
291+
# self.domains[x2_e][j],
292+
# **options,
293+
# )

0 commit comments

Comments
 (0)