• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

sandialabs / WecOptTool / 9120563447

16 May 2024 11:34PM UTC coverage: 94.538% (-0.09%) from 94.631%
9120563447

Pull #342

github

web-flow
Merge c8912d06b into c21878006
Pull Request #342: ENHANCEMENT: Implemented cyipopt minimize_ipopt

408 of 423 new or added lines in 7 files covered. (96.45%)

11 existing lines in 1 file now uncovered.

2752 of 2911 relevant lines covered (94.54%)

5.67 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

83.21
/wecopttool/pto.py
1
"""Provide power take-off (PTO) forces and produced energy functions
2
for common PTO control approaches.
3

4
The PTO produced energy can be used as the objective function for the
5
control optimization.
6
The PTO force can be included as an additional force in the WEC
7
dynamics.
8

9
Contains:
10

11
* The *PTO* class
12
* Controller functions
13

14
"""
15

16

17
from __future__ import annotations
6✔
18

19

20
__all__ = [
6✔
21
    "PTO",
22
    "controller_unstructured",
23
    "controller_pid",
24
    "controller_pi",
25
    "controller_p",
26
]
27

28

29
from typing import Optional, TypeVar, Callable, Union
6✔
30

31
import autograd.numpy as np
6✔
32
from autograd.builtins import isinstance, tuple, list, dict
6✔
33
from autograd.numpy import ndarray
6✔
34
from scipy.linalg import block_diag
6✔
35
from scipy.optimize import OptimizeResult
6✔
36
from xarray import DataArray, Dataset
6✔
37
from datetime import datetime
6✔
38
from scipy.optimize import OptimizeResult
6✔
39

40
from wecopttool.core import complex_to_real, td_to_fd
6✔
41
from wecopttool.core import dofmat_to_vec, vec_to_dofmat
6✔
42
from wecopttool.core import TWEC, TStateFunction, FloatOrArray
6✔
43

44

45
# type aliases
46
TPTO = TypeVar("TPTO", bound="PTO")
6✔
47
TLOSS = Callable[[FloatOrArray, FloatOrArray], FloatOrArray]
6✔
48

49

50
class PTO:
6✔
51
    """A power take-off (PTO) object to be used in conjunction with a
52
    :py:class:`wecopttool.WEC` object.
53
    """
54

55
    def __init__(self,
6✔
56
        ndof: int,
57
        kinematics: Union[TStateFunction, ndarray],
58
        controller: Optional[TStateFunction] = None,
59
        impedance: Optional[ndarray] = None,
60
        loss: Optional[TLOSS] = None,
61
        names: Optional[list[str]] = None,
62
    ) -> None:
63
        """Create a PTO object.
64

65
        The :py:class:`wecopttool.pto.PTO` class describes the
66
        kinematics, control logic, impedance and/or non-linear power
67
        loss of a power take-off system.
68
        The forces/moments applied by a
69
        :py:class:`wecopttool.pto.PTO` object can be applied to a
70
        :py:class:`wecopttool.WEC` object through the
71
        :py:attr:`wecopttool.WEC.f_add` property.
72
        The power produced by a :py:class:`wecopttool.pto.PTO` object
73
        can be used for the :python:`obj_fun` of pseudo-spectral
74
        optimization problem when calling
75
        :py:meth:`wecopttool.WEC.solve`.
76

77
        Parameters
78
        ----------
79
        ndof
80
            Number of degrees of freedom.
81
        kinematics
82
            Transforms state from WEC to PTO frame. May be a matrix
83
            (for linear kinematics) or function (for nonlinear
84
            kinematics).
85
        controller
86
            Function with signature
87
            :python:`def fun(pto, wec, x_wec, x_opt, waves, nsubsteps):`
88
            or matrix with shape (PTO DOFs, WEC DOFs) that converts
89
            from the WEC DOFs to the PTO DOFs.
90
        impedance
91
            Matrix representing the PTO impedance.
92
        loss
93
            Function that maps flow and effort variables to a
94
            non-linear power loss.
95
            The output is the dissipated power (loss) in Watts.
96
            This should be a positive value.
97
        names
98
            PTO names.
99
        """
100
        self._ndof = ndof
6✔
101
        # names
102
        if names is None:
6✔
103
            names = [f'PTO_{i}' for i in range(ndof)]
6✔
104
        elif ndof == 1 and isinstance(names, str):
6✔
105
            names = [names]
×
106
        self._names = names
6✔
107
        # kinematics
108
        if callable(kinematics):
6✔
109
            def kinematics_fun(wec, x_wec, x_opt, waves, nsubsteps=1):
×
110
                pos_wec = wec.vec_to_dofmat(x_wec)
×
111
                tmat = self._tmat(wec, nsubsteps)
×
112
                pos_wec_td = np.dot(tmat, pos_wec)
×
113
                return kinematics(pos_wec_td)
×
114
        else:
115
            def kinematics_fun(wec, x_wec, x_opt, waves, nsubsteps=1):
6✔
116
                n = wec.nt*nsubsteps
6✔
117
                return np.repeat(kinematics[:, :, np.newaxis], n, axis=-1)
6✔
118
        self._kinematics = kinematics_fun
6✔
119
        # controller
120
        if controller is None:
6✔
121
            controller = controller_unstructured
6✔
122

123
        def force(wec, x_wec, x_opt, waves, nsubsteps=1):
6✔
124
            return controller(self, wec, x_wec, x_opt, waves, nsubsteps)
6✔
125

126
        self._force = force
6✔
127

128
        # power
129
        if impedance is not None:
6✔
130
            check_1 = impedance.shape[0] == impedance.shape[1] == 2*self.ndof
×
131
            check_2 = len(impedance.shape) == 3
×
132
            if not (check_1 and check_2):
