Skip to content

Commit c1e8dd6

Browse files
authored
Add tests (#6)
* add tests * add better instructions to README file * add setup.py file * remove trailing white spaces * dont tets what we can't test right now * fix test * bugfix * add codecov secret * test with more python versions * change python versions * slighly change README * update setup.py * test with python 3.9 again
1 parent b68c1e6 commit c1e8dd6

File tree

8 files changed

+339
-15
lines changed

8 files changed

+339
-15
lines changed

.github/workflows/tests.yml

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
name: Tests
2+
3+
on:
4+
push:
5+
branches: [ main ]
6+
pull_request:
7+
branches: [ main ]
8+
9+
jobs:
10+
test:
11+
runs-on: ubuntu-latest
12+
strategy:
13+
matrix:
14+
python-version: [3.9, 3.11, 3.12, 3.13]
15+
16+
steps:
17+
- uses: actions/checkout@v2
18+
19+
- name: Set up Python ${{ matrix.python-version }}
20+
uses: actions/setup-python@v2
21+
with:
22+
python-version: ${{ matrix.python-version }}
23+
24+
- name: Install dependencies
25+
run: |
26+
python -m pip install --upgrade pip
27+
pip install pytest pytest-cov
28+
pip install -e .
29+
30+
- name: Run tests
31+
run: |
32+
pytest tests/ --cov=pynanigans --cov-report=xml
33+
34+
- name: Upload coverage to Codecov
35+
uses: codecov/codecov-action@v5
36+
with:
37+
files: ./coverage.xml
38+
fail_ci_if_error: false
39+
token: ${{ secrets.CODECOV_TOKEN }}

README.md

Lines changed: 73 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,80 @@
11
# pynanigans
2+
23
Python scripts for Oceananigans.jl NetCDF output
34

45
## Installation
56

6-
- Clone this repo somewhere
7-
- Install with pip:
8-
- `pip install /path/to/cloned/repo`
7+
### Using pip
8+
9+
You can install pynanigans directly from the repository:
10+
11+
```bash
12+
pip install git+https://github.com/yourusername/pynanigans.git
13+
```
14+
15+
Or install from a local clone:
16+
17+
```bash
18+
git clone https://github.com/yourusername/pynanigans.git
19+
cd pynanigans
20+
pip install -e .
21+
```
22+
23+
### Using conda
24+
25+
If you prefer using conda, you can create an environment with all dependencies:
26+
27+
```bash
28+
# Clone the repository
29+
git clone https://github.com/yourusername/pynanigans.git
30+
cd pynanigans
31+
32+
# Create and activate conda environment
33+
conda env create -f environment.yml
34+
conda activate p39
35+
36+
# Install the package
37+
pip install -e .
38+
```
39+
40+
## Dependencies
41+
42+
The package requires:
43+
- Python >= 3.9
44+
- numpy
45+
- xarray
46+
- xgcm
47+
- matplotlib
48+
49+
## Development
50+
51+
To set up a development environment:
52+
53+
1. Clone the repository:
54+
```bash
55+
git clone https://github.com/yourusername/pynanigans.git
56+
cd pynanigans
57+
```
58+
59+
2. Create a virtual environment and install dependencies:
60+
```bash
61+
python -m venv venv
62+
source venv/bin/activate # On Windows: venv\Scripts\activate
63+
pip install -e ".[dev]"
64+
```
65+
66+
3. Install development dependencies:
67+
```bash
68+
pip install pytest pytest-cov
69+
```
70+
71+
4. Run tests:
72+
```bash
73+
pytest tests/
74+
```
75+
76+
## License
77+
78+
See the [LICENSE](LICENSE) file for details.
979

1080

pynanigans/grids.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11

22
def get_coords(ds, topology="PPN",):
3-
"""
3+
"""
44
Constructs the coords dict for ds to be passed to xgcm.Grid
55
Flat dimensions (F) are treated the same as Periodic ones (P)
66
"""
@@ -9,7 +9,7 @@ def get_coords(ds, topology="PPN",):
99
per = { dim : dict(left=f"{dim}F", center=f"{dim}C") for dim in "xyz" }
1010
nper = { dim : dict(outer=f"{dim}F", center=f"{dim}C") for dim in "xyz" }
1111
coords = { dim : per[dim] if top in "FP" else nper[dim] for dim, top in zip("xyz", topology) }
12-
12+
1313
return coords
1414

1515

@@ -38,7 +38,7 @@ def get_distances(ds, dim="x", topology="P"):
3838

3939

4040
def get_metrics(ds, topology="PPN"):
41-
"""
41+
"""
4242
Constructs the metric dict for ds.
4343
(Not sure if the metrics are correct at the boundary points
4444
"""
@@ -67,7 +67,7 @@ def get_grid(ds, coords=None, metrics=None, topology="PPN", **kwargs):
6767
metrics = get_metrics(ds, topology=topology)
6868

6969
periodic = [ dim for (dim, top) in zip("xyz", topology) if top in "PF" ]
70-
return xg.Grid(ds, coords=coords, metrics=metrics,
70+
return xg.Grid(ds, coords=coords, metrics=metrics,
7171
periodic=periodic, **kwargs)
7272

7373

pynanigans/utils.py

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,10 @@ def biject(darray, *args, surjection=surjection):
1414
"""
1515
Renames darray so that the dimension names actually correspond to the physical
1616
dimensions, instead of being the name of the grid meshes in Oceananigans.
17-
If `*args` is provided, only those dimensions will be renamed. If not, `x`, `y`
17+
If `*args` is provided, only those dimensions will be renamed. If not, `x`, `y`
1818
and `z` will be automatically renamed.
1919
20-
This makes calling functions easier as instead of calling `darray.u.plot(x='xF', y='zC')`,
20+
This makes calling functions easier as instead of calling `darray.u.plot(x='xF', y='zC')`,
2121
you can call `darray.pnplot(x='x', y='z')`
2222
"""
2323
da_dims = darray.dims
@@ -39,7 +39,7 @@ def regular_indef_integrate(f, dim):
3939

4040

4141
def normalize_time_by(darray, seconds=1, new_units="seconds"):
42-
"""
42+
"""
4343
Converts the time dimension (a timedelta[ns] object by default) into a np.float64
4444
object while normalizing it by number of seconds `seconds`.
4545
"""
@@ -72,13 +72,15 @@ def downsample(darray, round_func=round, **dim_limits):
7272

7373

7474
def pnchunk(darray, maxsize_4d=1000**2, sample_var="u", round_func=round, **kwargs):
75-
""" Chunk `darray` in time while keeping each chunk's size roughly
75+
""" Chunk `darray` in time while keeping each chunk's size roughly
7676
around `maxsize_4d`. The default `maxsize_4d=1000**2` comes from
7777
xarray's rule of thumb for chunking:
7878
http://xarray.pydata.org/en/stable/dask.html#chunking-and-performance
7979
"""
80-
chunk_number = darray[sample_var].size / maxsize_4d
81-
chunk_size = int(round_func(len(darray[sample_var].time) / chunk_number))
80+
if type(darray) == xr.Dataset:
81+
darray = darray[sample_var]
82+
chunk_number = darray.size / maxsize_4d
83+
chunk_size = int(round_func(len(darray.time) / chunk_number))
8284
return darray.chunk(dict(time=chunk_size))
8385
xr.DataArray.pnchunk = pnchunk
8486
xr.Dataset.pnchunk = pnchunk
@@ -119,7 +121,7 @@ def pn{funcname}(darray, dim=None, surjection=surjection, **kwargs):
119121
exec(funcdef)
120122

121123

122-
def open_simulation(fname,
124+
def open_simulation(fname,
123125
grid_kwargs=dict(),
124126
load=False,
125127
squeeze=True,
@@ -131,7 +133,7 @@ def open_simulation(fname,
131133
132134
`kwargs` are passed to `xarray.open_dataset()` and `grid_kwargs` are passed to `pynanigans.get_grid()`.
133135
"""
134-
136+
135137
#++++ Open dataset and create grid before squeezing
136138
if load:
137139
ds = xr.load_dataset(fname, **kwargs)

setup.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
from setuptools import setup, find_packages
2+
3+
setup(
4+
name="pynanigans",
5+
version="0.1.0",
6+
packages=find_packages(),
7+
install_requires=[
8+
"numpy",
9+
"xarray",
10+
"xgcm",
11+
"matplotlib",
12+
],
13+
python_requires=">=3.9",
14+
author="Tomas Chor",
15+
author_email="contact@tomaschor.xyz",
16+
description="A Python package for working with Oceananigans data",
17+
long_description=open("README.md").read(),
18+
long_description_content_type="text/markdown",
19+
url="https://github.com/yourusername/pynanigans",
20+
classifiers=[
21+
"Development Status :: 3 - Alpha",
22+
"Intended Audience :: Science/Research",
23+
"License :: OSI Approved :: MIT License",
24+
"Programming Language :: Python",
25+
],
26+
)

tests/test_grids.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import pytest
2+
import xarray as xr
3+
import numpy as np
4+
from pynanigans.grids import get_coords, get_metrics, get_grid
5+
6+
def test_get_coords():
7+
# Test periodic coordinates
8+
coords = get_coords(None, topology="PPP")
9+
assert "x" in coords
10+
assert "y" in coords
11+
assert "z" in coords
12+
assert coords["x"]["left"] == "xF"
13+
assert coords["x"]["center"] == "xC"
14+
15+
# Test non-periodic coordinates
16+
coords = get_coords(None, topology="NNN")
17+
assert coords["x"]["outer"] == "xF"
18+
assert coords["x"]["center"] == "xC"

tests/test_pnplot.py

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import pytest
2+
import xarray as xr
3+
import numpy as np
4+
from pynanigans.pnplot import pnplot, _imshow, _pcolormesh, _contour, _contourf
5+
6+
def test_pnplot():
7+
# Create a test dataset
8+
data = np.random.rand(10, 10)
9+
dims = ['xC', 'yC']
10+
coords = {
11+
'xC': np.linspace(0, 1, 10),
12+
'yC': np.linspace(0, 1, 10)
13+
}
14+
ds = xr.Dataset(
15+
data_vars={'u': (dims, data)},
16+
coords=coords
17+
)
18+
19+
# Test plotting
20+
plot = pnplot(ds.u, x='x', y='y')
21+
assert plot is not None
22+
23+
def test_imshow():
24+
# Create a test dataset
25+
data = np.random.rand(10, 10)
26+
dims = ['xC', 'yC']
27+
coords = {
28+
'xC': np.linspace(0, 1, 10),
29+
'yC': np.linspace(0, 1, 10)
30+
}
31+
ds = xr.Dataset(
32+
data_vars={'u': (dims, data)},
33+
coords=coords
34+
)
35+
36+
# Test imshow
37+
plot = _imshow(ds.u, x='x', y='y')
38+
assert plot is not None
39+
40+
def test_pcolormesh():
41+
# Create a test dataset
42+
data = np.random.rand(10, 10)
43+
dims = ['xC', 'yC']
44+
coords = {
45+
'xC': np.linspace(0, 1, 10),
46+
'yC': np.linspace(0, 1, 10)
47+
}
48+
ds = xr.Dataset(
49+
data_vars={'u': (dims, data)},
50+
coords=coords
51+
)
52+
53+
# Test pcolormesh
54+
plot = _pcolormesh(ds.u, x='x', y='y')
55+
assert plot is not None
56+
57+
def test_contour():
58+
# Create a test dataset
59+
data = np.random.rand(10, 10)
60+
dims = ['xC', 'yC']
61+
coords = {
62+
'xC': np.linspace(0, 1, 10),
63+
'yC': np.linspace(0, 1, 10)
64+
}
65+
ds = xr.Dataset(
66+
data_vars={'u': (dims, data)},
67+
coords=coords
68+
)
69+
70+
# Test contour
71+
plot = _contour(ds.u, x='x', y='y')
72+
assert plot is not None
73+
74+
def test_contourf():
75+
# Create a test dataset
76+
data = np.random.rand(10, 10)
77+
dims = ['xC', 'yC']
78+
coords = {
79+
'xC': np.linspace(0, 1, 10),
80+
'yC': np.linspace(0, 1, 10)
81+
}
82+
ds = xr.Dataset(
83+
data_vars={'u': (dims, data)},
84+
coords=coords
85+
)
86+
87+
# Test contourf
88+
plot = _contourf(ds.u, x='x', y='y')
89+
assert plot is not None

0 commit comments

Comments
 (0)