Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

The geo data stack

Report 16 · Realignment plan for pipekit, xrtoolz and geotoolz across 18 distributions

Abstract

Three repositories ship 18 distributions: pipekit (7), xrtoolz (6) and geotoolz (5), and none of them merges into another. pipekit is the composition layer, geotoolz-cloud, geotoolz-catalog and geotoolz-patcher serve both carrier lanes, geotoolz and geotoolz-products are the GeoTensor lane, and the xrtoolz family is the xarray lane. We run one synthetic methane scene through the packages that touch it (pipekit and pipekit-array, geocatalog, geopatcher, geotoolz and the xrtoolz core, reader and top-level package), and the matched filter’s noise lands within 3.2 % of its closed form. However, six seams still need work: 1) two Signature classes that cannot share a pipeline, 2) eight model wrappers for three backends, 3) shared xarray vocabulary that lives in a downloader, 4) a training dataset that imports a geopatcher module which was removed on 8 October, 5) no rule for where a mission reader lives, and 6) a distribution name on PyPI that belongs to another project. The running example exercises four of them on the pinned code (the signatures, the vocabulary’s dependency, the training dataset and the dependency names). We read the model wrappers and the mission readers from the source in the audit, and the run does not import pipekit-jax, geotoolz-products, xrtoolz-einx, xrtoolz-grad or xrtoolz-sklearn. In this page we decide each seam, and we pull together the open questions which Reports 3, 6, 7 and 11 left on them.

Keywords:pipekitgeotoolzgeotoolz-cataloggeotoolz-patchergeotoolz-cloudgeotoolz-productsxrtoolzpackage ownership

Summary

Decision. No package merges into another, and the lanes stay apart, so no GeoTensor package imports an xarray one or the reverse. Each overlap below gets one owner, every move but one leaves a deprecated alias for the 0.x series (the exception is the window dataset’s old import, which does not work today), and the stack publishes under names which nobody else holds.

End state. pipekit-core holds every carrier-agnostic contract and imports nothing from the geo stack. geotoolz-cloud moves bytes, geotoolz-catalog says which data, geotoolz-patcher says which piece, geotoolz computes on GeoTensors, geotoolz-products decodes gridded missions, and the xrtoolz family computes on xarray over xrtoolz-core’s vocabulary. All 18 distributions install from PyPI into one environment.

Ownership rules

Foundations sit at the top and every arrow points up, which means that a package only imports from the tiers above it and that the two lanes never import each other (Figure 1; the tiers are in Table 1). The geophysical model packages which sit on this stack follow their own rules.

Tier 1: pipekit-core, and the external georeader, xarray and obstore. Tier 2: the pipekit subpackages, geotoolz-cloud and xrtoolz-core. Tier 3, shared by both lanes: geotoolz-catalog and geotoolz-patcher, with the proposed integrations.train module. Tier 4: the GeoTensor lane (geotoolz, geotoolz-products) and the xarray lane (xrtoolz, its einx, grad and sklearn bridges, and xrtoolz-reader), which never import each other. Scripts at the bottom.

Figure 1:The geo data stack after the realignment. Solid arrows are base dependencies, dashed ones optional extras; the dashed blue box is the one new surface.

  1. pipekit-core (import pipekit) owns Operator, Sequential, Graph, Signature, serialisation, control flow, the parallel maps, observation, QC and the shared protocols. It has no dependencies, and it never imports xarray, georeader or a geo package.

  2. pipekit-array, -cycle, -evaluate, -experiment, -jax and -train own the orchestration on top of that contract: Array API operators, cycles, benchmarks, tracking, the one Equinox model operator, and training loops. None of them imports a geo package (today pipekit-train does; see Section ??).

  3. geotoolz-cloud (import geocloud) owns object storage: the process-wide obstore pool, URI verbs, credentials, local copies, COG and HDF reads and writes, and the fsspec view. It imports georeader, obstore and rasterio, and never pipekit or another geo package.

  4. geotoolz-catalog (import geocatalog) owns which data: GeoSlice, catalogs and their backends, discovery sources with footprints (STAC, CMR, earthaccess, GEE), matchup, staging and field_for. It serves both lanes, and it only reaches geocloud and geopatcher through its [cloud] and [patch] extras.

  5. geotoolz-patcher (import geopatcher) owns which piece for every carrier: split and merge, temporal stencils, the patch cache, and the integrations which put patching into a pipekit graph or (after D) into a pipekit-train dataset. It only imports pipekit and geocloud inside those integrations and fields.

  6. geotoolz-products (import geoproducts) owns the decoding of missions whose output is a georeferenced raster: GOES-R ABI, Himawari AHI, Carbon Mapper and the toy sensor. Its presets bind geotoolz operators to channel names, and they only import geotoolz inside the [operators] extra.

  7. geotoolz owns the operators on GeoTensor: reads of one source, radiometry, indices, masks, matched filters, plume quantification, and the GeoTensor marshalling for models. It imports pipekit, it only imports geopatcher and geocloud through extras, and it never imports the xarray-lane packages.

  8. xrtoolz-core (import xrcore) owns the xarray Operator and, after C, the shared xarray vocabulary: the CF variable registry, validation and subsetting. It imports pipekit, xarray and NumPy, and after C also pandas, which the moved subsetting code uses and which it declares directly (xarray already requires it, so the install does not grow).

  9. xrtoolz-einx, xrtoolz-grad and xrtoolz-sklearn own labelled einsum, finite differences and the one xarray scikit-learn bridge. They import xrtoolz-core at most.

  10. xrtoolz-reader (import xrreader) owns the request-subset archives (CMEMS, CDS, AEMET) and the mission readers whose output is a swath, 2-D latitude and longitude, profiles or stations. After C, it imports the vocabulary from xrtoolz-core.

  11. xrtoolz owns diagnostics, metrics, and ocean and atmosphere physics on xarray. It imports the xrtoolz family and pipekit, and after C it only imports xrtoolz-reader through a [reader] extra.

  12. The lanes meet only in scripts. A script which needs both lanes passes arrays between them. Radiometry and spectral indices stay in geotoolz, and xrtoolz does not grow a second copy.

Table 1:The dependency tiers after the realignment.

TierPackagesMay importRole
1 Foundationspipekit-core; georeader, xarray, obstore (external)nothing in the stackcontracts, carriers, bytes
2 Framework and storagepipekit-array, -cycle, -evaluate, -experiment, -jax, -train; geotoolz-cloud; xrtoolz-coretier 1orchestration; object storage; the xarray operator and vocabulary
3 Shared geogeotoolz-catalog, geotoolz-patchertiers 1–2 (pipekit and geocloud through extras)which data, which piece
4 Lanesgeotoolz, geotoolz-products · xrtoolz, -einx, -grad, -sklearn, -readertiers 1–3, never the other lanecompute and decode on one carrier
5 Entry pointsgeostack scripts and notebookseverythingcompose both lanes