×
133
                raise TypeError(
×
134
                    "Impedance should have size [2*ndof, 2*ndof, nfreq]"
135
                )
136
            for i in range(impedance.shape[2]-1):
×
137
                check_3 = (
×
138
                    np.allclose(np.real(impedance[:, :, i+1]), np.real(impedance[:, :, 0]))
139
                )
140
                if not check_3:
×
141
                    raise ValueError(
×
142
                        "Real component of impedance must be constant for " +
143
                        "all frequencies."
144
                    )
145
            impedance_abcd = _make_abcd(impedance, ndof)
×
146
            self._transfer_mat = _make_mimo_transfer_mat(impedance_abcd, ndof)
×
147
        else:
148
            self._transfer_mat = None
6✔
149
        self._impedance = impedance
6✔
150
        self._loss = loss
6✔
151

152
    @property
6✔
153
    def ndof(self) -> int:
6✔
154
        """Number of degrees of freedom."""
155
        return self._ndof
6✔
156

157
    @property
6✔
158
    def names(self) -> ndarray:
6✔
159
        """DOF Names."""
160
        return self._names
6✔
161

162
    @property
6✔
163
    def kinematics(self) -> TStateFunction:
6✔
164
        """Kinematics function.
165
        """
166
        return self._kinematics
6✔
167

168
    @property
6✔
169
    def force(self) -> TStateFunction:
6✔
170
        """PTO force in PTO coordinates."""
171
        return self._force
6✔
172

173
    @property
6✔
174
    def impedance(self) -> ndarray:
6✔
175
        """Impedance matrix."""
176
        return self._impedance
6✔
177

178
    @property
6✔
179
    def loss(self) -> TLOSS:
6✔
180
        """Nonlinear power loss function with outputs in Watts."""
181
        return self._loss
6✔
182

183
    @property
6✔
184
    def transfer_mat(self) -> ndarray:
6✔
185
        """Transfer matrix."""
186
        return self._transfer_mat
×
187

188
    def _tmat(self, wec, nsubsteps: Optional[int] = 1):
6✔
189
        if nsubsteps==1:
6✔
190
            tmat = wec.time_mat
6✔
191
        else:
192
            tmat = wec.time_mat_nsubsteps(nsubsteps)
6✔
193
        return tmat
6✔
194

195
    def _fkinematics(self,
6✔
196
        f_wec: ndarray,
197
        wec: TWEC,
198
        x_wec: ndarray,
199
        x_opt: Optional[ndarray] = None,
200
        waves: Optional[Dataset] = None,
201
        nsubsteps: Optional[int] = 1,
202
    ) -> ndarray:
203
        """Return time-domain values in the PTO frame.
204

205
        Parameters
206
        ----------
207
        f_wec
208
            Fourier coefficients of some quantity "f" in the WEC frame.
209
        wec
210
            :py:class:`wecopttool.WEC` object.
211
        x_wec
212
            WEC dynamic state.
213
        x_opt
214
            Optimization (control) state.
215
        waves
216
            :py:class:`xarray.Dataset` with the structure and elements
217
            shown by :py:mod:`wecopttool.waves`.
218
        nsubsteps
219
            Number of steps between the default (implied) time steps.
220
            A value of :python:`1` corresponds to the default step
221
            length.
222
        """
223
        time_mat = self._tmat(wec, nsubsteps)
6✔
224
        f_wec_td = np.dot(time_mat, f_wec)
6✔
225
        assert f_wec_td.shape == (wec.nt*nsubsteps, wec.ndof)
6✔
226
        f_wec_td = np.expand_dims(np.transpose(f_wec_td), axis=0)
6✔
227
        kinematics_mat = self.kinematics(wec, x_wec, x_opt, waves, nsubsteps)
6✔
228
        return np.transpose(np.sum(kinematics_mat*f_wec_td, axis=1))
6✔
229

230
    def position(self,
6✔
231
        wec: TWEC,
232
        x_wec: ndarray,
233
        x_opt: ndarray,
234
        waves: Optional[Dataset] = None,
235
        nsubsteps: Optional[int] = 1,
236
    ) -> ndarray:
237
        """Calculate the PTO position time-series.
238

239
        Parameters
240
        ----------
241
        wec
242
            :py:class:`wecopttool.WEC` object.
243
        x_wec
244
            WEC dynamic state.
245
        x_opt
246
            Optimization (control) state.
247
        waves
248
            :py:class:`xarray.Dataset` with the structure and elements
249
            shown by :py:mod:`wecopttool.waves`.
250
        nsubsteps
251
            Number of steps between the default (implied) time steps.
252
            A value of :python:`1` corresponds to the default step
253
            length.
254
        """
255
        pos_wec = wec.vec_to_dofmat(x_wec)
6✔
256
        return self._fkinematics(pos_wec, wec, x_wec, x_opt, waves, nsubsteps)
6✔
257

258
    def velocity(self,
6✔
259
        wec: TWEC,
260
        x_wec: ndarray,
261
        x_opt: ndarray,
262
        waves: Optional[Dataset] = None,
263
        nsubsteps: Optional[int] = 1,
264
    ) -> ndarray:
265
        """Calculate the PTO velocity time-series.
266

267
        Parameters
268
        ----------
269
        wec
270
            :py:class:`wecopttool.WEC` object.
271
        x_wec
272
            WEC dynamic state.
273
        x_opt
274
            Optimization (control) state.
275
        waves
276
            :py:class:`xarray.Dataset` with the structure and elements
277
            shown by :py:mod:`wecopttool.waves`.
278
        nsubsteps
279
            Number of steps between the default (implied) time steps.
280
            A value of :python:`1` corresponds to the default step
281
            length.
282
        """
