turbo-design 1.0.1__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.

Potentially problematic release.


This version of turbo-design might be problematic. Click here for more details.

turbodesign/spool.py ADDED
@@ -0,0 +1,289 @@
1
+ # type: ignore[arg-type, reportUnknownArgumentType]
2
+ from dataclasses import field
3
+ import json
4
+ from typing import Dict, List, Union
5
+ import matplotlib.pyplot as plt
6
+ from .bladerow import BladeRow
7
+ import numpy as np
8
+ import numpy.typing as npt
9
+ from .enums import RowType, MassflowConstraint
10
+ from cantera.composite import Solution
11
+ from .loss.turbine import TD2
12
+ from pyturbo.helper import line2D
13
+ from scipy.interpolate import interp1d
14
+ from .passage import Passage
15
+ from .inlet import Inlet
16
+ from .outlet import Outlet
17
+
18
+ class Spool:
19
+ blade_rows: List[BladeRow] = []
20
+ massflow:float=0
21
+ rpm:float = 0
22
+
23
+ passage:Passage
24
+ t_streamline: npt.NDArray = field(default_factory=lambda: np.zeros((10,)))
25
+ num_streamlines:int=0
26
+
27
+ # Fluid entering the spool
28
+ _fluid: Solution = Solution("air.yaml")
29
+
30
+ massflow_constraint: MassflowConstraint
31
+
32
+ # Inlet Conditions
33
+ def __init__(self,passage:Passage,
34
+ massflow:float,rows=List[BladeRow],
35
+ num_streamlines:int=3,
36
+ fluid:Solution=Solution('air.yaml'),
37
+ rpm:float=-1,
38
+ massflow_constraint:MassflowConstraint=MassflowConstraint.MatchMassFlow):
39
+ """Initializes a Spool
40
+
41
+
42
+ Note:
43
+ When it comes to massflow, the exit angle of the blade rows along with the upstream total pressure is used to set the massflow through the spool. If turning is too high then massflow is limited and cannot.
44
+ This code gives you the option of setting a massflow and varying the exit angles to match that particular massflow. (MatchMassflow)
45
+ Or keeping the exit angle and modifying the inlet condition to set a specific massflow through the stage. (BalanceMassflow)
46
+
47
+ Args:
48
+ passage (Passage): Passage defining hub and shroud
49
+ massflow (float): massflow at spool inlet
50
+ rows (List[BladeRow], optional): List of blade rows. Defaults to List[BladeRow].
51
+ num_streamlines (int, optional): number of streamlines. Defaults to 3.
52
+ gas (ct.Solution, optional): cantera gas solution. Defaults to ct.Solution('air.yaml').
53
+ rpm (float, optional): RPM for the entire spool Optional, you can also set rpm of the blade rows individually. Defaults to -1.
54
+ massflow_constraint (MassflowConstraint, optional): MatchMassflow - Matches the massflow defined in the spool. BalanceMassflow - Balances the massflow between BladeRows, matches the lowest massflow.
55
+ """
56
+ self.passage = passage
57
+ self.fluid = fluid
58
+ self.massflow = massflow
59
+ self.num_streamlines = num_streamlines
60
+ self.blade_rows = rows
61
+ self.massflow_constraint = massflow_constraint
62
+
63
+ self.rpm = rpm
64
+
65
+ for i in range(len(self.blade_rows)):
66
+ '''Defining RPM in the bladerows
67
+ There's 2 ways of setting the RPM.
68
+ 1. Conventional: Stator-Rotor-Stator-Rotor-etc. Set the RPM equally across all
69
+ 2. Counter Rotation: We either use the RPM already persecribed for each blade row.
70
+ '''
71
+ if (type(self.blade_rows[i]) != Inlet) and (type(self.blade_rows[i]) != Outlet):
72
+ self.blade_rows[i].fluid = self.fluid
73
+ self.blade_rows[i].rpm = rpm
74
+ self.blade_rows[i].axial_chord = self.blade_rows[i].axial_location * self.passage.hub_length
75
+
76
+
77
+ @property
78
+ def fluid(self):
79
+ return self._fluid
80
+
81
+ @fluid.setter
82
+ def fluid(self,newFluid:Solution):
83
+ """Change the type of gas used in the spool
84
+
85
+ Args:
86
+ new_gas (ct.Solution.Solution): New gas mixture
87
+ """
88
+ self._fluid = newFluid
89
+
90
+
91
+ def set_blade_row_rpm(self,index:int,rpm:float):
92
+ """sets the rpm of a particular blade row"""
93
+ self.blade_rows[index].rpm = rpm
94
+
95
+ def set_blade_row_type(self,blade_row_index:int,rowType:RowType):
96
+ """Sets a blade row to be a different type.
97
+
98
+ Args:
99
+ blade_row_index (int): Index of blade row. Keep in mind Blade row 0 is inlet, -1 = outlet
100
+ """
101
+ self.blade_rows[blade_row_index].row_type = rowType
102
+
103
+ def set_blade_row_exit_angles(self,radius:Dict[int,List[float]],beta:Dict[int,List[float]],IsSupersonic:bool=False):
104
+ """Set the intended exit flow angles for the spool.
105
+
106
+ Useful if you already have a geometry that you want to simulate
107
+
108
+ You can set the values for each blade row or simply the inlet and exit
109
+
110
+ Args:
111
+ radius (np.ndarray): Matrix containing [[r1,r2,r3],[r1,r2,r3]] blade_rows x radial locations
112
+ beta (Dict[int,List[float]]): Metal Angle of the geometry, it is assumed that the fluid will leave at this angle.
113
+ IsSupersonic(bool): if solution is supersonic at spool exit
114
+
115
+ Example:
116
+ # Station 0 has angles of [73.1,74.2,75.4]
117
+ # Station 5 has angles of [69,69,69]
118
+ beta = {0: [73.1,74.2,75.4],
119
+ 5: [69,69,69]}
120
+
121
+ """
122
+ for k,v in radius.items():
123
+ self.blade_rows[k].radii_geom = v
124
+ for k,v in beta.items():
125
+ self.blade_rows[k].beta_geom = v
126
+ self.blade_rows[k].beta_fixed = True
127
+ for br in self.blade_rows:
128
+ if not IsSupersonic:
129
+ br.solution_type = SolutionType.subsonic
130
+ else:
131
+ br.solution_type = SolutionType.supersonic
132
+
133
+
134
+ def initialize_streamlines(self):
135
+ """Initialize streamline and compute the curvature.
136
+ This function is called once
137
+ """
138
+
139
+ for row in self.blade_rows:
140
+ row.phi = np.zeros((self.num_streamlines,))
141
+ row.rm = np.zeros((self.num_streamlines,))
142
+ row.r = np.zeros((self.num_streamlines,))
143
+
144
+ t_radial = np.linspace(0,1,self.num_streamlines)
145
+ self.calculate_streamline_curvature(row,t_radial)
146
+
147
+ # Set the loss function if it's not set
148
+ if (row.loss_function == None):
149
+ row.loss_function = TD2()
150
+
151
+ def calculate_streamline_curvature(self,row:BladeRow,t_radial:Union[List[float],npt.NDArray]):
152
+ """Called to calculate new streamline curvature
153
+
154
+ Args:
155
+ row (BladeRow): blade row
156
+ t_radial (Union[List[float],npt.NDArray]): normalized streamline radial locations
157
+ """
158
+ for i,tr in enumerate(t_radial):
159
+ t_streamline, x_streamline, r_streamline = self.passage.get_streamline(tr)
160
+ phi, rm, r = self.passage.streamline_curvature(x_streamline,r_streamline)
161
+ row.phi[i] = float(interp1d(t_streamline,phi)(row.axial_location))
162
+ row.rm[i] = float(interp1d(t_streamline,rm)(row.axial_location))
163
+ row.r[i] = float(interp1d(t_streamline,r)(row.axial_location))
164
+
165
+ def solve(self):
166
+ raise NotImplementedError('Solve is not implemented')
167
+
168
+
169
+ def plot(self):
170
+ """Plots the hub, shroud, and all the streamlines
171
+ """
172
+ plt.figure(num=1,clear=True,dpi=150,figsize=(15,10))
173
+ plt.plot(self.passage.xhub_pts,self.passage.rhub_pts,label='hub',linestyle='solid',linewidth=2,color='black')
174
+ plt.plot(self.passage.xshroud_pts,self.passage.rshroud_pts,label='shroud',linestyle='solid',linewidth=2,color='black')
175
+
176
+ hub_length = np.sum(np.sqrt(np.diff(self.passage.xhub_pts)**2 + np.diff(self.passage.rhub_pts)**2))
177
+ # Populate the streamlines
178
+ x_streamline = np.zeros((self.num_streamlines,len(self.blade_rows)))
179
+ r_streamline = np.zeros((self.num_streamlines,len(self.blade_rows)))
180
+ for i in range(len(self.blade_rows)):
181
+ x_streamline[:,i] = self.blade_rows[i].x
182
+ r_streamline[:,i] = self.blade_rows[i].r
183
+
184
+ for i in range(1,len(self.blade_rows)-1): # plot dashed lines for each steamline
185
+ plt.plot(x_streamline[:,i],r_streamline[:,i],'--b',linewidth=1.5)
186
+
187
+ for i in range(len(self.blade_rows)):
188
+
189
+ row = self.blade_rows[i]
190
+ plt.plot(row.x,row.r,linestyle='dashed',linewidth=1.5,color='blue',alpha=0.4)
191
+ plt.plot(x_streamline[:,i],r_streamline[:,i],'or')
192
+
193
+ if i == 0:
194
+ pass # Inlet
195
+ else: # i>0
196
+ upstream = self.blade_rows[i-1]
197
+ if upstream.row_type== RowType.Inlet:
198
+ cut_line1,_,_ = self.passage.get_cutting_line((row.axial_location*hub_length +(0.5*row.blade_to_blade_gap*row.axial_chord) - row.axial_chord)/hub_length)
199
+ else:
200
+ cut_line1,_,_ = self.passage.get_cutting_line((upstream.axial_location*hub_length)/hub_length)
201
+ cut_line2,_,_ = self.passage.get_cutting_line((row.axial_location*hub_length-(0.5*row.blade_to_blade_gap*row.axial_chord))/hub_length)
202
+
203
+ if self.blade_rows[i].row_type == RowType.Stator:
204
+ x1,r1 = cut_line1.get_point(np.linspace(0,1,10))
205
+ plt.plot(x1,r1,'m')
206
+ x2,r2 = cut_line2.get_point(np.linspace(0,1,10))
207
+ plt.plot(x2,r2,'m')
208
+ x_text = (x1+x2)/2; r_text = (r1+r2)/2
209
+ plt.text(x_text.mean(),r_text.mean(),"Stator",fontdict={"fontsize":"xx-large"})
210
+ elif self.blade_rows[i].row_type == RowType.Rotor:
211
+ x1,r1 = cut_line1.get_point(np.linspace(0,1,10))
212
+ plt.plot(x1,r1,color='brown')
213
+ x2,r2 = cut_line2.get_point(np.linspace(0,1,10))
214
+ plt.plot(x2,r2,color='brown')
215
+ x_text = (x1+x2)/2; r_text = (r1+r2)/2
216
+ plt.text(x_text.mean(),r_text.mean(),"Rotor",fontdict={"fontsize":"xx-large"})
217
+ plt.axis('scaled')
218
+ plt.savefig(f"Meridional.png",transparent=False,dpi=150)
219
+ plt.show()
220
+
221
+ def plot_velocity_triangles(self):
222
+ """Plots the velocity triangles for each blade row
223
+ Made for turbines
224
+ """
225
+ prop = dict(arrowstyle="-|>,head_width=0.4,head_length=0.8",
226
+ shrinkA=0,shrinkB=0)
227
+
228
+
229
+ for j in range(self.num_streamlines):
230
+ x_start = 0
231
+ y_max = 0; y_min = 0
232
+ plt.figure(num=1,clear=True)
233
+ for i in range(1,len(self.blade_rows)):
234
+ row = self.blade_rows[i]
235
+ x_end = x_start + row.Vx.mean()
236
+ dx = x_end - x_start
237
+
238
+ Vt = row.Vt[j]
239
+ Wt = row.Wt[j]
240
+ U = row.U[j]
241
+
242
+ y_max = (Vt if Vt>y_max else y_max)
243
+ y_max = (Wt if Wt>y_max else y_max)
244
+
245
+ y_min = (Vt if Vt<y_min else y_min)
246
+ y_min = (Wt if Wt<y_min else y_min)
247
+
248
+ # V
249
+ plt.annotate("", xy=(x_end,Vt), xytext=(x_start,0), arrowprops=prop)
250
+ plt.text((x_start+x_end)/2,Vt/2*1.1,"V",fontdict={"fontsize":"xx-large"})
251
+
252
+ # W
253
+ plt.annotate("", xy=(x_end,Wt), xytext=(x_start,0), arrowprops=prop)
254
+ plt.text((x_start+x_end)/2,Wt/2*1.1,"W",fontdict={"fontsize":"xx-large"})
255
+
256
+ if (abs(row.Vt[j]) > abs(row.Wt[j])):
257
+ # Shift Vt to right just a bit so we can see it
258
+ plt.annotate("", xy=(x_end,Wt), xytext=(x_end,0), arrowprops=prop) # Wt
259
+ plt.text(x_end+dx*0.1,Wt/2,"Wt",fontdict={"fontsize":"xx-large"})
260
+
261
+ plt.annotate("", xy=(x_end,U+Wt), xytext=(x_end,Wt), arrowprops=prop) # U
262
+ plt.text(x_end+dx*0.1,(Wt+U)/2,"U",fontdict={"fontsize":"xx-large"})
263
+ else:
264
+ # Shift Wt to right just a bit so we can see it
265
+ plt.annotate("", xy=(x_end,Vt), xytext=(x_end,0), arrowprops=prop) # Vt
266
+ plt.text(x_end+dx*0.1,Vt/2,"Vt",fontdict={"fontsize":"xx-large"})
267
+
268
+ plt.annotate("", xy=(x_end,Wt+U), xytext=(x_end,Wt), arrowprops=prop) # U
269
+ plt.text(x_end+dx*0.1,Wt+U/2,"U",fontdict={"fontsize":"xx-large"})
270
+
271
+ plt.text((x_start+x_end)/2,-y_max*0.95,row.row_type.name,fontdict={"fontsize":"xx-large"})
272
+ x_start += row.Vx[j]
273
+ plt.axis([0,x_end+dx, y_min, y_max])
274
+ plt.title(f"Velocity Triangles for Streamline {j}")
275
+ plt.savefig(f"streamline_{j:04d}.png",transparent=False,dpi=150)
276
+
277
+
278
+
279
+
280
+
281
+ def JSON_to_Spool(filename:str="spool.json"):
282
+ """Reads a JSON file with the properties
283
+ #! Need to instantiate and return spool. Fluid isn't loaded fyi
284
+
285
+ Args:
286
+ filename (str, optional): json filename with properties . Defaults to "spool.json".
287
+ """
288
+ with open(filename, "r") as f:
289
+ data = json.load(f)
turbodesign/stage.py ADDED
@@ -0,0 +1,7 @@
1
+ from dataclasses import dataclass,field
2
+
3
+ @dataclass
4
+ class Stage :
5
+ blade_rows: list = None
6
+ cp: float = None
7
+ streamlines:list = field(default_factory=list)
turbodesign/td_math.py ADDED
@@ -0,0 +1,388 @@
1
+ from typing import List, Tuple
2
+ import numpy as np
3
+ import math
4
+ import numpy.typing as npt
5
+ from .bladerow import BladeRow, compute_gas_constants
6
+ from .enums import RowType, LossType
7
+
8
+ def T0_coolant_weighted_average(row:BladeRow) -> float:
9
+ """Calculate the new weighted Total Temperature array considering coolant
10
+
11
+ Args:
12
+ coolant (Coolant): Coolant
13
+ massflow (np.ndarray): massflow mainstream
14
+
15
+ Returns:
16
+ float: Total Temperature drop
17
+ """
18
+ row.coolant.fluid.TP = row.coolant.T0, row.coolant.P0
19
+ massflow = row.massflow
20
+ total_massflow_no_coolant = row.total_massflow_no_coolant
21
+ Cp = row.Cp
22
+ Cpc = row.coolant.fluid.cp
23
+ T0c = row.coolant.T0
24
+ massflow_coolant = row.coolant.massflow_percentage*total_massflow_no_coolant*row.massflow[1:]/row.massflow[-1]
25
+ if massflow_coolant.mean()>0:
26
+ if row.row_type == RowType.Stator:
27
+ T0= row.T0
28
+ dT0 = T0.copy() * 0
29
+ T0_new = (massflow[1:]*Cp*T0[1:] + massflow_coolant*Cpc*T0c) \
30
+ /(massflow[1:]*Cp + massflow_coolant*Cpc)
31
+ dT0[1:] = T0_new - row.T0[1:]
32
+ dT0[0] = dT0[1]
33
+ else:
34
+ T0R = row.T0R
35
+ T0R_new = T0R.copy()
36
+ T0R_new[1:] = (massflow[1:]*Cp*T0R[1:] + massflow_coolant*Cpc*T0c) \
37
+ /(massflow[1:]*row.fluid.cp + massflow_coolant*Cpc)
38
+ T0R_new[0] = T0R_new[1]
39
+
40
+ T = T0R_new - row.W**2/(2*Cp) # Dont change the velocity triangle but adjust the static temperature
41
+ T0_new = T+row.V**2/(2*Cp) # Use new static temperature to calculate the total temperature
42
+ dT0 = T0_new - row.T0
43
+ return dT0
44
+ else:
45
+ return row.T0*0
46
+
47
+ def compute_massflow(row:BladeRow) -> None:
48
+ """Populates row.massflow and row.calculated_massflow
49
+
50
+ Calculated_massflow is massflow[-1]
51
+
52
+ Args:
53
+ row (BladeRow): current blade row. All quantities are at exit
54
+ upstream (BladeRow): upstream blade row. All quantities are at exit
55
+ """
56
+ massflow_fraction = np.linspace(0,1,len(row.percent_hub_shroud))
57
+ massflow = row.percent_hub_shroud*0
58
+ total_area = 0
59
+ for j in range(1,len(row.percent_hub_shroud)):
60
+ Vm = row.Vm[j]
61
+ rho = row.rho[j]
62
+ if np.abs((row.x[j]-row.x[j-1]))<1E-12: # Axial Machines
63
+ total_area += np.pi*(row.r[j]**2-row.r[j-1]**2)
64
+ massflow[j] = Vm * rho * np.pi* (row.r[j]**2-row.r[j-1]**2) + massflow[j-1]
65
+ else: # Radial Machines
66
+ dx = row.x[j]-row.x[j-1]
67
+ S = (row.r[j]-row.r[j-1])
68
+ C = np.sqrt(1+((row.r[j]-row.r[j-1])/dx)**2)
69
+ area = 2*np.pi*C*(S/2*dx**2+row.r[j-1]*dx)
70
+ total_area += area
71
+ massflow[j] = Vm * rho *area + massflow[j-1]
72
+
73
+ row.total_massflow_no_coolant = massflow[-1]
74
+ if row.coolant != None:
75
+ massflow += massflow_fraction*row.coolant.massflow_percentage*row.total_massflow_no_coolant # Take into account the coolant massflow
76
+ row.massflow = massflow
77
+ row.calculated_massflow = massflow[-1]
78
+ row.area = total_area
79
+
80
+ def compute_power(row:BladeRow,upstream:BladeRow) -> None:
81
+ """Calculates the power
82
+
83
+ Args:
84
+ row (BladeRow): _description_
85
+ upstream (BladeRow): _description_
86
+ """
87
+ if row.row_type == RowType.Stator:
88
+ row.power = 0
89
+ row.eta_static = 0
90
+ row.eta_total = 0
91
+ row.stage_loading = 0
92
+ row.euler_power = 0
93
+ row.T_is = 0
94
+ row.T0_is = 0
95
+ else:
96
+ row.T_is = row.T0R*(row.P/upstream.P0R)**((row.gamma-1)/row.gamma)
97
+ row.T0_is = row.T_is*(1+(row.gamma-1)/2*row.M**2)
98
+ row.power = row.massflow[-1] * row.Cp * (upstream.T0.mean() - row.T0.mean())
99
+ row.eta_static = row.power/ (row.massflow[-1]*row.Cp*(upstream.T0.mean()-row.T0_is.mean()))
100
+ row.eta_total = row.power / (row.massflow[-1]*row.Cp * (upstream.T0.mean()-row.T0_is.mean()))
101
+ row.stage_loading = row.Cp*(upstream.T0.mean() - row.T0.mean())/row.U.mean()**2
102
+ row.euler_power = (row.massflow[-1]*((row.U+upstream.U)/2).mean()*(upstream.Vt-row.Vt).mean())
103
+
104
+ def compute_quantities(row:BladeRow,upstream:BladeRow):
105
+ """Calculation of all quantites after radial equilibrium has been solved assuming we know the static pressure at the exit.
106
+
107
+ Note:
108
+ Radial Equilibrium gives P0, T0, Vm. This code assumes the loss either enthalpy or pressure loss has already been calculated
109
+
110
+ compute_velocity has been called so we know W, Wt, V, Vt, U, M, M_rel
111
+
112
+ Static Pressure and Temperature should come from Total Temperature and Pressure + Velocity
113
+ Args:
114
+ row (BladeRow): current blade row. All quantities are at exit
115
+ upstream (BladeRow): upstream blade row. All quantities are at exit
116
+ """
117
+
118
+ if row.row_type == RowType.Rotor:
119
+ Cp_avg = (row.Cp+upstream.Cp)/2
120
+ # Factor any coolant added and changes in streamline radius
121
+ row.T0R = upstream.T0R - T0_coolant_weighted_average(row) - (upstream.U**2-row.U**2)/(2*Cp_avg)
122
+ row.P = upstream.P0_stator_inlet/row.P0_P
123
+
124
+ if row.loss_function.loss_type == LossType.Pressure:
125
+ # This affects the velocity triangle
126
+ row.P0R = upstream.P0R - row.Yp*(upstream.P0R-row.P)
127
+ row.T = (row.P/row.P0R)**((row.gamma-1)/row.gamma) * row.T0R
128
+ row.T0 = (1+(row.gamma-1)/2 * row.M**2) * row.T
129
+ row.power_distribution = row.massflow * row.Cp * (upstream.T0 - row.T0)
130
+ row.power = np.trapz(row.power_distribution,row.r-row.r[0])
131
+ row.power_mean = row.massflow[-1] * row.Cp * (upstream.T0.mean()-row.T0.mean())
132
+
133
+ elif row.loss_function.loss_type == LossType.Enthalpy:
134
+ ' For Enthalpy related loss, assume the static quantities do not change '
135
+ row.T = (row.P/row.P0R)**((row.gamma-1)/row.gamma) * row.T0R
136
+ row.T0 = (1+(row.gamma-1)/2 * row.M**2) * row.T
137
+
138
+ def calculate_power(T0:npt.NDArray):
139
+ row.power_distribution = row.massflow * row.Cp * (upstream.T0 - T0)
140
+ row.power = np.trapz(row.power_distribution,row.r-row.r[0])
141
+ row.power_mean = row.massflow[-1] * row.Cp * (upstream.T0.mean() - T0.mean())
142
+
143
+ # Factor in T0R_drop. Convert T0R drop to absolute terms
144
+ T_drop = (upstream.T0R - row.T0R) - row.W**2/(2*row.Cp) # row.T0R contains the drop
145
+ T0_drop = T_drop*(1+(row.gamma-1)/2*row.M**2)
146
+
147
+ T0 = upstream.T0 - row.power/row.eta_total/(row.total_massflow*row.Cp) + T0_drop
148
+ return T0
149
+
150
+ T0 = row.T0.copy()
151
+ for _ in range(5): # interate on the convergence of T0_drop
152
+ T0 = calculate_power(T0)
153
+
154
+ row.T0 = T0
155
+ row.T = row.T0-row.W**2/(2*row.Cp)
156
+ row.P0R = row.P * (row.T0R/row.T)**((row.gamma)/(row.gamma-1))
157
+ row.P0 = row.P * (row.T0/row.T)**((row.gamma)/(row.gamma-1))
158
+
159
+ elif row.row_type == RowType.Stator:
160
+ ' For the stator we already assume the upstream P0 already applied '
161
+ if row.loss_function == LossType.Pressure:
162
+ row.P0 = upstream.P0 - row.Yp*(upstream.P0-row.P)
163
+ else:
164
+ row.P0 = upstream.P0
165
+ row.T0 = upstream.T0 - T0_coolant_weighted_average(row)
166
+ row.T = row.T0 * (1+(row.gamma-1)/2*row.M**2)
167
+ row.P = row.P0 * (row.T/row.T0)**((row.gamma)/(row.gamma-1))
168
+ row.T0R = row.T + row.W**2 / (2*row.Cp)
169
+ row.P0R = row.P*(row.T0R/row.T)**((row.gamma)/(row.gamma-1))
170
+
171
+ def compute_quantities_power(row:BladeRow,upstream:BladeRow):
172
+ """Calculation of all quantites after radial equilibrium has been solved assuming we know the power at the exit
173
+
174
+ Note:
175
+ Radial Equilibrium gives P0, T0, Vm. This code assumes the loss either enthalpy or pressure loss has already been calculated
176
+
177
+ compute_velocity has been called so we know W, Wt, V, Vt, U, M, M_rel
178
+
179
+ Static Pressure and Temperature should come from Total Temperature and Pressure + Velocity
180
+
181
+ Args:
182
+ row (BladeRow): current blade row. All quantities are at exit
183
+ upstream (BladeRow): upstream blade row. All quantities are at exit
184
+
185
+ """
186
+ if row.row_type == RowType.Rotor:
187
+ Cp_avg = (row.Cp+upstream.Cp)/2
188
+ row.T0R = upstream.T0R - T0_coolant_weighted_average(row) - (upstream.U**2-row.U**2)/(2*Cp_avg)
189
+
190
+ # Factor in T0R_drop. Convert T0R drop to absolute terms
191
+ T_drop = (upstream.T0R - row.T0R) - row.W**2/(2*row.Cp) # row.T0R contains the drop
192
+ T0_drop = T_drop*(1+(row.gamma-1)/2*row.M**2)
193
+
194
+ # Adjust Total Temperature to match power
195
+ T0 = upstream.T0 - row.power/row.eta_total/(row.total_massflow*row.Cp) + T0_drop
196
+
197
+ if row.loss_function == LossType.Pressure:
198
+ row.P0R = upstream.P0R - row.Yp*(upstream.P0R-row.P)
199
+ row.T0 = T0
200
+ row.T = row.T0/(1+(row.gamma-1)/2*row.M**2)
201
+ row.P = row.P0R*(row.T/row.T0R)**(row.gamma/(row.gamma-1))
202
+
203
+ elif row.loss_function == LossType.Enthalpy:
204
+ row.P0 = row.P*(T0/row.T)**(row.gamma/(row.gamma-1))
205
+ row.T = T0 - row.W**2/(2*row.Cp) + T_drop
206
+ row.T0 = T0
207
+ row.P = row.P0*(row.T0/row.T)**(row.gamma/(row.gamma-1))
208
+ row.P0R = row.P * (row.T0R/row.T)**((row.gamma)/(row.gamma-1))
209
+
210
+ elif row.row_type == RowType.Stator:
211
+ row.T0 = upstream.T0 - T0_coolant_weighted_average(row)
212
+ if row.loss_function == LossType.Pressure:
213
+ row.P0 = upstream.P0 - row.Yp*(upstream.P0-row.P)
214
+ else:
215
+ row.P0 = upstream.P0
216
+ row.T = row.T0 * (1+(row.gamma-1)/2*row.M**2)
217
+ row.P = row.P0 * (row.T/row.T0)**((row.gamma)/(row.gamma-1))
218
+ row.T0R = row.T + row.W**2 / (2*row.Cp)
219
+ row.P0R = row.P*(row.T0R/row.T)**((row.gamma)/(row.gamma-1))
220
+
221
+ def stator_calc(row:BladeRow,upstream:BladeRow,downstream:BladeRow=None,calculate_vm:bool=True):
222
+ """Given P0, T0, P, alpha2 of stator calculate all other quantities
223
+
224
+ Usage:
225
+ Set row.P0 = upstream.P0 - any pressure loss
226
+ row.T0 = upstream.T0 - any cooling
227
+ row.P = row.rp*(row.P0 - rotor.P) + rotor.P
228
+ Set alpha2
229
+
230
+ Args:
231
+ row (BladeRow): Stator Row
232
+ upstream (BladeRow): Stator or Rotor Row
233
+ downstream (BladeRow): Stator or Rotor Row. Defaults to None
234
+
235
+ """
236
+ ## degree of reaction (rp) is assumed
237
+ # downstream.P = upstream.P0 * 1/downstream.P0_P
238
+ # if downstream is not None:
239
+ # # Use the upstream P0 value then later factor in the loss
240
+ # row.P = downstream.rp*(upstream.P0 - downstream.P) + downstream.P
241
+ # else:
242
+ # row.P = upstream.P
243
+
244
+ # Static Pressure is assumed
245
+ row.P0 = upstream.P0 - row.Yp*(upstream.P0-row.P)
246
+ row.P0_P = row.P0/downstream.P
247
+ if downstream is not None:
248
+ row.rp = (row.P-downstream.P)/(upstream.P0-downstream.P)
249
+
250
+ if calculate_vm:
251
+ row.M = ((row.P0/row.P)**((row.gamma-1)/row.gamma) - 1) * 2/(row.gamma-1)
252
+ row.M = np.sqrt(row.M)
253
+ T0_T = (1+(row.gamma-1)/2 * row.M.mean()**2)
254
+ row.T0 = upstream.T0 - T0_coolant_weighted_average(row)
255
+ row.T = row.T0/T0_T
256
+ row.V = row.M*np.sqrt(row.gamma*row.R*row.T)
257
+ VV = row.V*np.cos(row.phi)
258
+ row.Vx = VV*np.cos(row.alpha2)
259
+ row.Vt = VV*np.sin(row.alpha2)
260
+ row.Vr = row.V*np.sin(row.phi)
261
+ row.Vm = np.sqrt(row.Vx**2+row.Vr**2)
262
+ else: # We know Vm, P0, T0
263
+ row.Vx = row.Vm*np.cos(row.phi)
264
+ row.Vr = row.Vm*np.sin(row.phi)
265
+ row.Vt = row.Vm*np.cos(row.phi)*np.tan(row.alpha2)
266
+ row.V = np.sqrt(row.Vx**2 + row.Vr**2 + row.Vt**2)
267
+ for _ in range(3): # Mach is a function of T and T is a function of Mach
268
+ row.M = row.V/np.sqrt(row.gamma*row.R*row.T)
269
+ T0_T = (1+(row.gamma-1)/2 * row.M**2)
270
+ row.P = row.P0 *(1/T0_T)**(row.gamma/(row.gamma-1))
271
+ row.T = row.T0 * (1/T0_T)
272
+
273
+ if upstream.row_type == RowType.Rotor:
274
+ row.alpha1 = upstream.alpha2 # Upstream rotor absolute frame flow angle
275
+ row.beta1 = upstream.beta2
276
+ row.rho = row.P/(row.R*row.T)
277
+ row.U = row.omega*row.r
278
+ row.P0_stator_inlet = upstream.P0
279
+
280
+ def rotor_calc(row:BladeRow,upstream:BladeRow,calculate_vm:bool=True):
281
+ """Calculates quantities given beta2
282
+
283
+ Args:
284
+ row (BladeRow): Rotor Row
285
+ upstream (BladeRow): Stator Row or Rotor Row
286
+ """
287
+ row.P0_stator_inlet = upstream.P0_stator_inlet
288
+ ## P0_P is assumed
289
+ # row.P = row.P0_stator_inlet*1/row.P0_P
290
+
291
+ # Static Pressure is assumed
292
+ row.P0_P = row.P0_stator_inlet/row.P
293
+ upstream_radius = upstream.r
294
+ row.U = row.omega*row.r
295
+ # Upstream Relative Frame Calculations
296
+ upstream.U = upstream.rpm*np.pi/30 * upstream_radius # rad/s
297
+ upstream.Wt = upstream.Vt - upstream.U
298
+ upstream.W = np.sqrt(upstream.Vx**2 + upstream.Wt**2 + upstream.Vr**2)
299
+ upstream.beta2 = np.arctan2(upstream.Wt,upstream.Vx)
300
+ upstream.T0R = upstream.T+upstream.W**2/(2*upstream.Cp)
301
+ upstream.P0R = upstream.P * (upstream.T0R/upstream.T)**((upstream.gamma)/(upstream.gamma-1))
302
+ upstream.M_rel = upstream.W/np.sqrt(upstream.gamma*upstream.R*upstream.T)
303
+
304
+ upstream_rothalpy = upstream.T0R*upstream.Cp - 0.5*upstream.U**2 # H01R - 1/2 U1^2
305
+
306
+ # Rotor Exit Calculations
307
+ row.beta1 = upstream.beta2
308
+ #row.Yp # Evaluated earlier
309
+ row.P0R = upstream.P0R - row.Yp*(upstream.P0R-row.P)
310
+
311
+ # Total Relative Temperature stays constant through the rotor. Adjust for change in radius from rotor inlet to exit
312
+ row.T0R = (upstream_rothalpy + 0.5*row.U**2)/row.Cp - T0_coolant_weighted_average(row)
313
+ P0R_P = row.P0R / row.P
314
+ T0R_T = P0R_P**((row.gamma-1)/row.gamma)
315
+ row.T = (row.T0R/T0R_T) # Exit static temperature
316
+ if calculate_vm: # Calculates the T0 at the exit
317
+ row.W = np.sqrt(2*row.Cp*(row.T0R-row.T)) #! nan popups here a lot for radial machines
318
+ if np.isnan(np.sum(row.W)):
319
+ # Need to adjust T
320
+ print(f'nan detected: check flow path. Turbine inlet cut should be horizontal')
321
+ row.Vr = row.W*np.sin(row.phi)
322
+ row.Wt = np.sqrt(row.W**2 - row.Vr**2) * np.sin(row.beta2)
323
+ row.Vt = row.Wt + row.U
324
+ ww = row.W*np.cos(row.phi)
325
+ row.Vx = ww*np.cos(row.beta2)
326
+ row.V = np.sqrt(row.Vr**2+row.Vt**2+row.Vx**2)
327
+ row.M = row.V/np.sqrt(row.gamma*row.R*row.T)
328
+ row.Vm = np.sqrt(row.Vx**2+row.Vr**2)
329
+ row.T0 = row.T + row.V**2/(2*row.Cp)
330
+ row.P0 = row.P*(row.T0/row.T)**(row.gamma/(row.gamma-1))
331
+ row.alpha2 = np.arctan2(row.Vt,row.Vx)
332
+ else: # We know Vm, P0, T0
333
+ row.Vr = row.Vm*np.sin(row.phi)
334
+ row.Vx = row.Vm*np.cos(row.phi)
335
+
336
+ row.W = np.sqrt(2*row.Cp*(row.T0R-row.T))
337
+ row.Wt = np.sqrt(row.W**2 - row.Vr**2) * np.sin(row.beta2)
338
+ row.U = row.omega * row.r
339
+ row.Vt = row.Wt+row.U
340
+
341
+ row.alpha2 = np.arctan2(row.Vt,row.Vx)
342
+ row.V = np.sqrt(row.Vx**2 + row.Vr**2 + row.Vt**2)
343
+
344
+ row.M = row.V/np.sqrt(row.gamma*row.R*row.T)
345
+ T0_T = (1+(row.gamma-1)/2 * row.M**2)
346
+ row.P0 = row.P * T0_T**(row.gamma/(row.gamma-1))
347
+
348
+ row.M_rel = row.W/np.sqrt(row.gamma*row.R*row.T)
349
+ row.T0 = row.T+row.V**2/(2*row.Cp)
350
+
351
+ T3_is = upstream.T0 * (1/row.P0_P)**((row.gamma-1)/row.gamma)
352
+ a = np.sqrt(row.gamma*row.R*T3_is)
353
+ T03_is = T3_is * (1+(row.gamma-1)/2*(row.V/a)**2)
354
+ row.eta_total = (upstream.T0.mean() - row.T0.mean())/(upstream.T0.mean()-T03_is.mean())
355
+
356
+ def inlet_calc(row:BladeRow):
357
+ """Calculates the conditions for the Inlet
358
+
359
+ Args:
360
+ row (BladeRow): _description_
361
+ """
362
+ area = row.Vm.copy()*0
363
+ row.T = row.T0
364
+ row.P = row.P0
365
+ total_area = 0
366
+ for _ in range(5): # Lets converge the Mach and Total and Static pressures
367
+ for j in range(1,len(row.percent_hub_shroud)):
368
+ rho = row.rho[j]
369
+ tube_massflow = row.massflow[j]-row.massflow[j-1]
370
+ if np.abs((row.x[j]-row.x[j-1]))<1E-12: # Axial Machines
371
+ total_area += np.pi*(row.r[j]**2-row.r[j-1]**2)
372
+ row.Vm[j] = tube_massflow/(rho*np.pi*(row.r[j]**2-row.r[j-1]**2))
373
+ else: # Radial Machines
374
+ dx = row.x[j]-row.x[j-1]
375
+ S = (row.r[j]-row.r[j-1])
376
+ C = np.sqrt(1+((row.r[j]-row.r[j-1])/dx)**2)
377
+ area[j] = 2*np.pi*C*(S/2*dx**2+row.r[j-1]*dx)
378
+ total_area += area[j]
379
+ row.Vm[j] = tube_massflow/(rho*area[j])
380
+ row.Vm[0] = 1/(len(row.Vm)-1)*row.Vm[1:].sum() # Initialize the value at the hub to not upset the mean
381
+ row.Vr = row.Vm*np.sin(row.phi)
382
+ row.Vt = row.Vm*np.cos(row.phi)*np.tan(row.alpha2)
383
+ row.V = np.sqrt(row.Vt**2+row.Vm**2)
384
+
385
+ row.M = row.V/np.sqrt(row.gamma*row.R*row.T)
386
+ row.T = row.T0 * 1/(1+(row.gamma-1)/2*row.M**2)
387
+ row.P = row.P0 * (row.T/row.T0)**(row.gamma/(row.gamma-1))
388
+ compute_gas_constants(row)