Parcels: Lagrangian Particle Tracking Framework

repository·main·Indexed 43 days ago

https://github.com/parcels-code/parcels

A Python-based framework for Lagrangian tracking of virtual ocean particles. Parcels simulates the movement of passive and active particulates, such as water, plankton, plastic, and fish, using gridded output from ocean circulation models.

Tokens
48K
Snippets
131
Records
187
Agent score
56%

What's inside Parcels

  1. Overview of Parcels

    main

    Parcels is a Python framework designed for creating customizable particle tracking simulations. It uses gridded output from (ocean) circulation models to track both passive and active particulates, such as water, plankton, plastic, and fish.

    Key features include:

    • Lagrangian ocean analysis framework.
    • Support for large-scale ocean flows.
    • Ability to simulate various types of particles (passive or active).
  2. Overview of Parcels for particle tracking

    main

    Parcels is a Python-based framework designed for creating customizable particle tracking simulations. It uses gridded output from (ocean) circulation models to advect particles. It is suitable for tracking both passive and active particulates, including:

    • Water
    • Plankton
    • Plastic
    • Fish
  3. How index search integrates with SpatialHash

    main

    The index search process used for field interpolation follows a two-stage optimization to minimize expensive hash table lookups:

    1. Cell Guessing: If a previous cell index (yi, xi) is available from the previous time step, the system first attempts curvilinear_point_in_cell at those indices. If the particle hasn't moved enough to leave its cell, it is resolved immediately.
    2. Hash Lookup: If the guess fails (or no guess exists), the system calls grid.get_spatial_hash().query(y, x).

    The SpatialHash object is lazily constructed on its first call and is cached on the grid object for subsequent use.

    This pattern ensures high performance for particles that remain in the same cell between time steps.

    parcels._core.index_search._search_indices_curvilinear_2d
  4. How the Parcels Kernel loop works

    main

    When you call pset.execute(), Parcels enters a Kernel loop that iterates through time and executes the provided list of Kernels for all particles.

    To ensure that multiple Kernels (e.g., combining currents and winds) are commutable—meaning the order of execution does not change the final displacement—Parcels does not update particle positions (particles.x, particles.y, particles.z) directly within the Kernels. Instead, Kernels must update a displacement variable (particles.dx, particles.dy, particles.dz).

    The Kernel loop lifecycle:

    1. Initialize particles.dx = 0 (and similarly for dy, dz).
    2. For each time step: a. Update position: particles.x += particles.dx. b. Update time: particles.t += particles.dt. c. Reset displacement: particles.dx = 0. d. For each Kernel in the list: i. Execute the Kernel. ii. Accumulate the change: particles.dx += change_in_x. e. If the output interval is met, write the current state to the output file.

    This mechanism ensures that field sampling (e.g., fieldset.Var[particles.t, particles.z, particles.y, particles.x]) uses a consistent particle location throughout the entire loop and matches the location recorded in the output files.

  5. Understand the Parcels versioning scheme (EffVer)

    main

    Parcels uses Intended Effort Versioning (EffVer), which follows a MACRO.MESO.MICRO format. This scheme communicates the level of effort required for you to upgrade to a new version:

    • MACRO version: Requires dedicated time to upgrade.
    • MESO version: Requires some small effort to upgrade.
    • MICRO version: No effort is intended for you to upgrade.

    When backward incompatible changes occur, Parcels will provide instructions via changelogs, migration guides, or informative error messages.

  6. How the Parcels conceptual workflow works

    main

    A Parcels simulation is built using four core components that work together in a specific lifecycle:

    1. FieldSet: The environment. It contains the gridded datasets (like ocean velocity or temperature) where particles exist.
    2. ParticleSet: The subjects. A collection of virtual particles that exist within a FieldSet. Each particle must have initial values for t (time), z (depth), y (latitude), and x (longitude).
    3. Kernels: The logic. Functions applied to every particle at every timestep to define behavior (e.g., moving with the flow, aging, or sampling temperature).
    4. Execute: The engine. The method that runs the simulation by integrating the Kernels over a specified runtime and timestep, writing the results to a ParticleFile.
  7. How Morton Encoding works in Parcels

    main

    Morton encoding converts 3-D floating-point coordinates into a single unsigned 32-bit integer that preserves spatial locality. This is implemented in src/parcels/_core/spatialhash.py through three steps:

    1. Quantization (quantize_coordinates): Maps each axis from its range [min, max] to an integer in [0, bitwidth]. The default bitwidth is 1023 (10 bits per axis), allowing 3 axes to fit into a 30-bit uint32.
    2. Bit Dilation (_dilate_bits): Spreads the 10 bits of each integer so that there are two zero bits between each active bit. This prepares the bits for interleaving.
    3. Bit Interleaving (_encode_quantized_morton3d): Combines the three dilated axes by OR-ing them together. The resulting bit layout follows the pattern: x0, y0, z0, x1, y1, z1, ..., x9, y9, z9.

    The combined function _encode_morton3d performs all three steps during queries.

  8. How field interpolation works in Parcels

    main

    Interpolation is used to evaluate the state of particles within an environmental field (e.g., temperature or velocity) at their specific real-time and spatial positions (t, z, y, x).

    In Parcels, you can evaluate a field at particle positions using square bracket notation. This is a shorthand that provides the grid search algorithm with the particles' current positions as a 'first guess' for element indices, making the operation efficient.

    If you need to sample at locations or times that are not necessarily close to the particles' current positions, you can pass specific coordinates, but note that this may be slower for curvilinear or unstructured grids as it requires a full grid search.

    # Efficiently evaluate field at particle positions
    particles.temperature = fieldset.temperature[particles]
    
    # Shorthand for:
    # particles.temperature = fieldset.temperature[particles.t, particles.z, particles.y, particles.x, particles]
    
    # Sample at arbitrary locations (potentially slower for complex grids)
    particles.temperature = fieldset.temperature[t, z, y, x]
  9. How to write custom Kernels using displacement variables

    main

    When writing custom Kernels, do not update particles.x, particles.y, or particles.z directly. Doing so will interfere with the Kernel loop and trigger a warning.

    Instead, calculate the intended change in position and add it to the particles.dx, particles.dy, or particles.dz variables. This allows Parcels to sum the displacements from all Kernels before applying them to the actual particle coordinates, ensuring the simulation remains order-independent (commutable).

    def my_custom_kernel(particles, fieldset):
        # Do NOT do: particles.x += 10
        
        # DO: update the displacement variable
        particles.dx += 10
  10. How the Status Code system works in Parcels

    main

    Parcels uses a StatusCode system to capture and manage errors during the Kernel loop. When an error occurs (such as a Field Interpolation error or a FieldOutOfBoundSurfaceError), the particles.state attribute is updated to a specific status code.

    Developers can write custom Kernels that inspect particles.state to handle these errors programmatically. Common workflows include:

    • Deleting particles: Setting particles.state = parcels.StatusCode.Delete for particles in an error state.
    • Correcting particles: Modifying particle properties (like z, dx, or dy) to resolve the error and then resetting the state to parcels.StatusCode.Evaluate so the simulation can continue.

    Important: Kernels that manage particles.state should typically be placed at the end of the kernel list passed to pset.execute() to prevent subsequent kernels from overwriting the state or particle displacements.

    import parcels
    
    # Inspect available status codes
    for statuscode, val in parcels.StatusCode.__dict__.items():
        if statuscode.startswith("__"):
            continue
        print(f"{statuscode} = {val}")
  11. Design limitations of SpatialHash

    main

    When using SpatialHash for index searching, be aware of the following design characteristics:

    • Memory Usage: The CSR structure stores entries for every (face, overlapping-hash-cell) pair. Large faces that overlap many hash cells will increase memory consumption. Quantization is limited to 10-bit (1024 levels per axis) to bound this growth.
    • Spherical Geometry: To avoid degeneracy near the poles (where lon/lat cells become highly elongated), the system works in Cartesian space. This ensures more uniform hash grid resolution.
    • Flat Meshes: For flat meshes, the z-coordinate is fixed at 0. While the Morton code remains 3-D, the z-bits are always 0, which uses only 2/3 of the available code space but maintains correctness.