Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
nt2/tests/testdata.tar.gz filter=lfs diff=lfs merge=lfs -text
16 changes: 15 additions & 1 deletion .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,22 @@ jobs:

steps:
- uses: actions/checkout@v3
with:
lfs: true
- name: Set up Python 3.12
uses: actions/setup-python@v4
with:
python-version: "3.12"
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install pytest
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
- name: Test with `pytest`
run: |
pytest
- name: Publish package
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags')
uses: pypa/gh-action-pypi-publish@release/v1
with:
password: ${{ secrets.PYPI_API_TOKEN }}
password: ${{ secrets.PYPI_API_TOKEN }}
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@ dmypy.json
# Cython debug symbols
cython_debug/

nt2/tests/testdata
test/
temp/
*.bak
6 changes: 6 additions & 0 deletions .vscode/extensions.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"recommendations": [
"ms-python.python",
"ms-python.black-formatter"
]
}
48 changes: 41 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,14 @@ pip install nt2py

### Usage

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.
Simply pass the location to the data when initializing the main `Data` object:

```python
import nt2

data = nt2.Data(path="path/to/data")
data = nt2.Data("path/to/data")
# example:
# data = nt2.Data(path="path/to/shock.h5") : for single-file
# data = nt2.Data(path="path/to/shock") : for multi-file
# data = nt2.Data("path/to/shock")
```

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

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.

### CLI

Since version 1.0.0, `nt2py` also offers a command-line interface, accessed via `nt2` command. To view all the options, simply run:

```sh
nt2 --help
```

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:

```sh
nt2 show myrun/mysimulation
```

Or if you want to make a quick plot (a-la `inspect` discussed above) of the specific quantities, you may simply run:

```sh
nt2 plot myrun/mysimulation --fields "E.*;B.*" --isel "t=5" --sel "x=slice(-5, None); z=0.5"
```

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:

```sh
nt2 plot myrun/mysimulation --fields "E.*;B.*" --sel "x=slice(-5, None); z=0.5"
```

> If you want to only install the CLI, without the library itself, you may do that via `pipx`: `pipx install nt2py`.

### Features

