|
15 | 15 | "id": "1", |
16 | 16 | "metadata": {}, |
17 | 17 | "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", |
19 | 19 | "\n", |
20 | | - "- Particle-particle interaction only works in Scipy mode\n", |
21 | 20 | "- The type of interactions that are supported is still limited\n", |
22 | 21 | "\n", |
23 | 22 | "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 | 53 | "metadata": {}, |
55 | 54 | "outputs": [], |
56 | 55 | "source": [ |
57 | | - "%matplotlib notebook\n", |
58 | 56 | "import matplotlib.pyplot as plt\n", |
59 | 57 | "import numpy as np\n", |
60 | 58 | "import xarray as xr\n", |
61 | | - "from IPython.display import HTML\n", |
62 | 59 | "from matplotlib.animation import FuncAnimation\n", |
63 | 60 | "\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" |
65 | 65 | ] |
66 | 66 | }, |
67 | 67 | { |
|
71 | 71 | "metadata": {}, |
72 | 72 | "outputs": [], |
73 | 73 | "source": [ |
74 | | - "def Pull(particle, fieldset, time, neighbors, mutator):\n", |
| 74 | + "def Pull(particles, fieldset):\n", |
75 | 75 | " \"\"\"InterActionKernel that \"pulls\" all neighbor particles\n", |
76 | 76 | " toward the attracting particle with a constant velocity\"\"\"\n", |
77 | 77 | " distances = []\n", |
78 | 78 | " na_neighbors = []\n", |
79 | 79 | " # only execute kernel for particles that are 'attractor'\n", |
80 | | - " if not particle.attractor:\n", |
| 80 | + " if not particles.attractor:\n", |
81 | 81 | " return StateCode.Success\n", |
82 | 82 | " for n in neighbors:\n", |
83 | 83 | " if n.attractor:\n", |
|
119 | 119 | "id": "5", |
120 | 120 | "metadata": {}, |
121 | 121 | "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": [], |
122 | 144 | "source": [ |
123 | 145 | "npart = 11\n", |
124 | | - "\n", |
125 | 146 | "X, Y = np.meshgrid(np.linspace(-1, 1, npart), np.linspace(-1, 1, npart))\n", |
126 | 147 | "\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", |
135 | 148 | "# Create custom particle class with extra variable that indicates\n", |
136 | 149 | "# whether the interaction kernel should be executed on this particle.\n", |
137 | 150 | "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", |
139 | 152 | ")\n", |
140 | 153 | "\n", |
141 | | - "\n", |
142 | 154 | "attractor = [\n", |
143 | 155 | " True if i in [int(npart * npart / 2 - 3), int(npart * npart / 2 + 3)] else False\n", |
144 | 156 | " for i in range(npart * npart)\n", |
145 | 157 | "]\n", |
| 158 | + "\n", |
146 | 159 | "pset = parcels.ParticleSet(\n", |
147 | | - " fieldset=fieldset,\n", |
| 160 | + " fieldset=DiffusionFieldSet(),\n", |
148 | 161 | " pclass=InteractingParticle,\n", |
149 | 162 | " lon=X,\n", |
150 | 163 | " lat=Y,\n", |
151 | | - " interaction_distance=0.5, # note the interaction_distance argument here\n", |
152 | 164 | " attractor=attractor,\n", |
153 | 165 | ")\n", |
154 | 166 | "\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", |
156 | 176 | "\n", |
157 | 177 | "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", |
162 | 181 | " output_file=output_file,\n", |
163 | 182 | ")" |
164 | 183 | ] |
165 | 184 | }, |
166 | 185 | { |
167 | 186 | "cell_type": "code", |
168 | 187 | "execution_count": null, |
169 | | - "id": "6", |
| 188 | + "id": "7", |
170 | 189 | "metadata": {}, |
171 | 190 | "outputs": [], |
172 | 191 | "source": [ |
173 | | - "%%capture\n", |
174 | 192 | "data_xarray = xr.open_zarr(\"InteractingParticles.zarr\")\n", |
175 | 193 | "data_attr = data_xarray.where(data_xarray[\"attractor\"].compute() == 1, drop=True)\n", |
176 | 194 | "data_other = data_xarray.where(data_xarray[\"attractor\"].compute() == 0, drop=True)\n", |
177 | 195 | "\n", |
178 | 196 | "timerange = np.arange(\n", |
179 | 197 | " np.nanmin(data_xarray[\"time\"].values),\n", |
180 | 198 | " np.nanmax(data_xarray[\"time\"].values),\n", |
181 | | - " np.timedelta64(1, \"s\"), # timerange in nanoseconds\n", |
| 199 | + " np.timedelta64(1, \"s\"),\n", |
182 | 200 | ")\n", |
183 | 201 | "\n", |
184 | 202 | "fig = plt.figure(figsize=(4, 4), constrained_layout=True)\n", |
|
189 | 207 | "ax.set_xlim(-1.1, 1.1)\n", |
190 | 208 | "ax.set_ylim(-1.1, 1.1)\n", |
191 | 209 | "\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", |
198 | 212 | "\n", |
199 | 213 | "scatter = ax.scatter(\n", |
200 | 214 | " data_other[\"lon\"].values[time_id],\n", |
|
247 | 261 | " data_attr[\"lat\"].values[time_id_attr],\n", |
248 | 262 | " ):\n", |
249 | 263 | " c.center = (lon_a, lat_a)\n", |
250 | | - " return (\n", |
251 | | - " scatter,\n", |
252 | | - " scatter_attr,\n", |
253 | | - " circs,\n", |
254 | | - " )\n", |
255 | 264 | "\n", |
256 | 265 | "\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" |
269 | 270 | ] |
270 | 271 | }, |
271 | 272 | { |
|
293 | 294 | "X = np.random.uniform(-1, 1, size=npart)\n", |
294 | 295 | "Y = np.random.uniform(-1, 1, size=npart)\n", |
295 | 296 | "\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", |
304 | 297 | "# Create custom InteractionParticle class\n", |
305 | 298 | "# with extra variables nearest_neighbor and mass\n", |
306 | | - "MergeParticle = parcels.InteractionParticle.add_variables(\n", |
| 299 | + "MergeParticle = parcels.Particle.add_variable(\n", |
307 | 300 | " [\n", |
308 | 301 | " parcels.Variable(\"nearest_neighbor\", dtype=np.int64, to_write=False),\n", |
309 | 302 | " parcels.Variable(\"mass\", initial=1, dtype=np.float32),\n", |
310 | 303 | " ]\n", |
311 | 304 | ")\n", |
312 | 305 | "\n", |
313 | 306 | "pset = parcels.ParticleSet(\n", |
314 | | - " fieldset=fieldset,\n", |
| 307 | + " fieldset=DiffusionFieldSet(),\n", |
315 | 308 | " pclass=MergeParticle,\n", |
316 | 309 | " lon=X,\n", |
317 | 310 | " lat=Y,\n", |
318 | | - " interaction_distance=0.05, # note this argument here\n", |
319 | 311 | ")\n", |
320 | 312 | "\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", |
322 | 322 | "\n", |
323 | 323 | "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", |
329 | 327 | " output_file=output_file,\n", |
330 | 328 | ")" |
331 | 329 | ] |
|
337 | 335 | "metadata": {}, |
338 | 336 | "outputs": [], |
339 | 337 | "source": [ |
340 | | - "%%capture\n", |
341 | 338 | "data_xarray = xr.open_zarr(\"MergingParticles.zarr\")\n", |
342 | 339 | "\n", |
343 | 340 | "timerange = np.arange(\n", |
|
354 | 351 | "ax.set_xlim(-1.1, 1.1)\n", |
355 | 352 | "ax.set_ylim(-1.1, 1.1)\n", |
356 | 353 | "\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", |
360 | 355 | "\n", |
361 | 356 | "scatter = ax.scatter(\n", |
362 | 357 | " data_xarray[\"lon\"].values[time_id],\n", |
|
383 | 378 | " return (scatter,)\n", |
384 | 379 | "\n", |
385 | 380 | "\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" |
409 | 384 | ] |
410 | 385 | } |
411 | 386 | ], |
|
0 commit comments