Skip to content

Commit 4fc4137

Browse files
authored
Merge pull request #6 from entity-toolkit/1.0.0rc
1.0.0 Release Candidate
2 parents 284be6a + cd19f28 commit 4fc4137

39 files changed

+3266
-1694
lines changed

.gitattributes

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
nt2/tests/testdata.tar.gz filter=lfs diff=lfs merge=lfs -text

.github/workflows/publish.yml

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,22 @@ jobs:
88

99
steps:
1010
- uses: actions/checkout@v3
11+
with:
12+
lfs: true
13+
- name: Set up Python 3.12
14+
uses: actions/setup-python@v4
15+
with:
16+
python-version: "3.12"
17+
- name: Install dependencies
18+
run: |
19+
python -m pip install --upgrade pip
20+
pip install pytest
21+
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
22+
- name: Test with `pytest`
23+
run: |
24+
pytest
1125
- name: Publish package
1226
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags')
1327
uses: pypa/gh-action-pypi-publish@release/v1
1428
with:
15-
password: ${{ secrets.PYPI_API_TOKEN }}
29+
password: ${{ secrets.PYPI_API_TOKEN }}

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,7 @@ dmypy.json
151151
# Cython debug symbols
152152
cython_debug/
153153

154+
nt2/tests/testdata
154155
test/
155156
temp/
156157
*.bak

.vscode/extensions.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"recommendations": [
3+
"ms-python.python",
4+
"ms-python.black-formatter"
5+
]
6+
}

README.md

Lines changed: 41 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,14 @@ pip install nt2py
88

99
### Usage
1010

11-
The Library works both with single-file output as well as with separate files. In either case, the location of the data is passed via `path` keyword argument.
11+
Simply pass the location to the data when initializing the main `Data` object:
1212

1313
```python
1414
import nt2
1515

16-
data = nt2.Data(path="path/to/data")
16+
data = nt2.Data("path/to/data")
1717
# example:
18-
# data = nt2.Data(path="path/to/shock.h5") : for single-file
19-
# data = nt2.Data(path="path/to/shock") : for multi-file
18+
# data = nt2.Data("path/to/shock")
2019
```
2120

2221
The data is stored in specialized containers which can be accessed via corresponding attributes:
@@ -146,16 +145,51 @@ nt2.Dashboard()
146145

147146
This will output the port where the dashboard server is running, e.g., `Dashboard: http://127.0.0.1:8787/status`. Click on it (or enter in your browser) to open the dashboard.
148147

148+
### CLI
149+
150+
Since version 1.0.0, `nt2py` also offers a command-line interface, accessed via `nt2` command. To view all the options, simply run:
151+
152+
```sh
153+
nt2 --help
154+
```
155+
156+
The plotting routine is pretty customizable. For instance, if the data is located in `myrun/mysimulation`, you can inspect the content of the data structure using:
157+
158+
```sh
159+
nt2 show myrun/mysimulation
160+
```
161+
162+
Or if you want to make a quick plot (a-la `inspect` discussed above) of the specific quantities, you may simply run:
163+
164+
```sh
165+
nt2 plot myrun/mysimulation --fields "E.*;B.*" --isel "t=5" --sel "x=slice(-5, None); z=0.5"
166+
```
167+
168+
This plots the 6-th snapshot (`t=5`) of all the `E` and `B` field components, sliced for `x > -5`, and at `z = 0.5` (notice, that you can use both `--isel` and `--sel`). If instead, you prefer to make a movie, simply do not specify the time:
169+
170+
```sh
171+
nt2 plot myrun/mysimulation --fields "E.*;B.*" --sel "x=slice(-5, None); z=0.5"
172+
```
173+
174+
> If you want to only install the CLI, without the library itself, you may do that via `pipx`: `pipx install nt2py`.
175+
149176
### Features
150177