The running example: one methane scene through both lanes

The problem. A 256 × 256 scene of 30 m pixels in UTM zone 13N, observed in NSy=40N_{\mathcal{S}_y} = 40 SWIR bands from 2100 to 2450 nm on 15 July 2026, contains a methane plume. The goal is the emission rate Q∈R≥0Q \in \mathbb{R}_{\ge 0} in kg s⁻¹. The scene is synthetic, so every stage has an exact answer which we can check against. The physics of the methane chain is scoped in GeoModels: the retrieval in radtrax · 1, the emission rate in plumax · 1, and where each piece moves in the package boundaries page. This page only checks how the data stack carries it.

The data. Let yn∈RNSy\vect{y}_n \in \mathbb{R}^{N_{\mathcal{S}_y}} be the radiance spectrum of pixel nn, μy∈RNSy\gvect{\mu}_y \in \mathbb{R}^{N_{\mathcal{S}_y}} the background mean, Σy∈RNSy×NSy\gmat{\Sigma}_y \in \mathbb{R}^{N_{\mathcal{S}_y} \times N_{\mathcal{S}_y}} the background covariance (rank 6 plus a diagonal), Jh∈RNSy\vect{J}_h \in \mathbb{R}^{N_{\mathcal{S}_y}} the unit-absorption target (the radiance change per ppm·m of methane, i.e. the Jacobian of the spectrum with respect to the enhancement, which we build from two absorption bands near 2300 and 2360 nm) and αn∈R\alpha_n \in \mathbb{R} the enhancement in ppm·m, which peaks at 1500 ppm·m. Each pixel follows the linear model (1):

yn=μy+Σy1/2zn+αnJh,\vect{y}_n = \gvect{\mu}_y + \gmat{\Sigma}_y^{1/2} \vect{z}_n + \alpha_n \vect{J}_h ,

where the latent zn∈RNSy\vect{z}_n \in \mathbb{R}^{N_{\mathcal{S}_y}} is standard normal and Σy1/2∈RNSy×NSy\gmat{\Sigma}_y^{1/2} \in \mathbb{R}^{N_{\mathcal{S}_y} \times N_{\mathcal{S}_y}} is a square root of the covariance:

zn∼N(0,I),\vect{z}_n \sim \mathcal{N}(\vect{0}, \mat{I}) ,
Σy1/2(Σy1/2)⊤=Σy.\gmat{\Sigma}_y^{1/2} (\gmat{\Sigma}_y^{1/2})^\top = \gmat{\Sigma}_y .

The best that a matched filter can do is set by the noise floor σα=(Jh⊤Σy−1Jh)−1/2∈R>0\sigma_\alpha = (\vect{J}_h^\top \gmat{\Sigma}_y^{-1} \vect{J}_h)^{-1/2} \in \mathbb{R}_{>0}, which is 103.9 ppm·m here.

Stage 1 · Which data (geotoolz-catalog → geotoolz-patcher)

TL;DR. Index the scene file, ask for the day’s slice over the scene, and get back one field that the patcher can split.

Problem. A catalog row holds a footprint, a time interval and a path. A GeoSlice ξ=(B,T,r,crs)\xi = (\mathcal{B}, \mathcal{T}, \vect{r}, \mathrm{crs}) is a request, made of the bounds B⊂R2\mathcal{B} \subset \mathbb{R}^2, a time interval T⊂R\mathcal{T} \subset \mathbb{R}, a resolution r∈R>02\vect{r} \in \mathbb{R}^2_{>0} and a CRS. field_for returns the rows which intersect ξ\xi, mosaicked onto its grid, as one RasterField.

1
2
3
4
5
6
7
8
9
10
11
# Shapes: N_Sy = 40 bands, H = W = 256 pixels
from geocatalog import GeoSlice, build, patch

cat = build.build_raster_catalog([path], filename_regex=r"scene_(?P<date>\d{8})",
                                 date_format="%Y%m%d")            # 1 row        today
# ξ = (𝓑, 𝒯, r, crs): the scene's bounds, 15 July, 30 m, UTM 13N
slice_ = GeoSlice(bounds=(x0, y0 - H * res, x0 + W * res, y0),
                  interval=pd.Interval(day_start, day_end, closed="both"),
                  resolution=(30.0, 30.0), crs="EPSG:32613")     # today
field = patch.field_for(cat, slice_)          # rows ∩ ξ → RasterField (N_Sy, H, W)   today
# → RasterField, domain shape (40, 256, 256)

Catalog to field; nothing in this stage changes under the plan.

This is the seam that Report 6 documents: the catalog only imports geopatcher inside geocatalog.patch, through the [patch] extra, which is why rule 4 holds today.

Stage 2 · Which piece, and the enhancement (geotoolz-patcher → geotoolz)

TL;DR. Cut the scene into overlapping chips, run a matched filter fitted on each chip, and blend the chips back into one enhancement map.

Problem. Chips are Nchip=128N_\text{chip} = 128 pixels on a side at a stride of Nstride=64N_\text{stride} = 64, so the scene of Nscene=256N_\text{scene} = 256 pixels on a side has, by (4),

Nanchor=⌊Nscene−NchipNstride⌋+1=3N_\text{anchor} = \left\lfloor \frac{N_\text{scene} - N_\text{chip}}{N_\text{stride}} \right\rfloor + 1 = 3

anchors per axis, and so Npatch=Nanchor2=9N_\text{patch} = N_\text{anchor}^2 = 9 chips, which we index by pp. On chip pp, the filter fits a median μ^y(p)∈RNSy\hat{\gvect{\mu}}_y^{(p)} \in \mathbb{R}^{N_{\mathcal{S}_y}} and a Ledoit–Wolf shrinkage covariance Σ^y(p)∈RNSy×NSy\hat{\gmat{\Sigma}}_y^{(p)} \in \mathbb{R}^{N_{\mathcal{S}_y} \times N_{\mathcal{S}_y}} Ledoit & Wolf, 2004, and then it estimates the enhancement (5) Thompson et al., 2015 (derived in radtrax · 1, stage 5):

α^n(p)=Jh⊤(Σ^y(p))−1(yn−μ^y(p))Jh⊤(Σ^y(p))−1Jh.\hat{\alpha}_n^{(p)} = \frac{\vect{J}_h^\top (\hat{\gmat{\Sigma}}_y^{(p)})^{-1} (\vect{y}_n - \hat{\gvect{\mu}}_y^{(p)})}{\vect{J}_h^\top (\hat{\gmat{\Sigma}}_y^{(p)})^{-1} \vect{J}_h} .

