turbo_moc.geometry package#

Blade geometry and coordinate mappings.

The canonical design pipeline lives in geometry_2d, geometry_3d, meridional_channel and linear_cascade. Shared spline/section fitting remains in blade and spline_tool. Historical public names below resolve to preserved deprecated implementations so existing clients and the comparison app run.

class turbo_moc.geometry.BSplineTool(degree=3)[source]#

Bases: object

Methods

backward(data_points, num_cp)

Generates control points from data points using Least Squares.

backward_fixed(data_points, num_cp)

forward(control_points, knots[, num_points])

Generates the curve from control points (Forward Method).

reparameterize_equidistant(data_points[, ...])

Re-samples data points so they are physically equidistant along the arc length.

backward(data_points, num_cp)[source]#

Generates control points from data points using Least Squares. Uses a Uniform Knot Sequence for equispaced control variables.

backward_fixed(data_points, num_cp)[source]#
forward(control_points, knots, num_points=500)[source]#

Generates the curve from control points (Forward Method).

reparameterize_equidistant(data_points, num_points=500)[source]#

Re-samples data points so they are physically equidistant along the arc length. Prevents clustered data from distorting CP distribution.

turbo_moc.geometry.apply_conformal_mapping(chordwise, pitchwise, x1, y1, r1, r2, c_axial, theta0=0.0)[source]#

Log-spiral conformal map from the unrolled (chordwise, pitchwise) frame to Cartesian (X, Y) on an annulus: r=r1 at chordwise=x1, r=r2 at chordwise=x1+c_axial, theta=theta0 at pitchwise=y1.

turbo_moc.geometry.build_meridional_view(stator_axial_chord, stator_r_hub_in, stator_r_hub_out, stator_r_tip_in, stator_r_tip_out, rotor_axial_chord, rotor_r_hub_in, rotor_r_hub_out, rotor_r_tip_in, rotor_r_tip_out, gap)[source]#

Hub/tip line coordinates for a stator row (x in [0, stator_axial_chord]) followed by a rotor row (x in [stator_axial_chord + gap, … + rotor_axial_chord]) – for turbo_moc.plotly/mpl’s plot_meridional_view.

Parameters:
stator_axial_chord, rotor_axial_chordfloat

Each row’s own axial (flow-direction) extent – e.g. the stator blade’s own chordwise span (mm), the rotor’s own “chord” (CSTAR, already dimensionalized if it was designed with r_star).

stator_r_hub_in/out, stator_r_tip_in/out, rotor_r_hub_in/out,
rotor_r_tip_in/outfloat

Hub/tip radii at each row’s own inlet/outlet.

gapfloat

Axial (row-to-row) spacing between the stator’s own trailing edge and the rotor’s own leading edge.

turbo_moc.geometry.compute_throat_sonic_state(nozzle_result: dict) → tuple[float, float][source]#