1. Lazy loading and parallel processing of the simulation data with [`dask`](https://dask.org/).
2. Context-aware data manipulation with [`xarray`](http://xarray.pydata.org/en/stable/).
3. Parellel plotting and movie generation with [`multiprocessing`](https://docs.python.org/3/library/multiprocessing.html) and [`ffmpeg`](https://ffmpeg.org/).
3. Parallel plotting and movie generation with [`multiprocessing`](https://docs.python.org/3/library/multiprocessing.html) and [`ffmpeg`](https://ffmpeg.org/).
4. Command-line interface, the `nt2` command, for quick plotting (both movies and snapshots).

### Testing

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`.

### TODO

- [ ] Unit tests
- [ ] Plugins for other simulation data formats
- [x] Unit tests
- [x] Plugins for other simulation data formats
- [ ] Support for sparse arrays for particles via `Sparse` library
- [x] Command-line interface
- [ ] Support for multiple runs
- [ ] Interactive regime (`hvplot`, `bokeh`, `panel`)
- [x] Ghost cells support
Expand Down
9 changes: 6 additions & 3 deletions nt2/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
__version__ = "0.6.0"
__version__ = "1.0.0"

from nt2.data import Data as Data
from nt2.dashboard import Dashboard as Dashboard
import nt2.containers.data as nt2_data


class Data(nt2_data.Data):
pass
Empty file added nt2/cli/__init__.py
Empty file.
3 changes: 3 additions & 0 deletions nt2/cli/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from .main import app

app(prog_name="nt2")
159 changes: 159 additions & 0 deletions nt2/cli/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
import typer, nt2, os
from typing_extensions import Annotated
import matplotlib.pyplot as plt

app = typer.Typer()


@app.command(help="Print the data info")
def version():
print(nt2.__version__)


def check_path(path: str) -> str:
if not os.path.exists(path) or not (
os.path.exists(os.path.join(path, "fields"))
or os.path.exists(os.path.join(path, "particles"))
or os.path.exists(os.path.join(path, "spectra"))
):
raise typer.BadParameter(
f"Path {path} does not exist or is not a valid nt2 data directory."
)
return path


def check_sel(sel: str) -> dict[str, int | float | slice]:
if sel == "":
return {}
sel_list = sel.strip().split(";")
sel_dict: dict[str, int | float | slice] = {}
for _, s in enumerate(sel_list):
coord, arg = s.strip().split("=", 1)
coord = coord.strip()
arg_exec = eval(arg.strip())
assert isinstance(
arg_exec, (int, float, slice)
), f"Invalid selection argument for '{coord}': {arg_exec}. Must be int, float, or slice."
sel_dict[coord] = arg_exec
return sel_dict


def check_species(species: int) -> int:
if species < 0:
raise typer.BadParameter(
f"Species index must be a non-negative integer, got {species}."
)
return species


def check_what(what: str) -> str:
valid_options = ["fields", "particles", "spectra"]
if what not in valid_options:
raise typer.BadParameter(
f"Invalid option '{what}'. Valid options are: {', '.join(valid_options)}."
)
return what


@app.command(help="Print the data info")
def show(
path: Annotated[
str,
typer.Argument(
callback=check_path,
help="Path to the data",
),
] = "",
):
data = nt2.Data(path)
print(data.to_str())


@app.command(help="Plot the data")
def plot(
path: Annotated[
str,
typer.Argument(
callback=check_path,
help="Path to the data",
),
] = "",
what: Annotated[
Annotated[
str,
typer.Option(
callback=check_what,
help="Which data to plot [fields, particles, spectra]",
),
],
str,
] = "fields",
fields: Annotated[
str,
typer.Option(
help="Which fields to plot (only when `what` is `fields`). Separate multiple fields with ';'. May contain regex. Empty = all fields. Example: `--fields \"E.*;B.*\"`",
),
] = "",
# species: Annotated[
# Annotated[
# int,
# typer.Option(
# callback=check_species,
# help="Which species to take (only when `what` is `particles`). 0 = all species",
# ),
# ],
# str,
# ] = 0,
sel: Annotated[
str,
typer.Option(
callback=check_sel,
help="Select a subset of the data with xarray.sel. Separate multiple selections with ';'. Example: `--sel \"t=23;z=slice(0, None)\"`",
),
] = "",
isel: Annotated[
str,
typer.Option(
callback=check_sel,
help="Select a subset of the data with xarray.isel. Separate multiple selections with ';'. Example: `--isel \"t=slice(None, 5);z=5\"`",
),
] = "",
):
fname = os.path.basename(path.strip("/"))
data = nt2.Data(path)
assert isinstance(
sel, dict
), f"Invalid selection format: {sel}. Must be a dictionary."
assert isinstance(isel, dict), f"Invalid isel format: {isel}. Must be a dictionary."
if what == "fields":
d = data.fields
if sel != {}:
slices = {}
sels = {}
slices: dict[str, slice | float | int] = {
k: v for k, v in sel.items() if isinstance(v, slice)
}
sels: dict[str, slice | float | int] = {
k: v for k, v in sel.items() if not isinstance(v, slice)
}
d = d.sel(**sels, method="nearest")
d = d.sel(**slices)
if isel != {}:
d = d.isel(**isel)
if fields != "":
ret = d.inspect.plot(
name=fname, only_fields=fields.split(";"), fig_kwargs={"dpi": 200}
)
else:
ret = d.inspect.plot(name=fname, fig_kwargs={"dpi": 200})
if not isinstance(ret, bool):
plt.savefig(fname=f"{fname}.png")

elif what == "particles":
raise NotImplementedError("Particles plotting is not implemented yet.")
elif what == "spectra":
raise NotImplementedError("Spectra plotting is not implemented yet.")
else:
raise typer.BadParameter(
f"Invalid option '{what}'. Valid options are: fields, particles, spectra."
)
Loading