1"""A drawing made in pid-designer the way a person makes one.
3pid-designer's lines have no direction: "walking any line in either direction.
4What 'upstream' means on a drawing with no arrows" (its `checks.ts`). Which end
5is a line's ``source`` is only the end the mouse started at. The shipped stand
6drawings were authored in flow order, so reading ``source -> target`` as
7upstream to downstream happened to work on them; a drawing made freely broke on
8it. Run a press line *from* the tank *to* its solenoid and both of the
9solenoid's lines arrived at its inlet, its outlet hung open, and the press
10valve was read as a vent from the regulator to atmosphere.
12These pin the rule that replaced it -- the port a line was drawn on decides the
13side, but only where the old reading left a side with nothing on it -- and the
14two messages a user importing their own drawing actually needs.
17from __future__
import annotations
30def P(value: float, unit: str, source: str =
"measured") -> dict[str, object]:
31 return {
"value": value,
"unit": unit,
"source": source,
"reference":
""}
34def node(nid: str, ctype: str, label: str, **data: object) -> dict[str, object]:
37 "position": {
"x": 0,
"y": 0},
38 "data": {
"componentType": ctype,
"label": label, **data},
43 eid: str, a: str, b: str, from_port: str =
"", to_port: str =
""
44) -> dict[str, object]:
45 out: dict[str, Any] = {
52 "length":
P(1.0,
"m"),
54 "roughness":
P(0.0015,
"mm"),
59 out[
"sourceHandle"] = from_port
61 out[
"targetHandle"] = to_port
65def press_leg(*lines: dict[str, object], sol: dict[str, Any] |
None =
None) -> Any:
66 """Bottle, press solenoid, fuel tank, with the two lines as given."""
74 params={
"pressure":
P(4500,
"psi"),
"temperature":
P(293.15,
"K")},
81 params={
"Cv":
P(1.7,
"Cv"),
"bore":
P(6.35,
"mm")},
89 "pressure":
P(550,
"psi"),
90 "temperature":
P(293.15,
"K"),
91 "volume":
P(8.67,
"L"),
100 return build_network(read_diagram(payload, name=
"press leg"))
107 """Both lines drawn *into* the solenoid, on its two ports. The tank's line is
108 on ``r``, so it is the outlet; nothing is vented, and the pressurant reaches
109 the tank's ullage."""
112 line(
"bottle-sv",
"KB",
"SV", to_port=
"l"),
113 line(
"tank-sv",
"TK",
"SV", from_port=
"t", to_port=
"r"),
117 assert net.branches[
"bottle-sv"].downstream ==
"SV.in"
118 assert net.branches[
"tank-sv"].downstream ==
"SV.out"
119 assert net.nodes[
"SV.out"].pressure
is None,
"the press valve was read as a vent"
120 assert net.branches[
"tank-sv"].upstream == b.tanks[
"TK"].ullage
121 assert not [w
for w
in b.warnings
if "venting" in w]
127 line(
"sv-bottle",
"SV",
"KB", from_port=
"l"),
128 line(
"sv-tank",
"SV",
"TK", from_port=
"r", to_port=
"t"),
132 assert net.branches[
"sv-bottle"].upstream ==
"SV.in"
133 assert net.branches[
"sv-tank"].upstream ==
"SV.out"
134 assert net.nodes[
"SV.out"].pressure
is None
140 line(
"bottle-sv",
"KB",
"SV", to_port=
"l"),
141 line(
"tank-sv",
"TK",
"SV", from_port=
"t", to_port=
"r"),
144 assert [w
for w
in b.warnings
if "SV-PRESS" in w
and "port" in w]
148 """The gate. Drawn in flow order, the ports are not consulted even when they
149 disagree with the direction -- a drawing that worked yesterday builds the
150 same network today."""
152 line(
"bottle-sv",
"KB",
"SV", to_port=
"r"),
153 line(
"sv-tank",
"SV",
"TK", from_port=
"l", to_port=
"t"),
155 b =
built(flow_order)
156 assert b.network.branches[
"bottle-sv"].downstream ==
"SV.in"
157 assert b.network.branches[
"sv-tank"].upstream ==
"SV.out"
158 assert not [w
for w
in b.warnings
if "port" in w]
161def _through(ctype: str, *lines: dict[str, object]) -> Any:
162 """Bottle, a check valve or regulator ``X``, vent: the smallest directed run."""
163 params = {
"setpoint":
P(500,
"psi")}
if ctype ==
"PR" else {}
171 params={
"pressure":
P(4500,
"psi"),
"temperature":
P(293.15,
"K")},
173 node(
"X", ctype, f
"{ctype}-1", params=params),
174 node(
"V",
"VENT",
"VENT-1"),
176 "edges": list(lines),
183 result = solve_steady(
built(payload).network, raise_on_failure=
False)
184 return float(result.flows.get(
"X", 0.0))
187@pytest.mark.parametrize("ctype", ["CV", "PR"])
191 """Ports right (``l`` in, as pid-designer draws and checks a CV), both lines
192 dragged from the far end. pid-designer is silent, so the twin must flow;
193 it read the direction and put the bottle on the outlet, flowing nothing."""
196 line(
"x-kb",
"X",
"KB", from_port=
"l", to_port=
"r"),
197 line(
"v-x",
"V",
"X", from_port=
"l", to_port=
"r"),
199 net =
built(payload).network
200 assert net.branches[
"x-kb"].upstream ==
"X.in",
"the bottle is on the inlet"
201 assert net.branches[
"v-x"].downstream ==
"X.out"
202 assert _flow(payload) > 0.1
205@pytest.mark.parametrize("ctype", ["CV", "PR"])
209 line(
"kb-x",
"KB",
"X", from_port=
"r", to_port=
"l"),
210 line(
"x-v",
"X",
"V", from_port=
"r", to_port=
"l"),
213 assert b.network.branches[
"kb-x"].downstream ==
"X.in"
214 assert not [w
for w
in b.warnings
if "port" in w]
215 assert _flow(payload) > 0.1
219 """Lines in flow order, but onto ``r`` from the bottle: pid-designer's
220 ``cv-backwards``. The valve's own inlet faces the vent, so it does not
221 pass the bottle -- and the build says why."""
224 line(
"kb-x",
"KB",
"X", from_port=
"r", to_port=
"r"),
225 line(
"x-v",
"X",
"V", from_port=
"l", to_port=
"l"),
228 assert b.network.branches[
"kb-x"].downstream ==
"X.out"
229 assert [w
for w
in b.warnings
if "CV-1" in w
and "port" in w], b.warnings
230 assert _flow(payload) <= 1e-6
234 """No port to read, so nothing to correct: the old reading stands."""
237 line(
"bottle-sv",
"KB",
"SV"),
238 line(
"tank-sv",
"TK",
"SV"),
241 assert b.network.nodes[
"SV.out"].pressure == pytest.approx(AMBIENT)
245 """A QD with no coefficient is a fitting, priced as a bend. Capped, it is a
246 stub, and the solver back-fills a stub at exactly zero flow -- where the
247 bend correlation (Rennels, via fluids) refuses a Reynolds number of zero.
248 The ValueError escaped a solve asked not to raise, on every solve of any
249 drawing with a fill QD on it."""
253 payload[
"nodes"].append(node(
"QD",
"QD",
"QD-FILL"))
254 payload[
"edges"].append(
line(
"tank-qd",
"TK",
"QD", from_port=
"b", to_port=
"l"))
256 result = solve_steady(
257 b.network, signals={
"SV-PRESS.command": 1.0}, raise_on_failure=
False
259 assert result.converged
260 assert result.flows[
"tank-qd"] == 0.0
267 """A solenoid with ``Cd = 20`` reached the API as an unhandled SpecError, a
268 bare 500, and the message said "valve" rather than which one."""
273 params={
"Cd":
P(20,
"-",
"estimated"),
"bore":
P(6.35,
"mm")},
276 line(
"bottle-sv",
"KB",
"SV"),
line(
"sv-tank",
"SV",
"TK"), sol=bad
278 with pytest.raises(AssemblyError, match=
"FM_SOL_G")
as caught:
279 assemble_model(read_diagram(payload, name=
"press leg"), diagram_id=
"t")
280 assert "Cd" in str(caught.value)
284 """pid-designer offers ``copv`` as a tank *wall material*; the vessel that
285 supplies the press lines is its Pressurant bottle (KBOTTLE). A COPV drawn as
286 a TANK is read as a propellant tank, and nothing presses anything."""
288 payload[
"nodes"].append(
294 options={
"material":
"copv"},
295 params={
"pressure":
P(4000,
"psi"),
"temperature":
P(293,
"K")},
299 told = [w
for w
in b.warnings
if w.startswith(
"TK-1")]
300 assert any(
"KBOTTLE" in w
and "Pressurant bottle" in w
for w
in told), told
302 assert not [w
for w
in b.warnings
if w.startswith(
"TK-FUEL")
and "KBOTTLE" in w]
307 params = {
"pressure":
P(4000,
"psi")}
309 params[
"temperature"] =
P(293,
"K")
310 payload[
"nodes"].append(
311 node(
"COPV",
"TANK",
"TK-1", fluid=
"nitrogen", params=params)
317 from pathlib
import Path
323 Path(__file__).resolve().parents[3] /
"feed-twin" /
"backend" /
"statemachines"
325 if not tables.is_dir():
326 pytest.skip(
"state machine tables absent")
327 model = assemble_model(read_diagram(payload, name=
"press leg"), diagram_id=
"t")
328 machine = load_machine(tables=tables)
329 return model, Session(model, machine, bind(machine, {}))
333 """Nitrogen stated at 293 K is above its 126 K critical point: it can only be
334 gas, so the TANK is the pressurant bottle. LE4 drew its COPV this way, and
335 read as a propellant tank it could not even open (a propellant tank starts
336 from a saturated liquid that does not exist there)."""
338 assert "COPV" in session.bottles
and "COPV" not in session.tanks
339 assert [w
for w
in model.report.warnings
if w.startswith(
"TK-1")
and "KBOTTLE" in w]
343 """LE4's COPV gives no volume. It was given a 44 L K-bottle, so a burn took
344 250 psi out of a bottle that really loses thousands: 45 scf of air at 4500
345 psi is a 4.64 L cylinder (4.6871 L with its fittings), the one the stand and
348 assert session.bottles[
"COPV"].volume.volume == pytest.approx(4.6871e-3)
349 assert any(
"4.69 L assumed" in a
for a
in session.assumptions)
353 """With no temperature the drawing has not ruled a liquid out, so nothing is
354 re-read -- and the stand refuses to open with the tank named and the reason
355 given, rather than CoolProp's "rhoV is invalid" as a bare 500."""
356 with pytest.raises(AssemblyError, match=
"TK-1")
as caught:
358 assert "critical temperature (126 K)" in str(caught.value)
359 assert "KBOTTLE" in str(caught.value)
365 """LE4's regulator dome line: a solenoid, then a quick-disconnect left open
366 to nothing. With the solenoid shut that run is an island cut off from every
367 fixed pressure. Peeled from both ends, the stubs met in the middle and left
368 one node with no equation and no live end -- "the Jacobian could not be
369 factorised" on every solve with the solenoid shut, which in a DAQ sequence
370 that never commands it is every solve."""
374 payload[
"nodes"] += [
376 "DS",
"SOL",
"DOME-SOL", params={
"Cv":
P(1.0,
"Cv"),
"bore":
P(4.0,
"mm")}
378 node(
"QDX",
"QD",
"QD-DOME"),
379 node(
"QDF",
"QD",
"QD-FILL"),
381 payload[
"edges"] += [
382 line(
"tank-fill",
"TK",
"QDF", from_port=
"b", to_port=
"l"),
383 line(
"bottle-ds",
"KB",
"DS", to_port=
"l"),
384 line(
"ds-qd",
"DS",
"QDX", from_port=
"r", to_port=
"l"),
387 for command
in (0.0, 1.0):
388 result = solve_steady(
390 signals={
"SV-PRESS.command": 1.0,
"DOME-SOL.command": command},
391 raise_on_failure=
False,
393 assert result.converged, f
"DOME-SOL at {command}"
400 """A relief valve is shut until its set pressure; with none it never lifts.
401 It used to be built as an always-open Cv valve -- a hole to atmosphere on
402 the tank it guards, so the stand could never hold press."""
404 payload[
"nodes"] += [node(
"RV",
"RV",
"RV-F"), node(
"QDF",
"QD",
"QD-FILL")]
405 payload[
"edges"] += [
406 line(
"tank-rv",
"TK",
"RV", from_port=
"t", to_port=
"r"),
407 line(
"tank-fill",
"TK",
"QDF", from_port=
"b", to_port=
"l"),
410 assert "RV" not in b.network.branches
412 n
for n, v
in b.network.nodes.items()
if n.startswith(
"RV.")
and v.pressure
414 assert [w
for w
in b.warnings
if w.startswith(
"RV-F")
and "read as shut" in w]
418 """pid-designer's engine dialog saves "Chamber pressure" as
419 `chamber_pressure`; hand-written drawings say `pressure`. Both are read."""
421 payload[
"nodes"].append(
422 node(
"ENG",
"ENGINE",
"ENG-1", params={
"chamber_pressure":
P(385,
"psi")})
424 payload[
"edges"].append(
425 line(
"tank-eng",
"TK",
"ENG", from_port=
"b", to_port=
"fuel")
428 assert b.network.nodes[
"ENG"].pressure == pytest.approx(385 * 6894.757293168361)
429 assert not [w
for w
in b.warnings
if w.startswith(
"ENG-1 has no pressure")]
433 """Bottle, regulator-less press manifold, two tanks, two press solenoids,
434 two mains to an engine -- tagged the way LE4 tags them."""
442 params={
"pressure":
P(4000,
"psi"),
"temperature":
P(293,
"K")},
444 node(
"MF",
"MANIFOLD",
"MF-1"),
451 "pressure":
P(580,
"psi"),
452 "temperature":
P(293,
"K"),
453 "volume":
P(8.19,
"L"),
462 "pressure":
P(585,
"psi"),
463 "temperature":
P(90.19,
"K"),
464 "volume":
P(8.19,
"L"),
471 params={
"Cv":
P(4,
"Cv"),
"bore":
P(6.35,
"mm")},
477 params={
"Cv":
P(4,
"Cv"),
"bore":
P(6.35,
"mm")},
483 params={
"Cv":
P(20,
"Cv"),
"bore":
P(12.7,
"mm")},
486 "OM",
"ROT",
"OM-R", params={
"Cv":
P(20,
"Cv"),
"bore":
P(12.7,
"mm")}
488 node(
"ENG",
"ENGINE",
"ENG-1", params={
"chamber_pressure":
P(385,
"psi")}),
491 line(
"kb-mf",
"KB",
"MF"),
492 line(
"mf-fp",
"MF",
"FP", to_port=
"l"),
493 line(
"ft-fp",
"FT",
"FP", from_port=
"t", to_port=
"r"),
494 line(
"mf-op",
"MF",
"OP", to_port=
"l"),
495 line(
"ot-op",
"OT",
"OP", from_port=
"t", to_port=
"r"),
496 line(
"ft-fm",
"FT",
"FM", from_port=
"b", to_port=
"l"),
497 line(
"fm-eng",
"FM",
"ENG", from_port=
"r", to_port=
"fuel"),
498 line(
"ot-om",
"OT",
"OM", from_port=
"b", to_port=
"l"),
499 line(
"om-eng",
"OM",
"ENG", from_port=
"r", to_port=
"t"),
506 from pathlib
import Path
511 Path(__file__).resolve().parents[3] /
"feed-twin" /
"backend" /
"statemachines"
513 if not tables.is_dir():
514 pytest.skip(
"state machine tables absent")
515 return load_machine(tables=tables)
520 assert b.valve_roles == {
521 "FP": frozenset({
"fuel",
"press"}),
522 "OP": frozenset({
"lox",
"press"}),
523 "FM": frozenset({
"fuel",
"main"}),
524 "OM": frozenset({
"lox",
"main"}),
529 """A motor where the rotary has a pneumatic actuator changes nothing about
530 what the valve does on the stand."""
532 for n
in payload[
"nodes"]:
533 if n[
"id"]
in (
"FM",
"OM"):
534 n[
"data"][
"componentType"] =
"MOV"
536 assert b.valve_roles[
"FM"] == frozenset({
"fuel",
"main"})
537 assert b.valve_roles[
"OM"] == frozenset({
"lox",
"main"})
541 """ "FU_SOL_R" shares no word with "Fuel Press", so LE4 bound nothing and
542 Fire opened nothing. Bound by role, every press and main is commanded."""
556 assert bind(machine, labels).to_symbol == {}
557 binding = bind(machine, labels, roles=b.valve_roles)
559 a: binding.to_symbol.get(a)
560 for a
in (
"Fuel Press",
"LOX Press",
"Fuel Main",
"LOX Main")
561 } == {
"Fuel Press":
"FP",
"LOX Press":
"OP",
"Fuel Main":
"FM",
"LOX Main":
"OM"}
562 assert set(binding.by_role) == {
"Fuel Press",
"LOX Press",
"Fuel Main",
"LOX Main"}
563 assert binding.uncommanded == ()
570 labels = {
"FP":
"FU_SOL_R",
"OP":
"OU_SOL_R",
"FM":
"MV-FU",
"OM":
"OM-R"}
571 binding = bind(
_machine(), labels, roles=b.valve_roles)
572 assert binding.to_symbol[
"Fuel Main"] ==
"FM"
573 assert "Fuel Main" not in binding.by_role
577 """Two fuel press valves in parallel: which one "Fuel Press" means is not
578 settled by the plumbing, so neither is guessed."""
581 labels = {
"FP":
"FU_SOL_R",
"FP2":
"FU_SOL_R2"}
582 roles = {
"FP": frozenset({
"fuel",
"press"}),
"FP2": frozenset({
"fuel",
"press"})}
583 binding = bind(
_machine(), labels, roles=roles)
584 assert "Fuel Press" not in binding.to_symbol
588 """LE4's regulator is dome-loaded, with its dome filled through a QD rather
589 than by a drawn control regulator. Nothing exposed the dome, so the
590 cockpit's dome knob did not reach it and Layer X could not solve the dome
591 for a lockup ("Could not solve the dome setting"). It is a signal now --
592 and the press valves are still found through the regulator."""
594 payload[
"nodes"].append(
599 options={
"domeLoaded":
"yes"},
601 "dome_pressure":
P(535,
"psi"),
602 "dome_bias":
P(50,
"psi"),
607 payload[
"edges"] = [e
for e
in payload[
"edges"]
if e[
"id"] !=
"kb-mf"]
608 payload[
"edges"] += [
609 line(
"kb-reg",
"KB",
"REG", to_port=
"l"),
610 line(
"reg-mf",
"REG",
"MF", from_port=
"r"),
613 assert b.actuators[
"REG"] ==
"DPR_HP.dome"
614 assert b.valve_roles[
"FP"] == frozenset({
"fuel",
"press"})
615 assert b.valve_roles[
"OP"] == frozenset({
"lox",
"press"})
616 assert "REG" not in b.valve_roles
623 """LE4's LOX tank top: a manifold on the tank's ``t2`` port carrying a
624 transducer and a quick-disconnect with nothing beyond it -- the GSE vent
625 couples there, and the vent valve is on the cart."""
628 for n
in payload[
"nodes"]:
630 n[
"data"][
"rotation"] = rotation
631 payload[
"nodes"] += [
632 node(
"VM",
"MANIFOLD",
"MF-3"),
633 node(
"PT",
"PT",
"QD_PT_R"),
634 node(
"QD",
"QD",
"QD_OVA"),
636 payload[
"edges"] += [
637 line(
"ot-vm",
"OT",
"VM", from_port=
"t2"),
638 line(
"vm-pt",
"VM",
"PT", to_port=
"b"),
639 line(
"vm-qd",
"VM",
"QD", to_port=
"r"),
642 payload[
"nodes"].append(
647 params={
"Cv":
P(1.7,
"Cv"),
"bore":
P(6.35,
"mm")},
650 payload[
"edges"].append(
line(
"vm-ov",
"VM",
"OV", to_port=
"l"))
655 branch = b.network.branches[edge_id]
656 ports = b.tanks[tank]
659 for e
in (branch.upstream, branch.downstream)
660 if e
in (ports.ullage, ports.outlet)
665 """It reaches no supply and no atmosphere, so topology alone hung it on the
666 *liquid outlet*: LE4's LOX vent manifold read as full of LOX, and its
667 transducer read the outlet. Drawn on the top port, it is on the ullage."""
669 assert _tank_end(b,
"ot-vm",
"OT") == b.tanks[
"OT"].ullage
670 assert b.network.nodes[
"VM"].phase ==
"gas"
675 assert _tank_end(b,
"ot-vm",
"OT") == b.tanks[
"OT"].outlet
679 """No vent valve on the drawing: the pad's Ox Fill boiled TK-3 up to the
680 717 psig critical pin and the sequence never left it. The disconnect is
681 where the cart's vent couples, so the table's LOX Vent opens it."""
685 assert b.actuators[
"QD"] ==
"QD_OVA.command"
686 assert b.valve_roles[
"QD"] == frozenset({
"lox",
"vent"})
687 branch = b.network.branches[
"QD"]
688 ends = {branch.upstream, branch.downstream}
689 assert any(b.network.nodes[e].pressure == AMBIENT
for e
in ends)
690 assert any(
"QD_OVA" in w
and "GSE vent" in w
for w
in b.warnings)
698 binding = bind(
_machine(), labels, roles=b.valve_roles)
699 assert binding.to_symbol[
"LOX Vent"] ==
"QD"
704 assert b.valve_roles[
"OV"] == frozenset({
"lox",
"vent"})
705 assert "QD" not in b.actuators
706 assert type(b.network.branches[
"QD"].component).__name__ ==
"Fitting"
707 assert not any(
"GSE vent" in w
for w
in b.warnings)
711 """The cockpit's own pad: Ox Fill with the LOX Vent bound to the disconnect
712 holds the ullage near ambient instead of boiling it toward critical."""
718 from pathlib
import Path
721 Path(__file__).resolve().parents[3] /
"feed-twin" /
"backend" /
"statemachines"
724 model = assemble_model(read_diagram(payload, name=
"le4 top"), diagram_id=
"t")
725 machine = load_machine(tables=tables)
727 n.id: n.label
for n
in model.diagram.nodes
if n.id
in model.built.actuators
730 model, machine, bind(machine, labels, roles=model.built.valve_roles)
732 for state
in (
"Armed",
"Ox Fill"):
733 session.command_state(state)
737 sample = session.step(0.5)
738 ullage = model.built.tanks[
"OT"].ullage
739 assert psig(sample.pressures[ullage]) < 50.0
746 for e
in payload[
"edges"]:
747 if e[
"id"]
in edge_ids:
748 e[
"data"][
"params"] = {}
753 """A line drawn with no length and no bore is two parts screwed together --
754 a tank's port into its valve. It used to be solved as the fallback tube, a
755 metre of 9.5 mm, which put 94 psi on a LOX feed that has none."""
757 assert "ot-om" not in b.network.branches
758 assert b.network.branches[
"OM"].upstream == b.tanks[
"OT"].outlet
759 assert not [w
for w
in b.warnings
if "fallback tube" in w]
765 design = EngineDesign(
767 injector_type=
"impinging",
768 oxidiser=InjectorSide(
"oxygen", 99.245e-6, 2.2e-3, DischargeModel(), 26),
769 fuel=InjectorSide(
"ethanol", 75.125e-6, 1.92e-3, DischargeModel(), 26),
771 design_mixture_ratio=1.65,
774 b = build_network(read_diagram(payload, name=
"direct"), engine=design)
775 leg = b.network.branches[b.engine_ports[
"oxidiser"]]
776 assert leg.upstream == b.network.branches[
"OM"].downstream
779 assert b.network.nodes[leg.upstream].pressure
is None
780 assert not [w
for w
in b.warnings
if "OM-R" in w
and "venting" in w]
781 assert leg.downstream == b.engine_ports[
"chamber"]
782 assert "om-eng" not in b.network.branches
783 assert b.valve_roles[
"OM"] == frozenset({
"lox",
"main"})
788 edge = next(e
for e
in payload[
"edges"]
if e[
"id"] ==
"ot-om")
789 edge[
"data"][
"params"] = {
"length":
P(0.5,
"m")}
791 assert "ot-om" in b.network.branches
792 found = [w
for w
in b.warnings
if "fallback" in w
and "TK-3 -> OM-R" in w]
793 assert found, b.warnings
797 """A bottle drawn straight into a tank: two pressures cannot be one place."""
799 payload[
"edges"].append(
line(
"kb-ft",
"KB",
"FT", to_port=
"t2"))
800 payload[
"edges"][-1][
"data"][
"params"] = {}
802 assert "kb-ft" in b.network.branches
803 assert any(
"TK-1 -> TK-2" in w
for w
in b.warnings), b.warnings
807 """A stated 0 mm passed the reader where a missing bore was reported, and the
808 run then contributed no loss at all (LE4's fuel tank-to-main run)."""
810 edge = next(e
for e
in payload[
"edges"]
if e[
"id"] ==
"ft-fm")
811 edge[
"data"][
"segments"] = [
814 "method":
"itemised",
815 "bore":
P(0,
"mm",
"estimated"),
816 "length":
P(429.5,
"mm"),
820 assert any(
"seg_1" in w
and "no bore" in w
for w
in b.warnings), b.warnings
824 """A line-level 0 mm with no segments built without a word, as a branch no
825 flow can pass, and a steady solve through it stalls on its first step. It
826 is said; the line itself is left as drawn, because Layer X reads that 0 as
827 "not a line" (its preflight names it) and a fallback here would contradict
830 edge = next(e
for e
in payload[
"edges"]
if e[
"id"] ==
"ot-om")
831 edge[
"data"][
"params"] = {
"length":
P(0.5,
"m"),
"bore":
P(0,
"mm",
"estimated")}
833 line = b.network.branches[
"ot-om"].component
834 assert line.p[
"bore"] == 0.0,
"as drawn: said, not replaced"
836 "ot-om (TK-3 -> OM-R)" in w
and "bore of 0 mm" in w
and "no flow" in w
840 edge[
"data"][
"params"][
"bore"] =
P(9.5,
"mm")
841 sized =
built(payload)
842 assert not any(
"ot-om" in w
and "no flow can pass" in w
for w
in sized.warnings)
846 """A run split before its diameter was given, one stretch bored and one
847 not. The solver sizes the bare stretch at the run's narrowest stated bore
848 (`SegmentedPipe`), so it is priced -- and the reader used to say it
849 "contributes nothing", which the run report then repeated."""
855 def run(first_bore: float |
None) -> Any:
856 first: dict[str, Any] = {
"id":
"s1",
"length":
P(300,
"mm")}
857 if first_bore
is not None:
858 first[
"bore"] =
P(first_bore,
"mm")
859 return read_segments(
860 [first, {
"id":
"s2",
"bore":
P(7.7,
"mm"),
"length":
P(400,
"mm")}],
864 def dp(loss: Any) -> float:
865 line = build_component(
866 ComponentInstance.build(
867 "L1",
"pipe", {}, model=
"segmented", segments=loss.segments
870 flow = FlowConditions(rho=789.0, mu=1.2e-3, p_upstream=4e6, temperature=293.15)
871 return float(line.pressure_drop(0.2, flow))
873 for bare
in (
None, 0.0):
875 assert dp(loss) == pytest.approx(dp(
run(7.7))),
"priced at 7.7 mm"
876 (said,) = [w
for w
in loss.warnings
if "s1" in w]
877 assert "narrowest stated bore, 7.70 mm" in said, said
878 assert "contributes nothing" not in said, said
881 alone = read_segments([{
"id":
"s1",
"length":
P(300,
"mm")}],
"L1")
882 assert dp(alone) == 0.0
883 assert any(
"contributes nothing" in w
for w
in alone.warnings), alone.warnings
887 """A K multiplies a dynamic head, which needs a bore. With none on the run
888 the segment priced nothing and the reader said nothing."""
891 k = {
"id":
"k1",
"method":
"measured_K",
"K":
P(2.0,
"-")}
892 alone = read_segments([k],
"L1")
894 "k1" in w
and "contributes nothing" in w
for w
in alone.warnings
896 borrowed = read_segments(
897 [k, {
"id":
"s2",
"bore":
P(6.0,
"mm"),
"length":
P(0.4,
"m")}],
"L1"
900 "k1" in w
and "narrowest stated bore, 6.00 mm" in w
for w
in borrowed.warnings
902 own = read_segments([{**k,
"bore":
P(6.0,
"mm")}],
"L1")
903 assert not any(
"k1" in w
and "bore" in w
for w
in own.warnings), own.warnings
907 """Press valves joined straight onto their tanks: the two ullages trade gas
908 through the manifold, and what one sent and the other refused was handed
909 to the bottle, whose draw cannot go negative -- so it vanished. LE4 lost
910 7 g/s this way and emptied its COPV before ignition."""
916 from pathlib
import Path
919 Path(__file__).resolve().parents[3] /
"feed-twin" /
"backend" /
"statemachines"
922 payload[
"nodes"].append(
927 options={
"domeLoaded":
"yes"},
929 "dome_pressure":
P(500,
"psi"),
930 "dome_bias":
P(50,
"psi"),
935 payload[
"edges"] = [e
for e
in payload[
"edges"]
if e[
"id"] !=
"kb-mf"]
936 payload[
"edges"] += [
937 line(
"kb-reg",
"KB",
"REG", to_port=
"l"),
938 line(
"reg-mf",
"REG",
"MF", from_port=
"r"),
940 model = assemble_model(read_diagram(payload, name=
"direct press"), diagram_id=
"t")
941 machine = load_machine(tables=tables)
943 n.id: n.label
for n
in model.diagram.nodes
if n.id
in model.built.actuators
946 model, machine, bind(machine, labels, roles=model.built.valve_roles)
948 session.prime(fill_fraction=0.95, tank_psi=563.0, copv_psi=4000.0, state=
"Ready")
949 for valve
in press_valves(session):
950 session.set_valve(valve,
True)
952 def pressurant() -> float:
953 return sum(b.state.mass
for b
in session.bottles.values()) + sum(
954 t.state.ullage.mass
for t
in session.tanks.values()
957 before = pressurant()
960 assert pressurant() == pytest.approx(before, rel=1e-9)
None test_a_directed_valve_dragged_against_the_flow_reads_its_ports(str ctype)
None test_two_tanks_on_one_direct_press_manifold_conserve_pressurant()
str _tank_end(b, str edge_id, str tank)
None test_a_K_with_no_bore_to_refer_it_to_is_said()
None test_a_directed_valve_drawn_in_flow_order_is_read_as_before(str ctype)
None test_a_fill_disconnect_left_capped_does_not_crash_the_solve()
None test_a_drawn_vent_valve_leaves_the_disconnect_capped()
None test_a_motorized_main_valve_has_the_same_job_as_a_rotary_one()
None test_a_line_bore_of_zero_is_said()
None test_ox_fill_vents_through_the_gse_disconnect()
dict[str, object] P(float value, str unit, str source="measured")
Any _le4_with_top_manifold(*, bool vent_valve=False, int rotation=0)
None test_a_turned_tanks_ports_say_nothing_about_up()
None test_without_ports_a_one_sided_valve_is_still_a_vent()
None test_a_role_two_valves_share_binds_neither()
None test_a_tank_that_only_forgot_its_temperature_is_not_reread_and_says_why()
None test_a_check_valve_drawn_backwards_checks_the_flow()
Any _le4_like(str fuel_main_label="FM-R")
None test_a_part_the_library_refuses_is_named_and_fails_the_assembly()
None test_a_bottle_with_no_volume_is_the_stands_copv_not_a_k_bottle()
None test_a_copv_drawn_as_a_tank_is_read_as_the_pressurant_supply()
None test_two_fixed_pressures_joined_directly_stay_a_line()
None test_a_line_with_no_size_is_a_direct_connection()
None test_valves_the_names_do_not_match_are_bound_by_what_they_do()
Any press_leg(*dict[str, object] lines, dict[str, Any]|None sol=None)
None test_a_direct_line_into_the_engine_starts_the_injector_leg_at_the_valve()
( None) test_a_capped_fitting_behind_a_shut_valve_does_not_make_the_solve_singular()
None test_reading_the_ports_says_so()
None test_a_valve_with_lines_on_both_sides_is_read_exactly_as_before()
None test_a_capped_disconnect_on_an_unvented_tank_top_is_its_gse_vent()
None test_a_manifold_on_a_tanks_top_port_is_on_its_ullage()
dict[str, object] node(str nid, str ctype, str label, **object data)
dict[str, object] line(str eid, str a, str b, str from_port="", str to_port="")
None test_a_stretch_with_no_bore_on_a_bored_run_is_sized_at_the_run_and_said()
None test_a_line_with_half_a_size_takes_the_fallback_for_the_rest_and_says_so()
None test_a_press_line_drawn_from_the_tank_lands_on_the_valves_other_side()
None test_a_dome_nothing_loads_is_the_operators_setting()
None test_a_name_that_matches_still_wins_over_a_role()
None test_a_tank_full_of_gas_is_told_to_be_a_pressurant_bottle()
None test_the_engine_symbols_chamber_pressure_is_its_boundary()
None test_each_commandable_valves_job_is_read_off_the_plumbing()
None test_the_same_holds_when_both_lines_are_drawn_out_of_the_valve()
Any _through(str ctype, *dict[str, object] lines)
Any _sizeless(Any payload, *str edge_ids)
None test_a_segment_with_a_zero_bore_is_reported()
Any _stand_with_copv_tank(bool temperature)
None test_a_relief_valve_with_no_set_pressure_is_shut_not_a_hole()