283
        pos_wec = wec.vec_to_dofmat(x_wec)
6✔
284
        vel_wec = np.dot(wec.derivative_mat, pos_wec)
6✔
285
        return self._fkinematics(vel_wec, wec, x_wec, x_opt, waves, nsubsteps)
6✔
286

287
    def acceleration(self,
6✔
288
        wec: TWEC,
289
        x_wec: ndarray,
290
        x_opt: ndarray,
291
        waves: Optional[Dataset] = None,
292
        nsubsteps: Optional[int] = 1,
293
    ) -> np.ndarray:
294
        """Calculate the PTO acceleration time-series.
295

296
        Parameters
297
        ----------
298
        wec
299
            :py:class:`wecopttool.WEC` object.
300
        x_wec
301
            WEC dynamic state.
302
        x_opt
303
            Optimization (control) state.
304
        waves
305
            :py:class:`xarray.Dataset` with the structure and elements
306
            shown by :py:mod:`wecopttool.waves`.
307
        nsubsteps
308
            Number of steps between the default (implied) time steps.
309
            A value of :python:`1` corresponds to the default step
310
            length.
311
        """
312
        pos_wec = wec.vec_to_dofmat(x_wec)
6✔
313
        acc_wec = np.dot(wec.derivative2_mat, pos_wec)
6✔
314
        return self._fkinematics(acc_wec, wec, x_wec, x_opt, waves, nsubsteps)
6✔
315

316
    def force_on_wec(self,
6✔
317
        wec: TWEC,
318
        x_wec: ndarray,
319
        x_opt: ndarray,
320
        waves: Optional[Dataset] = None,
321
        nsubsteps: Optional[int] = 1,
322
    ) -> ndarray:
323
        """Calculate the PTO force on WEC.
324

325
        Parameters
326
        ----------
327
        wec
328
            :py:class:`wecopttool.WEC` object.
329
        x_wec
330
            WEC dynamic state.
331
        x_opt
332
            Optimization (control) state.
333
        waves
334
            :py:class:`xarray.Dataset` with the structure and elements
335
            shown by :py:mod:`wecopttool.waves`.
336
        nsubsteps
337
            Number of steps between the default (implied) time steps.
338
            A value of :python:`1` corresponds to the default step
339
            length.
340
        """
341
        force_td = self.force(wec, x_wec, x_opt, waves, nsubsteps)
6✔
342
        assert force_td.shape == (wec.nt*nsubsteps, self.ndof)
6✔
343
        force_td = np.expand_dims(np.transpose(force_td), axis=0)
6✔
344
        assert force_td.shape == (1, self.ndof, wec.nt*nsubsteps)
6✔
345
        kinematics_mat = self.kinematics(wec, x_wec, x_opt, waves, nsubsteps)
6✔
346
        kinematics_mat = np.transpose(kinematics_mat, (1,0,2))
6✔
347
        return np.transpose(np.sum(kinematics_mat*force_td, axis=1))
6✔
348

349
    def mechanical_power(self,
6✔
350
        wec: TWEC,
351
        x_wec: ndarray,
352
        x_opt: ndarray,
353
        waves: Optional[Dataset] = None,
354
        nsubsteps: Optional[int] = 1,
355
    ) -> np.ndarray:
356
        """Calculate the mechanical power time-series in each PTO DOF
357
        for a given system state.
358

359
        Parameters
360
        ----------
361
        wec
362
            :py:class:`wecopttool.WEC` object.
363
        x_wec
364
            WEC dynamic state.
365
        x_opt
366
            Optimization (control) state.
367
        waves
368
            :py:class:`xarray.Dataset` with the structure and elements
369
            shown by :py:mod:`wecopttool.waves`.
370
        nsubsteps
371
            Number of steps between the default (implied) time steps.
372
            A value of :python:`1` corresponds to the default step
373
            length.
374
        """
375
        force_td = self.force(wec, x_wec, x_opt, waves, nsubsteps)
6✔
376
        vel_td = self.velocity(wec, x_wec, x_opt, waves, nsubsteps)
6✔
377
        return vel_td * force_td
6✔
378

379
    def mechanical_energy(self,
6✔
380
        wec: TWEC,
381
        x_wec: ndarray,
382
        x_opt: ndarray,
383
        waves: Optional[Dataset] = None,
384
        nsubsteps: Optional[int] = 1,
385
    ) -> float:
386
        """Calculate the mechanical energy in each PTO DOF for a given
387
        system state.
388

389
        Parameters
390
        ----------
391
        wec
392
            :py:class:`wecopttool.WEC` object.
393
        x_wec
394
            WEC dynamic state.
395
        x_opt
396
            Optimization (control) state.
397
        waves
398
            :py:class:`xarray.Dataset` with the structure and elements
399
            shown by :py:mod:`wecopttool.waves`.
400
        nsubsteps
401
            Number of steps between the default (implied) time steps.
402
            A value of :python:`1` corresponds to the default step
403
            length.
404
        """
405
        power_td = self.mechanical_power(wec, x_wec, x_opt, waves, nsubsteps)
6✔
406
        return np.sum(power_td) * wec.dt/nsubsteps
6✔
407

408
    def mechanical_average_power(self,
6✔
409
        wec: TWEC,
410
        x_wec: ndarray,
411
        x_opt: ndarray,
412
        waves: Optional[Dataset] = None,
413
        nsubsteps: Optional[int] = 1,
414
    ) -> float:
415
        """Calculate average mechanical power in each PTO DOF for a
416
        given system state.
417

418
        Parameters
419
        ----------
420
        wec
421
            :py:class:`wecopttool.WEC` object.
422
        x_wec
423
            WEC dynamic state.
424
        x_opt
425
            Optimization (control) state.
426
        waves
427
            :py:class:`xarray.Dataset` with the structure and elements
428
            shown by :py:mod:`wecopttool.waves`.
429
        nsubsteps
430
            Number of steps between the default (implied) time steps.
431
            A value of :python:`1` corresponds to the default step
432
            length.
433
        """