(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.

turbo_moc.geometry.conformal_length_scale(chord, x1, r1, r2, c_axial)[source]#

Exact local length-magnification factor of the log-spiral conformal map at a given chordwise coordinate.

w = X + iY, as a function of u = chordwise + i*pitchwise, is w = C * exp(a*u) with a = log(r2/r1)/c_axial and C a constant complex number – a holomorphic (complex-differentiable) function of u. For any holomorphic map, the local length-magnification factor is exactly |dw/du| = |a| * |w| = |a| * r (the map’s own r-formula, at this chordwise coordinate; the absolute value matters here – this module’s own _reference_point_and_scale anchors x1 at the chord MAXIMUM, which makes c_axial = min(chord)-max(chord), hence a, negative by convention, even though the magnification itself is always positive). A length measured in the 2D cascade plane at this chordwise location is scaled by exactly this factor once mapped onto the annulus – not an approximation, and no numerical search needed.

The conformal map preserves ANGLES (that’s what “conformal” means), not lengths – e.g. the 2D-plane throat_opening is not the actual radial-passage throat width; see mapped_throat_width.

turbo_moc.geometry.consistent_radial_wrap(blade_data, r1, r2_target, source='stator')[source]#

Blade count and outer radius that keep the designed 2D passage.

Under the log-spiral map one 2D pitch p turns by |a| p, a = ln(r2/r1) / c_axial, at every radius; the copies are laid out 2*pi/N apart. Both agree only for N = 2*pi / (|a| p) (implied_n_blades_radial, which is exactly this, independent of where it is evaluated). So N is the nearest integer to that count at (r1, r2_target), and r2 is then moved to the radius where it is exact:

|ln(r2/r1)| = 2*pi |c_axial| / (N p),   same side of r1 as r2_target

Returns n_blades, r2, and the target’s non-integer implied count.

turbo_moc.geometry.export_annular_blade_step(blade_data, r_hub, r_shroud, face_path, solid_path=None, n_blades=1, flare='pitch_scale', n_span=13, theta0=0.0, source='stator', z_offset=0.0)[source]#

Export a TRUE annular 3D blade solid: the profile is wrapped circumferentially at every span station from r_hub to r_shroud (see turbo_moc.geometry.annular’s module docstring for the wrap math), then lofted through all n_span sections and capped at the hub/shroud ends (_close_loft_solid) into one watertight body – unlike export_flared_blade_step()’s flat 2-wire loft between parallel planes, this actually curves around the machine axis. Always exports a SINGLE blade; n_blades only affects the pitch-scale flare physics (pitch = 2*pi*r/n_blades convention) and is not used to multiply the exported solid.

Requires cadquery (not a core turbo_moc dependency – import kept local so the rest of turbo_moc.geometry works without it installed).

Unlike export_flared_blade_step’s hub_face (flat, so a genuine planar Face), the hub SECTION here is a non-planar 3D curve – it curves around the cylinder AND varies axially along the profile at the same time – so cq.Face.makeFromWires (planar faces only) can’t build a face from it. face_path therefore gets the hub WIRE itself (a valid STEP curve, still useful as a reference/sketch import), not a face.

z_offsetfloat

Added to every axial (Z) coordinate – positions this solid in a shared global frame against an already-placed neighbor (e.g. a rotor STEP file), same purpose as turbo_moc.geometry. export_turbogrid_blade’s x_offset. No sign/orientation logic is applied here (unlike that function’s axial_sign) – a STEP solid has no inherent “inlet/outlet” semantics, so z_offset is a plain additive shift; figure out which end of THIS blade (its wrapped curve/trailing_edge z-range) needs to land where before picking a value.

turbo_moc.geometry.export_annular_blade_stl(blade_data, r_hub, r_shroud, stl_path, n_blades=1, flare='pitch_scale', n_span=13, theta0=0.0, source='stator', z_offset=0.0)[source]#

Export the same watertight annular blade solid as export_annular_blade_step, but as an STL mesh – for CAD-quality (true solid, properly shaded) 3D preview in the app via dash-vtk, which reads triangulated meshes, not BREP/STEP. Requires cadquery.

turbo_moc.geometry.export_blade_step(blade_data, face_path, solid_path=None, extrude_length=1.0)[source]#

Export the closed blade contour (from parametrize_stator_blade) to STEP: a 2D face and, optionally, an extruded 3D solid.

Requires cadquery (not a core turbo_moc dependency – import kept local so the rest of turbo_moc.geometry works without it installed).

turbo_moc.geometry.export_flared_blade_step(blade_data, r_hub_in, r_hub_out, r_tip_in, r_tip_out, face_path, solid_path=None, source='stator')[source]#

Export a FLARED 3D blade solid: lofts between a hub-radius copy of the 2D blade profile (at Z=0) and a tip-radius copy (at Z=blade_height) whose pitchwise (tangential) extent is scaled by r_tip_mean/r_hub_mean – pitch = 2*pi*r/Z_blades at constant blade count, so this is the physically-correct scaling for “same blade count, wider pitch at larger radius”, not an arbitrary stretch.

See turbo_moc.geometry.meridional’s module docstring for why this needs no separate axial (chordwise) shift between hub and tip: flare is assumed to share the same axial stations for hub/tip (Anderson et al.’s own convention), so it shows up as radius change (the meridional view, build_meridional_view) and this pitch scaling here – the two share the SAME hub/tip radii inputs, not independently guessed ones.

Parameters:
blade_datadict

A stator blade dict (parametrize_stator_blade/_semi’s output, needs “blade_curve”/”trailing_edge”) or a rotor blade dict (design_rotor_vortex_blade’s output, needs “blade”) – pick which via source.

r_hub_in, r_hub_out, r_tip_in, r_tip_outfloat

Hub/tip radii at this row’s own inlet/outlet (same length units as blade_data, typically mm) – blade_height and the pitch-scale factor are both derived from these, not separate inputs.

sourcestr

“stator” or “rotor” – which blade_data shape to expect.

Requires cadquery (not a core turbo_moc dependency – import kept local so
the rest of turbo_moc.geometry works without it installed).
turbo_moc.geometry.export_radial_blade_step(blade_data, r1, r2, depth, face_path, solid_path=None, source='stator', theta0=0.0, z_offset=0.0)[source]#

Export a radial-wrapped blade (see _build_radial_blade_solid) as STEP – face_path gets the bottom (z = z_offset) section wire, solid_path gets the watertight extruded solid. Requires cadquery (not a core dependency – import kept local).

turbo_moc.geometry.export_radial_blade_stl(blade_data, r1, r2, depth, stl_path, source='stator', theta0=0.0, z_offset=0.0)[source]#

Export the same watertight radial blade solid as export_radial_blade_step, but as an STL mesh for CAD-quality 3D preview via dash-vtk. Requires cadquery.

turbo_moc.geometry.export_turbogrid_blade(blade_data, r_hub, r_shroud, n_blades, output_dir, span_sections=11, hub_shroud_offset_fraction=0.01, inlet_extension_fraction=1.0, outlet_extension_fraction=2.0, x_offset=0.0, flare='pitch_scale', theta0=0.0, source='stator', axis='X', units='MM', te_type='CutOffEnd')[source]#

Export BladeGen.inf + hub.curve + shroud.curve + profile.curve for TurboGrid meshing – the wrapped-annulus counterpart to turbo_moc. geometry.export_annular_blade_step’s STEP solid (same wrap math, turbo_moc.geometry.wrap_blade_annular, same r_hub/r_shroud/flare conventions), but as the plain-text profile-points format TurboGrid itself consumes, with no cadquery dependency.

Parameters:
blade_datadict

Stator (parametrize_stator_blade/_semi’s output) or rotor (design_rotor_vortex_blade’s output, already scaled to mm) – pick which via source.

r_hub, r_shroudfloat

Hub/shroud radii (mm).

n_bladesint

Blade count – written as “Number of Blade Sets” and used for the pitch-scale flare physics (see wrap_blade_annular).

output_dirPath

Directory the four files are written into (created if missing).

span_sectionsint

Number of profile.curve span stations (>= 2).

hub_shroud_offset_fractionfloat

Inset the first/last profile station this fraction of span in from r_hub/r_shroud – TurboGrid wants near-wall profiles, not sections sitting exactly on the hub/shroud wall.

inlet_extension_fraction, outlet_extension_fractionfloat

How far hub.curve/shroud.curve extend upstream/downstream of the blade’s own LE/TE, as a fraction of its axial chord (asymmetric by default – more room downstream for wake development). Set the side facing an adjacent row to 0 – extensions belong at the machine’s true inlet/outlet, not padding an internal row interface (see shift_turbogrid_files’s docstring).

x_offsetfloat

Added to every axial (X) coordinate after the fact – positions this row in a shared global frame against an already-exported neighbor (e.g. a turbodash rotor), equivalent to exporting at 0 then calling shift_turbogrid_files, just in one step.

flare, theta0see wrap_blade_annular.
source“stator” or “rotor”.
axisAxis of Rotation written to BladeGen.inf (“X”, “Y”, or “Z”).
unitsGeometry Units written to BladeGen.inf.
te_typeBlade 0 TE treatment written to BladeGen.inf.
Returns:
dict

Paths written: {“inf”, “profile”, “hub”, “shroud”}.

turbo_moc.geometry.extract_axial_passage_2d(blade_data, pitch, face_path, solid_path=None, extrude_length=1.0, source='stator', inlet_chords=1.0, outlet_chords=6.0, bend_margin_inlet=0.15, bend_margin_outlet=0.05)[source]#

One periodic passage domain (a pitch-wide domain, vertical near the inlet/outlet and following the blade-to-blade centerline in between, with the blade solid subtracted out as an internal hole) in the unrolled/axial cascade view, as a STEP face and, optionally, an extruded 3D solid. See module docstring for the domain’s shape and why the stator and rotor get different middle-section constructions.

Parameters:
blade_datadict

Stator (parametrize_stator_blade/_semi output – needs “blade_curve”/”trailing_edge”/”suction”/”metal_angle_in”/ “metal_angle_out”/”axial_chord_convergent”/ “axial_chord_divergent”) or rotor (design_rotor_vortex_blade output – needs “blade”/”suction”/”pressure”/”chord”) blade dict. Pick which via source.

pitchfloat

Blade-to-blade pitch (mm), i.e. the periodic translation. For a stator this IS blade_data[“pitch”]; for a rotor that’s been scaled from its own nondimensional r* (see turbo_moc.app’s rotor_scale_mm), pass the SCALED pitch, and pre-scale blade_data’s own length-valued arrays/fields by the same factor (angles need no scaling).

face_path, solid_pathstr

Output STEP file paths. solid_path is optional – omit to skip the 3D extrusion and only export the 2D face.

extrude_lengthfloat

3D solid extrusion depth (mm), only used if solid_path is given.

sourcestr

“stator” or “rotor” – which raw-surface axis convention to use: stator’s pitch runs along x (pitchwise), rotor’s along y (see turbo_moc/geometry/radial.py’s module docstring on the two blades’ opposite chordwise/pitchwise axis conventions).

inlet_chords, outlet_chordsfloat

How far the inlet/outlet caps sit upstream of the leading edge / downstream of the trailing edge, each as a multiple of the blade’s own chord (stator: axial_chord_convergent + axial_chord_divergent; rotor: “chord”) – e.g. the outlet default 6.0 places the outlet six chords downstream, far enough that the outlet boundary condition doesn’t interact with the blade’s own near-field. Decoupled from the stator’s own inlet_chords/ outlet_chords – pass whichever value applies to source. NOTE: for the rotor, max(c_vals) (blade’s own raw chordwise convention) is the OUTLET side, not the inlet (opposite of the stator’s), so internally outlet_chords is applied there and inlet_chords at min(c_vals) – this function handles that swap; callers just pass the physically-correct inlet/outlet values for the source.

bend_margin_inlet, bend_margin_outletfloat

Stator only: where the side walls switch between vertical and staggered, each as a fraction of the blade’s own chordwise extent measured inward from the nearer edge (leading edge for bend_margin_inlet, trailing edge for bend_margin_outlet). Clamped so the vertical sections never cross the LE/TE-angle- lines’ own intersection point. The rotor’s own transition points are the natural chordwise overlap of its own suction surface with the blade’s own true LE/TE (see module docstring) – these two params are unused for it.

turbo_moc.geometry.extract_radial_passage_2d(blade_data, pitch, r1, r2, face_path, solid_path=None, extrude_length=1.0, source='stator', inlet_chords=1.0, outlet_chords=6.0, bend_margin_inlet=0.15, bend_margin_outlet=0.05, n_vertical=25, n_cap=15)[source]#

Conformally maps the SAME periodic passage domain as extract_axial_passage_2d (see its docstring/module docstring for the construction) onto an annular passage between radii r1/r2, using turbo_moc.geometry.radial’s log-spiral wrap – the same mapping already used to wrap blades onto an annulus for Phase 5. One passage, one blade hole (matching n_blades=1 in wrap_curve_radial terms).

IMPORTANT: r1/r2 land at the BLADE’s own leading/trailing edge – same convention as wrap_blade_radial/wrap_rotor_blade_radial – NOT at the passage domain’s own (extended) inlet/outlet caps. The mapping’s reference point/scale (see turbo_moc.geometry.radial’s _reference_point_and_scale) is derived from the blade’s own contour and then shared with the band’s outer boundary, so the band’s inlet/outlet caps (which sit beyond the blade’s LE/TE by inlet_chords/outlet_chords) extrapolate naturally to radii outside [r1, r2] via the same log-spiral formula, rather than r1/r2 themselves being squeezed onto the (much wider) extended domain.

Parameters:
blade_data, pitch, source, inlet_chords, outlet_chords,
bend_margin_inlet, bend_margin_outletsee extract_axial_passage_2d.
r1, r2float

Inner/outer radii (same length units as blade_data), reached at the blade’s own leading/trailing edge (not the extended domain caps – see above).

face_path, solid_path, extrude_lengthsee extract_axial_passage_2d.
n_vertical, n_capint

How densely to resample the domain’s straight vertical/cap segments before mapping (see _dense_boundary_points) – these become log-spiral arcs, not straight lines, once mapped.

turbo_moc.geometry.implied_n_blades_radial(blade_data, r1, r2, source='stator')[source]#

The blade count that makes the copy-to-copy spacing (2*pi*r_eval/n_blades) match this blade’s own mapped pitch at the evaluation point (stator: its throat; rotor: its hub reference point, see _eval_reference) – i.e. the n_blades that actually tiles the annulus without overlap or gaps, AT THAT RADIUS (pitch still varies with span under this map, so this is a single representative value, same role as the axial wrap’s own pitch-consistency check, not an exact guarantee everywhere along the blade). A default N_blades chosen far from this will visibly overlap (too large) or leave gaps (too small) in the 3D preview.

turbo_moc.geometry.mapped_pitch_width(blade_data, r1, r2, source='stator')[source]#

The 2D-plane pitch, rescaled by the exact local conformal length factor at the evaluation point’s chordwise location (the stator’s own throat, or the rotor’s hub reference point – see _eval_reference) – i.e. the actual circumferential footprint ONE mapped blade copy occupies there. Unlike the axial wrap, nothing here guarantees this matches 2*pi*r/n_blades for whatever n_blades you pick (see this module’s docstring and implied_n_blades_radial) – adjacent copies will overlap if n_blades is too large for this footprint.

turbo_moc.geometry.mapped_throat_width(blade_data, r1, r2, source='stator')[source]#

The 2D-plane throat_opening, rescaled by the EXACT local conformal length factor at the throat’s own (chordwise, pitchwise) location – see conformal_length_scale. Uses the SAME reference (x1, c_axial) the actual wrap (wrap_blade_radial/wrap_rotor_blade_radial) derives from the main blade contour, so the result is consistent with the geometry that actually gets rendered/exported.

blade_datadict

Stator: needs “blade_curve” ({“x”,”y”}) and “throat_point” ({“x”,”y”}, see parametrize_stator_blade/_semi) and “throat_opening”. Rotor: this tool’s rotor method has no throat_opening/throat_point – not supported here (its sizing is pitch-consistency only to begin with, see turbo_moc.geometry.sizing.size_rotor_from_pitch).

turbo_moc.geometry.move_control_point_along_normal(blade_data, cp_index, distance, num_curve_points=None)[source]#

Nudge one B-spline control point of a parametrized blade by distance (mm, positive/negative) along its local normal – estimated as the 90-degree rotation of the discrete tangent through its neighboring control points – then re-evaluate the fitted curve from the updated control points.

True click-and-drag isn’t practical in a Plotly/Dash app without heavy custom clientside JS (Plotly’s editable config covers shapes/ annotations, not scatter markers), so this select-a-point-then-nudge workflow is the supported editing mode for turbo_moc.app’s Phase 2.

Moving cp_index 0 or -1 (the fixed leading/trailing anchor points from backward_fixed) will break their exact match to the raw suction/ pressure endpoints – allowed, but usually not what you want.

Returns a new blade_data dict (does not mutate the input): only “control_points” and “blade_curve” change, everything else (pitch, trailing_edge, suction/pressure raw sides, knots, …) is carried over.

turbo_moc.geometry.parametrize_stator_blade(wall_x, wall_y, metal_angle_in=0.0, metal_angle_out=70.0, r_trailing=0.5, inlet_opening_ratio=1.8, n_cp=30, num_baseline_points=500, num_curve_points=500, mirror=False)[source]#

Build a closed stator blade profile from a MOC nozzle wall contour (used as the divergent-section suction side), following the approach of opt_framework’s 1.1_stator_axial.py.

Parameters:
wall_x, wall_yarray_like

Nozzle wall contour, in METERS, matching design_nozzle()’s wall_final["x"]/["y"] (throat at index 0, increasing x downstream). Converted to mm internally – all lengths in the returned dict are in mm, matching the original script’s units.

metal_angle_in, metal_angle_outfloat

Camberline metal angles [deg] at the convergent-section inlet and at the throat (matching the divergent-section wall angle).

r_trailingfloat

Trailing-edge circle radius [mm].

inlet_opening_ratiofloat

pitch / inlet_opening (throat pitch to passage-inlet-opening ratio), sets how far upstream the pressure-side Hermite spline reaches.

n_cpint

Number of B-spline control points fitted to the closed contour.

num_baseline_points, num_curve_pointsint

Resampling / evaluation resolution for the B-spline fit.

mirrorbool

Flip the blade’s concavity (which side is suction/pressure) by reflecting the finished shape across the chordwise (y) axis – i.e. negating every “x” (pitchwise) coordinate in the returned dict, post-construction. NOT the same as negating metal_angle_out: that feeds a turning-angle profile AND a separate frame rotation, both driven by metal_angle_out, and negating it does not cancel cleanly – it produces a self-crossing, invalid shape (confirmed by direct comparison, not assumed). Reflecting the finished 2D points is the only method that’s actually been verified to give a clean mirror image.

Returns:
dict

Flat, JSON-safe dict: suction / pressure (raw sides, mm), blade_curve (closed B-spline-fitted contour, mm), control_points (n_cp x 2, mm), trailing_edge (mm), pitch, throat_opening, inlet_opening (mm), and the input parameters echoed back.

turbo_moc.geometry.parametrize_stator_blade_semi(wall_x, wall_y, metal_angle_in=0.0, metal_angle_out=70.0, r_trailing=0.5, inlet_opening_ratio=2.5, n_cp=30, num_baseline_points=500, num_curve_points=500, trailing_edge_num_points=15, mirror=False)[source]#

Build a closed stator blade profile from a MOC nozzle wall contour, following opt_framework’s stator_parametrization_semi_nozzle.py – a lighter-weight alternative to parametrize_stator_blade() that only needs the suction-side wall, not a mirrored copy of it:

  • Pressure side: a single circular arc between two points (near the leading and trailing edge), not a mirrored-wall-plus-Hermite-spline bridge – so pitch/inlet_opening/the arc’s own geometry set the passage shape directly, rather than the wall’s own divergence.

  • Control points: a plain (non-fixed-endpoint) least-squares B-spline fit (BSplineTool.backward), not backward_fixed – the fitted curve is not forced through the exact suction/pressure start/end points.

Same “plain args in, flat JSON-safe dict out” pattern, same units convention (wall_x/wall_y in METERS, everything in the returned dict in mm) as parametrize_stator_blade() – see its docstring for the parameter meanings shared between the two. inlet_opening_ratio defaults to 2.5 here (vs. 1.8 for the mirrored-wall variant) and trailing_edge_num_points to 15 (vs. 50), matching the two original scripts’ own defaults.

turbo_moc.geometry.read_turbogrid_x_extent(hub_curve_path)[source]#

(x_min, x_max) of a hub.curve/shroud.curve file – format-agnostic (works on ANY BladeGen-format export, not just this module’s own, since the X column convention is shared; see this module’s docstring). Use this on a row’s hub.curve where the facing side’s extension fraction was exported as 0 to get that row’s TRUE blade/domain edge there, for computing the axial offset to hand to shift_turbogrid_files.

turbo_moc.geometry.rescale_rotor_blade(rotor_data: dict, k: float) → dict[source]#

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.

turbo_moc.geometry.rescale_stator_blade(blade_data: dict, k: float) → dict[source]#

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.

turbo_moc.geometry.rotate_radial_points(x, y, angle_rad)[source]#

Rigid rotation by angle_rad about the origin – lays out the n_blades copies around the annulus after mapping.

turbo_moc.geometry.shift_turbogrid_files(src_dir, dst_dir, x_offset)[source]#

Copy a BladeGen export (profile.curve + hub.curve + shroud.curve + BladeGen.inf – from THIS module or any other tool writing the same format, e.g. turbodash’s) into dst_dir with every X (axial) coordinate shifted by x_offset. BladeGen.inf has no coordinates, so it’s just copied unchanged.

This is the tool for lining up two independently-exported blade rows (each with its own local axial origin) so they share one global axial frame with no overlap and no gap at their interface – same principle turbodash’s own assembly exporter uses (one additive offset per row), just applied here as a standalone post-export step since the two rows come from different, unconnected design tools. Pick one row to anchor (x_offset=0, leave it as exported) and shift the OTHER by:

x_offset = (anchor_x_max_at_interface + gap) - this_row_x_min_at_interface

where both x_max/x_min are read (read_turbogrid_x_extent) from each row’s hub.curve AFTER re-exporting with that row’s facing-side extension fraction set to 0 – otherwise you’re offsetting by each row’s own inlet/outlet padding instead of its true blade/domain edge, and the rows will overlap or leave an unwanted gap at the interface.

Radius (hub/shroud) continuity at the interface is NOT handled here – shifting coordinates only repositions a row axially. If the two rows’ hub/shroud radii differ at the interface, that’s a design mismatch (fix it in whichever tool designed that row), not a file-shift fix.

turbo_moc.geometry.size_rotor_from_pitch(rotor_data: dict, r_hub: float, r_shroud: float, n_blades: int) → dict[source]#

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.

turbo_moc.geometry.size_stator_mode_a(blade_data: dict, nozzle_result: dict, mdot: float, n_blades: int, r_hub: float) → dict[source]#

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.

turbo_moc.geometry.size_stator_mode_b(blade_data: dict, nozzle_result: dict, r_hub: float, H: float, n_blades: int, mdot: float) → dict[source]#

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.

turbo_moc.geometry.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)[source]#

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_datadict

