60 PSI, SIDES, feed_line, inp, linear_characteristic, model_block, unavailable,
129def moc(pipe: Pipeline, *, t_end: float, n_reaches: int = 16,
130 valve_R: Optional[Callable[[float], float]] =
None,
131 valve_flow: Optional[Callable[[float], float]] =
None,
132 R_down0: Optional[float] =
None) -> Dict[str, Any]:
133 """Method of characteristics on the reservoir-pipe-valve line, in pressure and mass flow:
135 C+: p_P = p_A - B (m_P - m_A) - Rf m_A|m_A| C-: p_P = p_B + B (m_P - m_B) + Rf m_B|m_B|
137 with ``B = a/A`` and ``Rf`` the reach's friction (Pa/(kg/s)^2). The valve end is either a
138 resistance ``valve_R(t)`` [Pa/(kg/s)^2, inf = shut] to a downstream pressure set so the initial
139 state is steady (``R_down0`` is its value at t = 0), or a prescribed flow ``valve_flow(t)``.
140 Returns the valve-end pressure and flow histories and their extremes."""
143 N =
max(2, int(n_reaches))
146 Rf = pipe.R_friction / N
150 p[0] = pipe.p_tank - pipe.R_entry * m0 * abs(m0)
151 for i
in range(1, N + 1):
152 p[i] = p[i - 1] - Rf * m0 * abs(m0)
153 if valve_flow
is None:
155 raise ValueError(
"a valve resistance or a prescribed flow is needed")
156 r0 = valve_R(0.0)
if R_down0
is None else R_down0
157 p_down = p[N] - r0 * m0 * abs(m0)
160 ts, pv, mv = [0.0], [p[N]], [m0]
161 steps = int(math.ceil(t_end / dt))
162 for n
in range(1, steps + 1):
166 for i
in range(1, N):
167 cp = p[i - 1] + B * m[i - 1] - Rf * m[i - 1] * abs(m[i - 1])
168 cm = p[i + 1] - B * m[i + 1] + Rf * m[i + 1] * abs(m[i + 1])
169 mn[i] = (cp - cm) / (2.0 * B)
170 pn[i] = 0.5 * (cp + cm)
172 cm = p[1] - B * m[1] + Rf * m[1] * abs(m[1])
174 pn[0] = cm + B * mn[0]
176 cp = p[N - 1] + B * m[N - 1] - Rf * m[N - 1] * abs(m[N - 1])
177 if valve_flow
is not None:
178 mn[N] = valve_flow(t)
181 mn[N] = 0.0
if not math.isfinite(R)
else _solve_boundary(R, B, cp - p_down)
182 pn[N] = cp - B * mn[N]
187 return {
"t": ts,
"p_valve": pv,
"m_valve": mv,
"p0_valve": pv[0],
"p_max":
max(pv),
"p_min":
min(pv),
188 "dt": dt,
"B": B,
"p_down": p_down,
"n_reaches": N}
223 start: Optional[Mapping[str, Any]] =
None) -> List[Dict[str, Any]]:
224 """``diagnostics.water_hammer`` rows. ``start`` is ``diagnostics.start`` (for the opening surge).
225 Never raises: a line that cannot be computed is a row ``{line, side, available: false, error}``."""
226 rows: List[Dict[str, Any]] = []
227 for side, key
in SIDES:
229 rows.append(
_row(prep, result, side, key, settings, start))
230 except Exception
as exc:
231 rows.append({
"line":
None,
"side": key, **unavailable(f
"water hammer: {exc}")})
235def _row(prep: Any, result: Mapping[str, Any], side: str, key: str, settings: HammerSettings,
236 start: Optional[Mapping[str, Any]]) -> Dict[str, Any]:
237 series = result[
"series"]
238 firing = [i
for i, f
in enumerate(series.get(
"firing")
or [])
if f]
240 raise ValueError(
"the burn never fired")
243 i =
max(firing, key=
lambda j: float(sub[
"mdot"][j]))
244 m0 = float(sub[
"mdot"][i])
245 p_tank = float(sub[
"tank_psia"][i]) * PSI
246 T = float(sub[
"liquid_K"][i])
247 pc = float(series[
"chamber"][
"pc_psia"][i]) * PSI
248 feed = feed_line(prep, side, mdot_ref=m0, p_ref=p_tank, T_ref=T)
249 if feed.valve
is None:
250 raise ValueError(
"no main valve with a travel time on the drawn path")
251 pipes = [e
for e
in feed.upstream
if e.kind ==
"pipe"]
253 raise ValueError(
"no drawn tube between the tank and the main valve")
254 warnings: List[str] = list(feed.warnings)
255 bores = {round(e.bore, 6)
for e
in pipes}
257 warnings.append(
"the upstream tube changes bore; taken as one pipe at the bore nearest the valve")
259 L = sum(e.length
for e
in pipes)
261 wall = pipe0.wall
or 0.0
262 a =
wave_speed(feed.a_fluid, feed.rho, pipe0.bore, wall, settings.E_wall_pa, settings.c1)
263 v0 = m0 / (feed.rho * A)
264 closure = settings.closure_s
if settings.closure_s
is not None else float(feed.valve.travel_s
or 0.0)
266 comp = prep.model.built.network.branches[pipes[0].id].component
267 inst_params = getattr(comp,
"p", {})
or {}
268 K_entry = float(inst_params.get(
"K_minor", 0.0)
or 0.0)
269 R_entry = K_entry / (2.0 * feed.rho * pipes[0].area ** 2)
270 R_up_total = feed.sum(
"up",
"R")
271 R_fric =
max(R_up_total - R_entry, 0.0)
274 p_in = float(sub[
"inlet_psia"][i]) * PSI
275 phi = m0 / math.sqrt(
max(p_in - pc, 1.0))
276 R_rest = feed.sum(
"dn",
"R") + 1.0 / phi ** 2
278 frac = feed.valve.cv_fraction
or linear_characteristic
280 def valve_R(t: float) -> float:
282 return math.inf
if t > 0.0
else R_v + R_rest
283 x = 1.0 - t / closure
284 f = frac(
max(x, 0.0))
if x < 1.0
else 1.0
285 return math.inf
if f <= 0.0
else R_v / (f * f) + R_rest
287 pipeline =
Pipeline(rho=feed.rho, a=a, bore=pipe0.bore, length=L, p_tank=p_tank, mdot0=m0,
288 R_entry=R_entry, R_friction=R_fric)
289 n_step = int(math.ceil(L / (a * settings.max_step_s)))
if settings.max_step_s > 0
else 0
290 n_reaches =
max(int(settings.n_reaches), n_step)
291 sim =
moc(pipeline, t_end=
max(closure, 0.0) + settings.after_closure_s, n_reaches=n_reaches,
292 valve_R=valve_R, R_down0=R_v + R_rest)
293 surge_moc = sim[
"p_max"] - sim[
"p0_valve"]
295 slow =
michaud(feed.rho, L, v0, closure, a)
296 separation = sim[
"p_min"] < feed.p_sat
298 opening: Dict[str, Any] = {
"available":
False}
299 if start
and start.get(
"available"):
300 arr = (start.get(
"arrival_mdot")
or {}).get(key)
302 dn_pipes = [e
for e
in feed.downstream
if e.kind ==
"pipe"]
303 A_front = dn_pipes[-1].area
if dn_pipes
else A
304 a_front =
wave_speed(feed.a_fluid, feed.rho, dn_pipes[-1].bore
if dn_pipes
else pipe0.bore,
305 (dn_pipes[-1].wall
if dn_pipes
else pipe0.wall)
or 0.0, settings.E_wall_pa, settings.c1)
306 phi_s = (((start.get(
"model")
or {}).get(
"inputs")
or {}).get(f
"{key}.phi")
or {}).get(
"value")
or phi
307 pa = float(prep.ambient_pa)
308 o =
opening_surge(p_front=pa, pc=pa, mdot_arrival=float(arr), phi=float(phi_s), B=a_front / A_front)
309 opening = {
"available":
True,
"peak_psia": o[
"p_peak"] / PSI,
"arrival_mdot": float(arr),
310 "mdot_after": o[
"mdot_after"],
311 "basis":
"start model's rigid-column arrival flow meeting the orifices (gas cushion ignored: "
312 "upper bound); at the manifold, which carries no rating"}
317 mawp = ((prep.derived
or {}).get(
"tank_mawp_psi")
or {}).get(tank)
318 rating = float(mawp)
if mawp
is not None else None
319 said = (((prep.derived
or {}).get(
"vessel_ratings")
or {}).get(tank)
or {}).get(
"words")
320 rating_prov = (f
"{tank} trips the stand at {said or f'{float(mawp):.1f} psia'} (feedtwin's trip); no line, valve or "
321 "manifold rating is drawn")
if mawp
is not None else "no rating drawn"
322 peak_close = sim[
"p_max"]
323 peak_open = opening[
"peak_psia"] * PSI
if opening.get(
"available")
else 0.0
324 peak, peak_source = (peak_close,
"closure")
if peak_close >= peak_open
else (peak_open,
"opening")
329 warnings.append(
"column separation: the valve-end pressure falls below the liquid's vapour pressure after "
330 "the closure; the MOC is not valid past it and the cavity collapse is not computed. Graded on "
331 "the larger of the computed peak and the Joukowsky value, which is not an upper bound here: "
332 "a cavity's collapse can exceed it (Bergant, Simpson & Tijsseling, J. Fluids Struct. 22, 2006)")
333 bound = sim[
"p0_valve"] + jouk
335 peak, peak_source = bound,
"closure (Joukowsky value, column separation; the collapse can exceed it)"
336 ok =
None if rating
is None else bool(peak / PSI <= rating)
337 characteristic = str((getattr(prep.model.built.network.branches[feed.valve.id].component,
"opt", {})
or {})
338 .get(
"characteristic",
"linear"))
340 "length": inp(L,
"m",
"drawn tube, tank outlet to the main valve: " +
" + ".join(e.id
for e
in pipes)),
341 "bore": inp(pipe0.bore,
"m", (pipe0.inputs.get(f
"{pipe0.id}.bore")
or {}).get(
"provenance",
"drawing")),
342 "wall": inp(wall,
"m", ((pipe0.inputs.get(f
"{pipe0.id}.wall_thickness")
or {}).get(
"provenance",
"drawing"))
343 +
" -- flagged (AUDIT 6): one wall value is copy-pasted on every edge of the LE4 drawings"),
344 "E_wall": inp(settings.E_wall_pa,
"Pa", E_316_SOURCE),
345 "c1": inp(settings.c1,
"-",
"assumed 1: tube free to move axially (Wylie & Streeter 1993)"),
346 "a_fluid": inp(feed.a_fluid,
"m/s", f
"CoolProp via feedtwin Fluid('{feed.fluid}') at {p_tank / PSI:.1f} psia, {T:.2f} K"),
347 "rho": inp(feed.rho,
"kg/m^3", f
"CoolProp via feedtwin Fluid('{feed.fluid}')"),
348 "mdot0": inp(m0,
"kg/s", f
"largest line flow while firing (series, t = {float(series['t'][i]):.3f} s)"),
349 "p_tank": inp(p_tank / PSI,
"psia",
"tank at that step (series)"),
350 "closure_s": inp(closure,
"s", ((feed.valve.inputs.get(f
"{feed.valve.id}.travel_time")
or {}).get(
351 "provenance",
"drawing main-valve travel_time") +
" (AUDIT 6: no test cited)")
352 if settings.closure_s
is None else "setting"),
353 "n_reaches": inp(sim[
"n_reaches"],
"-", f
"MOC reaches: at least {settings.n_reaches}, and a step under "
354 f
"{settings.max_step_s:g} s"),
355 "K_entry": inp(K_entry,
"-",
"drawing K_minor of the first tube (tank exit)"),
356 "characteristic": inp(characteristic,
"-",
"the main valve's Cv characteristic as the twin builds it (the drawn "
357 "mains declare none, so feedtwin's default; AUDIT 9.5 §5). With a valve this large against "
358 "the line and injector, the flow falls only in the last few % of travel, so the effective "
359 "closure is a few ms, not the full travel"),
360 "p_sat": inp(feed.p_sat / PSI,
"psia",
"vapour pressure at the liquid temperature (feedtwin Fluid)"),
364 "Joukowsky (1898) dp = rho a dv; Michaud's (1878) slow-closure figure 2 rho L v0 / t_c; the method of "
365 "characteristics for a reservoir-pipe-valve line and the Korteweg wave speed (Wylie & Streeter, Fluid "
366 "Transients in Systems, 1993); opening surge as the MOC orifice boundary at the arriving front.",
368 "the tank holds its pressure behind the outlet loss (ullage compliance >> line compliance)",
369 "the upstream tube is one pipe at the bore nearest the valve; friction steady (frozen factor)",
370 "the valve closes linearly in travel along its Cv characteristic; downstream of the seat the tube and "
371 "injector are quasi-steady and discharge to the chamber held at its pre-closure pressure (a Pc that "
372 "decays during the closure keeps more flow through the last of the travel and steepens the stop, so "
373 "the computed peak can be low; Joukowsky is the first-wave limit)",
374 "the liquid column downstream of the seat is not followed: its own suction and column separation at "
375 "the seat's downstream face are not computed",
376 "worst instant: the largest line flow of the burn, at that step's tank pressure",
377 "column separation (pressure below vapour) ends the MOC's validity; it is flagged, not modelled, and "
378 "graded on the Joukowsky value, which a cavity's collapse can exceed",
379 "the opening surge ignores the gas cushion in the manifold (upper bound)",
380 "ratings: only the tanks are rated on the drawing (MAWP or burst, estimated); lines are graded against the "
381 "pressure their tank trips the stand at",
385 "line":
"+".join(e.id
for e
in pipes),
"side": key,
"valve": feed.valve.id,
"available":
True,
386 "closure_s": closure,
387 "joukowsky_psi": jouk / PSI,
388 "slow_close_psi": slow / PSI,
389 "moc_surge_psi": surge_moc / PSI,
390 "peak_psia": peak / PSI,
391 "peak_source": peak_source,
392 "close_peak_psia": peak_close / PSI,
393 "min_psia": sim[
"p_min"] / PSI,
394 "column_separation": bool(separation),
396 "rating_psia": rating,
397 "rating_basis": rating_prov,
399 "wave_speed_m_s": a,
"v0_m_s": v0,
"length_m": L,
"round_trip_s": 2.0 * L / a,
400 "at_s": float(series[
"t"][i]),
401 "warnings": warnings,
402 "unmeasured": [f
"closure time (travel {closure:g} s, no test cited)",
"main valve Cv characteristic (not drawn)",
403 "tube wall (copy-pasted on the drawing)",
404 "E at LOX temperature",
"c1 (tube anchoring)",
"line and manifold pressure ratings",
405 "manifold gas cushion at priming"],
Dict[str, Any] moc(Pipeline pipe, *, float t_end, int n_reaches=16, Optional[Callable[[float], float]] valve_R=None, Optional[Callable[[float], float]] valve_flow=None, Optional[float] R_down0=None)