151178
1. Lazy loading and parallel processing of the simulation data with [`dask`](https://dask.org/).
152179
2. Context-aware data manipulation with [`xarray`](http://xarray.pydata.org/en/stable/).
153-
3. Parellel plotting and movie generation with [`multiprocessing`](https://docs.python.org/3/library/multiprocessing.html) and [`ffmpeg`](https://ffmpeg.org/).
180+
3. Parallel plotting and movie generation with [`multiprocessing`](https://docs.python.org/3/library/multiprocessing.html) and [`ffmpeg`](https://ffmpeg.org/).
181+
4. Command-line interface, the `nt2` command, for quick plotting (both movies and snapshots).
182+
183+
### Testing
184+
185+
There are unit tests included with the code which also require downloading test data with [`git lfs`](https://git-lfs.com/) (installed separately from `git`). You may download the data simply by running `git lfs pull`.
154186

155187
### TODO
156188

157-
- [ ] Unit tests
158-
- [ ] Plugins for other simulation data formats
189+
- [x] Unit tests
190+
- [x] Plugins for other simulation data formats
191+
- [ ] Support for sparse arrays for particles via `Sparse` library
192+
- [x] Command-line interface
159193
- [ ] Support for multiple runs
160194
- [ ] Interactive regime (`hvplot`, `bokeh`, `panel`)
161195
- [x] Ghost cells support

nt2/__init__.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
__version__ = "0.6.0"
1+
__version__ = "1.0.0"
22

3-
from nt2.data import Data as Data
4-
from nt2.dashboard import Dashboard as Dashboard
3+
import nt2.containers.data as nt2_data
4+
5+
6+
class Data(nt2_data.Data):
7+
pass

nt2/cli/__init__.py

Whitespace-only changes.

nt2/cli/__main__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from .main import app
2+
3+
app(prog_name="nt2")

nt2/cli/main.py

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
import typer, nt2, os
2+
from typing_extensions import Annotated
3+
import matplotlib.pyplot as plt
4+
5+
app = typer.Typer()
6+
7+
8+
@app.command(help="Print the data info")
9+
def version():
10+
print(nt2.__version__)
11+
12+
13+
def check_path(path: str) -> str:
14+
if not os.path.exists(path) or not (
15+
os.path.exists(os.path.join(path, "fields"))
16+
or os.path.exists(os.path.join(path, "particles"))
17+
or os.path.exists(os.path.join(path, "spectra"))
18+
):
19+
raise typer.BadParameter(
20+
f"Path {path} does not exist or is not a valid nt2 data directory."
21+
)
22+
return path
23+
24+
25+
def check_sel(sel: str) -> dict[str, int | float | slice]:
26+
if sel == "":
27+
return {}
28+
sel_list = sel.strip().split(";")
29+
sel_dict: dict[str, int | float | slice] = {}
30+
for _, s in enumerate(sel_list):
31+
coord, arg = s.strip().split("=", 1)
32+
coord = coord.strip()
33+
arg_exec = eval(arg.strip())
34+
assert isinstance(
35+
arg_exec, (int, float, slice)
36+
), f"Invalid selection argument for '{coord}': {arg_exec}. Must be int, float, or slice."
37+
sel_dict[coord] = arg_exec
38+
return sel_dict
39+
40+
41+
def check_species(species: int) -> int:
42+
if species < 0:
43+
raise typer.BadParameter(
44+
f"Species index must be a non-negative integer, got {species}."
45+
)
46+
return species
47+
48+
49+
def check_what(what: str) -> str:
50+
valid_options = ["fields", "particles", "spectra"]
51+
if what not in valid_options:
52+
raise typer.BadParameter(
53+
f"Invalid option '{what}'. Valid options are: {', '.join(valid_options)}."
54+
)
55+
return what
56+
57+
58+
@app.command(help="Print the data info")
59+
def show(
60+
path: Annotated[
61+
str,
62+
typer.Argument(
63+
callback=check_path,
64+
help="Path to the data",
65+
),
66+
] = "",
67+
):
68+
data = nt2.Data(path)
69+
print(data.to_str())
70+
71+
72+
@app.command(help="Plot the data")
73+
def plot(
74+
path: Annotated[
75+
str,
76+
typer.Argument(
77+
callback=check_path,
78+
help="Path to the data",
79+
),
80+
] = "",
81+
what: Annotated[
82+
Annotated[
83+
str,
84+
typer.Option(
85+
callback=check_what,
86+
help="Which data to plot [fields, particles, spectra]",
87+
),
88+
],
89+
str,
90+
] = "fields",
91+
fields: Annotated[
92+
str,
93+
typer.Option(
94+
help="Which fields to plot (only when `what` is `fields`). Separate multiple fields with ';'. May contain regex. Empty = all fields. Example: `--fields \"E.*;B.*\"`",
95+
),
96+
] = "",
97+
# species: Annotated[
98+
# Annotated[
99+
# int,
100+
# typer.Option(
101+
# callback=check_species,
102+
# help="Which species to take (only when `what` is `particles`). 0 = all species",
103+
# ),
104+
# ],
105+
# str,
106+
# ] = 0,
107+
sel: Annotated[
108+
str,
109+
typer.Option(
110+
callback=check_sel,
111+
help="Select a subset of the data with xarray.sel. Separate multiple selections with ';'. Example: `--sel \"t=23;z=slice(0, None)\"`",
112+
),
113+
] = "",
114+
isel: Annotated[
115+
str,
116+
typer.Option(
117+
callback=check_sel,
118+
help="Select a subset of the data with xarray.isel. Separate multiple selections with ';'. Example: `--isel \"t=slice(None, 5);z=5\"`",
119+
),
120+
] = "",
121+
):
122+
fname = os.path.basename(path.strip("/"))
123+
data = nt2.Data(path)
124+
assert isinstance(
125+
sel, dict
126+
), f"Invalid selection format: {sel}. Must be a dictionary."
127+
assert isinstance(isel, dict), f"Invalid isel format: {isel}. Must be a dictionary."
128+
if what == "fields":
129+
d = data.fields
130+
if sel != {}:
131+
slices = {}
132+
sels = {}
133+
slices: dict[str, slice | float | int] = {
134+
k: v for k, v in sel.items() if isinstance(v, slice)
135+
}
136+
sels: dict[str, slice | float | int] = {
137+
k: v for k, v in sel.items() if not isinstance(v, slice)
138+
}
139+
d = d.sel(**sels, method="nearest")
140+
d = d.sel(**slices)
141+
if isel != {}:
142+
d = d.isel(**isel)
143+
if fields != "":
144+
ret = d.inspect.plot(
145+
name=fname, only_fields=fields.split(";"), fig_kwargs={"dpi": 200}
146+
)
147+
else:
148+
ret = d.inspect.plot(name=fname, fig_kwargs={"dpi": 200})
149+
if not isinstance(ret, bool):
150+
plt.savefig(fname=f"{fname}.png")
151+
152+
elif what == "particles":
153+
raise NotImplementedError("Particles plotting is not implemented yet.")
154+
elif what == "spectra":
155+
raise NotImplementedError("Spectra plotting is not implemented yet.")
156+
else:
157+
raise typer.BadParameter(
158+
f"Invalid option '{what}'. Valid options are: fields, particles, spectra."
159+
)

0 commit comments

Comments
 (0)