Source code for turbo_moc.deprecated.geometry.sizing

"""
turbo_moc.geometry.sizing -- couple a reference 2D cascade stator blade
(parametrize_stator_blade/_semi's output) to a real annulus + mass-flow
target, producing an actual sized stator (r_hub, r_shroud, N_blades, and a
rescaled blade geometry).

Two consistency equations, in this module's own mm/m^3/kg/s unit convention
(matching parametrize_stator_blade's "units": "mm" and the existing cascade
mdot_choked calc in app.py):

    pitch:      k * pitch_ref = 2*pi*r_hub / N_blades
    mass flow:  mdot = rho_star * a_star * (k * throat_ref) * H * N_blades * 1e-6

rho_star/a_star come from the ORIGINAL Phase 1 nozzle solve's throat (M=1)
point (wall_final index 0), via compute_throat_sonic_state -- never from a
fresh design_nozzle() call at a different scale (the MOC solver is not
numerically scale-invariant; rescaling must be pure post-hoc multiplication
of the already-solved reference geometry, see rescale_stator_blade).
"""

import numpy as np
import jaxprop as jxp

__all__ = [
    "compute_throat_sonic_state",
    "rescale_stator_blade",
    "size_stator_mode_a",
    "size_stator_mode_b",
    "rescale_rotor_blade",
    "size_rotor_from_pitch",
]

# Point-array fields holding length data (mm) -- multiply both x and y by k.
_LENGTH_POINT_KEYS = ("suction", "pressure", "blade_curve", "trailing_edge")
# Scalar fields holding length data (mm) -- multiply by k.
_LENGTH_SCALAR_KEYS = (
    "pitch", "throat_opening", "inlet_opening",
    "axial_chord_convergent", "axial_chord_divergent", "r_trailing",
)
# Left untouched: knots (normalized B-spline parameter), metal_angle_in/out
# (degrees), mirror (bool), units (string), n_cp/variant if present.


