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.

Dependency graph

How the stack's repositories depend on one another, and the rules that keep the graph acyclic

Abstract

geostack builds on eighteen repositories in four layers. This page draws how they depend on one another, from what each library declares (base dependencies and extras) and what its code actually imports (including lazy, try-guarded and type-checking-only imports). It then states eight rules that keep the graph free of cycles.

Today it has one cycle between repositories (geotoolz ⇄ pipekit), one cycle between packages, one edge that does not point down a tier and four imports their package does not declare. Table 2 lists each one with its fix.

Keywords:dependency graphcircular dependencieslayerspipekitgeotoolz

1The graph

The diagram folds packages into repositories: pipekit’s seven packages are one box, so are geotoolz’s five, xrtoolz’s six and pyrox’s four. Each arrow reads “imports or depends on”.

The repositories in four layer bands, with edges pointing down from each repository to those it depends on or imports.
The same graph as Mermaid

18 repositories, 37 packages and 29 dependencies between repositories, from each submodule’s pyproject.toml and imports. Solid arrows are base dependencies or imports, dashed ones extras; an arrow a longer path implies is not drawn. Dev, docs and tutorial dependency groups are not library dependencies and are left out. Red arrows form a cycle.

What it shows:

2The rules

Each rule is a check in scripts/dependency_graph.py. The name in brackets is the rule’s name in Table 2.

  1. Layers point down (upward). A repository may depend on its own layer or a lower one: GeoStack and GeoNumerics at the bottom, GeoML above them, GeoModels on top. GeoStack and GeoNumerics never import each other.

  2. Within a layer, tiers point down (tier). Each repository has a tier in its layer (Table 1) and imports only repositories of a lower tier. Two repositories of the same tier never import each other. In GeoModels, radtrax and somax share tier 0 and plumax sits above them in tier 1, because its plume-to-radiance coupling composes a plume model with radtrax’s forward model.

  3. Every library edge counts. Base dependencies, extras, and imports, whether at module level, inside a function, behind a try or under TYPE_CHECKING, all go into the graph. An optional edge is still an edge, and optional edges are where cycles hide: the one cycle today is a lazy, guarded import. Dependency groups (dev, docs, tutorials) are not: they install tools for working on the repository, not the library, so a tutorial may use a library above it.

  4. No cycles (cycle, package-cycle): not between repositories, and not between the packages of one workspace (pipekit’s, geotoolz’s, xrtoolz’s, pyrox’s).

  5. Every import is declared (undeclared), as a base dependency or an extra. An extra suits an optional module (one the package’s __init__ does not import) or a lazy or guarded import; whatever import <package> runs needs a base dependency. A TYPE_CHECKING-only import needs no declaration but still obeys rules 1 and 2. Code the package ships never relies on a dependency group. The one way out is UNDECLARED_OK in scripts/dependency_graph.py: a guarded integration with a library that cannot be installed beside the package, listed with its reason.

  6. A reverse edge is fixed by moving code, never by a shim. When a lower repository needs something from a higher one, either the primitive moves down into the lower one, or the adapter moves up into the higher one (as an integrations/ module behind an extra). A moved name leaves no re-export behind: its old path raises an ImportError naming its new home, because a re-export is a reverse edge (Package boundaries).

  7. A third-party repository is a leaf (third-party). georeader belongs to spaceml-org: we depend on it and never change it, and nothing of ours moves into it.

  8. A new repository arrives with its place (untiered, stale-tier). Adding a submodule means adding it to TIERS in scripts/dependency_graph.py with its layer and tier, in the same pull request, and removing or renaming one means changing its entry there too.

3Layers and tiers

Table 1:Each repository’s tier in its layer. A repository may import lower tiers of its own layer and any tier of a lower layer.

LayerTierRepositoriesHolds
GeoModels1plumaxplume models, which may build on radtrax
GeoModels0radtrax, somaxradiative transfer and dynamics
GeoML3vardaxvariational assimilation (may build on filterax)
GeoML2filterax, pyroxensemble filters and probabilistic models
GeoML1kernellib, optax_bayes, xtremaxkernels, optimisers and extremes
GeoML0gauss_flows, geonnaxlayers and flows
GeoNumerics2finitevolXfinite volume
GeoNumerics1spectraldiffxspectral
GeoNumerics0gaussxlinear algebra and Gaussians
GeoStack1geotoolz, xrtoolzoperators on arrays and GeoTensors
GeoStack0georeader, pipekitpipelines and readers

The tiers follow what each repository is for, not only the edges it has today. filterax could sit lower, since it imports only gaussx; it sits at tier 2 so that vardax, alone at tier 3, may build on it and never the other way round. The two already overlap: each has its own observation operators (filterax’s AbstractObsOperator and LinearizableObsOperator, vardax’s LinearObs, InterpObs and MaskedIdentity), adjoint strategies and dynamics wrappers, and vardax’s ensemble posterior already defers to filterax. A hybrid ensemble–variational scheme builds vardax’s background covariance from a filterax ensemble; nothing in filterax needs a variational solver. So the shared vocabulary belongs in filterax, and vardax reuses it (open question 1).

4Where the graph breaks the rules today