434
        energy = self.mechanical_energy(wec, x_wec, x_opt, waves, nsubsteps)
6✔
435
        return energy / wec.tf
6✔
436

437
    def power_variables(self,
6✔
438
        wec: TWEC,
439
        x_wec: ndarray,
440
        x_opt: ndarray,
441
        waves: Optional[Dataset] = None,
442
        nsubsteps: Optional[int] = 1,
443
    ) -> tuple[ndarray, ndarray]:
444
        """Calculate the power variables (flow q and effort e) time-series
445
        in each PTO DOF for a given system state.
446

447
        Parameters
448
        ----------
449
        wec
450
            :py:class:`wecopttool.WEC` object.
451
        x_wec
452
            WEC dynamic state.
453
        x_opt
454
            Optimization (control) state.
455
        waves
456
            :py:class:`xarray.Dataset` with the structure and elements
457
            shown by :py:mod:`wecopttool.waves`.
458
        nsubsteps
459
            Number of steps between the default (implied) time steps.
460
            A value of :python:`1` corresponds to the default step
461
            length.
462
        """
463
        # convert q1 (PTO velocity), e1 (PTO force)
464
        # to q2 (flow variable), e2 (effort variable)
465
        if self.impedance is not None:
6✔
466
            q1_td = self.velocity(wec, x_wec, x_opt, waves)
×
467
            e1_td = self.force(wec, x_wec, x_opt, waves)
×
468
            q1 = complex_to_real(td_to_fd(q1_td, False))
×
469
            e1 = complex_to_real(td_to_fd(e1_td, False))
×
470
            vars_1 = np.hstack([q1, e1])
×
471
            vars_1_flat = dofmat_to_vec(vars_1)
×
472
            vars_2_flat = np.dot(self.transfer_mat, vars_1_flat)
×
473
            vars_2 = vec_to_dofmat(vars_2_flat, 2*self.ndof)
×
474
            q2 = vars_2[:, :self.ndof]
×
475
            e2 = vars_2[:, self.ndof:]
×
476
            time_mat = self._tmat(wec, nsubsteps)
×
477
            q2_td = np.dot(time_mat, q2)
×
478
            e2_td = np.dot(time_mat, e2)
×
479
        else:
480
            q2_td = self.velocity(wec, x_wec, x_opt, waves, nsubsteps)
6✔
481
            e2_td = self.force(wec, x_wec, x_opt, waves, nsubsteps)
6✔
482
        return q2_td, e2_td
6✔
483

484
    def power(self,
6✔
485
        wec: TWEC,
486
        x_wec: ndarray,
487
        x_opt: ndarray,
488
        waves: Optional[Dataset] = None,
489
        nsubsteps: Optional[int] = 1,
490
    ) -> ndarray:
491
        """Calculate the power time-series in each PTO DOF for a given
492
        system state.
493

494
        Parameters
495
        ----------
496
        wec
497
            :py:class:`wecopttool.WEC` object.
498
        x_wec
499
            WEC dynamic state.
500
        x_opt
501
            Optimization (control) state.
502
        waves
503
            :py:class:`xarray.Dataset` with the structure and elements
504
            shown by :py:mod:`wecopttool.waves`.
505
        nsubsteps
506
            Number of steps between the default (implied) time steps.
507
            A value of :python:`1` corresponds to the default step
508
            length.
509
        """
510
        q2_td, e2_td = self.power_variables(wec, x_wec,
6✔
511
                                            x_opt, waves, nsubsteps)
512
        # power
513
        power_out = q2_td * e2_td
6✔
514
        if self.loss is not None:
6✔
515
            power_out = power_out + self.loss(q2_td, e2_td)
×
516
        return power_out
6✔
517

518
    def energy(self,
6✔
519
        wec: TWEC,
520
        x_wec: ndarray,
521
        x_opt: ndarray,
522
        waves: Optional[Dataset] = None,
523
        nsubsteps: Optional[int] = 1,
524
    ) -> float:
525
        """Calculate the energy in each PTO DOF for a given system
526
        state.
527

528
        Parameters
529
        ----------
530
        wec
531
            :py:class:`wecopttool.WEC` object.
532
        x_wec
533
            WEC dynamic state.
534
        x_opt
535
            Optimization (control) state.
536
        waves
537
            :py:class:`xarray.Dataset` with the structure and elements
538
            shown by :py:mod:`wecopttool.waves`.
539
        nsubsteps
540
            Number of steps between the default (implied) time steps.
541
            A value of :python:`1` corresponds to the default step
542
            length.
543
        """
544
        power_td = self.power(wec, x_wec, x_opt, waves, nsubsteps)
6✔
545
        return np.sum(power_td) * wec.dt/nsubsteps
6✔
546

547
    def average_power(self,
6✔
548
        wec: TWEC,
549
        x_wec: ndarray,
550
        x_opt: ndarray,
551
        waves: Optional[Dataset] = None,
552
        nsubsteps: Optional[int] = 1,
553
    ) -> float:
554
        """Calculate the average power in each PTO DOF for a given
555
        system state.
556

557
        Parameters
558
        ----------
559
        wec
560
            :py:class:`wecopttool.WEC` object.
561
        x_wec
562
            WEC dynamic state.
563
        x_opt
564
            Optimization (control) state.
565
        waves
566
            :py:class:`xarray.Dataset` with the structure and elements
567
            shown by :py:mod:`wecopttool.waves`.
568
        nsubsteps
569
            Number of steps between the default (implied) time steps.
570
            A value of :python:`1` corresponds to the default step
571
            length.
572
        """
573
        energy = self.energy(wec, x_wec, x_opt, waves, nsubsteps)
6✔
574
        return energy / wec.tf
6✔
575

576
    def transduced_flow(self,
6✔
577
        wec: TWEC,
578
        x_wec: ndarray,
579
        x_opt: ndarray,
580
        waves: Optional[Dataset] = None,
581
        nsubsteps: Optional[int] = 1,
582
    ) -> float:
583
        """Calculate the transduced flow variable time-series in each PTO DOF
584
        for a given system state. Equals the PTO velocity if no impedance
585
        is defined.
586

587
        Examples for PTO impedance and corresponding flow variables:
588

589
        - OWC: (pneumatic admittance)^-1 : flow = volumetric air flow
590

591
        - Drive-train: rotational impedance : flow = rotational velocity
592

593
        - Generator: winding impedance: flow = electric current
594

595
        - Drive-train and Generator combined: flow = electric current
596

597
        Parameters
598
        ----------
599
        wec
600
            :py:class:`wecopttool.WEC` object.
601
        x_wec
602
            WEC dynamic state.
603
        x_opt
604
            Optimization (control) state.
605
        waves
606
            :py:class:`xarray.Dataset` with the structure and elements
607
            shown by :py:mod:`wecopttool.waves`.
608
        nsubsteps
609
            Number of steps between the default (implied) time steps.
610
            A value of :python:`1` corresponds to the default step
611
            length.
612
        """
613
        q2_td, _ = self.power_variables(wec, x_wec,
×
614
                                        x_opt, waves, nsubsteps)
615
        return q2_td
×
616

617
    def transduced_effort(self,
6✔
618
        wec: TWEC,
619
        x_wec: ndarray,
620
        x_opt: ndarray,
621
        waves: Optional[Dataset] = None,
622
        nsubsteps: Optional[int] = 1,
623
    ) -> float:
624
        """Calculate the transduced flow variable time-series in each PTO DOF
625
        for a given system state. Equals the PTO force if no impedance
626
        is defined.
627

628
        Examples for PTO impedance and corresponding effort variables:
629

630
        - OWC: (pneumatic admittance)^-1 : effort =  air pressure
631

632
        - Drive-train: rotational impedance : effort = torque
633

634
        - Generator: winding impedance: effort = voltage
635

636
        - Drive-train and Generator combined: effort = voltage
637

638
        Parameters
639
        ----------
640
        wec
641
            :py:class:`wecopttool.WEC` object.
642
        x_wec
643
            WEC dynamic state.
644
        x_opt
645
            Optimization (control) state.
646
        waves
647
            :py:class:`xarray.Dataset` with the structure and elements
648
            shown by :py:mod:`wecopttool.waves`.
649
        nsubsteps
650
            Number of steps between the default (implied) time steps.
651
            A value of :python:`1` corresponds to the default step
652
            length.
653
        """
654
        _, e2_td = self.power_variables(wec, x_wec, x_opt, waves, nsubsteps)
×
655
        return e2_td
×
656

657
    def post_process(self,
6✔
658
        wec: TWEC,
659
        res: Union[OptimizeResult, list],
660
        waves: Optional[DataArray] = None,
661
        nsubsteps: Optional[int] = 1,
662
    ) -> tuple[list[Dataset], list[Dataset]]:
663
        """Transform the results from optimization solution to a form
664
        that the user can work with directly.
665

666
        Examples
667
        --------
668
        The :py:meth:`wecopttool.WEC.solve` method only returns the
669
        raw results dictionary produced by :py:func:`scipy.optimize.minimize`.
670

671
        >>> res_opt = wec.solve(waves=wave,
672
                                obj_fun=pto.average_power,
673
                                nstate_opt=2*nfreq+1)
674

675
        To get the post-processed results for the
676
        :py:class:`wecopttool.pto.PTO`, you may call
677

678
        >>> res_pto_fd, res_pto_td = pto.post_process(wec,res_opt[0],wave)
679

680
        For smoother plots, you can set :python:`nsubsteps` to a value
681
        greater than 1.
682

683
        >>> res_pto_fd, res_pto_td = pto.post_process(wec,res_opt,
684
                                                      nsubsteps=4)
685
        >>> res_pto_td[0].power.plot()
686

687
        Parameters
688
        ----------
689
        wec
690
            :py:class:`wecopttool.WEC` object.
691
        res
692
            Results produced by :py:meth:`wecopttool.WEC.solve`.
693
        waves
694
            :py:class:`xarray.Dataset` with the structure and elements
695
            shown by :py:mod:`wecopttool.waves`.
696
        nsubsteps
697
            Number of steps between the default (implied) time steps.
698
            A value of :python:`1` corresponds to the default step
699
            length.
700

701
        Returns
702
        -------
703
        results_fd
704
            list of :py:class:`xarray.Dataset` with frequency domain results.
705
        results_td
706
            list of :py:class:`xarray.Dataset` with time domain results.
707
        """
708
        def _postproc(wec, res, waves, nsubsteps):
6✔
709

710
            create_time = f"{datetime.utcnow()}"
6✔
711

712
            x_wec, x_opt = wec.decompose_state(res.x)
6✔
713

714
            # position
715
            pos_td = self.position(wec, x_wec, x_opt, waves, nsubsteps)
6✔
716
            pos_fd = wec.td_to_fd(pos_td[::nsubsteps])
6✔
717

718
            # velocity
719
            vel_td = self.velocity(wec, x_wec, x_opt, waves, nsubsteps)
6✔
720
            vel_fd = wec.td_to_fd(vel_td[::nsubsteps])
6✔
721