Each chip carries a separable Hann weight wn(p)=ω(i)ω(j)∈[0,1]w_n^{(p)} = \omega(i) \omega(j) \in [0, 1], where (i,j)(i, j) are the row and column of pixel nn inside chip pp and ω(i)=sin⁡2(π(i+1/2)/Nchip)\omega(i) = \sin^2(\pi (i + 1/2) / N_\text{chip}) is sampled at the pixel centres. The merged map is then the weight-normalised sum over the set Cn\mathcal{C}_n of chips which cover pixel nn, (6):

α^n=∑p∈Cnwn(p)α^n(p)∑p∈Cnwn(p).\hat{\alpha}_n = \frac{\sum_{p \in \mathcal{C}_n} w_n^{(p)} \hat{\alpha}_n^{(p)}}{\sum_{p \in \mathcal{C}_n} w_n^{(p)}} .
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# Shapes: N_Sy = 40 bands, N_chip = 128 chip side, N_patch = 9 chips
from geopatcher import SpatialPatcher, spatial
from geotoolz import matched_filter as mf
from geotoolz.patch_ops import ApplyToChips, GridSampler, MergePatches
from pipekit_ops import Sequential

patcher = SpatialPatcher(
    geometry=spatial.geometry.Rectangular(size=(128, 128)),
    sampler=spatial.sampler.RegularStride(step=(64, 64)),
    window=spatial.window.Hann(),                    # ω(i) = sin²(π (i + ½) / N_chip)
    aggregation=spatial.aggregation.OverlapAdd(),
)                                                                    # today
pipe = Sequential([
    GridSampler(patcher=patcher),                    # (N_Sy, H, W) → 9 × (N_Sy, 128, 128)   today
    # α̂_n^(p) = J_hᵀ Σ̂_y^(p)⁻¹ (y_n − μ̂_y^(p)) / (J_hᵀ Σ̂_y^(p)⁻¹ J_h)
    ApplyToChips(operator=mf.MatchedFilter(target=j_h, fit_on_call=True)),
                                                     # 9 × (N_Sy, 128, 128) → 9 × (128, 128)  today
    # α̂_n = ∑_p w_n^(p) α̂_n^(p) / ∑_p w_n^(p)
    MergePatches(aggregation=spatial.aggregation.OverlapAdd(),
                 domain=field.domain),               # 9 × (128, 128) → (H, W)        today
])
alpha = pipe(field)                                  # GeoTensor (H, W), ppm·m         today
# → background std 107.2 ppm·m against σ_α = 103.9 (+3.2 %); peak 1588 against 1500
# → same CRS and transform as the scene; correlation with the truth 0.73

Split, filter and merge as one pipekit graph.

The same pipeline with an identity operator in place of the filter returns the scene with a maximum absolute error of 0.0, so the split and the merge add nothing of their own. This stage is the payoff of rule 5, as the patcher is carrier-agnostic, the filter is pure GeoTensor, and pipekit’s Sequential joins them.

Stage 3 · Mass and mask (geotoolz)

TL;DR. Turn the enhancement into column mass and keep the pixels that stand clear of the noise.

Problem. Let MCH4=0.01604M_{\mathrm{CH_4}} = 0.01604 kg mol⁻¹ be the molar mass of methane and Vm=0.024465V_m = 0.024465 m³ mol⁻¹ the molar volume of air at 298.15 K and 1 atm. Then the column mass ΔΩn∈R\Delta\Omega_n \in \mathbb{R} of pixel nn, in kg m⁻², is

ΔΩn=10−6α^nMCH4Vm.\Delta\Omega_n = 10^{-6} \hat{\alpha}_n \frac{M_{\mathrm{CH_4}}}{V_m} .

The plume mask P\mathcal{P} is the set of pixels in connected regions of at least 50 pixels where ΔΩn>3σ^ΔΩ\Delta\Omega_n > 3 \hat{\sigma}_{\Delta\Omega}, and σ^ΔΩ∈R>0\hat{\sigma}_{\Delta\Omega} \in \mathbb{R}_{>0} is the robust noise estimate 1.4826mediann∣ΔΩn−median(ΔΩ)∣1.4826 \mathrm{median}_n \vert \Delta\Omega_n - \mathrm{median}(\Delta\Omega) \vert.

1
2
3
4
5
6
7
8
9
# Shapes: H = W = 256
from geotoolz import plume

# ΔΩ = 10⁻⁶ α̂ M_CH₄ / V_m
kg_m2 = plume.ColumnToMass(gas="CH4", units_in="ppm_m")(alpha)   # (H, W) → (H, W)  today
# σ̂_ΔΩ = 1.4826 · median |ΔΩ − median ΔΩ|
sigma_kg = 1.4826 * np.median(np.abs(kg_m2 - np.median(kg_m2)))  # scalar     user
mask = plume.PlumeMask(threshold=3 * sigma_kg, min_area_px=50)(kg_m2)  # (H, W) bool  today
# → σ̂_ΔΩ = 111.2 ppm·m equivalent; 1246 plume pixels, 3 outside the true 3σ_α region

Column mass and a 3σ̂ mask.

ColumnToMass matches the closed form (7) to 1e-12. Stage 3 only uses geotoolz, so nothing in it moves.

Stage 4 · Wind (xrtoolz → xrtoolz-core)

TL;DR. Take the 10 m wind from a reanalysis cube at the overpass and turn it into an effective wind for the plume.

Problem. Let u10u_{10} and v10v_{10} be the 10 m wind components (scalars per grid point and time step) on an ERA5-shaped grid of 24 hourly steps and 5 × 5 points, with u10=3+0.5sin⁡(2πt/24)u_{10} = 3 + 0.5 \sin(2\pi t / 24) m s⁻¹ at hour tt and v10=4v_{10} = 4 m s⁻¹. The wind speed is (8), and the effective wind of Varon et al. Varon et al., 2018 is (9):

U10=u102+v102,U_{10} = \sqrt{u_{10}^2 + v_{10}^2} ,
Ueff=0.33U10+0.45.U_\text{eff} = 0.33 U_{10} + 0.45 .
1
2
3
4
5
6
7
8
9
# Shapes: T = 24 hourly steps, 5 × 5 grid
from xrtoolz.atm import WindSpeed

# U₁₀ = √(u₁₀² + v₁₀²)
ws = WindSpeed(u="u10", v="v10", name="ws10")(era5)["ws10"]       # (T, 5, 5) → (T, 5, 5)  today
U10 = float(ws.interp(time=overpass, latitude=39.5, longitude=-105.5))  # → scalar  today
# U_eff = 0.33 U₁₀ + 0.45
U_eff = 0.33 * U10 + 0.45                                          # scalar     user
# → U₁₀ = 4.722 m s⁻¹, U_eff = 2.008 m s⁻¹

