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.
@@ -0,0 +1,466 @@
1
+ from typing import List
2
+ from cantera.composite import Solution
3
+ from .bladerow import BladeRow, interpolate_streamline_radii
4
+ from .enums import RowType, MassflowConstraint, LossType, PassageType
5
+ from .spool import Spool
6
+ import json
7
+ from .passage import Passage
8
+ from scipy.interpolate import interp1d
9
+ import numpy as np
10
+ import numpy.typing as npt
11
+ from .td_math import inlet_calc,rotor_calc, stator_calc, compute_massflow, compute_power, compute_gas_constants
12
+ from .solve_radeq import adjust_streamlines, radeq
13
+ from scipy.optimize import minimize_scalar, minimize, fmin_slsqp
14
+
15
+
16
+ class TurbineSpool(Spool):
17
+ def __init__(self,passage:Passage,
18
+ massflow:float,rows:List[BladeRow],
19
+ num_streamlines:int=3,
20
+ fluid:Solution=Solution('air.yaml'),
21
+ rpm:float=-1,
22
+ massflow_constraint:MassflowConstraint=MassflowConstraint.MatchMassFlow):
23
+ """Initializes a Turbine Spool
24
+
25
+ Args:
26
+ passage (Passage): Passage defining hub and shroud
27
+ massflow (float): massflow at spool inlet
28
+ rows (List[BladeRow], optional): List of blade rows. Defaults to List[BladeRow].
29
+ num_streamlines (int, optional): number of streamlines. Defaults to 3.
30
+ fluid (ct.Solution, optional): cantera gas solution. Defaults to ct.Solution('air.yaml').
31
+ rpm (float, optional): RPM for the entire spool Optional, you can also set rpm of the blade rows individually. Defaults to -1.
32
+ massflow_constraint (MassflowConstraint, optional): MatchMassflow - Matches the massflow defined in the spool. BalanceMassflow - Balances the massflow between BladeRows, matches the lowest massflow.
33
+
34
+ """
35
+ super().__init__(passage, massflow, rows,num_streamlines, fluid, rpm)
36
+ self.massflow_constraint = massflow_constraint
37
+ pass
38
+
39
+ def initialize_quantities(self):
40
+ """Initializes the massflow throughout the rows
41
+ """
42
+ # Massflow from inlet already defined
43
+
44
+ # Inlet
45
+ W0 = self.massflow
46
+ inlet = self.blade_rows[0]
47
+ if self.blade_rows[0].row_type == RowType.Inlet:
48
+ self.blade_rows[0].massflow = np.linspace(0,1,self.num_streamlines)*W0
49
+ self.blade_rows[0].total_massflow = W0
50
+ self.blade_rows[0].total_massflow_no_coolant = W0
51
+
52
+ interpolate_streamline_radii(self.blade_rows[0],self.passage)
53
+
54
+ # Set the gas to total values for now
55
+ self.blade_rows[0].fluid.TP = self.blade_rows[0].T0.mean(), self.blade_rows[0].P0.mean()
56
+ self.blade_rows[0].Cp = self.blade_rows[0].fluid.cp
57
+ self.blade_rows[0].Cv = self.blade_rows[0].fluid.cv
58
+ self.blade_rows[0].R = self.blade_rows[0].Cp-self.blade_rows[0].Cv
59
+ self.blade_rows[0].gamma = self.blade_rows[0].Cp/self.blade_rows[0].Cv
60
+ self.blade_rows[0].rho[:] = self.blade_rows[0].fluid.density
61
+ inlet_calc(self.blade_rows[0])
62
+
63
+ for row in self.blade_rows:
64
+ interpolate_streamline_radii(row,self.passage)
65
+
66
+ outlet = self.blade_rows[-1]
67
+ for j in range(self.num_streamlines):
68
+ P0 = inlet.get_total_pressure(inlet.percent_hub_shroud[j])
69
+ percents = np.zeros(shape=(len(self.blade_rows)-2)) + 0.3
70
+ percents[-1] = 1
71
+ Ps_range = outlet_pressure(percents=percents,inletP0=inlet.P0[j],outletP=outlet.P[j])
72
+ for i in range(1,len(self.blade_rows)-1):
73
+ self.blade_rows[i].P[j] = Ps_range[i-1]
74
+
75
+ # Pass T0 and P0 to all the other blade_rows
76
+ for i in range(1,len(self.blade_rows)-1):
77
+ upstream = self.blade_rows[i-1] # Inlet conditions solved before this step
78
+ if i+1<len(self.blade_rows):
79
+ downstream = self.blade_rows[i+1]
80
+ else:
81
+ downstream = None
82
+
83
+ row = self.blade_rows[i]
84
+ if (row.coolant is not None):
85
+ T0c = self.blade_rows[i].coolant.fluid.T
86
+ P0c = self.blade_rows[i].coolant.fluid.P
87
+ W0c = self.blade_rows[i].coolant.massflow_percentage * self.massflow
88
+ Cpc = self.blade_rows[i].coolant.fluid.cp
89
+ else:
90
+ T0c = 100
91
+ P0c = 0
92
+ W0c = 0
93
+ Cpc = 0
94
+
95
+ T0 = upstream.T0
96
+ P0 = upstream.P0
97
+ Cp = upstream.Cp
98
+
99
+ T0 = (W0*Cp*T0 + W0c*Cpc*T0c)/(Cpc * W0c + Cp*W0)
100
+ P0 = (W0*Cp*P0 + W0c*Cpc*P0c)/(Cpc * W0c + Cp*W0)
101
+ Cp = (W0*Cp + W0c*Cpc)/(W0c + W0) # Weighted
102
+
103
+ if row.row_type == RowType.Stator:
104
+ T0 = upstream.T0
105
+ else:
106
+ T0 = upstream.T0 - row.power / (Cp*(W0 + W0c))
107
+
108
+ W0 += W0c
109
+ row.T0 = T0
110
+ row.P0 = P0
111
+ row.Cp = Cp
112
+ row.total_massflow = W0
113
+ row.massflow = np.linspace(0,1,self.num_streamlines)*row.total_massflow
114
+ # Pass Quantities: rho, P0, T0
115
+
116
+ row.rho = upstream.rho
117
+ row.gamma = upstream.gamma
118
+ row.R = upstream.R
119
+
120
+ if row.row_type == RowType.Stator:
121
+ stator_calc(row,upstream,downstream)
122
+ elif row.row_type == RowType.Rotor:
123
+ rotor_calc(row,upstream)
124
+ compute_massflow(row)
125
+ compute_power(row,upstream)
126
+
127
+ def solve(self):
128
+ """
129
+ Solve for the exit flow angles to match the massflow distribution at the stage exit
130
+ """
131
+ self.initialize_streamlines()
132
+ self.initialize_quantities()
133
+
134
+ if self.massflow_constraint ==MassflowConstraint.MatchMassFlow:
135
+ self.__match_massflow()
136
+ elif self.massflow_constraint == MassflowConstraint.BalanceMassFlow:
137
+ self.__balance_massflow()
138
+
139
+
140
+ def __match_massflow(self):
141
+ """ Matches the massflow between streamtubes by changing exit angles. Doesn't use radial equilibrium.
142
+ """
143
+ for _ in range(3):
144
+ # Step 1: Solve a blade row for exit angle to maintain massflow
145
+ for i in range(len(self.blade_rows)):
146
+ row = self.blade_rows[i]
147
+ # Upstream Row
148
+ if i == 0:
149
+ upstream = self.blade_rows[i]
150
+ else:
151
+ upstream = self.blade_rows[i-1]
152
+ if i<len(self.blade_rows)-1:
153
+ downstream = self.blade_rows[i+1]
154
+ else:
155
+ downstream = None
156
+
157
+ if row.row_type == RowType.Stator:
158
+ bounds = [0,80]
159
+ elif row.row_type == RowType.Rotor:
160
+ bounds = [-80,0]
161
+ if row.row_type != RowType.Inlet:
162
+ for j in range(1,self.num_streamlines):
163
+ res = minimize_scalar(massflow_loss_function, bounds=bounds,args=(j,row,upstream,downstream),tol=1E-2)
164
+ if row.row_type == RowType.Rotor:
165
+ row.beta2[j] = np.radians(res.x)
166
+ # Initialize the value at the hub to not upset the mean
167
+ row.beta2[0] = 1/(len(row.beta2)-1)*row.beta2[1:].sum()
168
+ elif row.row_type == RowType.Stator:
169
+ row.alpha2[j] = np.radians(res.x)
170
+ row.alpha2[0] = 1/(len(row.alpha2)-1)*row.alpha2[1:].sum()
171
+ upstream = compute_gas_constants(upstream)
172
+ row = compute_gas_constants(row)
173
+
174
+
175
+ # Step 3: Adjust streamlines to evenly divide massflow
176
+ adjust_streamlines(self.blade_rows,self.passage)
177
+
178
+ def __balance_massflow(self):
179
+ """ Balances the massflow between rows. Use radial equilibrium.
180
+
181
+ Types of stages:
182
+ 1. Stator - Rotor | Stator - Rotor
183
+ 2. Rotor | Stator - Rotor | Stator - Rotor
184
+ 3. Stator - Rotor | CounterRotating | Stator - Rotor
185
+ 4. Rotor-Counter Rotating | Stator - Rotor
186
+ 5. Counter Rotating - Rotor | Stator - Rotor
187
+
188
+ Steps:
189
+ 1. Split the blade rows into stages stator-rotor pairs or rotor rotor or rotor
190
+ 2. Change degree of reaction to match the total massflow
191
+ 3. Adjust the streamlines for each blade row to balance the massflow
192
+ """
193
+
194
+ # Balance the massflow between Stages
195
+ def balance_massflows(x0:List[float],blade_rows:List[List[BladeRow]],P0:npt.NDArray,P:npt.NDArray):
196
+ total_massflow = list(); massflow_stage = list()
197
+ stage_ids = list(set([row.stage_id for row in self.blade_rows if row.stage_id>=0])); s = 0
198
+ sign = 1
199
+ for j in range(self.num_streamlines):
200
+ Ps_range = outlet_pressure(x0,P0[j],P[j])
201
+ for i in range(1,len(blade_rows)-1):
202
+ blade_rows[i].P[j] = Ps_range[i-1]
203
+ blade_rows[-1].P = P
204
+ calculate_massflows(blade_rows,True)
205
+ for row in blade_rows[1:]:
206
+ total_massflow.append(row.total_massflow_no_coolant)
207
+ for s in stage_ids:
208
+ for row in blade_rows:
209
+ if row.stage_id == s and row.row_type == RowType.Rotor:
210
+ massflow_stage.append(sign*row.total_massflow_no_coolant)
211
+ sign*=-1
212
+ if len(stage_ids) % 2 == 1:
213
+ massflow_stage.append(massflow_stage[-1]*sign)
214
+ print(x0)
215
+ return np.std(total_massflow)*2 # + abs(sum(massflow_stage)) # Equation 28
216
+
217
+ # Break apart the rows to stages
218
+ outlet_P=list(); outlet_P_guess = list()
219
+
220
+ for i in range(1,len(self.blade_rows)-2):
221
+ outlet_P.append(self.blade_rows[i].inlet_to_outlet_pratio)
222
+ outlet_P_guess.append(np.mean(self.blade_rows[i].inlet_to_outlet_pratio))
223
+
224
+ if len(outlet_P) == 1:
225
+ res1 = minimize_scalar(fun=balance_massflows,args=(self.blade_rows[:-1],self.blade_rows[0].P0,self.blade_rows[-1].P),
226
+ bounds=outlet_P[0],tol=0.001,options={'disp': True})
227
+ x = res1.x
228
+ else:
229
+ x = fmin_slsqp(func=balance_massflows,args=(self.blade_rows[:-1],self.blade_rows[0].P0,self.blade_rows[-1].P),
230
+ bounds=outlet_P, x0=outlet_P_guess,epsilon=0.001,iter=100) # ,tol=0.001,options={'disp': True})
231
+
232
+ # Adjust the inlet: Set the massflow
233
+ self.blade_rows[0].massflow = np.linspace(0,1,self.num_streamlines)*self.blade_rows[1].total_massflow_no_coolant
234
+ inlet_calc(self.blade_rows[0]) # adjust the inlet to match massflow
235
+ for _ in range(3):
236
+ adjust_streamlines(self.blade_rows[:-1],self.passage)
237
+ self.blade_rows[-1].transfer_quantities(self.blade_rows[-2])
238
+ self.blade_rows[-1].P = self.blade_rows[-1].get_static_pressure(self.blade_rows[-1].percent_hub_shroud)
239
+ err = balance_massflows(x,self.blade_rows[:-1],self.blade_rows[0].P0,self.blade_rows[-1].P)
240
+ if err>5E-2:
241
+ print(f"Massflow is not convergenced error:{err}")
242
+ else:
243
+ print(f"Massflow converged to less than 0.05kg/s error:{err}")
244
+
245
+ def export_properties(self,filename:str="turbine_spool.json"):
246
+ """Export the spool object to json
247
+
248
+ Args:
249
+ filename (str, optional): name of export file. Defaults to "spool.json".
250
+ """
251
+ blade_rows = list()
252
+ degree_of_reaction = list()
253
+ total_total_efficiency = list()
254
+ total_static_efficiency = list()
255
+ stage_loading = list()
256
+ euler_power = list()
257
+
258
+ x_streamline = np.zeros((self.num_streamlines,len(self.blade_rows)))
259
+ r_streamline = np.zeros((self.num_streamlines,len(self.blade_rows)))
260
+ massflow = list()
261
+ for indx,row in enumerate(self.blade_rows):
262
+ blade_rows.append(row.to_dict()) # Appending data
263
+ if row.row_type == RowType.Rotor:
264
+ # Calculation for these are specific to Turbines
265
+ degree_of_reaction.append(((self.blade_rows[indx-1].P- row.P)/(self.blade_rows[indx-2].P-row.P)).mean())
266
+
267
+ total_total_efficiency.append(row.eta_total)
268
+ total_static_efficiency.append(row.eta_static)
269
+
270
+ stage_loading.append(row.stage_loading)
271
+ euler_power.append(row.euler_power)
272
+
273
+ if row.row_type!=RowType.Inlet and row.row_type!=RowType.Outlet:
274
+ massflow.append(row.massflow[-1])
275
+
276
+ for j,p in enumerate(row.percent_hub_shroud):
277
+ t,x,r = self.passage.get_streamline(p)
278
+ x_streamline[j,indx] = float(interp1d(t,x)(row.axial_location))
279
+ r_streamline[j,indx] = float(interp1d(t,r)(row.axial_location))
280
+
281
+ data = {
282
+ "blade_rows": blade_rows,
283
+ "massflow":np.mean(massflow),
284
+ "rpm":self.rpm,
285
+ "r_streamline":r_streamline.tolist(),
286
+ "x_streamline":x_streamline.tolist(),
287
+ "rhub":self.passage.rhub_pts.tolist(),
288
+ "rshroud":self.passage.rshroud_pts.tolist(),
289
+ "xhub":self.passage.xhub_pts.tolist(),
290
+ "xshroud":self.passage.xshroud_pts.tolist(),
291
+ "num_streamlines":self.num_streamlines,
292
+ "total-total_efficiency":total_total_efficiency,
293
+ "total-static_efficiency":total_static_efficiency,
294
+ "stage_loading":stage_loading,
295
+ "degree_of_reaction":degree_of_reaction
296
+ }
297
+ # Dump all the Python objects into a single JSON file.
298
+ class NumpyEncoder(json.JSONEncoder):
299
+ def default(self, obj):
300
+ if isinstance(obj, np.ndarray):
301
+ return obj.tolist()
302
+ return super().default(obj)
303
+
304
+ with open(filename, "w") as f:
305
+ json.dump(data, f, indent=4,cls=NumpyEncoder)
306
+
307
+
308
+ def calculate_massflows(blade_rows:List[BladeRow],calculate_vm:bool=False):
309
+ """Calculates the massflow
310
+
311
+ Args:
312
+ blade_rows (List[BladeRow]): _description_
313
+ passage (Passage): _description_
314
+ calculate_vm (bool, optional): _description_. Defaults to False.
315
+ """
316
+ for p in range(3):
317
+ for i in range(1,len(blade_rows)):
318
+ row = blade_rows[i]
319
+ # Upstream Row
320
+ if i == 0:
321
+ upstream = blade_rows[i]
322
+ else:
323
+ upstream = blade_rows[i-1]
324
+ if i<len(blade_rows)-1:
325
+ downstream = blade_rows[i+1]
326
+ else:
327
+ downstream = None
328
+
329
+ # Pressure loss = shift in entropy which affects the total pressure of the row
330
+ if row.row_type == RowType.Inlet:
331
+ row.Yp = 0
332
+ else:
333
+ if row.loss_function.loss_type == LossType.Pressure:
334
+ row.Yp = row.loss_function(row,upstream)
335
+ for _ in range(2):
336
+ if row.row_type == RowType.Rotor:
337
+ rotor_calc(row,upstream,calculate_vm=True)
338
+ # Finds Equilibrium between Vm, P0, T0
339
+ row = radeq(row,upstream)
340
+ row = compute_gas_constants(row)
341
+ rotor_calc(row,upstream,calculate_vm=False)
342
+ elif row.row_type == RowType.Stator:
343
+ stator_calc(row,upstream,downstream,calculate_vm=True)
344
+ # Finds Equilibrium between Vm, P0, T0
345
+ row = radeq(row,upstream)
346
+ row = compute_gas_constants(row)
347
+ stator_calc(row,upstream,downstream,calculate_vm=False)
348
+ row = compute_gas_constants(row)
349
+ compute_massflow(row)
350
+ compute_power(row,upstream)
351
+
352
+ elif row.loss_function.loss_type == LossType.Enthalpy:
353
+ if row.row_type == RowType.Rotor:
354
+ row.Yp = 0
355
+ rotor_calc(row,upstream,calculate_vm=calculate_vm)
356
+ eta_total = float(row.loss_function(row,upstream))
357
+ def find_yp(Yp,row,upstream):
358
+ row.Yp = Yp
359
+ rotor_calc(row,upstream,calculate_vm=True)
360
+ row = radeq(row,upstream)
361
+ row = compute_gas_constants(row)
362
+ rotor_calc(row,upstream,calculate_vm=False)
363
+ return abs(row.eta_total - eta_total)
364
+
365
+ res = minimize_scalar(find_yp,bounds=[0,0.6],args=(row,upstream))
366
+ row.Yp = res.x
367
+ elif row.row_type == RowType.Stator:
368
+ row.Yp = 0
369
+ stator_calc(row,upstream,downstream,calculate_vm=True)
370
+ row = radeq(row,upstream)
371
+ row = compute_gas_constants(row)
372
+ stator_calc(row,upstream,downstream,calculate_vm=False)
373
+ row = compute_gas_constants(row)
374
+ compute_massflow(row)
375
+ compute_power(row,upstream)
376
+
377
+ def massflow_loss_function(exit_angle:float,index:int,row:BladeRow,upstream:BladeRow,downstream:BladeRow=None):
378
+ """Finds the blade exit angles that balance the massflow throughout the stage
379
+
380
+ Args:
381
+ exit_angle (float): exit flow angle of the rotor row
382
+ index (int): streamline index for the current row
383
+ row (BladeRow): current blade row
384
+ upstream (BladeRow): upstream blade row
385
+ downstream (BladeRow): downstream blade row
386
+
387
+ Returns:
388
+ float: massflow loss
389
+ """
390
+ # Pressure loss = shift in entropy which affects the total pressure of the row
391
+ if row.row_type == RowType.Inlet:
392
+ row.Yp = 0
393
+ else:
394
+ if row.loss_function.loss_type == LossType.Pressure:
395
+ row.Yp = row.loss_function(row,upstream)
396
+ if row.row_type == RowType.Rotor:
397
+ row.beta2[index] = np.radians(exit_angle)
398
+ rotor_calc(row,upstream)
399
+ elif row.row_type == RowType.Stator:
400
+ row.alpha2[index] = np.radians(exit_angle)
401
+ stator_calc(row,upstream,downstream)
402
+ upstream = compute_gas_constants(upstream)
403
+ row = compute_gas_constants(row)
404
+ elif row.loss_function.loss_type == LossType.Enthalpy:
405
+ # Search for pressure loss that results in the correct total temperature drop
406
+ if row.row_type == RowType.Rotor:
407
+ row.Yp = 0
408
+ row.beta2[index] = np.radians(exit_angle)
409
+ rotor_calc(row,upstream)
410
+ T0_drop = row.loss_function(row,upstream)
411
+ T0_target = row.T0.mean()-T0_drop
412
+ def find_yp(Yp):
413
+ row.Yp = Yp
414
+ rotor_calc(row,upstream)
415
+ upstream = compute_gas_constants(upstream)
416
+ row = compute_gas_constants(row)
417
+ return abs(row.T0.mean() - T0_target)
418
+ res = minimize_scalar(find_yp,bounds=[0,0.6])
419
+ row.Yp = res.x
420
+ elif row.row_type == RowType.Stator:
421
+ row.Yp = 0
422
+ row.alpha2[index] = np.radians(exit_angle)
423
+ stator_calc(row,upstream,downstream)
424
+ upstream = compute_gas_constants(upstream)
425
+ row = compute_gas_constants(row)
426
+
427
+
428
+ # if use_radeq:
429
+ # row = radeq(row,upstream) # Finds Equilibrium between Vm, P0, T0
430
+
431
+ compute_massflow(row)
432
+ compute_power(row,upstream)
433
+
434
+ if row.row_type!=RowType.Inlet:
435
+ if row.row_type == RowType.Rotor:
436
+ T3_is = upstream.T0 * (1/row.P0_P)**((row.gamma-1)/row.gamma)
437
+ else:
438
+ T3_is = upstream.T0 * (row.P0/row.P)**((row.gamma-1)/row.gamma)
439
+ a = np.sqrt(row.gamma*row.R*T3_is)
440
+ T03_is = T3_is * (1+(row.gamma-1)/2*(row.Vm/a)**2)
441
+ row.eta_total = (upstream.T0.mean() - row.T0.mean())/(upstream.T0.mean()-T03_is.mean())
442
+
443
+ return np.abs(row.total_massflow*index/(len(row.massflow)-1) - row.massflow[index])
444
+
445
+ def outlet_pressure(percents:List[float],inletP0:float,outletP:float) -> npt.NDArray:
446
+ """Given a list of percents from 0 to 1 for each row, output each row's outlet static pressure
447
+
448
+ Args:
449
+ percents (List[float]): List of floats as percents [[0 to 1],[0 to 1]]
450
+ inletP0 (float): Inlet Total Pressure
451
+ outletP (float): Outlet Static Pressure
452
+
453
+ Returns:
454
+ npt.NDArray: Array of static pressures
455
+ """
456
+ maxP = inletP0
457
+ minP = outletP
458
+ if isinstance(percents, float):
459
+ Ps = [percents*(minP-maxP)+maxP]
460
+ else:
461
+ Ps = np.zeros(shape=(len(percents),1)); i = 0
462
+ for p in percents:
463
+ Ps[i] = p*(minP-maxP)+maxP
464
+ maxP = Ps[i]
465
+ i+=1
466
+ return Ps