722
            # acceleration
723
            acc_td = self.acceleration(wec, x_wec, x_opt, waves, nsubsteps)
6✔
724
            acc_fd = wec.td_to_fd(acc_td[::nsubsteps])
6✔
725

726
            # force
727
            force_td = self.force(wec, x_wec, x_opt, waves, nsubsteps)
6✔
728
            force_fd = wec.td_to_fd(force_td[::nsubsteps])
6✔
729

730
            # power
731
            elec_power_td = self.power(wec, x_wec, x_opt, waves, nsubsteps)
6✔
732
            elec_power_fd = wec.td_to_fd(elec_power_td[::nsubsteps])
6✔
733

734
            # mechanical power
735
            mech_power_td = self.mechanical_power(wec, x_wec, x_opt, waves,
6✔
736
                                                nsubsteps)
737
            mech_power_fd = wec.td_to_fd(mech_power_td[::nsubsteps])
6✔
738

739
            # stack mechanical and electrical power
740
            power_names = ['mech','elec']
6✔
741
            power_fd = np.stack((mech_power_fd,elec_power_fd))
6✔
742
            power_td = np.stack((mech_power_td,elec_power_td))
6✔
743

744
            pos_attr = {'long_name': 'Position', 'units': 'm or rad'}
6✔
745
            vel_attr = {'long_name': 'Velocity', 'units': 'm/s or rad/s'}
6✔
746
            acc_attr = {'long_name': 'Acceleration',
6✔
747
                        'units': 'm/s^2 or rad/s^2'}
748
            force_attr = {'long_name': 'Force or moment on WEC',
6✔
749
                        'units': 'N or Nm'}
750
            power_attr = {'long_name': 'Power', 'units': 'W'}
6✔
751
            mech_power_attr = {'long_name': 'Mechanical power', 'units': 'W'}
6✔
752
            omega_attr = {'long_name': 'Radial frequency', 'units': 'rad/s'}
6✔
753
            freq_attr = {'long_name': 'Frequency', 'units': 'Hz'}
6✔
754
            period_attr = {'long_name': 'Period', 'units': 's'}
6✔
755
            dof_attr = {'long_name': 'PTO degree of freedom'}
6✔
756
            time_attr = {'long_name': 'Time', 'units': 's'}
6✔
757
            type_attr = {'long_name': 'Power type'}
6✔
758

759
            t_dat = wec.time_nsubsteps(nsubsteps)
6✔
760

761
            results_fd = Dataset(
6✔
762
                data_vars={
763
                    'pos': (['omega','dof'], pos_fd, pos_attr),
764
                    'vel': (['omega','dof'], vel_fd, vel_attr),
765
                    'acc': (['omega','dof'], acc_fd, acc_attr),
766
                    'force': (['omega','dof'], force_fd, force_attr),
767
                    'power': (['type','omega','dof'], power_fd, power_attr),
768
                },
769
                coords={
770
                    'omega':('omega', wec.omega, omega_attr),
771
                    'freq':('omega', wec.frequency, freq_attr),
772
                    'period':('omega', wec.period, period_attr),
773
                    'dof':('dof', self.names, dof_attr),
774
                    'type':('type', power_names, power_attr)},
775
                attrs={"time_created_utc": create_time}
776
                )
777

778
            results_td = Dataset(
6✔
779
                data_vars={
780
                    'pos': (['time','dof'], pos_td, pos_attr),
781
                    'vel': (['time','dof'], vel_td, vel_attr),
782
                    'acc': (['time','dof'], acc_td, acc_attr),
783
                    'force': (['time','dof'], force_td, force_attr),
784
                    'power': (['type','time','dof'], power_td, power_attr),
785
                },
786
                coords={
787
                    'time':('time', t_dat, time_attr),
788
                    'dof':('dof', self.names, dof_attr),
789
                    'type':('type', power_names, power_attr)},
790
                attrs={"time_created_utc": create_time}
791
                )
792

793
            if self.impedance is not None:
6✔
794
            #transduced flow and effort variables
NEW
795
                q2_td, e2_td = self.power_variables(wec, x_wec, x_opt,
×
796
                                                    waves, nsubsteps)
NEW
797
                q2_fd = wec.td_to_fd(q2_td[::nsubsteps])
×
NEW
798
                e2_fd = wec.td_to_fd(e2_td[::nsubsteps])
×
799

NEW
800
                q2_attr = {'long_name': 'Transduced Flow',
×
801
                        'units': 'A or m^3/s or rad/s or m/s'}
NEW
802
                e2_attr = {'long_name': 'Transduced Effort',
×
803
                        'units': 'V or N/m^2 or Nm or Ns'}
804

NEW
805
                results_td = results_td.assign({
×
806
                            'trans_flo': (['time','dof'], q2_td, q2_attr),
807
                            'trans_eff': (['time','dof'], e2_td, e2_attr),
808
                        })
NEW
809
                results_fd = results_fd.assign({
×
810
                            'trans_flo': (['omega','dof'], q2_fd, q2_attr),
811
                            'trans_eff': (['omega','dof'], e2_fd, e2_attr),
812
                        })
813

814

815
            return results_fd, results_td
6✔
816

817
        results_fd = []
6✔
818
        results_td = []
6✔
819
        for idx, ires in enumerate(res):
6✔
820
            ifd, itd = _postproc(wec, ires, waves.sel(realization=idx), nsubsteps)
6✔
821
            results_fd.append(ifd)
6✔
822
            results_td.append(itd)
6✔
823
        return results_fd, results_td
6✔
824

825