Wind speed on xarray, interpolated to the overpass.

WindSpeed writes CF attributes through apply_cf_attrs and the Variable registry, which it imports from xrtoolz-reader today. After C, the same names come from xrtoolz-core and nothing in this block changes. A real run would download the cube with xrreader.CDSSource, whereas here we build it in memory so that the page needs no credentials. This UeffU_\text{eff} is calibrated for a plume length L=AL = \sqrt{A} (with A∈R>0A \in \mathbb{R}_{>0} the plume’s area), and stage 5 passes it to an operator whose length is the chord (see the note there).

Stage 5 · Emission rate (geotoolz)

TL;DR. Sum the mass inside the mask and divide by the time the wind takes to carry it the plume’s length.

Problem. geotoolz’s IMEEstimate sums the mass over the mask (with Apx=900A_\text{px} = 900 m² per pixel) and returns Q=UeffIME/ℓQ = U_\text{eff} \mathrm{IME} / \ell, where ℓ∈R>0\ell \in \mathbb{R}_{>0} is the mask’s longest axis (the chord), along with σQ=0.5Q\sigma_Q = 0.5 Q at its default fraction. The method and its limits are derived in plumax · 1.

1
2
3
4
# Shapes: H = W = 256; U_eff scalar from stage 4
# IME = ∑_{n∈𝒫} ΔΩ_n A_px,  Q = U_eff IME / ℓ
est = plume.IMEEstimate(wind_speed=U_eff)(kg_m2, mask)   # (H, W), (H, W) → dict   today
# → IME 534.9 kg, ℓ 1651.1 m, Q 0.651 kg s⁻¹, σ_Q 0.325 kg s⁻¹

The emission rate from the GeoTensor and xarray stages together.

The script passes one float from the xarray lane to the GeoTensor lane and nothing imports across, which is rule 12. Within the mask the true IME is 584.6 kg, so the estimate is 8.5 % low. Fitting μ^y(p)\hat{\gvect{\mu}}_y^{(p)} and Σ^y(p)\hat{\gmat{\Sigma}}_y^{(p)} on chips that contain the plume absorbs part of the signal, and with the true μy\gvect{\mu}_y and Σy\gmat{\Sigma}_y the same mask gives +1.7 % (the notebook runs both and asserts both). That is a retrieval choice and not a packaging one, so it is out of scope here.

Stage 6 · The seams, as they stand (pipekit, xrtoolz, geotoolz-patcher)

TL;DR. Exercise the four places where today’s packages disagree, each of which becomes a workstream.

Problem. A Signature is a symbolic record of named dimensions and a dtype, sig=((d1,N1),…,(dR,NR);τ)\mathrm{sig} = ((d_1, N_1), \ldots, (d_R, N_R); \tau) for RR dimensions with names did_i, sizes Ni∈NN_i \in \mathbb{N} and the dtype τ\tau, which Sequential.summary threads through every step. For example, a mean over axis 0 maps ((d1,N1),…,(dR,NR))↦((d2,N2),…,(dR,NR))((d_1, N_1), \ldots, (d_R, N_R)) \mapsto ((d_2, N_2), \ldots, (d_R, N_R)).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# Shapes: T = 24, 5 × 5 grid
wind_mean = Sequential([WindSpeed(u="u10", v="v10", name="ws10"),
                        MeanScalar(axis=0)])                       # today
# (a) two Signature classes                                           → workstream A
wind_mean.summary(xrcore.Signature({"time": 24, "latitude": 5, "longitude": 5}))
# → ValueError: too many values to unpack (expected 2)               today
wind_mean.summary(pipekit_ops.Signature(dims=(("time", 24), ("latitude", 5),
                                           ("longitude", 5))))
# → last step: Signature((latitude=5, longitude=5) float64)           today

# (b) the window dataset's stencil import                              → workstream D
import geopatcher.time
# → ModuleNotFoundError: No module named 'geopatcher.time'           today
from pipekit_train import XarrayWindowDataset
# → ImportError: "XarrayWindowDataset requires `geopatcher` … not published
#   on PyPI … `uv pip install -e ../geopatcher`"                      today

# (c) a pure-numpy stencil import loads the raster stack               → workstream D
import geopatcher.temporal.stencils   # sys.modules gains georeader, rasterio, shapely   today

# (d) base installs                                                    → workstreams C, F
importlib.metadata.requires("xrtoolz")   # includes xrtoolz-reader>=0.0.2       today
importlib.metadata.requires("geotoolz")  # includes pipekit>=0.0.2 (another project on PyPI)

The seams the workstreams fix, run against the pinned revisions.

The XarrayWindowDataset message is wrong as well as unlucky, because geopatcher is installed and the module it wants was renamed. Report 11 records the same break as its A5.

Current-state audit

Six overlaps need action, whereas seven look like duplication and are not (Table 2; Figure 2 shows what the page’s sketches call). The audit read references/geostack/pipekit at ef3c17f, references/geostack/geotoolz at 088475e and references/geostack/xrtoolz at 308c7f2 on 14 October 2026.

A grid with one row per stage of the running example and per workstream, and one square per stack call in that row's sketches, coloured by its marker. Of 42 calls, 29 run today, 11 are proposed by this report's workstreams and 2 are code the reader writes.

Figure 2:Of the page’s 42 marked stack calls, 29 run today, 11 are proposed by this report’s workstreams and 2 are code the reader writes (how to read it: conventions).

Table 2:Overlaps across the three repositories.

SymbolsDefined inUsed byVerdict
Signaturepipekit/signature.py:29 (ordered pairs); xrcore/signature.py:64 (mapping)pipekit-array reduce.py; 18 constructions in 5 modules of xrtoolz and xrtoolz-einxSplit ownership. Two layouts; mixing them in one Sequential fails (stage 6a)
nested_config, callable_name, require_operatorpipekit/_base/operator.py:282, 293, 302geotoolz _src/config.py, augment/_src/operators.py; geopatcher integrations/pipekit.pySplit ownership. Private API used by two packages
JaxModelOp ×2, EquinoxModelOp, ModelOp ×2, SklearnModelOp, SklearnOp ×2pipekit_jax/model_op.py:42; pipekit_train/adapters/equinox.py:66; xrtoolz/inference/modelop.py:30, 227, 257; xrsklearn/_src/operator.py:32; geotoolz/learn/_src/operators.py:45, 469EquinoxModelOp at 13 sites in pipekit-train; SklearnModelOp at 4True duplicate. Three backends, eight classes, one name for two unrelated classes
Variable, REGISTRY, apply_cf_attrs, subset_bbox, subset_time, subset_wherexrreader/types/_src/ (2,341 lines; NumPy, pandas, xarray and the standard library)8 imports in 7 xrtoolz modules (atm, geo, viz)Split ownership. Vocabulary in the downloader; xrtoolz requires xrtoolz-reader
XarrayWindowDataset, _xreader.training_iteratorpipekit_train/xarray_window.py:134, _xreader.py:347pipekit-train usersSplit ownership. Imports geopatcher.time (xarray_window.py:35, _xreader.py:380), removed by geotoolz #431
open_emit_ch4_l2b, open_ghgsat_ch4_l2, open_tropomi_ch4_l2 · goes, himawari, carbonmapper readersxrreader/_src/local/openers/ (379 lines) · geoproducts/xrreader users · geoproducts usersSplit ownership. Two homes for mission readers, no rule between them (Report 3, question 7)
exact_quotient · count_stepsgeopatcher/_src/temporal/stencils.py:55 · geocatalog.gridtemporal stencils · GeoSlice alignmentHealthy. Named for what they do since #434; the draft’s divide_evenly clash is gone
obstore pool, files, cache, cog, hdfgeocloudgeocatalog staging, geopatcher CogField, geotoolz io.ReadHDF / ReadNetCDF, geoproducts HDF5Healthy. One pool per process, one home since #431
pipekit_ops.Cache · geopatcher.run.PatchCache · geocloud.cache · xrreader cache_pathfour packagesHealthy. Operator results, patches across runs, local copies of remote objects, downloads: four jobs
geoproducts.goes.TrueColor · geotoolz.viz.TrueColorgoes/presets.py:152 · viz/_src/operators.py:124Healthy. The preset binds ABI channels to the geotoolz operator and imports it lazily
geotoolz.einx · xreinxboth lanesHealthy. Each speaks its carrier: transform survival on GeoTensor, dimension names on xarray
geocatalog Source · xrreader DataSourcegeocatalog.sources · xrreader/_src/base.pyHealthy. File inventories with footprints against server-side subsets
pipekit_ops.Retry · geocatalog.utils.retry_transient_ioHealthy. Retry any operator in a graph against retry one remote read

Smaller findings:

Workstream A · pipekit owns the composition contract

Decision. There is one Signature, which is pipekit’s, and the helpers that other packages import from pipekit are public (Table 3).

Table 3:Workstream A’s moves.

SymbolDestinationReason
Signature (ordered (name, size) pairs)stays in pipekit_ops.signaturepositional operators (pipekit-array) need axis order
Signature(dims) accepting a mapping as well as pairs, Signature.from_sizes(mapping, dtype), .sizes (read-only mapping)pipekit_ops.signature, newxrtoolz builds and compares signatures by name; a dict’s order is the axis order, so xrcore.Signature({"time": 24, …}) keeps its call semantics, not only its import
dtype canonicalisation without NumPy: a table of the accepted spellings, each mapped to the name np.dtype(x).name gives ("f8", "<f8", "float64", float → "float64"; "f4", "<f4" → "float32"; the integer, unsigned, bool and complex codes likewise, with any byte-order prefix); a dtype object gives its .name and a scalar type its __name__, both then looked up in the same table; anything else raises TypeError naming the inputpipekit_ops.signaturexrcore’s one behaviour pipekit lacks; pipekit-core stays dependency-free, and gate A1 checks the table against np.dtype(x).name for every entry, so equal dtypes give equal signatures
xrcore.Signaturere-export of pipekit_ops.Signaturefrom xrcore import Signature keeps working
nested_config, callable_namepipekit_ops.configused by geotoolz and geopatcher
require_operatorpipekitused by geotoolz augment
  1. Widen pipekit’s class. We let the constructor take a mapping as well as ordered pairs (a mapping is converted to pairs in insertion order), and we add from_sizes, sizes and dtype canonicalisation, while dims stays as ordered pairs. Existing xrcore.Signature({...}) calls keep working unchanged.

  2. Re-export it from xrcore. We replace xrcore/signature.py with the re-export, construct with Signature.from_sizes(ds.sizes), and compare with a.sizes == b.sizes wherever order should not matter.

  3. Publish the helpers. We add the three names to pipekit’s public surface and replace the three private import statements.

1
2
3
4
5
6
7
8
9
10
11
# Shapes: T = 24, 5 × 5 grid
# Today:
sig = xrcore.Signature({"time": 24, "latitude": 5, "longitude": 5})   # today
wind_mean.summary(sig)                       # → ValueError               today
# After:
# {d: N} in insertion order ↦ ((d₁, N₁), …, (d_R, N_R))
sig = xrcore.Signature({"time": 24, "latitude": 5,
                        "longitude": 5})     # the same call     proposed (A)
xrcore.Signature is pipekit_ops.Signature        # → True               proposed (A)
sig == pipekit_ops.Signature.from_sizes(sig.sizes)   # → True           proposed (A)
wind_mean.summary(sig)                       # last: (latitude=5, longitude=5)  proposed (A)

Stage 6a, before and after workstream A.

This revisits nothing in Report 2. Its Signature stays, and it only gains the mapping view that Report 3 found missing.

Workstream B · One model-operator family

Decision. Each lane owns how its carrier becomes an array and back, and each backend is wrapped once (Table 4).

Table 4:Workstream B’s moves.

SymbolDestinationReason
pipekit_jax.JaxModelOpstays: the one Equinox wrapperweight round-trip through the model registry
pipekit_train.adapters.equinox.EquinoxModelOpdeprecated alias of the above; the adapter returns JaxModelOpReport 11’s B1; it exists only so pipekit-train avoids pipekit-jax
pipekit-cycle in pipekit-jax’s basepipekit-jax[diffrax]only diffrax_model.py needs it, for RecursiveCheckpointAdjoint; this removes the reason for the twin
xrtoolz.inference.ModelOpstays: xarray marshalling for any callablemethod=None means “call the model”
xrtoolz.inference.JaxModelOp(model, *, jit=True, **kw)deprecated wrapper: warns, wraps model in jax.jit, imported lazily, when jit is true (today’s default and today’s implementation), and builds ModelOp(model, method=None, **kw)answers Report 3’s question 4; existing calls with jit= keep working for the deprecation period, and the wrapper adds no dependency: xrtoolz declares neither JAX nor Equinox, and Equinox stays out
xrtoolz.inference.SklearnModelOpdeprecated alias of xrsklearn.SklearnOpone xarray scikit-learn bridge
geotoolz.learn.ModelOp, SklearnOpstayGeoTensor marshalling
  1. Return JaxModelOp from the Equinox adapter, as Report 11’s B1 says, and alias EquinoxModelOp to it.

  2. Move pipekit-cycle into pipekit-jax’s [diffrax] extra, so that pipekit-train’s [equinox] extra can depend on pipekit-jax.

  3. Alias the two xrtoolz wrappers and teach ModelOp to accept method=None.

