Skip to content

Commit d104573

Browse files
Making interaction tutorial v4-compatible
Not yet touching the Interaction kernels themselves...
1 parent 54fbc40 commit d104573

1 file changed

Lines changed: 70 additions & 95 deletions

File tree

docs/user_guide/examples_v3/tutorial_interaction.ipynb

Lines changed: 70 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,8 @@
1515
"id": "1",
1616
"metadata": {},
1717
"source": [
18-
"In this notebook, we show an example of the new 'particle-particle-interaction' functionality in Parcels. Note that this functionality is still in development, and the implementation is fairly rudimentary and slow for now. Importantly:\n",
18+
"In this notebook, we show an example of the new 'particle-particle-interaction' functionality in Parcels. Note that this functionality is still in development, and the implementation is fairly rudimentary for now. Importantly:\n",
1919
"\n",
20-
"- Particle-particle interaction only works in Scipy mode\n",
2120
"- The type of interactions that are supported is still limited\n",
2221
"\n",
2322
"Interactions are implemented through `InteractionKernels`, which are similar to normal `Kernels`. The `InteractionKernels` are applied between particles that are located closer to each other than a specified `interaction_distance`. In general, the code structure needs three adaptations to apply particle-particle interaction:\n",
@@ -54,14 +53,15 @@
5453
"metadata": {},
5554
"outputs": [],
5655
"source": [
57-
"%matplotlib notebook\n",
5856
"import matplotlib.pyplot as plt\n",
5957
"import numpy as np\n",
6058
"import xarray as xr\n",
61-
"from IPython.display import HTML\n",
6259
"from matplotlib.animation import FuncAnimation\n",
6360
"\n",
64-
"import parcels"
61+
"plt.rcParams[\"animation.html\"] = \"jshtml\"\n",
62+
"\n",
63+
"import parcels\n",
64+
"from parcels._datasets.structured.generated import simple_UV_dataset"
6565
]
6666
},
6767
{
@@ -71,13 +71,13 @@
7171
"metadata": {},
7272
"outputs": [],
7373
"source": [
74-
"def Pull(particle, fieldset, time, neighbors, mutator):\n",
74+
"def Pull(particles, fieldset):\n",
7575
" \"\"\"InterActionKernel that \"pulls\" all neighbor particles\n",
7676
" toward the attracting particle with a constant velocity\"\"\"\n",
7777
" distances = []\n",
7878
" na_neighbors = []\n",
7979
" # only execute kernel for particles that are 'attractor'\n",
80-
" if not particle.attractor:\n",
80+
" if not particles.attractor:\n",
8181
" return StateCode.Success\n",
8282
" for n in neighbors:\n",
8383
" if n.attractor:\n",
@@ -119,66 +119,84 @@
119119
"id": "5",
120120
"metadata": {},
121121
"outputs": [],
122+
"source": [
123+
"def DiffusionFieldSet():\n",
124+
" \"\"\"Define a fieldset without flow but with diffusion\"\"\"\n",
125+
" ds = simple_UV_dataset(dims=(1, 1, 1, 1), mesh=\"flat\")\n",
126+
" ds = ds.assign_coords(time=ds.time - ds.time[0]) # set time to timedelta\n",
127+
" ds[\"time\"].attrs.update({\"axis\": \"T\"})\n",
128+
" grid = parcels.XGrid.from_dataset(ds, mesh=\"flat\")\n",
129+
" U = parcels.Field(\"U\", ds[\"U\"], grid, interp_method=parcels.interpolators.XLinear)\n",
130+
" V = parcels.Field(\"V\", ds[\"V\"], grid, interp_method=parcels.interpolators.XLinear)\n",
131+
" UV = parcels.VectorField(\"UV\", U, V)\n",
132+
" fieldset = parcels.FieldSet([U, V, UV])\n",
133+
" fieldset.add_constant_field(\"Kh_zonal\", 0.0005, mesh=\"flat\")\n",
134+
" fieldset.add_constant_field(\"Kh_meridional\", 0.0005, mesh=\"flat\")\n",
135+
" return fieldset"
136+
]
137+
},
138+
{
139+
"cell_type": "code",
140+
"execution_count": null,
141+
"id": "6",
142+
"metadata": {},
143+
"outputs": [],
122144
"source": [
123145
"npart = 11\n",
124-
"\n",
125146
"X, Y = np.meshgrid(np.linspace(-1, 1, npart), np.linspace(-1, 1, npart))\n",
126147
"\n",
127-
"# Define a fieldset without flow\n",
128-
"fieldset = parcels.FieldSet.from_data(\n",
129-
" {\"U\": 0, \"V\": 0}, {\"lon\": 0, \"lat\": 0}, mesh=\"flat\"\n",
130-
")\n",
131-
"fieldset.add_constant_field(\"Kh_zonal\", 0.0005, mesh=\"flat\")\n",
132-
"fieldset.add_constant_field(\"Kh_meridional\", 0.0005, mesh=\"flat\")\n",
133-
"\n",
134-
"\n",
135148
"# Create custom particle class with extra variable that indicates\n",
136149
"# whether the interaction kernel should be executed on this particle.\n",
137150
"InteractingParticle = parcels.Particle.add_variable(\n",
138-
" \"attractor\", dtype=np.bool_, to_write=\"once\"\n",
151+
" parcels.Variable(\"attractor\", dtype=np.bool_, to_write=\"once\"),\n",
139152
")\n",
140153
"\n",
141-
"\n",
142154
"attractor = [\n",
143155
" True if i in [int(npart * npart / 2 - 3), int(npart * npart / 2 + 3)] else False\n",
144156
" for i in range(npart * npart)\n",
145157
"]\n",
158+
"\n",
146159
"pset = parcels.ParticleSet(\n",
147-
" fieldset=fieldset,\n",
160+
" fieldset=DiffusionFieldSet(),\n",
148161
" pclass=InteractingParticle,\n",
149162
" lon=X,\n",
150163
" lat=Y,\n",
151-
" interaction_distance=0.5, # note the interaction_distance argument here\n",
152164
" attractor=attractor,\n",
153165
")\n",
154166
"\n",
155-
"output_file = pset.ParticleFile(name=\"InteractingParticles.zarr\", outputdt=1)\n",
167+
"output_file = parcels.ParticleFile(\n",
168+
" store=\"InteractingParticles.zarr\",\n",
169+
" outputdt=np.timedelta64(1, \"s\"),\n",
170+
")\n",
171+
"\n",
172+
"kernels = [\n",
173+
" parcels.kernels.DiffusionUniformKh,\n",
174+
" # Pull, # Add the Pull kernel defined above\n",
175+
"]\n",
156176
"\n",
157177
"pset.execute(\n",
158-
" pyfunc=parcels.kernels.DiffusionUniformKh,\n",
159-
" pyfunc_inter=Pull, # note the pyfunc_inter here\n",
160-
" runtime=60,\n",
161-
" dt=1,\n",
178+
" pyfunc=kernels,\n",
179+
" runtime=np.timedelta64(60, \"s\"),\n",
180+
" dt=np.timedelta64(1, \"s\"),\n",
162181
" output_file=output_file,\n",
163182
")"
164183
]
165184
},
166185
{
167186
"cell_type": "code",
168187
"execution_count": null,
169-
"id": "6",
188+
"id": "7",
170189
"metadata": {},
171190
"outputs": [],
172191
"source": [
173-
"%%capture\n",
174192
"data_xarray = xr.open_zarr(\"InteractingParticles.zarr\")\n",
175193
"data_attr = data_xarray.where(data_xarray[\"attractor\"].compute() == 1, drop=True)\n",
176194
"data_other = data_xarray.where(data_xarray[\"attractor\"].compute() == 0, drop=True)\n",
177195
"\n",
178196
"timerange = np.arange(\n",
179197
" np.nanmin(data_xarray[\"time\"].values),\n",
180198
" np.nanmax(data_xarray[\"time\"].values),\n",
181-
" np.timedelta64(1, \"s\"), # timerange in nanoseconds\n",
199+
" np.timedelta64(1, \"s\"),\n",
182200
")\n",
183201
"\n",
184202
"fig = plt.figure(figsize=(4, 4), constrained_layout=True)\n",
@@ -189,12 +207,8 @@
189207
"ax.set_xlim(-1.1, 1.1)\n",
190208
"ax.set_ylim(-1.1, 1.1)\n",
191209
"\n",
192-
"time_id = np.where(\n",
193-
" data_other[\"time\"] == timerange[0]\n",
194-
") # Indices of the data where time = 0\n",
195-
"time_id_attr = np.where(\n",
196-
" data_attr[\"time\"] == timerange[0]\n",
197-
") # Indices of the data where time = 0\n",
210+
"time_id = np.where(data_other[\"time\"] == timerange[0])\n",
211+
"time_id_attr = np.where(data_attr[\"time\"] == timerange[0])\n",
198212
"\n",
199213
"scatter = ax.scatter(\n",
200214
" data_other[\"lon\"].values[time_id],\n",
@@ -247,25 +261,12 @@
247261
" data_attr[\"lat\"].values[time_id_attr],\n",
248262
" ):\n",
249263
" c.center = (lon_a, lat_a)\n",
250-
" return (\n",
251-
" scatter,\n",
252-
" scatter_attr,\n",
253-
" circs,\n",
254-
" )\n",
255264
"\n",
256265
"\n",
257-
"anim = FuncAnimation(fig, animate, frames=len(timerange), interval=200, blit=True)\n",
258-
"data_xarray.close()"
259-
]
260-
},
261-
{
262-
"cell_type": "code",
263-
"execution_count": null,
264-
"id": "7",
265-
"metadata": {},
266-
"outputs": [],
267-
"source": [
268-
"HTML(anim.to_jshtml())"
266+
"# Create animation\n",
267+
"anim = FuncAnimation(fig, animate, frames=len(timerange), interval=100)\n",
268+
"plt.close(fig)\n",
269+
"anim"
269270
]
270271
},
271272
{
@@ -293,39 +294,36 @@
293294
"X = np.random.uniform(-1, 1, size=npart)\n",
294295
"Y = np.random.uniform(-1, 1, size=npart)\n",
295296
"\n",
296-
"# Define a fieldset without flow\n",
297-
"fieldset = parcels.FieldSet.from_data(\n",
298-
" {\"U\": 0, \"V\": 0}, {\"lon\": 0, \"lat\": 0}, mesh=\"flat\"\n",
299-
")\n",
300-
"fieldset.add_constant_field(\"Kh_zonal\", 0.0005, mesh=\"flat\")\n",
301-
"fieldset.add_constant_field(\"Kh_meridional\", 0.0005, mesh=\"flat\")\n",
302-
"\n",
303-
"\n",
304297
"# Create custom InteractionParticle class\n",
305298
"# with extra variables nearest_neighbor and mass\n",
306-
"MergeParticle = parcels.InteractionParticle.add_variables(\n",
299+
"MergeParticle = parcels.Particle.add_variable(\n",
307300
" [\n",
308301
" parcels.Variable(\"nearest_neighbor\", dtype=np.int64, to_write=False),\n",
309302
" parcels.Variable(\"mass\", initial=1, dtype=np.float32),\n",
310303
" ]\n",
311304
")\n",
312305
"\n",
313306
"pset = parcels.ParticleSet(\n",
314-
" fieldset=fieldset,\n",
307+
" fieldset=DiffusionFieldSet(),\n",
315308
" pclass=MergeParticle,\n",
316309
" lon=X,\n",
317310
" lat=Y,\n",
318-
" interaction_distance=0.05, # note this argument here\n",
319311
")\n",
320312
"\n",
321-
"output_file = pset.ParticleFile(name=\"MergingParticles.zarr\", outputdt=1)\n",
313+
"output_file = parcels.ParticleFile(\n",
314+
" store=\"MergingParticles.zarr\",\n",
315+
" outputdt=np.timedelta64(1, \"s\"),\n",
316+
")\n",
317+
"\n",
318+
"kernels = [\n",
319+
" parcels.kernels.DiffusionUniformKh,\n",
320+
" # Merge, # Add the Merge kernel defined above\n",
321+
"]\n",
322322
"\n",
323323
"pset.execute(\n",
324-
" pyfunc=parcels.kernels.DiffusionUniformKh,\n",
325-
" pyfunc_inter=pset.InteractionKernel(parcels.NearestNeighborWithinRange)\n",
326-
" + parcels.MergeWithNearestNeighbor, # note the pyfunc_inter here\n",
327-
" runtime=60,\n",
328-
" dt=1,\n",
324+
" pyfunc=kernels,\n",
325+
" runtime=np.timedelta64(60, \"s\"),\n",
326+
" dt=np.timedelta64(1, \"s\"),\n",
329327
" output_file=output_file,\n",
330328
")"
331329
]
@@ -337,7 +335,6 @@
337335
"metadata": {},
338336
"outputs": [],
339337
"source": [
340-
"%%capture\n",
341338
"data_xarray = xr.open_zarr(\"MergingParticles.zarr\")\n",
342339
"\n",
343340
"timerange = np.arange(\n",
@@ -354,9 +351,7 @@
354351
"ax.set_xlim(-1.1, 1.1)\n",
355352
"ax.set_ylim(-1.1, 1.1)\n",
356353
"\n",
357-
"time_id = np.where(\n",
358-
" data_xarray[\"time\"] == timerange[0]\n",
359-
") # Indices of the data where time = 0\n",
354+
"time_id = np.where(data_xarray[\"time\"] == timerange[0])\n",
360355
"\n",
361356
"scatter = ax.scatter(\n",
362357
" data_xarray[\"lon\"].values[time_id],\n",
@@ -383,29 +378,9 @@
383378
" return (scatter,)\n",
384379
"\n",
385380
"\n",
386-
"anim = FuncAnimation(fig, animate, frames=len(timerange), interval=200, blit=True)\n",
387-
"data_xarray.close()"
388-
]
389-
},
390-
{
391-
"cell_type": "code",
392-
"execution_count": null,
393-
"id": "11",
394-
"metadata": {},
395-
"outputs": [],
396-
"source": [
397-
"HTML(anim.to_jshtml())"
398-
]
399-
},
400-
{
401-
"attachments": {},
402-
"cell_type": "markdown",
403-
"id": "12",
404-
"metadata": {},
405-
"source": [
406-
"## Interacting with the FieldSet\n",
407-
"\n",
408-
"An important feature of Parcels is to evaluate a `Field` at the `Particle` location using the square bracket notation: `particle.Temperature = fieldset.T[time, depth, lat, lon]`. These types of particle-field interactions are recommended to be implemented in standard `Kernels`, since the `InteractionKernels` do not report the `StateCodes` that are used to flag particles that encounter an error in the particle-field interaction, e.g. `OutOfBoundsError`. Any variable that is needed in the particle-particle interaction can be stored in a `Variable` by sampling the field in a `Kernel` before executing the `InteractionKernel`.\n"
381+
"anim = FuncAnimation(fig, animate, frames=len(timerange), interval=100)\n",
382+
"plt.close(fig)\n",
383+
"anim"
409384
]
410385
}
411386
],

0 commit comments

Comments
 (0)