Boundary conditions¶
What a boundary condition (BC) receives from a pipe end, which function
decides inflow against outflow and choked against subsonic, where each
refusal happens, and what comes out. A BC is a callable
bc(pipe, end) -> None. It reads the end cell of pipe.U_pad, and it writes
the Ng ghost rows at end (and, for a species-carrying inlet, the
pipe.species_inflow_left / _right dict). The pipe residual then takes the
Riemann flux between the ghost row and the interior row (see
Pipe time step); the BC never computes a flux. The
contracts of every function below are in their docstrings (the standard is
Function contracts and flow charts).
Conventions every BC and every test relies on.
U_padhas shape(N + 2 Ng, 3): rows0 .. Ng-1are the left ghosts,Ng .. Ng+N-1the interior,Ng+N .. Ng+N+Ng-1the right ghosts. Columns arerho[kg/m^3],rho u[kg/(m^2 s)],rho E[J/m^3].uis positive toward +x. Flow INTO the pipe is thereforeu > 0at the LEFT end andu < 0at the RIGHT end. A mass flowmdot > 0and au_inlet > 0mean "into the pipe" at either end (the BC applies the end's sign); a negative value is an outflow through that end.- Every BC writes the SAME state into all
Ngghost rows of its end, encoded as an ideal gas (rho E = p/(gamma-1) + rho u^2/2), unless it goes throughghost_U_eos_aware(mass-flow and velocity inlets), which uses the pipe's EOS energy on a real-gas pipe. - Pressures in Pa, temperatures in K, areas in m^2, gas constants
R_gasin J/(kg K).
From the case file to a BC callable¶
Each [[bc]] block becomes one (pipe, end, callable) triple in the
network's far-BC list. Refusals happen at three layers: the TOML validator
(before the build), the build (unknown pipe, an end a junction already
owns) and the factory itself (added 2026-09-30 for the Python API, which the
validator never sees).
flowchart TD
C["[[bc]]: pipe, end, kind, parameters"] --> V{"io.validate"}
V -- "p0, T0, p_back, A_max, rho_inf or p_inf not positive for a kind that needs it" --> X1["Issue from _validate_bc_static_params"]
V -- "a transient schedule empty or of unequal length" --> X2["Issue from _validate_transient_bc_schedules"]
V -- "unknown pipe, two BCs on one end, BC on a junction port, uncovered pipe end" --> X3["Issues from the pipe-ref, unique-end, junction-conflict and end-coverage checks"]
V --> B{"_build_far_bcs"}
B -- "unknown pipe, or the end is a junction port" --> X4["ValueError"]
B --> G["_pipe_bc_gas: the pipe's own gamma and R_gas, not the global [gas]"]
G --> K{"kind"}
K -- "wall or transmissive" --> S1["the shared wall_bc / transmissive_bc functions, no gas state"]
K -- "egr_valve" --> E1["make_egr_valve_bc, valve state kept for the controller"]
K -- "anything else" --> M{"_make_bc"}
M -- "unknown kind, or a transient kind without a Clock" --> X5["ValueError"]
M -- "throttle without p0 > 0 and T0 > 0" --> X6["ValueError"]
M --> A["_resolve_bc_A_pipe: an unset A_pipe becomes the pipe's end-face area"]
A --> F{"factory: make_stagnation_inlet_bc, make_pressure_outlet_bc, make_mass_flow_inlet_bc, make_velocity_inlet_bc, make_nonreflecting_bc, make_valve_inlet_bc, make_valve_orifice_bc, make_throttle_bc, make_transient_*"}
E1 --> F
F -- "non-physical parameters, a bad lift table, a bad schedule, an unknown ISO 5167 tapping" --> X7["ValueError naming the values, at build"]
F --> H{"_hand_over_in_pipe_model"}
S1 --> OUT["far_bcs entry"]
H -- "needs_translation: real gas, per-cell gamma, or gamma differs from the BC's" --> W["translate_bc wraps the BC (last chart)"]
H -- "plain ideal pipe at the BC's gamma" --> T["BC kept as it is, tagged gas_gamma and gas_R"]
W --> OUT
T --> OUT
OUT --> HS{"_apply_handshake"}
HS -- "species lost or gas model mismatched on the connection" --> X8["CaseValidationError (strict) or a warning"]
Every stage: who calls a BC, and what a failure looks like¶
The time loop sets clock.t to the stage time, then fills every ghost. A
network calls fill_network_ghosts; the single-pipe driver calls the
closure that compose builds. Transient BCs read clock.t, so the order
"set the clock, then fill" is part of the contract.
flowchart TD
ST["RK stage at time t"] --> CK["clock.t = t"]
CK --> WHO{"driver"}
WHO -- "single pipe" --> CO["compose: left(pipe, LEFT) then right(pipe, RIGHT)"]
WHO -- "network" --> FG["fill_network_ghosts"]
FG --> FB["every far BC, in list order: bc(pipe, end)"]
FB --> SW{"arrays has plenum ports?"}
SW -- yes --> SW1["one compiled sweep of the plenum-port ghosts, skip flag armed"]
SW -- no --> JN
SW1 --> JN["every junction: apply_port_bcs (swept ports skipped); the flag is disarmed even if one raises"]
CO --> RES
JN --> RES["residual kernel: Riemann flux between ghost row and interior row"]
FB -- "a jit BC meets a dead end cell (rho or p equal to 0)" --> X1["bare ZeroDivisionError from the BC (not converted: only residual faults get the pipe named)"]
FB -- "a BC raises" --> X2["propagates before the sweep is armed and before any junction runs"]
Which ghost a BC writes¶
Each kind has one decision that matters. The kernels take the end cell's
(rho, u, p) from U_pad with the pipe's gamma (the gas-model wrapper
below makes that exact for other pipes).
Stagnation inlet: inflow, backflow or supersonic outflow¶
The test is on the velocity the pipe would have if it were an open end at
p0 (u_o), not on the sign of the interior velocity, so a slightly
reversed interior with p << p0 is still an inflow. s_in is +1 at the left
end and -1 at the right (the sign of an into-the-pipe velocity).
flowchart TD
I["end cell rho, u, p; reservoir p0, T0"] --> Z{"rho == 0 or p == 0?"}
Z -- yes --> X["ZeroDivisionError"]
Z -- no --> O{"_stagnation_inlet_apply_jit: open-end state with p_b = p0, interior entropy, outgoing invariant kept gives u_o"}
O -- "s_in u_o >= 0: points into the pipe" --> IN["_stagnation_inflow_jit"]
IN --> Q{"_solve_a_b_jit: a positive root of the stagnation quadratic?"}
Q -- yes --> Q1["a_b, u_b from the invariant; T_b and p_b isentropic from T0 and p0"]
Q -- "no: returns -1" --> Q2["reservoir at rest: (rho0, 0, p0)"]
O -- "s_in u_o < 0 and s_in u <= -a: supersonic outflow" --> SS["ghost is the end cell"]
O -- "s_in u_o < 0, subsonic" --> BF["ghost (rho_o, u_o, p0): an open end discharging into the reservoir"]
Pressure outlet and non-reflecting outlet¶
Only supersonic outflow is detected. Supersonic INflow through an outlet is not: the subsonic characteristic formulas run and give a finite ghost.
flowchart TD
P["end cell; p_back (outlet) or a freestream rho_inf, u_inf, p_inf (non-reflecting)"] --> D{"supersonic OUT of the pipe? right end u >= a, left end u <= -a"}
D -- yes --> TR["transmissive_bc: copy the end cell"]
D -- no --> K{"kind"}
K -- "pressure_outlet: _pressure_outlet_apply_jit returns 1" --> PO["ghost p = p_back exactly; rho from the interior entropy; u from the outgoing invariant"]
K -- "nonreflecting" --> NR["outgoing invariant from the end cell, incoming from the freestream; entropy from the end cell"]
NR --> AB{"recovered a_b > 0?"}
AB -- no --> TR
AB -- yes --> NR1["ghost from a_b, u_b"]
PO -.->|"p_back == 0"| X["ZeroDivisionError (a non-positive p_back is refused at build)"]
Mass-flow and velocity inlets¶
These two do not look at the interior velocity. The mass-flow inlet keeps the
end cell's pressure (zero gradient) and solves for the velocity that carries
mdot through the end-face area; it has no choking limit. The velocity inlet
prescribes u and derives p from p0 and T0.
flowchart TD
MF["mass_flow_inlet: mdot signed, T0"] --> Q["quadratic for abs(u_b) from rho_b abs(u_b) A_face = abs(mdot) at the end cell's pressure"]
Q --> SG["sign: +u at the left end, -u at the right; mdot < 0 flips it"]
SG --> TB{"T_b = T0 - u^2/(2 cp) below 1 K?"}
TB -- yes --> TF["T_b = 1 K"]
TB -- no --> ENC
TF --> ENC["ghost_U_eos_aware: ideal energy, or the pipe's EOS energy"]
VI["velocity_inlet: u_inlet, T0, p0"] --> T2{"T_b = T0 - u^2/(2 cp) above 0?"}
T2 -- "no: u beyond the energy limit" --> RS["reservoir at rest (p0, T0)"]
T2 -- yes --> IS["p_b = p0 (T_b/T0)^(gamma/(gamma-1))"]
IS --> ENC
RS --> ENC
Orifice family: valve, throttle and EGR valve¶
Choked against subsonic is decided in one place, orifice_throat_state
(critical pressure ratio (2/(gamma+1))^(gamma/(gamma-1))). The area ratio
scales the ghost velocity so rho_b u_b A_pipe equals the orifice mass flow.
flowchart TD
L{"lift fraction f (valves) or open (throttle, EGR)"} -- "f < 1e-6, or open <= 1e-6" --> W["wall_bc"]
L --> KIND{"kind"}
KIND -- "valve_inlet" --> VI["stagnation-inlet ghost, then momentum scaled by f (mass flow is NOT f times the full flow)"]
KIND -- "throttle or egr_valve" --> BK{"end-cell pressure >= p0?"}
BK -- yes --> W
BK -- no --> WG["write_orifice_inflow_ghost"]
KIND -- "valve_orifice" --> CDR{"cd_reynolds?"}
CDR -- yes --> CD["Cd from iso5167_cd at the live pipe Reynolds number, floored at 5e3"]
CDR -- no --> CDF["fixed Cd, or default_cd of the area ratio when Cd <= 0"]
CD --> DIR
CDF --> DIR{"p0 > end-cell pressure?"}
DIR -- "yes: forward" --> FW["orifice_throat_state(p0, T0, p_int); velocity into the pipe"]
DIR -- "no: reverse (also p0 equal to p_int, at zero flow)" --> RV["orifice_throat_state(p_int, T_int, p0); velocity out of the pipe"]
WG --> TS
FW --> TS{"orifice_throat_state: p_down / p0 below the critical ratio?"}
RV --> TS
TS -- "yes: choked" --> CH["sonic throat at p0 times the critical ratio"]
TS -- "no" --> SU["throat at p_down, isentropic T, u from the energy equation"]
CH --> SC["ghost velocity = u_throat A_eff / A_pipe; A_eff = Cd A_max f (times n_holes)"]
SU --> SC
SC --> BC{"borda_carnot?"}
BC -- yes --> BCP["p_b += rho u_throat^2 sigma (1 - sigma)"]
Cd <= 0 means "derive it from the area ratio" (default_cd). An unknown
ISO 5167 tapping, a NaN in a lift table and A_max < 0 are refused at build.
Transient BCs and the quasi-steady driver¶
A transient BC interpolates its schedules at clock.t with
_interp1d_scalar (clamped past the table ends, never extrapolated) and then
applies the same kernel as the static kind. Its species schedule replaces
pipe.species_inflow_* (the EGR and throttle BCs merge instead). The QS
driver recognises a far BC only by its qs_* tags.
flowchart TD
F["make_transient_*_bc at build"] --> S{"_schedule per required table"}
S -- "empty, unequal length, decreasing or NaN time, non-finite value" --> X1["ValueError"]
S --> P{"_prep_species_schedules"}
P -- "a lone Y_t or Y_v, unequal lengths, non-finite value" --> X2["ValueError"]
P --> BC["bc(pipe, end)"]
BC --> I["_interp1d_scalar at clock.t"]
I --> KN{"kind"}
KN -- "transient stagnation inlet, pressure outlet" --> K1["the jit kernel directly, schedule values NOT checked"]
KN -- "transient mass-flow or velocity inlet" --> K2["a fresh static factory per call: a scheduled T0 or p0 that is not positive raises ValueError"]
K1 --> SP["_apply_species_inflow"]
K2 --> SP
BC --> Q{"_build_connectivity (quasi-steady driver)"}
Q -- "qs_kind: mass_flow_inlet, pressure_outlet, transient_mass_flow_inlet, transient_pressure_outlet" --> Q1["descriptor from the qs_* tags"]
Q -- "any other far BC: stagnation or velocity inlet, wall" --> Q2["NotImplementedError"]
Handing the ghost to the pipe's gas model¶
Every gas-carrying factory encodes its ghost as an ideal gas at one fixed
gamma. A pipe with per-cell gamma or a real-gas EOS decodes it with a
different model, so the pressure it saw was not the one the BC set.
translate_bc makes the handover exact without changing any factory.
flowchart TD
W["translate_bc(bc, gamma_bc)"] --> V["_IdealView: a copy of U_pad; the Ng end cells re-encoded from cell_primitives at gamma_bc"]
V --> R["bc runs unchanged on the view; attribute writes (species inflow) land on the real pipe"]
R --> LOOP{"each ghost row from _ghost_cells: changed by the BC?"}
LOOP -- "no: left as it was" --> KEEP["pipe row untouched"]
LOOP -- yes --> DEC["decode (rho, u, p) with gamma_bc"]
DEC -- "rho == 0" --> X["ZeroDivisionError"]
DEC --> ENC["encode_for_pipe: the slot's gamma, or the EOS temperature that gives p at rho"]
ENC --> ROW["pipe.U_pad ghost row"]
Functions¶
src/aeolus1d/bc/base.py::wall_bcsrc/aeolus1d/bc/base.py::transmissive_bcsrc/aeolus1d/bc/base.py::composesrc/aeolus1d/bc/base.py::fill_network_ghostssrc/aeolus1d/bc/base.py::ghost_U_eos_awaresrc/aeolus1d/bc/base.py::_first_interior_indexsrc/aeolus1d/bc/stagnation_inlet.py::make_stagnation_inlet_bcsrc/aeolus1d/bc/stagnation_inlet.py::_stagnation_inlet_apply_jitsrc/aeolus1d/bc/stagnation_inlet.py::_stagnation_inflow_jitsrc/aeolus1d/bc/stagnation_inlet.py::_solve_a_b_jitsrc/aeolus1d/bc/pressure_outlet.py::make_pressure_outlet_bcsrc/aeolus1d/bc/pressure_outlet.py::_pressure_outlet_apply_jitsrc/aeolus1d/bc/nonreflecting.py::make_nonreflecting_bcsrc/aeolus1d/bc/mass_flow_inlet.py::make_mass_flow_inlet_bcsrc/aeolus1d/bc/velocity_inlet.py::make_velocity_inlet_bcsrc/aeolus1d/bc/valve.py::make_valve_inlet_bcsrc/aeolus1d/bc/valve_orifice.py::make_valve_orifice_bcsrc/aeolus1d/bc/throttle.py::make_throttle_bcsrc/aeolus1d/bc/egr_valve.py::make_egr_valve_bcsrc/aeolus1d/bc/orifice_flow.py::orifice_throat_statesrc/aeolus1d/bc/orifice_flow.py::orifice_mass_flux_per_areasrc/aeolus1d/bc/orifice_flow.py::write_orifice_inflow_ghostsrc/aeolus1d/bc/orifice_flow.py::default_cdsrc/aeolus1d/bc/orifice_flow.py::iso5167_cdsrc/aeolus1d/bc/orifice_flow.py::borda_carnot_pressure_risesrc/aeolus1d/bc/transient.py::make_transient_stagnation_inlet_bcsrc/aeolus1d/bc/transient.py::make_transient_pressure_outlet_bcsrc/aeolus1d/bc/transient.py::make_transient_mass_flow_inlet_bcsrc/aeolus1d/bc/transient.py::make_transient_velocity_inlet_bcsrc/aeolus1d/bc/transient.py::_schedulesrc/aeolus1d/bc/transient.py::_prep_species_schedulessrc/aeolus1d/bc/transient.py::_interp1d_scalarsrc/aeolus1d/bc/transient.py::_apply_species_inflowsrc/aeolus1d/bc/gas_model.py::needs_translationsrc/aeolus1d/bc/gas_model.py::translate_bcsrc/aeolus1d/bc/gas_model.py::_IdealView.__init__src/aeolus1d/bc/gas_model.py::cell_primitivessrc/aeolus1d/bc/gas_model.py::encode_for_pipesrc/aeolus1d/bc/gas_model.py::_ghost_cellssrc/aeolus1d/io/build.py::_build_far_bcssrc/aeolus1d/io/build.py::_make_bcsrc/aeolus1d/io/build.py::_pipe_bc_gassrc/aeolus1d/io/build.py::_hand_over_in_pipe_modelsrc/aeolus1d/io/build.py::_resolve_bc_A_pipesrc/aeolus1d/io/build.py::_apply_handshakesrc/aeolus1d/io/validate.py::_validate_bc_static_paramssrc/aeolus1d/io/validate.py::_validate_transient_bc_schedulessrc/aeolus1d/solver/quasi_steady_network.py::_build_connectivitysrc/aeolus1d/solver/network_timeloop.py::_ExplicitNetwork.fill_ghostssrc/aeolus1d/solver/timeloop.py::_single_pipe_ghosts