1
2
3
4
5
6
7
8
9
# Shapes: X (T, 5, 5, N_f = 4) features; g_net(·; θ) : ℝ^N_f → ℝ^N_out, N_out = 1
# Today:
op = xrtoolz.inference.JaxModelOp(net, feature_dim="feature")     # today
# After:
model = pipekit_jax.JaxModelOp(net)          # serialize_weights()  today
# stack non-feature dims: (T, 5, 5, N_f) → (N = 25 T, N_f);  ŷ_n = g_net(x_n; θ): (N, N_out)
op = xrtoolz.inference.ModelOp(model, method=None, feature_dim="feature",
                               output_dim="output")             # proposed (B)
# → (T, 5, 5, output = 1)

A per-pixel Equinox emulator on the stage 4 cube, before and after.

Workstream C · xrtoolz-core owns the shared xarray vocabulary

Decision. The names that every xarray operator speaks live in xrtoolz-core, and xrtoolz-reader keeps its archives and readers (Table 5).

This corrects one point of the 7 October draft. xrtoolz does not import BBox or TimeRange from xrreader; it imports Variable, REGISTRY, apply_cf_attrs and the three subset functions.

Table 5:Workstream C’s moves.

SymbolDestinationReason
BBox, TimeRange, DepthRange, PressureLevelsxrcore.typesrequest vocabulary, shared with every reader
Variable, REGISTRY, resolve, registerxrcore.typesCF variables used by xrtoolz.atm, geo and viz
apply_cf_attrsxrcore.typesused by xrtoolz.atm
validate_dataset, validate_variable, ValidationReportxrcore.typesthe validators belong beside the registry they check against
subset_bbox, subset_time, subset_wherexrcore.typesused by xrtoolz.geo
Request, Station, StationCollectionstay in xrreader.typesreader-only
xrtoolz-reader in xrtoolz’s base dependenciesa [reader] extrathe base install stops pulling the archive code
xrtoolz.rs, xrtoolz.iceremovedempty; radiometry and indices are geotoolz’s (rule 12)
  1. Move the modules from xrreader/types/_src/ into xrcore/types/. They need NumPy, pandas, xarray and the standard library. Today pandas is only in xrtoolz-core’s tree through xarray, so the move adds pandas to xrtoolz-core’s dependencies, because a package declares what it imports (rule 8).

  2. Re-export from xrreader.types for the 0.x series.

  3. Repoint xrtoolz’s eight imports, move xrtoolz-reader to [reader], and delete the empty rs and ice packages.

1
2
3
4
5
# Today:
from xrreader.types import Variable, apply_cf_attrs, subset_bbox      # today
# After:
from xrcore.types import Variable, apply_cf_attrs, subset_bbox        # proposed (C)
from xrreader.types import Variable          # still works, same object      proposed (C)

Stage 4’s imports, before and after workstream C.

Workstream D · geotoolz-patcher owns windows for every carrier

Decision. Windowed sampling for training lives with the stencils that define it, in geotoolz-patcher, and nothing in pipekit imports a geo package (Table 6).

Table 6:Workstream D’s moves.

SymbolDestinationReason
from geopatcher.time import …from geopatcher.temporal.stencils import …D1: the module was renamed in #431
XarrayWindowDataset, _xreadergeopatcher.integrations.train, behind geotoolz-patcher[train] (pipekit-train, xarray, zarr, Grain)D2: removes the only pipekit → geo edge
from pipekit_train import XarrayWindowDatasetremoved, with no shim; the notebooks of Reports 11 and 15, which import it through a geopatcher.time alias, already try geopatcher.integrations.train firstit raises ImportError without that alias (stage 6), and a re-export would bring back the pipekit → geo edge D2 removes
the raster fields, reprojection, matched fields and the RasterDomain aliasimported on first use from the package rootD3: a stencil import should load NumPy only
xrpatcher in xrtoolz’s design docsgeopatcher’s XarrayField, GridSampler, MergePatchesD4: one patcher for both lanes

D1 · Fix the import

This is Report 11’s A5: we import from geopatcher.temporal.stencils and make the error name the missing module. It ships first, because the dataset does not import at all today.

D2 · Move the dataset

XarrayWindowDataset subclasses pipekit-train’s TrainingDataset and rides its Grain path, so it needs pipekit-train, whereas the stencils need geopatcher. The package that knows both is the one which already has the integration modules: geopatcher has integrations/pipekit.py for operator graphs, and it gains integrations/train.py. Two callers in this repository use the old path, which are the notebooks of Reports 11 and 15. Both import from geopatcher.integrations.train when it exists and fall back to pipekit-train otherwise, so they keep running on either side of D2.

D3 · A cheap stencil import

The package root imports the raster field, the reprojection and the matched fields, which import georeader, rasterio and shapely at module scope. We load them on first use from the root (as geocatalog already does for its extras), so that geopatcher.temporal.stencils loads NumPy and nothing heavier. This is the lazy half of Report 7’s question 3. Moving them behind a [raster] extra is the other half, and it stays open.

D4 · The xarray lane uses geopatcher

We mark ob-1.1-xrpatcher-integration.md as superseded, rewrite the ownership map of boundaries.md from this page’s rules, and change the interpolate/_src/downscale.py docstring to compose with GridSampler and MergePatches.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# Shapes: T = 24 hourly steps; input window 6 h, target window 1 h
# Today:
from pipekit_train import XarrayWindowDataset      # → ImportError        today
# After:
from geopatcher.temporal.stencils import TimeStencil                  # today
from geopatcher.integrations.train import XarrayWindowDataset   # proposed (D2)
# x = (u, v)[t₀−5h … t₀]: (2, 6, 5, 5);  y = (u, v)[t₀+1h]: (2, 1, 5, 5)
ds = XarrayWindowDataset(
    source={"input": era5, "target": era5},
    stencils={"input": TimeStencil(start="-5h", stop="0h", step="1h", closed="both"),
              "target": TimeStencil(start="1h", stop="1h", step="0h", closed="both")},
    store_version="stage4-wind", split_guard=0,
)                                                               # proposed (D2)
x, y = ds[0]                         # (2, 6, 5, 5), (2, 1, 5, 5)  proposed (D2)
# valid origins: |𝒪| = T − a₋ − a₊ = 24 − 5 − 1 = 18         today (stencils)

