turbigen 2.0.0__cp313-cp313-musllinux_1_2_x86_64.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.
- embsolvec.cpython-313-x86_64-linux-musl.so +0 -0
- turbigen/__init__.py +6 -0
- turbigen/annulus.py +668 -0
- turbigen/autogrid/__init__.py +2 -0
- turbigen/autogrid/ag_server.sh +178 -0
- turbigen/autogrid/autogrid.py +599 -0
- turbigen/autogrid/reader.py +163 -0
- turbigen/autogrid/script_ag.py2 +336 -0
- turbigen/autogrid/script_igg.py2 +44 -0
- turbigen/autogrid/script_sh +28 -0
- turbigen/autogrid/server.py +72 -0
- turbigen/average.py +509 -0
- turbigen/base.py +2253 -0
- turbigen/blade.py +277 -0
- turbigen/camber.py +186 -0
- turbigen/clusterfunc/__init__.py +3 -0
- turbigen/clusterfunc/check.py +107 -0
- turbigen/clusterfunc/double.py +248 -0
- turbigen/clusterfunc/exceptions.py +5 -0
- turbigen/clusterfunc/plot.py +41 -0
- turbigen/clusterfunc/single.py +271 -0
- turbigen/clusterfunc/symmetric.py +63 -0
- turbigen/clusterfunc/util.py +18 -0
- turbigen/compflow_native.py +268 -0
- turbigen/config.py +572 -0
- turbigen/config2.py +775 -0
- turbigen/exceptions.py +6 -0
- turbigen/flowfield.py +120 -0
- turbigen/fluid.py +789 -0
- turbigen/geometry.py +952 -0
- turbigen/grid.py +2004 -0
- turbigen/hmesh.py +852 -0
- turbigen/inlet.py +89 -0
- turbigen/iterators.py +360 -0
- turbigen/main.py +376 -0
- turbigen/marching_cubes.py +676 -0
- turbigen/meanline.py +461 -0
- turbigen/mesh.py +43 -0
- turbigen/nblade.py +83 -0
- turbigen/ohmesh.py +254 -0
- turbigen/polynomial.py +650 -0
- turbigen/post/blade_surf_dist.py +153 -0
- turbigen/post/calc_surf_dissipation.py +137 -0
- turbigen/post/check_phase.py +26 -0
- turbigen/post/contour.py +280 -0
- turbigen/post/find_extrema.py +22 -0
- turbigen/post/plot_annulus.py +95 -0
- turbigen/post/plot_blade.py +39 -0
- turbigen/post/plot_camber.py +63 -0
- turbigen/post/plot_convergence.py +122 -0
- turbigen/post/plot_incidence.py +33 -0
- turbigen/post/plot_isen_mach.py +148 -0
- turbigen/post/plot_nose.py +165 -0
- turbigen/post/plot_pressure_distributions.py +232 -0
- turbigen/post/plot_section.py +166 -0
- turbigen/post/plot_thickness.py +50 -0
- turbigen/post/spanwise.py +146 -0
- turbigen/post/write_annulus.py +28 -0
- turbigen/post/write_cuts.py +66 -0
- turbigen/post/write_ibl.py +134 -0
- turbigen/post/write_stl.py +161 -0
- turbigen/post.py +485 -0
- turbigen/run.py +1443 -0
- turbigen/run2.py +65 -0
- turbigen/slurm.py +261 -0
- turbigen/solver.py +123 -0
- turbigen/solvers/base.py +34 -0
- turbigen/solvers/convert_ts3_to_ts4_native.py +101 -0
- turbigen/solvers/emb.py +1528 -0
- turbigen/solvers/plot3d.py +25 -0
- turbigen/solvers/ts3.py +1518 -0
- turbigen/solvers/ts4.py +880 -0
- turbigen/tables.py +508 -0
- turbigen/thickness.py +527 -0
- turbigen/util.py +1532 -0
- turbigen/util_post.py +180 -0
- turbigen/vtri.py +66 -0
- turbigen/yaml.py +117 -0
- turbigen-2.0.0.dist-info/METADATA +34 -0
- turbigen-2.0.0.dist-info/RECORD +82 -0
- turbigen-2.0.0.dist-info/WHEEL +5 -0
- turbigen-2.0.0.dist-info/entry_points.txt +4 -0
|
Binary file
|
turbigen/__init__.py
ADDED
turbigen/annulus.py
ADDED
|
@@ -0,0 +1,668 @@
|
|
|
1
|
+
r"""Classes to calculate meridional coordinates of an axisymmetric annulus.
|
|
2
|
+
|
|
3
|
+
The purpose of these objects is to evaluate x/r coordinates over a turbomachine
|
|
4
|
+
annulus as a function of spanwise and streamwise location.
|
|
5
|
+
|
|
6
|
+
To make a new annulus, subclass the BaseAnnulus and implement:
|
|
7
|
+
- __init__(self, rmid, span, Beta, [... your choice of design variables])
|
|
8
|
+
- evaluate_xr(m, spf)
|
|
9
|
+
- nrow
|
|
10
|
+
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from turbigen import util
|
|
14
|
+
from turbigen.geometry import MeridionalLine
|
|
15
|
+
from scipy.optimize import minimize, root_scalar
|
|
16
|
+
import scipy.interpolate
|
|
17
|
+
from abc import ABC, abstractmethod
|
|
18
|
+
|
|
19
|
+
import numpy as np
|
|
20
|
+
|
|
21
|
+
logger = util.make_logger()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class AnnulusDesigner(util.BaseDesigner):
|
|
25
|
+
"""Base class defining the interface for an annulus."""
|
|
26
|
+
|
|
27
|
+
_supplied_design_vars = ("rmid", "span", "Beta")
|
|
28
|
+
|
|
29
|
+
@abstractmethod
|
|
30
|
+
def forward(self, rmid, span, Beta, *args, **kwargs):
|
|
31
|
+
"""Set the coordinates of the mean line of the annulus.
|
|
32
|
+
|
|
33
|
+
Do whatever is necessary to set up the annulus geometry, such as
|
|
34
|
+
calculating the hub and casing lines.
|
|
35
|
+
|
|
36
|
+
Parameters
|
|
37
|
+
----------
|
|
38
|
+
rmid : (nrow*2) array
|
|
39
|
+
Mid-span radii at inlet and exit of all rows.
|
|
40
|
+
span : (nrow*2) array
|
|
41
|
+
Annulus span perpendicular to pitch angle at all stations.
|
|
42
|
+
Beta : (nrow*2) array
|
|
43
|
+
Pitch angles at all stations [deg].
|
|
44
|
+
"""
|
|
45
|
+
raise NotImplementedError
|
|
46
|
+
|
|
47
|
+
@abstractmethod
|
|
48
|
+
def evaluate_xr(self, m, spf) -> np.ndarray:
|
|
49
|
+
"""Get meridional coordinates within the annulus.
|
|
50
|
+
|
|
51
|
+
The input non-dimensional coordinates must be broadcastable
|
|
52
|
+
to the same shape.
|
|
53
|
+
|
|
54
|
+
Parameters
|
|
55
|
+
----------
|
|
56
|
+
m: array_like
|
|
57
|
+
Normalised meridional distance, where 0 is the inlet,
|
|
58
|
+
1 is the first row LE, 2 is the first row TE, etc.
|
|
59
|
+
spf : array_like
|
|
60
|
+
Span fraction, where 0 is the hub and 1 is the casing.
|
|
61
|
+
|
|
62
|
+
Returns
|
|
63
|
+
-------
|
|
64
|
+
xr : array_like (2, ...)
|
|
65
|
+
Meridional coordinates of the requested points in the annulus.
|
|
66
|
+
First axis is x or r coordinates, remaining axes are broadcasted
|
|
67
|
+
shape of mnorm and spf.
|
|
68
|
+
|
|
69
|
+
"""
|
|
70
|
+
raise NotImplementedError
|
|
71
|
+
|
|
72
|
+
def setup_annulus(self, mean_line):
|
|
73
|
+
"""Setup the annulus using coordinates from a mean line."""
|
|
74
|
+
self.forward(mean_line.rmid, mean_line.span, mean_line.Beta, **self.design_vars)
|
|
75
|
+
|
|
76
|
+
@property
|
|
77
|
+
@abstractmethod
|
|
78
|
+
def nrow(self) -> int:
|
|
79
|
+
"""Number of blade rows in this annulus."""
|
|
80
|
+
raise NotImplementedError
|
|
81
|
+
|
|
82
|
+
@property
|
|
83
|
+
def nseg(self) -> int:
|
|
84
|
+
"""Number of segments in this annulus, 2*nrow + 1."""
|
|
85
|
+
return 2 * self.nrow + 1
|
|
86
|
+
|
|
87
|
+
@property
|
|
88
|
+
def mmax(self):
|
|
89
|
+
"""Maximum value of normalised meridional coordinate."""
|
|
90
|
+
return float(self.nseg)
|
|
91
|
+
|
|
92
|
+
def chords(self, spf):
|
|
93
|
+
"""Meridional chords of rows and row gaps.
|
|
94
|
+
|
|
95
|
+
Parameters
|
|
96
|
+
----------
|
|
97
|
+
spf : float
|
|
98
|
+
Span fraction at which to evaluate the chords.
|
|
99
|
+
0 is the hub, 1 is the casing.
|
|
100
|
+
|
|
101
|
+
Returns
|
|
102
|
+
-------
|
|
103
|
+
cm: (nseg-1) array
|
|
104
|
+
Meridional chord lengths of all segments.
|
|
105
|
+
|
|
106
|
+
"""
|
|
107
|
+
|
|
108
|
+
# Preallocate chords
|
|
109
|
+
nchord = self.nseg
|
|
110
|
+
chords = np.zeros(nchord)
|
|
111
|
+
|
|
112
|
+
# Loop over all chords
|
|
113
|
+
for i in range(nchord):
|
|
114
|
+
# Evaluate meridional coordinates and integrate arc length
|
|
115
|
+
mq = np.linspace(i, i + 1, 100)
|
|
116
|
+
chords[i] = util.arc_length(self.evaluate_xr(mq, spf))
|
|
117
|
+
|
|
118
|
+
return chords
|
|
119
|
+
|
|
120
|
+
def get_interfaces(self):
|
|
121
|
+
"""Meridional coordinates of row interfaces.
|
|
122
|
+
|
|
123
|
+
Returns
|
|
124
|
+
-------
|
|
125
|
+
xr_interfaces : array_like (nrow-1, 2, 2)
|
|
126
|
+
Meridional coordinates of the interfaces between blade rows.
|
|
127
|
+
First axis is row index, second axis is x/r, third axis is
|
|
128
|
+
the hub or casing.
|
|
129
|
+
|
|
130
|
+
"""
|
|
131
|
+
mq = np.arange(2.5, (self.nrow + 1.5), 2.0).reshape(-1, 1)
|
|
132
|
+
return self.get_cut_plane(mq)
|
|
133
|
+
|
|
134
|
+
def get_cut_plane(self, m):
|
|
135
|
+
"""Coordinates on the hub and casing at a constant meridional posistion.
|
|
136
|
+
|
|
137
|
+
Parameters
|
|
138
|
+
----------
|
|
139
|
+
m: (n,) array
|
|
140
|
+
Normalised meridional positions to evaluate the cut planes.
|
|
141
|
+
|
|
142
|
+
Returns
|
|
143
|
+
-------
|
|
144
|
+
xr_cut : array_like (n, 2, 2)
|
|
145
|
+
Meridional coordinates of the cut planes.
|
|
146
|
+
Axes are: cut index, x/r, hub/cas.
|
|
147
|
+
|
|
148
|
+
"""
|
|
149
|
+
spf = np.reshape([0.0, 1.0], (1, -1))
|
|
150
|
+
mq = np.reshape(m, (-1, 1))
|
|
151
|
+
return self.evaluate_xr(mq, spf).transpose(1, 0, 2)
|
|
152
|
+
|
|
153
|
+
def get_offset_planes(self, offset):
|
|
154
|
+
"""Meridional cut lines offset a distance up/downstream of each row.
|
|
155
|
+
|
|
156
|
+
Parameters
|
|
157
|
+
----------
|
|
158
|
+
offset : array_like
|
|
159
|
+
Offset of the cut planes upstream from the LE and downstream
|
|
160
|
+
from the TE of each row. Defaults to 2% meridional chord.
|
|
161
|
+
|
|
162
|
+
Returns
|
|
163
|
+
-------
|
|
164
|
+
xr_cut : array_like (2*nrow, 2, 2)
|
|
165
|
+
Meridional coordinates of the cut planes. Axes are: row, x/r, hub/cas.
|
|
166
|
+
"""
|
|
167
|
+
|
|
168
|
+
# Separate out row and gap chords, repeat for LE/TE cuts
|
|
169
|
+
chords = self.chords(0.5)
|
|
170
|
+
chords_blade = np.repeat(chords[1::2], 2)
|
|
171
|
+
chords_gap = chords[::2]
|
|
172
|
+
chords_gap = np.concatenate(
|
|
173
|
+
[[chords_gap[0]], np.repeat(chords_gap[1:-1], 2), [chords_gap[-1]]]
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
# Convert the offset specified as a fraction of blade chord
|
|
177
|
+
# to a fraction of gap chord
|
|
178
|
+
offsets = offset * np.ones((self.nrow * 2,))
|
|
179
|
+
offsets[::2] *= -1.0
|
|
180
|
+
offsets *= chords_blade / chords_gap
|
|
181
|
+
|
|
182
|
+
# Calculate query meridional coordinates
|
|
183
|
+
mq = np.arange(1.0, self.nseg) + offsets
|
|
184
|
+
return self.get_cut_plane(mq)
|
|
185
|
+
|
|
186
|
+
def get_coords(self, npts=50):
|
|
187
|
+
"""Sample the coordinates of hub and casing lines in AutoGrid format.
|
|
188
|
+
|
|
189
|
+
Parameters
|
|
190
|
+
----------
|
|
191
|
+
npts : int
|
|
192
|
+
Number of sampled points per blade and gap.
|
|
193
|
+
|
|
194
|
+
Returns
|
|
195
|
+
-------
|
|
196
|
+
xr: (2, nrow*npts, 2)
|
|
197
|
+
Coordinates of the annulus lines. Axes are: hub/cas, streamwise, x/r.
|
|
198
|
+
|
|
199
|
+
"""
|
|
200
|
+
m = np.linspace(0.0, self.mmax, self.nseg * npts + 1)
|
|
201
|
+
return self.get_cut_plane(m).transpose(2, 0, 1)
|
|
202
|
+
|
|
203
|
+
def get_span(self, m):
|
|
204
|
+
"""Span of the annulus at a given meridional position.
|
|
205
|
+
|
|
206
|
+
Parameters
|
|
207
|
+
----------
|
|
208
|
+
m : (n,) array
|
|
209
|
+
Normalised meridional positions to evaluate the span.
|
|
210
|
+
|
|
211
|
+
Returns
|
|
212
|
+
-------
|
|
213
|
+
span : (n,) array
|
|
214
|
+
Span of the annulus at the given meridional position.
|
|
215
|
+
|
|
216
|
+
"""
|
|
217
|
+
xr_span = self.get_cut_plane(m).transpose(1, 2, 0)
|
|
218
|
+
return util.arc_length(xr_span)
|
|
219
|
+
|
|
220
|
+
def get_span_curve(self, spf, n=201, mlim=None):
|
|
221
|
+
"""Meridional xr curve along a given span fraction.
|
|
222
|
+
|
|
223
|
+
Parameters
|
|
224
|
+
----------
|
|
225
|
+
spf : float
|
|
226
|
+
Span fraction at which to evaluate the curve.
|
|
227
|
+
0 is the hub, 1 is the casing.
|
|
228
|
+
n : int
|
|
229
|
+
Number of streamwise points to evaluate.
|
|
230
|
+
mlim : tuple
|
|
231
|
+
Normalised meridional limits to evaluate the curve.
|
|
232
|
+
Defaults to the entire annulus.
|
|
233
|
+
|
|
234
|
+
Returns
|
|
235
|
+
-------
|
|
236
|
+
xr : array_like (2, n)
|
|
237
|
+
Meridional coordinates of the curve. Axes are: x/r, streamwise.
|
|
238
|
+
|
|
239
|
+
"""
|
|
240
|
+
util.check_scalar(spf=spf)
|
|
241
|
+
if mlim is None:
|
|
242
|
+
mlim = (0.0, self.mmax)
|
|
243
|
+
m_ref = np.linspace(*mlim, n)
|
|
244
|
+
return self.evaluate_xr(m_ref, spf).squeeze()
|
|
245
|
+
|
|
246
|
+
def xr_row(self, irow):
|
|
247
|
+
"""Make a meridional interpolator restricted to one blade row.
|
|
248
|
+
|
|
249
|
+
Parameters
|
|
250
|
+
----------
|
|
251
|
+
irow : int
|
|
252
|
+
Index of the blade row to evaluate.
|
|
253
|
+
|
|
254
|
+
Returns
|
|
255
|
+
-------
|
|
256
|
+
xr_row : callable
|
|
257
|
+
Function to evaluate meridional coordinates inside the blade row
|
|
258
|
+
Has signature xr_row(spf, m) where
|
|
259
|
+
- m=0 is the row LE, m=1 is the row TE;
|
|
260
|
+
- spf=0 is the hub, spf=1 is the casing.
|
|
261
|
+
|
|
262
|
+
"""
|
|
263
|
+
|
|
264
|
+
mst = 2 * irow + 1
|
|
265
|
+
|
|
266
|
+
def func(spf, m):
|
|
267
|
+
return self.evaluate_xr(mst + m, spf)
|
|
268
|
+
|
|
269
|
+
return func
|
|
270
|
+
|
|
271
|
+
def get_mp_from_xr(self, spf, n=4999, mlim=None):
|
|
272
|
+
"""Return a function to find 1D unwrapped distance from a 2D xr point.
|
|
273
|
+
|
|
274
|
+
Parameters
|
|
275
|
+
----------
|
|
276
|
+
spf : float
|
|
277
|
+
Span fraction at which to evaluate the curve.
|
|
278
|
+
0 is the hub, 1 is the casing.
|
|
279
|
+
n : int
|
|
280
|
+
Number of streamwise points to evaluate.
|
|
281
|
+
mlim : tuple
|
|
282
|
+
Normalised meridional limits to evaluate the curve.
|
|
283
|
+
Defaults to the entire annulus.
|
|
284
|
+
|
|
285
|
+
Returns
|
|
286
|
+
-------
|
|
287
|
+
mp_from_xr : callable
|
|
288
|
+
Function to evaluate the unwrapped meridional distance from a
|
|
289
|
+
2D point in the meridional plane. Has signature mp_from_xr(xr)
|
|
290
|
+
where xr is a (2,n) array of x/r coordinates.
|
|
291
|
+
|
|
292
|
+
"""
|
|
293
|
+
# We want to plot along a general meridional surface
|
|
294
|
+
# So brute force a mapping from x/r to meridional distance
|
|
295
|
+
|
|
296
|
+
xr_ref = self.get_span_curve(spf, n, mlim)
|
|
297
|
+
|
|
298
|
+
# Calculate normalised meridional distance (angles are angles)
|
|
299
|
+
dxr = np.diff(xr_ref, n=1, axis=1)
|
|
300
|
+
dm = np.sqrt(np.sum(dxr**2.0, axis=0))
|
|
301
|
+
rc = 0.5 * (xr_ref[1, 1:] + xr_ref[1, :-1])
|
|
302
|
+
mp_ref = util.cumsum0(dm / rc)
|
|
303
|
+
assert (np.diff(mp_ref) > 0.0).all()
|
|
304
|
+
|
|
305
|
+
func = scipy.interpolate.NearestNDInterpolator(xr_ref.T, mp_ref)
|
|
306
|
+
|
|
307
|
+
def mp_from_xr(xr):
|
|
308
|
+
xru = xr.reshape(2, -1)
|
|
309
|
+
mpu = func(xru.T) # % - mp_stack
|
|
310
|
+
return mpu.reshape(xr.shape[1:])
|
|
311
|
+
|
|
312
|
+
return mp_from_xr
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
class FixedAxialChord(AnnulusDesigner):
|
|
316
|
+
def forward(
|
|
317
|
+
self,
|
|
318
|
+
rmid,
|
|
319
|
+
span,
|
|
320
|
+
Beta,
|
|
321
|
+
cx_row,
|
|
322
|
+
cx_gap,
|
|
323
|
+
nozzle_ratio=1.0,
|
|
324
|
+
):
|
|
325
|
+
# Check input data
|
|
326
|
+
npt = len(rmid)
|
|
327
|
+
nrow = npt // 2
|
|
328
|
+
ngap = nrow + 1
|
|
329
|
+
util.check_vector((npt,), rmid=rmid, span=span, Beta=Beta)
|
|
330
|
+
util.check_vector((nrow,), cx_row=cx_row)
|
|
331
|
+
util.check_vector((ngap,), cx_gap=cx_gap)
|
|
332
|
+
|
|
333
|
+
self.span = span
|
|
334
|
+
|
|
335
|
+
# Assemble vector of all cx
|
|
336
|
+
cx = np.zeros(nrow * 2 + 1)
|
|
337
|
+
cx[::2] = cx_gap
|
|
338
|
+
cx[1::2] = cx_row
|
|
339
|
+
|
|
340
|
+
# Integrate x
|
|
341
|
+
xmid = util.cumsum0(cx)
|
|
342
|
+
xmid -= xmid[1] # Place x origin at first row LE
|
|
343
|
+
|
|
344
|
+
# Extend r coords for inlet and exit ducts at constant Beta
|
|
345
|
+
rmid = np.r_[0.0, rmid, 0.0]
|
|
346
|
+
sinBeta = np.sin(np.radians(Beta))
|
|
347
|
+
rmid[0] = rmid[1] - cx[0] * sinBeta[0]
|
|
348
|
+
rmid[-1] = rmid[-2] + cx[-1] * sinBeta[-1]
|
|
349
|
+
|
|
350
|
+
# The extensions have same span and pitch angle as first/last point
|
|
351
|
+
span = np.pad(span, 1, "edge")
|
|
352
|
+
Beta = np.pad(Beta, 1, "edge")
|
|
353
|
+
|
|
354
|
+
# Adjust to nozzle exit area
|
|
355
|
+
radius_ratio = rmid[-2] / rmid[-1]
|
|
356
|
+
span[-1] *= nozzle_ratio * radius_ratio
|
|
357
|
+
|
|
358
|
+
# We now have coordinates of the mid-span line
|
|
359
|
+
# So make the hub and casing lines
|
|
360
|
+
sinBeta = np.sin(np.radians(Beta))
|
|
361
|
+
cosBeta = np.cos(np.radians(Beta))
|
|
362
|
+
xhub = xmid + 0.5 * span * sinBeta
|
|
363
|
+
xcas = xmid - 0.5 * span * sinBeta
|
|
364
|
+
rhub = rmid - 0.5 * span * cosBeta
|
|
365
|
+
rcas = rmid + 0.5 * span * cosBeta
|
|
366
|
+
|
|
367
|
+
# Make hub and casing line splines
|
|
368
|
+
self._hub = MeridionalLine(xhub, rhub, Beta).smooth()
|
|
369
|
+
self._cas = MeridionalLine(xcas, rcas, Beta).smooth()
|
|
370
|
+
|
|
371
|
+
def __repr__(self):
|
|
372
|
+
try:
|
|
373
|
+
cm = self.chords(0.5)[1::2]
|
|
374
|
+
mq = np.arange(1.5, self.nrow + 1.5)
|
|
375
|
+
span = self.get_span(mq)
|
|
376
|
+
xrhub = self.evaluate_xr(mq, 0.0)
|
|
377
|
+
xrcas = self.evaluate_xr(mq, 0.0)
|
|
378
|
+
xrrms = np.sqrt(0.5 * (xrhub**2 + xrcas**2))
|
|
379
|
+
return (
|
|
380
|
+
"FixedAxialChord(\n"
|
|
381
|
+
f" x={util.format_array(xrrms[1])} m,\n"
|
|
382
|
+
f" r_rms={util.format_array(xrrms[1])} m,\n"
|
|
383
|
+
f" span={util.format_array(span)} m,\n"
|
|
384
|
+
f" AR={util.format_array(span / cm)}\n"
|
|
385
|
+
")"
|
|
386
|
+
)
|
|
387
|
+
except Exception:
|
|
388
|
+
return f"FixedAxialChord()"
|
|
389
|
+
|
|
390
|
+
@property
|
|
391
|
+
def nrow(self):
|
|
392
|
+
return self._hub.N // 2 - 1
|
|
393
|
+
|
|
394
|
+
def evaluate_xr(self, m, spf):
|
|
395
|
+
tb, spfb = np.broadcast_arrays(m, spf)
|
|
396
|
+
|
|
397
|
+
# t is a vector that describes grid spacings where each unit interval
|
|
398
|
+
# corresponds to a gap or blade
|
|
399
|
+
# We need to map to meridional distance fractions
|
|
400
|
+
npts = self.nseg + 1
|
|
401
|
+
tctrl = np.linspace(0, npts - 1, npts)
|
|
402
|
+
mhub = np.interp(tb, tctrl, self._hub.mctrl)
|
|
403
|
+
mcas = np.interp(tb, tctrl, self._cas.mctrl)
|
|
404
|
+
|
|
405
|
+
# Evaluate hub and casing coordinates
|
|
406
|
+
xr_hub = self._hub.xr(mhub)
|
|
407
|
+
xr_cas = self._cas.xr(mcas)
|
|
408
|
+
|
|
409
|
+
# Finally evaluate the meridional grid
|
|
410
|
+
spf1 = np.expand_dims(np.stack((1.0 - spfb, spfb)), 1)
|
|
411
|
+
xr_hc = np.stack((xr_hub, xr_cas))
|
|
412
|
+
xr = np.sum(spf1 * xr_hc, axis=0)
|
|
413
|
+
|
|
414
|
+
return xr
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
class Smooth(AnnulusDesigner):
|
|
418
|
+
"""Annlus defines the entire meridional geometry of the turbomachine."""
|
|
419
|
+
|
|
420
|
+
def forward(
|
|
421
|
+
self,
|
|
422
|
+
rmid,
|
|
423
|
+
span,
|
|
424
|
+
Beta,
|
|
425
|
+
AR_chord,
|
|
426
|
+
AR_gap,
|
|
427
|
+
nozzle_ratio=1.0,
|
|
428
|
+
rcout_offset=0.0,
|
|
429
|
+
smooth=True,
|
|
430
|
+
):
|
|
431
|
+
r"""Construct an annulus from geometric parameters.
|
|
432
|
+
|
|
433
|
+
Parameters
|
|
434
|
+
----------
|
|
435
|
+
rmid : (nrow*2) array
|
|
436
|
+
Mid-span radii at inlet and exit of all rows.
|
|
437
|
+
span : (nrow*2) array
|
|
438
|
+
Annulus span perpendicular to pitch angle at all stations.
|
|
439
|
+
Beta : (nrow*2) array
|
|
440
|
+
Pitch angles at all stations [deg].
|
|
441
|
+
AR_chord : (nrow)
|
|
442
|
+
Span to meridional chord aspect ratio for each blade row. When the
|
|
443
|
+
pitch angle is 90 degrees, the aspect ratio is constrained by
|
|
444
|
+
`rmid` and not a free parameter, so must be set to `NaN`.
|
|
445
|
+
AR_gap : (nrow+1) array
|
|
446
|
+
Meridional aspect ratio of inlet, exit and gaps between rows. When
|
|
447
|
+
the pitch angle is 90 degrees, the aspect ratio is constrained by
|
|
448
|
+
`rmid` and not a free parameter, so must be set to `NaN`.
|
|
449
|
+
nozzle_ratio : float
|
|
450
|
+
Area ratio of exit nozzle, default to 1. for no contraction.
|
|
451
|
+
|
|
452
|
+
"""
|
|
453
|
+
|
|
454
|
+
npt = len(rmid)
|
|
455
|
+
nrow = npt // 2
|
|
456
|
+
ngap = nrow + 1
|
|
457
|
+
nchord = nrow
|
|
458
|
+
|
|
459
|
+
# Check input data
|
|
460
|
+
util.check_scalar(nozzle_ratio=nozzle_ratio, rcout_offset=rcout_offset)
|
|
461
|
+
util.check_vector((npt,), rmid=rmid, span=span, Beta=Beta)
|
|
462
|
+
util.check_vector((ngap,), AR_gap=AR_gap)
|
|
463
|
+
util.check_vector((nchord,), AR_chord=AR_chord)
|
|
464
|
+
|
|
465
|
+
# Store input data
|
|
466
|
+
self.rmid = np.reshape(rmid, (npt,))
|
|
467
|
+
self.span = np.reshape(span, (npt,))
|
|
468
|
+
self.Beta = np.reshape(Beta, (npt,))
|
|
469
|
+
self.AR_chord = np.reshape(AR_chord, (nchord,))
|
|
470
|
+
self.AR_gap = np.reshape(AR_gap, (ngap,))
|
|
471
|
+
self.nozzle_ratio = nozzle_ratio
|
|
472
|
+
|
|
473
|
+
# Assemble vectors of all ARs and spans
|
|
474
|
+
AR = np.zeros(self.nrow * 2 + 1)
|
|
475
|
+
AR[::2] = self.AR_gap
|
|
476
|
+
AR[1::2] = self.AR_chord
|
|
477
|
+
span_avg = 0.5 * (self.span[1:] + self.span[:-1])
|
|
478
|
+
span_avg = np.append(np.insert(span_avg, 0, self.span[0]), self.span[-1])
|
|
479
|
+
|
|
480
|
+
# Calculate meridional lengths, estimate axial lenghts
|
|
481
|
+
AR_guess = AR + 0.0
|
|
482
|
+
AR_guess[AR < 0.0] = 0.4
|
|
483
|
+
Ds = span_avg / AR_guess
|
|
484
|
+
cosBeta_avg = np.cos(np.radians(0.5 * (self.Beta[1:] + self.Beta[:-1])))
|
|
485
|
+
cosBeta = np.cos(np.radians(self.Beta))
|
|
486
|
+
cosBeta_avg = np.append(np.insert(cosBeta_avg, 0, cosBeta[0]), cosBeta[-1])
|
|
487
|
+
Dx = cosBeta_avg * Ds
|
|
488
|
+
Dx[cosBeta_avg < 1e-3] = 0.0
|
|
489
|
+
Ds[AR < 0.0] = -1.0
|
|
490
|
+
|
|
491
|
+
# Integrate x
|
|
492
|
+
xmid = util.cumsum0(Dx)
|
|
493
|
+
xmid -= xmid[1] # Place x origin at first row LE
|
|
494
|
+
|
|
495
|
+
# Extended r coords
|
|
496
|
+
rmid = np.zeros((self.nrow + 1) * 2)
|
|
497
|
+
|
|
498
|
+
# Fill in known radii
|
|
499
|
+
rmid[1:-1] = self.rmid
|
|
500
|
+
|
|
501
|
+
# Inlet/exit ducts
|
|
502
|
+
sinBeta = np.sin(np.radians(Beta))
|
|
503
|
+
rmid[0] = rmid[1] - Ds[0] * sinBeta[0]
|
|
504
|
+
rmid[-1] = rmid[-2] + Ds[-1] * sinBeta[-1]
|
|
505
|
+
|
|
506
|
+
# We now have an initial guess of axial coordinates
|
|
507
|
+
# So make the hub and casing lines
|
|
508
|
+
|
|
509
|
+
# Extract data
|
|
510
|
+
span = np.pad(self.span, 1, "edge")
|
|
511
|
+
Beta = np.pad(self.Beta, 1, "edge")
|
|
512
|
+
cosBeta = np.cos(np.radians(Beta))
|
|
513
|
+
sinBeta = np.sin(np.radians(Beta))
|
|
514
|
+
|
|
515
|
+
# Adjust to meet nozzle exit area
|
|
516
|
+
radius_ratio = rmid[-2] / rmid[-1]
|
|
517
|
+
span[-1] *= self.nozzle_ratio * radius_ratio
|
|
518
|
+
|
|
519
|
+
xhub = xmid + 0.5 * span * sinBeta
|
|
520
|
+
xcas = xmid - 0.5 * span * sinBeta
|
|
521
|
+
rhub = rmid - 0.5 * span * cosBeta
|
|
522
|
+
rcas = rmid + 0.5 * span * cosBeta
|
|
523
|
+
|
|
524
|
+
# Offset the exit casing radius (defaults to zero)
|
|
525
|
+
rcas[-1] += rcout_offset * span[-1]
|
|
526
|
+
|
|
527
|
+
# Smoothed the initial guess lines
|
|
528
|
+
self.hub = MeridionalLine(xhub, rhub, Beta).smooth()
|
|
529
|
+
self.cas = MeridionalLine(xcas, rcas, Beta).smooth()
|
|
530
|
+
|
|
531
|
+
# Now we need to offset the x-coordinates of each control point in turn
|
|
532
|
+
# to reach target aspect ratios
|
|
533
|
+
|
|
534
|
+
# Initialise the offsets to zero
|
|
535
|
+
Dx_AR = np.zeros_like(xhub)
|
|
536
|
+
|
|
537
|
+
# To optimise the kth chord, we offset the k+1th and downstream control points
|
|
538
|
+
def _iter_chord(delta, k):
|
|
539
|
+
Dx_AR[k + 1 :] = delta
|
|
540
|
+
self.hub.x = xhub + Dx_AR
|
|
541
|
+
self.cas.x = xcas + Dx_AR
|
|
542
|
+
self.hub._fit()
|
|
543
|
+
self.cas._fit()
|
|
544
|
+
chords = self.chords(0.5)
|
|
545
|
+
err = chords - Ds
|
|
546
|
+
return err[k]
|
|
547
|
+
|
|
548
|
+
# To optimise the kth chord, we offset the k+1th and downstream control points
|
|
549
|
+
def _iter_smooth(delta, k):
|
|
550
|
+
Dx_AR[k + 1 :] = delta
|
|
551
|
+
self.hub.x = xhub + Dx_AR
|
|
552
|
+
self.cas.x = xcas + Dx_AR
|
|
553
|
+
self.hub._fit()
|
|
554
|
+
self.cas._fit()
|
|
555
|
+
# self.hub.smooth()
|
|
556
|
+
# self.cas.smooth()
|
|
557
|
+
return self.hub.smoothness_metric + self.cas.smoothness_metric
|
|
558
|
+
|
|
559
|
+
# Loop over all chords and iterate axial coordinates
|
|
560
|
+
def _solve_k(k):
|
|
561
|
+
dxref = np.max(np.abs((xmid[k + 1] - xmid[k], rmid[k + 1] - rmid[k])))
|
|
562
|
+
|
|
563
|
+
if np.isnan(Ds[k]):
|
|
564
|
+
return
|
|
565
|
+
|
|
566
|
+
elif Ds[k] < 0.0:
|
|
567
|
+
minimize(
|
|
568
|
+
_iter_smooth,
|
|
569
|
+
0.0,
|
|
570
|
+
args=(k,),
|
|
571
|
+
tol=dxref * 1e-6,
|
|
572
|
+
options={"maxiter": 200},
|
|
573
|
+
)
|
|
574
|
+
|
|
575
|
+
else:
|
|
576
|
+
# Find a bracket safely
|
|
577
|
+
dx_lower = None
|
|
578
|
+
|
|
579
|
+
# High guess
|
|
580
|
+
for rel_dx in (0.1, 0.2, 0.4, 0.8, 1.6):
|
|
581
|
+
dx_upper = dxref * rel_dx
|
|
582
|
+
err = _iter_chord(dx_upper, k)
|
|
583
|
+
if err > 0.0:
|
|
584
|
+
break
|
|
585
|
+
else:
|
|
586
|
+
dx_lower = dxref * rel_dx
|
|
587
|
+
|
|
588
|
+
# Low guess
|
|
589
|
+
if dx_lower is None:
|
|
590
|
+
for rel_dx in (0.1, 0.2, 0.4, 0.8, 1.6):
|
|
591
|
+
dx_lower = -dxref * rel_dx
|
|
592
|
+
err = _iter_chord(dx_lower, k)
|
|
593
|
+
if err < 0.0:
|
|
594
|
+
break
|
|
595
|
+
|
|
596
|
+
try:
|
|
597
|
+
root_scalar(
|
|
598
|
+
_iter_chord,
|
|
599
|
+
bracket=(dx_lower, dx_upper),
|
|
600
|
+
args=(k,),
|
|
601
|
+
xtol=dxref * 1e-3,
|
|
602
|
+
)
|
|
603
|
+
except ValueError:
|
|
604
|
+
pass
|
|
605
|
+
|
|
606
|
+
if smooth:
|
|
607
|
+
# for k in range(1, 3):
|
|
608
|
+
for k in range(1, self.nseg - 1):
|
|
609
|
+
_solve_k(k)
|
|
610
|
+
|
|
611
|
+
# err_out_abs = self.chords(0.5) - Ds
|
|
612
|
+
# err_out_rel = err_out_abs / self.chords(0.5)
|
|
613
|
+
# assert (np.abs(err_out_rel[~np.isnan(err_out_rel)]) < 1e-2).all()
|
|
614
|
+
|
|
615
|
+
self.cas.smooth()
|
|
616
|
+
self.hub.smooth()
|
|
617
|
+
|
|
618
|
+
if not rcout_offset:
|
|
619
|
+
assert all(self.hub._is_straight() == self.cas._is_straight())
|
|
620
|
+
|
|
621
|
+
@property
|
|
622
|
+
def nrow(self):
|
|
623
|
+
return self.rmid.size // 2
|
|
624
|
+
|
|
625
|
+
def evaluate_xr(self, m, spf):
|
|
626
|
+
tb, spfb = np.broadcast_arrays(m, spf)
|
|
627
|
+
|
|
628
|
+
# t is a vector that describes grid spacings where each unit interval
|
|
629
|
+
# corresponds to a gap or blade
|
|
630
|
+
# We need to map to meridional distance fractions
|
|
631
|
+
npts = self.nseg + 1
|
|
632
|
+
tctrl = np.linspace(0, npts - 1, npts)
|
|
633
|
+
mhub = np.interp(tb, tctrl, self.hub.mctrl)
|
|
634
|
+
mcas = np.interp(tb, tctrl, self.cas.mctrl)
|
|
635
|
+
|
|
636
|
+
# Evaluate hub and casing coordinates
|
|
637
|
+
xr_hub = self.hub.xr(mhub)
|
|
638
|
+
xr_cas = self.cas.xr(mcas)
|
|
639
|
+
|
|
640
|
+
# Finally evaluate the meridional grid
|
|
641
|
+
spf1 = np.expand_dims(np.stack((1.0 - spfb, spfb)), 1)
|
|
642
|
+
xr_hc = np.stack((xr_hub, xr_cas))
|
|
643
|
+
xr = np.sum(spf1 * xr_hc, axis=0)
|
|
644
|
+
|
|
645
|
+
return xr
|
|
646
|
+
|
|
647
|
+
def __repr__(self):
|
|
648
|
+
try:
|
|
649
|
+
cm = self.chords(0.5)[1::2]
|
|
650
|
+
mq = np.arange(1.5, self.nrow + 1.5)
|
|
651
|
+
span = self.get_span(mq)
|
|
652
|
+
xr_mid = self.evaluate_xr(mq, 0.5)
|
|
653
|
+
return f"FixedAR(nrow={self.nrow}, x={xr_mid[0]}, r={xr_mid[1]}, AR={span / cm})"
|
|
654
|
+
except Exception:
|
|
655
|
+
return f"FixedAR()"
|
|
656
|
+
|
|
657
|
+
|
|
658
|
+
#
|
|
659
|
+
#
|
|
660
|
+
# def load_annulus(annulus_type):
|
|
661
|
+
# """Get annulus class by string, including any custom classes."""
|
|
662
|
+
# available_annulus_types = {a.__name__: a for a in BaseAnnulus.__subclasses__()}
|
|
663
|
+
# if annulus_type not in available_annulus_types:
|
|
664
|
+
# raise ValueError(
|
|
665
|
+
# f"Unknown annulus type: {annulus_type}, should be one of {available_annulus_types.keys()}"
|
|
666
|
+
# )
|
|
667
|
+
# else:
|
|
668
|
+
# return available_annulus_types[annulus_type]
|