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.

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.

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.

Jobgeotoolzxrtoolz
metageotoolzxrtoolz (no code)
transformgeoops (tier 1)xrops (tier 2, new)
productsgeoproductsxrproducts (was xrreader)
carrygeoarrayxarray
sharedgeocatalog · geocloud · geopatchergeocatalog · 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.

  1. geotoolz and xrtoolz hold no code. Each depends on its lane’s distributions and documents the import table.

  2. 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.

  3. xrops holds xarray-native work (selection by value, many files, Dataset and DataTree), xrops.lift for 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).

  4. Domain physics on model states lives in xrops; on observations it belongs to GeoModels (plumax, radtrax), never to geoops (stack levels, SL-O1).

  5. geoproducts and xrproducts read provider data into their lane’s carrier and never transform it.

  6. Heavy backends (JAX, einx, scikit-learn) are extras, never base dependencies.

  7. 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.

SymbolsDefined inUsed byVerdict
Umbrellageotoolz/__init__.py (no code); xrtoolz (code + hard deps on all five siblings)every installSplit ownership. xrtoolz is both the ops package and the meta package
Operator basepipekit.Operator + geoops._src.contract; xrcore.operator.Operator (DataTree dispatch)all operatorsSplit ownership. Base lives in the ops package on one side, its own distribution on the other
einx bridgegeoops.einx; xreinx (dist), with a xrtoolz.einx shimpipelinesTrue duplicate. Same job, module vs distribution
sklearn bridgegeoops.learn; xrsklearn (dist)pipelinesTrue duplicate. Module vs distribution, and learn vs sklearn
Geometrygeoops.geom + geoops._src.engine; xrtoolz.geo (crs, mosaic, rasterize, vectorize, footprint, masks, wavelets, extremes, detrend, …)both lanesSplit ownership. xrtoolz.geo mixes geometry with analysis
RS physicsgeoops.indices, geoops.radiometry; xrtoolz.rs (empty, plans NDVI and radiance-to-reflectance)none yetTrue duplicate in plan. Observation kernels belong to geoops and already run on cubes

Smaller findings:

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.

Jobgeotoolzxrtoolz todayxrtoolz after
metageotoolzxrtoolz (code + meta)xrtoolz (no code)
transformgeoops, slim base plus [raster], [vector], [morph], [sensors]xrtoolzxrops
Operator basepipekit.Operator in geoopsxrcorexrops._src; pipekit.Signature (PL-Q2)
tensor algebrageoops.einxxreinxpipekit-array [einx], shared (OP-O2)
ML bridgegeoops.learnxrsklearnpipekit-array to_rows / from_rows and one estimator wrapper (OP-O3)
calculusgeoops.geom.Gradientxrgradxrops.Gradient over the geoops kernel (OP-O7)
lifting over cubes——xrops.lift (OP-O4)
geometrygeoops.geomxrtoolz.geoxrops.geom + analysis families (PL-Q4)
RS physicsgeoops.indices, radiometryxrtoolz.rs (empty)removed
state physics—xrtoolz.atm, ocn, budgetsxrops.atm, ocn, budgets (PL-Q6)
productsgeoproductsxrreaderxrproducts (PL-Q3)
carrygeoarrayxarrayunchanged
read + index, store, patchgeocatalog, geocloud, geopatchersharedunchanged
  1. Create xrtoolz-ops. Move every module of the xrtoolz distribution into xrops with history, tests unchanged apart from imports.

  2. Empty the umbrella. xrtoolz depends on xrtoolz-ops and xrtoolz-products, re-exports nothing but __version__, and documents the import table, as geotoolz/__init__.py does.

  3. Retire the bridges. Per PL-Q1, xreinx and xrsklearn become shims over pipekit-array’s bridges, and xrgrad becomes xrops.Gradient.

  4. Rename the families. Per PL-Q4, split xrtoolz.geo; delete rs and ice.

  5. Rename the reader. Per PL-Q3, xrtoolz-reader becomes xrtoolz-products.

  6. Shims. Each old import path re-exports with a DeprecationWarning naming 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.

ProductLevelOne read returnsPackage
Sentinel-2 L1C, Landsat 8/9 L1L1one scene on its gridgeoproducts
EMIT L1B, L2BL1, L2one scene in sensor geometry (GeoSwath)geoproducts
GOES ABI, Himawari AHIL1one scangeoproducts
Gridded SLA maps (DUACS)L4a cube over timexrproducts
GLORYS12 reanalysisL4a state cubexrproducts
ERA5L4a state cubexrproducts
AEMET stationsin situa station seriesxrproducts
  1. Both read, neither transforms. A reader returns its lane’s carrier on the provider’s native grid; regridding and derived fields are operators.

  2. Neither imports the other. A product that needs both (EMIT scenes matched to ERA5 winds) is joined in geocatalog or a pipekit graph.

  3. Rename xrreader’s catalog module. xrreader._src.catalog is a short-name registry (glorys12.daily → source and dataset ID), not a catalog. It becomes xrproducts.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.

