Source code for turbo_moc.coupling

"""
turbo_moc.coupling -- derive the rotor's relative-frame inlet conditions
(M_rel, beta_rel, P0_rel, T0_rel) from the Phase 1 nozzle/stator's EXIT
state plus a chosen blade speed U, via the absolute -> relative velocity
triangle. This is the rotor-side analogue of turbo_moc.geometry.sizing:
instead of hand-typing the rotor's "uniform flow" Mach/P0_rel/T0_rel/
beta_inlet (today's only option, still available as a manual fallback),
derive them from the stage's own upstream design -- no separate loss model
needed, since the stator is loss-free in this tool (isentropic MOC solve),
so rothalpy (h0_rel = h_static + W^2/2) at the stator's exit entropy is
exactly the rotor's relative stagnation state.
"""

import numpy as np
import jaxprop as jxp

__all__ = ["derive_rotor_inlet_from_stator"]


[docs] def derive_rotor_inlet_from_stator(nozzle_result, alpha_deg, U): """Velocity triangle at the rotor inlet, from the stator's own EXIT static state (wall_final's LAST point: p, T, rho, M -- the converged nozzle wall exit, not the throat) plus the absolute flow angle alpha_deg (from axial -- the stator blade's own metal_angle_out, the angle the MOC design actually targets) and blade speed U [m/s]. Uses DmassT_INPUTS (not PT_INPUTS) for the static-state EOS query -- same reasoning as turbo_moc.geometry.sizing.compute_throat_sonic_state: a wall point can sit arbitrarily close to the saturation curve (wet- steam/two-phase designs), where (p, T) alone is degenerate for phase determination, but (rho, T) never is. Returns ------- dict Rotor inlet conditions with the following keys: - ``p_static``, ``T_static``, ``rho_static``: stator exit static state. - ``M_abs``, ``V_abs``: absolute Mach number and velocity at stator exit. - ``a``: local speed of sound, the same in both frames. - ``W_rel``, ``M_rel``, ``beta_rel_deg``: relative velocity, Mach number, and flow angle. - ``P0_rel``, ``T0_rel``: relative stagnation state at the stator exit's entropy and the rothalpy-consistent relative stagnation enthalpy. - ``U``: blade speed used. - ``is_supersonic``: whether ``M_rel > 1``. The vortex-flow method requires supersonic relative inflow; otherwise its geometry is undefined. """ wall = nozzle_result["wall_final"] p_static = float(wall["p"][-1]) T_static = float(wall["T"][-1]) rho_static = float(wall["rho"][-1]) M_abs = float(wall["M"][-1]) fluid = jxp.Fluid(nozzle_result["fluid_name"]) state_static = fluid.get_state(jxp.DmassT_INPUTS, rho_static, T_static) a = float(state_static.a) h_static = float(state_static.h) s_static = float(state_static.s) V_abs = M_abs * a alpha = np.radians(alpha_deg) V_m = V_abs * np.cos(alpha) V_t = V_abs * np.sin(alpha) W_m = V_m W_t = V_t - U W_rel = float(np.hypot(W_m, W_t)) beta_rel_deg = float(np.degrees(np.arctan2(W_t, W_m))) M_rel = W_rel / a h0_rel = h_static + 0.5 * W_rel ** 2 state0_rel = fluid.get_state(jxp.HmassSmass_INPUTS, h0_rel, s_static) P0_rel = float(state0_rel.p) T0_rel = float(state0_rel.T) # design_rotor_vortex_blade hands (P0_rel, T0_rel) straight to its own # FluidManager, which re-derives the state via a SEPARATE PT_INPUTS # query (core/classes.py's __init__) -- a razor-thin coincidence where # this isentropic reconstruction lands (P0_rel, T0_rel) almost exactly # ON the saturation curve makes that downstream query raise # "Saturation pressure ... is within 1e-4% of given p" with no context # connecting it back to Omega/r here. Confirmed empirically: the failing # band is a few 1e-4 K wide (matching CoolProp's own 1e-4% tolerance), # not a broad unsafe region -- nudging T0_rel by a small, scale-aware # margin reliably clears it (and is a no-op in every other case, well # under this app's 2-decimal display precision). T0_rel += max(1e-3, abs(T0_rel) * 1e-4) return { "p_static": p_static, "T_static": T_static, "rho_static": rho_static, "M_abs": M_abs, "V_abs": float(V_abs), "a": a, "W_rel": W_rel, "M_rel": float(M_rel), "beta_rel_deg": beta_rel_deg, "P0_rel": P0_rel, "T0_rel": T0_rel, "U": float(U), "is_supersonic": bool(M_rel > 1.0), }