Skip to content

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_pad has shape (N + 2 Ng, 3): rows 0 .. Ng-1 are the left ghosts, Ng .. Ng+N-1 the interior, Ng+N .. Ng+N+Ng-1 the right ghosts. Columns are rho [kg/m^3], rho u [kg/(m^2 s)], rho E [J/m^3].
  • u is positive toward +x. Flow INTO the pipe is therefore u > 0 at the LEFT end and u < 0 at the RIGHT end. A mass flow mdot > 0 and a u_inlet > 0 mean "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 Ng ghost rows of its end, encoded as an ideal gas (rho E = p/(gamma-1) + rho u^2/2), unless it goes through ghost_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_gas in 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_bc
  • src/aeolus1d/bc/base.py::transmissive_bc
  • src/aeolus1d/bc/base.py::compose
  • src/aeolus1d/bc/base.py::fill_network_ghosts
  • src/aeolus1d/bc/base.py::ghost_U_eos_aware
  • src/aeolus1d/bc/base.py::_first_interior_index
  • src/aeolus1d/bc/stagnation_inlet.py::make_stagnation_inlet_bc
  • src/aeolus1d/bc/stagnation_inlet.py::_stagnation_inlet_apply_jit
  • src/aeolus1d/bc/stagnation_inlet.py::_stagnation_inflow_jit
  • src/aeolus1d/bc/stagnation_inlet.py::_solve_a_b_jit
  • src/aeolus1d/bc/pressure_outlet.py::make_pressure_outlet_bc
  • src/aeolus1d/bc/pressure_outlet.py::_pressure_outlet_apply_jit
  • src/aeolus1d/bc/nonreflecting.py::make_nonreflecting_bc
  • src/aeolus1d/bc/mass_flow_inlet.py::make_mass_flow_inlet_bc
  • src/aeolus1d/bc/velocity_inlet.py::make_velocity_inlet_bc
  • src/aeolus1d/bc/valve.py::make_valve_inlet_bc
  • src/aeolus1d/bc/valve_orifice.py::make_valve_orifice_bc
  • src/aeolus1d/bc/throttle.py::make_throttle_bc
  • src/aeolus1d/bc/egr_valve.py::make_egr_valve_bc
  • src/aeolus1d/bc/orifice_flow.py::orifice_throat_state
  • src/aeolus1d/bc/orifice_flow.py::orifice_mass_flux_per_area
  • src/aeolus1d/bc/orifice_flow.py::write_orifice_inflow_ghost
  • src/aeolus1d/bc/orifice_flow.py::default_cd
  • src/aeolus1d/bc/orifice_flow.py::iso5167_cd
  • src/aeolus1d/bc/orifice_flow.py::borda_carnot_pressure_rise
  • src/aeolus1d/bc/transient.py::make_transient_stagnation_inlet_bc
  • src/aeolus1d/bc/transient.py::make_transient_pressure_outlet_bc
  • src/aeolus1d/bc/transient.py::make_transient_mass_flow_inlet_bc
  • src/aeolus1d/bc/transient.py::make_transient_velocity_inlet_bc
  • src/aeolus1d/bc/transient.py::_schedule
  • src/aeolus1d/bc/transient.py::_prep_species_schedules
  • src/aeolus1d/bc/transient.py::_interp1d_scalar
  • src/aeolus1d/bc/transient.py::_apply_species_inflow
  • src/aeolus1d/bc/gas_model.py::needs_translation
  • src/aeolus1d/bc/gas_model.py::translate_bc
  • src/aeolus1d/bc/gas_model.py::_IdealView.__init__
  • src/aeolus1d/bc/gas_model.py::cell_primitives
  • src/aeolus1d/bc/gas_model.py::encode_for_pipe
  • src/aeolus1d/bc/gas_model.py::_ghost_cells
  • src/aeolus1d/io/build.py::_build_far_bcs
  • src/aeolus1d/io/build.py::_make_bc
  • src/aeolus1d/io/build.py::_pipe_bc_gas
  • src/aeolus1d/io/build.py::_hand_over_in_pipe_model
  • src/aeolus1d/io/build.py::_resolve_bc_A_pipe
  • src/aeolus1d/io/build.py::_apply_handshake
  • src/aeolus1d/io/validate.py::_validate_bc_static_params
  • src/aeolus1d/io/validate.py::_validate_transient_bc_schedules
  • src/aeolus1d/solver/quasi_steady_network.py::_build_connectivity
  • src/aeolus1d/solver/network_timeloop.py::_ExplicitNetwork.fill_ghosts
  • src/aeolus1d/solver/timeloop.py::_single_pipe_ghosts