Plume operators (geo)¶
Plume post-processing on GeoTensors, behind the [geo] extra (pip install 'plumax[geo]'): the ten operators ported from geotoolz's plume module, as pipekit.Operators built with geoops's public carrier helpers (the pattern of radtrax's [geo] operators). Importing plumax.geo does not need the extra; the operators load on first use, and a missing extra raises an ImportError naming it.
geo
¶
Plume operators on GeoTensors, behind the [geo] extra.
- :mod:
plumax.geo.plume: the ten plume operators ported from geotoolz'splumemodule (#134): masks, contours, footprints, shape filters, column statistics and QND features, wind cones, the integrated mass enhancement, the cross-sectional flux and column-unit conversion. - :mod:
plumax.geo.plume_array: the array functions under them (plume_mask,wind_advection_cone,convert_column_units, ...). - :mod:
plumax.geo.vetting:PlumeVetting, :mod:plumax.vetting's candidate vetting and scene null on GeoTensors, as one Dataset (#157).
Importing this subpackage does not need the extra; the operators load on
first use, and a missing extra raises an ImportError naming it.
plume — the operators¶
The plume operators on GeoTensors (behind the [geo] extra).
Ten operators ported from geotoolz's plume module (#134) with their
behaviour unchanged; its eleventh, SBMP, moved to radtrax as
radtrax.geo.MBMP. They wrap the array functions in
:mod:plumax.geo.plume_array and are pipekit Operators built with geoops's
public carrier helpers, as radtrax's [geo] operators are.
Per-pixel operators accept a georeader.GeoTensor or a plain
np.ndarray and return the same carrier kind; geo-dependent operators
(advection geometry, pixel-area maths, footprints) require a georeferenced
GeoTensor and raise TypeError otherwise. The area and distance
operators (PlumeFootprint, WindAdvectionCone, IMEEstimate,
CrossSectionalFlux) work in metres and raise ValueError on a
geographic CRS (see :func:geoops.carrier.require_projected_crs). Vector
outputs are geopandas.GeoDataFrame. Algorithms follow the literature
cited in :mod:plumax.geo.plume_array:
PlumeMask/PlumeContours/PlumeFootprint: Frankenberg et al. (2016) detection and segmentation conventions.IMEEstimate: Varon et al. (2018) integrated mass enhancement.CrossSectionalFlux: Krings et al. (2011) / Varon (2018) appendix.PlumeShapeFilter/PlumeColumnStats/PlumeQNDFeatures: Pérez Carrasco et al. (2026), the false-plume classifier's features.
The IME and cross-section operators expect the enhancement carrier to be
in kg/m^2. Use :class:ColumnToMass to convert from ppm m or mol/m^2.
Nodata: pixels that are non-finite or equal the carrier's
fill_value_default (see :func:geoops.carrier.valid_pixels) never
enter a threshold, statistic or sum, and are never part of a plume mask or
contour. Output fill values follow the output's meaning, not the
input's: boolean masks declare fill_value_default=False, float
products (scores, converted columns) hold NaN at nodata pixels and
declare fill_value_default=NaN, and label images use 0 (see
:class:PlumeContours for its one documented exception).
PlumeMask
¶
PlumeMask(*, threshold: ThresholdMode = 'otsu', min_area_px: int = 50, connectivity: Connectivity = 8, nbins: int = 256)
Bases: Operator
Binary plume mask from a single-band score or enhancement map.
Thresholds the input (absolute number, Otsu, or percentile) and
drops connected components smaller than min_area_px pixels — the
Frankenberg et al. (2016) detection convention, also used by Varon
et al. (2018, 2021) for S2/AVIRIS plume identification.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
threshold
|
ThresholdMode
|
|
'otsu'
|
min_area_px
|
int
|
Minimum component size in pixels. |
50
|
connectivity
|
Connectivity
|
4 or 8 connectivity for component labelling. |
8
|
nbins
|
int
|
Histogram bins for the |
256
|
Nodata pixels (non-finite or equal to the input's fill value) are
excluded from the Otsu / percentile threshold and are always
False in the mask, which declares fill_value_default=False.
Examples:
Source code in src/plumax/geo/plume.py
PlumeContours
¶
Bases: Operator
Connected-component labelling of a plume mask.
Returns either an int32 label image (default) or a boolean mask of
the retained components. Component size threshold and connectivity
follow :func:label_components.
Nodata pixels (non-finite -- e.g. a NaN score -- or equal to the
input's fill value) are never part of a contour. In the boolean
output they are False (fill_value_default=False). In the
label image they hold the input's fill value when it is a
non-positive int32 value (e.g. -9999, so it cannot collide with
a label); otherwise (NaN, no fill, or a
positive fill) they are 0 (background), and a GeoTensor output
then carries fill_value_default=0.
Examples:
Source code in src/plumax/geo/plume.py
PlumeFootprint
¶
PlumeFootprint(*, min_area_m2: float = 500.0, simplify_tolerance: float | None = 15.0, properties: Sequence[str] | None = None, extra_properties: Sequence[Callable[..., Any]] | None = None)
Bases: Operator
Vectorize a plume mask into polygons with per-plume metadata.
The region properties come from the same machinery as
:class:geoops.measure.RegionProps (defaults to
:data:geoops.measure.DEFAULT_REGIONPROPS, pixel units) and the
polygons from :func:georeader.vectorize.get_polygons. Returns a
GeoDataFrame with one row per surviving plume: geometry, area_m2,
centroid,
mean_enhancement / max_enhancement (if enhancement is
supplied), n_pixels, label_id, and skimage region properties such as
major_axis_length, orientation, eccentricity, solidity,
perimeter, bbox-*, and inertia_tensor_eigvals-*.
Nodata pixels never form a polygon (of a boolean mask, or of a label
image whose fill is positive, such as a uint8 raster's 255),
non-positive labels are background, and nodata enhancement pixels are ignored
by the per-polygon statistics.
Geo-dependent: polygonisation needs the carrier's transform and CRS,
so the input must be a georeferenced GeoTensor (plain arrays
raise TypeError). Areas are in m^2 and the simplify tolerance in
m, so the CRS must be projected with metre units; a geographic CRS
raises ValueError (reproject with :class:geoops.geom.Reproject
first).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
min_area_m2
|
float
|
Drop polygons smaller than this area (m^2). |
500.0
|
simplify_tolerance
|
float | None
|
Douglas-Peucker tolerance (m) for polygon
simplification; |
15.0
|
properties
|
Sequence[str] | None
|
Region properties forwarded to |
None
|
extra_properties
|
Sequence[Callable[..., Any]] | None
|
User-defined |
None
|
Called as op(mask, enhancement=None). The optional enhancement
carrier must sit on the mask's pixel grid (ValueError otherwise);
it enables the mean_enhancement / max_enhancement statistics
per polygon.
Examples:
Source code in src/plumax/geo/plume.py
PlumeShapeFilter
¶
PlumeShapeFilter(*, min_area_px: int = 50, min_fiber_to_major_ratio: float = 1.0, max_fiber_to_major_ratio: float = 10.0, min_fiber_width: float = 1.0, max_fiber_width: float = 100.0)
Bases: Operator
Drop instance labels that fail a fiber-shape morphology gate.
Adapted from Pérez Carrasco et al. (2026), physical_constraints.
apply_physical_constraints — a post-Mask R-CNN gate that distinguishes
elongated plume features from compact false positives. Per instance:
area = mask.sum(); drop when belowmin_area_px.fiber_length= longest Euclidean path through the skeleton, in pixel widths (diagonal steps countsqrt(2); see :func:geoops.measure.skeleton_length).major_axis=regionprops.axis_major_length— the PCA-based major axis of the equivalent ellipse. The original paper usescv2.minAreaRect(rotated minimum-area rectangle), avoided here to keep the optional-dependency surface unchanged; the two measures agree closely for typical plume morphologies.fiber_width = area / fiber_length.- Keep iff
min_fiber_to_major_ratio <= fiber_length / major_axis <= max_fiber_to_major_ratioANDmin_fiber_width <= fiber_width <= max_fiber_width.
Surviving instances keep their original integer labels; rejected
instances are mapped to background (0).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
min_area_px
|
int
|
Minimum pixel count to consider an instance. |
50
|
min_fiber_to_major_ratio
|
float
|
Lower bound on |
1.0
|
max_fiber_to_major_ratio
|
float
|
Upper bound on |
10.0
|
min_fiber_width
|
float
|
Lower bound on |
1.0
|
max_fiber_width
|
float
|
Upper bound on |
100.0
|
Source code in src/plumax/geo/plume.py
PlumeColumnStats
¶
Bases: Operator
Per-instance in-mask vs background statistics of a column field.
Adapted from Pérez Carrasco et al. (2026), rfc_utils.xch4_metrics.
For every non-zero label in the input map, returns a row with the
in-mask mean, the out-of-mask (background) mean / std, and three
derived metrics used by the paper's Random-Forest false-plume
classifier:
contrast=mean_plume / mean_backgroundz_score=(mean_plume - mean_background) / std_backgroundintensity_per_area=(sum_plume - mean_background * n_plume) / n_plume
Nodata column pixels (non-finite or equal to the column carrier's fill value) are left out of every statistic.
Called as op(labels, column): column is the column field
(single-band GeoTensor or plain array) on the label map's pixel
grid (ValueError otherwise).
PlumeQNDFeatures
¶
PlumeQNDFeatures(*, degree: int = 6, eps: float = 50.0, min_samples: int = 50, perc_threshold: float = 98.0)
Bases: Operator
Per-instance Quantile-Normality-Deviation features for plume QA.
Adapted from Pérez Carrasco et al. (2026) — the feature pipeline that
feeds the Random-Forest false-plume classifier
(rfc_utils.get_QND_df_cms / get_plume_info). For each non-zero
label in the input map this operator emits one row containing:
qnd_column_*— polynomial coefficients of the Kolmogorov-Smirnov distance between the in-mask CDF of the column field and a Gaussian fit to the full scene's column distribution, sampled across 100 percentiles in[0.01, 0.99].qnd_albedo_*— same construction over the optional albedo field.n_clumps— DBSCAN cluster count of in-mask pixels above theperc_thresholdpercentile (per the paper's clump-count signal).contrast/z_score/intensity_per_area— the same stats that :class:PlumeColumnStatsemits, included so a single call assembles the full feature vector used by the paper.
Nodata column / albedo pixels (non-finite or equal to the carrier's fill value) are left out of every feature.
Called as op(labels, column, albedo=None): column is the
column-enhancement carrier and albedo an optional albedo carrier,
both on the label map's pixel grid (ValueError otherwise). Without
albedo the qnd_albedo_* coefficients are NaN.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
degree
|
int
|
Degree of the QND polynomial (paper default: 6). |
6
|
eps
|
float
|
DBSCAN |
50.0
|
min_samples
|
int
|
DBSCAN |
50
|
perc_threshold
|
float
|
Percentile (within the instance mask) above which pixels are eligible for clustering. |
98.0
|
Source code in src/plumax/geo/plume.py
WindAdvectionCone
¶
WindAdvectionCone(*, source: tuple[float, float], wind_u: float, wind_v: float, half_angle_deg: float = 30.0, max_distance: float = 5000.0, crs: str | None = None)
Bases: Operator
Mask likely downwind plume extent from a source and wind vector.
Rasterizes a circular sector centered at source, oriented along
(wind_u, wind_v), with half-angle half_angle_deg and radius
max_distance. The mask is useful as a prior for plume
segmentation when the source location and wind are known (e.g. from
a reanalysis product), following the practice of Varon et al. (2018,
2021).
Geo-dependent: the cone is rasterised in CRS coordinates via the
carrier's transform, so the input must be a georeferenced
GeoTensor (plain arrays raise TypeError). max_distance is
in metres and the cone angle is measured in the map plane, so the
carrier CRS must be projected with metre units; a geographic CRS
raises ValueError (reproject with :class:geoops.geom.Reproject
first).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
source
|
tuple[float, float]
|
|
required |
wind_u
|
float
|
Eastward wind component (m/s). |
required |
wind_v
|
float
|
Northward wind component (m/s). |
required |
half_angle_deg
|
float
|
Half-angle of the cone in degrees, in [0, 180];
|
30.0
|
max_distance
|
float
|
Radius of the cone (m). |
5000.0
|
crs
|
str | None
|
Optional CRS of |
None
|
Examples:
>>> cone = WindAdvectionCone(
... source=(x0, y0), wind_u=3.0, wind_v=1.0,
... half_angle_deg=30.0, max_distance=5000.0,
... )(enhancement)
Source code in src/plumax/geo/plume.py
IMEEstimate
¶
IMEEstimate(*, wind_speed: float, length_method: Literal['max_axis', 'convex_hull', 'skeleton'] = 'max_axis', pixel_area_m2: float | None = None, return_uncertainty: bool = True, uncertainty_fraction: float = 0.5)
Bases: Operator
Integrated Mass Enhancement (IME) flux estimator.
Implements the IME method of Varon et al. (2018), AMT Quantifying methane point sources from fine-scale satellite observations of atmospheric methane plumes:
.. math::
\mathrm{IME} \;=\; \sum_{i \in \mathrm{plume}}
\Omega_i \,\Delta A_i, \qquad
Q \;=\; \frac{U_{\mathrm{eff}}\,\mathrm{IME}}{L}
where :math:\Omega_i is the column enhancement (kg/m^2) of pixel
:math:i, :math:\Delta A_i is its area (m^2), :math:L is the
effective plume length (m) — see :func:plume_length — and
:math:U_{\mathrm{eff}} is the effective wind speed (m/s). Varon
et al. (2018) calibrate :math:U_{\mathrm{eff}} against LES
simulations; this operator takes the calibrated value as
wind_speed.
The enhancement carrier MUST be in kg/m^2. Use
:class:ColumnToMass upstream to convert from ppm m or mol/m^2:
passing other units silently produces nonsense. Nodata enhancement
pixels (non-finite or equal to the fill value) add nothing to the IME.
Geo-dependent: pixel area (m^2) and plume length (m) are derived from
the carrier's transform, so the input must be a georeferenced
GeoTensor (plain arrays raise TypeError) in a projected CRS
with metre units; a geographic CRS raises ValueError (reproject
with :class:geoops.geom.Reproject first).
Called as op(enhancement, plume_mask): plume_mask is a boolean
carrier selecting plume pixels on the enhancement's pixel grid (same
shape, transform and CRS; ValueError otherwise -- a mask on another
grid would be indexed against the wrong pixels).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
wind_speed
|
float
|
Effective wind speed :math: |
required |
length_method
|
Literal['max_axis', 'convex_hull', 'skeleton']
|
Length estimator: |
'max_axis'
|
pixel_area_m2
|
float | None
|
Override the pixel area (m^2); default is the absolute input transform determinant in the projected CRS's metres (exact on an equal-area projection; on UTM it ignores the projection's scale factor, a < 0.1 % error near the central meridian). |
None
|
return_uncertainty
|
bool
|
Append |
True
|
uncertainty_fraction
|
float
|
Fractional 1-sigma uncertainty on Q. The default 0.5 follows Varon et al. (2018) Table 3. |
0.5
|
The call returns a dict with keys ime_kg, length_m,
wind_speed_m_s, emission_rate_kg_s, and optionally
emission_rate_uncertainty_kg_s.
Examples:
Estimate Q in kg/s from a ppm m enhancement map::
from plumax.geo import ColumnToMass, IMEEstimate, PlumeMask
kg_m2 = ColumnToMass(units_in="ppm_m")(enhancement)
mask = PlumeMask(threshold="percentile:99")(kg_m2)
result = IMEEstimate(
wind_speed=3.5,
length_method="convex_hull",
)(kg_m2, mask)
print(result["emission_rate_kg_s"])
Source code in src/plumax/geo/plume.py
CrossSectionalFlux
¶
CrossSectionalFlux(*, source: tuple[float, float], wind_u: float, wind_v: float, n_transects: int = 5, transect_spacing_m: float = 100.0)
Bases: Operator
Estimate cross-sectional flux at downwind transects.
For each transect at downwind distance :math:d_k, the operator
integrates column mass across the plume:
.. math::
Q_k \;=\; U \int_{-\infty}^{\infty}
\Omega(d_k, y) \, dy
\;\approx\; U \sum_{i \in \mathrm{strip}_k}
\Omega_i \,\Delta y_i
where :math:\mathrm{strip}_k is the set of plume pixels whose
along-wind distance to the source falls in the half-open interval
:math:[d_k - \Delta y / 2,\, d_k + \Delta y / 2), so a pixel whose
centre lies on a strip boundary is counted once. This is the cross-section method of Krings et al. (2011)
and Varon et al. (2018, appendix); a flat downwind profile of
:math:Q_k versus :math:d_k confirms a steady-state estimate.
The enhancement carrier MUST be in kg/m^2 (see :class:ColumnToMass).
Nodata enhancement pixels (non-finite or equal to the fill value) add
nothing to a transect's flux.
The implementation assumes (approximately) square pixels: it uses
:math:\Delta y = \sqrt{\mathrm{pixel\_area}} as the across-wind
pixel size. For strongly anisotropic pixel grids, reproject first.
Geo-dependent: transect placement and pixel size come from the
carrier's transform and CRS, so the input must be a georeferenced
GeoTensor (plain arrays raise TypeError) in a projected CRS
with metre units; a geographic CRS raises ValueError (reproject
with :class:geoops.geom.Reproject first).
Called as op(enhancement, plume_mask): plume_mask is a boolean
plume carrier on the enhancement's pixel grid (ValueError
otherwise).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
source
|
tuple[float, float]
|
|
required |
wind_u
|
float
|
Eastward wind component (m/s). |
required |
wind_v
|
float
|
Northward wind component (m/s). |
required |
n_transects
|
int
|
Number of downwind transects to evaluate. |
5
|
transect_spacing_m
|
float
|
Along-wind distance between transects (m). |
100.0
|
The call returns a geopandas.GeoDataFrame with one row per
transect: geometry (across-wind LineString), transect_id,
distance_m, flux_kg_s, n_pixels.
Examples:
Evaluate fluxes at 100 m, 200 m, 300 m downwind::
gdf = CrossSectionalFlux(
source=(x0, y0),
wind_u=3.0, wind_v=1.0,
n_transects=3, transect_spacing_m=100.0,
)(kg_m2, mask)
Source code in src/plumax/geo/plume.py
ColumnToMass
¶
Bases: Operator
Convert column enhancement units (ppm m / mol m^-2 / kg m^-2).
Thin wrapper over :func:convert_column_units. The ppm m conversion
assumes a standard molar volume (298.15 K, 1 atm); see that
function's docstring for the exact relation and caveats. Nodata
pixels (non-finite or equal to the fill value) are not converted:
they hold NaN and the output declares fill_value_default=NaN
(a converted column is a new quantity, so the input's fill -- e.g.
-9999 ppm m -- is not carried over).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
gas
|
str
|
|
'CH4'
|
units_in
|
ColumnUnit
|
Units of the input carrier. |
'ppm_m'
|
units_out
|
ColumnUnit
|
Desired output units. |
'kg_m2'
|
Examples:
Source code in src/plumax/geo/plume.py
vetting — plume vetting on carriers¶
PlumeVetting runs plume vetting (vet_candidate and the scene null) on every candidate of a scene and returns the scores as one xarray.Dataset; the decision rule is left to the caller.
Plume vetting on GeoTensors (behind the [geo] extra): one Dataset per scene.
PlumeVetting is the L2 operator over the array-level vetting in
:mod:plumax.vetting (#157): it reads a radiance GeoTensor, its
matched-filter GeoTensor and a stack of candidate masks, runs
vet_candidate on each candidate and the scene null (null_masks,
null_pvalue) beside it, and returns the scores as one xarray.Dataset.
It returns scores, not a verdict: the decision rule is #158's.
PlumeVetting
¶
PlumeVetting(window: AbsorptionWindow, table: Float[ArrayLike, 'D Bf'], delta_grid: Float[ArrayLike, ' D'], *, n_null: int = 100, key: Array | None = None, k: int = 40, keep: float = 0.5, degree: int = 2, box: int = 200)
Bases: Operator
Vet every candidate of a scene and its scene null; scores as a Dataset.
Called as PlumeVetting(window, table, delta_grid)(cube, mf, masks).
For each candidate M_c of masks, with every candidate kept out of
the background pool,
(ΔX̂_c, D_norm,c) = vet_candidate(L, MF, M_c), and
p̂_c = null_pvalue(D_norm,c, {D_norm(M_s)}) over n_null copies
M_s of M_c from null_masks, each vetted as M_c is; a copy
that cannot be scored (radtrax's DegenerateInputError) scores
NaN and null_pvalue leaves it out.
The cube is (B, H, W) with its band centres in nm in
attrs["wavelengths"]; mf is (H, W) or (1, H, W) on the
cube's pixel grid, in mol/m² (the units of
window.mf_background_mol_m2). Nodata pixels of either carrier
(non-finite, or equal to its fill_value_default) are dropped from
every candidate and kept out of the pool and the null copies. The
candidates' random draws split key in candidate order, so the same
key gives the same p_null.
The Dataset has dims candidate (C) and band (the window's
Bf fitting bands, coordinate wavelength_nm) and holds
delta_x_mol_m2 (ΔX̂), d_norm, p_null (NaN when
n_null = 0) and n_pairs per candidate; ratio (R),
continuum (P(λ; ĉ)) and fit (P(λ; ĉ) E(λ; ΔX̂), the fitted
ratio) per candidate and band; and attributes window (the window's
fields as JSON) and key (the key's raw data).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
window
|
AbsorptionWindow
|
The gas's |
required |
table
|
Float[ArrayLike, 'D Bf']
|
|
required |
delta_grid
|
Float[ArrayLike, ' D']
|
The columns |
required |
n_null
|
int
|
Number of null copies per candidate; |
100
|
key
|
Array | None
|
A |
None
|
k
|
int
|
Number of seeds before growth ( |
40
|
keep
|
float
|
Fraction of the pairs kept, in (0, 1]. |
0.5
|
degree
|
int
|
The continuum's Legendre degree. |
2
|
box
|
int
|
Margin, in pixels, around the seeds' bounding box that the pool is drawn from. |
200
|
Raises:
| Type | Description |
|---|---|
ParameterError
|
If |
ShapeError
|
If the cube is not |
Examples:
A pipeline sketch, after radtrax's matched filter::
from plumax.geo import PlumeVetting
scores = PlumeVetting(window, table, delta_grid, n_null=100)(
cube, mf, masks
)
scores["d_norm"], scores["p_null"]
Source code in src/plumax/geo/vetting.py
get_config
¶
The JSON-safe configuration, without the arrays (table, grid, key).
Source code in src/plumax/geo/vetting.py
from_config
classmethod
¶
from_config(config: dict[str, Any], *, table: Float[ArrayLike, 'D Bf'], delta_grid: Float[ArrayLike, ' D'], key: Array | None = None) -> PlumeVetting
Rebuild from get_config's output and the arrays it leaves out.
Source code in src/plumax/geo/vetting.py
plume_array — the array functions¶
Plume masks, geometry and column units on NumPy arrays (behind [geo]).
The array functions under the operators in :mod:plumax.geo.plume, ported
from geotoolz's plume module (#134) with their behaviour unchanged.
They take plain arrays and an affine transform, no carrier, but build on
geoops's public mask, measure and threshold helpers and on shapely, so they
need the [geo] extra too. Algorithms and unit conventions follow the
trace-gas plume literature cited per function:
- Varon et al. (2018), AMT: the integrated mass enhancement (IME) method.
- Frankenberg et al. (2016), PNAS: airborne CH4 plume detection.
- Thompson et al. (2015), AMT, and Foote et al. (2020), TGRS: the matched-filter CH4 retrievals whose columns these functions convert.
plume_mask
¶
plume_mask(values: Num[ndarray, 'h w'] | Num[ndarray, '1 h w'], *, threshold: ThresholdMode = 'otsu', min_area_px: int = 50, connectivity: Connectivity = 8, nbins: int = 256) -> Bool[np.ndarray, 'h w']
Threshold an enhancement map and remove small connected components.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
values
|
Num[ndarray, 'h w'] | Num[ndarray, '1 h w']
|
Single-band enhancement map, |
required |
threshold
|
ThresholdMode
|
Absolute number, |
'otsu'
|
min_area_px
|
int
|
Minimum connected-component size in pixels. |
50
|
connectivity
|
Connectivity
|
4 or 8 connectivity for component labelling. |
8
|
nbins
|
int
|
Histogram bins for the |
256
|
Returns:
| Type | Description |
|---|---|
Bool[ndarray, 'h w']
|
Boolean |
Source code in src/plumax/geo/plume_array.py
wind_advection_cone
¶
wind_advection_cone(shape: tuple[int, int], transform: Any, *, source: tuple[float, float], wind_u: float, wind_v: float, half_angle_deg: float = 30.0, max_distance: float = 5000.0) -> Bool[np.ndarray, 'h w']
Rasterize an analytical downwind sector mask.
Marks pixels whose center lies within max_distance of source
and whose bearing from the source is within half_angle_deg of the
wind direction (wind_u, wind_v) — a geometric prior for where an
advected plume can be. The source pixel itself is included.
With d = (x − x₀, y − y₀) the offset of a pixel center from the
source and ŵ = (u, v) / ‖(u, v)‖ the unit wind vector, a pixel is
inside when
‖d‖ ≤ max_distance and d·ŵ ≥ ‖d‖ · cos(half_angle_deg)
i.e. the angle between d and ŵ is at most half_angle_deg.
For half_angle_deg ≤ 90 the angle test alone keeps the sector in
the downwind half-plane (cos ≥ 0 ⇒ d·ŵ ≥ 0); half_angle_deg > 90
widens it past the crosswind line, and half_angle_deg = 180 is the
full disc of radius max_distance, upwind pixels included.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
shape
|
tuple[int, int]
|
Raster shape |
required |
transform
|
Any
|
Affine-like geotransform of the raster; its CRS units
must match |
required |
source
|
tuple[float, float]
|
|
required |
wind_u
|
float
|
Eastward wind component. |
required |
wind_v
|
float
|
Northward wind component. |
required |
half_angle_deg
|
float
|
Half-angle of the sector in degrees, in [0, 180]. |
30.0
|
max_distance
|
float
|
Sector radius in CRS units. |
5000.0
|
Returns:
| Type | Description |
|---|---|
Bool[ndarray, 'h w']
|
Boolean |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the wind vector is zero, |
Source code in src/plumax/geo/plume_array.py
convert_column_units
¶
convert_column_units(values: Num[ndarray, '*dims'], *, gas: str = 'CH4', units_in: ColumnUnit = 'ppm_m', units_out: ColumnUnit = 'kg_m2') -> Float[np.ndarray, '*dims']
Convert column enhancement among ppm m, mol/m^2, and kg/m^2.
The conversions are
.. math::
\Omega_{\mathrm{mol/m^2}} \;=\; \frac{X \cdot 10^{-6}}{V_m}, \qquad
\Omega_{\mathrm{kg/m^2}} \;=\; M_{\mathrm{gas}}
\cdot \Omega_{\mathrm{mol/m^2}}
where X is the column-integrated volume mixing ratio in ppm m,
:math:V_m = 0.024465 m^3/mol is the standard molar volume of an
ideal gas at 298.15 K and 1 atm, and :math:M_{\mathrm{gas}} is the
molar mass (CH4: 0.01604 kg/mol; CO2: 0.04401 kg/mol).
The ppm m branch therefore assumes near-surface conditions. Pass
mol_m2 or kg_m2 inputs when retrieval-specific pressure and
temperature corrections have already been applied upstream (e.g. by
a matched-filter retrieval per Thompson 2015 / Foote 2020).
Source code in src/plumax/geo/plume_array.py
plume_length
¶
plume_length(mask: Bool[ndarray, 'h w'], transform: Any, *, method: Literal['max_axis', 'convex_hull', 'skeleton'] = 'max_axis') -> float
Estimate plume length L from active pixel centers.
L is the effective length used by Varon et al. (2018) in the IME
method Q = U_eff * IME / L. Three estimators are supported:
"max_axis": maximum pairwise distance between active pixels, taken over the vertices of their convex hull (the same value as over every pair, in memory that does not grow withN²). Fast and robust; matches the original Varon 2018 definition for reasonably linear plumes."convex_hull": max diameter of the convex hull of the active pixels. For degenerate hulls (single point, collinear points) we fall back to the point-set diameter so the result is always well defined."skeleton": length of the plume centreline, via :func:geoops.measure.skeleton_lengthwith the transform as the pixel step. The mask is skeletonised andLis the longest geodesic path through the 8-connected skeleton, with each step weighted by its Euclidean length in CRS units (so diagonal steps and anisotropic pixels are measured correctly). Better than the chord for curved plumes. The path runs between skeleton pixel centres, so for thick plumes it is shorter than the mask extent by roughly the plume width.
Lengths are in CRS units (m for a projected metric CRS) and measured between pixel centres.
A single-pixel mask returns one linear pixel size,
sqrt(pixel_area), for every method: it has no geometry to
measure, and the one-pixel proxy is the documented convention.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
mask
|
Bool[ndarray, 'h w']
|
Boolean |
required |
transform
|
Any
|
Affine-like geotransform ( |
required |
method
|
Literal['max_axis', 'convex_hull', 'skeleton']
|
Length estimator (see above). |
'max_axis'
|
Returns:
| Type | Description |
|---|---|
float
|
The plume length in CRS units; |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Source code in src/plumax/geo/plume_array.py
288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 | |
pixel_area
¶
Return pixel area from an affine-like transform determinant.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
transform
|
Any
|
Affine-like object exposing |
required |
Returns:
| Type | Description |
|---|---|
float
|
|
float
|
projected metric CRS). |
Source code in src/plumax/geo/plume_array.py
pixel_centers
¶
pixel_centers(shape: tuple[int, int], transform: Any) -> tuple[Float[np.ndarray, 'h w'], Float[np.ndarray, 'h w']]
Return x/y coordinate grids for pixel centers.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
shape
|
tuple[int, int]
|
Raster shape |
required |
transform
|
Any
|
Affine-like geotransform mapping |
required |
Returns:
| Type | Description |
|---|---|
Float[ndarray, 'h w']
|
Tuple |
Float[ndarray, 'h w']
|
coordinates of each pixel center. |
Source code in src/plumax/geo/plume_array.py
squeeze_single_band
¶
squeeze_single_band(values: Shaped[ndarray, 'h w'] | Shaped[ndarray, '1 h w']) -> Shaped[np.ndarray, 'h w']
Return a 2-D plume map from a 2-D or singleton-band array.
Thin delegate to :func:geoops.carrier.single_band, kept under
geotoolz's historical public name.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
values
|
Shaped[ndarray, 'h w'] | Shaped[ndarray, '1 h w']
|
A |
required |
Returns:
| Type | Description |
|---|---|
Shaped[ndarray, 'h w']
|
The |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |