Skip to content

Commit 34d2d88

Browse files
BUG: WEC from impedance with more than one DOF (sandialabs#440)
--------- Co-authored-by: jtgrasb <[email protected]>
1 parent 5df060b commit 34d2d88

File tree

2 files changed

+151
-43
lines changed

2 files changed

+151
-43
lines changed

tests/test_integration.py

Lines changed: 136 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,27 @@ def nfreq():
2424
"""Number of frequencies in the array"""
2525
return 50
2626

27+
2728
@pytest.fixture(scope='module')
2829
def pto():
2930
"""Basic PTO: unstructured, 1 DOF, mechanical power."""
3031
ndof = 1
3132
kinematics = np.eye(ndof)
3233
controller = wot.controllers.unstructured_controller()
33-
pto = wot.pto.PTO(ndof=ndof,
34-
kinematics=kinematics,
34+
pto = wot.pto.PTO(ndof=ndof,
35+
kinematics=kinematics,
36+
controller=controller)
37+
return pto
38+
39+
40+
@pytest.fixture(scope='module')
41+
def pto_2d():
42+
"""Basic PTO: unstructured, 1 DOF, mechanical power."""
43+
ndof = 1
44+
kinematics = np.array([[1,0],])
45+
controller = wot.controllers.unstructured_controller()
46+
pto = wot.pto.PTO(ndof=ndof,
47+
kinematics=kinematics,
3548
controller=controller)
3649
return pto
3750

@@ -78,6 +91,28 @@ def fb():
7891
return fb
7992

8093

94+
@pytest.fixture(scope="module")
95+
def fb_2d():
96+
"""Capytaine FloatingBody object"""
97+
try:
98+
import wecopttool.geom as geom
99+
except ImportError:
100+
pytest.skip(
101+
'Skipping integration tests due to missing optional geometry ' +
102+
'dependencies. Run `pip install wecopttool[geometry]` to run ' +
103+
'these tests.'
104+
)
105+
mesh_size_factor = 0.2
106+
wb = geom.WaveBot()
107+
mesh = wb.mesh(mesh_size_factor)
108+
mesh_obj = load_from_meshio(mesh, 'WaveBot')
109+
lid_mesh = mesh_obj.generate_lid(-2e-2)
110+
fb = cpy.FloatingBody(mesh=mesh_obj, lid_mesh=lid_mesh, name="WaveBot")
111+
fb.add_translation_dof(name="Heave")
112+
fb.add_translation_dof(name="Surge")
113+
return fb
114+
115+
81116
@pytest.fixture(scope="module")
82117
def hydro_data(f1, nfreq, fb):
83118
"""Boundary element model (Capytaine) results (with friction added)"""
@@ -88,6 +123,16 @@ def hydro_data(f1, nfreq, fb):
88123
return hd
89124

90125

126+
@pytest.fixture(scope="module")
127+
def hydro_data_2d(f1, nfreq, fb_2d):
128+
"""Boundary element model (Capytaine) results (with friction added)"""
129+
freq = wot.frequency(f1, nfreq, False)
130+
hydro_data = wot.run_bem(fb_2d, freq)
131+
hd = wot.add_linear_friction(hydro_data)
132+
hd = wot.check_radiation_damping(hd, min_damping=min_damping)
133+
return hd
134+
135+
91136
@pytest.fixture(scope='module')
92137
def regular_wave(f1, nfreq):
93138
"""Single frequency wave"""
@@ -111,18 +156,35 @@ def long_crested_wave(f1, nfreq):
111156

112157

113158
@pytest.fixture(scope='module')
114-
def wec_from_bem(f1, nfreq, hydro_data, fb, pto):
159+
def wec_from_bem(hydro_data, pto):
115160
"""Simple WEC: 1 DOF, no constraints."""
116161
f_add = {"PTO": pto.force_on_wec}
117162
wec = wot.WEC.from_bem(hydro_data, f_add=f_add)
118163
return wec
119164

120165

166+
@pytest.fixture(scope='module')
167+
def wec_from_bem_2d(hydro_data_2d, pto_2d):
168+
"""Simple WEC: 2 DOF, no constraints."""
169+
f_add = {"PTO": pto_2d.force_on_wec}
170+
wec = wot.WEC.from_bem(hydro_data_2d, f_add=f_add)
171+
return wec
172+
173+
121174
@pytest.fixture(scope='module')
122175
def wec_from_floatingbody(f1, nfreq, fb, pto):
123176
"""Simple WEC: 1 DOF, no constraints."""
124177
f_add = {"PTO": pto.force_on_wec}
125-
wec = wot.WEC.from_floating_body(fb, f1, nfreq, f_add=f_add,
178+
wec = wot.WEC.from_floating_body(fb, f1, nfreq, f_add=f_add,
179+
min_damping=min_damping)
180+
return wec
181+
182+
183+
@pytest.fixture(scope='module')
184+
def wec_from_floatingbody_2d(f1, nfreq, fb_2d, pto_2d):
185+
"""Simple WEC: 2 DOF, no constraints."""
186+
f_add = {"PTO": pto_2d.force_on_wec}
187+
wec = wot.WEC.from_floating_body(fb_2d, f1, nfreq, f_add=f_add,
126188
min_damping=min_damping)
127189
return wec
128190

@@ -140,14 +202,39 @@ def wec_from_impedance(hydro_data, pto, fb):
140202
fb = fb.immersed_part()
141203
mass = bemc['inertia_matrix'].values
142204
hstiff = bemc['hydrostatic_stiffness'].values
143-
K = np.expand_dims(hstiff, 2)
144-
205+
K = np.expand_dims(hstiff, 0)
206+
145207
freqs = omega / (2 * np.pi)
146208
impedance = (A + mass)*(1j*w) + B + K/(1j*w)
147209
exc_coeff = hydro_data['Froude_Krylov_force'] + hydro_data['diffraction_force']
148210
f_add = {"PTO": pto.force_on_wec}
149211

150-
wec = wot.WEC.from_impedance(freqs, impedance, exc_coeff, hstiff, f_add,
212+
wec = wot.WEC.from_impedance(freqs, impedance, exc_coeff, hstiff, f_add,
213+
min_damping=min_damping)
214+
return wec
215+
216+
217+
@pytest.fixture(scope='module')
218+
def wec_from_impedance_2d(hydro_data_2d, pto_2d, fb_2d):
219+
"""Simple WEC: 2 DOF, no constraints."""
220+
bemc = hydro_data_2d.copy()
221+
omega = bemc['omega'].values
222+
w = np.expand_dims(omega, [1,2])
223+
A = bemc['added_mass'].values
224+
B = bemc['radiation_damping'].values
225+
fb_2d.center_of_mass = [0, 0, 0]
226+
fb_2d.rotation_center = fb_2d.center_of_mass
227+
fb_2d = fb_2d.immersed_part()
228+
mass = bemc['inertia_matrix'].values
229+
hstiff = bemc['hydrostatic_stiffness'].values
230+
K = np.expand_dims(hstiff, 0)
231+
232+
freqs = omega / (2 * np.pi)
233+
impedance = (A + mass)*(1j*w) + B + K/(1j*w)
234+
exc_coeff = hydro_data_2d['Froude_Krylov_force'] + hydro_data_2d['diffraction_force']
235+
f_add = {"PTO": pto_2d.force_on_wec}
236+
237+
wec = wot.WEC.from_impedance(freqs, impedance, exc_coeff, hstiff, f_add,
151238
min_damping=min_damping)
152239
return wec
153240

@@ -222,7 +309,28 @@ def test_same_wec_init(wec_from_bem,
222309
bem_res = wec_from_bem.residual(x_wec_0, x_opt_0, waves.sel(realization=0))
223310
fb_res = wec_from_floatingbody.residual(x_wec_0, x_opt_0, waves.sel(realization=0))
224311
imp_res = wec_from_impedance.residual(x_wec_0, x_opt_0, waves.sel(realization=0))
225-
312+
313+
assert fb_res == approx(bem_res, rel=0.01)
314+
assert imp_res == approx(bem_res, rel=0.01)
315+
316+
317+
def test_same_wec_init_2d(wec_from_bem_2d,
318+
wec_from_floatingbody_2d,
319+
wec_from_impedance_2d,
320+
f1,
321+
nfreq):
322+
"""Test that different init methods for WEC class produce the same object
323+
"""
324+
325+
waves = wot.waves.regular_wave(f1, nfreq, 0.3, 0.0625)
326+
np.random.seed(0)
327+
x_wec_0 = np.random.randn(wec_from_bem_2d.nstate_wec)
328+
np.random.seed(1)
329+
x_opt_0 = np.random.randn(wec_from_bem_2d.nstate_wec)
330+
bem_res = wec_from_bem_2d.residual(x_wec_0, x_opt_0, waves.sel(realization=0))
331+
fb_res = wec_from_floatingbody_2d.residual(x_wec_0, x_opt_0, waves.sel(realization=0))
332+
imp_res = wec_from_impedance_2d.residual(x_wec_0, x_opt_0, waves.sel(realization=0))
333+
226334
assert fb_res == approx(bem_res, rel=0.01)
227335
assert imp_res == approx(bem_res, rel=0.01)
228336

@@ -246,7 +354,7 @@ def hydro_impedance(self, hydro_data):
246354
"""Intrinsic hydrodynamic impedance"""
247355
Zi = wot.hydrodynamic_impedance(hydro_data)
248356
return Zi
249-
357+
250358
@pytest.fixture(scope='class')
251359
def unstruct_wec(self,
252360
hydro_data,
@@ -265,7 +373,7 @@ def long_crested_wave_unstruct_res(self,
265373
pto,
266374
hydro_data,
267375
nfreq):
268-
"""Solution for an unstructured controller with multiple long crested
376+
"""Solution for an unstructured controller with multiple long crested
269377
waves"""
270378

271379
f_add = {"PTO": pto.force_on_wec}
@@ -289,7 +397,7 @@ def test_p_controller_resonant_wave(self,
289397
hydro_impedance):
290398
"""Proportional controller should match optimum for natural resonant
291399
wave"""
292-
400+
293401
f_add = {"PTO": p_controller_pto.force_on_wec}
294402
wec = wot.WEC.from_bem(hydro_data, f_add=f_add)
295403

@@ -346,7 +454,7 @@ def test_pi_controller_regular_wave(self,
346454
).squeeze().sum('omega').item()
347455

348456
assert power_sol == approx(power_optimal, rel=1e-4)
349-
457+
350458
def test_unstructured_controller_long_crested_wave(self,
351459
unstruct_wec,
352460
long_crested_wave,
@@ -358,8 +466,8 @@ def test_unstructured_controller_long_crested_wave(self,
358466

359467
power_sol = -1*long_crested_wave_unstruct_res[0]['fun']
360468

361-
res_fd, _ = unstruct_wec.post_process(unstruct_wec, long_crested_wave_unstruct_res,
362-
long_crested_wave,
469+
res_fd, _ = unstruct_wec.post_process(unstruct_wec, long_crested_wave_unstruct_res,
470+
long_crested_wave,
363471
nsubsteps=1)
364472
Fex = res_fd.force.sel(realization=0,
365473
type=['Froude_Krylov', 'diffraction']).sum('type')
@@ -370,7 +478,7 @@ def test_unstructured_controller_long_crested_wave(self,
370478

371479
def test_unconstrained_solutions_multiple_phase_realizations(self,
372480
long_crested_wave_unstruct_res):
373-
"""Solutions for average power with an unstructured controller
481+
"""Solutions for average power with an unstructured controller
374482
(no constraints) match for different phase realizations"""
375483

376484
pow = [res['fun'] for res in long_crested_wave_unstruct_res]
@@ -390,27 +498,27 @@ def test_saturated_pi_controller(self,
390498
pto = {}
391499
wec = {}
392500
nstate_opt = {}
393-
501+
394502
# Constraint
395503
f_max = 2000.0
396504

397505
nstate_opt['us'] = 2*nfreq
398506
pto['us'] = pto_tmp
399507
def const_f_pto(wec, x_wec, x_opt, wave):
400-
f = pto['us'].force_on_wec(wec, x_wec, x_opt, wave,
508+
f = pto['us'].force_on_wec(wec, x_wec, x_opt, wave,
401509
nsubsteps=4)
402510
return f_max - np.abs(f.flatten())
403511
wec['us'] = wot.WEC.from_bem(hydro_data,
404512
f_add={"PTO": pto['us'].force_on_wec},
405513
constraints=[{'type': 'ineq',
406514
'fun': const_f_pto, }])
407-
408-
515+
516+
409517
ndof = 1
410518
nstate_opt['pi'] = 2
411519
# def saturated_pi(pto, wec, x_wec, x_opt, waves=None, nsubsteps=1):
412-
# return wot.pto.controller_pi(pto, wec, x_wec, x_opt, waves,
413-
# nsubsteps,
520+
# return wot.pto.controller_pi(pto, wec, x_wec, x_opt, waves,
521+
# nsubsteps,
414522
# saturation=[-f_max, f_max])
415523
saturated_pi = wot.controllers.pid_controller(1,True,True,False,
416524
saturation=[-f_max, f_max])
@@ -420,7 +528,7 @@ def const_f_pto(wec, x_wec, x_opt, wave):
420528
wec['pi'] = wot.WEC.from_bem(hydro_data,
421529
f_add={"PTO": pto['pi'].force_on_wec},
422530
constraints=[])
423-
531+
424532
x_opt_0 = {'us': np.ones(nstate_opt['us'])*0.1,
425533
'pi': [-1e3, 1e4]}
426534
scale_x_wec = {'us': 1e1,
@@ -431,7 +539,7 @@ def const_f_pto(wec, x_wec, x_opt, wave):
431539
'pi': 1e-2}
432540
bounds_opt = {'us': None,
433541
'pi': ((-1e4, 0), (0, 2e4),)}
434-
542+
435543
res = {}
436544
pto_fdom = {}
437545
pto_tdom = {}
@@ -447,13 +555,13 @@ def const_f_pto(wec, x_wec, x_opt, wave):
447555
optim_options={'maxiter': 200},
448556
bounds_opt=bounds_opt[key]
449557
)
450-
558+
451559
nsubstep_postprocess = 4
452-
pto_fdom[key], pto_tdom[key] = pto[key].post_process(wec[key],
453-
res[key],
454-
regular_wave,
560+
pto_fdom[key], pto_tdom[key] = pto[key].post_process(wec[key],
561+
res[key],
562+
regular_wave,
455563
nsubstep_postprocess)
456-
564+
457565
xr.testing.assert_allclose(
458566
pto_tdom['pi'].sel(realization=0).power.squeeze().mean('time'),
459567
pto_tdom['us'].sel(realization=0).power.squeeze().mean('time'),

wecopttool/core.py

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -507,7 +507,7 @@ def from_impedance(
507507
Complex impedance of size :python:`(nfreq, ndof, ndof)`.
508508
exc_coeff
509509
Complex excitation transfer function of size
510-
:python:`(ndof, nfreq)`.
510+
:python:`(nfreq, nwavedir, ndof)`.
511511
hydrostatic_stiffness
512512
Linear hydrostatic restoring coefficient of size
513513
:python:`(ndof, ndof)`.
@@ -553,7 +553,7 @@ def from_impedance(
553553
omega = freqs * 2*np.pi
554554
omega0 = np.expand_dims(omega, [1,2])
555555
transfer_func = impedance * (1j*omega0)
556-
transfer_func0 = np.expand_dims(hydrostatic_stiffness, 2)
556+
transfer_func0 = np.expand_dims(hydrostatic_stiffness, 0)
557557
transfer_func = np.concatenate([transfer_func0, transfer_func], axis=0)
558558
transfer_func = -1 * transfer_func # RHS of equation: ma = Σf
559559
force_impedance = force_from_rao_transfer_function(transfer_func)
@@ -586,9 +586,9 @@ def residual(self, x_wec: ndarray, x_opt: ndarray, wave: DataArray,
586586
x_opt
587587
Optimization (control) state.
588588
wave
589-
2D :py:class:`xarray.DataArray` containing the wave's complex
590-
amplitude for a single realization as a function of wave
591-
angular frequency :python:`omega` (rad/s) and direction
589+
2D :py:class:`xarray.DataArray` containing the wave's complex
590+
amplitude for a single realization as a function of wave
591+
angular frequency :python:`omega` (rad/s) and direction
592592
:python:`wave_direction` (rad).
593593
"""
594594
if not self.inertia_in_forces:
@@ -2032,8 +2032,8 @@ def force_from_wave(force_coeff: DataArray,
20322032
Parameters
20332033
----------
20342034
force_coeff
2035-
Complex excitation coefficients indexed by frequency and
2036-
direction angle.
2035+
Complex excitation coefficients indexed by frequency,
2036+
direction angle, and degree of freedom.
20372037
"""
20382038
def force(wec, x_wec, x_opt, wave):
20392039
force_fd = complex_to_real(wave_excitation(force_coeff, wave), False)
@@ -2197,7 +2197,7 @@ def run_bem(
21972197
'will be calculated assuming a solid and constant ' +
21982198
'density body.')
21992199
else:
2200-
_log.warning('FloatingBody has no inertia_matrix field. ' +
2200+
_log.warning('FloatingBody has no inertia_matrix field. ' +
22012201
'The FloatingBody mass is defined and will be ' +
22022202
'used for calculating the inertia matrix.')
22032203
wec_im.inertia_matrix = wec_im.compute_rigid_body_inertia(rho=rho)
@@ -2289,12 +2289,12 @@ def wave_excitation(exc_coeff: DataArray, wave: DataArray) -> ndarray:
22892289
Parameters
22902290
----------
22912291
exc_coeff
2292-
Complex excitation coefficients indexed by frequency and
2293-
direction angle.
2292+
Complex excitation coefficients indexed by frequency,
2293+
direction angle, and degree of freedom.
22942294
wave
2295-
2D :py:class:`xarray.DataArray` containing the wave's complex
2296-
amplitude for a single realization as a function of wave
2297-
angular frequency :python:`omega` (rad/s) and direction
2295+
2D :py:class:`xarray.DataArray` containing the wave's complex
2296+
amplitude for a single realization as a function of wave
2297+
angular frequency :python:`omega` (rad/s) and direction
22982298
:python:`wave_direction` (rad).
22992299
23002300
Raises
@@ -2607,8 +2607,8 @@ def set_fb_centers(
26072607
log_str = (
26082608
"Using the center of gravity (COG) as the rotation center " +
26092609
"for hydrostatics. Note that the hydrostatics do not use the " +
2610-
"axes defined by the FloatingBody degrees of freedom, and the " +
2611-
"rotation center should be set manually when using Capytaine to " +
2610+
"axes defined by the FloatingBody degrees of freedom, and the " +
2611+
"rotation center should be set manually when using Capytaine to " +
26122612
"calculate hydrostatics about an axis other than the COG.")
26132613
setattr(fb, property, def_val)
26142614
_log.warning(log_str)

0 commit comments

Comments
 (0)