Skip to content

Commit a58bb60

Browse files
Merge pull request #208 from OceanParcels/time_extrapolation_datetime_error
Same arguments for recovery kernels as for normal kernels. Note that after this API change, all recovery kernels will need to be changed in code
2 parents a51b310 + ddb8aec commit a58bb60

File tree

7 files changed

+105
-60
lines changed

7 files changed

+105
-60
lines changed

examples/example_globcurrent.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,3 +78,15 @@ def test_globcurrent_particles(mode):
7878

7979
assert(abs(pset[0].lon - 23.8) < 1)
8080
assert(abs(pset[0].lat - -35.3) < 1)
81+
82+
83+
@pytest.mark.xfail(reason="Time extrapolation error expected to be thrown")
84+
@pytest.mark.parametrize('mode', ['scipy', 'jit'])
85+
def test_globcurrent_time_extrapolation_error(mode):
86+
fieldset = set_globcurrent_fieldset()
87+
88+
pset = ParticleSet(fieldset, pclass=ptype[mode], lon=[25], lat=[-35])
89+
starttime = fieldset.U.time[0]-delta(days=1).total_seconds()
90+
91+
pset.execute(AdvectionRK4, starttime=starttime, runtime=delta(days=1),
92+
dt=delta(minutes=5), interval=delta(hours=1))

examples/example_recursive_errorhandling.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ def TestLon(particle, fieldset, time, dt):
4141
if particle.lon <= fieldset.minlon:
4242
return ErrorCode.Error
4343

44-
def Error_RandomiseLon(particle):
44+
def Error_RandomiseLon(particle, fieldset, time, dt):
4545
"""Error handling kernel that draws a new longitude.
4646
Note that this new longitude can be smaller than fieldset.minlon"""
4747
particle.lon = random.uniform(0., 1.)

examples/tutorial_Agulhasparticles.ipynb

