|
STAR
Cal STAR — firmware, DAQ server, and engine design
|
Classes | |
| class | SteadyResult |
| class | ConvergenceError |
| class | _BranchRow |
Functions | |
| SteadyResult | solve_steady (Network network, *, float tol=1.0e-6, int max_iterations=100, float initial_flow=INITIAL_FLOW, bool raise_on_failure=True, Mapping[str, float]|None signals=None, Mapping[str, float]|None guess=None, Iterable[str]|None isolate=None, Mapping[str, tuple[float, float]]|None storage=None, bool report=True) |
| dict[str, tuple[float, float]] | _stores (Network network, Mapping[str, tuple[float, float]]|None storage, list[str] branch_ids) |
| np.ndarray | _initial_guess (Network network, list[str] free, list[str] branch_ids, float initial_flow) |
| np.ndarray | _row_scales (Network network, list[str] free, list[str] branch_ids, float initial_flow) |
| np.ndarray | _local_scales (Network network, np.ndarray x, dict[str, int] node_index, dict[str, int] branch_index, dict[str, _BranchRow] branch_rows, np.ndarray base, float tol) |
| bool | _physical (np.ndarray x, list[str] free) |
| float | _scaled_norm (np.ndarray residual, np.ndarray scale, np.ndarray|None ignore=None) |
| float | _pressure_at (Network network, np.ndarray x, dict[str, int] node_index, str node_id) |
| _BranchRow | _branch_row (Network network, str branch_id, float mdot, float p_up, float p_dn, Mapping[str, float]|None signals=None) |
| dict[str, _BranchRow] | _branch_rows (Network network, np.ndarray x, dict[str, int] node_index, dict[str, int] branch_index, Mapping[str, float]|None signals=None) |
| np.ndarray | _assemble_residual (Network network, np.ndarray x, dict[str, int] node_index, dict[str, int] branch_index, dict[str, _BranchRow] rows, Mapping[str, tuple[float, float]]|None stores=None) |
| np.ndarray | _residuals (Network network, np.ndarray x, dict[str, int] node_index, dict[str, int] branch_index, Mapping[str, float]|None signals=None, Mapping[str, tuple[float, float]]|None stores=None) |
| None | _mass_rows (Network network, np.ndarray x, dict[str, int] node_index, dict[str, int] branch_index, Mapping[str, tuple[float, float]]|None stores, np.ndarray residual) |
| tuple[list[int], list[int]] | _incidence_pattern (Network network, dict[str, int] node_index, dict[str, int] branch_index, Mapping[str, tuple[float, float]]|None stores=None) |
| csc_matrix | _jacobian (Network network, dict[str, int] node_index, dict[str, int] branch_index, dict[str, _BranchRow] branch_rows, list[int] rows, list[int] cols, Mapping[str, tuple[float, float]]|None stores=None) |
| np.ndarray | _damped_update (Network network, np.ndarray x, np.ndarray step, np.ndarray residual, np.ndarray scale, dict[str, int] node_index, dict[str, int] branch_index, Mapping[str, float]|None signals=None, np.ndarray|None ignore=None, Mapping[str, tuple[float, float]]|None stores=None) |
| SteadyResult | _result (Network network, np.ndarray x, bool converged, int iterations, float norm, float started, list[DeadEnd] stubs, set[str] isolated, Mapping[str, tuple[float, float]]|None stores=None, *, bool report=True, Mapping[str, float]|None signals=None) |
Variables | |
| float | MIN_BRANCH_SLOPE = 1.0e-3 |
| float | INITIAL_FLOW = 1.0e-3 |
| float | LOCAL_TOLERANCE = 1.0e-2 |
| float | LOCAL_SCALE_FLOOR = 1.0e5 |
| float | _FD_REL = 1.0e-6 |
| float | _FD_ABS = 1.0e-9 |
| float | _CHOKE_STIFFNESS = 1.0e8 |
The steady network solve: pressure at every node, flow in every branch.
The system is small, square and highly structured. Unknowns are one pressure per
free node and one mass flow per branch; equations are one mass balance per free
node and one pressure relation per branch::
node i : sum(flows in) - sum(flows out) - demand = 0
branch b : (p_up - p_dn) - dp(mdot) = 0
Almost all of the Jacobian is constant. The mass-balance rows are the incidence
matrix -- plus and minus ones that never change. The branch rows are +1 on the
upstream pressure, -1 on the downstream, and a single nonlinear entry:
``d(dp)/d(mdot)``, one scalar per branch. So a Newton step costs one derivative
per component and a sparse solve, and nothing has to differentiate a correlation.
Two things about this problem bite, and both are handled explicitly rather than
hoped away:
**Zero flow is a singular point.** Loss goes as roughly ``mdot^2``, so its
derivative vanishes at zero and the branch row goes with it. A guess that starts
at zero flow cannot take a first step. Solved by starting away from zero and by
flooring the derivative -- see :data:`MIN_BRANCH_SLOPE`.
**Newton overshoots.** An undamped step from a poor guess can send a pressure
negative, at which point the property layer refuses and rightly so. Solved with
backtracking: halve the step until the residual actually improves.
|
protected |
Mass balances plus already-built branch rows. No component evaluation.
Definition at line 756 of file steady.py.
References _mass_rows().
Referenced by solve_steady().
|
protected |
Assemble one branch's equation, in whichever form applies. A symmetric component (``component.symmetric``) is priced at the node the fluid actually comes from: when the drawn downstream is the higher pressure, the conditions are the downstream node's and the choke test reads the drop the other way. It used to be priced at the drawn upstream whatever the gradient, and a reverse drop never choked: a valve drawn against its flow passed 3-8x less than the same valve drawn with it, both converged. One-way laws (check valve, regulator, relief, injector) keep the drawing's direction.
Definition at line 629 of file steady.py.
References max.
Referenced by _branch_rows().
|
protected |
Definition at line 735 of file steady.py.
References _branch_row(), and _pressure_at().
Referenced by _residuals(), _result(), and solve_steady().
|
protected |
Take the largest fraction of the Newton step that improves things. An undamped step from a crude guess routinely sends a pressure negative, at which point the property layer refuses -- correctly, but the solve should back off rather than fall over. Backtracking also handles the case where the step is simply too long, which quadratic losses make common. ``ignore`` must be the *same* mask the convergence test uses. Measuring improvement against a norm that includes a row nothing can improve makes every step look like a failure: the search backtracks to almost nothing, the iteration crawls, and the solve stops just above whatever tolerance was asked for -- tracking it exactly, which is the tell.
Definition at line 891 of file steady.py.
References _residuals(), and _scaled_norm().
Referenced by solve_steady().
|
protected |
Row and column indices of every structurally non-zero Jacobian entry. Computed once. The sparsity of a hydraulic network never changes during a solve -- only the values in it do.
Definition at line 815 of file steady.py.
Referenced by solve_steady().
|
protected |
Pressures between the boundary values, flows small but non-zero. Deliberately crude. A network solve is not very sensitive to where it starts once the first step is possible, and the thing that makes a first step impossible is zero flow rather than a poor pressure guess.
Definition at line 470 of file steady.py.
Referenced by solve_steady().
|
protected |
Definition at line 853 of file steady.py.
Referenced by solve_steady().
|
protected |
:func:`_row_scales`, with each branch also held against its own pressures. One pressure scale for every branch -- the highest fixed pressure in the network -- let a branch on a stand with a 6,000 psi K-bottle sit half a psi off its relation and call it converged. A liquid drain driven by its head (0.45 psi) then never left the solve's 1 g/s starting guess: the first residual was already "small", so Newton took no step. So a branch must also close to :data:`LOCAL_TOLERANCE` of the larger of its two end pressures (never less than a bar). It only ever tightens, and only where the stand's own scale is the looser: a tight solve is judged as before, and a choked charge into an empty bottle -- which a finite- difference Jacobian closes to about a tenth of a percent and no further -- is not asked for more than a percent. Nor is any choked row: its residual is a flow error in pressure units by an arbitrary stiffness (:data:`_CHOKE_STIFFNESS`), so a share of a pressure says nothing about it.
Definition at line 511 of file steady.py.
References _pressure_at(), max, and min.
Referenced by solve_steady().
|
protected |
Fill the mass-balance rows of ``residual``: inflow minus demand, minus what a stored node keeps (``admittance * (p - reference)``).
Definition at line 793 of file steady.py.
Referenced by _assemble_residual(), and _residuals().
|
protected |
Are the node pressures in this iterate actually pressures? Only the node block of the unknown vector: a *branch* flow is legitimately negative, which is just flow the other way.
Definition at line 552 of file steady.py.
Referenced by solve_steady().
|
protected |
Definition at line 588 of file steady.py.
Referenced by _branch_rows(), _local_scales(), and _result().
|
protected |
Residual from scratch. Used where the derivatives are not wanted.
Definition at line 774 of file steady.py.
References _branch_rows(), and _mass_rows().
Referenced by _damped_update().
|
protected |
Package a solution vector as a result. ``signals`` must be the solve's own: the report re-evaluates every branch at the answer, and without them a dome-loaded regulator reported its drawn setpoint and a part-open valve reported itself wide open. ``isolated`` is not optional and must be the same set the solve used. This function rebuilds the branch ordering to read ``x`` back, and that ordering is ``[b for b in branches if b not in stubs | isolated]`` -- so a caller that omits it gets a *longer* list than the vector it is indexing into. The failure is an IndexError on a good day and silently misattributed flows on a bad one, where every branch after the first isolated valve reports its neighbour's flow. It defaulted to empty until the convergence-failure path, which is the one place that omitted it, started raising on a stand with a shut valve on it.
Definition at line 950 of file steady.py.
References _branch_rows(), and _pressure_at().
Referenced by solve_steady().
|
protected |
A magnitude for each equation, so one tolerance can judge them all. The residual vector is in mixed units: mass balances are kg/s, branch relations are Pa. Comparing both against a single absolute tolerance means ``tol = 1e-6`` demands pressure to a micropascal -- about 1e-12 relative on a 30 bar system, which is far below what a finite-difference Jacobian can deliver, so the solve grinds without ever "converging". Scaling by row does not touch the Newton step -- ``J^-1 F`` is invariant under row scaling -- so this changes only the convergence test, and changes it from arbitrary to meaningful.
Definition at line 488 of file steady.py.
References max.
Referenced by solve_steady().
|
protected |
Worst row, scaled. Rows in ``ignore`` are excluded. Exclusion is not leniency. A branch whose ``d(dp)/d(mdot)`` has hit :data:`MIN_BRANCH_SLOPE` is one the Jacobian *cannot* resolve -- a fully open isolation valve carrying zero flow because the regulator downstream is holding has a genuinely flat row, and no step size moves it. Letting that one row veto the whole solve means a correct answer, with mass closing to 1e-14, is reported as a failure. Two things keep this from hiding a real fault: mass-balance rows are never excluded, so conservation still has to hold everywhere; and every excluded branch is listed in :attr:`SteadyResult.regularised_branches`, so a solve standing on twenty of them says so.
Definition at line 561 of file steady.py.
Referenced by _damped_update(), and solve_steady().
|
protected |
The ``storage`` entries that take part in this solve, in a fixed order. A boundary node with a positive admittance and at least one branch still carrying flow. One cut off by shut valves keeps its fixed pressure, so a stand with everything shut solves exactly as it would without storage.
Definition at line 441 of file steady.py.
Referenced by solve_steady().
| SteadyResult feedtwin.solve.steady.solve_steady | ( | Network | network, |
| * | , | ||
| float | tol = 1.0e-6, | ||
| int | max_iterations = 100, | ||
| float | initial_flow = INITIAL_FLOW, | ||
| bool | raise_on_failure = True, | ||
| Mapping[str, float] | None | signals = None, | ||
| Mapping[str, float] | None | guess = None, | ||
| Iterable[str] | None | isolate = None, | ||
| Mapping[str, tuple[float, float]] | None | storage = None, | ||
| bool | report = True ) |
Solve a network for its steady operating point.
Args:
network: The system. Validated first.
tol: Convergence tolerance on the scaled residual.
max_iterations: Newton iteration cap.
initial_flow: Starting flow for every branch [kg/s]. Non-zero on
purpose -- see the module docstring.
raise_on_failure: Raise :class:`ConvergenceError` on failure. Set false
to inspect a failed result instead, which is what a sweep wants.
isolate: Extra branches to treat as open circuits, on top of the
valves that report themselves shut. For what a caller knows and the
network cannot -- an empty tank's outlet being the case.
guess: Starting point, as ``{node or branch id: pressure or flow}``.
A transient re-solves the same network a few thousand times at
states that barely differ, and starting each one from a cold
1e-3 kg/s throws that away -- it costs iterations, and on a stiff
network it can land the search in a different basin than the step
before, which reads as a convergence failure at one instant and
nowhere else. Unknown ids are ignored, so a guess from a slightly
different network is safe to pass.
signals: Control inputs -- ``{"MV-01.command": 0.4}`` for a part-open
valve. Every component that reads a signal sees the same mapping,
so a scenario's timeline reaches the physics without the solver
knowing what any particular signal means. Omitted, each component
falls back to its own default, which for a valve is fully open.
storage: Boundary nodes to close implicitly, as ``{node id:
(admittance [kg/s per Pa], reference pressure [Pa])}``. A vessel
behind a wide line is a boundary only for as long as the flow it
receives cannot move it; when it can, the step that holds it at
its start-of-step pressure is an explicit coupling, and a small
ullage behind a Cv 4 solenoid flip-flops against it. A node listed
here becomes an unknown with a storage term in its mass balance,
``inflow - demand - admittance * (p - reference) = 0``: backward
Euler on the vessel, where ``reference`` is where it would land
with no flow and ``admittance`` is ``C / dt``. Its solved pressure
is the vessel's end-of-step pressure. Nodes not fixed, or with no
branch left carrying flow, are ignored; omitted, nothing changes.
report: Fill the result's per-branch report -- ``diagnostics``,
``regularised_branches``, ``violations``, ``stack`` -- which takes
one more evaluation of every branch at the answer. A live session
reads none of it and solves a few hundred times a second of stand,
so it asks for none; pressures, flows and the mass residuals are
the same either way.
Returns:
A :class:`SteadyResult`, including mass-conservation residuals whether
or not it converged.
Definition at line 154 of file steady.py.
References _assemble_residual(), _branch_rows(), _damped_update(), _incidence_pattern(), _initial_guess(), _jacobian(), _local_scales(), _physical(), _result(), _row_scales(), _scaled_norm(), and _stores().
|
protected |