wawi 0.0.7__py3-none-any.whl → 0.0.9__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 wawi might be problematic. Click here for more details.

wawi/model/_hydro.py ADDED
@@ -0,0 +1,882 @@
1
+ import numpy as np
2
+ import json
3
+ from inspect import isfunction
4
+ from scipy.interpolate import interp1d
5
+
6
+ from wawi.general import blkdiag, interp1d_angular
7
+ from wawi.wave import maxincrement, dispersion_relation_scalar, xsim
8
+ import wawi.wave
9
+ import pyvista as pv
10
+ from scipy.linalg import block_diag
11
+
12
+ '''
13
+ HYDRO SUBMODULE
14
+ '''
15
+ class Hydro:
16
+ def __init__(self, pontoons=None, phi_key='hydro', environment=None, phases_lead=False, seastate=None):
17
+ self.pontoons = pontoons
18
+ self.phi_key = phi_key
19
+ self._phases_lead = False
20
+ self._phi = None
21
+ self.seastate = seastate
22
+
23
+ if environment is None:
24
+ self.environment = Environment()
25
+
26
+ self.assign_environment()
27
+
28
+ @property
29
+ def phases_lead(self):
30
+ return self._phases_lead
31
+
32
+ @phases_lead.setter
33
+ def phases_lead(self, val):
34
+ self._phases_lead = val
35
+ self.assign_to_pontoon_types(phases_lead=val)
36
+
37
+ @property
38
+ def phi(self):
39
+ if self._phi is not None:
40
+ return self._phi
41
+ else:
42
+ return np.eye(self.ndofs)
43
+
44
+ @phi.setter
45
+ def phi(self, phi):
46
+ self._phi = phi
47
+
48
+
49
+ @property
50
+ def pontoon_types(self):
51
+ ptypes = [p.pontoon_type for p in self.pontoons]
52
+ unique_types = []
53
+ for pt in ptypes:
54
+ if pt not in unique_types:
55
+ unique_types.append(pt)
56
+ return unique_types
57
+
58
+
59
+ def reset_linearized_drag(self):
60
+ for p in self.pontoons:
61
+ p.Cquad_lin = np.diag(np.zeros(6))
62
+
63
+ def assign_direct_cquad(self, cquad_dict=None):
64
+ for p in self.pontoons:
65
+ p.Cquad = np.zeros(6)
66
+
67
+ if cquad_dict is None:
68
+ cquad_dict = {}
69
+
70
+ for pontoon_label in cquad_dict:
71
+ self.get_pontoon(pontoon_label).Cquad = cquad_dict[pontoon_label]
72
+
73
+ def assign_environment(self):
74
+ for pontoon in self.pontoons:
75
+ pontoon.environment = self.environment
76
+
77
+ def assign_to_pontoons(self, **kwargs):
78
+ for p in self.pontoons:
79
+ for k, v in kwargs.items():
80
+ setattr(p, k, v)
81
+
82
+ def assign_to_pontoon_types(self, **kwargs):
83
+ for pt in self.pontoon_types:
84
+ for k, v in kwargs.items():
85
+ setattr(pt, k, v)
86
+
87
+ def get_all_cquad(self, local=False):
88
+
89
+ if local:
90
+ cquad = [p.cquad_local for p in self.pontoons]
91
+ else:
92
+ cquad = [p.cquad for p in self.pontoons]
93
+
94
+ return block_diag(*cquad)
95
+
96
+
97
+
98
+ def get_all_Cd(self):
99
+ Cd = np.zeros(6*len(self.pontoons))
100
+ for ix, p in enumerate(self.pontoons):
101
+ Cd[ix*6:ix*6+6] = p.Cd
102
+
103
+ return Cd
104
+
105
+ def get_all_area(self):
106
+ area = np.zeros(6*len(self.pontoons))
107
+ for ix, p in enumerate(self.pontoons):
108
+ area[ix*6:ix*6+6] = p.area
109
+
110
+ return area
111
+
112
+
113
+ def get_from_all(self, par):
114
+ vals = []
115
+ for p in self.pontoons:
116
+ vals.append(getattr(p, par))
117
+
118
+ return vals
119
+
120
+ def get_pontoon(self, label):
121
+ matching_pontoons = [p for p in self.pontoons if p.label == label]
122
+ if len(matching_pontoons)>1:
123
+ raise ValueError('Multiple matching pontoons')
124
+ elif len(matching_pontoons)==0:
125
+ return None
126
+ else:
127
+ return matching_pontoons[0]
128
+
129
+
130
+ @property
131
+ def nodelabels(self):
132
+ return np.array([pont.node.label for pont in self.pontoons])
133
+
134
+ @property
135
+ def ndofs(self):
136
+ return len(self.pontoons)*6
137
+
138
+ '''
139
+ ENVIRONMENT CLASS
140
+ '''
141
+ class Environment:
142
+ def __init__(self, g=9.80655, rho=1.025e3, depth=np.inf, waterlevel=0):
143
+ self.g = g
144
+ self.rho = rho
145
+ self.depth = depth
146
+ self.waterlevel = waterlevel
147
+
148
+
149
+ '''
150
+ PONTOON CLASS
151
+ '''
152
+
153
+ class Pontoon:
154
+ def __init__(self, node, pontoon_type, rotation=0, label='pontoon',
155
+ current_affects_Q=None, seastate=None, environment=None, current_affects_k=None):
156
+
157
+ self.node = node
158
+ self.pontoon_type = pontoon_type
159
+ self.rotation = rotation
160
+
161
+ self.label = label
162
+
163
+ self.seastate = seastate
164
+ self.environment = environment
165
+ self._current_affects_Q = current_affects_Q
166
+ self._current_affects_k = current_affects_k
167
+
168
+ self.Cquad = np.zeros(6) # local, direct quadratic damping
169
+ self._Cd = None
170
+
171
+
172
+ def __repr__(self):
173
+ return f'Pontoon object: {self.label}'
174
+ def __str__(self):
175
+ return f'Pontoon object: {self.label} at {self.node}'
176
+
177
+ def sea_get(self, parameter):
178
+ return self.seastate.get_at_pos(parameter, self.node.x[0], self.node.x[1])
179
+
180
+
181
+ @property
182
+ def current_affects_Q(self):
183
+ if self._current_affects_Q is not None:
184
+ return self._current_affects_Q
185
+ elif (self.seastate is not None) and ('current_affects_Q' in self.seastate.pontoon_options):
186
+ return self.seastate.pontoon_options['current_affects_Q']
187
+
188
+ @current_affects_Q.setter
189
+ def current_affects_Q(self, val):
190
+ self._current_affects_Q = val
191
+
192
+ @property
193
+ def current_affects_k(self):
194
+ if self._current_affects_k is not None:
195
+ return self._current_affects_k
196
+ elif (self.seastate is not None) and ('current_affects_k' in self.seastate.pontoon_options):
197
+ return self.seastate.pontoon_options['current_affects_k']
198
+
199
+ @current_affects_k.setter
200
+ def current_affects_k(self, val):
201
+ self._current_affects_k = val
202
+
203
+ @property
204
+ def Cquad_lin(self):
205
+ if hasattr(self, '_Cquad_lin'):
206
+ return self._Cquad_lin
207
+ else:
208
+ return np.zeros([6, 6])
209
+
210
+ @Cquad_lin.setter
211
+ def Cquad_lin(self, Cquad_lin):
212
+ self._Cquad_lin = Cquad_lin
213
+
214
+ @property
215
+ def tmat(self):
216
+ return self.get_tmat_from_rotation()
217
+
218
+ @property
219
+ def cquad(self):
220
+ T = self.tmat
221
+ return T.T @ self.cquad_local @ T
222
+
223
+ @property
224
+ def cquad_local(self):
225
+ rho = self.environment.rho
226
+
227
+ # Directly specified C_quad
228
+ if hasattr(self, 'Cquad'): #to ensure backwards compatibility
229
+ Cquad = self.Cquad
230
+ else:
231
+ Cquad = np.zeros(6)
232
+
233
+ # C_quad from drag definitions
234
+ Cquad_Cd = 0.5 * rho * self.Cd * self.area
235
+
236
+ return np.diag(Cquad_Cd + Cquad)
237
+
238
+
239
+ @property
240
+ def Cd(self):
241
+ if hasattr(self, '_Cd') and self._Cd is not None:
242
+ val = self._Cd
243
+ else:
244
+ val = self.pontoon_type.Cd
245
+
246
+ if len(val)==3:
247
+ val = np.hstack([val, [0,0,0]])
248
+ else:
249
+ val = np.array(val)
250
+
251
+ return val
252
+
253
+ @Cd.setter
254
+ def Cd(self, Cd):
255
+ self._Cd = Cd
256
+
257
+ @property
258
+ def area(self):
259
+ val = self.pontoon_type.area
260
+
261
+ if len(val)==3:
262
+ val = np.hstack([val, [0, 0, 0]])
263
+ else:
264
+ val = np.array(val)
265
+
266
+ return val
267
+
268
+ # To simplify expressions inside class:
269
+ @property
270
+ def S(self):
271
+ return self.sea_get('S')
272
+
273
+ @property
274
+ def D(self):
275
+ return self.sea_get('D')
276
+
277
+ @property
278
+ def U(self):
279
+ return self.sea_get('U')
280
+
281
+ @property
282
+ def thetaU(self):
283
+ return self.sea_get('thetaU')
284
+
285
+ @property
286
+ def depth(self):
287
+ return self.sea_get('depth')
288
+
289
+ @property
290
+ def theta0(self):
291
+ return self.sea_get('theta0')
292
+
293
+ @property
294
+ def theta_shift(self):
295
+ return self.sea_get('theta_shift')
296
+
297
+ def doppler_shift_omega(self, omega_k, theta, kappa=None):
298
+ if kappa is None:
299
+ kappa = self.get_kappa(omega_k, theta=theta, U=0.0)
300
+
301
+ omega_shifted = theta*0.0
302
+
303
+ for ix, kappai in enumerate(kappa):
304
+ thetai_abs = theta[ix] + self.theta_shift
305
+ Uproj = self.U * np.cos(self.thetaU - thetai_abs)
306
+ omega_shifted[ix] = omega_k - Uproj * kappai
307
+
308
+ return omega_shifted
309
+
310
+
311
+ def get_kappa(self, omega_k, theta=0.0, U=None):
312
+ if not self.current_affects_k:
313
+ U = 0.0
314
+ elif U is None:
315
+ U = self.U*1.0
316
+
317
+ if U == 0.0:
318
+ kappa = dispersion_relation_scalar(omega_k, self.depth,
319
+ g=self.environment.g)
320
+
321
+ if np.ndim(theta)!=0:
322
+ kappa = kappa*np.ones(len(theta))
323
+ else:
324
+ kappa = theta*0.0
325
+ for ix, thetai in enumerate(theta):
326
+ thetai_abs = thetai + self.theta_shift
327
+ Uproj = U * np.cos(self.thetaU - thetai_abs)
328
+ kappa[ix] = dispersion_relation_scalar(omega_k, h=self.depth,
329
+ g=self.environment.g,
330
+ U=Uproj)
331
+
332
+ return kappa
333
+
334
+ def get_n(self):
335
+ return np.array([np.cos(self.rotation), np.sin(self.rotation), 0])
336
+
337
+ def get_tmat_from_rotation(self, e2=np.array([0, 0, 1])):
338
+ n = self.get_n()
339
+ e1 = np.cross(e2, n)
340
+ return blkdiag(np.vstack([n, e1, e2]), 2)
341
+
342
+
343
+ def get_K(self, omega, local=False):
344
+ if local:
345
+ T = np.eye(6)
346
+ else:
347
+ T = self.tmat
348
+
349
+ if self.pontoon_type.K is not None:
350
+ return T.T @ self.pontoon_type.K(omega) @ T
351
+ else:
352
+ return T*0
353
+
354
+
355
+ def get_C(self, omega, local=False):
356
+ if local:
357
+ T = np.eye(6)
358
+ else:
359
+ T = self.tmat
360
+
361
+
362
+ return T.T @ (self.pontoon_type.C(omega)) @ T
363
+
364
+
365
+ def get_M(self, omega, local=False):
366
+ if local:
367
+ T = np.eye(6)
368
+ else:
369
+ T = self.tmat
370
+
371
+ return T.T @ self.pontoon_type.M(omega) @ T
372
+
373
+
374
+ def get_Q0_legacy(self, omega_k, local=False):
375
+ if local:
376
+ T = np.eye(6)
377
+ else:
378
+ T = self.tmat
379
+
380
+ return T.T @ self.pontoon_type.Q(omega_k)
381
+
382
+
383
+ def get_Q0(self, omega, local=False):
384
+ if local:
385
+ T = np.eye(6)
386
+ else:
387
+ T = self.tmat
388
+
389
+ if np.ndim(omega)==0: #omega is scalar
390
+ return T.T @ self.pontoon_type.Q(omega)
391
+ else:
392
+ Qeval = self.pontoon_type.Q(omega)
393
+ for k in range(len(omega)):
394
+ Qeval[:,:,k] = T.T @ Qeval[:,:,k]
395
+
396
+ return Qeval
397
+
398
+
399
+ def get_Q(self, omega_k, theta, local=False, theta_interpolation='quadratic'):
400
+ if (not self.current_affects_Q) or (self.U == 0.0): #omega is scalar
401
+ Qk = interp1d_angular(self.pontoon_type.theta + self.rotation - self.theta_shift,
402
+ self.get_Q0(omega_k, local=local), axis=1, fill_value=0,
403
+ kind=theta_interpolation, bounds_error=False,
404
+ assume_sorted=False)(theta)
405
+ return Qk
406
+ else:
407
+ omega = self.doppler_shift_omega(omega_k, theta)
408
+ Q0 = self.get_Q0(omega, local=local)
409
+ Qk = np.zeros([6, len(theta)]).astype(complex)
410
+ for ix in range(len(theta)): # TODO: first implementation for loop - will make quicker by avoiding loop later
411
+ Qk[:, ix] = interp1d_angular(self.pontoon_type.theta + self.rotation - self.theta_shift,
412
+ Q0[:, :, ix], axis=1, fill_value=0,
413
+ kind=theta_interpolation, bounds_error=False,
414
+ assume_sorted=False)(theta[ix])
415
+ return Qk
416
+
417
+
418
+ def evaluate_Q(self, omega=None, theta=None,
419
+ theta_interpolation='quadratic', local=False):
420
+
421
+ if omega is None:
422
+ omega = self.pontoon_type.original_omegaQ
423
+
424
+ if theta is None:
425
+ theta = self.pontoon_type.theta
426
+
427
+ if np.ndim(theta)==0:
428
+ theta = np.array([theta])
429
+
430
+ Q = np.zeros([6, len(theta), len(omega)]).astype('complex')
431
+
432
+ for k, omega_k in enumerate(omega):
433
+ Q[:, :, k] = self.get_Q(omega_k, theta,
434
+ local=local, theta_interpolation=theta_interpolation)
435
+
436
+ return Q, theta, omega
437
+
438
+
439
+ def get_coh_2d(self, omega_k, theta, x0=[0,0], sign=-1):
440
+ kappa = self.get_kappa(omega_k, theta=theta)
441
+
442
+ # Minus sign in coh confirmed matching with phases lags in OrcaFlex
443
+
444
+ coh = np.exp(self.pontoon_type.phase_sign*1j*kappa* ((self.node.x[0]-x0[0])*np.cos(theta+self.theta_shift) +
445
+ (self.node.x[1]-x0[1])*np.sin(theta+self.theta_shift)))
446
+ return coh
447
+
448
+
449
+ def get_Z(self, omega_k, theta_int, theta_interpolation='quadratic',
450
+ local=False, x0=[0,0]):
451
+
452
+ if theta_int is None: # => long-crested
453
+ theta_int = np.array([self.theta0 - self.theta_shift])
454
+
455
+ if self.D.__code__.co_argcount==2: # count number of inputs
456
+ D = self.D(theta_int, omega_k)
457
+ else:
458
+ D = self.D(theta_int)
459
+
460
+ # Interpolate hydrodynamic transfer function, get 2d coh and est. Z
461
+ Q_int = self.get_Q(omega_k, theta_int, local=local,
462
+ theta_interpolation=theta_interpolation)
463
+
464
+ coh_2d = self.get_coh_2d(omega_k, theta_int, x0=x0)
465
+ Z = np.sqrt(self.S(omega_k)) * Q_int * np.tile(np.sqrt(D), [6, 1]) * np.tile(coh_2d, [6, 1])
466
+
467
+ return Z
468
+
469
+ def get_theta_int(self, omega_k, ds=None, max_rel_error=0.01):
470
+ if ds is None:
471
+ ds = self.max_distance
472
+
473
+ dtheta = np.min([maxincrement(ds, omega_k, 0, 2*np.pi, max_rel_error), 2*np.pi/20]) #minimum 20 divisions
474
+
475
+ return np.arange(-np.pi, np.pi, dtheta)
476
+
477
+
478
+ '''
479
+ SEASTATE CLASS
480
+ '''
481
+
482
+ class Seastate:
483
+ def __init__(self, Tp, Hs, gamma, theta0, s, depth=None, origin=None, ranges=None,
484
+ options={}, pontoon_options={}, name=None, plot_label=None, centered_dirdist=True,
485
+ U=None, thetaU=None, use_robust_D=True, angle_unit='deg'):
486
+
487
+ if angle_unit == 'deg':
488
+ ang_conv = np.pi/180
489
+
490
+ if origin is None:
491
+ self.origin = np.array([0,0])
492
+ else:
493
+ self.origin = origin
494
+
495
+ if depth is None:
496
+ self.depth = lambda x,y: np.inf
497
+
498
+ if ranges is None:
499
+ self.ranges = dict(x=[-np.inf, np.inf], y=[-np.inf, np.inf])
500
+ else:
501
+ self.ranges = ranges
502
+
503
+ self.name = name
504
+ if plot_label is None:
505
+ self.plot_label = name
506
+ else:
507
+ self.plot_label = plot_label
508
+
509
+ self.pars = ['Tp', 'Hs', 'gamma', 'theta0', 's']
510
+ self.pars_pretty = dict(Tp='$T_p$ [s]', Hs='$H_s$ [m]', gamma=r'$\gamma$', theta0=r'$\theta_0$ $[^\circ]$', s='s')
511
+
512
+ if s is None:
513
+ s = np.inf
514
+ if U is None:
515
+ U = 0.0
516
+ if thetaU is None:
517
+ thetaU = 0.0
518
+
519
+ self.Tp = Tp
520
+ self.Hs = Hs
521
+ self.gamma = gamma
522
+ self.s = s
523
+ self.theta0 = self.scale(theta0, ang_conv)
524
+ self.centered_dirdist = centered_dirdist
525
+ self.use_robust_D = use_robust_D
526
+
527
+ self.U = U
528
+ self.thetaU = self.scale(thetaU, ang_conv)
529
+
530
+ self.x0 = self.origin[0]
531
+ self.y0 = self.origin[1]
532
+ self.options = {'keep_coherence': True}
533
+ self.options.update(**options)
534
+
535
+ self.pontoon_options = {'current_affects_Q': True, 'current_affects_k': True}
536
+ self.pontoon_options.update(**pontoon_options)
537
+
538
+ self.fun_pars = [par for par in self.pars if self.isfun(par)]
539
+
540
+
541
+ def __str__(self):
542
+ string = f'''\
543
+ WAWI SeaState
544
+ --------------
545
+ Hs={self.Hs:.2f}m
546
+ Tp={self.Tp:.2f}s
547
+ theta0={self.theta0*180/np.pi:.1f}deg
548
+ gamma={self.gamma:.2f}
549
+ s={self.s:.1f}
550
+ U (current)={self.U:.1f}m/s
551
+ thetaU (current)={self.thetaU*180/np.pi:.1f}deg
552
+ '''
553
+
554
+ return string
555
+
556
+
557
+ @property
558
+ def homogeneous(self):
559
+ if len(self.fun_pars)==0:
560
+ return True
561
+ else:
562
+ return False
563
+
564
+ @property
565
+ def theta_shift(self):
566
+ if self.centered_dirdist:
567
+ return self.theta0
568
+ else:
569
+ return 0.0
570
+
571
+ @property
572
+ def short_crested(self):
573
+ return self.s not in [np.inf, None]
574
+
575
+ @property
576
+ def D(self):
577
+ if not self.short_crested:
578
+ return lambda x, y: lambda theta: 1*(theta==self.evpar('theta0', x, y)-self.evpar('theta_shift',x,y))
579
+ else:
580
+ if self.use_robust_D:
581
+ Dfun = wawi.wave.dirdist_robust
582
+ else:
583
+ Dfun = wawi.wave.dirdist_decimal
584
+
585
+ return lambda x,y: Dfun(self.evpar('s', x, y), theta0=self.evpar('theta0', x, y)-self.evpar('theta_shift',x,y))
586
+
587
+
588
+ @property
589
+ def S(self):
590
+ return lambda x,y: wawi.wave.jonswap(self.evpar('Hs', x, y),
591
+ self.evpar('Tp', x, y), self.evpar('gamma', x, y))
592
+
593
+ @property
594
+ def theta_int(self):
595
+ if self.short_crested == False:
596
+ th = np.array([self.theta0 - self.theta_shift])
597
+ elif 'truncate_theta' in self.options:
598
+ theta_lims = np.array(self.options['truncate_theta'])
599
+ dtheta = self.options['dtheta']
600
+ th = np.arange(theta_lims[0], theta_lims[1]+dtheta, dtheta)
601
+ else:
602
+ th = None
603
+
604
+ return th
605
+
606
+ def simulate(self, x, y, omega, fs=None, theta=None, phase=None, return_phases=False,
607
+ print_progress=True, time_history=True, grid_mode=True):
608
+
609
+ if theta is None:
610
+ theta = self.theta_int
611
+
612
+ output = xsim(x, y, self.S, self.D, omega, theta=theta, fs=fs,
613
+ grid_mode=grid_mode, print_progress=print_progress, time_history=time_history,
614
+ phase=phase, return_phases=return_phases, theta_shift=self.theta_shift)
615
+
616
+ if return_phases:
617
+ eta, t, phase = output
618
+ return eta, t, phase
619
+ else:
620
+ eta, t = output
621
+ return eta, t
622
+
623
+
624
+ @staticmethod
625
+ def scale(par, factor):
626
+ if isfunction(par):
627
+ if 'x' in par.__code__.co_varnames and 'y' in par.__code__.co_varnames:
628
+ return lambda x,y: par(x, y)*factor
629
+ elif 'x' in par.__code__.co_varnames:
630
+ return lambda x: par(x)*factor
631
+ elif 'y' in par.__code__.co_varnames:
632
+ return lambda y: par(y)*factor
633
+ else:
634
+ return par*factor
635
+
636
+ @classmethod
637
+ def from_json(cls, json_file, **kwargs):
638
+ with open(json_file, 'r') as fileobj:
639
+ data = json.load(fileobj)
640
+
641
+ Tp = eval(data['S']['Tp'])
642
+ Hs = eval(data['S']['Hs'])
643
+ gamma = eval(data['S']['gamma'])
644
+
645
+ theta0 = eval(data['D']['theta0']) #assumed in deg
646
+ s = eval(data['D']['s'])
647
+
648
+ if 'current' in data:
649
+ U = eval(data['current']['U'])
650
+ thetaU = eval(data['current']['thetaU'])
651
+ else:
652
+ U = None
653
+ thetaU = None
654
+
655
+ if 'options' in data:
656
+ options = data['options']
657
+ else:
658
+ options = {}
659
+
660
+ if 'pontoon_options' in data:
661
+ pontoon_options = data['pontoon_options']
662
+ else:
663
+ pontoon_options = {}
664
+
665
+ if 'origin' in data:
666
+ origin = np.array(data['origin'])
667
+ else:
668
+ origin = np.array([0,0])
669
+
670
+ if 'ranges' in data:
671
+ ranges = data['ranges']
672
+ if 'x' not in ranges:
673
+ ranges['x'] = [-np.inf, np.inf]
674
+ if 'y' not in ranges:
675
+ ranges['y'] = [-np.inf, np.inf]
676
+ else:
677
+ ranges = None
678
+
679
+ if 'name' in data:
680
+ name = data['name']
681
+ else:
682
+ name = 'Unnamed seastate'
683
+
684
+ if 'plot_label' in data:
685
+ plot_label = data['plot_label']
686
+ else:
687
+ plot_label = None
688
+
689
+ if 'centered_dirdist' in options:
690
+ centered_dirdist = options.pop('centered_dirdist')
691
+ else:
692
+ centered_dirdist = True
693
+
694
+ return cls(Tp, Hs, gamma, theta0, s, origin=origin, ranges=ranges,
695
+ name=name, plot_label=plot_label, options=options,
696
+ U=U, thetaU=thetaU, pontoon_options=pontoon_options,
697
+ centered_dirdist=centered_dirdist,
698
+ **kwargs)
699
+
700
+
701
+ def evpar(self, parameter, x, y):
702
+
703
+ # Constant extrapolate if outside borders
704
+ if x>np.max(self.ranges['x']): x = np.max(self.ranges['x'])
705
+ if x<np.min(self.ranges['x']): x = np.min(self.ranges['x'])
706
+ if y>np.max(self.ranges['y']): y = np.max(self.ranges['y'])
707
+ if y<np.min(self.ranges['y']): y = np.min(self.ranges['y'])
708
+
709
+ par = getattr(self, parameter)
710
+ if isfunction(par):
711
+ if 'x' in par.__code__.co_varnames and 'y' in par.__code__.co_varnames:
712
+ return par(x - self.x0, y - self.y0)
713
+ elif 'x' in par.__code__.co_varnames:
714
+ return par(x - self.x0)
715
+ elif 'y' in par.__code__.co_varnames:
716
+ return par(y - self.y0)
717
+ else:
718
+ return par
719
+
720
+ def get_at_pos(self, parameter, x, y):
721
+ par = getattr(self, parameter)
722
+ if isfunction(par):
723
+ if 'x' in par.__code__.co_varnames and 'y' in par.__code__.co_varnames:
724
+ return par(x - self.x0, y - self.y0)
725
+ elif 'x' in par.__code__.co_varnames:
726
+ return par(x - self.x0)
727
+ elif 'y' in par.__code__.co_varnames:
728
+ return par(y - self.y0)
729
+ else:
730
+ return par
731
+
732
+
733
+ def isfun(self, parameter):
734
+ return isfunction(getattr(self, parameter))
735
+
736
+ def with_unit(self, parameter):
737
+ return self.pars_pretty[parameter]
738
+
739
+
740
+ '''
741
+ NODE CLASS (MINIMUM CLASS FOR WHEN BEEF MODEL IS NOT APPENDED TO MODEL OBJECT)
742
+ '''
743
+
744
+ class Node:
745
+ def __init__(self, label, x=0.0, y=0.0, z=0.0):
746
+ self.x0 = np.array([x,y,z,0.0,0.0,0.0]).astype(float)
747
+ self.x = np.array([x,y,z,0.0,0.0,0.0]).astype(float)
748
+ self.label = label
749
+
750
+ def __repr__(self):
751
+ return f'Node {self.label}'
752
+
753
+ def __str__(self):
754
+ return f'Node {self.label}'
755
+
756
+
757
+ '''
758
+ PONTOONTYPE CLASS
759
+ '''
760
+
761
+ class PontoonType:
762
+ def __init__(self, K=None, C=None, M=None, Q=None, original_omega=None, original_omegaQ=None,
763
+ theta=None, label='unnamed', Cd=None, area=None, stl_path=None):
764
+ self.K = K
765
+ self.C = C
766
+ self.M = M
767
+ self.Q = Q
768
+ self.original_omega = original_omega
769
+ self.original_omegaQ = original_omegaQ
770
+ self.theta = theta
771
+ self.label = label
772
+ self.stl_path = stl_path
773
+
774
+ self.phases_lead = False
775
+
776
+ self.generate_mesh()
777
+ self._Cd = Cd
778
+ self._area = area
779
+
780
+ def __repr__(self):
781
+ return f'PontoonType object: {self.label}'
782
+ def __str__(self):
783
+ return f'PontoonType object: {self.label}'
784
+
785
+ def generate_mesh(self):
786
+ if self.stl_path is not None:
787
+ stl = pv.STLReader(self.stl_path)
788
+ self.mesh = stl.read().translate([0,0,0])
789
+ else:
790
+ self.mesh = None
791
+
792
+ @property
793
+ def phase_sign(self):
794
+ if hasattr(self, 'phases_lead'):
795
+ return -int(1 - 2*self.phases_lead*1)
796
+ else:
797
+ return -1 #standard is phases lag
798
+
799
+ @property
800
+ def Cd(self):
801
+ if hasattr(self, '_Cd') and self._Cd is not None:
802
+ return self._Cd
803
+ else:
804
+ return np.array([0,0,0,0,0,0])
805
+
806
+
807
+ @Cd.setter
808
+ def Cd(self, Cd):
809
+ self._Cd = Cd
810
+
811
+ @property
812
+ def area(self):
813
+ if hasattr(self, '_area') and self._area is not None:
814
+ return self._area
815
+ else:
816
+ return np.array([0,0,0,0,0,0])
817
+
818
+ @area.setter
819
+ def area(self, area):
820
+ self._area = area
821
+
822
+
823
+ @classmethod
824
+ def from_numeric(cls, interpolation_kind='linear', A=None, M0=None, B=None, Kh=None,
825
+ omega=None, Q=None, theta=None, omegaQ=None, **kwargs):
826
+
827
+ if omegaQ is None:
828
+ omegaQ = omega*1
829
+
830
+ if A is None and M0 is None:
831
+ M = None
832
+ else:
833
+ if A is None:
834
+ A = np.zeros([M0.shape + [len(omega)]])
835
+ elif M0 is None:
836
+ M0 = A[:,:,0]*0
837
+ M = interp1d(omega, (A.T+M0.T).T, axis=2, fill_value='extrapolate', kind=interpolation_kind)
838
+
839
+ if B is None:
840
+ C = None
841
+ else:
842
+ C = interp1d(omega, B, axis=2, fill_value='extrapolate', kind=interpolation_kind)
843
+
844
+ if Kh is not None:
845
+ K = lambda omega: Kh
846
+ else:
847
+ K = None
848
+
849
+ if Q is not None:
850
+ Q = interp1d(omegaQ, Q, fill_value=0.0, kind=interpolation_kind, axis=2, bounds_error=False)
851
+
852
+ return cls(M=M, C=C, K=K, Q=Q, original_omega=omega, original_omegaQ=omegaQ, theta=theta, **kwargs)
853
+
854
+
855
+ @classmethod
856
+ def from_wadam(cls, path, interpolation_kind='linear',
857
+ include=['added mass', 'added damping', 'restoring stiffness', 'inertia'], **kwargs):
858
+
859
+ from wawi.io import import_wadam_mat, import_wadam_hydro_transfer
860
+
861
+ A, B, Kh, M0, omega = import_wadam_mat(path)
862
+ omega_Q, theta, Q = import_wadam_hydro_transfer(path)
863
+
864
+ if 'added mass' not in include:
865
+ A = A*0
866
+
867
+ if 'added damping' not in include:
868
+ B = B*0
869
+
870
+ if 'restoring stiffness' not in include:
871
+ Kh = Kh*0
872
+
873
+ if 'inertia' not in include:
874
+ M0 = M0*0
875
+
876
+ M = interp1d(omega, (A.T+M0.T).T, axis=2, fill_value='extrapolate', kind=interpolation_kind)
877
+ C = interp1d(omega, B, axis=2, fill_value='extrapolate', kind=interpolation_kind)
878
+ K = lambda omega: Kh
879
+
880
+ Q = interp1d(omega_Q, Q, fill_value='extrapolate', kind=interpolation_kind, axis=2)
881
+
882
+ return cls(M=M, C=C, K=K, Q=Q, original_omega=omega, theta=theta, **kwargs)