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.

01 — Composition core walkthrough

Composition core — walkthrough

This notebook exercises every primitive in geotoolz end-to-end against plain Python integers. No GeoTensor setup required — the composition algebra is carrier-agnostic, so the same code that runs here works against ndarrays, scalars, or GeoTensors once domain operators land.

Skim it top-to-bottom for a tour, or jump to a section using the table of contents in your viewer. Every cell is executable.

For the model behind the primitives, see the Concepts page.

1. Defining an Operator

An Operator is a class with two methods: _apply (the work) and get_config (a JSON-serialisable dict of the constructor args, used for __repr__ and round-trip).

Add(n=5)
eager call: 15

2. Sequential — linear composition

A Sequential threads the output of each operator into the next. The | operator (inherited from Operator) builds one too, and flattens nested Sequentials automatically.

Sequential: 111
piped:     111
flattened len: 3

get_config() recurses through a Sequential

Every operator’s config is serialised. Lists of ops become lists of {"class": ..., "config": ...} dicts — friendly to hydra-zen, picklable, human-readable in tracebacks.

{
  "operators": [
    {
      "class": "Add",
      "config": {
        "n": 1
      }
    },
    {
      "class": "Add",
      "config": {
        "n": 2
      }
    },
    {
      "class": "Add",
      "config": {
        "n": 3
      }
    }
  ]
}

3. Dual-mode __call__ — eager vs graph

The same operator works two ways:

  • op(value) → runs _apply (eager)
  • op(input_node) → returns a Node (graph construction)

The dispatch is automatic — __call__ checks the argument type.

eager: 15
graph node: Node
node operator: Add(n=5)
node parents: (Input(name='x', parents=(), operator=None),)

4. Graph — symbolic multi-input / multi-output composition

When you need branching outputs or multi-input fusion, Graph is the shape. Build it by calling operators on Input placeholders, then wrap the result with Graph(inputs=..., outputs=...).

Graph(inputs=[x], outputs=[a, b, c])
{'a': 11, 'b': 12, 'c': 23}

Graph topologically sorts the nodes, evaluates each exactly once, and returns a dict keyed by output name. Cycles and unreachable inputs are caught at construction time.

5. Fanout — one input → many outputs (sugar over Graph)

For the common single-input / multi-output case, Fanout is more concise than a hand-built Graph.

{'doubled': 14, 'squared': 49, 'negated': -7}

6. Observers — Tap, Snapshot, ShapeTrace

Identity operators with side effects. The value flows through unchanged while something useful happens on the side.

Tap — fire a callback, pass through

The seed pattern. The callback’s return value is ignored — Tap is for side effects, not transforms.

result: 11
  after Add(1): 1
  after Add(10): 11

Snapshot — capture intermediates by name

A controller (not an Operator itself) that produces snapshot-taking operators via snap.at(key). After the pipeline runs, intermediates are keyed by name in snap[key].

captured keys: ['after_first', 'after_second', 'final']
after_first : 1
after_second: 11
final       : 111

ShapeTrace — log carrier metadata at each step

Useful for debugging “what happened to my GeoTensor between steps?”. Falls back to getattr(..., None) for objects that don’t have shape / dtype / crs (like our integers below), so the same op works on any carrier.

shape=None dtype=None crs=None
shape=(3,) dtype=int16 crs=None
array([ 7, 14, 21], dtype=int16)

7. Control flow — Branch, Switch

The Operator interface is general enough to express conditionals. Branch is the binary case; Switch is multi-way.

guarded(5) : 105
guarded(-5): -5
dispatcher(4): 5
dispatcher(3): 203

8. Small but load-bearing building blocks

Identity, Const, Lambda, Sink are tiny on their own. In combination they replace most one-off Operator subclasses.

Identity: 42
Const   : HELLO
Lambda  : HELLO
Sink    : 11
written : [1]

9. ModelOp — framework-agnostic inference

ModelOp wraps any callable. Use it for sklearn (method="predict"), torch (plain call), JAX (plain call), or any user-supplied function. With batch_size=N, it chunks the input along axis 0 and concatenates the results — handy when the whole input doesn’t fit in GPU memory.

input:
[0 1 2 3 4 5]
output:
[ 0 10 20 30 40 50]

10. Pickling — operator graphs as artifacts

The “operator graph as audit artifact” pattern depends on pickling working. Every YAML-safe operator in geotoolz round-trips through pickle cleanly. Operators flagged forbid_in_yaml = True (Tap, Lambda, Branch, Switch, Sink, ModelOp) hold closures and cannot reproducibly serialise — use Operator subclasses with named get_config() for those paths instead.

pickled config matches: True
restored call: 6

11. Putting it together

A non-trivial pipeline using several primitives at once:

pipeline(1) = 102
pipeline(0) = 1001

snap keys:     ['even_path', 'after_branch', 'odd_path']
snap[final]:   1001
log:           [('entered branch', 2), ('entered branch', 1)]

Where next

  • The Core API reference documents each operator with its constructor signature and config keys.
  • The Concepts page explains the model behind these primitives.
  • Domain operators (radiometry, indices, cloud masking, sampling, inference) land in v0.2+.