Table 2:Every rule the graph breaks, from the check. The check fails on a row not listed in KNOWN in the script, and on a listed one that is fixed, so this table only shrinks.

RuleFromToWhereWhy, and the fix
cyclegeotoolzpipekitgeotoolz ⇄ pipekitpipekit-train’s _xreader and xarray_window import geopatcher.time (which no longer exists); Report 16 D2 moves the dataset to geopatcher.integrations.train
package-cyclegeotoolz-patcherpipekit-traingeotoolz-patcher ⇄ pipekit-traingeotoolz#486 added geopatcher.integrations.train, which imports pipekit_train, while the pinned pipekit-train still imports geopatcher; clears when the pipekit pin moves past the train dataset’s move (Report 16 D2)
tierpipekit-traingeotoolz-patcherimport, src/pipekit_train/xarray_window.py (guarded)the same imports
undeclaredpipekit-train:src/pipekit_train/_xreader.pygeotoolz-patcherlazythe same imports
undeclaredpipekit-train:src/pipekit_train/xarray_window.pygeotoolz-patcherguardedthe same imports
undeclaredsomax:somax/_src/da/filterax_bridge.pyfilteraxmodulemodule-level imports in somax/_src/da, declared only in the da dependency group; make it an extra
undeclaredsomax:somax/_src/da/obs.pyfilteraxmodulethe same extra

The fixes:

  1. Move pipekit-train’s window dataset into geotoolz-patcher (geopatcher.integrations.train, behind geotoolz-patcher[train]). This is Report 16’s step D2, and rule 6’s “move the adapter up”. It removes the cycle, the tier break and the two undeclared imports, which are four rows for one dataset. The imports are broken anyway: geotoolz-patcher has geopatcher.temporal, not geopatcher.time.

  2. Make somax’s filterax import an extra. somax/_src/da imports filterax at module level, but somax declares filterax only in its da dependency group, so pip install somax gives a package whose da module cannot import. A [da] extra with filterax and vardax fixes it.

gauss_flows’s guarded gaussx import stays undeclared, as an exception in UNDECLARED_OK. gaussx is not on PyPI, and gauss_flows’s [interp] extra (interpax) caps lineax below what gaussx needs. Tried as a [gaussx] extra, it forked the lock and moved the plain install to a jax that its numpyro cannot import, so most of its tests could no longer be collected.

5Open questions

  1. Do vardax’s observation operators and dynamics move to filterax, or to a smaller shared package below both? Tier order decides only which may import which. Moving the shared types into filterax is the smallest step; a separate package is worth it only if a third library (somax’s da module, a future smoother) needs them without either engine.

  2. May pyrox and filterax import each other? They share tier 2. A GP-based observation model in filterax, or an ensemble likelihood in pyrox, would need one of them moved; no code asks for it today.

6Upcoming work

Table 3:Follow-ups.

Follow-upWhereWhy
Move XarrayWindowDataset and _xreader to geopatcher.integrations.trainpipekit, geotoolzfix 1; Report 16 D2
A [da] extra with filterax and vardaxsomaxfix 2
The same check inside each repository: python scripts/dependency_graph.py --guard <repo> writes its test_dependency_rules.pyeach upstream repository but georeadercatches a reverse edge before it reaches geostack’s pin
Move vardax’s observation operators and dynamics onto filterax’svardax, filteraxopen question 1

7Every edge

Table 4:Every dependency between two repositories, with its strongest evidence (base dependency, extra, import, in that order) and one package-level example.

FromToStrongest evidenceExample
plumaxfinitevolXbaseplumax → finitevolx
plumaxgaussxbaseplumax → gaussx
plumaxpipekitbaseplumax → pipekit
radtraxgaussxbaseradtrax → gaussx
radtraxgeoreaderextraradtrax → georeader-spaceml
somaxfilteraximportsomax → filterax
somaxfinitevolXbasesomax → finitevolx
somaxgeonnaxbasesomax → geonnax
somaxpipekitbasesomax → pipekit
somaxspectraldiffxbasesomax → spectraldiffx
filteraxgaussxbasefilterax → gaussx
gauss_flowsgaussximportgauss-flows → gaussx
kernellibgaussxbasekernellib → gaussx
kernellibgeonnaxbasekernellib → geonnax
optax_bayesgaussxextraoptax-bayes → gaussx
pyroxgauss_flowsextrapyrox-gp → gauss-flows
pyroxgaussxbasepyrox-gp → gaussx
pyroxgeonnaxbasepyrox-gp → geonnax
pyroxkernellibbasepyrox-gp → kernellib
vardaxgaussxbasevardax → gaussx
vardaxgeonnaxbasevardax → geonnax
vardaxpipekitbasevardax → pipekit
finitevolXgaussxbasefinitevolx → gaussx
finitevolXspectraldiffxbasefinitevolx → spectraldiffx
spectraldiffxgaussxbasespectraldiffx → gaussx
geotoolzgeoreaderbasegeotoolz-catalog → georeader-spaceml
geotoolzpipekitbasegeotoolz-ops → pipekit
pipekitgeotoolzimportpipekit-train → geotoolz-patcher
xrtoolzpipekitbasexrtoolz → pipekit