826
# power conversion chain
827
def _make_abcd(impedance: ndarray, ndof: int) -> ndarray:
6✔
828
    """Transform the impedance matrix into ABCD form from a MIMO
829
    transfer function.
830

831
    Parameters
832
    ----------
833
    impedance
834
        Matrix representing the PTO impedance.
835
        Size 2*n_dof.
836
    ndof
837
        Number of degrees of freedom.
838
    """
839
    z_11 = impedance[:ndof, :ndof, :]  # Fu
6✔
840
    z_12 = impedance[:ndof, ndof:, :]  # Fi
6✔
841
    z_21 = impedance[ndof:, :ndof, :]  # Vu
6✔
842
    z_22 = impedance[ndof:, ndof:, :]  # Vi
6✔
843
    z_12_inv = np.linalg.inv(z_12.T).T
6✔
844

845
    mmult = lambda a,b: np.einsum('mnr,mnr->mnr', a, b)
6✔
846
    abcd_11 = -1 * mmult(z_12_inv, z_11)
6✔
847
    abcd_12 = z_12_inv
6✔
848
    abcd_21 = z_21 - mmult(z_22, mmult(z_12_inv, z_11))
6✔
849
    abcd_22 = mmult(z_22, z_12_inv)
6✔
850

851
    row_1 = np.hstack([abcd_11, abcd_12])
6✔
852
    row_2 = np.hstack([abcd_21, abcd_22])
6✔
853
    return np.vstack([row_1, row_2])
6✔
854

855

856
def _make_mimo_transfer_mat(
6✔
857
    impedance_abcd: ndarray,
858
    ndof: int,
859
) -> np.ndarray:
860
    """Create a block matrix of a MIMO transfer function.
861

862
    Parameters
863
    ----------
864
    impedance
865
        PTO impedance in ABCD form.
866
    ndof
867
        Number of degrees of freedom.
868
    """
869
    def block(re, im): return np.array([[re, -im], [im, re]])
6✔
870
    for idof in range(2*ndof):
6✔
871
        for jdof in range(2*ndof):
6✔
872
            Zp = impedance_abcd[idof, jdof, :]
6✔
873
            re = np.real(Zp)
6✔
874
            im = np.imag(Zp)
6✔
875
            # Exclude the sine component of the 2-point wave
876
            blocks = [block(ire, iim) for (ire, iim) in zip(re[:-1], im[:-1])]
6✔
877
            # re[0] added for the zero frequency power loss (DC), could be re[n]
878
            blocks = [re[0]] + blocks + [re[-1]]
6✔
879
            if jdof==0:
6✔
880
                row = block_diag(*blocks)
6✔
881
            else:
882
                row = np.hstack([row, block_diag(*blocks)])
6✔
883
        if idof==0:
6✔
884
            mat = row
6✔
885
        else:
886
            mat = np.vstack([mat, row])
6✔
887
    return mat
6✔
888

889

890
# controllers
891
def controller_unstructured(
6✔
892
    pto: TPTO,
893
    wec: TWEC,
894
    x_wec: ndarray,
895
    x_opt: ndarray,
896
    waves: Optional[Dataset] = None,
897
    nsubsteps: Optional[int] = 1,
898
) -> ndarray:
899
    """Unstructured numerical optimal controller that returns a time
900
    history of PTO forces.
901

902
    Parameters
903
    ----------
904
    pto
905
        :py:class:`wecopttool.pto.PTO` object.
906
    wec
907
        :py:class:`wecopttool.WEC` object.
908
    x_wec
909
        WEC dynamic state.
910
    x_opt
911
        Optimization (control) state.
912
    waves
913
        :py:class:`xarray.Dataset` with the structure and elements
914
        shown by :py:mod:`wecopttool.waves`.
915
    nsubsteps
916
        Number of steps between the default (implied) time steps.
917
        A value of :python:`1` corresponds to the default step
918
        length.
919
    """
920
    tmat = pto._tmat(wec, nsubsteps)
6✔
921
    x_opt = np.reshape(x_opt[:len(tmat[0])*pto.ndof], (-1, pto.ndof), order='F')
6✔
922
    return np.dot(tmat, x_opt)
6✔
923

924

925
def controller_pid(
6✔
926
    pto: TPTO,
927
    wec: TWEC,
928
    x_wec: ndarray,
929
    x_opt: ndarray,
930
    waves: Optional[Dataset] = None,
931
    nsubsteps: Optional[int] = 1,
932
    proportional: Optional[bool] = True,
933
    integral: Optional[bool] = True,
934
    derivative: Optional[bool] = True,
935
    saturation: Optional[FloatOrArray] = None,
936
) -> ndarray:
937
    """Proportional-integral-derivative (PID) controller that returns
938
    a time history of PTO forces.
939

940
    Parameters
941
    ----------
942
    pto
943
        :py:class:`wecopttool.pto.PTO` object.
944
    wec
945
        :py:class:`wecopttool.WEC` object.
946
    x_wec
947
        WEC dynamic state.
948
    x_opt
949
        Optimization (control) state.
950
    waves
951
        :py:class:`xarray.Dataset` with the structure and elements shown
952
        by :py:mod:`wecopttool.waves`.
953
    nsubsteps
954
        Number of steps between the default (implied) time steps.
955
        A value of :python:`1` corresponds to the default step length.
956
    proportional
957
        True to include proportional gain
958
    integral
959
        True to include integral gain
960
    derivative
961
        True to include derivative gain
962
    saturation
963
        Maximum and minimum control value.
964
        Can be symmetric ([ndof]) or asymmetric ([ndof, 2]).
965
    """
966
    ndof = pto.ndof
6✔
967
    force_td_tmp = np.zeros([wec.nt*nsubsteps, ndof])
6✔
968

969
    # PID force
970
    idx = 0
6✔
971

972
    def update_force_td(response):
6✔
973
        nonlocal idx, force_td_tmp
