Channel at a Prescribed Flow Rate (constant unknowns)
Stokes flow through a channel is usually driven by a pressure you choose. Here the flow rate Q is
prescribed instead, and the inlet pressure that delivers it is solved for. That pressure is a single
number, so it is a constant unknown: P = d.unknown.scalar(constant=True).
The unknowns and their test functions
u = d.unknown.vector(2, order=2) # Taylor-Hood velocity
p = d.unknown.scalar(name="p") # pressure
P = d.unknown.scalar(constant=True, name="P_in") # the inlet pressure: one value
v, q, W = u.test(), p.test(), P.test()
Each test function is derived from its unknown, so space, shape and order always match.
The term list
fem = jno.fem([
inner(grad(u), grad(v), n_contract=2) - pi * div(v), # momentum
-qi * div(u), # continuity
P * (-v_in[0]), # inlet traction -P n
(u_in[0] - Q / H) * W, # the flow rate: an integral row
u(xw, yw) - 0.0, u(xi, yi)[1] - 0.0,
])
P multiplies the inlet traction like any coefficient. Its test function W is one everywhere, so the
term (u_in[0] - Q/H) * W integrated over the inlet is the equation ∫ u_x dy = Q. Nothing else is
needed: the constant is assembled as one extra DOF after the fields.
What to notice
- The answer is the Poiseuille value
P = 12 Q L / H³to round-off; the velocity is the parabola and the pressure the linear profile, both in the Taylor-Hood space, so they are exact too. Phas no diagonal entry of its own — its only equation is theWrow — so this is a bordered saddle system and is solved directly (linear=jno.solve.lu()).- The same constant can instead be tied to a field on a region,
u(region) - U, which makes the field uniform there with a solved-for value. See Boundary conditions.
Full script
"""A channel driven at a prescribed flow rate: the inlet pressure is a constant unknown.
-Δu + ∇p = 0, ∇·u = 0 on [0, L] x [0, H]
u = 0 on the walls, u_y = 0 on the inlet, a do-nothing outlet,
inlet traction -P n with P unknown, and ∫_inlet u_x dy = Q.
The pressure needed to push Q through the channel is not given -- it is solved for. It is ONE number,
so it is a constant unknown: `d.unknown.scalar(constant=True)`. Its trial multiplies the inlet traction
and its test function, which is one everywhere, turns a weak term on the inlet into the flow-rate
equation. Closed form (Poiseuille, unit viscosity): P = 12 Q L / H³.
"""
import jax
import numpy as np
import jno
jax.config.update("jax_enable_x64", True) # the assembler builds in float64
L, H, Q = 2.0, 1.0, 0.3
d = jno.shape.rect(0.0, 0.0, L, H).structured(n=6).domain()
d.tag("walls", lambda x, y: (y < 1e-9) | (y > H - 1e-9))
x, y = d.variable("interior", split=True)[:2]
xi, yi = d.variable("left", split=True)[:2] # the inlet
xw, yw = d.variable("walls", split=True)[:2]
print(jno.info(d))
u = d.unknown.vector(2, order=2) # Taylor-Hood velocity
p = d.unknown.scalar(name="p") # and pressure
P = d.unknown.scalar(constant=True, name="P_in") # the inlet pressure: one value
v, q, W = u.test(), p.test(), P.test()
grad = lambda w: jno.np.jacobian(w, [x, y]) # noqa: E731
div = lambda w: jno.np.trace(grad(w)) # noqa: E731
inner = jno.np.inner
pi, qi = p.bind(x=x, y=y), q.bind(x=x, y=y)
u_in, v_in = u.bind(x=xi, y=yi), v.bind(x=xi, y=yi)
fem = jno.fem(
[
inner(grad(u), grad(v), n_contract=2) - pi * div(v), # momentum
-qi * div(u), # continuity
P * (-v_in[0]), # inlet traction -P n, with n = (-1, 0)
(u_in[0] - Q / H) * W, # the flow rate through the inlet: an integral row
u(xw, yw) - 0.0, # no slip
u(xi, yi)[1] - 0.0, # inflow normal to the inlet
]
)
print(jno.info(fem))
sol = fem.solve(linear=jno.solve.lu()) # a bordered saddle system: solve it directly
P_in = float(np.asarray(sol)[fem.blocks[fem.block_index(P)]][0])
exact = 12 * Q * L / H**3
print(f"\ninlet pressure P = {P_in:.12f} Poiseuille 12 Q L / H^3 = {exact:.12f}")
assert abs(P_in - exact) < 1e-10