Build a network from a case¶
build_network turns a loaded Case (the dataclasses load_case produced, or
a Case assembled in code) into the objects the time loops advance: pipes,
junctions, cylinders, turbos, shafts, far BCs, controllers, the coolant system
and the connection ledger. It is the one seam every route to a live network
passes through: run_case, the CLI, the GUI's run, optimize, the FMU and the
tests. The contract of every function below is in its docstring (the standard is
Function contracts and flow charts).
This chart stops at the NetworkBundle that comes out. What happens before it
(reading the file, validate_case) and after it (the run, the writer) is drawn
in the case-load chart; the first step of this chart is the validation call.
The build, in order¶
What enters: a Case, an optional Clock, and validate (True by default).
What leaves: a NetworkBundle, which also unpacks as the legacy
(pipes, junctions, bcs).
flowchart TD
IN["Case, clock, validate"] --> V{"validate?"}
V -- yes --> VC["validate_case: CaseValidationError"]
V -- no --> TH
VC --> TH["clear the throttle registry"]
TH --> NG{"ng_for_scheme and limiter name"}
NG -- "unknown scheme or limiter" --> X1["ValueError"]
NG --> CK{"_case_has_transient_bcs and no clock?"}
CK -- yes --> NC["a fresh internal Clock, not returned"]
CK -- no --> SO
NC --> SO{"_check_species_ordering"}
SO -- "wrong species order" --> X2["ValueError naming the pipe"]
SO --> BP["_build_pipes: one Pipe per spec (second chart)"]
BP --> EN["_enable_species_transport_for_inlet_schedules"]
EN --> RX["_attach_reactions: [[reaction]] blocks bind columns"]
RX --> JN["_build_junctions (third chart)"]
JN --> SH["_build_shafts"]
SH --> TB["_build_turbos: claim inlet and outlet ends"]
TB --> CY["_build_cylinders: claim port ends"]
CY --> FB["_build_far_bcs: one BC per [[bc]]"]
FB --> CV{"_assert_pipe_ends_covered"}
CV -- "an end nothing writes" --> X3["ValueError naming pipe and end"]
CV --> CT["_build_controllers: bind ids to built objects"]
CT --> CO["_build_coolant_system: walls, coolant pipes, couplings"]
CO --> AB["_assemble_bundle: NetworkBundle, then the typed array mirror"]
AB --> HS{"_apply_handshake: build_ledger, then mode"}
HS -- "strict and problems" --> X4["CaseValidationError"]
HS -- "warn and problems" --> W["one log line per problem per process"]
HS --> OUT["NetworkBundle"]
W --> OUT
Three orderings matter. The species-order check is spec-only and runs first, so
its repair hint is what the user sees rather than a later column error. The
typed array mirror (bundle.arrays) is built last because it rebinds every
pipe's U_pad to a view of one flat buffer, so no pipe may be created after
it. Every component that owns a pipe end claims it through _claim_end, and
_build_far_bcs refuses a BC on a claimed end, so a pipe end has exactly one
writer.
One pipe¶
_build_pipes makes a Pipe per [[pipe]] spec. The closure families attach
in a fixed order, because the DPF columns and the urea injector read the species
names the species step registers. A refusal part-way leaves the earlier families
attached on a pipe that is then thrown away.
flowchart TD
SP["PipeSpec"] --> AR{"_resolve_pipe_area: exactly one of diameter, area, profile"}
AR -- "none or several, or a bad diameter" --> X1["ValueError naming the pipe"]
AR -- "profile" --> AP["_area_profile_array"]
AP -- "bad throat, width or table" --> X2["ValueError"]
AR --> PI["Pipe(L, N, gamma, Ng, A, scheme, limiter)"]
AP --> PI
PI --> FR["_attach_friction: required fields, then the kind"]
FR --> HE["_attach_heat: D_h, inert-field warning, then the kind"]
HE --> PO["_attach_porous"]
PO --> CA["_attach_catalyst"]
CA --> FI["_attach_filter: _dpf_state"]
FI --> SPC{"pipe_spec_has_species?"}
SPC -- yes --> AS["_attach_species: columns, per-cell gamma, reaction attacher"]
SPC -- no --> EO
AS --> EO["eos_kind_and_params, pipe.R_gas"]
EO --> DC{"a DPF on this pipe?"}
DC -- yes --> CF["_cache_filter_species_columns"]
DC -- no --> UR
CF --> UR["_attach_urea"]
UR --> LM{"hllc_low_mach and per-cell gamma?"}
LM -- yes --> X3["ValueError"]
LM -- no --> AI["_apply_init: uniform, riemann, regional or mode"]
AI --> PG{"per-cell gamma?"}
PG -- yes --> SG["settle_initial_per_cell_gamma"]
PG -- no --> DONE["pipe registered by id"]
SG --> DONE
Where the refusals are, by family:
| Stage | Refuses |
|---|---|
_resolve_pipe_area |
no or several area sources, a diameter that is not finite and positive |
_area_profile_array |
a parabola whose throat sits on a pipe end or that ignores an end area, a tanh width that is not positive, an empty or ragged table |
_attach_friction and the kind attachers |
a required field at its 0 default; a minor loss with no positive K; a python closure under the quasi-steady solver or without a path |
_attach_heat |
a wall-flux kind with no D_h (a field left at 0 only warns, in _warn_inert) |
_attach_porous, _attach_catalyst |
a Darcy-Forchheimer K that is not positive; a variable-Nu catalyst without the monolith form or with an unknown correlation; a mechanism catalyst with a missing file, an unknown surface species or a gas species not in the database |
_dpf_state, _cache_filter_species_columns |
frac_fast outside [0, 1]; a PM species the pipe does not track |
_attach_species and the reaction attachers |
a primary the species contract does not allow; a TWC hydrocarbon that is not propene; a mechanism file that is missing or whose species list differs |
_attach_urea |
an HNCO share with nowhere to go |
eos_kind_and_params |
an unknown EOS kind; a cv polynomial on the ideal kind |
_apply_init |
an empty regional init; a block with neither T nor rho; a T or a rho beside it that is not positive; a state Pipe calls non-physical; a mode init on a real-gas pipe |
Junctions, ends and BCs¶
Every component that writes the ghost cells of a pipe end owns that end. Junction
ports, turbo inlet and outlet ends, and cylinder ports claim theirs through
_claim_end; a biased_plenum claims none, because it turns into generated BCs
that cover the ends instead.
flowchart TD
JS["case.junctions"] --> K{"kind"}
K -- biased_plenum --> EB["_expand_biased_plenum: one transient mass-flow BC per port"]
EB -- "under 2 ports, mdot <= 0, bad UI, Y over 1, no supply T" --> X1["ValueError naming the junction"]
EB --> GE["far_bcs and generated_bc_ends"]
K -- "port names no pipe" --> X2["ValueError"]
K -- "other kinds" --> CL["_claim_end for every port"]
CL -- "end already owned" --> X3["ValueError naming the second owner"]
CL --> KD{"corberan, ehr, passthrough, else plenum"}
KD --> BJ["_build_corberan_junction, _build_ehr_junction, _build_passthrough_junction, _build_plenum_junction"]
BJ --> VOL["_make_volume_for_junction and _junction_init_T for ehr and plenum"]
VOL -- "volume not positive, mixed EOS ports, no init T" --> X4["ValueError"]
BS["case.bcs"] --> FB{"_build_far_bcs: pipe known, end free, not duplicated?"}
FB -- no --> X5["ValueError naming the end"]
FB --> EG{"kind egr_valve?"}
EG -- yes --> EV["EGRValve starts closed, make_egr_valve_bc"]
EG -- no --> MB["_make_bc on the pipe's own gas"]
MB -- "no Clock, throttle without p0 or T0, unknown kind" --> X6["ValueError"]
EV --> HO["_hand_over_in_pipe_model: gas tags, translation for real gas"]
MB --> HO
HO --> FO["far_bcs gets (pipe, End, bc)"]
_resolve_bc_A_pipe and _resolve_port_area fill an unset reference area from
the pipe's own end face, because that is the area the pipe debits at the face.
What comes out, and what a refusal looks like¶
A refusal is a ValueError that names the pipe, junction, controller or turbo
it concerns (CaseValidationError from validate_case and from a strict
handshake). Nothing is returned on a refusal: there is no partial network.
The bundle holds pipes, junctions (junctions, then turbos, then cylinders,
then shafts), bcs (biased-plenum BCs first, then the [[bc]] specs),
controllers and controllers_by_id, coolant_pipes, coolant_couplings,
wall_thermal_pairs, arrays (the typed mirror, None when AEOLUS_SOA=0) and
handshake.
A Clock passed in is the one every clocked BC, cylinder and user closure
reads. With none passed and a case that needs one, the build makes its own and
does not return it, so the BCs stay at t = 0 unless someone advances it; the
run paths all pass their own.
Defaults and who owns them¶
The schema dataclasses own every spec default. The build restates a few, as literals, where the schema cannot say "unset"; each is a second copy that must move with its owner.
Literal in io/build.py |
Where | Owner of the fact |
|---|---|---|
| 287.05 (air R) | _AIR_R_GAS, read by _pipe_R_gas as "no per-pipe override" |
EOSSpec.R_gas default; modes/reference.py and the GUI repeat the rule |
| 90 degrees | _friction_minor_loss when bend_angle_deg is 0 |
FrictionSpec.bend_angle_deg documents "90 = right angle" |
| 0.76 open frontal area | _catalyst_two_phase_heat_var_nu when ofa is 0 |
CatalystSpec.ofa default |
| 2.98 | the Sieder-Tate laminar Nu floor, set by a case flag | the build (the square-channel asymptote) |
| 298.15 K, CSF activation energies and heats | _dpf_state for an absent thermal or CSF block |
ThermalSpec, CSFSpec defaults |
| 28.01e-3, 42.08e-3, 30.01e-3, 31.998e-3, 46.0055e-3 kg/mol | the Voltz, TWC and SCR attachers | the closure factories' own defaults; the schema comment for the TWC masses says the case [[species]] table is the fallback, the code does not read it |
| the housing placeholders | _HOUSING_PLACEHOLDER_DEFAULTS |
TurboHeatSpec defaults, pinned equal by a test |
| 400 K | _attach_cylinder_wall_heat when only a zone temperature is set |
the build |
| 0.6 x bore | _combustion_multizone_diesel bowl diameter |
the build |
| 1.35 | _CylinderCombustion.si_gamma_unburnt |
WiebeSpec.si_gamma_unburnt default |
| open = 1.0 | the throttle's starting position in _make_bc |
the build |
| 2 % | _INIT_OVERDETERMINED_REL, the rho-versus-T warning band |
the build |
Functions¶
src/aeolus1d/io/build.py::build_networksrc/aeolus1d/io/build.py::_case_has_transient_bcssrc/aeolus1d/io/build.py::_check_species_orderingsrc/aeolus1d/io/build.py::_build_pipessrc/aeolus1d/io/build.py::_enable_species_transport_for_inlet_schedulessrc/aeolus1d/io/build.py::_attach_reactionssrc/aeolus1d/io/build.py::_bind_reaction_columnssrc/aeolus1d/io/build.py::_build_junctionssrc/aeolus1d/io/build.py::_build_shaftssrc/aeolus1d/io/build.py::_build_turbossrc/aeolus1d/io/build.py::_build_cylinderssrc/aeolus1d/io/build.py::_build_far_bcssrc/aeolus1d/io/build.py::_assert_pipe_ends_coveredsrc/aeolus1d/io/build.py::_build_controllerssrc/aeolus1d/io/build.py::_build_coolant_systemsrc/aeolus1d/io/build.py::_assemble_bundlesrc/aeolus1d/io/build.py::_apply_handshakesrc/aeolus1d/io/build.py::NetworkBundle.__init__src/aeolus1d/io/build.py::_resolve_pipe_areasrc/aeolus1d/io/build.py::_area_profile_arraysrc/aeolus1d/io/build.py::_attach_closuressrc/aeolus1d/io/build.py::_attach_frictionsrc/aeolus1d/io/build.py::_attach_heatsrc/aeolus1d/io/build.py::_warn_inertsrc/aeolus1d/io/build.py::_attach_poroussrc/aeolus1d/io/build.py::_attach_catalystsrc/aeolus1d/io/build.py::_attach_filtersrc/aeolus1d/io/build.py::_dpf_statesrc/aeolus1d/io/build.py::_attach_speciessrc/aeolus1d/io/build.py::pipe_spec_has_speciessrc/aeolus1d/io/build.py::_cache_filter_species_columnssrc/aeolus1d/io/build.py::_attach_ureasrc/aeolus1d/io/build.py::eos_kind_and_paramssrc/aeolus1d/io/build.py::_pipe_R_gassrc/aeolus1d/io/build.py::_apply_initsrc/aeolus1d/io/build.py::_apply_mode_initsrc/aeolus1d/io/build.py::_state_tuplesrc/aeolus1d/io/build.py::_init_T_winssrc/aeolus1d/io/build.py::eos_rho_from_p_Tsrc/aeolus1d/io/build.py::_claim_endsrc/aeolus1d/io/build.py::_expand_biased_plenumsrc/aeolus1d/io/build.py::_build_corberan_junctionsrc/aeolus1d/io/build.py::_build_ehr_junctionsrc/aeolus1d/io/build.py::_build_passthrough_junctionsrc/aeolus1d/io/build.py::_build_plenum_junctionsrc/aeolus1d/io/build.py::_make_volume_for_junctionsrc/aeolus1d/io/build.py::_junction_init_Tsrc/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::_resolve_port_area