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.
About this page
| Status | The graph and the tables are generated; the rules are proposed and checked |
| Audience | anyone adding a dependency, an import or a repository to the stack, and anyone reviewing one |
| Companions | Package boundaries (the plume and radiative-transfer moves, and the counterfactual cycle they avoid); Report 16 (step D2, which removes the cycle); the capability indexes (what each repository exports) |
| Code checked against | the submodule pins in references/<layer>/ on this commit |
| Generated by | scripts/dependency_graph.py, which reads files only (it imports and installs nothing); tests/test_dependency_graph.py fails when this page or the README is stale, when the graph breaks a rule not in Table 2, and when one of those is fixed |
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 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:
Every edge between layers points down. GeoModels uses GeoML, GeoNumerics and GeoStack. GeoML uses GeoNumerics and GeoStack. GeoNumerics uses nothing above it.
gaussx is the foundation. Every repository in GeoNumerics, GeoML and GeoModels except geonnax reaches it. A breaking change in gaussx is a change to almost the whole stack.
plumax is the one GeoModels repository that imports another. It builds on radtrax: plume physics uses radiative transfer, never the reverse (package boundaries, rule 2). somax imports no one in its layer.
The one cycle is inside GeoStack (in red): pipekit-train imports
geopatcher.timefrom geotoolz-patcher, and geotoolz depends on pipekit. The import is lazy and behind atry, so nothing fails at install time, but the two repositories can no longer be released, tested or reasoned about separately.
2The rules¶
Each rule is a check in scripts/dependency_graph.py. The name in brackets
is the rule’s name in Table 2.
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.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.Every library edge counts. Base dependencies, extras, and imports, whether at module level, inside a function, behind a
tryor underTYPE_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.No cycles (
cycle,package-cycle): not between repositories, and not between the packages of one workspace (pipekit’s, geotoolz’s, xrtoolz’s, pyrox’s).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; whateverimport <package>runs needs a base dependency. ATYPE_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 isUNDECLARED_OKinscripts/dependency_graph.py: a guarded integration with a library that cannot be installed beside the package, listed with its reason.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 anImportErrornaming its new home, because a re-export is a reverse edge (Package boundaries).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.A new repository arrives with its place (
untiered,stale-tier). Adding a submodule means adding it toTIERSinscripts/dependency_graph.pywith 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.
| Layer | Tier | Repositories | Holds |
|---|---|---|---|
| GeoModels | 1 | plumax | plume models, which may build on radtrax |
| GeoModels | 0 | radtrax, somax | radiative transfer and dynamics |
| GeoML | 3 | vardax | variational assimilation (may build on filterax) |
| GeoML | 2 | filterax, pyrox | ensemble filters and probabilistic models |
| GeoML | 1 | kernellib, optax_bayes, xtremax | kernels, optimisers and extremes |
| GeoML | 0 | gauss_flows, geonnax | layers and flows |
| GeoNumerics | 2 | finitevolX | finite volume |
| GeoNumerics | 1 | spectraldiffx | spectral |
| GeoNumerics | 0 | gaussx | linear algebra and Gaussians |
| GeoStack | 1 | geotoolz, xrtoolz | operators on arrays and GeoTensors |
| GeoStack | 0 | georeader, pipekit | pipelines 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.
| Rule | From | To | Where | Why, and the fix |
|---|---|---|---|---|
| cycle | geotoolz | pipekit | geotoolz ⇄ pipekit | pipekit-train’s _xreader and xarray_window import geopatcher.time (which no longer exists); Report 16 D2 moves the dataset to geopatcher.integrations.train |
| package-cycle | geotoolz-patcher | pipekit-train | geotoolz-patcher ⇄ pipekit-train | geotoolz#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) |
| tier | pipekit-train | geotoolz-patcher | import, src/pipekit_train/xarray_window.py (guarded) | the same imports |
| undeclared | pipekit-train:src/pipekit_train/_xreader.py | geotoolz-patcher | lazy | the same imports |
| undeclared | pipekit-train:src/pipekit_train/xarray_window.py | geotoolz-patcher | guarded | the same imports |
| undeclared | somax:somax/_src/da/filterax_bridge.py | filterax | module | module-level imports in somax/_src/da, declared only in the da dependency group; make it an extra |
| undeclared | somax:somax/_src/da/obs.py | filterax | module | the same extra |
The fixes:
Move pipekit-train’s window dataset into geotoolz-patcher (
geopatcher.integrations.train, behindgeotoolz-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 hasgeopatcher.temporal, notgeopatcher.time.Make somax’s filterax import an extra.
somax/_src/daimports filterax at module level, but somax declares filterax only in itsdadependency group, sopip install somaxgives a package whosedamodule 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¶
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
damodule, a future smoother) needs them without either engine.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-up | Where | Why |
|---|---|---|
Move XarrayWindowDataset and _xreader to geopatcher.integrations.train | pipekit, geotoolz | fix 1; Report 16 D2 |
A [da] extra with filterax and vardax | somax | fix 2 |
The same check inside each repository: python scripts/dependency_graph.py --guard <repo> writes its test_dependency_rules.py | each upstream repository but georeader | catches a reverse edge before it reaches geostack’s pin |
| Move vardax’s observation operators and dynamics onto filterax’s | vardax, filterax | open 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.
| From | To | Strongest evidence | Example |
|---|---|---|---|
| plumax | finitevolX | base | plumax → finitevolx |
| plumax | gaussx | base | plumax → gaussx |
| plumax | pipekit | base | plumax → pipekit |
| radtrax | gaussx | base | radtrax → gaussx |
| radtrax | georeader | extra | radtrax → georeader-spaceml |
| somax | filterax | import | somax → filterax |
| somax | finitevolX | base | somax → finitevolx |
| somax | geonnax | base | somax → geonnax |
| somax | pipekit | base | somax → pipekit |
| somax | spectraldiffx | base | somax → spectraldiffx |
| filterax | gaussx | base | filterax → gaussx |
| gauss_flows | gaussx | import | gauss-flows → gaussx |
| kernellib | gaussx | base | kernellib → gaussx |
| kernellib | geonnax | base | kernellib → geonnax |
| optax_bayes | gaussx | extra | optax-bayes → gaussx |
| pyrox | gauss_flows | extra | pyrox-gp → gauss-flows |
| pyrox | gaussx | base | pyrox-gp → gaussx |
| pyrox | geonnax | base | pyrox-gp → geonnax |
| pyrox | kernellib | base | pyrox-gp → kernellib |
| vardax | gaussx | base | vardax → gaussx |
| vardax | geonnax | base | vardax → geonnax |
| vardax | pipekit | base | vardax → pipekit |
| finitevolX | gaussx | base | finitevolx → gaussx |
| finitevolX | spectraldiffx | base | finitevolx → spectraldiffx |
| spectraldiffx | gaussx | base | spectraldiffx → gaussx |
| geotoolz | georeader | base | geotoolz-catalog → georeader-spaceml |
| geotoolz | pipekit | base | geotoolz-ops → pipekit |
| pipekit | geotoolz | import | pipekit-train → geotoolz-patcher |
| xrtoolz | pipekit | base | xrtoolz → pipekit |