Skip to content

UDF: junction boundary conditions

Status: a fixed selector, not an arbitrary path. py_junction_bc is reachable from TOML today, but it chooses from a registered set rather than importing your module.

Read the chapter overview for the shared contract.

What exists today

[[junction.ports]]
pipe = "runner1"
py_junction_bc = "learned_npz"
py_junction_bc_npz = "weights.npz"

Registered kinds:

kind code what it is
none 0 no override
resistive_demo 1 a worked example of the seam
learned_npz 2 an ML surrogate loaded from an .npz

learned_npz is the interesting one: it already does what a UDF is for — substituting a learned model for an analytic port closure — but it moves data, not code. The weights file is inert; loading it cannot execute anything.

Why it is not a dotted path

That inertness is the feature. A junction BC runs inside the network Picard/Newton loop, where a callable that is slow, non-deterministic, or raises on some iterate does not merely give a wrong answer — it can stop the coupled solve from converging at all, in a way that is very hard to attribute back to the closure.

Extending this seam to arbitrary user code is therefore a bigger step than friction, and would want:

  • a contract on determinism (same inputs → same outputs; the Picard iteration re-evaluates the same state repeatedly);
  • cost awareness — this is called per port per iteration, not once per step;
  • a failure policy inside a non-linear solve, where raising mid-Newton and unwinding cleanly is not free.

See src/aeolus1d/closures/learned_junction_bc.py for the existing implementation and the shape a surrogate must have.