[docs] def compute_throat_sonic_state(nozzle_result: dict) -> tuple[float, float]: """(rho_star, a_star) at the Phase 1 nozzle's throat/sonic point (wall_final index 0), using the design's own fluid_name + local (rho, T). DmassT_INPUTS, not PT_INPUTS: the throat state can sit arbitrarily close to the saturation curve (e.g. wet-steam/two-phase designs), where (p, T) alone is degenerate -- CoolProp raises "Saturation pressure ... is within 1e-4% of given p" because phase can't be resolved from pressure and temperature at saturation. rho and T together are never degenerate this way.""" wall = nozzle_result["wall_final"] rho_star = float(wall["rho"][0]) T_star = float(wall["T"][0]) fluid = jxp.Fluid(nozzle_result["fluid_name"]) a_star = float(fluid.get_state(jxp.DmassT_INPUTS, rho_star, T_star).a) return rho_star, a_star
[docs] def rescale_stator_blade(blade_data: dict, k: float) -> dict: """New blade dict (does not mutate input) with every length field (point arrays + scalar lengths + control_points) multiplied by k; angles/counts/booleans/strings/knots are carried over unchanged. Pure post-hoc multiplication of an already-solved reference shape -- never re-runs parametrize_stator_blade at a different scale.""" new_data = dict(blade_data) for key in _LENGTH_POINT_KEYS: pts = blade_data.get(key) if pts is not None: new_data[key] = { "x": (np.asarray(pts["x"], dtype=float) * k).tolist(), "y": (np.asarray(pts["y"], dtype=float) * k).tolist(), } for key in _LENGTH_SCALAR_KEYS: value = blade_data.get(key) if value is not None: new_data[key] = float(value) * k control_points = blade_data.get("control_points") if control_points is not None: new_data["control_points"] = (np.asarray(control_points, dtype=float) * k).tolist() new_data["sizing_k"] = float(k) return new_data
[docs] def size_stator_mode_a(blade_data: dict, nozzle_result: dict, mdot: float, n_blades: int, r_hub: float) -> dict: """Case A ('cycle-level'): mdot, n_blades, r_hub given; k and H derived in closed form from pitch + mass-flow consistency. No iteration, no re-invocation of design_nozzle at a different scale.""" pitch_ref = float(blade_data["pitch"]) throat_ref = float(blade_data["throat_opening"]) rho_star, a_star = compute_throat_sonic_state(nozzle_result) k = (2.0 * np.pi * r_hub / n_blades) / pitch_ref H = mdot / (rho_star * a_star * k * throat_ref * n_blades * 1e-6) r_shroud = r_hub + H return { "mode": "A", "k": float(k), "r_hub": float(r_hub), "r_shroud": float(r_shroud), "H": float(H), "n_blades": int(n_blades), "mdot": float(mdot), "rho_star": rho_star, "a_star": a_star, "scaled_blade": rescale_stator_blade(blade_data, k), }
[docs] def size_stator_mode_b(blade_data: dict, nozzle_result: dict, r_hub: float, H: float, n_blades: int, mdot: float) -> dict: """Case B ('full meanline'): r_hub, H, n_blades, mdot ALL given (e.g. from a turbodash YAML sizing). Computes k two independent ways and reports their mismatch as a diagnostic -- a large residual means the 2D blade SHAPE (metal_angle_out/solidity) needs redesigning, not just rescaling. scaled_blade is built from k_pitch: pitch consistency is the hard geometric constraint (N_blades copies of the blade must tile the annulus exactly), so it is treated as ground truth for the exported shape; the mass-flow side is the diagnostic that tells you whether that shape is also aerodynamically consistent.""" pitch_ref = float(blade_data["pitch"]) throat_ref = float(blade_data["throat_opening"]) rho_star, a_star = compute_throat_sonic_state(nozzle_result) k_pitch = (2.0 * np.pi * r_hub / n_blades) / pitch_ref k_massflow = mdot / (rho_star * a_star * throat_ref * H * n_blades * 1e-6) residual = (k_pitch - k_massflow) / k_pitch return { "mode": "B", "k_pitch": float(k_pitch), "k_massflow": float(k_massflow), "residual": float(residual), "r_hub": float(r_hub), "r_shroud": float(r_hub + H), "H": float(H), "n_blades": int(n_blades), "mdot": float(mdot), "rho_star": rho_star, "a_star": a_star, "scaled_blade": rescale_stator_blade(blade_data, k_pitch), }
[docs] def rescale_rotor_blade(rotor_data: dict, k: float) -> dict: """Pure post-hoc multiplication of design_rotor_vortex_blade's output by k -- only blade.x/y, pitch, chord: the fields the 3D wrap/export pipeline (wrap_blade_annular's source="rotor" branch reads ONLY blade_data["blade"]) and this module's own results table actually use. The rotor's other returned substructures (pressure/suction/mach_lines/ blade_lower/upper) are left out entirely -- not needed downstream, and not meaningfully rescaled here. Doesn't mutate the input.""" blade = rotor_data["blade"] return { "blade": { "x": [v * k for v in blade["x"]], "y": [v * k for v in blade["y"]], }, "pitch": float(rotor_data["pitch"]) * k, "chord": float(rotor_data["chord"]) * k, "beta_inlet": rotor_data["beta_inlet"], "beta_outlet": rotor_data["beta_outlet"], "units": "mm", "sizing_k": float(k), }
[docs] def size_rotor_from_pitch(rotor_data: dict, r_hub: float, r_shroud: float, n_blades: int) -> dict: """k derived from pitch consistency alone -- unlike the stator, no mass-flow equation is available at the 2D-cascade level for this tool's rotor method (design_rotor_vortex_blade has no mdot semantics). r_hub/r_shroud are expected to come from the STATOR's own sizing result (size_stator_mode_a/b): same annulus, axial stage, one through-flow passage -- n_blades is the rotor's own free choice, generally different from the stator's N.""" pitch_ref = float(rotor_data["pitch"]) k = (2.0 * np.pi * r_hub / n_blades) / pitch_ref return { "k": float(k), "r_hub": float(r_hub), "r_shroud": float(r_shroud), "n_blades": int(n_blades), "pitch_scaled": pitch_ref * k, "scaled_blade": rescale_rotor_blade(rotor_data, k), }