turbo-design 1.0.0__py2.py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- turbo_design-1.0.0.dist-info/METADATA +25 -0
- turbo_design-1.0.0.dist-info/RECORD +33 -0
- turbo_design-1.0.0.dist-info/WHEEL +4 -0
- turbodesign/__init__.py +9 -0
- turbodesign/arrayfuncs.py +19 -0
- turbodesign/bladerow.py +614 -0
- turbodesign/cantera_gas/co2.yaml +36 -0
- turbodesign/compressorspool.py +60 -0
- turbodesign/coolant.py +10 -0
- turbodesign/enums.py +36 -0
- turbodesign/inlet.py +146 -0
- turbodesign/isentropic.py +82 -0
- turbodesign/loss/__init__.py +1 -0
- turbodesign/loss/compressor/__init__.py +1 -0
- turbodesign/loss/losstype.py +25 -0
- turbodesign/loss/turbine/TD2.py +142 -0
- turbodesign/loss/turbine/__init__.py +8 -0
- turbodesign/loss/turbine/ainleymathieson.py +118 -0
- turbodesign/loss/turbine/craigcox.py +189 -0
- turbodesign/loss/turbine/fixedefficiency.py +29 -0
- turbodesign/loss/turbine/fixedpressureloss.py +25 -0
- turbodesign/loss/turbine/kackerokapuu.py +124 -0
- turbodesign/loss/turbine/traupel.py +95 -0
- turbodesign/lossinterp.py +178 -0
- turbodesign/outlet.py +56 -0
- turbodesign/passage.py +198 -0
- turbodesign/radeq.py +255 -0
- turbodesign/rotor.py +38 -0
- turbodesign/solve_radeq.py +37 -0
- turbodesign/spool.py +289 -0
- turbodesign/stage.py +7 -0
- turbodesign/td_math.py +388 -0
- turbodesign/turbinespool.py +466 -0
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import pickle, os
|
|
2
|
+
from typing import Dict
|
|
3
|
+
from ...bladerow import BladeRow, sutherland
|
|
4
|
+
from ...lossinterp import LossInterp
|
|
5
|
+
from ...enums import RowType, LossType
|
|
6
|
+
import numpy as np
|
|
7
|
+
import pathlib
|
|
8
|
+
from ..losstype import LossBaseClass
|
|
9
|
+
|
|
10
|
+
class CraigCox(LossBaseClass):
|
|
11
|
+
|
|
12
|
+
def __init__(self):
|
|
13
|
+
"""Craig and Cox used to estimate loss for subsonic higher mach number flows in axial turbines. Assumes suction side has low convex surface close to throat.
|
|
14
|
+
|
|
15
|
+
Reference:
|
|
16
|
+
Craig, H. R. M., and H. J. A. Cox. "Performance estimation of axial flow turbines." Proceedings of the Institution of Mechanical Engineers 185.1 (1970): 407-424.
|
|
17
|
+
|
|
18
|
+
"""
|
|
19
|
+
super().__init__(LossType.Enthalpy)
|
|
20
|
+
path = pathlib.Path(os.path.join(os.environ['TD3_HOME'],"craigcox"+".pkl"))
|
|
21
|
+
|
|
22
|
+
if not path.exists():
|
|
23
|
+
print('Download file if doesn\'t exist')
|
|
24
|
+
|
|
25
|
+
with open(path.absolute(),'rb') as f:
|
|
26
|
+
self.data = pickle.load(f) # type: ignore
|
|
27
|
+
|
|
28
|
+
self.C = 1/(200*32.2*778.16) # https://www.sciencedirect.com/science/article/pii/S2666202721000574
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def __call__(self,row:BladeRow, upstream:BladeRow) -> float:
|
|
32
|
+
"""Craig and Cox uses the enthalpy definition of loss to calculate the loss of a turbine stage.
|
|
33
|
+
|
|
34
|
+
Note:
|
|
35
|
+
Losses are organized as Group 1 which include profile losses and secondary flows.
|
|
36
|
+
Group 2 losses include rotor tip leakage, balance losses, guide gland losses, lacing wire losses.
|
|
37
|
+
|
|
38
|
+
All equation numbers are from the craig cox paper
|
|
39
|
+
Craig, H. R. M., and H. J. A. Cox. "Performance estimation of axial flow turbines." Proceedings of the Institution of Mechanical Engineers 185.1 (1970): 407-424.
|
|
40
|
+
|
|
41
|
+
Equations:
|
|
42
|
+
Eta_t = (Work done - Group 2 losses) / (Work done + Group 1 Losses)
|
|
43
|
+
|
|
44
|
+
Group1 Losses = (Xp + Xs + Xa)*C1**2/(200gJ) + (Xp + Xs + Xa)*W2**2/(200gJ)
|
|
45
|
+
Group1 Losses = Stator Component + Rotor Component where C1 and W2 are exit velocities for stator and rotor
|
|
46
|
+
|
|
47
|
+
i+i_stall = (i+i_stall)_basic + (\delta i + stall)_sb + (\delta i + stall)_cb
|
|
48
|
+
|
|
49
|
+
(i+i_stall)_basic from Figure 11
|
|
50
|
+
(delta incidence + stall)_sb + (delta incidence + stall)_cb from Figure 12
|
|
51
|
+
|
|
52
|
+
Profile Loss
|
|
53
|
+
Xp = x_pb N_pr N_pi N_pt + (\delta x_p)_t + (\delta x_p)_s/e + (\delta x_p)_m
|
|
54
|
+
|
|
55
|
+
- x_pb from Figure 5 but use Figure 4 to calculate Fl. Fl*x*s/b is the x axis for Figure 5
|
|
56
|
+
- N_pr from figure 3
|
|
57
|
+
- N_pi from Figure 10
|
|
58
|
+
- N_pt from Figure 6
|
|
59
|
+
- (\delta x_p)_t
|
|
60
|
+
- (\delta x_p)_s/e from Figure 9
|
|
61
|
+
- (\delta x_p)_m from Figure 8
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
upstream (BladeRow): Upstream blade row
|
|
65
|
+
row (BladeRow): downstream blade row
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
float: Stage Efficiency
|
|
69
|
+
"""
|
|
70
|
+
if row.row_type == RowType.Stator:
|
|
71
|
+
return 0
|
|
72
|
+
else:
|
|
73
|
+
V_inlet = upstream.V.mean()
|
|
74
|
+
V = row.W.mean()
|
|
75
|
+
|
|
76
|
+
def lookup_constants(currentRow:BladeRow):
|
|
77
|
+
# Contraction Ratio find s/b and use Fig07
|
|
78
|
+
s_b = currentRow.pitch/currentRow.camber
|
|
79
|
+
if currentRow.row_type == RowType.Stator:
|
|
80
|
+
V = currentRow.V.mean()
|
|
81
|
+
# See weird velocity triangle in Figure 1
|
|
82
|
+
inlet_flow_angle = 90-np.abs(np.degrees(currentRow.alpha1).mean())
|
|
83
|
+
outlet_flow_angle =90-np.abs(np.degrees(currentRow.alpha2).mean())
|
|
84
|
+
incidence_angle = np.degrees(currentRow.alpha1 - currentRow.beta1_metal).mean()
|
|
85
|
+
M_out = currentRow.M.mean()
|
|
86
|
+
else:
|
|
87
|
+
V = currentRow.W.mean()
|
|
88
|
+
inlet_flow_angle = 90-np.abs(np.degrees(currentRow.beta1).mean())
|
|
89
|
+
outlet_flow_angle =90-np.abs(np.degrees(currentRow.beta2).mean())
|
|
90
|
+
incidence_angle = np.degrees(currentRow.beta1 - currentRow.beta1_metal).mean()
|
|
91
|
+
M_out = currentRow.M_rel.mean()
|
|
92
|
+
|
|
93
|
+
Re = V * currentRow.rho*currentRow.chord / sutherland(currentRow.T.mean())
|
|
94
|
+
Re = Re.mean()
|
|
95
|
+
|
|
96
|
+
if currentRow.beta1_fixed:
|
|
97
|
+
blade_inlet_angle = 90-np.abs(np.degrees(currentRow.beta1_metal.mean())) # Alpha
|
|
98
|
+
else:
|
|
99
|
+
if currentRow.row_type == RowType.Stator:
|
|
100
|
+
blade_inlet_angle = 90-np.abs(np.degrees(currentRow.alpha1.mean())) # Alpha1
|
|
101
|
+
else:
|
|
102
|
+
blade_inlet_angle = 90-np.abs(np.degrees(currentRow.beta1.mean())) # beta1
|
|
103
|
+
|
|
104
|
+
te = currentRow.te_pitch * currentRow.pitch
|
|
105
|
+
e_s = 0.3 # Pitch to back radius ratio, assumed. lower value = less loss
|
|
106
|
+
if currentRow.beta1_fixed:
|
|
107
|
+
imin = 90-currentRow.beta1_metal.mean()
|
|
108
|
+
else:
|
|
109
|
+
imin = 90-currentRow.beta1.mean() # Incidence required for minimum loss
|
|
110
|
+
|
|
111
|
+
asin_os = np.degrees(np.arcsin(currentRow.throat/currentRow.pitch))
|
|
112
|
+
|
|
113
|
+
N_pr = self.data['Fig03'](Re,0.05) # use a good finish for the geometry
|
|
114
|
+
if (inlet_flow_angle-imin < 10):
|
|
115
|
+
Fl = 13
|
|
116
|
+
else:
|
|
117
|
+
Fl = self.data['Fig04'](outlet_flow_angle,inlet_flow_angle-imin)
|
|
118
|
+
|
|
119
|
+
x = 1-np.sin(np.radians(outlet_flow_angle))/np.sin(np.radians(inlet_flow_angle))
|
|
120
|
+
contraction_ratio = self.data['Fig07'](x,s_b) # contraction ratio
|
|
121
|
+
|
|
122
|
+
X_pb = self.data['Fig05'](Fl*s_b,contraction_ratio)
|
|
123
|
+
delta_X_pt = self.data['Fig06_delta_Xpt'](currentRow.te_pitch)
|
|
124
|
+
N_pt = self.data['Fig06_Npt'](currentRow.te_pitch,outlet_flow_angle)
|
|
125
|
+
delta_Xpm = self.data['Fig08'](M_out,np.degrees(np.arcsin((currentRow.throat+te)/currentRow.pitch)))
|
|
126
|
+
delta_Xp_se = self.data['Fig09'](e_s,M_out)
|
|
127
|
+
|
|
128
|
+
Fi = self.data['Fig15'](blade_inlet_angle,s_b)
|
|
129
|
+
# Incidence Effects
|
|
130
|
+
if currentRow.beta1_fixed:
|
|
131
|
+
if incidence_angle>0: # Positive incidence
|
|
132
|
+
stall_incidence_angle = self.data['Fig11'](currentRow.beta1.mean(),asin_os)
|
|
133
|
+
|
|
134
|
+
incidence_ratio = (incidence_angle - imin)/(stall_incidence_angle-imin)
|
|
135
|
+
|
|
136
|
+
i_plus_istall_sb = self.data['Fig12_sb'](s_b,asin_os)
|
|
137
|
+
i_plus_istall_cor = self.data['Fig12_cr'](contraction_ratio,asin_os)
|
|
138
|
+
|
|
139
|
+
if blade_inlet_angle<=90:
|
|
140
|
+
i_plus_istall_basic = self.data['Fig11'](inlet_flow_angle,asin_os)
|
|
141
|
+
i_plus_istall = i_plus_istall_basic + i_plus_istall_sb + i_plus_istall_cor # Eqn 5
|
|
142
|
+
else:
|
|
143
|
+
i_plus_istall_basic = self.data["Fig14_i+istall"](blade_inlet_angle,asin_os)
|
|
144
|
+
i_plus_istall = i_plus_istall_basic + (1-(blade_inlet_angle-90)/(90-asin_os))*(i_plus_istall_sb + i_plus_istall_cor) # Eqn 7
|
|
145
|
+
else:
|
|
146
|
+
i_minus_istall_sb = self.data['Fig13'](s_b,asin_os)
|
|
147
|
+
|
|
148
|
+
if blade_inlet_angle<=90:
|
|
149
|
+
i_minus_istall_basic = self.data['Fig13_alpha1'](s_b,asin_os)
|
|
150
|
+
i_minus_istall = i_minus_istall_basic + i_minus_istall_sb # Eqn 6
|
|
151
|
+
else:
|
|
152
|
+
i_minus_istall_basic = self.data["Fig14_i-istall"](blade_inlet_angle,asin_os)
|
|
153
|
+
i_minus_istall = i_minus_istall_basic + (1-(blade_inlet_angle - 90)/(90-asin_os)) * i_minus_istall_sb # Eqn 8
|
|
154
|
+
|
|
155
|
+
imin = (i_plus_istall + Fi * (i_minus_istall))/(1+Fi) # type: ignore # Eqn 9
|
|
156
|
+
N_pi = self.data['Fig10'](imin,incidence_ratio)
|
|
157
|
+
else:
|
|
158
|
+
N_pi = 1 # No effect
|
|
159
|
+
|
|
160
|
+
Xp = X_pb*N_pr*N_pi*N_pt + delta_X_pt + delta_Xp_se + delta_Xpm # Eqn 10
|
|
161
|
+
|
|
162
|
+
# Secondary Loss
|
|
163
|
+
Ns_hb = self.data['Fig17'](1/currentRow.aspect_ratio)
|
|
164
|
+
x_sb = self.data['Fig18']((V_inlet/V)**2,s_b*Fl)
|
|
165
|
+
|
|
166
|
+
Nsr = 1 # N_pr # I have no clue about this. Craig Cox doesn't describe. setting it to 1 for now.
|
|
167
|
+
Xs = Nsr*Ns_hb*x_sb
|
|
168
|
+
# Annulus Loss Factor
|
|
169
|
+
Xa = 0
|
|
170
|
+
return Xp, Xs, Xa
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
Xp1,Xs1,Xa1 = lookup_constants(upstream)
|
|
174
|
+
Xp2,Xs2,Xa2 = lookup_constants(row)
|
|
175
|
+
Group1_Loss = (Xp1 + Xs1 + Xa1) * V_inlet**2 *3.28**2 * self.C + (Xp2 + Xs2 + Xa2*V_inlet**2/V**2) * V**2 *3.28**2 * self.C # type: ignore
|
|
176
|
+
# Eqn 4, convert V from m^2/s^2to ft^2/s^2
|
|
177
|
+
|
|
178
|
+
# According to Equation 3, Group 1 loss is an enthalpy loss Cp*T0 Btu/lb. Need to convert to Pressure Loss
|
|
179
|
+
|
|
180
|
+
# Btu/lbf to J/Kg
|
|
181
|
+
|
|
182
|
+
T0_Loss = Group1_Loss * 2326 / row.Cp # Eqn 3 in Kelvin
|
|
183
|
+
T0_T = (row.T0/row.T).mean()
|
|
184
|
+
T02 = row.T0.mean()-T0_Loss # P02 Changes
|
|
185
|
+
|
|
186
|
+
# According to Equation 3, Group 1 loss is an enthalpy loss Cp*T0. Need to convert to Pressure Loss
|
|
187
|
+
eta_total = (upstream.T0.mean() - row.T0.mean())/(upstream.T0.mean()-(row.T0.mean()-T0_Loss))
|
|
188
|
+
return eta_total
|
|
189
|
+
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from ...bladerow import BladeRow, sutherland
|
|
2
|
+
from ...enums import RowType, LossType
|
|
3
|
+
from ..losstype import LossBaseClass
|
|
4
|
+
|
|
5
|
+
class FixedEfficiency(LossBaseClass):
|
|
6
|
+
efficiency:float
|
|
7
|
+
|
|
8
|
+
def __init__(self,efficiency:float):
|
|
9
|
+
"""Fixed Efficiency Loss
|
|
10
|
+
"""
|
|
11
|
+
super().__init__(LossType.Enthalpy)
|
|
12
|
+
self.efficiency = efficiency
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def __call__(self,row:BladeRow, upstream:BladeRow) -> float:
|
|
16
|
+
"""Fixed efficiency loss
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
upstream (BladeRow): Upstream blade row
|
|
20
|
+
row (BladeRow): downstream blade row
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
float: Stage Efficiency
|
|
24
|
+
"""
|
|
25
|
+
if row.row_type == RowType.Stator:
|
|
26
|
+
return 0
|
|
27
|
+
else:
|
|
28
|
+
return self.efficiency
|
|
29
|
+
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from ...bladerow import BladeRow
|
|
2
|
+
from ..losstype import LossBaseClass
|
|
3
|
+
from ...enums import LossType
|
|
4
|
+
|
|
5
|
+
class FixedPressureLoss(LossBaseClass):
|
|
6
|
+
pressure_loss:float
|
|
7
|
+
|
|
8
|
+
def __init__(self,pressure_loss:float):
|
|
9
|
+
"""Fixed Pressure Loss
|
|
10
|
+
"""
|
|
11
|
+
super().__init__(LossType.Pressure)
|
|
12
|
+
self.pressure_loss = pressure_loss
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def __call__(self,row:BladeRow, upstream:BladeRow) -> float:
|
|
16
|
+
"""Outputs the fixed Pressure Loss
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
upstream (BladeRow): Upstream blade row
|
|
20
|
+
row (BladeRow): downstream blade row
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
float: Pressure Loss
|
|
24
|
+
"""
|
|
25
|
+
return self.pressure_loss
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import pickle, os
|
|
2
|
+
from typing import Dict
|
|
3
|
+
from ...bladerow import BladeRow, sutherland
|
|
4
|
+
from ...lossinterp import LossInterp
|
|
5
|
+
from ...enums import RowType, LossType
|
|
6
|
+
import numpy as np
|
|
7
|
+
import pathlib
|
|
8
|
+
from ..losstype import LossBaseClass
|
|
9
|
+
|
|
10
|
+
class KrackerOkapuu(LossBaseClass):
|
|
11
|
+
|
|
12
|
+
def __init__(self):
|
|
13
|
+
"""KackerOkapuu model is an improvement to the Ainley Mathieson model.
|
|
14
|
+
|
|
15
|
+
Limitations:
|
|
16
|
+
- Doesn't factor incidence loss
|
|
17
|
+
- For steam turbines and impulse turbines
|
|
18
|
+
|
|
19
|
+
Reference:
|
|
20
|
+
Kacker, S. C., and U. Okapuu. "A mean line prediction method for axial flow turbine efficiency." (1982): 111-119.
|
|
21
|
+
|
|
22
|
+
"""
|
|
23
|
+
super().__init__(LossType.Pressure)
|
|
24
|
+
path = pathlib.Path(os.path.join(os.environ['TD3_HOME'],"kackerokapuu"+".pkl"))
|
|
25
|
+
|
|
26
|
+
if not path.exists():
|
|
27
|
+
print('Download file if doesn\'t exist')
|
|
28
|
+
|
|
29
|
+
with open(path.absolute(),'rb') as f:
|
|
30
|
+
self.data = pickle.load(f) # type: ignore
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def __call__(self,row:BladeRow, upstream:BladeRow) -> float:
|
|
35
|
+
"""Kacker Okapuu is an updated version of Ainley Mathieson and Dunham Came. This tool uses the pressure loss definition.
|
|
36
|
+
|
|
37
|
+
Note:
|
|
38
|
+
All equation numbers are from the Kacker Okapuu paper
|
|
39
|
+
|
|
40
|
+
Reference:
|
|
41
|
+
Kacker, S. C., and U. Okapuu. "A mean line prediction method for axial flow turbine efficiency." (1982): 111-119.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
upstream (BladeRow): Upstream blade row
|
|
45
|
+
row (BladeRow): downstream blade row
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
float: Pressure Loss
|
|
49
|
+
"""
|
|
50
|
+
if upstream.row_type == RowType.Stator:
|
|
51
|
+
M1 = upstream.M
|
|
52
|
+
else:
|
|
53
|
+
M1 = upstream.M_rel
|
|
54
|
+
|
|
55
|
+
c = row.chord
|
|
56
|
+
b = row.axial_chord
|
|
57
|
+
if row.row_type == RowType.Stator:
|
|
58
|
+
alpha1 = np.degrees(row.alpha1)
|
|
59
|
+
beta1 = np.degrees(row.beta1_metal)
|
|
60
|
+
alpha2 = np.degrees(row.alpha2)
|
|
61
|
+
M2 = row.M
|
|
62
|
+
h = 0
|
|
63
|
+
Rec = row.V*row.rho*row.chord / sutherland(row.T)
|
|
64
|
+
else:
|
|
65
|
+
h = row.tip_clearance * (row.r[-1]-row.r[0])
|
|
66
|
+
alpha1 = np.degrees(row.beta1)
|
|
67
|
+
beta1 = row.beta1_metal
|
|
68
|
+
alpha2 = np.degrees(row.beta2)
|
|
69
|
+
M2 = row.M_rel
|
|
70
|
+
Rec = row.W*row.rho*row.chord / sutherland(row.T)
|
|
71
|
+
|
|
72
|
+
Yp_beta0 = self.data['Fig01'](row.pitch_to_chord, alpha2)
|
|
73
|
+
Yp_beta1_alpha2 = self.data['Fig02'](row.pitch_to_chord, alpha2)
|
|
74
|
+
t_max_c = self.data['Fig04'](np.abs(beta1)+np.abs(alpha2))
|
|
75
|
+
|
|
76
|
+
Yp_amdc = (Yp_beta0 + np.abs(beta1/alpha2) *beta1/alpha2 * (Yp_beta1_alpha2-Yp_beta0)) * ((t_max_c)/0.2)**(beta1/alpha2) # Eqn 2, AMDC = Ainley Mathieson Dunham Came
|
|
77
|
+
|
|
78
|
+
# Shock Loss
|
|
79
|
+
dP_q1_hub = 0.75*(M1-0.4)**1.75 # Eqn 4, this is at the hub
|
|
80
|
+
dP_q1_shock = row.r[-1]/row.r[0] * dP_q1_hub # Eqn 5
|
|
81
|
+
Y_shock = dP_q1_shock * upstream.P/row.P * (1-(1+(upstream.gamma-1)/2*M1**2))/(1-(1+(row.gamma-1)/2*M2**2)) # Eqn 6
|
|
82
|
+
|
|
83
|
+
K1 = self.data['Fig08_K1'](M2)
|
|
84
|
+
K2 = (M1/M2)**2
|
|
85
|
+
Kp = 1-K2*(1-K1)
|
|
86
|
+
|
|
87
|
+
CFM = 1+60*(M2-1)**2 # Eqn 9
|
|
88
|
+
|
|
89
|
+
Yp = 0.914 * (2/3*Yp_amdc *Kp + Y_shock) # Eqn 8 Subsonic regime
|
|
90
|
+
if M2>1:
|
|
91
|
+
Yp = Yp*CFM
|
|
92
|
+
|
|
93
|
+
f_ar = (1-0.25*np.sqrt(2-h/c)) / (h/c) if h/c<=2 else 1/(h/c)
|
|
94
|
+
alpham = np.arctan(0.5*(np.tan(alpha1) - np.tan(alpha2)))
|
|
95
|
+
Cl_sc = 2*(np.tan(alpha1)+np.tan(alpha2))*np.cos(alpham)
|
|
96
|
+
Ys_amdc = 0.0334 *f_ar *np.cos(alpha2)/np.cos(beta1) * (Cl_sc)**2 * np.cos(alpha2)**2 / np.cos(alpham)**3
|
|
97
|
+
# Secondary Loss
|
|
98
|
+
K3 = 1/(h/(b))**2 # Fig 13, it's actually bx in the picture which is the axial chord
|
|
99
|
+
Ks = 1-K3*(1-Kp) # Eqn 15
|
|
100
|
+
Ys = 1.2*Ys_amdc*Ks # Eqn 16
|
|
101
|
+
|
|
102
|
+
# Trailing Edge
|
|
103
|
+
if np.abs(alpha1-alpha2)<5:
|
|
104
|
+
delta_phi2 = self.data['Fig14_Impulse'](row.te_pitch*row.pitch / row.throat)
|
|
105
|
+
else:
|
|
106
|
+
delta_phi2 = self.data['Fig14_Axial_Entry'](row.te_pitch*row.pitch / row.throat)
|
|
107
|
+
|
|
108
|
+
Ytet = (1-(row.gamma-1)/2 - M2**2 * (1/(1-delta_phi2)-1))**(-row.gamma/(row.gamma-1))-1
|
|
109
|
+
Ytet = Ytet / (1-(1+(row.gamma-1)/2*M2**2)**(-row.gamma/(row.gamma-1)))
|
|
110
|
+
|
|
111
|
+
# Tip Clearance
|
|
112
|
+
kprime = row.tip_clearance/(3)**0.42 # Number of seals
|
|
113
|
+
Ytc = 0.37*c/h * (kprime/c)**0.78 * Cl_sc**2 * np.cos(alpha2)**2 / np.cos(alpham)**3
|
|
114
|
+
|
|
115
|
+
if Rec <= 2E5:
|
|
116
|
+
f_re = (Rec/2E5)**-0.4
|
|
117
|
+
elif Rec<1E6:
|
|
118
|
+
f_re = 1
|
|
119
|
+
else:
|
|
120
|
+
f_re = (Rec/1E6)**-0.2
|
|
121
|
+
|
|
122
|
+
Yt = Yp*f_re + Ys + Ytet + Ytc
|
|
123
|
+
return Yt
|
|
124
|
+
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import pickle, os
|
|
2
|
+
from typing import Dict
|
|
3
|
+
from ...bladerow import BladeRow, sutherland
|
|
4
|
+
from ...lossinterp import LossInterp
|
|
5
|
+
from ...enums import RowType, LossType
|
|
6
|
+
import numpy as np
|
|
7
|
+
import pathlib
|
|
8
|
+
from ..losstype import LossBaseClass
|
|
9
|
+
|
|
10
|
+
class Traupel(LossBaseClass):
|
|
11
|
+
def __init__(self):
|
|
12
|
+
super().__init__(LossType.Enthalpy)
|
|
13
|
+
path = pathlib.Path(os.path.join(os.environ['TD3_HOME'],"traupel"+".pkl"))
|
|
14
|
+
|
|
15
|
+
if not path.exists():
|
|
16
|
+
print('Download file if doesn\'t exist')
|
|
17
|
+
|
|
18
|
+
with open(path.absolute(),'rb') as f:
|
|
19
|
+
self.data = pickle.load(f) # type: ignore
|
|
20
|
+
|
|
21
|
+
def __call__(self,row:BladeRow, upstream:BladeRow) -> float:
|
|
22
|
+
"""Enthalpy loss is computed for the entire stage.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
upstream (BladeRow): Stator Row
|
|
26
|
+
row (BladeRow): Rotor Row
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
float: Efficiency
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
alpha1 = 90-np.degrees(upstream.alpha1.mean())
|
|
33
|
+
alpha2 = 90-np.degrees(upstream.alpha2.mean())
|
|
34
|
+
beta2 = 90 - np.degrees(row.beta1.mean())
|
|
35
|
+
beta3 = 90 - np.degrees(row.beta2.mean())
|
|
36
|
+
|
|
37
|
+
g = upstream.pitch # G is the pitch
|
|
38
|
+
h_stator = upstream.r[-1] - upstream.r[0]
|
|
39
|
+
h_rotor = row.r[-1] - row.r[0]
|
|
40
|
+
|
|
41
|
+
if row.row_type == RowType.Rotor:
|
|
42
|
+
turning = np.abs(np.degrees(upstream.beta2-row.beta2).mean())
|
|
43
|
+
F = self.data['Fig06']((upstream.W/row.W).mean(),turning) # Inlet velocity
|
|
44
|
+
else:
|
|
45
|
+
turning = np.abs(np.degrees(upstream.alpha2-row.alpha2).mean())
|
|
46
|
+
F = self.data['Fig06']((upstream.V/row.V).mean(),turning) # Inlet velocity
|
|
47
|
+
|
|
48
|
+
H = self.data['Fig07'](alpha1-beta2,alpha2-beta3)
|
|
49
|
+
|
|
50
|
+
zeta_s = F*g/h_stator # (h1-h1s)/(0.5*c1s**2) # no idea what h1s or h2s is
|
|
51
|
+
zeta_r = F*g/h_rotor # (h2-h2s)/(0.5*w2s**2)
|
|
52
|
+
x_p_stator = self.data['Fig01'](alpha1,alpha2) # not sure if this is the right figure
|
|
53
|
+
x_p_rotor = self.data['Fig01'](beta2,beta3) # not sure if this is the right figure
|
|
54
|
+
zeta_p_stator = self.data['Fig02'](alpha1,alpha2)
|
|
55
|
+
x_m_stator = self.data['Fig03_0'](upstream.M)
|
|
56
|
+
zeta_p_rotor = self.data['Fig02'](beta2,beta3)
|
|
57
|
+
x_m_rotor = self.data['Fig03_0'](row.M_rel)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
e_te = upstream.te_pitch * g
|
|
61
|
+
o = upstream.throat
|
|
62
|
+
ssen_alpha2 = e_te/o # Thickness of Trailing edge divide by throat
|
|
63
|
+
ssen_beta2 = row.te_pitch*g / row.throat
|
|
64
|
+
|
|
65
|
+
x_delta_stator = self.data['Fig05'](ssen_alpha2,alpha2)
|
|
66
|
+
zeta_delta_stator = self.data['Fig04'](ssen_alpha2,alpha2)
|
|
67
|
+
x_delta_rotor = self.data['Fig05'](ssen_beta2,beta3)
|
|
68
|
+
zeta_delta_rotor = self.data['Fig04'](ssen_beta2,beta3)
|
|
69
|
+
|
|
70
|
+
Dm = 2* (upstream.r[-1] + upstream.r[0])/2 # Is this the mean diameter? I dont know
|
|
71
|
+
zeta_f = 0.5 * (h_stator/Dm)**2
|
|
72
|
+
|
|
73
|
+
zeta_pr_stator = zeta_p_stator * x_p_stator * x_m_stator * x_delta_stator + zeta_delta_stator + zeta_f
|
|
74
|
+
|
|
75
|
+
Dm = 2* (row.r[-1] + row.r[0])/2 # Is this the mean diameter? I dont know
|
|
76
|
+
zeta_f = 0.5 * (h_rotor/Dm)**2
|
|
77
|
+
|
|
78
|
+
zeta_pr_rotor = zeta_p_rotor * x_p_rotor * x_m_rotor * x_delta_rotor + zeta_delta_rotor + zeta_f
|
|
79
|
+
|
|
80
|
+
if row.row_type == RowType.Stator:
|
|
81
|
+
zeta_cl = 0
|
|
82
|
+
else:
|
|
83
|
+
zeta_cl = self.data['Fig08'](row.tip_clearance) # For simplicity assume unshrouded blade
|
|
84
|
+
|
|
85
|
+
zeta_z = 0 # Do not factor this in, a bit complicated
|
|
86
|
+
# 1 - (internal) - (external)
|
|
87
|
+
zeta_v = 0
|
|
88
|
+
zeta_off = 0
|
|
89
|
+
eta_stator = 1- (zeta_pr_stator + zeta_s + 0 + zeta_z) - (zeta_r+zeta_v) - zeta_off # Presentation slide 9
|
|
90
|
+
eta_rotor = 1 - (zeta_pr_rotor + zeta_r + zeta_cl + zeta_z) - (zeta_r+zeta_v) - zeta_off
|
|
91
|
+
return eta_stator+eta_rotor
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
from typing import List, Tuple, Union
|
|
2
|
+
import pandas as pd
|
|
3
|
+
import numpy as np
|
|
4
|
+
import numpy.typing as npt
|
|
5
|
+
from scipy.interpolate import bisplrep, bisplev, interp1d, LSQBivariateSpline
|
|
6
|
+
import matplotlib.pyplot as plt
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class LossInterp:
|
|
10
|
+
"""Interpret the loss of an XY Graph with an additional 3rd variable
|
|
11
|
+
"""
|
|
12
|
+
df: pd.DataFrame
|
|
13
|
+
x:np.ndarray
|
|
14
|
+
y:np.ndarray
|
|
15
|
+
c:np.ndarray
|
|
16
|
+
|
|
17
|
+
x_max_c:np.ndarray
|
|
18
|
+
x_min_c:np.ndarray
|
|
19
|
+
|
|
20
|
+
c_max:float
|
|
21
|
+
c_min:float
|
|
22
|
+
|
|
23
|
+
fxc_max: interp1d
|
|
24
|
+
fxc_min: interp1d
|
|
25
|
+
|
|
26
|
+
weights: np.ndarray
|
|
27
|
+
xlabel:str
|
|
28
|
+
ylabel:str
|
|
29
|
+
clabel:str
|
|
30
|
+
_name:str
|
|
31
|
+
|
|
32
|
+
is_xy:bool
|
|
33
|
+
func: interp1d
|
|
34
|
+
|
|
35
|
+
xlogscale:bool
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def name(self):
|
|
39
|
+
return self._name
|
|
40
|
+
|
|
41
|
+
@name.setter
|
|
42
|
+
def name(self,val:str):
|
|
43
|
+
self._name = val
|
|
44
|
+
|
|
45
|
+
def __init__(self,csv_filename:str,xlabel:str="",ylabel:str="",clabel:str="",logx10:bool=False):
|
|
46
|
+
"""Initialize the loss interpolation with data
|
|
47
|
+
|
|
48
|
+
Note:
|
|
49
|
+
The data contains x,y, and c. Y is predicted from x and c
|
|
50
|
+
Args:
|
|
51
|
+
csv_filename (str): csv data with 3 columns, x,y,c.
|
|
52
|
+
xlabel (str, optional): xlabel. Defaults to "".
|
|
53
|
+
ylabel (str, optional): ylabel. Defaults to "".
|
|
54
|
+
clabel (str, optional): clabel. Defaults to "".
|
|
55
|
+
logx10 (bool, optional): apply log base 10 to x axis. Defaults to False
|
|
56
|
+
"""
|
|
57
|
+
self.df = pd.read_csv(csv_filename)
|
|
58
|
+
self.df = self.df.sort_values(self.df.columns[0],ascending=True)
|
|
59
|
+
self.name = csv_filename
|
|
60
|
+
data = self.df.to_numpy()
|
|
61
|
+
self.xlabel = xlabel
|
|
62
|
+
self.ylabel = ylabel
|
|
63
|
+
self.logX10 = logx10
|
|
64
|
+
if data.shape[1] == 3:
|
|
65
|
+
self.x = data[:,0]
|
|
66
|
+
self.y = data[:,1]
|
|
67
|
+
self.c = data[:,2]
|
|
68
|
+
|
|
69
|
+
self.clabel = clabel
|
|
70
|
+
|
|
71
|
+
# Find the minimum x value for every item in row
|
|
72
|
+
self.x_max_c = np.zeros(shape=(len(self.df.iloc[:,2].unique()),2))
|
|
73
|
+
self.x_min_c = np.zeros(shape=(len(self.df.iloc[:,2].unique()),2))
|
|
74
|
+
|
|
75
|
+
i = 0
|
|
76
|
+
for inlet_flow in self.df.iloc[:,2].unique():
|
|
77
|
+
df_slice = self.df[self.df.iloc[:,2] == inlet_flow]
|
|
78
|
+
df_slice = df_slice.sort_values(df_slice.columns[0],ascending=True)
|
|
79
|
+
self.x_max_c[i,0] = inlet_flow
|
|
80
|
+
self.x_max_c[i,1] = df_slice.iloc[:,0].max()
|
|
81
|
+
|
|
82
|
+
self.x_min_c[i,0] = inlet_flow
|
|
83
|
+
self.x_min_c[i,1] = df_slice.iloc[:,0].min()
|
|
84
|
+
i+=1
|
|
85
|
+
self.fxc_max = interp1d(self.x_max_c[:,0],self.x_max_c[:,1]) # xmax as a function of c
|
|
86
|
+
self.fxc_min = interp1d(self.x_min_c[:,0],self.x_min_c[:,1]) # xmin as a function of c
|
|
87
|
+
|
|
88
|
+
# self.weights = bisplrep(self.x,self.c,self.y,kx=5, ky=5)
|
|
89
|
+
if self.logX10:
|
|
90
|
+
self.func = LSQBivariateSpline(np.log10(self.x),self.c,self.y,np.log10(np.unique(self.x[:4])),np.unique(self.c[:4]))
|
|
91
|
+
else:
|
|
92
|
+
self.func = LSQBivariateSpline(self.x,self.c,self.y,np.unique(self.x[:3]),np.unique(self.c[:3]))
|
|
93
|
+
self.is_xy = False # 3 columns
|
|
94
|
+
self.c_max = self.df.to_numpy()[:,-1].max()
|
|
95
|
+
self.c_min = self.df.to_numpy()[:,-1].min()
|
|
96
|
+
else:
|
|
97
|
+
self.is_xy = True
|
|
98
|
+
self.x = data[:,0]
|
|
99
|
+
self.y = data[:,1]
|
|
100
|
+
if self.logX10:
|
|
101
|
+
self.func = interp1d(np.log10(self.x),self.y)
|
|
102
|
+
else:
|
|
103
|
+
self.func = interp1d(self.x,self.y)
|
|
104
|
+
|
|
105
|
+
def __call__(self,x:Union[npt.NDArray,float],c:Union[npt.NDArray,float,None]=None) -> float:
|
|
106
|
+
"""Pass in an array of x and c values
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
x (Union[npt.NDArray,float]): value on x axis
|
|
110
|
+
c (Union[npt.NDArray,float,None]): Third axis value
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
float: y
|
|
114
|
+
"""
|
|
115
|
+
|
|
116
|
+
if self.is_xy or c==None:
|
|
117
|
+
if self.logX10:
|
|
118
|
+
x = np.log10(x)
|
|
119
|
+
if isinstance(x, np.ndarray):
|
|
120
|
+
return self.func(x)
|
|
121
|
+
elif isinstance(x, float):
|
|
122
|
+
return float(self.func(x))
|
|
123
|
+
else:
|
|
124
|
+
if isinstance(x, np.ndarray):
|
|
125
|
+
xmax = self.fxc_max(c)
|
|
126
|
+
xmin = self.fxc_min(c)
|
|
127
|
+
x[x>xmax] = xmax
|
|
128
|
+
x[x<xmin] = xmin
|
|
129
|
+
if self.logX10:
|
|
130
|
+
y = self.func(np.log10(x),c)
|
|
131
|
+
else:
|
|
132
|
+
y = self.func(x,c)
|
|
133
|
+
elif isinstance(x, float):
|
|
134
|
+
if (c>self.c_max):
|
|
135
|
+
c = self.c_max
|
|
136
|
+
elif c<self.c_min:
|
|
137
|
+
c = self.c_min
|
|
138
|
+
xmax = float(self.fxc_max(c))
|
|
139
|
+
xmin = float(self.fxc_min(c))
|
|
140
|
+
x = xmin if x<xmin else x
|
|
141
|
+
x = xmax if x>xmax else x
|
|
142
|
+
# y[j] = bisplev(x[j],cc,self.weights)
|
|
143
|
+
if self.logX10:
|
|
144
|
+
y = self.func(np.log10(x),c)[0][0] # type: ignore
|
|
145
|
+
else:
|
|
146
|
+
y = self.func(x,c)[0][0] # type: ignore
|
|
147
|
+
return y
|
|
148
|
+
|
|
149
|
+
def plot(self):
|
|
150
|
+
"""Plot the data with predicted values
|
|
151
|
+
"""
|
|
152
|
+
plt.figure(num=1,figsize=(10,6))
|
|
153
|
+
if not self.is_xy:
|
|
154
|
+
c_unique = np.unique(self.c)
|
|
155
|
+
graycolors = plt.cm.gray(np.linspace(0,1,len(c_unique)))
|
|
156
|
+
coolcolors = plt.cm.cool(np.linspace(0,1,len(c_unique)))
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
# Plot the actual data
|
|
160
|
+
i = 0
|
|
161
|
+
for c in c_unique:
|
|
162
|
+
df2 = self.df[self.df.iloc[:,2] == c]
|
|
163
|
+
x = df2.iloc[:,0].to_numpy()
|
|
164
|
+
plt.plot(x,df2.iloc[:,1],'.-',label=f'{c}',color=graycolors[i])
|
|
165
|
+
y = self(x,c)
|
|
166
|
+
plt.plot(x,y,'o-',label=f'predicted-{c}',color=coolcolors[i])
|
|
167
|
+
i+=1
|
|
168
|
+
else:
|
|
169
|
+
x = self.df.iloc[:,0].to_numpy()
|
|
170
|
+
plt.plot(x,self.df.iloc[:,1],'ro',linewidth=2,label='actual')
|
|
171
|
+
y = self(x)
|
|
172
|
+
plt.plot(x,y,'b-',label='predicted')
|
|
173
|
+
|
|
174
|
+
plt.ylabel(self.ylabel)
|
|
175
|
+
plt.xlabel(self.xlabel)
|
|
176
|
+
plt.title(self.name)
|
|
177
|
+
plt.legend()
|
|
178
|
+
plt.show()
|