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.
Files changed (82) hide show
  1. embsolvec.cpython-313-x86_64-linux-musl.so +0 -0
  2. turbigen/__init__.py +6 -0
  3. turbigen/annulus.py +668 -0
  4. turbigen/autogrid/__init__.py +2 -0
  5. turbigen/autogrid/ag_server.sh +178 -0
  6. turbigen/autogrid/autogrid.py +599 -0
  7. turbigen/autogrid/reader.py +163 -0
  8. turbigen/autogrid/script_ag.py2 +336 -0
  9. turbigen/autogrid/script_igg.py2 +44 -0
  10. turbigen/autogrid/script_sh +28 -0
  11. turbigen/autogrid/server.py +72 -0
  12. turbigen/average.py +509 -0
  13. turbigen/base.py +2253 -0
  14. turbigen/blade.py +277 -0
  15. turbigen/camber.py +186 -0
  16. turbigen/clusterfunc/__init__.py +3 -0
  17. turbigen/clusterfunc/check.py +107 -0
  18. turbigen/clusterfunc/double.py +248 -0
  19. turbigen/clusterfunc/exceptions.py +5 -0
  20. turbigen/clusterfunc/plot.py +41 -0
  21. turbigen/clusterfunc/single.py +271 -0
  22. turbigen/clusterfunc/symmetric.py +63 -0
  23. turbigen/clusterfunc/util.py +18 -0
  24. turbigen/compflow_native.py +268 -0
  25. turbigen/config.py +572 -0
  26. turbigen/config2.py +775 -0
  27. turbigen/exceptions.py +6 -0
  28. turbigen/flowfield.py +120 -0
  29. turbigen/fluid.py +789 -0
  30. turbigen/geometry.py +952 -0
  31. turbigen/grid.py +2004 -0
  32. turbigen/hmesh.py +852 -0
  33. turbigen/inlet.py +89 -0
  34. turbigen/iterators.py +360 -0
  35. turbigen/main.py +376 -0
  36. turbigen/marching_cubes.py +676 -0
  37. turbigen/meanline.py +461 -0
  38. turbigen/mesh.py +43 -0
  39. turbigen/nblade.py +83 -0
  40. turbigen/ohmesh.py +254 -0
  41. turbigen/polynomial.py +650 -0
  42. turbigen/post/blade_surf_dist.py +153 -0
  43. turbigen/post/calc_surf_dissipation.py +137 -0
  44. turbigen/post/check_phase.py +26 -0
  45. turbigen/post/contour.py +280 -0
  46. turbigen/post/find_extrema.py +22 -0
  47. turbigen/post/plot_annulus.py +95 -0
  48. turbigen/post/plot_blade.py +39 -0
  49. turbigen/post/plot_camber.py +63 -0
  50. turbigen/post/plot_convergence.py +122 -0
  51. turbigen/post/plot_incidence.py +33 -0
  52. turbigen/post/plot_isen_mach.py +148 -0
  53. turbigen/post/plot_nose.py +165 -0
  54. turbigen/post/plot_pressure_distributions.py +232 -0
  55. turbigen/post/plot_section.py +166 -0
  56. turbigen/post/plot_thickness.py +50 -0
  57. turbigen/post/spanwise.py +146 -0
  58. turbigen/post/write_annulus.py +28 -0
  59. turbigen/post/write_cuts.py +66 -0
  60. turbigen/post/write_ibl.py +134 -0
  61. turbigen/post/write_stl.py +161 -0
  62. turbigen/post.py +485 -0
  63. turbigen/run.py +1443 -0
  64. turbigen/run2.py +65 -0
  65. turbigen/slurm.py +261 -0
  66. turbigen/solver.py +123 -0
  67. turbigen/solvers/base.py +34 -0
  68. turbigen/solvers/convert_ts3_to_ts4_native.py +101 -0
  69. turbigen/solvers/emb.py +1528 -0
  70. turbigen/solvers/plot3d.py +25 -0
  71. turbigen/solvers/ts3.py +1518 -0
  72. turbigen/solvers/ts4.py +880 -0
  73. turbigen/tables.py +508 -0
  74. turbigen/thickness.py +527 -0
  75. turbigen/util.py +1532 -0
  76. turbigen/util_post.py +180 -0
  77. turbigen/vtri.py +66 -0
  78. turbigen/yaml.py +117 -0
  79. turbigen-2.0.0.dist-info/METADATA +34 -0
  80. turbigen-2.0.0.dist-info/RECORD +82 -0
  81. turbigen-2.0.0.dist-info/WHEEL +5 -0
  82. turbigen-2.0.0.dist-info/entry_points.txt +4 -0
turbigen/__init__.py ADDED
@@ -0,0 +1,6 @@
1
+ import importlib.metadata
2
+ from . import hmesh
3
+ from . import ohmesh
4
+
5
+ __version__ = importlib.metadata.version("turbigen")
6
+ __copyright__ = "2024 James Brind"
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]
@@ -0,0 +1,2 @@
1
+ from . import autogrid
2
+ from . import reader