Skip to content

Commit 21b07ce

Browse files
authored
Merge branch 'master' into fix_minor_spelling_grammar_tutorials
2 parents 88e6f22 + 168e404 commit 21b07ce

12 files changed

Lines changed: 157 additions & 24 deletions
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,3 +94,17 @@ jobs:
9494
user: __token__
9595
password: ${{ secrets.PARCELS_PYPI_PROD_TOKEN }}
9696
verbose: true
97+
98+
test-pypi-release:
99+
needs: upload-to-pypi
100+
runs-on: ubuntu-latest
101+
steps:
102+
- uses: conda-incubator/setup-miniconda@v3
103+
with:
104+
activate-environment: parcels
105+
python-version: 3.8
106+
channels: conda-forge
107+
- run: conda install -c conda-forge c-compiler pip
108+
- run: pip install parcels --no-cache
109+
- run: curl https://raw.githubusercontent.com/OceanParcels/parcels/master/docs/examples/example_peninsula.py > example_peninsula.py
110+
- run: python example_peninsula.py

docs/examples/example_globcurrent.py

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -173,14 +173,6 @@ def test_globcurrent_time_extrapolation_error(mode, use_xarray):
173173
pset.execute(AdvectionRK4, runtime=delta(days=1), dt=delta(minutes=5))
174174

175175

176-
@pytest.mark.parametrize('mode', ['scipy', 'jit'])
177-
@pytest.mark.parametrize('use_xarray', [True, False])
178-
def test_globcurrent_dt0(mode, use_xarray):
179-
fieldset = set_globcurrent_fieldset(use_xarray=use_xarray)
180-
pset = ParticleSet(fieldset, pclass=ptype[mode], lon=[25], lat=[-35])
181-
pset.execute(AdvectionRK4, dt=0.)
182-
183-
184176
@pytest.mark.parametrize('mode', ['scipy', 'jit'])
185177
@pytest.mark.parametrize('dt', [-300, 300])
186178
@pytest.mark.parametrize('with_starttime', [True, False])
@@ -260,3 +252,22 @@ def test_globcurrent_pset_fromfile(mode, dt, pid_offset, tmpdir):
260252

261253
for var in ['lon', 'lat', 'depth', 'time', 'id']:
262254
assert np.allclose([getattr(p, var) for p in pset], [getattr(p, var) for p in pset_new])
255+
256+
257+
@pytest.mark.parametrize('mode', ['scipy', 'jit'])
258+
def test_error_outputdt_not_multiple_dt(mode, tmpdir):
259+
# Test that outputdt is a multiple of dt
260+
fieldset = set_globcurrent_fieldset()
261+
262+
filepath = tmpdir.join("pfile_error_outputdt_not_multiple_dt.zarr")
263+
264+
dt = 81.2584344538292 # number for which output writing fails
265+
266+
pset = ParticleSet(fieldset, pclass=ptype[mode], lon=[0], lat=[0])
267+
ofile = pset.ParticleFile(name=filepath, outputdt=delta(days=1))
268+
269+
def DoNothing(particle, fieldset, time):
270+
pass
271+
272+
with pytest.raises(ValueError):
273+
pset.execute(DoNothing, runtime=delta(days=10), dt=dt, output_file=ofile)

