pyfoiltools 0.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.
@@ -0,0 +1,557 @@
1
+ from typing import TYPE_CHECKING, Any
2
+
3
+ from matplotlib.pyplot import figure
4
+ from numpy import arctan, block, concatenate, cos, diag, hstack, sin, zeros
5
+ from numpy.linalg import lstsq, norm
6
+ from py2md.classes import MDReport
7
+ from pygeom.geom2d import BSplineCurve2D, Vector2D
8
+ from pygeom.tools.solvers import solve_clsq
9
+
10
+ from .airfoilpoly import AirfoilPoly, AirfoilPolyFit
11
+ from .airfoilshape import AirfoilShape, AirfoilShapeFit
12
+ from .chordspacing import ChordSpacing
13
+
14
+ if TYPE_CHECKING:
15
+ from matplotlib.axes import Axes
16
+ from numpy.typing import NDArray
17
+
18
+
19
+ class AirfoilBSpline():
20
+ name: str = None
21
+ adef: AirfoilPoly | AirfoilShape = None
22
+ bspline_u: 'BSplineCurve2D' = None
23
+ bspline_l: 'BSplineCurve2D' = None
24
+ c: float = None
25
+ num: int = None
26
+ _spacing: 'ChordSpacing' = None
27
+ _xu: 'NDArray' = None
28
+ _yu: 'NDArray' = None
29
+ _xl: 'NDArray' = None
30
+ _yl: 'NDArray' = None
31
+ _x: 'NDArray' = None
32
+ _y: 'NDArray' = None
33
+
34
+ def __init__(self, name: str, adef: AirfoilPoly | AirfoilShape,
35
+ bspline_u: 'BSplineCurve2D', bspline_l: 'BSplineCurve2D',
36
+ c: float = 1.0, num: int = 101) -> None:
37
+ self.name = name
38
+ self.adef = adef
39
+ self.bspline_u = bspline_u
40
+ self.bspline_l = bspline_l
41
+ self.c = c
42
+ self.num = num
43
+
44
+ def reset(self) -> None:
45
+ for attr in self.__dict__.keys():
46
+ if attr.startswith('_'):
47
+ setattr(self, attr, None)
48
+
49
+ def set_chord(self, c: float) -> None:
50
+ self.c = c
51
+
52
+ @property
53
+ def spacing(self) -> ChordSpacing:
54
+ if self._spacing is None:
55
+ self._spacing = ChordSpacing.from_num(self.num)
56
+ return self._spacing
57
+
58
+ @property
59
+ def th(self) -> 'NDArray':
60
+ return self.spacing.th
61
+
62
+ @property
63
+ def xu(self) -> 'NDArray':
64
+ if self._xu is None:
65
+ pnts_u = self.bspline_u.evaluate_points_at_t(self.spacing.s)
66
+ self._xu, self._yu = pnts_u.to_xy()
67
+ return self._xu
68
+
69
+ @property
70
+ def yu(self) -> 'NDArray':
71
+ if self._yu is None:
72
+ pnts_u = self.bspline_u.evaluate_points_at_t(self.spacing.s)
73
+ self._xu, self._yu = pnts_u.to_xy()
74
+ return self._yu
75
+
76
+ @property
77
+ def xl(self) -> 'NDArray':
78
+ if self._xl is None:
79
+ pnts_l = self.bspline_l.evaluate_points_at_t(self.spacing.s)
80
+ self._xl, self._yl = pnts_l.to_xy()
81
+ return self._xl
82
+
83
+ @property
84
+ def yl(self) -> 'NDArray':
85
+ if self._yl is None:
86
+ pnts_l = self.bspline_l.evaluate_points_at_t(self.spacing.s)
87
+ self._xl, self._yl = pnts_l.to_xy()
88
+ return self._yl
89
+
90
+ @property
91
+ def x(self) -> 'NDArray':
92
+ if self._x is None:
93
+ self._x = hstack((self.xl[::-1], self.xu[1:]))
94
+ return self._x
95
+
96
+ @property
97
+ def y(self) -> 'NDArray':
98
+ if self._y is None:
99
+ self._y = hstack((self.yl[::-1], self.yu[1:]))
100
+ return self._y
101
+
102
+ def plot_shape(self, ax: 'Axes' = None, **kwargs: dict[str, Any]) -> 'Axes':
103
+ if ax is None:
104
+ fig = figure(figsize=(12, 8))
105
+ ax = fig.gca()
106
+ ax.grid(True)
107
+ ax.set_aspect('equal')
108
+ ax.set_xlabel(r'$x$')
109
+ ax.set_ylabel(r'$y$')
110
+
111
+ kwargs.setdefault('label', self.name)
112
+
113
+ ax.plot(self.x, self.y, **kwargs)
114
+ return ax
115
+
116
+ def __repr__(self) -> str:
117
+ return f'<AirfoilBSpline: {self.name:s}>'
118
+
119
+ def to_mdobj(self) -> MDReport:
120
+ return self.adef.to_mdobj()
121
+
122
+ def _repr_markdown_(self) -> str:
123
+ return self.to_mdobj()._repr_markdown_()
124
+
125
+ def __str__(self) -> str:
126
+ return f'AirfoilBSpline: {self.name:s}\nBSpline_u = \n{self.bspline_u}\nBSpline_l = \n{self.bspline_l}'
127
+
128
+
129
+ class AirfoilBSplineFit():
130
+ afit: AirfoilPolyFit | AirfoilShapeFit = None
131
+ Nu: int = None
132
+ Nl: int = None
133
+ _adef: AirfoilPoly | AirfoilShape = None
134
+ _spcu: ChordSpacing = None
135
+ _THu: 'NDArray' = None
136
+ _Ucu: 'NDArray' = None
137
+ _Vcu: 'NDArray' = None
138
+ _Xcu: 'NDArray' = None
139
+ _Ycu: 'NDArray' = None
140
+ _Tcu: 'NDArray' = None
141
+ _spcl: ChordSpacing = None
142
+ _THl: 'NDArray' = None
143
+ _Ucl: 'NDArray' = None
144
+ _Vcl: 'NDArray' = None
145
+ _Xcl: 'NDArray' = None
146
+ _Ycl: 'NDArray' = None
147
+ _Tcl: 'NDArray' = None
148
+ _aspl: AirfoilBSpline = None
149
+ _tu: 'NDArray' = None
150
+ _tl: 'NDArray' = None
151
+ _carr: 'NDArray' = None
152
+ _darr: 'NDArray' = None
153
+ _dXudTu: 'NDArray' = None
154
+ _dYudTu: 'NDArray' = None
155
+ _dXldTl: 'NDArray' = None
156
+ _dYldTl: 'NDArray' = None
157
+ _lmda: 'NDArray' = None
158
+
159
+ def __init__(self, afit: AirfoilPolyFit | AirfoilShapeFit,
160
+ Nu: int, Nl: int) -> None:
161
+ self.afit = afit
162
+ self.Nu = Nu
163
+ self.Nl = Nl
164
+
165
+ @property
166
+ def adef(self) -> AirfoilPoly | AirfoilShape:
167
+ if self._adef is None:
168
+ if isinstance(self.afit, AirfoilPolyFit):
169
+ self._adef = self.afit.aply
170
+ elif isinstance(self.afit, AirfoilShapeFit):
171
+ self._adef = self.afit.ashp
172
+ else:
173
+ raise ValueError('Invalid airfoil shape type.')
174
+ return self._adef
175
+
176
+ @property
177
+ def spcu(self) -> ChordSpacing:
178
+ if self._spcu is None:
179
+ self._spcu = ChordSpacing.from_num(self.Nu)
180
+ return self._spcu
181
+
182
+ @property
183
+ def THu(self) -> 'NDArray':
184
+ if self._THu is None:
185
+ myu = self.adef.calc_my(self.spcu, self.adef.Cn)
186
+ self._THu = arctan(myu)
187
+ return self._THu
188
+
189
+ @property
190
+ def Ucu(self) -> 'NDArray':
191
+ if self._Ucu is None:
192
+ self._Ucu = -sin(self.THu)/2
193
+ return self._Ucu
194
+
195
+ @property
196
+ def Vcu(self) -> 'NDArray':
197
+ if self._Vcu is None:
198
+ self._Vcu = cos(self.THu)/2
199
+ return self._Vcu
200
+
201
+ @property
202
+ def Tcu(self) -> 'NDArray':
203
+ if self._Tcu is None:
204
+ self._Tcu = self.adef.calc_tc(self.spcu, self.adef.Tn)
205
+ self._Tcu[0] = self._Tcu[1]
206
+ return self._Tcu
207
+
208
+ @property
209
+ def Xcu(self) -> 'NDArray':
210
+ if self._Xcu is None:
211
+ self._Xcu = self.adef.calc_xc(self.spcu)
212
+ return self._Xcu
213
+
214
+ @property
215
+ def Ycu(self) -> 'NDArray':
216
+ if self._Ycu is None:
217
+ self._Ycu = self.adef.calc_yc(self.spcu, self.adef.Cn)
218
+ return self._Ycu
219
+
220
+ @property
221
+ def spcl(self) -> ChordSpacing:
222
+ if self._spcl is None:
223
+ self._spcl = ChordSpacing.from_num(self.Nl)
224
+ return self._spcl
225
+
226
+ @property
227
+ def THl(self) -> 'NDArray':
228
+ if self._THl is None:
229
+ myl = self.adef.calc_my(self.spcl, self.adef.Cn)
230
+ self._THl = arctan(myl)
231
+ return self._THl
232
+
233
+ @property
234
+ def Ucl(self) -> 'NDArray':
235
+ if self._Ucl is None:
236
+ self._Ucl = sin(self.THl)/2
237
+ return self._Ucl
238
+
239
+ @property
240
+ def Vcl(self) -> 'NDArray':
241
+ if self._Vcl is None:
242
+ self._Vcl = -cos(self.THl)/2
243
+ return self._Vcl
244
+
245
+ @property
246
+ def Tcl(self) -> 'NDArray':
247
+ if self._Tcl is None:
248
+ self._Tcl = self.adef.calc_tc(self.spcl, self.adef.Tn)
249
+ self._Tcl[0] = self._Tcl[1]
250
+ return self._Tcl
251
+
252
+ @property
253
+ def Xcl(self) -> 'NDArray':
254
+ if self._Xcl is None:
255
+ self._Xcl = self.adef.calc_xc(self.spcl)
256
+ return self._Xcl
257
+
258
+ @property
259
+ def Ycl(self) -> 'NDArray':
260
+ if self._Ycl is None:
261
+ self._Ycl = self.adef.calc_yc(self.spcl, self.adef.Cn)
262
+ return self._Ycl
263
+
264
+ @property
265
+ def aspl(self) -> AirfoilBSpline:
266
+ if self._aspl is None:
267
+
268
+ ctlpnts_u = Vector2D.zeros(self.Nu + 1)
269
+ ctlpnts_u.x[1:] = self.Xcu + self.Ucu*self.Tcu
270
+ ctlpnts_u.y[1:] = self.Ycu + self.Vcu*self.Tcu
271
+ bspline_u = BSplineCurve2D(ctlpnts_u, degree=self.Nu)
272
+
273
+ ctlpnts_l = Vector2D.zeros(self.Nl + 1)
274
+ ctlpnts_l.x[1:] = self.Xcl + self.Ucl*self.Tcl
275
+ ctlpnts_l.y[1:] = self.Ycl + self.Vcl*self.Tcl
276
+ bspline_l = BSplineCurve2D(ctlpnts_l, degree=self.Nl)
277
+
278
+ self._aspl = AirfoilBSpline(self.adef.name, self.adef,
279
+ bspline_u, bspline_l)
280
+
281
+ return self._aspl
282
+
283
+ @property
284
+ def tu(self) -> 'NDArray':
285
+ if self._tu is None:
286
+ self._tu = self.afit.spcu.s.copy()
287
+ self._tu = bspline_lstsq_fit_t(self.aspl.bspline_u, self.afit.xu,
288
+ self.afit.yu, self._tu)
289
+ return self._tu
290
+
291
+ @property
292
+ def tl(self) -> 'NDArray':
293
+ if self._tl is None:
294
+ self._tl = self.afit.spcl.s.copy()
295
+ self._tl = bspline_lstsq_fit_t(self.aspl.bspline_l, self.afit.xl,
296
+ self.afit.yl, self._tl)
297
+ return self._tl
298
+
299
+ @property
300
+ def num_Tu(self) -> int:
301
+ return self.Tcu.size - 1
302
+
303
+ @property
304
+ def num_Tl(self) -> int:
305
+ return self.Tcl.size - 1
306
+
307
+ @property
308
+ def num_tu(self) -> int:
309
+ return self.tu.size
310
+
311
+ @property
312
+ def num_tl(self) -> int:
313
+ return self.tl.size
314
+
315
+ @property
316
+ def carr(self) -> 'NDArray':
317
+ if self._carr is None:
318
+ c_Tu = zeros((1, self.num_Tu))
319
+ c_Tu[0, 0] = 1.0
320
+ c_Tl = zeros((1, self.num_Tl))
321
+ c_Tl[0, 0] = -1.0
322
+ c_tu = zeros((1, self.num_tu))
323
+ c_tl = zeros((1, self.num_tl))
324
+ self._carr = hstack((c_Tu, c_Tl, c_tu, c_tl))
325
+ return self._carr
326
+
327
+ @property
328
+ def darr(self) -> 'NDArray':
329
+ if self._darr is None:
330
+ self._darr = zeros(1)
331
+ return self._darr
332
+
333
+ @property
334
+ def dXudTu(self) -> 'NDArray':
335
+ if self._dXudTu is None:
336
+ self._dXudTu = diag(self.Ucu[:-1])
337
+ return self._dXudTu
338
+
339
+ @property
340
+ def dYudTu(self) -> 'NDArray':
341
+ if self._dYudTu is None:
342
+ self._dYudTu = diag(self.Vcu[:-1])
343
+ return self._dYudTu
344
+
345
+ @property
346
+ def dXldTl(self) -> 'NDArray':
347
+ if self._dXldTl is None:
348
+ self._dXldTl = diag(self.Ucl[:-1])
349
+ return self._dXldTl
350
+
351
+ @property
352
+ def dYldTl(self) -> 'NDArray':
353
+ if self._dYldTl is None:
354
+ self._dYldTl = diag(self.Vcl[:-1])
355
+ return self._dYldTl
356
+
357
+ @property
358
+ def lmda(self) -> 'NDArray':
359
+ if self._lmda is None:
360
+ self._lmda = zeros(self.carr.shape[0])
361
+ return self._lmda
362
+
363
+ def plot_airfoil_bspline_fit(self, ax: 'Axes' = None) -> 'Axes':
364
+
365
+ if ax is None:
366
+ fig = figure(figsize=(12, 8))
367
+ ax = fig.gca()
368
+ ax.grid(True)
369
+ ax.set_aspect('equal')
370
+ ax.set_xlabel(r'$x$')
371
+ ax.set_ylabel(r'$y$')
372
+
373
+ ax = self.adef.plot_shape(ax=ax)
374
+ ax = self.afit.airfoil.plot(ax=ax)
375
+
376
+ pnts_u = self.aspl.bspline_u.evaluate_points(100)
377
+ pnts_l = self.aspl.bspline_l.evaluate_points(100)
378
+
379
+ ctls_u = self.aspl.bspline_u.ctlpnts
380
+ ctls_l = self.aspl.bspline_l.ctlpnts
381
+
382
+ ax.plot(pnts_u.x, pnts_u.y, label='BSpline Fit Upper')
383
+ ax.plot(pnts_l.x, pnts_l.y, label='BSpline Fit Lower')
384
+ ax.plot(ctls_u.x, ctls_u.y, '-o', label='Control Points Upper')
385
+ ax.plot(ctls_l.x, ctls_l.y, '-o', label='Control Points Lower')
386
+ ax.plot(self.afit.xu, self.afit.yu, 'x', label='Target Points Upper')
387
+ ax.plot(self.afit.xl, self.afit.yl, 'x', label='Target Points Lower')
388
+ _ = ax.legend()
389
+ return ax
390
+
391
+ def improved_fit(self, display: bool = False) -> tuple[float, float]:
392
+
393
+ xu, yu = self.aspl.bspline_u.evaluate_points_at_t(self.tu).to_xy()
394
+
395
+ Dxu = xu - self.afit.xu
396
+ Dyu = yu - self.afit.yu
397
+
398
+ xl, yl = self.aspl.bspline_l.evaluate_points_at_t(self.tl).to_xy()
399
+
400
+ Dxl = xl - self.afit.xl
401
+ Dyl = yl - self.afit.yl
402
+
403
+ f = concatenate((Dxu, Dyu, Dxl, Dyl))
404
+
405
+ Ntu = self.aspl.bspline_u.basis_functions(self.tu).transpose()
406
+
407
+ dDxudXu = Ntu[:, 1:self.Nu]
408
+ dDyudYu = Ntu[:, 1:self.Nu]
409
+
410
+ Ntl = self.aspl.bspline_l.basis_functions(self.tl).transpose()
411
+
412
+ dDxldXl = Ntl[:, 1:self.Nl]
413
+ dDyldYl = Ntl[:, 1:self.Nl]
414
+
415
+ dDxudTu = dDxudXu@self.dXudTu + dDyudYu@self.dYudTu
416
+ dDxudTl = zeros((xu.size, self.Nl - 1))
417
+
418
+ dDyudTu = dDyudYu@self.dYudTu + dDyudYu@self.dYudTu
419
+ dDyudTl = zeros((yu.size, self.Nl - 1))
420
+
421
+ dDxldTl = dDxldXl@self.dXldTl + dDyldYl@self.dYldTl
422
+ dDxldTu = zeros((xl.size, self.Nu - 1))
423
+
424
+ dDyldTl = dDyldYl@self.dYldTl + dDyldYl@self.dYldTl
425
+ dDyldTu = zeros((yl.size, self.Nu - 1))
426
+
427
+ dDxudtu, dDyudtu = self.aspl.bspline_u.evaluate_first_derivatives_at_t(self.tu).to_xy()
428
+
429
+ dDxudtu = diag(dDxudtu)
430
+ dDxudtl = zeros((xu.size, self.tl.size))
431
+
432
+ dDyudtu = diag(dDyudtu)
433
+ dDyudtl = zeros((yu.size, self.tl.size))
434
+
435
+ dDxldtl, dDyldtl = self.aspl.bspline_l.evaluate_first_derivatives_at_t(self.tl).to_xy()
436
+
437
+ dDxldtl = diag(dDxldtl)
438
+ dDxldtu = zeros((xl.size, self.tu.size))
439
+
440
+ dDyldtl = diag(dDyldtl)
441
+ dDyldtu = zeros((yl.size, self.tu.size))
442
+
443
+ dfdTt = block([[dDxudTu, dDxudTl, dDxudtu, dDxudtl],
444
+ [dDyudTu, dDyudTl, dDyudtu, dDyudtl],
445
+ [dDxldTu, dDxldTl, dDxldtu, dDxldtl],
446
+ [dDyldTu, dDyldTl, dDyldtu, dDyldtl]])
447
+
448
+ dvTt, dlTt = solve_clsq(dfdTt, f, self.carr, self.darr)
449
+
450
+ dTu = dvTt[:self.num_Tu]
451
+ dTl = dvTt[self.num_Tu:self.num_Tu + self.num_Tl]
452
+ dtu = dvTt[self.num_Tu + self.num_Tl:self.num_Tu + self.num_Tl + self.num_tu]
453
+ dtl = dvTt[self.num_Tu + self.num_Tl + self.num_tu:]
454
+
455
+ self.Tcu[:-1] -= dTu
456
+ self.Tcl[:-1] -= dTl
457
+
458
+ self.tu[:] -= dtu
459
+ self.tl[:] -= dtl
460
+
461
+ self.lmda[:] -= dlTt
462
+
463
+ self._aspl = None
464
+
465
+ normdvar = norm(dvTt)
466
+
467
+ dcon = self.darr
468
+ normdcon = norm(dcon)
469
+
470
+ if display:
471
+ print(f'normdvar = {normdvar}\n')
472
+ print(f'normdcon = {normdcon}\n')
473
+
474
+ return normdcon, normdvar
475
+
476
+ def improved_fit_loop(self, tol_dcon: float = 1e-12, tol_dvar: float = 1e-3,
477
+ max_iter: int = 50, display: bool = False) -> tuple[float, float]:
478
+
479
+ normdcon, normdvar = float('inf'), float('inf')
480
+
481
+ iter = 1
482
+ while normdcon > tol_dcon or normdvar > tol_dvar:
483
+ normdcon, normdvar = self.improved_fit()
484
+ if display:
485
+ print(f'Iteration {iter}')
486
+ print(f'normdcon = {normdcon:.12f}')
487
+ print(f'normdvar = {normdvar:.12f}')
488
+ print()
489
+ if iter >= max_iter:
490
+ err = f'Maximum number of iterations ({max_iter}) reached.'
491
+ raise RuntimeError(err)
492
+ iter += 1
493
+
494
+ return normdcon, normdvar
495
+
496
+ def __repr__(self) -> str:
497
+ return f'<AirfoilBSplineFit: {self.adef.name:s}>'
498
+
499
+ def __str__(self) -> str:
500
+ return f'AirfoilBSplineFit: {self.adef.name:s}'
501
+
502
+
503
+ def bspline_lstsq_fit_t(bspline: BSplineCurve2D, x_t: 'NDArray', y_t: 'NDArray',
504
+ t: 'NDArray', **kwargs: dict[str, Any]) -> 'NDArray':
505
+
506
+ max_iter: int = kwargs.get('max_iter', 50)
507
+ display: bool = kwargs.get('display', False)
508
+
509
+ count = 0
510
+
511
+ while True:
512
+
513
+ x, y = bspline.evaluate_points_at_t(t).to_xy()
514
+
515
+ Dx = x - x_t
516
+ Dy = y - y_t
517
+
518
+ f = concatenate((Dx, Dy))
519
+ norm_f = norm(f)
520
+
521
+ if display:
522
+ print(f'f = {f}\n')
523
+ print(f'norm_f = {norm_f}')
524
+
525
+ if norm_f < 1.0e-12:
526
+ if display:
527
+ print(f'Converged with norm_f = {norm_f}\n')
528
+ break
529
+
530
+ dDxdt, dDydt = bspline.evaluate_first_derivatives_at_t(t).to_xy()
531
+ dDxdt = diag(dDxdt)
532
+ dDydt = diag(dDydt)
533
+
534
+ dfdt = block([[dDxdt], [dDydt]])
535
+
536
+ dvt, _, _, _ = lstsq(dfdt, f)
537
+ t -= dvt
538
+ norm_dv = norm(dvt)
539
+
540
+ if display:
541
+ print(f'dvtt = {dvt}\n')
542
+ print(f'norm_dv = {norm_dv}\n')
543
+
544
+ if norm_dv < 1.0e-12:
545
+ if display:
546
+ print(f'Converged with norm_dv = {norm_dv}\n')
547
+ break
548
+
549
+ count += 1
550
+
551
+ if count > max_iter:
552
+ print(f'Convergence failed in {max_iter} iterations.\n')
553
+ print(f'norm_f = {norm_f}\n')
554
+ print(f'norm_dv = {norm_dv}\n')
555
+ break
556
+
557
+ return t