Plume vetting¶
Plume-candidate vetting on arrays (L1): which of a candidate's pixels are in (the seeds) and which are out (the background pool), and how this scene's clutter scores in the candidate's shape (the null), on radtrax's plume-agnostic absorption-shape test (radtrax.retrieval.absorption: the pairing, the ratio spectrum, the fit and D_norm; see radtrax's absorption-shape test page). Theory: plume vetting; a worked scene: the plume vetting notebook.
Errors are radtrax's public exception classes (ParameterError, DegenerateInputError, ShapeError), each also a ValueError.
vetting
¶
Plume-candidate vetting: is a matched-filter candidate the gas, or clutter?
Candidate-level vetting (Xiang et al. 2024; Růžička et al. 2026) as L1
functions on arrays: no carrier, no geotoolz. A candidate is a boolean mask
on a radiance cube and its matched-filter map; this package chooses which of
its pixels count as in (the seeds) and which as out (the background pool),
and runs radtrax's plume-agnostic absorption-shape test on them
(radtrax.retrieval.absorption: the pairing, the ratio spectrum, the fit
to the gas's transmittance and D_norm). Theory: the plume-vetting page.
seeds:select_seedsandbackground_pool, a candidate's in-plume seeds and the background pool they are paired from (#154).null:null_masksandnull_pvalue, rotated and translated copies of a candidate's mask off every candidate, and the candidate's p-value among their scores (#155).vet:vet_candidateandVettingResult, one candidate vetted in one call: seeds, pool, radtrax's pairs, ratio, fit andD_norm(#156).
vet — one candidate¶
Plume-candidate vetting: is a matched-filter candidate the gas, or clutter?
Candidate-level vetting (Xiang et al. 2024; Růžička et al. 2026) as L1
functions on arrays: no carrier, no geotoolz. A candidate is a boolean mask
on a radiance cube and its matched-filter map; this package chooses which of
its pixels count as in (the seeds) and which as out (the background pool),
and runs radtrax's plume-agnostic absorption-shape test on them
(radtrax.retrieval.absorption: the pairing, the ratio spectrum, the fit
to the gas's transmittance and D_norm). Theory: the plume-vetting page.
seeds:select_seedsandbackground_pool, a candidate's in-plume seeds and the background pool they are paired from (#154).null:null_masksandnull_pvalue, rotated and translated copies of a candidate's mask off every candidate, and the candidate's p-value among their scores (#155).vet:vet_candidateandVettingResult, one candidate vetted in one call: seeds, pool, radtrax's pairs, ratio, fit andD_norm(#156).
VettingResult
¶
Bases: Module
One candidate's scores and the spectra behind them.
Attributes:
| Name | Type | Description |
|---|---|---|
delta |
Float[Array, '']
|
|
d_norm |
Float[Array, '']
|
|
n_pairs |
int
|
The number of pairs the ratio averages (static). |
ratio |
Float[Array, ' Bf']
|
|
fit |
TransmittanceFit
|
radtrax's |
residual_ss_no_gas |
Float[Array, '']
|
The residual sum of squares of the continuum
alone ( |
vet_candidate
¶
vet_candidate(cube: Float[ArrayLike, 'H W B'], mf: Float[ArrayLike, 'H W'], mask: Bool[ArrayLike, 'H W'], wavelength_nm: Float[ArrayLike, ' B'], table: Float[ArrayLike, 'D Bf'], delta_grid: Float[ArrayLike, ' D'], window: AbsorptionWindow, *, exclude: Bool[ArrayLike, ' C H W'] | None = None, k: int = 40, keep: float = 0.5, degree: int = 2, box: int = 200) -> VettingResult
Vet one candidate mask: seeds, pool, pairs, ratio, fit and D_norm.
With F the window's fitting bands and O its pairing bands,
I = select_seeds(MF, M), P = background_pool(MF, I) below the
window's mf_background_mol_m2;
(i_n, o_n) = pair_pixels(L, I, P, O), the best keep of a
one-to-one assignment of the two sets as flat pixel indices;
R(λ) = ∑_n L_in,n(λ) / ∑_n L_out,n(λ) for λ ∈ F;
(ΔX̂, ĉ) = argmin ∑_λ (R(λ) − P(λ; c) E(λ; ΔX))² over the grid, and
D_norm of that fit.
Runs eagerly, not under jit (pairing is NumPy and SciPy), and is not
differentiable as a whole (seeds, pairs and ΔX̂ are discrete); the
pieces that are differentiable say so.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
cube
|
Float[ArrayLike, 'H W B']
|
Radiance, |
required |
mf
|
Float[ArrayLike, 'H W']
|
Matched-filter column |
required |
mask
|
Bool[ArrayLike, 'H W']
|
The candidate |
required |
wavelength_nm
|
Float[ArrayLike, ' B']
|
The cube's band centres [nm], |
required |
table
|
Float[ArrayLike, 'D Bf']
|
|
required |
delta_grid
|
Float[ArrayLike, ' D']
|
The columns |
required |
window
|
AbsorptionWindow
|
The gas's |
required |
exclude
|
Bool[ArrayLike, ' C H W'] | None
|
The scene's |
None
|
k
|
int
|
Number of seeds before growth; the reference code's 40. |
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
|
Returns:
| Type | Description |
|---|---|
VettingResult
|
The candidate's |
Raises:
| Type | Description |
|---|---|
ShapeError
|
If |
Examples:
>>> import numpy as np
>>> from radtrax.retrieval.absorption import AbsorptionWindow
>>> from plumax.vetting import vet_candidate
>>> wl = np.array([1000.0, 1100.0, 1200.0, 2300.0, 2310.0, 2320.0])
>>> eps = np.array([2.0, 5.0, 3.0]) # m²/mol on the fitting bands
>>> grid = np.linspace(0.0, 0.2, 21) # mol/m²
>>> table = np.exp(-grid[:, None] * eps) # (21, 3)
>>> mask = np.zeros((6, 6), bool)
>>> mask[2:4, 2:4] = True
>>> mf = np.where(mask, 0.1, 0.0)
>>> e = np.where(mask[..., None], np.exp(-0.1 * eps), 1.0) # (6, 6, 3)
>>> cube = np.concatenate([np.ones((6, 6, 3)), e], axis=-1)
>>> window = AbsorptionWindow(
... "CH4", ((2290.0, 2330.0),), ((2290.0, 2330.0),), 0.01
... )
>>> r = vet_candidate(cube, mf, mask, wl, table, grid, window, degree=0)
>>> print(f"{r.delta:.2f} {r.d_norm:.2f} {r.n_pairs}")
0.10 0.00 2
Source code in src/plumax/vetting/vet.py
71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 | |
seeds — the seeds and the background pool¶
Plume-candidate vetting: is a matched-filter candidate the gas, or clutter?
Candidate-level vetting (Xiang et al. 2024; Růžička et al. 2026) as L1
functions on arrays: no carrier, no geotoolz. A candidate is a boolean mask
on a radiance cube and its matched-filter map; this package chooses which of
its pixels count as in (the seeds) and which as out (the background pool),
and runs radtrax's plume-agnostic absorption-shape test on them
(radtrax.retrieval.absorption: the pairing, the ratio spectrum, the fit
to the gas's transmittance and D_norm). Theory: the plume-vetting page.
seeds:select_seedsandbackground_pool, a candidate's in-plume seeds and the background pool they are paired from (#154).null:null_masksandnull_pvalue, rotated and translated copies of a candidate's mask off every candidate, and the candidate's p-value among their scores (#155).vet:vet_candidateandVettingResult, one candidate vetted in one call: seeds, pool, radtrax's pairs, ratio, fit andD_norm(#156).
select_seeds
¶
select_seeds(mf: Float[ArrayLike, 'H W'], mask: Bool[ArrayLike, 'H W'], *, k: int = 40, cap_percentile: Float[ArrayLike, ''] = 99.0, grow: int = 1) -> Bool[Array, 'H W']
The k strongest matched-filter pixels of a candidate, grown inside it.
With M the mask's pixels of finite score and q the
cap_percentile-th percentile of the score over M,
I = grow(top_k{p ∈ M : MF(p) ≤ q}) ∩ M,
where each growth step is one 3 × 3 dilation intersected with M (the
reference code's Gaussian filter with σ = 1 and threshold 0.05 is the
same dilation). With grow=0, |I| = min(k, |{p ∈ M : MF(p) ≤ q}|)
exactly: ties at the k-th score are broken by the lower flat index
(jax.lax.top_k).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
mf
|
Float[ArrayLike, 'H W']
|
Matched-filter score, e.g. |
required |
mask
|
Bool[ArrayLike, 'H W']
|
The candidate, |
required |
k
|
int
|
Number of seeds before growth (static); the reference code's 40. |
40
|
cap_percentile
|
Float[ArrayLike, '']
|
Scores above this percentile of the mask's are not seeds, in (0, 100]; the reference code's 99. |
99.0
|
grow
|
int
|
Growth steps inside the mask (static); |
1
|
Returns:
| Type | Description |
|---|---|
Bool[Array, 'H W']
|
The seeds, |
Raises:
| Type | Description |
|---|---|
ParameterError
|
If |
ShapeError
|
If |
DegenerateInputError
|
If the mask holds no pixel with a finite
score. Under |
Examples:
>>> import jax.numpy as jnp
>>> from plumax.vetting import select_seeds
>>> mf = jnp.arange(25.0).reshape(5, 5)
>>> mask = jnp.zeros((5, 5), bool).at[1:4, 1:4].set(True)
>>> print(
... select_seeds(mf, mask, k=2, cap_percentile=90.0, grow=0).astype(int)
... )
[[0 0 0 0 0]
[0 0 0 0 0]
[0 0 0 0 0]
[0 1 1 0 0]
[0 0 0 0 0]]
>>> print(select_seeds(mf, mask, k=2, cap_percentile=90.0).astype(int))
[[0 0 0 0 0]
[0 0 0 0 0]
[0 1 1 1 0]
[0 1 1 1 0]
[0 0 0 0 0]]
Source code in src/plumax/vetting/seeds.py
background_pool
¶
background_pool(mf: Float[ArrayLike, 'H W'], seeds: Bool[ArrayLike, 'H W'], *, threshold: Float[ArrayLike, ''], exclude: Bool[ArrayLike, ' C H W'] | None = None, box: int = 200) -> Bool[Array, 'H W']
Background pixels near the seeds, outside every candidate, with small scores.
O = box(I) ∖ I ∖ ⋃_c M_c ∩ {p : |MF(p)| < τ}, where box(I) is the
seeds' bounding box widened by box pixels on every side (clipped to
the image) and M_c the C candidate masks in exclude. The box
keeps each pair under the same sun, elevation and water vapour; the
threshold keeps plume pixels out of the pool. Pass every candidate of
the scene, this one included, as exclude: without it only the seeds
themselves are kept out.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
mf
|
Float[ArrayLike, 'H W']
|
Matched-filter score, |
required |
seeds
|
Bool[ArrayLike, 'H W']
|
The in-plume set, |
required |
threshold
|
Float[ArrayLike, '']
|
|
required |
exclude
|
Bool[ArrayLike, ' C H W'] | None
|
The scene's |
None
|
box
|
int
|
Margin, in pixels, around the seeds' bounding box (static); the reference code's 200. |
200
|
Returns:
| Type | Description |
|---|---|
Bool[Array, 'H W']
|
The pool, |
Bool[Array, 'H W']
|
|
Raises:
| Type | Description |
|---|---|
ParameterError
|
If |
ShapeError
|
If |
DegenerateInputError
|
If the pool is empty (no seeds, or no pixel
near them passes the cuts). Under |
Examples:
>>> import jax.numpy as jnp
>>> from plumax.vetting import background_pool
>>> mf = jnp.zeros((6, 6)).at[0, :].set(5.0)
>>> seeds = jnp.zeros((6, 6), bool).at[2, 2].set(True)
>>> candidate = jnp.zeros((1, 6, 6), bool).at[0, 2:4, 2:4].set(True)
>>> pool = background_pool(
... mf, seeds, threshold=1.0, exclude=candidate, box=1
... )
>>> print(pool.astype(int))
[[0 0 0 0 0 0]
[0 1 1 1 0 0]
[0 1 0 0 0 0]
[0 1 0 0 0 0]
[0 0 0 0 0 0]
[0 0 0 0 0 0]]
Source code in src/plumax/vetting/seeds.py
115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 | |
null — a null from the same scene¶
Plume-candidate vetting: is a matched-filter candidate the gas, or clutter?
Candidate-level vetting (Xiang et al. 2024; Růžička et al. 2026) as L1
functions on arrays: no carrier, no geotoolz. A candidate is a boolean mask
on a radiance cube and its matched-filter map; this package chooses which of
its pixels count as in (the seeds) and which as out (the background pool),
and runs radtrax's plume-agnostic absorption-shape test on them
(radtrax.retrieval.absorption: the pairing, the ratio spectrum, the fit
to the gas's transmittance and D_norm). Theory: the plume-vetting page.
seeds:select_seedsandbackground_pool, a candidate's in-plume seeds and the background pool they are paired from (#154).null:null_masksandnull_pvalue, rotated and translated copies of a candidate's mask off every candidate, and the candidate's p-value among their scores (#155).vet:vet_candidateandVettingResult, one candidate vetted in one call: seeds, pool, radtrax's pairs, ratio, fit andD_norm(#156).
null_masks
¶
null_masks(mask: Bool[ArrayLike, 'H W'], n: int, *, key: PRNGKeyArray, exclude: Bool[ArrayLike, ' C H W'] | None = None, min_fraction: float = 0.9) -> Bool[np.ndarray, 'S H W']
n rotated and translated copies of a mask, off every candidate.
Each attempt draws an angle θ ~ U[0°, 360°) and rotates mask
about the image centre by θ (nearest neighbour, the image's shape
kept); a rotation that keeps fewer than min_fraction · |M| pixels is
discarded. Among the integer shifts that keep the rotated mask inside
the image, the free ones are those where
M_s ∩ (M ∪ ⋃_c M_c) = ∅,
with M_c the C candidate masks in exclude; one free shift is
drawn uniformly, and an angle with none is discarded. So a copy never
touches a candidate, this one included, and given its angle its place is
uniform over the free places (found exactly, by correlating the rotated
mask with the candidates, rather than by rejecting random shifts, which
a mask covering a large part of the scene almost never passes). Copies
may overlap one another. They are returned in the order they were drawn,
so the first m of n copies are the m copies the same key
gives.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
mask
|
Bool[ArrayLike, 'H W']
|
The candidate |
required |
n
|
int
|
Number of copies |
required |
key
|
PRNGKeyArray
|
A |
required |
exclude
|
Bool[ArrayLike, ' C H W'] | None
|
The scene's |
None
|
min_fraction
|
float
|
A copy keeps at least this fraction of |
0.9
|
Returns:
| Type | Description |
|---|---|
Bool[ndarray, 'S H W']
|
The copies, |
Raises:
| Type | Description |
|---|---|
ParameterError
|
If |
ShapeError
|
If |
DegenerateInputError
|
If |
Examples:
>>> import jax
>>> import numpy as np
>>> from plumax.vetting import null_masks
>>> mask = np.zeros((32, 32), bool)
>>> mask[4:8, 4:12] = True
>>> copies = null_masks(mask, 5, key=jax.random.key(0), exclude=mask[None])
>>> copies.shape
(5, 32, 32)
>>> bool((copies & mask).any())
False
>>> bool((copies.sum(axis=(1, 2)) >= 0.9 * mask.sum()).all())
True
Source code in src/plumax/vetting/null.py
30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 | |
null_pvalue
¶
The fraction of null copies that score at least as well as the candidate.
With D the candidate's score and D_s the S' finite scores of
its null copies (non-finite ones, copies that could not be scored, are
left out),
p̂ = (1/S') ∑_s 1[D_s ≤ D],
ties counting as at least as good. Lower scores are better, as for
D_norm; for a score where higher is better (a likelihood ratio),
pass both negated. p̂ moves in steps of 1/S' and p̂ = 0 says
only that no copy did as well; the Monte Carlo p-value, never zero and
valid when the candidate and the copies are exchangeable under the null
(Phipson and Smyth 2010), is (1 + S'·p̂) / (1 + S').
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
score
|
float
|
The candidate's score |
required |
null_scores
|
Float[ArrayLike, ' S']
|
The scores of its null copies, |
required |
Returns:
| Type | Description |
|---|---|
float
|
|
Raises:
| Type | Description |
|---|---|
ShapeError
|
If |
DegenerateInputError
|
If |
Examples:
>>> import numpy as np
>>> from plumax.vetting import null_pvalue
>>> null_pvalue(0.3, np.array([0.2, 0.3, 0.5, 0.9]))
0.5
>>> null_pvalue(0.1, np.array([0.2, np.nan, 0.5]))
0.0