from typing import List, Optional, Tuple
import warnings
import numpy as np
import numpy.typing as npt
from .isentropic import IsenP, IsenT
from turbodesign.loss import losstype
from .bladerow import BladeRow, compute_gas_constants
from .enums import RowType, LossType
from .outlet import OutletType
from scipy.integrate import trapezoid
from scipy.optimize import minimize
from .passage import Passage
from .isentropic import IsenP
from .flow_math import compute_massflow, compute_streamline_areas, compute_power
[docs]
def compute_reynolds(rows:List[BladeRow],passage:Passage):
"""Calculates the Reynolds Number
Args:
rows (List[BladeRow]): Blade row to calculate the Reynolds number
passage (Passage): Passage
"""
for i in range(1,len(rows)-1):
row = rows[i]
xr = passage.get_xr_slice(0.5,(rows[i-1].location,row.percent_hub))
dx = np.diff(xr[:,0])
dr = np.diff(xr[:,1])
c = np.sum(np.sqrt(dx**2+dr**2))
mp = [2/(xr[i,1]+xr[i-1,1])*np.sqrt(dr[i-1]**2 + dx[i-1]**2) for i in range(1,len(xr[:,1]))]
mp = np.hstack([[0],np.cumsum(mp)])
if row.row_type == RowType.Rotor:
V = row.W.mean()
else:
V = row.V.mean()
rho = row.rho.mean()
mu = row.mu
row.Reynolds = row.axial_chord*V*rho/mu
row.mprime = mp
row.axial_chord = max(c,1E-12) # Axial chord
# row.num_blades = int(2*np.pi*row.r.mean() / row.pitch_to_chord * row.axial_chord)
[docs]
def compute_quantities(row:BladeRow,upstream:BladeRow):
"""Calculation of all quantites after radial equilibrium has been solved assuming we know the static pressure at the exit.
Note:
Radial Equilibrium gives P0, T0, Vm. This code assumes the loss either enthalpy or pressure loss has already been calculated
compute_velocity has been called so we know W, Wt, V, Vt, U, M, M_rel
Static Pressure and Temperature should come from Total Temperature and Pressure + Velocity
Args:
row (BladeRow): current blade row. All quantities are at exit
upstream (BladeRow): upstream blade row. All quantities are at exit
"""
if row.row_type == RowType.Rotor:
Cp_avg = (row.Cp+upstream.Cp)/2
# Factor any coolant added and changes in streamline radius
row.T0R = upstream.T0R - T0_coolant_weighted_average(row) # - (upstream.U**2-row.U**2)/(2*Cp_avg)
row.P = upstream.P0_stator_inlet/row.P0_P
loss_type = getattr(row.loss_function, "loss_type", None)
if loss_type == LossType.Pressure:
# This affects the velocity triangle
row.P0R = upstream.P0R - row.Yp*(upstream.P0R-row.P)
row.T = (row.P/row.P0R)**((row.gamma-1)/row.gamma) * row.T0R
row.T0 = (1+(row.gamma-1)/2 * row.M**2) * row.T
row.power_distribution = row.massflow * row.Cp * (upstream.T0 - row.T0)
row.power = np.trapezoid(row.power_distribution,row.r-row.r[0])
row.power_mean = row.massflow[-1] * row.Cp * (upstream.T0.mean()-row.T0.mean())
elif loss_type == LossType.Enthalpy:
' For Enthalpy related loss, assume the static quantities do not change '
row.T = (row.P/row.P0R)**((row.gamma-1)/row.gamma) * row.T0R
row.T0 = (1+(row.gamma-1)/2 * row.M**2) * row.T
def calculate_power(T0:npt.NDArray):
row.power_distribution = row.massflow * row.Cp * (upstream.T0 - T0)
row.power = np.trapezoid(row.power_distribution,row.r-row.r[0])
row.power_mean = row.massflow[-1] * row.Cp * (upstream.T0.mean() - T0.mean())
# Factor in T0R_drop. Convert T0R drop to absolute terms
T_drop = (upstream.T0R - row.T0R) - row.W**2/(2*row.Cp) # row.T0R contains the drop
T0_drop = T_drop*(1+(row.gamma-1)/2*row.M**2)
T0 = upstream.T0 - row.power/row.eta_total/(row.total_massflow*row.Cp) + T0_drop
return T0
T0 = row.T0.copy()
for _ in range(5): # interate on the convergence of T0_drop
T0 = calculate_power(T0)
row.T0 = T0
row.T = row.T0-row.W**2/(2*row.Cp)
row.P0R = row.P * (row.T0R/row.T)**((row.gamma)/(row.gamma-1))
row.P0 = row.P * (row.T0/row.T)**((row.gamma)/(row.gamma-1))
elif row.row_type == RowType.Stator:
' For the stator we already assume the upstream P0 already applied '
loss_type = getattr(row.loss_function, "loss_type", None)
if loss_type == LossType.Pressure:
row.P0 = upstream.P0 - row.Yp*(upstream.P0-row.P)
else:
row.P0 = upstream.P0
row.T0 = upstream.T0 - T0_coolant_weighted_average(row)
row.T = row.T0 / (1+(row.gamma-1)/2*row.M**2)
row.P = row.P0 * (row.T/row.T0)**((row.gamma)/(row.gamma-1))
row.T0R = row.T + row.W**2 / (2*row.Cp)
row.P0R = row.P*(row.T0R/row.T)**((row.gamma)/(row.gamma-1))
[docs]
def stator_calc(row:BladeRow,upstream:BladeRow,downstream:Optional[BladeRow]=None,calculate_vm:bool=True, outlet_type:OutletType=OutletType.static_pressure):
"""Given P0, T0, P, alpha2 of stator calculate all other quantities
Usage:
Set row.P0 = upstream.P0 - any pressure loss
row.T0 = upstream.T0 - any cooling
row.P = row.rp*(row.P0 - rotor.P) + rotor.P
Set alpha2
Args:
row (BladeRow): Stator Row
upstream (BladeRow): Stator or Rotor Row
downstream (BladeRow): Stator or Rotor Row. Defaults to None
calculate_vm (bool): True to calculate the meridional velocity. False, do not calculate this and let radeq calculate it
outlet_type (OutletType): OutletType.static_pressure if static conditions defined at outlet, OutletType.total_pressure if total conditions defined
"""
# Static Pressure is assumed
T0_coolant = 0
if row.coolant is not None:
T0_coolant = T0_coolant_weighted_average(row)
row.T0 = upstream.T0 - T0_coolant
loss_type = getattr(row.loss_function, "loss_type", None)
if loss_type == LossType.Pressure:
row.P0 = upstream.P0 - row.Yp*(upstream.P0-row.P) # When static conditions are defined, use it to calculate P0
elif loss_type == LossType.Enthalpy:
b = row.total_area * row.P0 / np.sqrt(row.T0) * np.sqrt(row.gamma/row.R)
solve_for_M = upstream.total_massflow / b
fun = lambda M : np.abs(solve_for_M - M*(1+(row.gamma-1)/2 * M**2) ** (-(row.gamma+1)/(2*(row.gamma-1))))
M_subsonic = minimize(fun,0.1, method='L-BFGS-B', bounds=[0,1])
M_supersonic = minimize(fun,1.5, method='L-BFGS-B', bounds=[1,5])
row.M = M_subsonic
row.T = row.T0/IsenT(M_subsonic,row.gamma)
a = np.sqrt(row.T*row.gamma*row.R)
row.P = row.total_massflow * row.R*row.T / (row.total_area * row.M * a) # Use the massflow to find static pressure
if downstream is not None:
row.P0_P = float((row.P0/downstream.P).mean())
row.rp = ((row.P-downstream.P)/(upstream.P0-downstream.P)).mean()
if calculate_vm:
row.M = ((row.P0/row.P)**((row.gamma-1)/row.gamma) - 1) * 2/(row.gamma-1)
row.M = np.sqrt(row.M)
T0_T = (1+(row.gamma-1)/2 * row.M**2)
row.T = row.T0/T0_T
row.V = row.M*np.sqrt(row.gamma*row.R*row.T)
row.Vm = row.V*np.cos(row.alpha2)
row.Vx = row.Vm*np.cos(row.phi)
row.Vr = row.Vm*np.sin(row.phi)
row.Vt = row.Vm*np.tan(row.alpha2)
else: # We know Vm, P0, T0, P
row.Vx = row.Vm*np.cos(row.phi)
row.Vr = row.Vm*np.sin(row.phi)
row.Vt = row.Vm*np.tan(row.alpha2)
row.V = np.sqrt(row.Vx**2 + row.Vr**2 + row.Vt**2)
row.T = row.P/(row.R*row.rho) # We know P, this is a guess
row.M = row.V/np.sqrt(row.gamma*row.R*row.T)
if upstream.row_type == RowType.Rotor:
row.alpha1 = upstream.alpha2 # Upstream rotor absolute frame flow angle
row.beta1 = upstream.beta2
row.rho = row.P/(row.R*row.T)
row.U = row.omega*row.r
row.Wt = row.Vt-row.U
row.P0_stator_inlet = upstream.P0
[docs]
def rotor_calc(row:BladeRow,upstream:BladeRow,calculate_vm:bool=True,outlet_type:OutletType=OutletType.static_pressure):
"""Calculates quantities given beta2
Args:
row (BladeRow): Rotor Row
upstream (BladeRow): Stator Row or Rotor Row
calculate_vm (bool): True to calculate the meridional velocity. False, do not calculate this and let radeq calculate it
outlet_type (OutletType): OutletType.static_pressure if static conditions defined at outlet, OutletType.total_pressure if total conditions defined
"""
def _log_rotor_failure(reason:str):
def _fmt(val):
try:
return np.array2string(np.asarray(val), precision=5)
except Exception:
return str(val)
print(f"[RotorCalc] Failure detected: {reason}")
print(f" row.T0R: {_fmt(row.T0R)}")
print(f" row.T: {_fmt(row.T)}")
print(f" row.W: {_fmt(row.W)}")
print(f" row.M: {_fmt(getattr(row,'M', np.nan))}")
print(f" row.M_rel: {_fmt(getattr(row,'M_rel', np.nan))}")
print(f" row.Yp: {_fmt(getattr(row,'Yp', np.nan))}")
if np.any(row.T >= row.T0R):
print(" Note: T should be less than T0R.")
yp_val = getattr(row,'Yp', None)
if yp_val is not None and np.any(yp_val > 0.3):
print(" Note: row.Yp exceeded 0.3 which may indicate an issue with the design or loss model.")
row.P0_stator_inlet = upstream.P0_stator_inlet
## P0_P is assumed
# row.P = row.P0_stator_inlet*1/row.P0_P
upstream_radius = upstream.r
# Upstream Relative Frame Calculations
upstream.U = upstream.rpm*np.pi/30 * upstream_radius # rad/s
upstream.Wt = upstream.Vt - upstream.U
upstream.W = np.sqrt(upstream.Vx**2 + upstream.Wt**2 + upstream.Vr**2)
upstream.beta2 = np.arctan2(upstream.Wt,upstream.Vm)
upstream.T0R = upstream.T+upstream.W**2/(2*upstream.Cp)
upstream.P0R = upstream.P * (upstream.T0R/upstream.T)**((upstream.gamma)/(upstream.gamma-1))
upstream.M_rel = upstream.W/np.sqrt(upstream.gamma*upstream.R*upstream.T)
upstream_rothalpy = upstream.T0R*upstream.Cp - 0.5*upstream.U**2 # H01R - 1/2 U1^2
row.U = row.omega*row.r
if np.any(upstream_rothalpy < 0):
print('U is too high, reduce RPM or radius')
# Rotor Exit Calculations
row.beta1 = upstream.beta2
#row.Yp # Evaluated earlier
row.P0R = upstream.P0R - row.Yp*(upstream.P0R-row.P)
# Total Relative Temperature stays constant through the rotor. Adjust for change in radius from rotor inlet to exit
T0_coolant = 0
if row.coolant is not None:
T0_coolant = T0_coolant_weighted_average(row)
row.T0R = (upstream_rothalpy + 0.5*row.U**2)/row.Cp - T0_coolant
P0R_P = row.P0R / row.P
T0R_T = P0R_P**((row.gamma-1)/row.gamma)
row.T = (row.T0R/T0R_T) # Exit static temperature
if calculate_vm: # Calculates the T0 at the exit
row.W = np.sqrt(2*row.Cp*(row.T0R-row.T)) #! nan popups here a lot for radial machines
nan_in_velocity = np.isnan(np.sum(row.W))
temp_issue = np.any(row.T >= row.T0R)
high_loss = np.any(getattr(row,'Yp',0) > 0.3)
if nan_in_velocity:
# Need to adjust T
reason = "nan detected in relative velocity"
if temp_issue:
reason += "; T >= T0R shouldn't happen because of T-s diagram'"
if high_loss:
reason += "; Yp > 0.3 This could be a problem with the loss model;"
_log_rotor_failure(reason)
raise ValueError(f'nan detected')
row.Vm = row.W*np.cos(row.beta2)
row.Vr = row.Vm*np.sin(row.phi)
row.Wt = row.W*np.sin(row.beta2)
row.Vx = row.Vm*np.cos(row.phi)
row.Vt = row.Wt + row.U
row.V = np.sqrt(row.Vr**2+row.Vt**2+row.Vx**2)
row.M = row.V/np.sqrt(row.gamma*row.R*row.T)
row.Vm = np.sqrt(row.Vx**2+row.Vr**2)
row.T0 = row.T + row.V**2/(2*row.Cp)
row.alpha2 = np.arctan2(row.Vt,row.Vm)
else: # We know Vm from radeq, beta2 from blade angle, T0R from rothalpy
row.Vr = row.Vm*np.sin(row.phi)
row.Vx = row.Vm*np.cos(row.phi)
# Compute W from velocity triangle (not thermodynamics) to close the triangle
row.W = row.Vm / np.cos(row.beta2)
row.Wt = row.W*np.sin(row.beta2)
row.U = row.omega * row.r
row.Vt = row.Wt + row.U
row.alpha2 = np.arctan2(row.Vt,row.Vm)
row.V = np.sqrt(row.Vm**2*(1+np.tan(row.alpha2)**2))
# Update T from energy conservation in rotating frame
row.T = row.T0R - row.W**2 / (2*row.Cp)
row.M = row.V/np.sqrt(row.gamma*row.R*row.T)
# Compute T0 first, then derive P0 from P0R to keep the velocity triangle consistent.
# P0/P0R = (T0/T0R)^(gamma/(gamma-1)) always holds since both reference the same static state.
# Using P0R (from the loss model) avoids dependence on the boundary-condition P, which may
# not be consistent with T after radeq adjusts Vm.
row.T0 = row.T + row.V**2/(2*row.Cp)
row.P0 = row.P0R * (row.T0 / row.T0R) ** (row.gamma / (row.gamma - 1))
row.P0_P = (row.P0_stator_inlet/row.P).mean()
row.M_rel = row.W/np.sqrt(row.gamma*row.R*row.T)
[docs]
def inlet_calc(row:BladeRow):
"""Calculates the conditions for the Inlet
Args:
row (BladeRow): _description_
"""
# Estimate the density
row.T = row.T0 * 1/(1+(row.gamma-1)/2*row.M**2)
row.P = row.P0 * (row.T/row.T0)**(row.gamma/(row.gamma-1))
compute_gas_constants(row)
total_area, streamline_area = compute_streamline_areas(row)
row.total_area = total_area
row.area = streamline_area
Vm_tube = np.zeros(len(row.percent_hub_shroud)-1)
Vm_mean = row.total_massflow / (row.rho.mean()*row.total_area)
for j in range(1,len(row.percent_hub_shroud)):
rho = row.rho[j]
Vm_tube[j-1] = (row.massflow[j]-row.massflow[j-1])/(rho*row.area[j])
# Recover per-streamline Vm from tube-averaged Vm_tube assuming piecewise linear variation
row.Vm[0] = Vm_tube[0] if len(Vm_tube) else Vm_mean
for j in range(1, len(row.percent_hub_shroud)):
rho_bar = 0.5 * (row.rho[j] + row.rho[j-1])
Vm_tube_j = (row.massflow[j] - row.massflow[j-1]) / (rho_bar * row.area[j] + 1e-12)
row.Vm[j] = 2 * Vm_tube_j - row.Vm[j-1]
row.Vr = row.Vm*np.sin(row.phi)
row.V = row.M*np.sqrt(row.gamma*row.R*row.T)
row.Vt = row.V*np.sin(row.alpha2)
row.Vx = row.Vm*np.cos(row.phi)
[docs]
def T0_coolant_weighted_average(row:BladeRow) -> npt.NDArray:
"""Calculate the new weighted Total Temperature array considering coolant
Args:
row (BladeRow): Coolant
massflow (np.ndarray): massflow mainstream
Returns:
float: Total Temperature drop
"""
massflow = row.massflow
total_massflow_no_coolant = row.total_massflow_no_coolant
Cp = row.Cp
Cpc = row.coolant.Cp
T0c = row.coolant.T0
massflow_coolant = row.coolant.massflow_percentage*total_massflow_no_coolant*row.massflow[1:]/row.massflow[-1]
if massflow_coolant.mean()>0:
if row.row_type == RowType.Stator:
T0= row.T0
dT0 = T0.copy() * 0
T0_new = (massflow[1:]*Cp*T0[1:] + massflow_coolant*Cpc*T0c) \
/(massflow[1:]*Cp + massflow_coolant*Cpc)
dT0[1:] = T0_new - row.T0[1:]
dT0[0] = dT0[1]
else: # Rotor use relative total temperature
T0R = row.T0R
T0R_new = T0R.copy()
Cp = row.Cp
T0R_new[1:] = (massflow[1:]*Cp*T0R[1:] + massflow_coolant*Cpc*T0c) \
/(massflow[1:]*Cp + massflow_coolant*Cpc)
T0R_new[0] = T0R_new[1]
T = T0R_new - row.W**2/(2*Cp) # Dont change the velocity triangle but adjust the static temperature
T0_new = T+row.V**2/(2*Cp) # Use new static temperature to calculate the total temperature
dT0 = T0_new - row.T0
return dT0
else:
return row.T0*0