docs/examples/tutorial_jit_vs_scipy.ipynb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
"cell_type": "markdown",
66
"metadata": {},
77
"source": [
8-
"# JIT Particles and Scipy particles\n"
8+
"# JIT-vs-Scipy Particles"
99
]
1010
},
1111
{

docs/examples/tutorial_parcels_structure.ipynb

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -302,7 +302,7 @@
302302
" ```python\n",
303303
" value = fieldset.U[time, particle.depth, particle.lat, particle.lon]\n",
304304
" ```\n",
305-
" or simple\n",
305+
" or simply\n",
306306
" ```python\n",
307307
" value = fieldset.U[particle]\n",
308308
" ```\n",
@@ -321,7 +321,14 @@
321321
" \n",
322322
" - It is advised _not_ to update the particle location (`particle.lon`, `particle.lat`, `particle.depth`, and/or `particle.time`) directly, as that can negatively interfere with the way that particle movements by different kernels are vectorially added. Use `particle_dlon`, `particle_dlat`, `particle_ddepth`, and/or `particle_dtime` instead. See also the [kernel loop tutorial](https://docs.oceanparcels.org/en/latest/examples/tutorial_kernelloop.html).\n",
323323
" \n",
324-
" - Note that one has to be careful with writing kernels for vector fields on Curvilinear grids. While Parcels automatically rotates the U and V field when necessary, this is not the case for for example wind data. In that case, a custom rotation function will have to be written.\n"
324+
" - Note that one has to be careful with writing kernels for vector fields on Curvilinear grids. While Parcels automatically rotates the U and V field when necessary, this is not the case for for example wind data. In that case, a custom rotation function will have to be written.\n",
325+
"\n",
326+
" <div class=\"alert alert-info\">\n",
327+
" A note on Field interpoaltion notation\n",
328+
"\n",
329+
" Note that for the interpolation of a `Field`, the second option (`value = fieldset.U[particle]`) is not only a short-hand notation for the (`value = fieldset.U[time, particle.depth, particle.lat, particle.lon]`); it is actually a _faster_ way to interpolate the field at the particle location in Scipy mode, as described in [this section of the JIT-vs-Scipy tutorial](https://docs.oceanparcels.org/en/latest/examples/tutorial_jit_vs_scipy.html#Further-digging-into-Scipy-mode:-adding-particle-keyword-to-Field-sampling).\n",
330+
" \n",
331+
" </div>"
325332
]
326333
},
327334
{

parcels/compilation/codegenerator.py

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -346,7 +346,7 @@ def visit_Call(self, node):
346346
convert = True
347347
if "applyConversion" in node.keywords:
348348
k = node.keywords["applyConversion"]
349-
if isinstance(k, ast.NameConstant):
349+
if isinstance(k, ast.Constant):
350350
convert = k.value
351351

352352
# convert args to Index(Tuple(*args))
@@ -364,7 +364,7 @@ def visit_Call(self, node):
364364
convert = True
365365
if "applyConversion" in node.keywords:
366366
k = node.keywords["applyConversion"]
367-
if isinstance(k, ast.NameConstant):
367+
if isinstance(k, ast.Constant):
368368
convert = k.value
369369

370370
# convert args to Index(Tuple(*args))
@@ -463,8 +463,7 @@ def _check_FieldSamplingArguments(ccode):
463463
def visit_FunctionDef(self, node):
464464
# Generate "ccode" attribute by traversing the Python AST
465465
for stmt in node.body:
466-
if not (hasattr(stmt, 'value') and type(stmt.value) is ast.Str): # ignore docstrings
467-
self.visit(stmt)
466+
self.visit(stmt)
468467

469468
# Create function declaration and argument list
470469
decl = c.Static(c.DeclSpecifier(c.Value("StatusCode", node.name), spec='inline'))
@@ -492,7 +491,7 @@ def visit_FunctionDef(self, node):
492491
body += [c.Statement(f"particles->{coord}[pnum] = particles->{coord}_nextloop[pnum]")]
493492
body += [c.Statement("particles->time[pnum] = particles->time_nextloop[pnum]")]
494493

495-
body += [stmt.ccode for stmt in node.body if not (hasattr(stmt, 'value') and type(stmt.value) is ast.Str)]
494+
body += [stmt.ccode for stmt in node.body]
496495

497496
for coord in ['lon', 'lat', 'depth']:
498497
body += [c.Statement(f"particles->{coord}_nextloop[pnum] = particles->{coord}[pnum] + particle_d{coord}")]
@@ -878,14 +877,14 @@ def visit_Print(self, node):
878877
node.ccode = c.Statement(f'printf("{stat}\\n", {vars})')
879878

880879
def visit_Constant(self, node):
881-
if node.s == 'parcels_customed_Cfunc_pointer_args':
882-
node.ccode = node.s
883-
elif isinstance(node.s, str):
880+
if node.value == 'parcels_customed_Cfunc_pointer_args':
881+
node.ccode = node.value
882+
elif isinstance(node.value, str):
884883
node.ccode = '' # skip strings from docstrings or comments
885-
elif isinstance(node.s, bool):
886-
node.ccode = "1" if node.s is True else "0"
884+
elif isinstance(node.value, bool):
885+
node.ccode = "1" if node.value is True else "0"
887886
else:
888-
node.ccode = str(node.n)
887+
node.ccode = str(node.value)
889888

890889

891890
class LoopGenerator:

parcels/fieldset.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import importlib.util
2+
import sys
13
from copy import deepcopy
24
from glob import glob
35
from os import path
@@ -1062,6 +1064,32 @@ def from_xarray_dataset(cls, ds, variables, dimensions, mesh='spherical', allow_
10621064
v = fields.pop('V', None)
10631065
return cls(u, v, fields=fields)
10641066

1067+
@classmethod
1068+
def from_modulefile(cls, filename, modulename="create_fieldset", **kwargs):
1069+
"""Initialises FieldSet data from a file containing a python module file with a create_fieldset() function.
1070+
1071+
Parameters
1072+
----------
1073+
filename: path to a python file containing at least a function which returns a FieldSet object.
1074+
modulename: name of the function in the python file that returns a FieldSet object. Default is "create_fieldset".
1075+
"""
1076+
# check if filename exists
1077+
if not path.exists(filename):
1078+
raise IOError(f"FieldSet module file {filename} does not exist")
1079+
1080+
# Importing the source file directly (following https://docs.python.org/3/library/importlib.html#importing-a-source-file-directly)
1081+
spec = importlib.util.spec_from_file_location(modulename, filename)
1082+
fieldset_module = importlib.util.module_from_spec(spec)
1083+
sys.modules[modulename] = fieldset_module
1084+
spec.loader.exec_module(fieldset_module)
1085+
1086+
if not hasattr(fieldset_module, modulename):
1087+
raise IOError(f"{filename} does not contain a {modulename} function")
1088+
fieldset = getattr(fieldset_module, modulename)(**kwargs)
1089+
if not isinstance(fieldset, FieldSet):
1090+
raise IOError(f"Module {filename}.{modulename} does not return a FieldSet object")
1091+
return fieldset
1092+
10651093
def get_fields(self):
10661094
"""Returns a list of all the :class:`parcels.field.Field` and :class:`parcels.field.VectorField`
10671095
objects associated with this FieldSet.

parcels/particleset.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -878,8 +878,10 @@ def execute(self, pyfunc=AdvectionRK4, pyfunc_inter=None, endtime=None, runtime=
878878
runtime = runtime.total_seconds()
879879
if isinstance(dt, delta):
880880
dt = dt.total_seconds()
881-
if dt > 0 and dt <= 1e-6:
881+
if abs(dt) <= 1e-6:
882882
raise ValueError('Time step dt is too small')
883+
if (dt * 1e6) % 1 != 0:
884+
raise ValueError('Output interval should not have finer precision than 1e-6 s')
883885
outputdt = output_file.outputdt if output_file else np.infty
884886
if isinstance(outputdt, delta):
885887
outputdt = outputdt.total_seconds()

pyproject.toml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,21 @@ classifiers = [
1919
"Topic :: Scientific/Engineering",
2020
"Intended Audience :: Science/Research",
2121
]
22+
dependencies = [
23+
"cgen",
24+
"cftime",
25+
"numpy",
26+
"dask",
27+
"cftime",
28+
"psutil",
29+
"netCDF4",
30+
"zarr",
31+
"tqdm",
32+
"pymbolic",
33+
"pytest",
34+
"scipy",
35+
"xarray",
36+
]
2237

2338
[project.urls]
2439
homepage = "https://oceanparcels.org/"

tests/test_data/fieldset_nemo.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
from os import path
2+
3+
import parcels
4+
5+
6+
def create_fieldset(indices=None):
7+
data_path = path.join(path.dirname(__file__))
8+
9+
filenames = {'U': {'lon': path.join(data_path, 'mask_nemo_cross_180lon.nc'),
10+
'lat': path.join(data_path, 'mask_nemo_cross_180lon.nc'),
11+
'data': path.join(data_path, 'Uu_eastward_nemo_cross_180lon.nc')},
12+
'V': {'lon': path.join(data_path, 'mask_nemo_cross_180lon.nc'),
13+
'lat': path.join(data_path, 'mask_nemo_cross_180lon.nc'),
14+
'data': path.join(data_path, 'Vv_eastward_nemo_cross_180lon.nc')}}
15+
variables = {'U': 'U', 'V': 'V'}
16+
dimensions = {'lon': 'glamf', 'lat': 'gphif', 'time': 'time_counter'}
17+
indices = indices or {}
18+
return parcels.FieldSet.from_nemo(filenames, variables, dimensions, indices=indices)
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
from os import path
2+
3+
import parcels
4+
5+
6+
def random_function_name():
7+
data_path = path.join(path.dirname(__file__))
8+
9+
filenames = {'U': {'lon': path.join(data_path, 'mask_nemo_cross_180lon.nc'),
10+
'lat': path.join(data_path, 'mask_nemo_cross_180lon.nc'),
11+
'data': path.join(data_path, 'Uu_eastward_nemo_cross_180lon.nc')},
12+
'V': {'lon': path.join(data_path, 'mask_nemo_cross_180lon.nc'),
13+
'lat': path.join(data_path, 'mask_nemo_cross_180lon.nc'),
14+
'data': path.join(data_path, 'Vv_eastward_nemo_cross_180lon.nc')}}
15+
variables = {'U': 'U', 'V': 'V'}
16+
dimensions = {'lon': 'glamf', 'lat': 'gphif', 'time': 'time_counter'}
17+
return parcels.FieldSet.from_nemo(filenames, variables, dimensions)
18+
19+
20+
def none_returning_function():
21+
return None

0 commit comments

Comments
 (0)