974
        gain = np.diag(x_opt[idx*ndof:(idx+1)*ndof])
6✔
975
        force_td_tmp = force_td_tmp + np.dot(response, gain.T)
6✔
976
        idx = idx + 1
6✔
977
        return
6✔
978

979
    if proportional:
6✔
980
        vel_td = pto.velocity(wec, x_wec, x_opt, waves, nsubsteps)
6✔
981
        update_force_td(vel_td)
6✔
982
    if integral:
6✔
983
        pos_td = pto.position(wec, x_wec, x_opt, waves, nsubsteps)
6✔
984
        update_force_td(pos_td)
6✔
985
    if derivative:
6✔
986
        acc_td = pto.acceleration(wec, x_wec, x_opt, waves, nsubsteps)
6✔
987
        update_force_td(acc_td)
6✔
988

989
    # Saturation
990
    if saturation is not None:
6✔
991
        saturation = np.atleast_2d(np.squeeze(saturation))
6✔
992
        assert len(saturation)==ndof
6✔
993
        if len(saturation.shape) > 2:
6✔
994
            raise ValueError("`saturation` must have <= 2 dimensions.")
×
995
        if saturation.shape[1] == 1:
6✔
996
            f_min, f_max = -1*saturation, saturation
×
997
        elif saturation.shape[1] == 2:
6✔
998
            f_min, f_max = saturation[:,0], saturation[:,1]
6✔
999
        else:
1000
            raise ValueError("`saturation` must have 1 or 2 columns.")
×
1001

1002
        force_td_list = []
6✔
1003
        for i in range(ndof):
6✔
1004
            tmp = np.clip(force_td_tmp[:,i], f_min[i], f_max[i])
6✔
1005
            force_td_list.append(tmp)
6✔
1006
        force_td = np.array(force_td_list).T
6✔
1007
    else:
1008
        force_td = force_td_tmp
6✔
1009

1010
    return force_td
6✔
1011

1012

1013
def controller_pi(
6✔
1014
    pto: TPTO,
1015
    wec: TWEC,
1016
    x_wec: ndarray,
1017
    x_opt: ndarray,
1018
    waves: Optional[Dataset] = None,
1019
    nsubsteps: Optional[int] = 1,
1020
    saturation: Optional[FloatOrArray] = None,
1021
) -> ndarray:
1022
    """Proportional-integral (PI) controller that returns a time
1023
    history of PTO forces.
1024

1025
    Parameters
1026
    ----------
1027
    pto
1028
        :py:class:`wecopttool.pto.PTO` object.
1029
    wec
1030
        :py:class:`wecopttool.WEC` object.
1031
    x_wec
1032
        WEC dynamic state.
1033
    x_opt
1034
        Optimization (control) state.
1035
    waves
1036
        :py:class:`xarray.Dataset` with the structure and elements shown
1037
        by :py:mod:`wecopttool.waves`.
1038
    nsubsteps
1039
        Number of steps between the default (implied) time steps.
1040
        A value of :python:`1` corresponds to the default step length.
1041
    saturation
1042
        Maximum and minimum control value.
1043
        Can be symmetric ([ndof]) or asymmetric ([ndof, 2]).
1044
    """
1045
    force_td = controller_pid(
6✔
1046
        pto, wec, x_wec, x_opt, waves, nsubsteps,
1047
        True, True, False, saturation,
1048
    )
1049
    return force_td
6✔
1050

1051

1052
def controller_p(
6✔
1053
    pto: TPTO,
1054
    wec: TWEC,
1055
    x_wec: ndarray,
1056
    x_opt: ndarray,
1057
    waves: Optional[Dataset] = None,
1058
    nsubsteps: Optional[int] = 1,
1059
    saturation: Optional[FloatOrArray] = None,
1060
) -> ndarray:
1061
    """Proportional (P) controller that returns a time history of
1062
    PTO forces.
1063

1064
    Parameters
1065
    ----------
1066
    pto
1067
        :py:class:`wecopttool.pto.PTO` object.
1068
    wec
1069
        :py:class:`wecopttool.WEC` object.
1070
    x_wec
1071
        WEC dynamic state.
1072
    x_opt
1073
        Optimization (control) state.
1074
    waves
1075
        :py:class:`xarray.Dataset` with the structure and elements shown
1076
        by :py:mod:`wecopttool.waves`.
1077
    nsubsteps
1078
        Number of steps between the default (implied) time steps.
1079
        A value of :python:`1` corresponds to the default step length.
1080
    saturation
1081
        Maximum and minimum control value. Can be symmetric ([ndof]) or
1082
        asymmetric ([ndof, 2]).
1083
    """
1084
    force_td = controller_pid(
6✔
1085
        pto, wec, x_wec, x_opt, waves, nsubsteps,
1086
        True, False, False, saturation,
1087
    )
1088
    return force_td
6✔
1089

1090

1091
# utilities
1092
def nstate_unstructured(nfreq: int, ndof: int) -> int:
6✔
1093
    """
1094
    Number of states needed to represent an unstructured controller.
1095

1096
    Parameters
1097
    ----------
1098
    nfreq
1099
        Number of frequencies.
1100
    ndof
1101
        Number of degrees of freedom.
1102
    """
1103
    return 2*nfreq*ndof
×
1104

1105

1106
def nstate_pid(
6✔
1107
        nterm: int,
1108
        ndof: int,
1109
) -> int:
1110
    """
1111
    Number of states needed to represent an unstructured controller.
1112

1113
    Parameters
1114
    ----------
1115
    nterm
1116
        Number of terms (e.g. 1 for P, 2 for PI, 3 for PID).
1117
    ndof
1118
        Number of degrees of freedom.
1119
    """
1120
    return int(nterm*ndof)
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc