processpi 0.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- processpi/__init__.py +1 -0
- processpi/calculations/__init__.py +3 -0
- processpi/calculations/base.py +55 -0
- processpi/calculations/engine.py +69 -0
- processpi/calculations/fluids/__init__.py +13 -0
- processpi/calculations/fluids/cool_brook_white.py +49 -0
- processpi/calculations/fluids/flow_type.py +37 -0
- processpi/calculations/fluids/optimium_pipe_dia.py +45 -0
- processpi/calculations/fluids/pressure_drop_darcy.py +25 -0
- processpi/calculations/fluids/pressure_drop_fanning.py +25 -0
- processpi/calculations/fluids/pump_power.py +34 -0
- processpi/calculations/fluids/reynolds_number.py +24 -0
- processpi/calculations/fluids/velocity.py +36 -0
- processpi/calculations/heat_transfer/__init__.py +4 -0
- processpi/calculations/heat_transfer/conduction_heat_loss.py +26 -0
- processpi/calculations/heat_transfer/convection_heat_loss.py +23 -0
- processpi/calculations/heat_transfer/heat_exchanger_area.py +26 -0
- processpi/calculations/mass_transfer/__init__.py +3 -0
- processpi/calculations/mass_transfer/distillation_stage_count.py +346 -0
- processpi/calculations/mass_transfer/drying_rate.py +132 -0
- processpi/calculations/reaction_engineering/__init__.py +4 -0
- processpi/calculations/reaction_engineering/catalyst_activity.py +109 -0
- processpi/calculations/reaction_engineering/reaction_rate.py +143 -0
- processpi/calculations/reaction_engineering/residence_time.py +80 -0
- processpi/calculations/thermodynamics/__init__.py +4 -0
- processpi/calculations/thermodynamics/enthalpy_change.py +23 -0
- processpi/calculations/thermodynamics/entropy_change.py +25 -0
- processpi/calculations/thermodynamics/heat_of_vaporization.py +20 -0
- processpi/components/__init__.py +5 -0
- processpi/components/aceticacid.py +19 -0
- processpi/components/acetone.py +20 -0
- processpi/components/acrylicacid.py +19 -0
- processpi/components/air.py +20 -0
- processpi/components/ammonia.py +19 -0
- processpi/components/base.py +85 -0
- processpi/components/benzene.py +40 -0
- processpi/components/benzoicacid.py +19 -0
- processpi/components/bromine.py +19 -0
- processpi/components/butane.py +19 -0
- processpi/components/carbondioxide.py +19 -0
- processpi/components/carbonmonoxide.py +17 -0
- processpi/components/carbontetrachloride.py +17 -0
- processpi/components/chlorine.py +17 -0
- processpi/components/chlorobenzene.py +17 -0
- processpi/components/chloroform.py +17 -0
- processpi/components/chloromethane.py +17 -0
- processpi/components/cyanogen.py +17 -0
- processpi/components/cyclohexene.py +17 -0
- processpi/components/ethane.py +17 -0
- processpi/components/ethanol.py +20 -0
- processpi/components/ethylacetate.py +17 -0
- processpi/components/methanol.py +20 -0
- processpi/components/toluene.py +22 -0
- processpi/components/water.py +24 -0
- processpi/pipelines/__init__.py +0 -0
- processpi/pipelines/base.py +76 -0
- processpi/pipelines/design_rules.py +91 -0
- processpi/pipelines/engine.py +718 -0
- processpi/pipelines/equipment.py +32 -0
- processpi/pipelines/fittings.py +76 -0
- processpi/pipelines/insulation.py +0 -0
- processpi/pipelines/materials.py +102 -0
- processpi/pipelines/network.py +277 -0
- processpi/pipelines/pipelineresults.py +95 -0
- processpi/pipelines/pipes.py +84 -0
- processpi/pipelines/pumps.py +83 -0
- processpi/pipelines/selection.py +89 -0
- processpi/pipelines/standards.py +279 -0
- processpi/pipelines/vessel.py +34 -0
- processpi/units/__init__.py +38 -0
- processpi/units/area.py +51 -0
- processpi/units/base.py +57 -0
- processpi/units/density.py +47 -0
- processpi/units/diameter.py +58 -0
- processpi/units/dimensionless.py +42 -0
- processpi/units/heat_flux.py +46 -0
- processpi/units/heat_of_vaporization.py +47 -0
- processpi/units/heat_transfer_coefficient.py +45 -0
- processpi/units/length.py +47 -0
- processpi/units/mass.py +50 -0
- processpi/units/mass_flowrate.py +50 -0
- processpi/units/pressure.py +57 -0
- processpi/units/specific_heat.py +47 -0
- processpi/units/strings.py +31 -0
- processpi/units/temperature.py +51 -0
- processpi/units/thermal_conductivity.py +45 -0
- processpi/units/thermal_resistance.py +46 -0
- processpi/units/velocity.py +45 -0
- processpi/units/viscosity.py +78 -0
- processpi/units/volume.py +50 -0
- processpi/units/volumetric_flowrate.py +52 -0
- processpi-0.0.0.dist-info/METADATA +5 -0
- processpi-0.0.0.dist-info/RECORD +96 -0
- processpi-0.0.0.dist-info/WHEEL +5 -0
- processpi-0.0.0.dist-info/licenses/LICENSE +21 -0
- processpi-0.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,718 @@
|
|
|
1
|
+
# processpi/pipelines/engine.py
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
from typing import Any, Dict, List, Optional, Tuple, Union
|
|
5
|
+
|
|
6
|
+
# Package-local imports expected in your project. Adjust if your paths differ.
|
|
7
|
+
from ..units import (
|
|
8
|
+
Diameter, Length, Pressure, Density, Viscosity, VolumetricFlowRate, Velocity
|
|
9
|
+
)
|
|
10
|
+
from .pipelineresults import PipelineResults # wrapper/DTO (assumed to exist in your project)
|
|
11
|
+
from ..components import Component # fluids with .density() / .viscosity() / optional .service_type
|
|
12
|
+
from ..calculations.fluids.optimium_pipe_dia import OptimumPipeDiameter
|
|
13
|
+
from ..pipelines.standards import get_recommended_velocity
|
|
14
|
+
from ..calculations.fluids import (
|
|
15
|
+
ReynoldsNumber,
|
|
16
|
+
ColebrookWhite,
|
|
17
|
+
PressureDropDarcy,
|
|
18
|
+
FluidVelocity,
|
|
19
|
+
)
|
|
20
|
+
from .pipes import Pipe
|
|
21
|
+
from .fittings import Fitting
|
|
22
|
+
from .network import PipelineNetwork
|
|
23
|
+
|
|
24
|
+
Number = Union[int, float]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class PipelineEngine:
|
|
28
|
+
"""
|
|
29
|
+
Sklearn-like interface:
|
|
30
|
+
|
|
31
|
+
engine = PipelineEngine()
|
|
32
|
+
engine.fit(
|
|
33
|
+
flowrate=VolumetricFlowRate(...), # or mass_flowrate + density
|
|
34
|
+
mass_flowrate=..., density=..., viscosity=...,
|
|
35
|
+
velocity=Velocity(...), # optional (if missing, computed)
|
|
36
|
+
pipe=Pipe(...), # optional
|
|
37
|
+
diameter=Diameter(...) or number(mm), # optional
|
|
38
|
+
length=Length(...), # optional if pipe provided
|
|
39
|
+
fluid=Component(...), # optional if density & viscosity given
|
|
40
|
+
network=PipelineNetwork(...), # optional (else single-pipe mode)
|
|
41
|
+
flow_split={"Parallel-AB": [0.3, 0.7]}, # optional branch allocation
|
|
42
|
+
fittings=[Fitting(...), ...], # optional minor losses in single-pipe mode
|
|
43
|
+
inlet_pressure=Pressure(...), # optional
|
|
44
|
+
outlet_pressure=Pressure(...), # optional
|
|
45
|
+
available_dp=Pressure(...), # optional: if given & diameter missing, engine can size pipe
|
|
46
|
+
target_outlet_pressure=Pressure(...), # optional: compute residual dp for control valve
|
|
47
|
+
).run()
|
|
48
|
+
|
|
49
|
+
- If diameter is missing (single-pipe), engine uses OptimumPipeDiameter based on flowrate & density.
|
|
50
|
+
- If flowrate missing, engine infers it from (mass_flowrate, density) or (velocity, diameter).
|
|
51
|
+
- Supports series/parallel PipelineNetwork with configurable flow splits.
|
|
52
|
+
- Computes residual DP (e.g., for CV) if inlet/outlet/available_dp are provided.
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
# -------------------- INIT / FIT --------------------
|
|
56
|
+
def __init__(self, **kwargs: Any) -> None:
|
|
57
|
+
self.data: Dict[str, Any] = {}
|
|
58
|
+
if kwargs:
|
|
59
|
+
self.fit(**kwargs)
|
|
60
|
+
|
|
61
|
+
def fit(self, **kwargs: Any) -> "PipelineEngine":
|
|
62
|
+
"""
|
|
63
|
+
Accepts optional inputs; normalizes common aliases; stores raw config for run().
|
|
64
|
+
Returns self (sklearn-style).
|
|
65
|
+
"""
|
|
66
|
+
# Shallow copy to preserve caller values
|
|
67
|
+
self.data = dict(kwargs)
|
|
68
|
+
|
|
69
|
+
# Normalize aliases without discarding originals
|
|
70
|
+
alias_map = {
|
|
71
|
+
"flowrate": ["flow_rate", "q", "Q"],
|
|
72
|
+
"mass_flowrate": ["mass_flow", "m_dot", "mdot"],
|
|
73
|
+
"velocity": ["v"],
|
|
74
|
+
"diameter": ["dia", "D", "nominal_diameter", "internal_diameter"],
|
|
75
|
+
"length": ["len", "L"],
|
|
76
|
+
"inlet_pressure": ["in_pressure", "pin", "p_in"],
|
|
77
|
+
"outlet_pressure": ["out_pressure", "pout", "p_out"],
|
|
78
|
+
"available_dp": ["available_pressure_drop", "dp_available"],
|
|
79
|
+
"target_outlet_pressure": ["target_pout"],
|
|
80
|
+
}
|
|
81
|
+
for canon, alts in alias_map.items():
|
|
82
|
+
if canon not in self.data:
|
|
83
|
+
for alt in alts:
|
|
84
|
+
if alt in self.data and self.data[alt] is not None:
|
|
85
|
+
self.data[canon] = self.data[alt]
|
|
86
|
+
break
|
|
87
|
+
|
|
88
|
+
# Basic defaults (non-destructive)
|
|
89
|
+
self.data.setdefault("flow_split", {}) # for parallel branches
|
|
90
|
+
self.data.setdefault("fittings", []) # minor-loss list in single-pipe mode
|
|
91
|
+
self.data.setdefault("assume_mm_for_numbers", True) # numeric diameter assumed mm
|
|
92
|
+
|
|
93
|
+
# Light validation
|
|
94
|
+
network = self.data.get("network")
|
|
95
|
+
if network is not None and not isinstance(network, PipelineNetwork):
|
|
96
|
+
raise TypeError("network must be a PipelineNetwork if provided.")
|
|
97
|
+
|
|
98
|
+
return self
|
|
99
|
+
|
|
100
|
+
# -------------------- Internal getters / inference ------------------------
|
|
101
|
+
|
|
102
|
+
def _get_density(self) -> Density:
|
|
103
|
+
if "density" in self.data and self.data["density"] is not None:
|
|
104
|
+
return self.data["density"]
|
|
105
|
+
if "fluid" in self.data and isinstance(self.data["fluid"], Component):
|
|
106
|
+
return self.data["fluid"].density()
|
|
107
|
+
raise ValueError("Provide density or a fluid Component (with .density()).")
|
|
108
|
+
|
|
109
|
+
def _get_viscosity(self) -> Viscosity:
|
|
110
|
+
if "viscosity" in self.data and self.data["viscosity"] is not None:
|
|
111
|
+
return self.data["viscosity"]
|
|
112
|
+
if "fluid" in self.data and isinstance(self.data["fluid"], Component):
|
|
113
|
+
return self.data["fluid"].viscosity()
|
|
114
|
+
raise ValueError("Provide viscosity or a fluid Component (with .viscosity()).")
|
|
115
|
+
|
|
116
|
+
def _infer_flowrate(self) -> VolumetricFlowRate:
|
|
117
|
+
"""
|
|
118
|
+
Infer volumetric flowrate from:
|
|
119
|
+
1) explicit flowrate
|
|
120
|
+
2) mass_flowrate & density
|
|
121
|
+
3) velocity & diameter
|
|
122
|
+
"""
|
|
123
|
+
# 1) Direct
|
|
124
|
+
q = self.data.get("flowrate")
|
|
125
|
+
if q is not None:
|
|
126
|
+
return q
|
|
127
|
+
|
|
128
|
+
# 2) From mass flow & density
|
|
129
|
+
m = self.data.get("mass_flowrate")
|
|
130
|
+
if m is not None:
|
|
131
|
+
rho = self._get_density()
|
|
132
|
+
# Volumetric flow = mass flow / density
|
|
133
|
+
q = VolumetricFlowRate((m.value if hasattr(m, "value") else float(m)) /
|
|
134
|
+
(rho.value if hasattr(rho, "value") else float(rho)), "m^3/s")
|
|
135
|
+
self.data["flowrate"] = q
|
|
136
|
+
return q
|
|
137
|
+
|
|
138
|
+
# 3) From velocity & diameter
|
|
139
|
+
vel = self.data.get("velocity")
|
|
140
|
+
d = self.data.get("diameter")
|
|
141
|
+
if vel is not None and (d is not None or isinstance(self.data.get("pipe"), Pipe)):
|
|
142
|
+
if d is None:
|
|
143
|
+
d = self._internal_diameter_m(self.data.get("pipe"))
|
|
144
|
+
if not isinstance(d, Diameter):
|
|
145
|
+
if self.data.get("assume_mm_for_numbers", True):
|
|
146
|
+
d = Diameter(float(d), "mm")
|
|
147
|
+
else:
|
|
148
|
+
d = Diameter(float(d), "m")
|
|
149
|
+
q = FluidVelocity(volumetric_flow_rate=None, diameter=d, velocity=vel).invert_to_flowrate()
|
|
150
|
+
if not isinstance(q, VolumetricFlowRate):
|
|
151
|
+
# fallback via area
|
|
152
|
+
from math import pi
|
|
153
|
+
v_val = vel.value if hasattr(vel, "value") else float(vel)
|
|
154
|
+
d_m = d.to("m").value
|
|
155
|
+
q = VolumetricFlowRate(v_val * (pi * d_m * d_m / 4.0), "m^3/s")
|
|
156
|
+
self.data["flowrate"] = q
|
|
157
|
+
return q
|
|
158
|
+
|
|
159
|
+
raise ValueError("Unable to infer flowrate. Provide flowrate, or (mass_flowrate & density), or (velocity & diameter).")
|
|
160
|
+
|
|
161
|
+
def _ensure_pipe(self) -> Pipe:
|
|
162
|
+
"""Return a Pipe or construct one (single-pipe mode) if possible."""
|
|
163
|
+
p = self.data.get("pipe")
|
|
164
|
+
if isinstance(p, Pipe):
|
|
165
|
+
return p
|
|
166
|
+
|
|
167
|
+
# Build from diameter/length if provided
|
|
168
|
+
d = self.data.get("diameter")
|
|
169
|
+
L = self.data.get("length") or Length(1.0, "m")
|
|
170
|
+
if d is not None:
|
|
171
|
+
if not isinstance(d, Diameter):
|
|
172
|
+
if self.data.get("assume_mm_for_numbers", True):
|
|
173
|
+
d = Diameter(float(d), "mm")
|
|
174
|
+
else:
|
|
175
|
+
d = Diameter(float(d), "m")
|
|
176
|
+
p = Pipe(internal_diameter=d, length=L)
|
|
177
|
+
self.data["pipe"] = p
|
|
178
|
+
return p
|
|
179
|
+
|
|
180
|
+
# Else compute optimum diameter (single-pipe only)
|
|
181
|
+
if "network" not in self.data or self.data["network"] is None:
|
|
182
|
+
q = self._infer_flowrate()
|
|
183
|
+
calc = OptimumPipeDiameter(flow_rate=q, density=self._get_density())
|
|
184
|
+
d_opt = calc.calculate()
|
|
185
|
+
p = Pipe(nominal_diameter=d_opt, length=L)
|
|
186
|
+
self.data["pipe"] = p
|
|
187
|
+
return p
|
|
188
|
+
|
|
189
|
+
# In network mode, each element should define its own geometry.
|
|
190
|
+
raise ValueError("No pipe/diameter provided. In network mode, supply pipe elements inside the network.")
|
|
191
|
+
|
|
192
|
+
def _internal_diameter_m(self, pipe: Optional[Pipe]) -> Diameter:
|
|
193
|
+
if pipe:
|
|
194
|
+
if getattr(pipe, "internal_diameter", None) is not None:
|
|
195
|
+
return pipe.internal_diameter
|
|
196
|
+
if getattr(pipe, "nominal_diameter", None) is not None:
|
|
197
|
+
return pipe.nominal_diameter
|
|
198
|
+
d = self.data.get("diameter")
|
|
199
|
+
if d is not None:
|
|
200
|
+
return d if isinstance(d, Diameter) else Diameter(float(d), "mm")
|
|
201
|
+
# As a last resort, compute optimum (single-pipe)
|
|
202
|
+
q = self._infer_flowrate()
|
|
203
|
+
return OptimumPipeDiameter(flow_rate=q, density=self._get_density()).calculate()
|
|
204
|
+
|
|
205
|
+
def _maybe_velocity(self, pipe: Pipe) -> Velocity:
|
|
206
|
+
v = self.data.get("velocity")
|
|
207
|
+
if v is not None:
|
|
208
|
+
self._apply_recommended_velocity(pipe, given_velocity=v)
|
|
209
|
+
return v
|
|
210
|
+
q = self._infer_flowrate()
|
|
211
|
+
v = FluidVelocity(volumetric_flow_rate=q, diameter=self._internal_diameter_m(pipe)).calculate()
|
|
212
|
+
self._apply_recommended_velocity(pipe, flowrate=q, computed_velocity=v)
|
|
213
|
+
return v
|
|
214
|
+
|
|
215
|
+
def _apply_recommended_velocity(
|
|
216
|
+
self,
|
|
217
|
+
pipe: Pipe,
|
|
218
|
+
flowrate: Optional[VolumetricFlowRate] = None,
|
|
219
|
+
computed_velocity: Optional[Velocity] = None,
|
|
220
|
+
given_velocity: Optional[Velocity] = None,
|
|
221
|
+
) -> None:
|
|
222
|
+
"""If fluid.service_type has a recommended velocity, optionally resize pipe to hit midpoint."""
|
|
223
|
+
service = getattr(self.data.get("fluid"), "service_type", None)
|
|
224
|
+
if not service:
|
|
225
|
+
return
|
|
226
|
+
rec = get_recommended_velocity(service)
|
|
227
|
+
if not rec:
|
|
228
|
+
return
|
|
229
|
+
|
|
230
|
+
v_curr = given_velocity or computed_velocity
|
|
231
|
+
if v_curr is None and flowrate is not None:
|
|
232
|
+
v_curr = FluidVelocity(volumetric_flow_rate=flowrate, diameter=self._internal_diameter_m(pipe)).calculate()
|
|
233
|
+
if v_curr is None:
|
|
234
|
+
return
|
|
235
|
+
|
|
236
|
+
if isinstance(rec, tuple):
|
|
237
|
+
v_min, v_max = rec
|
|
238
|
+
v_val = v_curr.value if hasattr(v_curr, "value") else float(v_curr)
|
|
239
|
+
if v_val >= v_min and v_val <= v_max:
|
|
240
|
+
return
|
|
241
|
+
target_v = 0.5 * (v_min + v_max)
|
|
242
|
+
else:
|
|
243
|
+
target_v = rec
|
|
244
|
+
v_val = v_curr.value if hasattr(v_curr, "value") else float(v_curr)
|
|
245
|
+
if abs(v_val - target_v) / target_v <= 0.10:
|
|
246
|
+
return
|
|
247
|
+
|
|
248
|
+
# resize internal diameter to hit target velocity (kept in mm)
|
|
249
|
+
q = flowrate or self._infer_flowrate()
|
|
250
|
+
from math import pi, sqrt
|
|
251
|
+
A_new = q.value / target_v
|
|
252
|
+
D_new = sqrt(4.0 * A_new / pi) # meters
|
|
253
|
+
pipe.internal_diameter = Diameter(D_new * 1000.0, "mm")
|
|
254
|
+
|
|
255
|
+
# --- Unit helpers (inside PipelineEngine) ---
|
|
256
|
+
def _as_pressure(self, maybe_pressure, default_unit="Pa"):
|
|
257
|
+
"""Accept Pressure, float, or None and return a Pressure (or None)."""
|
|
258
|
+
if maybe_pressure is None:
|
|
259
|
+
return None
|
|
260
|
+
if isinstance(maybe_pressure, Pressure):
|
|
261
|
+
return maybe_pressure
|
|
262
|
+
# assume float in default_unit
|
|
263
|
+
return Pressure(float(maybe_pressure), default_unit)
|
|
264
|
+
|
|
265
|
+
def _pump_gain_pa(self, pump) -> Pressure:
|
|
266
|
+
"""
|
|
267
|
+
Convert your Pump (head in m or pressures) to a pressure 'gain' (Pa).
|
|
268
|
+
Priority:
|
|
269
|
+
- If both inlet/outlet pressures exist -> use their difference
|
|
270
|
+
- Else if head exists -> rho*g*H
|
|
271
|
+
- Else -> 0 Pa
|
|
272
|
+
"""
|
|
273
|
+
rho = getattr(pump, "density", None) or self._get_density().value
|
|
274
|
+
g = 9.81
|
|
275
|
+
pin = getattr(pump, "inlet_pressure", None)
|
|
276
|
+
pout = getattr(pump, "outlet_pressure", None)
|
|
277
|
+
if pin is not None and pout is not None:
|
|
278
|
+
return (self._as_pressure(pout).to("Pa") - self._as_pressure(pin).to("Pa"))
|
|
279
|
+
head = getattr(pump, "head", 0.0) or 0.0
|
|
280
|
+
return Pressure(rho * g * float(head), "Pa")
|
|
281
|
+
|
|
282
|
+
def _equipment_dp_pa(self, eq) -> Pressure:
|
|
283
|
+
"""
|
|
284
|
+
Your Equipment stores pressure_drop in 'bar' (float).
|
|
285
|
+
Convert to Pressure in Pa.
|
|
286
|
+
"""
|
|
287
|
+
dp_bar = getattr(eq, "pressure_drop", 0.0) or 0.0
|
|
288
|
+
return Pressure(float(dp_bar), "bar").to("Pa")
|
|
289
|
+
|
|
290
|
+
def _fitting_dp_pa(self, fitting, v: Velocity, f: Optional[float], d: Diameter) -> Pressure:
|
|
291
|
+
"""
|
|
292
|
+
Compute minor loss from your Fitting using total_K() or equivalent_length().
|
|
293
|
+
"""
|
|
294
|
+
rho = self._get_density().value
|
|
295
|
+
v_val = v.value if hasattr(v, "value") else float(v)
|
|
296
|
+
# Try K-method first
|
|
297
|
+
Ktot = None
|
|
298
|
+
try:
|
|
299
|
+
Ktot = fitting.total_K()
|
|
300
|
+
except Exception:
|
|
301
|
+
Ktot = None
|
|
302
|
+
if Ktot is not None:
|
|
303
|
+
return Pressure(0.5 * rho * v_val * v_val * float(Ktot), "Pa")
|
|
304
|
+
|
|
305
|
+
# Fallback: equivalent length method
|
|
306
|
+
Le = None
|
|
307
|
+
try:
|
|
308
|
+
Le = fitting.equivalent_length()
|
|
309
|
+
except Exception:
|
|
310
|
+
Le = None
|
|
311
|
+
if Le is not None:
|
|
312
|
+
# We need friction factor for Le/D method. If not passed, recompute here.
|
|
313
|
+
if f is None:
|
|
314
|
+
# compute f using pipe diameter 'd'
|
|
315
|
+
Re = self._reynolds(v, type("Tmp", (), {"roughness": 0.0, "length": Length(1.0, "m")})()) # roughness ignored here
|
|
316
|
+
f_val = self._friction_factor(Re, type("Tmp", (), {"roughness": 0.0})())
|
|
317
|
+
else:
|
|
318
|
+
f_val = f
|
|
319
|
+
d_m = d.to("m").value
|
|
320
|
+
return Pressure(float(f_val) * (float(Le) / d_m) * 0.5 * rho * v_val * v_val, "Pa")
|
|
321
|
+
|
|
322
|
+
# Neither K nor Le available => no minor loss accounted
|
|
323
|
+
return Pressure(0.0, "Pa")
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
# -------------------- Primitive calcs ------------------------------------
|
|
327
|
+
|
|
328
|
+
def _reynolds(self, v: Velocity, pipe: Pipe):
|
|
329
|
+
return ReynoldsNumber(
|
|
330
|
+
density=self._get_density(),
|
|
331
|
+
velocity=v,
|
|
332
|
+
diameter=self._internal_diameter_m(pipe),
|
|
333
|
+
viscosity=self._get_viscosity(),
|
|
334
|
+
).calculate()
|
|
335
|
+
|
|
336
|
+
def _friction_factor(self, Re: float, pipe: Pipe):
|
|
337
|
+
return ColebrookWhite(
|
|
338
|
+
reynolds_number=Re,
|
|
339
|
+
roughness=float(pipe.roughness),
|
|
340
|
+
diameter=self._internal_diameter_m(pipe),
|
|
341
|
+
).calculate()
|
|
342
|
+
|
|
343
|
+
def _major_loss_dp(self, f: float, v: Velocity, pipe: Pipe):
|
|
344
|
+
return PressureDropDarcy(
|
|
345
|
+
friction_factor=f,
|
|
346
|
+
length=pipe.length,
|
|
347
|
+
diameter=self._internal_diameter_m(pipe),
|
|
348
|
+
density=self._get_density(),
|
|
349
|
+
velocity=v,
|
|
350
|
+
).calculate()
|
|
351
|
+
|
|
352
|
+
def _minor_loss_dp(self, fitting: Fitting, v: Velocity, f: Optional[float] = None, d: Optional[Diameter] = None) -> Pressure:
|
|
353
|
+
"""Compute minor loss (supports K or equivalent length Le)."""
|
|
354
|
+
rho = self._get_density()
|
|
355
|
+
v_val = v.value if hasattr(v, "value") else float(v)
|
|
356
|
+
rho_val = rho.value if hasattr(rho, "value") else float(rho)
|
|
357
|
+
d_val = d.value if hasattr(d, "value") else (d if d is not None else None)
|
|
358
|
+
f_val = f.value if hasattr(f, "value") else (f if f is not None else None)
|
|
359
|
+
|
|
360
|
+
if getattr(fitting, "K", None) is not None:
|
|
361
|
+
return Pressure(float(fitting.K) * 0.5 * rho_val * v_val * v_val, "Pa")
|
|
362
|
+
if getattr(fitting, "Le", None) is not None:
|
|
363
|
+
if f_val is None or d_val is None:
|
|
364
|
+
raise ValueError("Equivalent length method needs friction factor and diameter.")
|
|
365
|
+
return Pressure(f_val * (fitting.Le / d_val) * 0.5 * rho_val * v_val * v_val, "Pa")
|
|
366
|
+
return Pressure(0.0, "Pa")
|
|
367
|
+
|
|
368
|
+
# -------------------- Series/Parallel evaluation -------------------------
|
|
369
|
+
|
|
370
|
+
def _pipe_calculation(self, pipe: Pipe, flow_rate: Optional[VolumetricFlowRate]) -> Dict[str, Any]:
|
|
371
|
+
"""Compute velocity, Re, f, major DP for a single pipe."""
|
|
372
|
+
# get velocity (if engine has a fixed velocity, use it; else compute from provided flow_rate)
|
|
373
|
+
if self.data.get("velocity") is not None:
|
|
374
|
+
v = self.data["velocity"]
|
|
375
|
+
else:
|
|
376
|
+
q = flow_rate or self._infer_flowrate()
|
|
377
|
+
v = FluidVelocity(volumetric_flow_rate=q, diameter=self._internal_diameter_m(pipe)).calculate()
|
|
378
|
+
|
|
379
|
+
Re = self._reynolds(v, pipe)
|
|
380
|
+
f = self._friction_factor(Re, pipe)
|
|
381
|
+
dp_major = self._major_loss_dp(f, v, pipe)
|
|
382
|
+
|
|
383
|
+
# Handle any inlined fittings array in the pipe (if your Pipe stores them) OR engine-level single-pipe fittings
|
|
384
|
+
dp_minor = Pressure(0.0, "Pa")
|
|
385
|
+
d = self._internal_diameter_m(pipe)
|
|
386
|
+
fittings: List[Fitting] = []
|
|
387
|
+
# prefer element fittings; else engine-level list in single-pipe mode
|
|
388
|
+
if hasattr(pipe, "fittings") and isinstance(pipe.fittings, list):
|
|
389
|
+
fittings = pipe.fittings
|
|
390
|
+
elif "network" not in self.data and isinstance(self.data.get("fittings"), list):
|
|
391
|
+
fittings = self.data["fittings"]
|
|
392
|
+
|
|
393
|
+
for ft in fittings:
|
|
394
|
+
dp_minor += self._minor_loss_dp(ft, v, f=f, d=d)
|
|
395
|
+
|
|
396
|
+
return {
|
|
397
|
+
"velocity": v,
|
|
398
|
+
"reynolds": Re,
|
|
399
|
+
"friction_factor": f,
|
|
400
|
+
"pressure_drop": dp_major + dp_minor,
|
|
401
|
+
"major_dp": dp_major,
|
|
402
|
+
"minor_dp": dp_minor,
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
def _compute_series(self, series_list, flow_rate, fluid):
|
|
406
|
+
results = []
|
|
407
|
+
dp_total = Pressure(0, "Pa")
|
|
408
|
+
|
|
409
|
+
# Precompute a working velocity/diameter if there is at least one pipe
|
|
410
|
+
# (needed for fittings). We update this on the fly for each pipe.
|
|
411
|
+
current_d: Optional[Diameter] = None
|
|
412
|
+
current_v: Optional[Velocity] = None
|
|
413
|
+
current_f: Optional[float] = None
|
|
414
|
+
|
|
415
|
+
for element in series_list:
|
|
416
|
+
element_name = getattr(element, "name", getattr(element, "fitting_type", element.__class__.__name__.lower()))
|
|
417
|
+
|
|
418
|
+
# --- Pipe ---
|
|
419
|
+
if isinstance(element, Pipe):
|
|
420
|
+
calc = self._pipe_calculation(element, flow_rate)
|
|
421
|
+
dp_total += calc["pressure_drop"]
|
|
422
|
+
current_d = self._internal_diameter_m(element)
|
|
423
|
+
current_v = calc["velocity"]
|
|
424
|
+
# friction factor for this pipe (useful for subsequent fittings via Le/D)
|
|
425
|
+
current_f = calc["friction_factor"]
|
|
426
|
+
|
|
427
|
+
results.append({
|
|
428
|
+
"type": "pipe",
|
|
429
|
+
"name": element_name,
|
|
430
|
+
"length": element.length,
|
|
431
|
+
"diameter": current_d,
|
|
432
|
+
"pressure_drop_Pa": calc["pressure_drop"],
|
|
433
|
+
"reynolds": calc["reynolds"],
|
|
434
|
+
"friction_factor": current_f,
|
|
435
|
+
"velocity": current_v,
|
|
436
|
+
})
|
|
437
|
+
|
|
438
|
+
# --- Pump (your Pump class) ---
|
|
439
|
+
elif element.__class__.__name__ == "Pump":
|
|
440
|
+
pump_gain = self._pump_gain_pa(element) # Pa
|
|
441
|
+
dp_total -= pump_gain # pump adds energy
|
|
442
|
+
results.append({
|
|
443
|
+
"type": "pump",
|
|
444
|
+
"name": element_name,
|
|
445
|
+
"head_gain_Pa": pump_gain,
|
|
446
|
+
"head_m": getattr(element, "head", None),
|
|
447
|
+
"efficiency": getattr(element, "efficiency", None),
|
|
448
|
+
})
|
|
449
|
+
|
|
450
|
+
# --- Equipment (your Equipment class with pressure_drop in bar) ---
|
|
451
|
+
elif element.__class__.__name__ == "Equipment":
|
|
452
|
+
eq_dp = self._equipment_dp_pa(element)
|
|
453
|
+
dp_total += eq_dp
|
|
454
|
+
results.append({
|
|
455
|
+
"type": "equipment",
|
|
456
|
+
"name": getattr(element, "name", element_name),
|
|
457
|
+
"pressure_drop_Pa": eq_dp,
|
|
458
|
+
})
|
|
459
|
+
|
|
460
|
+
# --- Fitting (minor loss) ---
|
|
461
|
+
elif isinstance(element, Fitting):
|
|
462
|
+
# We need a velocity & diameter context. If none yet, try to derive from engine-level flowrate.
|
|
463
|
+
if current_d is None or current_v is None:
|
|
464
|
+
# try derive from any pipe dimension on fitting or engine diameter
|
|
465
|
+
d_for_fit = None
|
|
466
|
+
if hasattr(element, "diameter") and element.diameter:
|
|
467
|
+
d_for_fit = element.diameter if isinstance(element.diameter, Diameter) else Diameter(float(element.diameter), "mm")
|
|
468
|
+
else:
|
|
469
|
+
d_for_fit = self._internal_diameter_m() # could compute via optimum if single-line
|
|
470
|
+
q = flow_rate or self._maybe_flowrate()
|
|
471
|
+
current_v = FluidVelocity(volumetric_flow_rate=q, diameter=d_for_fit).calculate()
|
|
472
|
+
current_d = d_for_fit
|
|
473
|
+
# friction factor unknown; pass None (method will recompute if needed)
|
|
474
|
+
current_f = None
|
|
475
|
+
|
|
476
|
+
dp_fit = self._fitting_dp_pa(element, current_v, current_f, current_d)
|
|
477
|
+
dp_total += dp_fit
|
|
478
|
+
results.append({
|
|
479
|
+
"type": "fitting",
|
|
480
|
+
"name": element_name,
|
|
481
|
+
"pressure_drop_Pa": dp_fit,
|
|
482
|
+
"using": "K" if element.total_K() is not None else ("Le" if element.equivalent_length() is not None else "none"),
|
|
483
|
+
"quantity": getattr(element, "quantity", 1),
|
|
484
|
+
})
|
|
485
|
+
|
|
486
|
+
# --- Vessel (no default ΔP; acts as connection/boundary unless later extended) ---
|
|
487
|
+
elif element.__class__.__name__ == "Vessel":
|
|
488
|
+
# treat as zero-ΔP pass-through unless you decide to add a vessel ΔP later
|
|
489
|
+
results.append({
|
|
490
|
+
"type": "vessel",
|
|
491
|
+
"name": getattr(element, "name", element_name),
|
|
492
|
+
"pressure_drop_Pa": Pressure(0.0, "Pa"),
|
|
493
|
+
"note": "No default ΔP; boundary/holdup element.",
|
|
494
|
+
})
|
|
495
|
+
|
|
496
|
+
# --- Nested Networks (unchanged) ---
|
|
497
|
+
elif isinstance(element, PipelineNetwork):
|
|
498
|
+
# Handled by _compute_network (this path normally won't show up here)
|
|
499
|
+
raise TypeError("Nested networks should be handled in _compute_network, not _compute_series.")
|
|
500
|
+
|
|
501
|
+
else:
|
|
502
|
+
raise TypeError(f"Unsupported element type: {type(element).__name__}")
|
|
503
|
+
|
|
504
|
+
# --- Optional inlet/outlet pressures on any element (duck-typed) ---
|
|
505
|
+
inlet_p = getattr(element, "inlet_pressure", None)
|
|
506
|
+
outlet_p = getattr(element, "outlet_pressure", None)
|
|
507
|
+
if inlet_p is not None and outlet_p is not None:
|
|
508
|
+
dp_element = self._as_pressure(inlet_p).to("Pa") - self._as_pressure(outlet_p).to("Pa")
|
|
509
|
+
dp_total += dp_element
|
|
510
|
+
results[-1].update({
|
|
511
|
+
"inlet_pressure_Pa": self._as_pressure(inlet_p).to("Pa"),
|
|
512
|
+
"outlet_pressure_Pa": self._as_pressure(outlet_p).to("Pa"),
|
|
513
|
+
"net_dp_from_defined_pressures_Pa": dp_element
|
|
514
|
+
})
|
|
515
|
+
|
|
516
|
+
return dp_total, results
|
|
517
|
+
|
|
518
|
+
def _resolve_parallel_flows(self, net: PipelineNetwork, q_m3s: VolumetricFlowRate, branches: List[Any]) -> List[float]:
|
|
519
|
+
split_cfg = (self.data.get("flow_split") or {}).get(net.name)
|
|
520
|
+
n = len(branches)
|
|
521
|
+
if split_cfg is None:
|
|
522
|
+
return [q_m3s.value / n] * n
|
|
523
|
+
vals = [float(x) for x in split_cfg]
|
|
524
|
+
if sum(vals) > 1.5 * q_m3s.value: # treat as absolute flows
|
|
525
|
+
return vals
|
|
526
|
+
s = sum(vals)
|
|
527
|
+
return [q_m3s.value * (v / s) for v in vals]
|
|
528
|
+
|
|
529
|
+
def _compute_network(self, net: PipelineNetwork, q_m3s: Optional[VolumetricFlowRate]) -> Tuple[Pressure, List[Dict[str, Any]], List[Dict[str, Any]]]:
|
|
530
|
+
"""
|
|
531
|
+
Returns:
|
|
532
|
+
dp_across: Pressure
|
|
533
|
+
element_results: list
|
|
534
|
+
branch_summaries: list
|
|
535
|
+
"""
|
|
536
|
+
if net.connection_type in (None, "series"):
|
|
537
|
+
return (*self._compute_series(net.elements, q_m3s), [])
|
|
538
|
+
|
|
539
|
+
# parallel
|
|
540
|
+
branches = net.elements
|
|
541
|
+
q_branches = self._resolve_parallel_flows(net, q_m3s, branches if isinstance(branches, list) else [])
|
|
542
|
+
branch_summaries: List[Dict[str, Any]] = []
|
|
543
|
+
all_results: List[Dict[str, Any]] = []
|
|
544
|
+
dp_across = Pressure(0.0, "Pa")
|
|
545
|
+
|
|
546
|
+
for idx, (child, qb) in enumerate(zip(branches, q_branches), start=1):
|
|
547
|
+
q_child = VolumetricFlowRate(qb, "m^3/s")
|
|
548
|
+
if isinstance(child, PipelineNetwork):
|
|
549
|
+
dp_b, elems_b, subs = self._compute_network(child, q_child)
|
|
550
|
+
all_results.extend(elems_b)
|
|
551
|
+
branch_summaries.extend(subs)
|
|
552
|
+
else:
|
|
553
|
+
dp_b, elems_b = self._compute_series([child], q_child)
|
|
554
|
+
all_results.extend(elems_b)
|
|
555
|
+
|
|
556
|
+
dp_across = max(dp_across, dp_b)
|
|
557
|
+
branch_summaries.append({
|
|
558
|
+
"parallel_name": net.name,
|
|
559
|
+
"branch_index": idx,
|
|
560
|
+
"flow_m3_s": qb,
|
|
561
|
+
"pressure_drop_Pa": dp_b
|
|
562
|
+
})
|
|
563
|
+
|
|
564
|
+
return dp_across, all_results, branch_summaries
|
|
565
|
+
|
|
566
|
+
# -------------------- RUN / SUMMARY --------------------------------------
|
|
567
|
+
|
|
568
|
+
def run(self) -> PipelineResults:
|
|
569
|
+
"""
|
|
570
|
+
Execute the configured calculation and return PipelineResults.
|
|
571
|
+
Also computes residual DP if inlet/outlet pressures and/or available_dp are provided.
|
|
572
|
+
"""
|
|
573
|
+
results: Dict[str, Any] = {}
|
|
574
|
+
network = self.data.get("network")
|
|
575
|
+
|
|
576
|
+
# Determine inlet flow (for network and diagnostics)
|
|
577
|
+
q_in = None
|
|
578
|
+
try:
|
|
579
|
+
q_in = self._infer_flowrate()
|
|
580
|
+
except Exception:
|
|
581
|
+
pass # For some pure-pressure driven cases user may not provide flow; but most sizing needs it.
|
|
582
|
+
|
|
583
|
+
# NETWORK MODE
|
|
584
|
+
if isinstance(network, PipelineNetwork):
|
|
585
|
+
if q_in is None:
|
|
586
|
+
raise ValueError("Network mode requires flowrate (or sufficient info to infer it).")
|
|
587
|
+
total_dp, element_results, branch_summaries = self._compute_network(network, q_in)
|
|
588
|
+
results["mode"] = "network"
|
|
589
|
+
results["summary"] = {"inlet_flow_m3_s": q_in, "total_pressure_drop_Pa": total_dp}
|
|
590
|
+
results["elements"] = element_results
|
|
591
|
+
if branch_summaries:
|
|
592
|
+
results["parallel_sections"] = branch_summaries
|
|
593
|
+
if hasattr(network, "schematic"):
|
|
594
|
+
results["schematic"] = network.schematic()
|
|
595
|
+
|
|
596
|
+
# SINGLE-PIPE MODE
|
|
597
|
+
else:
|
|
598
|
+
pipe = self._ensure_pipe()
|
|
599
|
+
v = self._maybe_velocity(pipe)
|
|
600
|
+
Re = self._reynolds(v, pipe)
|
|
601
|
+
f = self._friction_factor(Re, pipe)
|
|
602
|
+
dp_major = self._major_loss_dp(f, v, pipe)
|
|
603
|
+
|
|
604
|
+
# Minor losses (engine-level in single-pipe mode)
|
|
605
|
+
dp_minor = Pressure(0.0, "Pa")
|
|
606
|
+
for ft in self.data.get("fittings", []):
|
|
607
|
+
dp_minor += self._minor_loss_dp(ft, v, f=f, d=self._internal_diameter_m(pipe))
|
|
608
|
+
|
|
609
|
+
dp_total = dp_major + dp_minor
|
|
610
|
+
|
|
611
|
+
results["mode"] = "single"
|
|
612
|
+
results["pipe"] = {
|
|
613
|
+
"internal_diameter": self._internal_diameter_m(pipe),
|
|
614
|
+
"length": pipe.length,
|
|
615
|
+
}
|
|
616
|
+
results["velocity_m_s"] = v
|
|
617
|
+
results["reynolds_number"] = Re
|
|
618
|
+
results["friction_factor"] = f
|
|
619
|
+
results["pressure_drop_Pa"] = dp_total
|
|
620
|
+
results["major_dp_Pa"] = dp_major
|
|
621
|
+
results["minor_dp_Pa"] = dp_minor
|
|
622
|
+
|
|
623
|
+
# Residual / control-valve DP logic (works for both modes)
|
|
624
|
+
inlet_p = self.data.get("inlet_pressure")
|
|
625
|
+
outlet_p_target = self.data.get("target_outlet_pressure") or self.data.get("outlet_pressure")
|
|
626
|
+
available_dp = self.data.get("available_dp")
|
|
627
|
+
|
|
628
|
+
if inlet_p is not None and outlet_p_target is not None:
|
|
629
|
+
required_system_dp = inlet_p.to("Pa") - outlet_p_target.to("Pa")
|
|
630
|
+
calc_dp = results["summary"]["total_pressure_drop_Pa"] if results["mode"] == "network" else results["pressure_drop_Pa"]
|
|
631
|
+
residual_dp = required_system_dp - calc_dp
|
|
632
|
+
results["residual_dp_Pa"] = residual_dp
|
|
633
|
+
|
|
634
|
+
if available_dp is not None:
|
|
635
|
+
calc_dp = results["summary"]["total_pressure_drop_Pa"] if results["mode"] == "network" else results["pressure_drop_Pa"]
|
|
636
|
+
results["unused_available_dp_Pa"] = available_dp.to("Pa") - calc_dp
|
|
637
|
+
|
|
638
|
+
self._results = results
|
|
639
|
+
return PipelineResults(results)
|
|
640
|
+
|
|
641
|
+
def summary(self) -> None:
|
|
642
|
+
"""Pretty print a concise human-readable summary of the latest run()."""
|
|
643
|
+
if not hasattr(self, "_results") or not self._results:
|
|
644
|
+
raise ValueError("No results. Call run() first.")
|
|
645
|
+
|
|
646
|
+
r = self._results
|
|
647
|
+
print("\n=== Pipeline Summary ===")
|
|
648
|
+
|
|
649
|
+
if r.get("mode") == "network":
|
|
650
|
+
s = r.get("summary", {})
|
|
651
|
+
q = s.get("inlet_flow_m3_s")
|
|
652
|
+
dp = s.get("total_pressure_drop_Pa")
|
|
653
|
+
if q: print(f"Inlet Flow: {q.value if hasattr(q,'value') else q:.6f} m³/s")
|
|
654
|
+
if dp: print(f"Total Pressure Drop: {dp.value if hasattr(dp,'value') else dp:.2f} Pa")
|
|
655
|
+
|
|
656
|
+
if "parallel_sections" in r:
|
|
657
|
+
print("\n--- Parallel Sections ---")
|
|
658
|
+
for b in r["parallel_sections"]:
|
|
659
|
+
dpb = b["pressure_drop_Pa"]
|
|
660
|
+
print(f" {b['parallel_name']} | Branch {b['branch_index']}: "
|
|
661
|
+
f"Flow={b['flow_m3_s']:.6f} m³/s, ΔP={dpb.value if hasattr(dpb,'value') else dpb:.2f} Pa")
|
|
662
|
+
|
|
663
|
+
if "elements" in r:
|
|
664
|
+
print("\n--- Element Details ---")
|
|
665
|
+
for e in r["elements"]:
|
|
666
|
+
t = e.get("type", "element")
|
|
667
|
+
name = e.get("name", t)
|
|
668
|
+
print(f" {t.capitalize()}: {name}")
|
|
669
|
+
if t == "pipe":
|
|
670
|
+
d = e["diameter"]; v = e["velocity"]; Re = e["reynolds"]; f = e["friction_factor"]; dp = e["pressure_drop_Pa"]
|
|
671
|
+
print(f" D: {d.value if hasattr(d,'value') else d:.2f} mm, "
|
|
672
|
+
f"V: {v.value if hasattr(v,'value') else v:.6f} m/s, "
|
|
673
|
+
f"Re: {Re.value if hasattr(Re,'value') else Re:.0f}, "
|
|
674
|
+
f"f: {f.value if hasattr(f,'value') else f:.6f}, "
|
|
675
|
+
f"ΔP: {dp.value if hasattr(dp,'value') else dp:.2f} Pa")
|
|
676
|
+
elif t == "pump":
|
|
677
|
+
gain = e["head_gain_Pa"]
|
|
678
|
+
print(f" Head Gain: {gain.value if hasattr(gain,'value') else gain:.2f} Pa")
|
|
679
|
+
else:
|
|
680
|
+
dp = e.get("pressure_drop_Pa")
|
|
681
|
+
if dp:
|
|
682
|
+
print(f" ΔP: {dp.value if hasattr(dp,'value') else dp:.2f} Pa")
|
|
683
|
+
|
|
684
|
+
if "schematic" in r:
|
|
685
|
+
print("\n=== Schematic ===")
|
|
686
|
+
print(r["schematic"])
|
|
687
|
+
|
|
688
|
+
else:
|
|
689
|
+
# Single mode
|
|
690
|
+
p = r.get("pipe", {})
|
|
691
|
+
d = p.get("internal_diameter")
|
|
692
|
+
L = p.get("length")
|
|
693
|
+
v = r.get("velocity_m_s")
|
|
694
|
+
Re = r.get("reynolds_number")
|
|
695
|
+
f = r.get("friction_factor")
|
|
696
|
+
dp = r.get("pressure_drop_Pa")
|
|
697
|
+
dpM = r.get("major_dp_Pa")
|
|
698
|
+
dpm = r.get("minor_dp_Pa")
|
|
699
|
+
|
|
700
|
+
print("\n--- Single Pipe ---")
|
|
701
|
+
if d: print(f"Diameter: {d.value if hasattr(d,'value') else d:.2f} mm")
|
|
702
|
+
if L: print(f"Length: {L.value if hasattr(L,'value') else L:.2f} m")
|
|
703
|
+
if v: print(f"Velocity: {v.value if hasattr(v,'value') else v:.6f} m/s")
|
|
704
|
+
if Re is not None:
|
|
705
|
+
print(f"Reynolds: {Re.value if hasattr(Re,'value') else Re:.0f}")
|
|
706
|
+
if f is not None:
|
|
707
|
+
print(f"f (CW): {f.value if hasattr(f,'value') else f:.6f}")
|
|
708
|
+
if dp:
|
|
709
|
+
print(f"ΔP total: {dp.value if hasattr(dp,'value') else dp:.2f} Pa"
|
|
710
|
+
+ (f" (Major {dpM.value if hasattr(dpM,'value') else dpM:.2f}, Minor {dpm.value if hasattr(dpm,'value') else dpm:.2f})" if dpM and dpm else ""))
|
|
711
|
+
|
|
712
|
+
# Residuals
|
|
713
|
+
resid = r.get("residual_dp_Pa")
|
|
714
|
+
if resid is not None:
|
|
715
|
+
print(f"\nResidual ΔP (e.g., for control valve): {resid.value if hasattr(resid,'value') else resid:.2f} Pa")
|
|
716
|
+
unused = r.get("unused_available_dp_Pa")
|
|
717
|
+
if unused is not None:
|
|
718
|
+
print(f"Unused Available ΔP: {unused.value if hasattr(unused,'value') else unused:.2f} Pa")
|