Package layout across geotoolz and xrtoolz
Design · one job per distribution, matching family names, readers named as products
1Summary¶
xrtoolz adopts geotoolz’s split: distributions by job, modules inside the ops package by operation kind, heavy backends as extras. geotoolz keeps its shape apart from a slimmer geoops base install, and no code moves between the two repos except bridges that become shared machinery. xrops sits one tier above geoops (the tiers), so every kernel lives in geoops or below and xrops adds only what needs xarray.
PL-A. xrtoolz becomes a meta package. Today
xrtoolzis both the ops code and the umbrella that hard-depends on every sibling. Its code moves to a newxrtoolz-ops(importxrops), andxrtoolzholds no code, asgeotoolzdoes.PL-B. Backend bridges stop being distributions.
xreinxandxrsklearngive way to one named-dims bridge each in pipekit-array (OP-O2, OP-O3);xrgradbecomesxrops.Gradientover the geoops kernel (OP-O7).PL-C. One home for the xarray Operator base.
xrcore’sOperator(with itsDataTreedispatch) folds intoxrops._src; itsSignaturebecomes a re-export ofpipekit.Signaturefor one release, then goes (pipekit kernels).PL-D. Matching family names. geoops is organised by operation kind, xrtoolz by domain plus a
geograb-bag.xropstakes geoops’s family names wherever the job is the same, keeps state-physics families, and drops the emptyrsandicestubs.PL-E. Readers named as products.
xrreader(CMEMS, CDS, AEMET) is the xarray twin ofgeoproductsand becomesxrtoolz-products(importxrproducts).
End state: each repo has a code-free umbrella and the same job-shaped distributions, and geoops and xrops share family names where the job is the same.
2Ownership rules¶
One axis per level: distributions split by job, modules inside an ops package split by operation kind, and heavy backends sit behind extras.
| Job | geotoolz | xrtoolz |
|---|---|---|
| meta | geotoolz | xrtoolz (no code) |
| transform | geoops (tier 1) | xrops (tier 2, new) |
| products | geoproducts | xrproducts (was xrreader) |
| carry | geoarray | xarray |
| shared | geocatalog · geocloud · geopatcher | geocatalog · geocloud · geopatcher |
Each row is one job; only the names on the right change. The shared row already serves both lanes, and pipekit-array (tier 0) sits under both ops packages.
geotoolz and xrtoolz hold no code. Each depends on its lane’s distributions and documents the import table.
geoops holds every kernel operator, written once, and every per-item geometry operator. Its base install is slim: rasterio, geopandas and shapely, scikit-image and georeader sit behind
[raster],[vector],[morph]and[sensors]; it has no[xarray]extra and never imports xrops or xarray.xrops holds xarray-native work (selection by value, many files,
DatasetandDataTree),xrops.liftfor geoops operators over cubes, and state physics (atm,ocn,budgets). It may import geoops and geoarray; crossing lanes goes through geoarray’s converters (labeled lane).Domain physics on model states lives in
xrops; on observations it belongs to GeoModels (plumax, radtrax), never to geoops (stack levels, SL-O1).geoproducts and xrproducts read provider data into their lane’s carrier and never transform it.
Heavy backends (JAX, einx, scikit-learn) are extras, never base dependencies.
Every package writes the three levels the same way: primitives in
_src/array.py, operators on carriers above them, carrier code only where the carrier may be imported.
3Current-state audit¶
Six mismatches need action.
The audit read the main branches of geotoolz (umbrella 0.12.0) and xrtoolz (0.0.5) on 21 Oct 2026; call-site counts are still to do.
| Symbols | Defined in | Used by | Verdict |
|---|---|---|---|
| Umbrella | geotoolz/__init__.py (no code); xrtoolz (code + hard deps on all five siblings) | every install | Split ownership. xrtoolz is both the ops package and the meta package |
| Operator base | pipekit.Operator + geoops._src.contract; xrcore.operator.Operator (DataTree dispatch) | all operators | Split ownership. Base lives in the ops package on one side, its own distribution on the other |
| einx bridge | geoops.einx; xreinx (dist), with a xrtoolz.einx shim | pipelines | True duplicate. Same job, module vs distribution |
| sklearn bridge | geoops.learn; xrsklearn (dist) | pipelines | True duplicate. Module vs distribution, and learn vs sklearn |
| Geometry | geoops.geom + geoops._src.engine; xrtoolz.geo (crs, mosaic, rasterize, vectorize, footprint, masks, wavelets, extremes, detrend, …) | both lanes | Split ownership. xrtoolz.geo mixes geometry with analysis |
| RS physics | geoops.indices, geoops.radiometry; xrtoolz.rs (empty, plans NDVI and radiance-to-reflectance) | none yet | True duplicate in plan. Observation kernels belong to geoops and already run on cubes |
Smaller findings:
Domain modules.
xrtoolz.atm,ocnandbudgetsact on gridded states;xrtoolz.iceis an empty stub. geoops’splumeandmatched_filterare already scheduled to leave for plumax and radtrax (radtrax backlog RTX-34, RTX-30).Readers.
xrreaderreads CMEMS, CDS and AEMET archives into xarray. It does the job ofgeoproductsbut is named like a different kind of package.Shim policy.
xrtoolz._operator,xrtoolz.signatureandxrtoolz.einxsay they are removed at 1.0. The stack bumps 0.X.0 for breaking changes, so the removal needs a named 0.X release.Shared packages stay shared.
geocatalog(withgeocatalog.states),geocloudandgeopatcheralready serve both lanes; nothing in xrtoolz duplicates them.
4Target layout and moves¶
Every job has the same distribution shape in both repos, and every ops family has the same name where the job is the same.
| Job | geotoolz | xrtoolz today | xrtoolz after |
|---|---|---|---|
| meta | geotoolz | xrtoolz (code + meta) | xrtoolz (no code) |
| transform | geoops, slim base plus [raster], [vector], [morph], [sensors] | xrtoolz | xrops |
| Operator base | pipekit.Operator in geoops | xrcore | xrops._src; pipekit.Signature (PL-Q2) |
| tensor algebra | geoops.einx | xreinx | pipekit-array [einx], shared (OP-O2) |
| ML bridge | geoops.learn | xrsklearn | pipekit-array to_rows / from_rows and one estimator wrapper (OP-O3) |
| calculus | geoops.geom.Gradient | xrgrad | xrops.Gradient over the geoops kernel (OP-O7) |
| lifting over cubes | — | — | xrops.lift (OP-O4) |
| geometry | geoops.geom | xrtoolz.geo | xrops.geom + analysis families (PL-Q4) |
| RS physics | geoops.indices, radiometry | xrtoolz.rs (empty) | removed |
| state physics | — | xrtoolz.atm, ocn, budgets | xrops.atm, ocn, budgets (PL-Q6) |
| products | geoproducts | xrreader | xrproducts (PL-Q3) |
| carry | geoarray | xarray | unchanged |
| read + index, store, patch | geocatalog, geocloud, geopatcher | shared | unchanged |
Create
xrtoolz-ops. Move every module of thexrtoolzdistribution intoxropswith history, tests unchanged apart from imports.Empty the umbrella.
xrtoolzdepends onxrtoolz-opsandxrtoolz-products, re-exports nothing but__version__, and documents the import table, asgeotoolz/__init__.pydoes.Retire the bridges. Per PL-Q1,
xreinxandxrsklearnbecome shims over pipekit-array’s bridges, andxrgradbecomesxrops.Gradient.Rename the families. Per PL-Q4, split
xrtoolz.geo; deletersandice.Rename the reader. Per PL-Q3,
xrtoolz-readerbecomesxrtoolz-products.Shims. Each old import path re-exports with a
DeprecationWarningnaming its new home, removed in a named 0.X release (PL-Q5).
5Boundary: geoproducts and xrproducts¶
The line is what one read returns: geoproducts reads one acquisition into a geoarray carrier, and xrproducts reads anything stacked over time, members or levels, or a station series, into xarray (PL-Q7). It follows the workload rule of the crossover, and it replaces the 21 Oct wording of observations versus states drawn at an L3 carrier switch.
Provenance alone does not split cleanly. CMEMS serves both gridded satellite maps and the GLORYS reanalysis, and CDS serves ERA5 and satellite products. A provenance rule would split one provider’s reader across two packages.
| Product | Level | One read returns | Package |
|---|---|---|---|
| Sentinel-2 L1C, Landsat 8/9 L1 | L1 | one scene on its grid | geoproducts |
| EMIT L1B, L2B | L1, L2 | one scene in sensor geometry (GeoSwath) | geoproducts |
| GOES ABI, Himawari AHI | L1 | one scan | geoproducts |
| Gridded SLA maps (DUACS) | L4 | a cube over time | xrproducts |
| GLORYS12 reanalysis | L4 | a state cube | xrproducts |
| ERA5 | L4 | a state cube | xrproducts |
| AEMET stations | in situ | a station series | xrproducts |
Both read, neither transforms. A reader returns its lane’s carrier on the provider’s native grid; regridding and derived fields are operators.
Neither imports the other. A product that needs both (EMIT scenes matched to ERA5 winds) is joined in geocatalog or a pipekit graph.
Rename
xrreader’scatalogmodule.xrreader._src.catalogis a short-name registry (glorys12.daily→ source and dataset ID), not a catalog. It becomesxrproducts.registry, so “catalog” means geocatalog everywhere.
Note: how geocatalog handles states¶
geocatalog already indexes states on main through geocatalog.states; this plan only adds the hand-off from xrproducts.
Rows. A state catalog is an xarray-kind catalog with five reserved columns (schema v1):
kind,run_time,member,producer,parents.build_state_catalogfills them from each file’s metadata, withCF_NAMESandCFGRIB_NAMESas name maps.Queries.
forecasts,ensemble_at,forecast_chain,lead_window,at_timeandlatest_analysiswork on either backend.Loading.
load_statesstacks one file per (run, member) into an(init_time, member, lead_time, y, x)cube, lazily, behind the[xarray-raster]extra. Overlapping runs are never dropped.
The split of work:
xrproducts fetches. Authentication, the provider request, download, cache and opening one dataset.
geocatalog indexes and loads. Many files become rows, rows are queried, and
load_statesbuilds the cube, as it does today.The hand-off is rows, not imports. xrproducts writes state-catalog rows (or calls
build_state_catalogon what it downloaded) so a fetch lands indexed. geocatalog never imports xrproducts, and xrproducts depends on geocatalog only through an extra (PL-Q8).
6The elephants: points, polygons, rasters, states¶
Each geometry kind gets one carrier per lane.
Changing kind (rasterize, sample, bin) is an operator, never the carrier’s job.
On the array side the carriers are GeoArray (grids), GeoSwath (sensor geometry) and GeoPoints (point sets), with stored dims and time stamps on a TimeAxis (geoarray); xarray formalizes neither geometry nor time meaning, conventions do (CF, xvec).
| Kind | Examples | Array lane (NumPy / JAX) | Labeled lane (xarray) | At rest |
|---|---|---|---|---|
| Raster | Sentinel-2 scene, EMIT L1B | GeoArray, GeoSwath | DataArray on (y, x) or (row, col) | COG |
| State | GLORYS12, ERA5, a somax run | shaped arrays in the model; a carrier slice at its boundary, e.g. for H(x) | Dataset on (time, member, level, y, x) | Zarr, NetCDF |
| Polygons | plume outlines, AOIs, field boundaries | rasterized to GeoArray by an operator | xvec DataArray with a geometry dim | GeoParquet |
| Points | Argo profiles, along-track SSH, AEMET stations, plume source points | GeoPoints | Dataset on one obs dim (CF DSG) | GeoParquet, NetCDF DSG |
Point clouds: the gap¶
Points have three shapes today and no carrier:
geoops’s
RasterToPoints,PointsToRaster,RasterToPointCloudandPointCloudToRastertake a GeoDataFrame or an xvec array.xrtoolz.geoalong-track helpers take bare 1-D lon and lat arrays.geocatalog.matchuppairs catalog rows, not points.
The fix, one carrier per lane (PL-Q9):
Labeled lane: a CF Discrete Sampling Geometry dataset. One
obsdim, with lon, lat, time and depth as coordinates on it, andfeatureTypenaming the layout (point, timeSeries, trajectory, profile). Ragged profiles use DSG’s contiguous ragged layout, onerow_sizeper feature. Argo and CMEMS along-track files already ship this way, so xrproducts opens them with no conversion.xvec only on demand. Build an xvec geometry index on
obsonly when a spatial query needs it; one shapely object per sample is the cost to avoid at 10⁷ along-track points.Array lane:
GeoPointsin geoarray. Horizontalxy(obs, 2), an optionalz(obs,)with its own vertical descriptor, values, per-sample time, a CRS and a fill, as a pytree; the geoarray page defines it and says whyzis never a third column. vardax and filterax need it: an observation operator maps a gridded state to values at points insidejitandgrad, and a GeoDataFrame cannot crossjit.Kind changes are operators with matching names in both lanes. Sample (raster to points), bin or grid (points to raster), rasterize and vectorize (polygons to and from raster). geoarray’s converters cross lanes (
GeoPointsto and from a DSG dataset), as for grids (labeled lane).At rest. GeoParquet for point tables geocatalog queries with DuckDB; NetCDF DSG or Zarr when a profile or a trajectory is the unit.
Alternatives considered:
One table carrier (GeoDataFrame or GeoArrow) for points in both lanes. It is best for huge sets and DuckDB, but not labeled N-D and not
jit-able.xvec for points too. One vector story, but one shapely object per point at scale.
When to leave a carrier for xarray is the subject of the crossover.
7Open questions¶
Each has a recommendation and its alternative; flip any before the issues open.
| ID | Question | Recommended | Alternative | Status |
|---|---|---|---|---|
| PL-Q1 | Backend bridges: extras or distributions? | Retire xreinx and xrsklearn in favour of pipekit-array’s shared bridges (OP-O2, OP-O3); xrgrad becomes xrops.Gradient (OP-O7) | Fold them into xrops behind extras (the 21 Oct recommendation), or split geoops.einx and geoops.learn out into distributions | superseded by OP-O2, OP-O3, OP-O7 |
| PL-Q2 | Where the xarray Operator base lives | xrops._src, as geoops keeps its base; Signature is pipekit.Signature | Keep xrcore if something outside xrtoolz needs the base without the ops | open; Signature decided 21 Oct (GA-Q9) |
| PL-Q3 | Reader name | xrtoolz-products (import xrproducts), matching geoproducts | Keep xrreader | open |
| PL-Q4 | How to split xrtoolz.geo | Geometry (crs, mosaic, rasterize, vectorize, footprint, subset) → xrops.geom; analysis (detrend, wavelets, extremes, regimes) into named families | Keep geo as the generic family and rename geoops.geom to match | open |
| PL-Q5 | Shim lifetime | Removed in the release after the switch, named in each warning | Keep the shims through the whole 0.x series | open |
| PL-Q6 | Domain families | xrops.atm, ocn, budgets stay, because they act on model states; geoops stays domain-neutral | Move them to GeoModels packages next to their physics | decided 22 Oct 2026 (OP-O1); the split by discretisation is SL-O1 |
| PL-Q7 | geoproducts versus xrproducts | What one read returns: one acquisition → geoproducts; a cube or a station series → xrproducts | Strict provenance (observations versus model output), splitting each provider’s reader across both | open |
| PL-Q8 | Who assembles state cubes | load_states stays in geocatalog, which already serves both lanes; xrproducts hands off rows | Move cube assembly into xrproducts and keep geocatalog index-only | open |
| PL-Q9 | Point carriers | CF DSG Dataset in the labeled lane, GeoPoints in the array lane | One table carrier for both lanes, or xvec for points | GeoPoints decided (geoarray page); DSG open |
8Gates, sequencing and risks¶
Each workstream merges only when its gate passes; PL-A ships first because every other move lands inside xrops.
| Gate | Workstream | Passes when |
|---|---|---|
| A1 Port parity | PL-A | the moved tests pass with only import changes, and the test count equals the old xrtoolz suite’s |
| A2 Empty umbrella | PL-A | xrtoolz/__init__.py exports only __version__, and import xrtoolz imports no ops module |
| B1 Light base | PL-B | a fresh pip install xrtoolz-ops installs none of jax, einx, scikit-learn |
| C1 One base | PL-C | every xrops operator is an instance of the one base class, and xrcore.Operator is xrops.Operator through the shim |
| D1 Name parity | PL-D | a script lists the families of geoops and xrops; every name in both means the same job, and the list is checked into the docs |
| E1 Reader shims | PL-E | import xrreader warns and resolves every public name to xrproducts |
Order: PL-A, then PL-B, PL-C and PL-E in parallel, then PL-D.
PL-B waits for the pipekit-array release that ships the shared bridges, and xrops.lift waits for geoops’s slim base (operators).
Each is one minor bump of xrtoolz (0.1.0, then 0.2.0).
Downstream imports break. pipekit, geostack notebooks and GeoBench import
xrtoolz.*today. Fallback: shims for one release, and a grep of the stack for old paths before the release.The shared bridges slip. If pipekit-array’s einx and scikit-learn bridges are late, PL-B blocks. Fallback: keep
xreinxandxrsklearnas they are for one more release; PL-D still applies.xrtoolz.geohas no clean split. Some functions are both geometry and analysis. Fallback: keep a thinxrops.geore-export until D1 lists every name.