Stator: needs “blade_curve” and “trailing_edge” ({“x”, “y”} each, mm). Rotor: needs “blade” ({“x”, “y”}).

r_hub, r_shroudfloat

Hub/shroud radii (same length units as blade_data, mm).

n_bladesint

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).

flarestr

“pitch_scale” (pitch grows with radius, matching export_flared_blade_step’s convention) or “constant” (profile unchanged across span).

n_spanint

Number of span stations from r_hub to r_shroud (>= 2).

theta0float

Angular offset [rad] of the blade’s own reference (index-0) copy.

sourcestr

“stator” or “rotor” – which blade_data shape to expect.

n_previewint 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).

turbo_moc.geometry.wrap_blade_radial(blade_data, r1, r2, n_blades, theta0=0.0)[source]#

Wrap a Phase-2-style stator blade (parametrize_stator_blade/_semi’s output dict) onto an annular passage between radii r1 and r2.

Parameters:
blade_datadict

Needs “blade_curve” and “trailing_edge” ({“x”, “y”} each, mm).

r1, r2float

Inner/outer radii of the annular passage (same length units as blade_data, mm), reached at the blade’s own chordwise extremes.

n_bladesint

Number of blade copies laid out around the full annulus.

theta0float

Angular offset [rad] of the first (index-0) blade copy.

Returns:
dict

“blades”: list of n_blades {“x”, “y”} closed-contour dicts; “trailing_edges”: matching list for the trailing-edge arc; “r1”/”r2”/”n_blades”/”d_theta” echoed back; “units”.

turbo_moc.geometry.wrap_curve_radial(x, y, r1, r2, n_blades, theta0=0.0, chordwise_axis='y')[source]#

Wrap ONE closed (or open) curve (x, y) onto an annular passage between radii r1/r2, laid out as n_blades copies spaced 2*pi/n_blades apart. chordwise_axis picks which of the curve’s own axes runs chordwise (the one that maps to radius) – “y” for a stator-convention curve, “x” for a rotor-convention one; see module docstring.

Returns a list of n_blades {“x”, “y”} dicts (the mapped curve, one per copy) plus the reference (x1, y1, c_axial, d_theta) used, for reuse when wrapping a second curve (e.g. a trailing edge) with the SAME reference frame as this one.

turbo_moc.geometry.wrap_rotor_blade_radial(rotor_data, r1, r2, n_blades, theta0=0.0)[source]#

Wrap a Phase-3-style rotor blade (turbo_moc.rotor.design_rotor_vortex_blade’s output dict) onto an annular passage between radii r1 and r2.

Parameters:
rotor_datadict

Needs “blade” ({“x”, “y”}) – the closed LE/TE-rounded blade contour. Its trailing edge is already part of this closed curve (unlike the stator’s, which keeps a separate arc), so there is no separate trailing-edge overlay here. Its units don’t matter (it may well be nondimensional, r*): the map normalises the contour by its own chordwise extent, so a uniformly rescaled blade wraps to exactly the same result.

r1, r2float

Radii [mm] the blade’s chordwise extremes map to; the result is in the same units.

n_bladesint

Number of blade copies laid out around the full annulus.

theta0float

Angular offset [rad] of the first (index-0) blade copy.

Returns:
dict

Same shape as wrap_blade_radial’s return, with “trailing_edges” as a list of empty {“x”: [], “y”: []} placeholders (nothing extra to draw – kept for a uniform shape with the stator’s result, so plot_radial_cascade works unchanged for either source).

Submodules#