|
241 | 241 | "source": [ |
242 | 242 | "## Merging particles\n", |
243 | 243 | "\n", |
244 | | - "Another type of interaction that is supported is the merging of particles. The supported merging functions also comes with limitations (only mutual-nearest particles can be accurately merged), so this is really just a prototype. Nevertheless, the example below shows the possibilities that merging of particles can provide for more complex simulations.\n", |
245 | | - "\n", |
246 | | - "In the example below, we use two build-in Kernels: `NearestNeighborWithinRange` and `MergeWithNearestNeighbor`.\n" |
| 244 | + "Another type of interaction is the merging of particles. The merging Kernel also comes with limitations (only mutual-nearest particles can be accurately merged), so this is really just a prototype. Nevertheless, the example below shows the possibilities that merging of particles can provide for more complex simulations." |
247 | 245 | ] |
248 | 246 | }, |
249 | 247 | { |
|
253 | 251 | "metadata": {}, |
254 | 252 | "outputs": [], |
255 | 253 | "source": [ |
256 | | - "npart = 800\n", |
| 254 | + "def Merge(particles, fieldset):\n", |
| 255 | + " \"\"\"Kernel that \"merges\" two particles that are within interaction_distance range\n", |
| 256 | + " Particles must have a 'mass' Variable, and during merging the mass of the smallest\n", |
| 257 | + " particle is transferred to the largest particle. The smallest particle is then deleted.\n", |
| 258 | + " \"\"\"\n", |
| 259 | + " interaction_distance = 0.05\n", |
| 260 | + "\n", |
| 261 | + " N = len(particles.lon)\n", |
| 262 | + "\n", |
| 263 | + " # calculate pairwise distances (n_particles × n_particles)\n", |
| 264 | + " dx = particles.lon[None, :] - particles.lon[:, None]\n", |
| 265 | + " dy = particles.lat[None, :] - particles.lat[:, None]\n", |
| 266 | + " distances = np.sqrt(dx**2 + dy**2)\n", |
257 | 267 | "\n", |
258 | | - "X = np.random.uniform(-1, 1, size=npart)\n", |
259 | | - "Y = np.random.uniform(-1, 1, size=npart)\n", |
| 268 | + " # mask distances by interaction range\n", |
| 269 | + " within = (distances > 0) & (distances < interaction_distance)\n", |
| 270 | + " dist_masked = np.where(within, distances, np.inf)\n", |
| 271 | + "\n", |
| 272 | + " # nearest neighbour for each particle (index) and corresponding distance\n", |
| 273 | + " nn = np.argmin(dist_masked, axis=1)\n", |
| 274 | + " nn_dist = dist_masked[np.arange(N), nn]\n", |
| 275 | + " has_nn = np.isfinite(nn_dist)\n", |
| 276 | + "\n", |
| 277 | + " # mutual nearest: nn[nn[i]] == i\n", |
| 278 | + " i_idx = np.arange(N)\n", |
| 279 | + " mutual = has_nn & (nn[nn] == i_idx)\n", |
| 280 | + "\n", |
| 281 | + " # process each mutual pair only once (i < j)\n", |
| 282 | + " pair_mask = mutual & (i_idx < nn)\n", |
| 283 | + " pair_i = i_idx[pair_mask]\n", |
| 284 | + " pair_j = nn[pair_mask]\n", |
| 285 | + "\n", |
| 286 | + " if pair_i.size == 0:\n", |
| 287 | + " return\n", |
| 288 | + "\n", |
| 289 | + " # larger = where mass_j >= mass_i -> j else i\n", |
| 290 | + " mass_i = particles.mass[pair_i]\n", |
| 291 | + " mass_j = particles.mass[pair_j]\n", |
| 292 | + " larger_idx = np.where(mass_j > mass_i, pair_j, pair_i)\n", |
| 293 | + " smaller_idx = np.where(mass_j > mass_i, pair_i, pair_j)\n", |
| 294 | + "\n", |
| 295 | + " # perform transfer and mark deletions\n", |
| 296 | + " # note that we use temporary arrays for indexing because of KernelParticle bug (GH #2143)\n", |
| 297 | + " masses = particles.mass\n", |
| 298 | + " states = particles.state\n", |
| 299 | + "\n", |
| 300 | + " # transfer mass from smaller to larger and mark smaller for deletion\n", |
| 301 | + " masses[larger_idx] += particles.mass[smaller_idx]\n", |
| 302 | + " states[smaller_idx] = parcels.StatusCode.Delete\n", |
| 303 | + "\n", |
| 304 | + " # TODO use particle variables directly KernelParticle bug (GH #2143) is fixed\n", |
| 305 | + " particles.mass = masses\n", |
| 306 | + " particles.state = states" |
| 307 | + ] |
| 308 | + }, |
| 309 | + { |
| 310 | + "cell_type": "code", |
| 311 | + "execution_count": null, |
| 312 | + "id": "10", |
| 313 | + "metadata": {}, |
| 314 | + "outputs": [], |
| 315 | + "source": [ |
| 316 | + "npart = 800\n", |
260 | 317 | "\n", |
261 | | - "# Create custom InteractionParticle class\n", |
262 | | - "# with extra variables nearest_neighbor and mass\n", |
| 318 | + "# Create custom Particle class with extra variable mass\n", |
263 | 319 | "MergeParticle = parcels.Particle.add_variable(\n", |
264 | | - " [\n", |
265 | | - " parcels.Variable(\"nearest_neighbor\", dtype=np.int64, to_write=False),\n", |
266 | | - " parcels.Variable(\"mass\", initial=1, dtype=np.float32),\n", |
267 | | - " ]\n", |
| 320 | + " parcels.Variable(\"mass\", dtype=np.float32)\n", |
268 | 321 | ")\n", |
269 | 322 | "\n", |
270 | 323 | "pset = parcels.ParticleSet(\n", |
271 | 324 | " fieldset=DiffusionFieldSet(),\n", |
272 | 325 | " pclass=MergeParticle,\n", |
273 | | - " lon=X,\n", |
274 | | - " lat=Y,\n", |
| 326 | + " lon=np.random.uniform(-1, 1, size=npart),\n", |
| 327 | + " lat=np.random.uniform(-1, 1, size=npart),\n", |
| 328 | + " mass=np.random.uniform(0.5, 1.5, size=npart),\n", |
275 | 329 | ")\n", |
276 | 330 | "\n", |
277 | 331 | "output_file = parcels.ParticleFile(\n", |
|
281 | 335 | "\n", |
282 | 336 | "kernels = [\n", |
283 | 337 | " parcels.kernels.DiffusionUniformKh,\n", |
284 | | - " # Merge, # Add the Merge kernel defined above\n", |
| 338 | + " Merge, # Add the Merge kernel defined above\n", |
285 | 339 | "]\n", |
286 | 340 | "\n", |
287 | 341 | "pset.execute(\n", |
|
295 | 349 | { |
296 | 350 | "cell_type": "code", |
297 | 351 | "execution_count": null, |
298 | | - "id": "10", |
| 352 | + "id": "11", |
299 | 353 | "metadata": {}, |
300 | 354 | "outputs": [], |
301 | 355 | "source": [ |
|
305 | 359 | " np.nanmin(data_xarray[\"time\"].values),\n", |
306 | 360 | " np.nanmax(data_xarray[\"time\"].values),\n", |
307 | 361 | " np.timedelta64(1, \"s\"),\n", |
308 | | - ") # timerange in nanoseconds\n", |
| 362 | + ")\n", |
309 | 363 | "\n", |
310 | 364 | "fig = plt.figure(figsize=(4, 4), constrained_layout=True)\n", |
311 | 365 | "ax = fig.add_subplot()\n", |
|
350 | 404 | ], |
351 | 405 | "metadata": { |
352 | 406 | "kernelspec": { |
353 | | - "display_name": "Python 3 (ipykernel)", |
| 407 | + "display_name": "test-latest", |
354 | 408 | "language": "python", |
355 | 409 | "name": "python3" |
356 | 410 | }, |
|
364 | 418 | "name": "python", |
365 | 419 | "nbconvert_exporter": "python", |
366 | 420 | "pygments_lexer": "ipython3", |
367 | | - "version": "3.11.6" |
| 421 | + "version": "3.11.0" |
368 | 422 | } |
369 | 423 | }, |
370 | 424 | "nbformat": 4, |
|
0 commit comments