Receptors#

A receptor is a point (or set of points) in space and time that defines where and when STILT releases particles for a backward run. PYSTILT provides three concrete receptor classes that share a common base:

Class

When to use

Key attributes

PointReceptor

Single measurement location (tower, flask, aircraft point)

longitude, latitude, altitude

ColumnReceptor

Vertically-integrated column at one horizontal location (e.g. TCCON)

longitude, latitude, bottom, top

MultiPointReceptor

Slant or multi-angle column (e.g. OCO-2, satellite soundings)

longitudes, latitudes, altitudes (arrays)

PointReceptor#

The most common type. Use it for any fixed surface site or airborne sample at a single height.

import stilt

receptor = stilt.PointReceptor(
    time="2023-07-15 18:00:00",
    longitude=-111.848,
    latitude=40.766,
    altitude=10.0,          # metres AGL (default) or MSL
)

altitude_ref defaults to "agl" (above ground level). Pass altitude_ref="msl" for mean-sea-level altitudes.

ColumnReceptor#

Use when the measurement integrates over a vertical range at a single horizontal position. STILT releases particles at both bottom and top and the footprints are averaged with weighting supplied externally (e.g. an averaging kernel).

receptor = stilt.ColumnReceptor(
    time="2023-07-15 18:00:00",
    longitude=-111.848,
    latitude=40.766,
    bottom=50.0,            # lower altitude (must be < top)
    top=6000.0,
    altitude_ref="msl",
)

bottom must be strictly less than top; both are validated against altitude_ref (AGL altitudes must be ≥ 0).

MultiPointReceptor#

Use when the instrument views along a slant path, producing observations at multiple distinct horizontal and vertical positions simultaneously (e.g. OCO-2 soundings, satellite-retrieved columns with a full line-of-sight geometry).

import numpy as np

receptor = stilt.MultiPointReceptor(
    time="2023-07-15 18:00:00",
    longitudes=np.linspace(-111.85, -111.80, 10),
    latitudes=np.linspace(40.76,  40.80,  10),
    altitudes=np.linspace(0.0,    8000.0, 10),
    altitude_ref="msl",
)

All three coordinate arrays must have the same length. The location identifier is an order-independent SHA-256 hash of the point set, so reordering the points produces the same simulation ID.

Shared interface#

All three classes share the following interface, which lets generic code work with any receptor type.

receptor.id

A ReceptorID string in YYYYMMDDHHMM_{location} format. Used as the unique key throughout the storage and index layers.

receptor.time

A naive UTC datetime.datetime.

receptor.altitude_ref

"agl" or "msl".

for lat, lon, alt in receptor:

Iterates over (latitude, longitude, altitude) tuples — one for PointReceptor, two for ColumnReceptor, n for MultiPointReceptor. This is the primary way generic code reads coordinates without branching on type.

receptor.geometry

A shapely geometry (Point, LineString, or MultiPoint).

receptor.plot.map()

Quick map visualisation.

receptor.to_dict() / Receptor.from_dict(d)

JSON-round-trippable serialisation. The dict always contains a "type" key ("PointReceptor", "ColumnReceptor", or "MultiPointReceptor") so Receptor.from_dict can reconstruct the correct subclass.

Particle distribution#

numpar controls the total number of particles released per simulation. How those particles are distributed depends on receptor type.

PointReceptor — all numpar particles are released from the single location.

ColumnReceptor — particles are evenly spread across the column from bottom to top.

MultiPointReceptornumpar particles are distributed as evenly as possible across the n release locations. Choosing numpar as a multiple of n ensures every location receives exactly equal counts.

Loading from CSV#

read_receptors() loads a receptor CSV and returns a list of the appropriate subclass objects. The CSV format follows the R-STILT convention (time, longitude/long, latitude/lati, zagl/zmsl). Rows grouped under the same r_idx are assembled into a ColumnReceptor or MultiPointReceptor automatically.

receptors = stilt.read_receptors("receptors.csv")

Type dispatch#

Use isinstance() to branch on receptor type in generic code — never inspect string attributes:

from stilt import ColumnReceptor, MultiPointReceptor, PointReceptor

if isinstance(receptor, PointReceptor):
    print(f"Single point at {receptor.altitude} m")
elif isinstance(receptor, ColumnReceptor):
    print(f"Column from {receptor.bottom} to {receptor.top} m")
elif isinstance(receptor, MultiPointReceptor):
    print(f"Multi-point with {len(receptor)} locations")

All three are subclasses of Receptor, so isinstance(receptor, stilt.Receptor) is always True for any receptor object.

Smart constructor#

Receptor.from_points() picks the right subclass from a list of (longitude, latitude, altitude) tuples:

r = stilt.Receptor.from_points(
    time="2023-07-15 18:00:00",
    points=[(-111.848, 40.766, 10.0)],
)
# → PointReceptor