Training windows from the stage 4 cube, before and after workstream D.

The number of valid origins, the size of the set O\mathcal{O} in (10), follows from the stencils. With a−=5a_- = 5 steps of history and a+=1a_+ = 1 step of lead, an origin t0t_0 is only valid when both windows fit inside the T=24T = 24 steps, which today’s valid_origin_points confirms on the stage 4 cube:

∣O∣=T−a−−a+=18.\vert \mathcal{O} \vert = T - a_- - a_+ = 18 .

Workstream E · A mission reader lives with its output carrier

Decision. A reader whose output is a georeferenced raster (an affine transform and a CRS) belongs to geotoolz-products, whereas one whose output is a swath, 2-D latitude and longitude, profiles or stations belongs to xrtoolz-reader.

This answers Report 3’s question 7, as the TROPOMI reader stays on the xarray side (Table 7).

Table 7:Workstream E’s placements.

ReaderOutputLives in
GOES-R ABI, Himawari AHI, Carbon Mapper, toy sensorgridded GeoTensorgeotoolz-products (today)
TROPOMI CH₄ L2(scanline, ground_pixel) swathxrtoolz-reader (today)
GHGSat CH₄ L2(y, x) with 2-D latitude and longitudexrtoolz-reader (today)
EMIT L2B CH₄ enhancement, NetCDF container(time, y, x) with 2-D latitude and longitudexrtoolz-reader (today)
EMIT L2B CH₄ enhancement, distributed GeoTIFFgridded GeoTensorgeotoolz-products, new: xrreader’s opener declines this format
  1. Write the rule into both packages’ contributing guides and into AGENTS.md.

  2. Add the EMIT L2B GeoTIFF reader to geotoolz-products, which is the one format the rule assigns there that neither package reads today.

  3. Index both packages’ subpackages in the capability index, so that a mission name which is read in two places is visible (Report 3).

1
2
3
# Shapes: H × W ortho grid
enh = geoproducts.emit.Reader(tif_path).read()        # GeoTensor (H, W)   proposed (E)
ds = xrreader.open_emit_ch4_l2b(nc_path)               # Dataset (time, y, x)  today

One EMIT plume through each reader, after workstream E.

Workstream F · Packaging and release

Decision. The stack publishes under names it owns, it pins itself by version range, and every repository tests its own packaging.

F1 · Names

pipekit publishes as pipekit-core and keeps import pipekit. This revisits Report 2’s settled question 1: the import name stays pipekit, but the distribution name cannot, because pipekit on PyPI belongs to another project. The same decision serves the GeoML stack, which also depends on pipekit. As for Report 6 and Report 7, the public names are geotoolz-catalog and geotoolz-patcher, as the monorepo already builds them.

Recommended: pipekit-core. Alternative: a new import name as well, which renames every import in three repositories for no functional gain.

F2 · Pins and floors

  1. Replace git pins with version ranges once a tier is on PyPI, and drop the pipekit entries of both workspace roots from [tool.uv.sources].

  2. Raise pipekit-train’s zarr floor to zarr>=3.0 in its cache, s3, gcs and xreader extras.

  3. Fix the xrtoolz hint, either by adding oceanography = ["gsw"] or by changing it to pip install gsw.

F3 · Packaging tests