Lines changed: 58 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,30 @@
22
"cells": [
33
{
44
"cell_type": "markdown",
5-
"metadata": {},
5+
"metadata": {
6+
"deletable": true,
7+
"editable": true
8+
},
69
"source": [
710
"## Tutorial showing how to create Parcels in Agulhas animated gif"
811
]
912
},
1013
{
1114
"cell_type": "markdown",
12-
"metadata": {},
15+
"metadata": {
16+
"deletable": true,
17+
"editable": true
18+
},
1319
"source": [
1420
"This brief tutorial shows how to recreate the [animated gif](http://oceanparcels.org/animated-gifs/globcurrent_fullyseeded.gif) showing particles in the Agulhas region south of Africa."
1521
]
1622
},
1723
{
1824
"cell_type": "markdown",
19-
"metadata": {},
25+
"metadata": {
26+
"deletable": true,
27+
"editable": true
28+
},
2029
"source": [
2130
"We start with importing the relevant modules"
2231
]
@@ -25,7 +34,9 @@
2534
"cell_type": "code",
2635
"execution_count": 1,
2736
"metadata": {
28-
"collapsed": true
37+
"collapsed": true,
38+
"deletable": true,
39+
"editable": true
2940
},
3041
"outputs": [],
3142
"source": [
@@ -36,15 +47,22 @@
3647
},
3748
{
3849
"cell_type": "markdown",
39-
"metadata": {},
50+
"metadata": {
51+
"deletable": true,
52+
"editable": true
53+
},
4054
"source": [
4155
"Now load the Globcurrent fields from the `GlobCurrent_example_data` directory"
4256
]
4357
},
4458
{
4559
"cell_type": "code",
4660
"execution_count": 2,
47-
"metadata": {},
61+
"metadata": {
62+
"collapsed": false,
63+
"deletable": true,
64+
"editable": true
65+
},
4866
"outputs": [
4967
{
5068
"name": "stderr",
@@ -69,15 +87,22 @@
6987
},
7088
{
7189
"cell_type": "markdown",
72-
"metadata": {},
90+
"metadata": {
91+
"deletable": true,
92+
"editable": true
93+
},
7394
"source": [
7495
"Now create vectors of Longitude and Latitude starting locations on a regular mesh, and use these to initialise a `ParticleSet` object."
7596
]
7697
},
7798
{
7899
"cell_type": "code",
79100
"execution_count": 3,
80-
"metadata": {},
101+
"metadata": {
102+
"collapsed": true,
103+
"deletable": true,
104+
"editable": true
105+
},
81106
"outputs": [],
82107
"source": [
83108
"lons, lats = np.meshgrid(range(15, 35), range(-40, -30))\n",
@@ -86,7 +111,10 @@
86111
},
87112
{
88113
"cell_type": "markdown",
89-
"metadata": {},
114+
"metadata": {
115+
"deletable": true,
116+
"editable": true
117+
},
90118
"source": [
91119
"Now we want to advect the particles. However, the Globcurrent data that we loaded in is only for a limited, regional domain and particles might be able to leave this domain. We therefore need to tell Parcels that particles that leave the domain need to be deleted. We do that using a `Recovery Kernel`, which will be invoked when a particle encounters an `ErrorOutOfBounds` error:"
92120
]
@@ -95,48 +123,39 @@
95123
"cell_type": "code",
96124
"execution_count": 4,
97125
"metadata": {
98-
"collapsed": true
126+
"collapsed": true,
127+
"deletable": true,
128+
"editable": true
99129
},
100130
"outputs": [],
101131
"source": [
102-
"def DeleteParticle(particle):\n",
132+
"def DeleteParticle(particle, fieldset, time, dt):\n",
103133
" particle.delete()"
104134
]
105135
},
106136
{
107137
"cell_type": "markdown",
108-
"metadata": {},
138+
"metadata": {
139+
"deletable": true,
140+
"editable": true
141+
},
109142
"source": [
110143
"Now we can advect the particles. Note that we do this inside a `for`-loop, so we can save a plot every six hours (which is the value of `runtime`). See the [plotting tutorial](http://nbviewer.jupyter.org/github/OceanParcels/parcels/blob/master/examples/tutorial_plotting.ipynb) for more information on the `pset.show()` method."
111144
]
112145
},
113146
{
114147
"cell_type": "code",
115148
"execution_count": 5,
116-
"metadata": {},
149+
"metadata": {
150+
"collapsed": false,
151+
"deletable": true,
152+
"editable": true
153+
},
117154
"outputs": [
118155
{
119156
"name": "stderr",
120157
"output_type": "stream",
121158
"text": [
122-
"/opt/local/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/site-packages/mpl_toolkits/basemap/__init__.py:1767: MatplotlibDeprecationWarning: The get_axis_bgcolor function was deprecated in version 2.0. Use get_facecolor instead.\n",
123-
" axisbgc = ax.get_axis_bgcolor()\n",
124-
"/Users/erik/Codes/PARCELScode/parcels/particleset.py:402: RuntimeWarning: invalid value encountered in divide\n",
125-
" normU = U/speed\n",
126-
"/Users/erik/Codes/PARCELScode/parcels/particleset.py:403: RuntimeWarning: invalid value encountered in divide\n",
127-
" normV = V/speed\n",
128-
"/opt/local/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/site-packages/mpl_toolkits/basemap/__init__.py:3707: MatplotlibDeprecationWarning: The ishold function was deprecated in version 2.0.\n",
129-
" b = ax.ishold()\n",
130-
"/opt/local/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/site-packages/mpl_toolkits/basemap/__init__.py:3716: MatplotlibDeprecationWarning: axes.hold is deprecated.\n",
131-
" See the API Changes document (http://matplotlib.org/api/api_changes.html)\n",
132-
" for more details.\n",
133-
" ax.hold(b)\n",
134-
"/opt/local/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/site-packages/mpl_toolkits/basemap/__init__.py:3222: MatplotlibDeprecationWarning: The ishold function was deprecated in version 2.0.\n",
135-
" b = ax.ishold()\n",
136-
"/opt/local/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/site-packages/mpl_toolkits/basemap/__init__.py:3231: MatplotlibDeprecationWarning: axes.hold is deprecated.\n",
137-
" See the API Changes document (http://matplotlib.org/api/api_changes.html)\n",
138-
" for more details.\n",
139-
" ax.hold(b)\n",
140159
"INFO: Plot saved to particles00.png\n",
141160
"INFO: Compiled JITParticleAdvectionRK4 ==> /var/folders/r2/8593q8z93kd7t4j9kbb_f7p00000gr/T/parcels-504/27805ff3aa34ba12ddb373f3f2cb1d1b.so\n",
142161
"INFO: Plot saved to particles01.png\n",
@@ -159,14 +178,20 @@
159178
},
160179
{
161180
"cell_type": "markdown",
162-
"metadata": {},
181+
"metadata": {
182+
"deletable": true,
183+
"editable": true
184+
},
163185
"source": [
164186
"This now has created 3 plots. Note that the original animated gif contained 20 plots, but to keep running of this notebook fast we have reduced the number here. Of course, it is trivial to increase the number of plots by changing the value in the `range()` in the cell above."
165187
]
166188
},
167189
{
168190
"cell_type": "markdown",
169-
"metadata": {},
191+
"metadata": {
192+
"deletable": true,
193+
"editable": true
194+
},
170195
"source": [
171196
"As a final step, you can use [ImageMagick](http://www.imagemagick.org/script/index.php) or an online tool to stitch these individual plots together in an animated gif."
172197
]

parcels/field.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,10 @@ class TimeExtrapolationError(RuntimeError):
3232
"""Utility error class to propagate erroneous time extrapolation sampling"""
3333

3434
def __init__(self, time, field=None):
35-
self.field = field
36-
self.time = time
37-
message = "%s sampled outside time domain at time %f." % (
38-
field.name if field else "Field", self.time
39-
)
35+
if field is not None and field.time_origin != 0:
36+
time = field.time_origin + timedelta(seconds=time)
37+
message = "%s sampled outside time domain at time %s." % (
38+
field.name if field else "Field", time)
4039
message += " Try setting allow_time_extrapolation to True"
4140
super(TimeExtrapolationError, self).__init__(message)
4241

parcels/kernel.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,7 @@ def remove_deleted(pset):
190190
for p in error_particles:
191191
recovery_kernel = recovery_map[p.state]
192192
p.state = ErrorCode.Success
193-
recovery_kernel(p)
193+
recovery_kernel(p, self.fieldset, p.time, dt)
194194

195195
# Remove all particles that signalled deletion
196196
remove_deleted(pset)

parcels/kernels/error.py

Lines changed: 27 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"""Collection of pre-built recovery kernels"""
22
from enum import IntEnum
3+
from datetime import timedelta
34

45

56
__all__ = ['ErrorCode', 'KernelError', 'OutOfBoundsError', 'recovery_map']
@@ -17,61 +18,69 @@ class ErrorCode(IntEnum):
1718
class KernelError(RuntimeError):
1819
"""General particle kernel error with optional custom message"""
1920

20-
def __init__(self, particle, msg=None):
21-
message = ("%s\nParticle %s\nTime time: %f,\ttimestep size: %f\n") % (
22-
particle.state, particle, particle.time, particle.dt
21+
def __init__(self, particle, fieldset=None, msg=None):
22+
message = ("%s\nParticle %s\nTime: %s,\ttimestep dt: %f\n") % (
23+
particle.state, particle, parse_particletime(particle.time, fieldset),
24+
particle.dt
2325
)
2426
if msg:
2527
message += msg
2628
super(KernelError, self).__init__(message)
2729

2830

29-
def recovery_kernel_error(particle):
31+
def parse_particletime(time, fieldset):
32+
if fieldset is not None and fieldset.U.time_origin != 0:
33+
# TODO assuming that error was thrown on U field
34+
time = fieldset.U.time_origin + timedelta(seconds=time)
35+
return time
36+
37+
38+
def recovery_kernel_error(particle, fieldset, time, dt):
3039
"""Default error kernel that throws exception"""
3140
msg = "Error: %s" % particle.exception if particle.exception else None
32-
raise KernelError(particle, msg=msg)
41+
raise KernelError(particle, fieldset=fieldset, msg=msg)
3342

3443

3544
class OutOfBoundsError(KernelError):
3645
"""Particle kernel error for out-of-bounds field sampling"""
3746

38-
def __init__(self, particle, lon=None, lat=None, depth=None, field=None):
47+
def __init__(self, particle, fieldset=None, lon=None, lat=None, depth=None):
3948
if lon and lat:
40-
message = "%s sampled at (%f, %f, %f)" % (
41-
field.name if field else "Field", lon, lat, depth
49+
message = "Field sampled at (%f, %f, %f)" % (
50+
lon, lat, depth
4251
)
4352
else:
4453
message = "Out-of-bounds sampling by particle at (%f, %f, %f)" % (
4554
particle.lon, particle.lat, particle.depth
4655
)
47-
super(OutOfBoundsError, self).__init__(particle, msg=message)
56+
super(OutOfBoundsError, self).__init__(particle, fieldset=fieldset, msg=message)
4857

4958

5059
class OutOfTimeError(KernelError):
5160
"""Particle kernel error for time extrapolation field sampling"""
5261

53-
def __init__(self, particle):
54-
message = "Field sampled outside time domain at time %f." % (
55-
particle.time
62+
def __init__(self, particle, fieldset):
63+
message = "Field sampled outside time domain at time %s." % (
64+
parse_particletime(particle.time, fieldset)
5665
)
5766
message += " Try setting allow_time_extrapolation to True"
58-
super(OutOfTimeError, self).__init__(particle, msg=message)
67+
super(OutOfTimeError, self).__init__(particle, fieldset=fieldset, msg=message)
5968

6069

61-
def recovery_kernel_out_of_bounds(particle):
70+
def recovery_kernel_out_of_bounds(particle, fieldset, time, dt):
6271
"""Default sampling error kernel that throws OutOfBoundsError"""
6372
if particle.exception is None:
6473
# TODO: JIT does not yet provide the context that created
6574
# the exception. We need to pass that info back from C.
66-
raise OutOfBoundsError(particle)
75+
raise OutOfBoundsError(particle, fieldset)
6776
else:
6877
error = particle.exception
69-
raise OutOfBoundsError(particle, error.x, error.y, error.z, error.field)
78+
raise OutOfBoundsError(particle, fieldset, error.x, error.y, error.z)
7079

7180

72-
def recovery_kernel_time_extrapolation(particle):
81+
def recovery_kernel_time_extrapolation(particle, fieldset, time, dt):
7382
"""Default sampling error kernel that throws OutOfTimeError"""
74-
raise OutOfTimeError(particle)
83+
raise OutOfTimeError(particle, fieldset)
7584

7685

7786
# Default mapping of failure types (KernelOp)

tests/test_kernel_execution.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ def MoveRight(particle, fieldset, time, dt):
128128
fieldset.U[time, particle.lon + 0.1, particle.lat, particle.depth]
129129
particle.lon += 0.1
130130

131-
def MoveLeft(particle):
131+
def MoveLeft(particle, fieldset, time, dt):
132132
particle.lon -= 1.
133133

134134
lon = np.linspace(0.05, 0.95, npart, dtype=np.float32)
@@ -147,7 +147,7 @@ def MoveRight(particle, fieldset, time, dt):
147147
fieldset.U[time, particle.lon + 0.1, particle.lat, particle.depth]
148148
particle.lon += 0.1
149149

150-
def DeleteMe(particle):
150+
def DeleteMe(particle, fieldset, time, dt):
151151
particle.delete()
152152

153153
lon = np.linspace(0.05, 0.95, npart, dtype=np.float32)

0 commit comments

Comments
 (0)