Source code for turbo_moc.deprecated.geometry.annular

"""
turbo_moc.geometry.annular -- true 3D annular wrap of an axial blade row,
from a hub radius to a shroud radius.

Unlike turbo_moc.geometry.radial (which maps a blade's CHORDWISE position to
radius, for radial-flow-style visualization), this module keeps the blade's
axial (chordwise) stations fixed across span and instead sweeps a SPAN
parameter r from r_hub to r_shroud, wrapping the blade's pitchwise extent
circumferentially at each radius::

    z_i     = y                          (axial, unchanged across span)
    x_i     = x * (r_i / r_hub)          if flare == "pitch_scale"
            = x                          if flare == "constant"
    theta_i = theta0 + x_i / r_i         (arc length = r_i * theta)
    X_i, Y_i = r_i * cos(theta_i), r_i * sin(theta_i)

"pitch_scale" matches the convention already used by
turbo_moc.geometry.blade.export_flared_blade_step (pitch grows
proportionally with radius at constant blade count); under it theta_i is
constant across span, i.e. a straight radial-fiber blade -- the standard
simplification, not something hard-coded here.

Blade axis convention (see turbo_moc.geometry.radial's module docstring):
a stator's blade_curve/trailing_edge have x = pitchwise, y = chordwise
(after the final profile rotation); a rotor's "blade" has x = chordwise,
y = pitchwise.
"""

import numpy as np

__all__ = ["wrap_blade_annular"]


def _span_wrap(x, y, r_hub, r_shroud, n_span, flare, theta0, chordwise_axis):
    """3D-wrap ONE curve (x, y) across n_span stations from r_hub to
    r_shroud. Returns (r_vals, X, Y, Z), each shape (n_span, len(x))."""
    x = np.asarray(x, dtype=float)
    y = np.asarray(y, dtype=float)
    chord, pitch = (y, x) if chordwise_axis == "y" else (x, y)

    r_vals = np.linspace(r_hub, r_shroud, n_span)
    if flare == "pitch_scale":
        pitch_i = pitch[None, :] * (r_vals[:, None] / r_hub)
    elif flare == "constant":
        pitch_i = np.broadcast_to(pitch[None, :], (n_span, pitch.size))
    else:
        raise ValueError(f"flare must be 'pitch_scale' or 'constant', got {flare!r}")

    # Minus sign: increasing pitchwise coordinate sweeps theta in the
    # negative (clockwise, viewed from +Z) direction -- matches the
    # machine's actual circumferential sense; a "+" here wrapped the
    # blade the wrong way round the axis.
    theta_i = theta0 - pitch_i / r_vals[:, None]
    X = r_vals[:, None] * np.cos(theta_i)
    Y = r_vals[:, None] * np.sin(theta_i)
    Z = np.broadcast_to(chord[None, :], (n_span, chord.size)).copy()
    return r_vals, X, Y, Z


def _rotate_copies(X, Y, Z, n_blades, n_draw):
    """Rotate an (n_span, n_pts) (X, Y, Z) triple about the Z axis into
    n_draw copies (n_draw <= n_blades), spaced 2*pi/n_blades apart -- the
    TRUE physical spacing always comes from n_blades, even when only a
    handful of copies are actually drawn (a preview-performance cap must
    never change how tightly packed the blades look)."""
    d_theta = 2.0 * np.pi / n_blades
    copies = []
    for k in range(n_draw):
        c, s = np.cos(k * d_theta), np.sin(k * d_theta)
        copies.append((X * c - Y * s, X * s + Y * c, Z))
    return copies, d_theta


[docs] def wrap_blade_annular(blade_data, r_hub, r_shroud, n_blades=1, flare="pitch_scale", n_span=13, theta0=0.0, source="stator", n_preview=None): """ Wrap a Phase-2-style stator blade (parametrize_stator_blade/_semi's output) or a Phase-3-style rotor blade (design_rotor_vortex_blade's output) onto a true annulus from r_hub to r_shroud: the blade curves circumferentially at every span station, not just a flat hub/tip loft. Parameters ---------- blade_data : dict Stator: needs "blade_curve" and "trailing_edge" ({"x", "y"} each, mm). Rotor: needs "blade" ({"x", "y"}). r_hub, r_shroud : float Hub/shroud radii (same length units as blade_data, mm). n_blades : int TRUE number of blades around the full annulus -- sets the angular spacing (2*pi/n_blades) between copies. Changing this always changes how tightly packed the returned copies are, even if only a few of them are actually returned (see n_preview). flare : str "pitch_scale" (pitch grows with radius, matching export_flared_blade_step's convention) or "constant" (profile unchanged across span). n_span : int Number of span stations from r_hub to r_shroud (>= 2). theta0 : float Angular offset [rad] of the blade's own reference (index-0) copy. source : str "stator" or "rotor" -- which blade_data shape to expect. n_preview : int or None Cap on how many of the n_blades copies to actually compute/return (e.g. for a responsive Mesh3d preview) -- purely a rendering cap, NEVER changes d_theta/the physical spacing above. None (default) returns all n_blades copies. Returns ------- dict "curve": {"r", "x", "y", "z"} each shape (n_drawn, n_span, n_pts) -- the main blade contour (blade_curve for stator, blade for rotor). "trailing_edge": same shape, or None for a rotor (its TE is part of the closed "blade" contour already). "r_hub"/"r_shroud"/"n_blades"/"n_span"/"flare"/"units" echoed back ("n_blades" is always the TRUE count, not n_drawn). """ if n_span < 2: raise ValueError(f"n_span must be >= 2, got {n_span}") n_draw = n_blades if n_preview is None else min(n_blades, n_preview) if source == "stator": curve = blade_data["blade_curve"] te = blade_data["trailing_edge"] chordwise_axis = "y" elif source == "rotor": curve = blade_data["blade"] te = None chordwise_axis = "x" else: raise ValueError(f"source must be 'stator' or 'rotor', got {source!r}") def _wrap(cv): r_vals, X, Y, Z = _span_wrap(cv["x"], cv["y"], r_hub, r_shroud, n_span, flare, theta0, chordwise_axis) copies, _ = _rotate_copies(X, Y, Z, n_blades, n_draw) Xs = np.stack([c[0] for c in copies]) Ys = np.stack([c[1] for c in copies]) Zs = np.stack([c[2] for c in copies]) r_full = np.broadcast_to(r_vals[None, :, None], Xs.shape) return {"r": r_full, "x": Xs, "y": Ys, "z": Zs} result = { "curve": _wrap(curve), "trailing_edge": _wrap(te) if te is not None else None, "r_hub": float(r_hub), "r_shroud": float(r_shroud), "n_blades": int(n_blades), "n_drawn": int(n_draw), "n_span": int(n_span), "flare": flare, "units": blade_data.get("units", "mm"), } return result