We copy geotoolz’s tests/test_packaging.py (which fails when a module-level import is missing from [project.dependencies]) and its base-install CI job into every xrtoolz and pipekit package, and the test into geotoolz-cloud and geotoolz-products. However, that test only catches undeclared module-level imports, so two more checks cover the faults which F2 fixes:

  1. A minimum-versions job installs each extra at its floors (uv pip install --resolution lowest-direct) and runs a test which calls the code path the extra exists for. For pipekit-train’s cache extra, that is CachedDataset with an fsspec URL as its cache directory (memory://…, so no cloud is needed), which opens the store through zarr.storage.FsspecStore (dataset.py:478) and fails on today’s zarr>=2.18 floor. A local path never reaches that line.

  2. An install-hint test collects every pip install <dist>[<extra>] string in a package’s source and checks that <extra> is declared in its [project.optional-dependencies], which fails on today’s xrtoolz[oceanography] hint.

F4 · Release in tier order

Table 8 orders the releases by base dependency.

Table 8:Release order, by base dependencies. An extra may name a later tier; it resolves once that tier is published, and gate F4 checks the whole set.

ReleaseDistributionsBase dependencies in the stack
1pipekit-core, geotoolz-cloud, geotoolz-catalog, geotoolz-patcher, geotoolz-products, xrtoolz-gradnone
2pipekit-array, -cycle, -evaluate, -experiment, -jax, -train, xrtoolz-core, geotoolzpipekit-core (pipekit-jax after B)
3xrtoolz-einx, xrtoolz-sklearn, xrtoolz-readerxrtoolz-core (xrtoolz-reader after C)
4xrtoolzthe xrtoolz family

F5 · One stack check

A scheduled job installs all 18 distributions from PyPI into one fresh environment, with geotoolz-patcher[train] (which brings in pipekit-train’s Grain and zarr extras), and it runs each package’s fast tests and this page’s notebook with REPORT16_STATE=target. In that mode, the notebook checks the seams of stage 6 against the realigned stack: one Signature, the window dataset importing from geopatcher and building 18 windows of the right shapes from stage 4’s cube, a NumPy-only stencil import, no reader in xrtoolz’s base install, and none of the 18 distributions still requiring pipekit by name (after F1 that is another project, which pip would install beside pipekit-core without a conflict). By default, it checks them as they stand today, so that the page’s stage 6 stays reproducible at the pinned revisions.

Testing gates

Each workstream only merges when its gates pass (Table 9). The K gates are checks which the code passes today and must keep passing.

Table 9:Gates for the proposals and for what ships.

GateWorkstreamPasses when
A1 One SignatureAxrcore.Signature is pipekit_ops.Signature, and stage 6a’s summary with a signature built by today’s call, xrcore.Signature({"time": 24, "latitude": 5, "longitude": 5}), ends in (latitude=5, longitude=5) (today ValueError); and for every spelling in the dtype table ("f8", "<f4", float, "i4", "?", …), Signature.from_sizes(sizes, dtype=x).dtype == np.dtype(x).name, with NumPy used only by the test
A2 Signature round-tripAevery existing pipekit, pipekit-array and xrtoolz signature test passes against the merged class, with 0 failures
A3 No private importsAgrep -r "pipekit\._" over geotoolz returns 0 lines (today 3)
B1 One Equinox wrapperBpipekit-train’s tests pass with the adapter returning JaxModelOp, and pip install pipekit-jax without extras installs no pipekit-cycle
B2 Alias parityBeach deprecated alias returns outputs equal to its replacement to 0 on xrtoolz’s inference tests and emits exactly one DeprecationWarning; JaxModelOp(net, feature_dim="feature", jit=True) and jit=False both construct and agree to 0, in an environment with JAX and without Equinox
C1 Reader-free xrtoolzCimportlib.metadata.requires("xrtoolz") lists no xrtoolz-reader outside [reader] (today it does), and import xrtoolz succeeds without it
C2 Vocabulary shimsCfor every name the move table sends to xrcore.types (BBox, TimeRange, DepthRange, PressureLevels, Variable, REGISTRY, resolve, register, apply_cf_attrs, validate_dataset, validate_variable, ValidationReport, subset_bbox, subset_time, subset_where), getattr(xrreader.types, n) is getattr(xrcore.types, n), so old imports and isinstance checks keep working
D1 Window importDXarrayWindowDataset imports with the pinned geopatcher and no shim (today ImportError); Report 11’s stage 8 pairs satisfy y=Fx−1\vect{y} = \mat{F} \vect{x}_{-1} to 1e-5
D2 No upward edgesDgrepping every pipekit package for geopatcher, geocatalog, geotoolz or xrcore returns 0 lines (today 2 files); the dataset’s content_hash on stage 4’s cube is identical before and after the move
D3 Cheap stencil importDin a fresh interpreter, import geopatcher.temporal.stencils puts none of georeader, rasterio, shapely, pyproj, geopandas, xarray, pandas, scipy, dask or zarr in sys.modules (today the first three)
E1 Carrier ruleEthe capability index lists no mission name under both geoproducts and xrreader except EMIT L2B, whose GeoTIFF and NetCDF containers are different carriers by design (stage table above); and those two EMIT readers agree on a fixture’s enhancement to 1e-6
F1 NamesFpipekit-core, geotoolz-cloud, geotoolz-products, geotoolz-catalog and geotoolz-patcher resolve to this stack on PyPI
F2 One environmentFgeotoolz and xrtoolz install editable into one environment without --no-sources (today a URL conflict)
F3 Packaging testsFxrtoolz and pipekit run test_packaging.py and a base-install job; the minimum-versions job fails on today’s zarr>=2.18 floor at FsspecStore and the install-hint test fails on today’s [oceanography] hint, and both pass after F2
F4 Clean installFall 18 distributions install from PyPI into one fresh environment with no git URLs and no resolver conflict
F5 Stack checkFthe scheduled job passes on the latest release of every distribution, with the notebook’s REPORT16_STATE=target seam checks
K1 Noise floor (existing)—stage 2: the background standard deviation of α^\hat{\alpha} is within 5 % of σα=(Jh⊤Σy−1Jh)−1/2\sigma_\alpha = (\vect{J}_h^\top \gmat{\Sigma}_y^{-1} \vect{J}_h)^{-1/2}. Passes today: 107.2 against 103.9, +3.2 %
K2 Split and merge (existing)—stage 2: the identity operator through GridSampler, ApplyToChips and MergePatches returns the scene to 1e-5. Passes today: 0.0
K3 Column mass (existing)—stage 3: ColumnToMass equals (7) to 1e-12. Passes today
K4 Emission rate (existing)—stage 5: IMEEstimate’s rate equals UeffIME/ℓU_\text{eff} \mathrm{IME} / \ell to 1e-12. Passes today

Sequencing and risks

The name and the contract go first, because every later change either ships under the new name or touches operators which carry signatures. B, C, D and E are independent of each other and run side by side, and publishing waits for all of them (Figure 3).

Phase 1: F1 publish as pipekit-core and A one Signature with public helpers, then gates F1, A1, A2, A3. Phase 2: B model operators, C vocabulary into xrtoolz-core, D datasets into the patcher, E the carrier rule for mission readers, side by side, then gates B1, B2, C1, C2, D1, D2, D3, E1. Phase 3: F2 to F5, pins and floors, packaging tests, publishing in tier order and the stack check, then their gates.

Figure 3:The name and the contract first, then four independent moves, then the release.

Open questions

  1. Does geoproducts get an xarray output? A to_xarray on its readers would let the xarray lane use gridded missions without a second reader, at the cost of xarray in the base of geotoolz-products.

  2. Should geotoolz-cloud’s base drop georeader? It only needs georeader for the GeoTensor that cog and hdf return, so an index-only or bytes-only install would move those modules behind an extra, as Report 7 asks for the patcher.

  3. Does xrtoolz-reader use geotoolz-cloud for its downloads? Its cache and the cloud package’s cache keep local copies by different keys (a request against a URI). Sharing one would cross the lane rule at tier 2, which the tiers allow.

Upcoming work

Table 10 lists the follow-ups outside the workstreams.

Table 10:Follow-ups outside the workstreams.

Follow-upWhereWhy
Point the 7 October draft at this pagethe “Geo data stack” Claude docthis page is the source of truth
Rewrite boundaries.md from this page’s ownership rulesxrtoolz docs/design/it predates pipekit and geopatcher (D4)
Fix the misleading XarrayWindowDataset error message in the same PR as D1pipekit-train xarray_window.py:40geopatcher is installed; the module was renamed
Index geoproducts, geocloud and the xrtoolz subpackagesgeostack scripts/capabilities.pyE1 and the JaxModelOp clash need them (Report 3)

References

Citations

The matched filter follows Thompson et al. Thompson et al., 2015, its covariance is the shrinkage estimator of Ledoit and Wolf Ledoit & Wolf, 2004, and the effective wind is the calibration of Varon et al. Varon et al., 2018.

Sources

References
  1. Ledoit, O., & Wolf, M. (2004). A well-conditioned estimator for large-dimensional covariance matrices. Journal of Multivariate Analysis, 88(2), 365–411. 10.1016/S0047-259X(03)00096-4
  2. Thompson, D. R., Leifer, I., Bovensmann, H., Eastwood, M., Fladeland, M., Frankenberg, C., Gerilowski, K., Green, R. O., Kratwurst, S., Krings, T., Luna, B., & Thorpe, A. K. (2015). Real-time remote detection and measurement for airborne imaging spectroscopy: a case study with methane. Atmospheric Measurement Techniques, 8(10), 4383–4397. 10.5194/amt-8-4383-2015
  3. Varon, D. J., Jacob, D. J., McKeever, J., Jervis, D., Durak, B. O. A., Xia, Y., & Huang, Y. (2018). Quantifying methane point sources from fine-scale satellite observations of atmospheric methane plumes. Atmospheric Measurement Techniques, 11(10), 5673–5686. 10.5194/amt-11-5673-2018