Skip to content

UDF: porous drag

Status: available. kind = "python" on any [pipe.porous] block.

Read the chapter overview first — the permission model, the PipeState object and the return-validation rules are shared with friction.

[pipe.porous]
kind = "python"
path = "my_models:darcy_forch"
import numpy as np

def darcy_forch(state):
    mu, K, C_F = 1.8e-5, 1e-9, 0.5
    s_mom = -(mu / K) * state.u - C_F * state.rho * np.abs(state.u) * state.u
    return s_mom, np.zeros_like(s_mom)

Same (s_mom, s_E) return as friction, same length-N validation, same PipeState. The adapter is literally the same function — kept shared on purpose, because two adapters that "obviously do the same thing" is how one of them ends up with the shape check and the other without.

The QS refusal bites harder here

Friction under solver = "quasi_steady" is refused because the QS network folds it into a constant drag. Porous is refused for the same mechanism and a worse consequence.

_qs_alpha_beta reads pipe.porous.params for alpha, beta and mu and folds them into the QS drag coefficient. A user callable has neither an alpha nor a beta, so the pipe would emerge with no porous resistance at all. For a monolith or a filter that is not a small error — it is the entire pressure drop, and the run would look plausible while being completely wrong about flow split.

So the build raises. Use explicit / strang, or supply constants via kind = "darcy_forchheimer".

Provenance

pipe.porous is set to a PorousState with kind = 3 (POROUS_PYTHON) and a params array whose tail slot still carries R_eff — every other porous kind puts the gas constant there, and anything reading that slot must still find it.

_py_closure_kinds hands the @njit kernel NONE because py_porous is set, so the drag is applied exactly once, by the Python overlay.

Both native-only JIT overlay paths in implicit_kernels are gated on all three py_* seams being absent, so the provenance code can never reach a compiled dispatcher. That is pinned by a test, because the failure mode is silent: the @njit if/elif chains have no else, so an unknown kind would add no source and the bug would look like "my closure ran once" rather than an error.