The split of work:

  1. xrproducts fetches. Authentication, the provider request, download, cache and opening one dataset.

  2. geocatalog indexes and loads. Many files become rows, rows are queried, and load_states builds the cube, as it does today.

  3. The hand-off is rows, not imports. xrproducts writes state-catalog rows (or calls build_state_catalog on 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).

KindExamplesArray lane (NumPy / JAX)Labeled lane (xarray)At rest
RasterSentinel-2 scene, EMIT L1BGeoArray, GeoSwathDataArray on (y, x) or (row, col)COG
StateGLORYS12, ERA5, a somax runshaped arrays in the model; a carrier slice at its boundary, e.g. for H(x)Dataset on (time, member, level, y, x)Zarr, NetCDF
Polygonsplume outlines, AOIs, field boundariesrasterized to GeoArray by an operatorxvec DataArray with a geometry dimGeoParquet
PointsArgo profiles, along-track SSH, AEMET stations, plume source pointsGeoPointsDataset on one obs dim (CF DSG)GeoParquet, NetCDF DSG

Point clouds: the gap

Points have three shapes today and no carrier:

The fix, one carrier per lane (PL-Q9):

  1. Labeled lane: a CF Discrete Sampling Geometry dataset. One obs dim, with lon, lat, time and depth as coordinates on it, and featureType naming the layout (point, timeSeries, trajectory, profile). Ragged profiles use DSG’s contiguous ragged layout, one row_size per feature. Argo and CMEMS along-track files already ship this way, so xrproducts opens them with no conversion.

  2. xvec only on demand. Build an xvec geometry index on obs only when a spatial query needs it; one shapely object per sample is the cost to avoid at 10⁷ along-track points.

  3. Array lane: GeoPoints in geoarray. Horizontal xy (obs, 2), an optional z (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 why z is never a third column. vardax and filterax need it: an observation operator maps a gridded state to values at points inside jit and grad, and a GeoDataFrame cannot cross jit.

  4. 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 (GeoPoints to and from a DSG dataset), as for grids (labeled lane).

  5. 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:

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.

IDQuestionRecommendedAlternativeStatus
PL-Q1Backend 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 distributionssuperseded by OP-O2, OP-O3, OP-O7
PL-Q2Where the xarray Operator base livesxrops._src, as geoops keeps its base; Signature is pipekit.SignatureKeep xrcore if something outside xrtoolz needs the base without the opsopen; Signature decided 21 Oct (GA-Q9)
PL-Q3Reader namexrtoolz-products (import xrproducts), matching geoproductsKeep xrreaderopen
PL-Q4How to split xrtoolz.geoGeometry (crs, mosaic, rasterize, vectorize, footprint, subset) → xrops.geom; analysis (detrend, wavelets, extremes, regimes) into named familiesKeep geo as the generic family and rename geoops.geom to matchopen
PL-Q5Shim lifetimeRemoved in the release after the switch, named in each warningKeep the shims through the whole 0.x seriesopen
PL-Q6Domain familiesxrops.atm, ocn, budgets stay, because they act on model states; geoops stays domain-neutralMove them to GeoModels packages next to their physicsdecided 22 Oct 2026 (OP-O1); the split by discretisation is SL-O1
PL-Q7geoproducts versus xrproductsWhat one read returns: one acquisition → geoproducts; a cube or a station series → xrproductsStrict provenance (observations versus model output), splitting each provider’s reader across bothopen
PL-Q8Who assembles state cubesload_states stays in geocatalog, which already serves both lanes; xrproducts hands off rowsMove cube assembly into xrproducts and keep geocatalog index-onlyopen
PL-Q9Point carriersCF DSG Dataset in the labeled lane, GeoPoints in the array laneOne table carrier for both lanes, or xvec for pointsGeoPoints 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.

GateWorkstreamPasses when
A1 Port parityPL-Athe moved tests pass with only import changes, and the test count equals the old xrtoolz suite’s
A2 Empty umbrellaPL-Axrtoolz/__init__.py exports only __version__, and import xrtoolz imports no ops module
B1 Light basePL-Ba fresh pip install xrtoolz-ops installs none of jax, einx, scikit-learn
C1 One basePL-Cevery xrops operator is an instance of the one base class, and xrcore.Operator is xrops.Operator through the shim
D1 Name parityPL-Da 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 shimsPL-Eimport 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).