procedural

graph_generators.procedural

Resolve a Procedural GraphGenerator’s typed DAG into backend-independent expressions.

A Procedural generator describes a network construction as an ordered DAG of typed steps — a distance kernel, a stochastic connection mask, a Gaussian field, an axis normalisation — instead of per-generator Python. This module turns that metadata into SymPy expressions, which the printers in tvbo/codegen/code.py render natively for every backend.

Two properties make it declarative rather than a lowering dialect:

Nothing is built from expression strings. Each step’s options are schema fields, and this module constructs the SymPy tree directly (Function("pairwise_distance")(pos)). That is not a stylistic choice: SymPy’s parser cannot represent keyword arguments at all, silently collapses M[M != 0] to True, and turns an unregistered head into implicit multiplication (eigvals(M) -> M*eigvals) with no error. Building nodes directly makes all three unreachable. The single exception is the equation step, whose rhs is author-written algebra — which is what the parser is actually for.

The deterministic/stochastic split is inferred, not declared. partition computes which steps transitively depend on the generator’s seed. A backend evaluates the deterministic prefix once and traces only the stochastic suffix per realisation, so sweeping N network realisations costs N x (the seeded tail), not N x (the whole construction). The boundary is a property of the DAG, so every backend gets the same answer.

Attributes

Name Description
KEY The reserved symbol a step’s seeded draw is keyed by.
N_NODES

Classes

Name Description
ProceduralError A Procedural generator’s DAG is malformed (unknown step type, bad reference, …).

ProceduralError

graph_generators.procedural.ProceduralError()

A Procedural generator’s DAG is malformed (unknown step type, bad reference, …).

Functions

Name Description
build Resolve a Procedure’s steps DAG to ordered (name, expression) pairs.
draw Sample shape values from distribution, through the same printer sampler.
materialize Evaluate a Procedure’s DAG eagerly, in numpy, and return its output values.
partition Split the DAG into (deterministic prefix, stochastic suffix), in DAG order.
seeded_steps Names of steps that depend on the generator’s PRNG state, transitively.

build

graph_generators.procedural.build(spec)

Resolve a Procedure’s steps DAG to ordered (name, expression) pairs.

spec is a Procedure block — steps (an ordered mapping of typed ProcedureSteps, keyed by name) and output — plus the generator’s parameters. Node positions are not a special slot: a layout is an ordinary step.

draw

graph_generators.procedural.draw(distribution, shape, seed=None, substream=0)

Sample shape values from distribution, through the same printer sampler.

A construction that needs one array of draws rather than a whole DAG — a per-unit downward projection, say — still goes through the printer table, so it cannot drift from what a sample step produces for the same distribution and seed.

materialize

graph_generators.procedural.materialize(spec, params=None, seed=None)

Evaluate a Procedure’s DAG eagerly, in numpy, and return its output values.

The evaluation goes through the SAME primitive tables the other backends emit from: each step is rendered by the numpy printer and evaluated. There is deliberately no second numpy implementation of sample, eigvals, normalize and friends — one definition per backend is what keeps eager materialisation and emitted JAX from drifting apart, which a parallel evaluator cannot guarantee.

Only genuinely non-expression operations stay host-side (see _host_env): resolving a Network IRI is I/O, not array algebra, and no printer should ever emit it.

partition

graph_generators.procedural.partition(spec)

Split the DAG into (deterministic prefix, stochastic suffix), in DAG order.

The prefix is evaluated once; the suffix is what a backend re-evaluates per network realisation. For a generator whose only randomness is a connection mask, the suffix is a handful of array ops while the geometry stays a constant.

seeded_steps

graph_generators.procedural.seeded_steps(spec, resolved=None)

Names of steps that depend on the generator’s PRNG state, transitively.

Seededness is read off the RESOLVED EXPRESSION, not the declared step type: a step is seeded if its expression draws randomness anywhere (it carries the PRNG symbol) or if it references a seeded intermediate. Trusting the declared type instead would miss an equation step that calls a sampler head directly — and a missed draw is the worst possible failure here, because the step would be hoisted into the deterministic prefix and every “independent” realisation would silently share one draw.