motulator-multiphase 0.1.0__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.
- motulator_multiphase/__init__.py +20 -0
- motulator_multiphase/_helpers.py +23 -0
- motulator_multiphase/control.py +452 -0
- motulator_multiphase/model.py +503 -0
- motulator_multiphase/py.typed +0 -0
- motulator_multiphase/utils.py +297 -0
- motulator_multiphase-0.1.0.dist-info/METADATA +130 -0
- motulator_multiphase-0.1.0.dist-info/RECORD +10 -0
- motulator_multiphase-0.1.0.dist-info/WHEEL +4 -0
- motulator_multiphase-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Multi-three-phase machine drives.
|
|
3
|
+
|
|
4
|
+
This package provides models and controls of machines with n three-phase winding
|
|
5
|
+
sets (e.g., dual-three-phase machines), based on the decoupled multi-stator (DMS)
|
|
6
|
+
transformation [#Rub2021]_. The common mode of the winding sets is equivalent to a
|
|
7
|
+
three-phase machine whose sets are connected in parallel. Therefore, the three-phase
|
|
8
|
+
machine models, magnetic models, and vector controllers of motulator are reused for
|
|
9
|
+
the common mode, while the differential modes are modeled and controlled separately.
|
|
10
|
+
The package is organized like `motulator.drive`::
|
|
11
|
+
|
|
12
|
+
from motulator_multiphase import control, model, utils
|
|
13
|
+
|
|
14
|
+
References
|
|
15
|
+
----------
|
|
16
|
+
.. [#Rub2021] Rubino, Dordevic, Armando, Bojoi, Levi, "A novel matrix transformation
|
|
17
|
+
for decoupled control of modular multiphase PMSM drives," IEEE Trans. Power
|
|
18
|
+
Electron., 2021, https://doi.org/10.1109/TPEL.2020.3043083
|
|
19
|
+
|
|
20
|
+
"""
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Helper functions copied from the private modules of motulator."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def wrap(theta: float) -> Any:
|
|
10
|
+
"""Limit the angle into the range [-pi, pi)."""
|
|
11
|
+
return np.mod(theta + np.pi, 2 * np.pi) - np.pi
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def get_value(u: Any | Callable[[Any], Any] | None, x: Any) -> Any:
|
|
15
|
+
"""Return `u(x)` if `u` is callable, otherwise `u`."""
|
|
16
|
+
if u is None:
|
|
17
|
+
raise ValueError("Input value cannot be None")
|
|
18
|
+
return u(x) if callable(u) else u
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def empty_array() -> np.ndarray:
|
|
22
|
+
"""Return an empty array."""
|
|
23
|
+
return np.array([])
|
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Control of multi-three-phase machine drives.
|
|
3
|
+
|
|
4
|
+
The common mode of the three-phase sets is controlled by the vector controllers of
|
|
5
|
+
three-phase machines, and the differential modes are current-controlled.
|
|
6
|
+
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from cmath import exp
|
|
10
|
+
from collections.abc import Callable, Sequence
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
|
+
from math import isclose, pi
|
|
13
|
+
from typing import Any, Protocol
|
|
14
|
+
|
|
15
|
+
import numpy as np
|
|
16
|
+
from motulator.common.control import (
|
|
17
|
+
PWM,
|
|
18
|
+
ComplexPIController,
|
|
19
|
+
ControlSystem,
|
|
20
|
+
DiscreteComplexController,
|
|
21
|
+
PIController,
|
|
22
|
+
TimeSeries,
|
|
23
|
+
)
|
|
24
|
+
from motulator.common.utils import abc2complex
|
|
25
|
+
from motulator.drive.control.sm import SpeedController
|
|
26
|
+
|
|
27
|
+
from motulator_multiphase._helpers import empty_array, get_value, wrap
|
|
28
|
+
from motulator_multiphase.model import MultiThreePhaseDrive
|
|
29
|
+
from motulator_multiphase.utils import default_set_angles, dms_matrix
|
|
30
|
+
|
|
31
|
+
__all__ = [
|
|
32
|
+
"DifferentialModeController",
|
|
33
|
+
"DifferentialModeSignals",
|
|
34
|
+
"ExternalReferences",
|
|
35
|
+
"Measurements",
|
|
36
|
+
"MultiThreePhaseControlSystem",
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
type Sharing = Sequence[float] | np.ndarray
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# %%
|
|
43
|
+
class Feedbacks(Protocol):
|
|
44
|
+
"""Protocol defining the required fields for the feedback signals."""
|
|
45
|
+
|
|
46
|
+
w_M: float # Mechanical angular speed
|
|
47
|
+
w_c: float # Angular speed of the coordinate system
|
|
48
|
+
theta_c: float # Angular position of the coordinate system
|
|
49
|
+
u_s: complex # Stator voltage (controller coordinates) for the control laws
|
|
50
|
+
u_dc: float # DC-bus voltage
|
|
51
|
+
i_s: complex # Current of the parallel-connected machine
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class References(Protocol):
|
|
55
|
+
"""Protocol defining the required fields for the reference signals."""
|
|
56
|
+
|
|
57
|
+
T_s: float # Sampling period for the next control step
|
|
58
|
+
tau_M: float # Torque reference
|
|
59
|
+
u_s: complex # Stator voltage reference
|
|
60
|
+
u_s_lim: complex # Limited voltage reference (set by the control system)
|
|
61
|
+
w_M: float | None # Speed reference (set by the control system)
|
|
62
|
+
d_abc: Sequence[float] # Duty ratios (set by the control system)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class VectorController(Protocol):
|
|
66
|
+
"""Protocol defining the interface of the vector controllers of motulator."""
|
|
67
|
+
|
|
68
|
+
sensorless: bool
|
|
69
|
+
k_comp: float # Angle-advance factor
|
|
70
|
+
|
|
71
|
+
def get_feedback(
|
|
72
|
+
self,
|
|
73
|
+
u_s_ab: complex,
|
|
74
|
+
i_s_ab: complex,
|
|
75
|
+
w_M_meas: float | None,
|
|
76
|
+
theta_M_meas: float | None,
|
|
77
|
+
) -> Any:
|
|
78
|
+
"""Get feedback signals from measurements."""
|
|
79
|
+
...
|
|
80
|
+
|
|
81
|
+
def compute_output(self, tau_M_ref: float, fbk: Any) -> Any:
|
|
82
|
+
"""Compute control output from feedback signals."""
|
|
83
|
+
...
|
|
84
|
+
|
|
85
|
+
def update(self, ref: Any, fbk: Any) -> None:
|
|
86
|
+
"""Update controller states."""
|
|
87
|
+
...
|
|
88
|
+
|
|
89
|
+
def post_process(self, ts: TimeSeries) -> None:
|
|
90
|
+
"""Post-process controller outputs."""
|
|
91
|
+
...
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@dataclass
|
|
95
|
+
class Measurements:
|
|
96
|
+
"""Measured signals."""
|
|
97
|
+
|
|
98
|
+
i_c_ab: np.ndarray # Currents of the sets in their own coordinates
|
|
99
|
+
u_dc: float
|
|
100
|
+
w_M: float | None = None
|
|
101
|
+
theta_M: float | None = None
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@dataclass
|
|
105
|
+
class ExternalReferences:
|
|
106
|
+
"""External reference signals."""
|
|
107
|
+
|
|
108
|
+
w_M: float | Callable[[float], float] | None = None
|
|
109
|
+
tau_M: float | Callable[[float], float] | None = None
|
|
110
|
+
k_share: Sharing | Callable[[float], Sharing] | None = None
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
@dataclass
|
|
114
|
+
class DifferentialModeSignals:
|
|
115
|
+
"""DM signals in the controller coordinates, each of shape (n_sets - 1,)."""
|
|
116
|
+
|
|
117
|
+
i: np.ndarray # Currents
|
|
118
|
+
u: np.ndarray # Realized voltages
|
|
119
|
+
i_ref: np.ndarray = field(default_factory=empty_array)
|
|
120
|
+
u_ref: np.ndarray = field(default_factory=empty_array)
|
|
121
|
+
u_lim: np.ndarray = field(default_factory=empty_array) # Limited u_ref
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
# %%
|
|
125
|
+
class DifferentialModeController:
|
|
126
|
+
"""
|
|
127
|
+
Current controller of the differential modes.
|
|
128
|
+
|
|
129
|
+
Each DM is a resistive-inductive circuit with the leakage inductance `L_ls`. The DM
|
|
130
|
+
currents are controlled in the coordinates of the vector controller using the
|
|
131
|
+
flux-linkage-based complex-vector 2DOF controllers, in the same manner as in the
|
|
132
|
+
current controllers of motulator. The DM current references correspond to the
|
|
133
|
+
current-sharing coefficients of the sets, i.e., the reference of the set k is
|
|
134
|
+
`k_share[k]*i_cm`, where `i_cm` is the common-mode current.
|
|
135
|
+
|
|
136
|
+
Parameters
|
|
137
|
+
----------
|
|
138
|
+
L_ls : float
|
|
139
|
+
Leakage inductance (H) of a set.
|
|
140
|
+
n_sets : int, optional
|
|
141
|
+
Number of three-phase sets, defaults to 2.
|
|
142
|
+
alpha_c : float, optional
|
|
143
|
+
Current-control bandwidth (rad/s), defaults to 2*pi*200.
|
|
144
|
+
alpha_i : float, optional
|
|
145
|
+
Integral-action bandwidth (rad/s), defaults to `alpha_c`. Not used in the
|
|
146
|
+
direct discrete-time design.
|
|
147
|
+
T_s : float, optional
|
|
148
|
+
Sampling period (s). If given, the direct discrete-time design is used instead
|
|
149
|
+
of the continuous-time design. The vector controller must then be a direct
|
|
150
|
+
discrete-time design as well (`k_comp = 0`), since the angle advance of the
|
|
151
|
+
voltage references is common to all modes, and its sampling period must equal
|
|
152
|
+
this value.
|
|
153
|
+
|
|
154
|
+
"""
|
|
155
|
+
|
|
156
|
+
def __init__(
|
|
157
|
+
self,
|
|
158
|
+
L_ls: float,
|
|
159
|
+
n_sets: int = 2,
|
|
160
|
+
alpha_c: float = 2 * pi * 200,
|
|
161
|
+
alpha_i: float | None = None,
|
|
162
|
+
T_s: float | None = None,
|
|
163
|
+
) -> None:
|
|
164
|
+
self.L_ls = L_ls
|
|
165
|
+
self.n_sets = n_sets
|
|
166
|
+
self.T_s = T_s
|
|
167
|
+
self.discrete = T_s is not None
|
|
168
|
+
self.ctrls: list[ComplexPIController] | list[DiscreteComplexController]
|
|
169
|
+
if T_s is None:
|
|
170
|
+
alpha_i = alpha_c if alpha_i is None else alpha_i
|
|
171
|
+
k_p, k_i = alpha_c + alpha_i, alpha_c * alpha_i
|
|
172
|
+
self.ctrls = [
|
|
173
|
+
ComplexPIController(k_p, k_i, alpha_c) for _ in range(n_sets - 1)
|
|
174
|
+
]
|
|
175
|
+
else:
|
|
176
|
+
self.ctrls = [
|
|
177
|
+
DiscreteComplexController(alpha_c, T_s) for _ in range(n_sets - 1)
|
|
178
|
+
]
|
|
179
|
+
self.T = dms_matrix(n_sets)
|
|
180
|
+
self.c_dm = np.zeros(n_sets - 1) # DM current references per CM current
|
|
181
|
+
|
|
182
|
+
def set_sharing(self, k_share: Sharing) -> None:
|
|
183
|
+
"""Set the current-sharing coefficients, summing up to the number of sets."""
|
|
184
|
+
k_share = np.asarray(k_share, dtype=float)
|
|
185
|
+
if not np.isclose(np.sum(k_share), self.n_sets):
|
|
186
|
+
raise ValueError(
|
|
187
|
+
"The sharing coefficients must sum up to the number of sets"
|
|
188
|
+
)
|
|
189
|
+
self.c_dm = self.T[1:] @ k_share
|
|
190
|
+
|
|
191
|
+
def compute_output(
|
|
192
|
+
self, i_cm: complex, i_dm: np.ndarray
|
|
193
|
+
) -> tuple[np.ndarray, np.ndarray]:
|
|
194
|
+
"""Compute the DM current references and voltage references."""
|
|
195
|
+
i_dm_ref = self.c_dm * i_cm
|
|
196
|
+
u_dm_ref = [
|
|
197
|
+
ctrl.compute_output(self.L_ls * complex(i_ref), self.L_ls * complex(i))
|
|
198
|
+
for ctrl, i_ref, i in zip(self.ctrls, i_dm_ref, i_dm, strict=True)
|
|
199
|
+
]
|
|
200
|
+
return i_dm_ref, np.array(u_dm_ref, dtype=complex)
|
|
201
|
+
|
|
202
|
+
def update(self, T_s: float, dm: DifferentialModeSignals, w_c: float) -> None:
|
|
203
|
+
"""Update the integral states using the realized or limited DM voltages."""
|
|
204
|
+
if self.T_s is not None and not isclose(T_s, self.T_s):
|
|
205
|
+
raise ValueError(
|
|
206
|
+
"The sampling period must equal the design value of the direct "
|
|
207
|
+
"discrete-time DM controller"
|
|
208
|
+
)
|
|
209
|
+
u_dm = dm.u_lim if self.discrete else dm.u
|
|
210
|
+
for ctrl, u in zip(self.ctrls, u_dm, strict=True):
|
|
211
|
+
ctrl.update(T_s, complex(u), w_c)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
# %%
|
|
215
|
+
class MultiThreePhaseControlSystem(
|
|
216
|
+
ControlSystem[MultiThreePhaseDrive, Measurements, References, Feedbacks]
|
|
217
|
+
):
|
|
218
|
+
"""
|
|
219
|
+
Vector control system of multi-three-phase machine drives.
|
|
220
|
+
|
|
221
|
+
The control is based on the decoupled multi-stator (DMS) transformation, which maps
|
|
222
|
+
the three-phase sets to the common mode (CM) and the differential modes (DMs)
|
|
223
|
+
[#Rub2021]_. The CM is controlled by the given vector controller of a three-phase
|
|
224
|
+
machine, see :func:`~motulator_multiphase.utils.to_parallel`. Its feedback signals
|
|
225
|
+
are the CM voltage and the current of the equivalent machine, i.e., the sum of the
|
|
226
|
+
set currents. The DMs are controlled by the DM current controller in the
|
|
227
|
+
coordinates of the vector controller. Each set has its own PWM. As in
|
|
228
|
+
`VectorControlSystem`, the angles of the voltage references are advanced by
|
|
229
|
+
`k_comp*T_s*w_c`, where `k_comp` is given by the vector controller, and the speed
|
|
230
|
+
controller and the external references work in the same manner. The DM signals are
|
|
231
|
+
logged in the signal group `dm`, see :class:`DifferentialModeSignals`, with the
|
|
232
|
+
shape (N, n_sets - 1). In the post-processed time series, they are in the same
|
|
233
|
+
coordinates as the signals of the vector controller (e.g., in the estimated rotor
|
|
234
|
+
flux coordinates for induction machines).
|
|
235
|
+
|
|
236
|
+
Parameters
|
|
237
|
+
----------
|
|
238
|
+
vector_ctrl : VectorController
|
|
239
|
+
Vector controller of the CM, configured with the parameters of the
|
|
240
|
+
parallel-connected equivalent machine.
|
|
241
|
+
dm_ctrl : DifferentialModeController
|
|
242
|
+
Current controller of the DMs.
|
|
243
|
+
speed_ctrl : SpeedController | PIController | None, optional
|
|
244
|
+
Speed controller. If None (default), torque-control mode is used.
|
|
245
|
+
theta_sets : Sequence[float], optional
|
|
246
|
+
Angles (rad) of the phases a of the sets, defaults to the asymmetrical
|
|
247
|
+
arrangement.
|
|
248
|
+
pwms : Sequence[PWM], optional
|
|
249
|
+
Pulse-width modulators of the sets, defaults to `PWM()` for each set.
|
|
250
|
+
|
|
251
|
+
References
|
|
252
|
+
----------
|
|
253
|
+
.. [#Rub2021] Rubino, Dordevic, Armando, Bojoi, Levi, "A novel matrix transformation
|
|
254
|
+
for decoupled control of modular multiphase PMSM drives," IEEE Trans. Power
|
|
255
|
+
Electron., 2021, https://doi.org/10.1109/TPEL.2020.3043083
|
|
256
|
+
|
|
257
|
+
"""
|
|
258
|
+
|
|
259
|
+
def __init__(
|
|
260
|
+
self,
|
|
261
|
+
vector_ctrl: VectorController,
|
|
262
|
+
dm_ctrl: DifferentialModeController,
|
|
263
|
+
speed_ctrl: SpeedController | PIController | None = None,
|
|
264
|
+
theta_sets: Sequence[float] | np.ndarray | None = None,
|
|
265
|
+
pwms: Sequence[PWM] | None = None,
|
|
266
|
+
) -> None:
|
|
267
|
+
super().__init__()
|
|
268
|
+
n_sets = dm_ctrl.n_sets
|
|
269
|
+
if theta_sets is None:
|
|
270
|
+
theta_sets = default_set_angles(n_sets)
|
|
271
|
+
if len(theta_sets) != n_sets:
|
|
272
|
+
raise ValueError("The number of the set angles must equal n_sets")
|
|
273
|
+
if pwms is None:
|
|
274
|
+
pwms = [PWM() for _ in range(n_sets)]
|
|
275
|
+
if len(pwms) != n_sets:
|
|
276
|
+
raise ValueError("The number of the PWMs must equal n_sets")
|
|
277
|
+
if dm_ctrl.discrete and vector_ctrl.k_comp != 0:
|
|
278
|
+
raise ValueError(
|
|
279
|
+
"The direct discrete-time DM controller requires a direct "
|
|
280
|
+
"discrete-time vector controller (k_comp = 0)"
|
|
281
|
+
)
|
|
282
|
+
self.vector_ctrl = vector_ctrl
|
|
283
|
+
self.dm_ctrl = dm_ctrl
|
|
284
|
+
self.speed_ctrl = speed_ctrl
|
|
285
|
+
self.n_sets = n_sets
|
|
286
|
+
# Maps the set vectors in their own coordinates to the CM and the DMs
|
|
287
|
+
self.D = dms_matrix(n_sets) * np.exp(1j * np.asarray(theta_sets))
|
|
288
|
+
self.pwms = list(pwms)
|
|
289
|
+
self.k_comp = vector_ctrl.k_comp
|
|
290
|
+
self.ext_ref = ExternalReferences()
|
|
291
|
+
zeros = np.zeros(n_sets - 1, dtype=complex)
|
|
292
|
+
self.dm = DifferentialModeSignals(i=zeros, u=zeros)
|
|
293
|
+
|
|
294
|
+
def set_speed_ref(self, ref_fcn: float | Callable[[float], float]) -> None:
|
|
295
|
+
"""
|
|
296
|
+
Set the external speed reference for speed-control mode.
|
|
297
|
+
|
|
298
|
+
Parameters
|
|
299
|
+
----------
|
|
300
|
+
ref_fcn : float | Callable[[float], float]
|
|
301
|
+
Speed reference (mechanical rad/s), constant or a function of time.
|
|
302
|
+
|
|
303
|
+
"""
|
|
304
|
+
if self.speed_ctrl is None:
|
|
305
|
+
raise ValueError("Speed-control mode requires speed_ctrl")
|
|
306
|
+
self.ext_ref.w_M = ref_fcn
|
|
307
|
+
|
|
308
|
+
def set_torque_ref(self, ref_fcn: float | Callable[[float], float]) -> None:
|
|
309
|
+
"""
|
|
310
|
+
Set the external torque reference for torque-control mode.
|
|
311
|
+
|
|
312
|
+
Parameters
|
|
313
|
+
----------
|
|
314
|
+
ref_fcn : float | Callable[[float], float]
|
|
315
|
+
Torque reference (Nm), constant or a function of time.
|
|
316
|
+
|
|
317
|
+
"""
|
|
318
|
+
if self.speed_ctrl is not None:
|
|
319
|
+
raise ValueError("The speed controller gives the torque reference")
|
|
320
|
+
self.ext_ref.tau_M = ref_fcn
|
|
321
|
+
|
|
322
|
+
def set_current_sharing(
|
|
323
|
+
self, ref_fcn: Sharing | Callable[[float], Sharing]
|
|
324
|
+
) -> None:
|
|
325
|
+
"""
|
|
326
|
+
Set the current-sharing coefficients.
|
|
327
|
+
|
|
328
|
+
Parameters
|
|
329
|
+
----------
|
|
330
|
+
ref_fcn : Sequence[float] | Callable[[float], Sequence[float]]
|
|
331
|
+
Coefficients of the sets, summing up to the number of sets, constant or a
|
|
332
|
+
function of time. Equal sharing, i.e., all coefficients equal to one, is
|
|
333
|
+
used by default.
|
|
334
|
+
|
|
335
|
+
"""
|
|
336
|
+
self.ext_ref.k_share = ref_fcn
|
|
337
|
+
|
|
338
|
+
def get_measurement(self, mdl: MultiThreePhaseDrive) -> Measurements:
|
|
339
|
+
"""Sample the sensors, the speed and position sensors if the drive has them."""
|
|
340
|
+
if mdl.machine.n_sets != self.n_sets:
|
|
341
|
+
raise ValueError(
|
|
342
|
+
"The drive model and control system must have equal n_sets"
|
|
343
|
+
)
|
|
344
|
+
sensors = mdl.sensors
|
|
345
|
+
i_c_ab = abc2complex(sensors.i_c.sample())
|
|
346
|
+
u_dc = sensors.u_dc.sample()
|
|
347
|
+
w_M = None if sensors.w_M is None else sensors.w_M.sample()
|
|
348
|
+
theta_M = None if sensors.theta_M is None else wrap(sensors.theta_M.sample())
|
|
349
|
+
return Measurements(i_c_ab, u_dc, w_M, theta_M)
|
|
350
|
+
|
|
351
|
+
def get_feedback(self, meas: Measurements) -> Feedbacks:
|
|
352
|
+
"""Get the feedback signals."""
|
|
353
|
+
# The observers use the voltage of the ongoing sampling period, while the
|
|
354
|
+
# control laws use the voltage averaged over the previous and ongoing periods
|
|
355
|
+
u_c_ab = [
|
|
356
|
+
pwm.get_realized_voltage(i, meas.u_dc, average=False)
|
|
357
|
+
for pwm, i in zip(self.pwms, meas.i_c_ab, strict=True)
|
|
358
|
+
]
|
|
359
|
+
u_zoh_cm = complex(self.D[0] @ u_c_ab)
|
|
360
|
+
u_c_ab = [
|
|
361
|
+
pwm.get_realized_voltage(i, meas.u_dc)
|
|
362
|
+
for pwm, i in zip(self.pwms, meas.i_c_ab, strict=True)
|
|
363
|
+
]
|
|
364
|
+
u_modes = self.D @ u_c_ab
|
|
365
|
+
i_modes = self.D @ meas.i_c_ab
|
|
366
|
+
# Parallel-connected equivalent machine
|
|
367
|
+
fbk = self.vector_ctrl.get_feedback(
|
|
368
|
+
u_zoh_cm, complex(self.n_sets * i_modes[0]), meas.w_M, meas.theta_M
|
|
369
|
+
)
|
|
370
|
+
exp_mj_theta_c = exp(-1j * fbk.theta_c)
|
|
371
|
+
fbk.u_s = exp_mj_theta_c * complex(u_modes[0])
|
|
372
|
+
fbk.u_dc = meas.u_dc
|
|
373
|
+
self.dm = DifferentialModeSignals(
|
|
374
|
+
i=exp_mj_theta_c * i_modes[1:], u=exp_mj_theta_c * u_modes[1:]
|
|
375
|
+
)
|
|
376
|
+
return fbk
|
|
377
|
+
|
|
378
|
+
def compute_output(self, fbk: Feedbacks) -> References:
|
|
379
|
+
"""Compute the references and the duty ratios of the sets."""
|
|
380
|
+
ext_ref = self.ext_ref
|
|
381
|
+
if self.speed_ctrl is not None: # Speed-control mode
|
|
382
|
+
if ext_ref.w_M is None:
|
|
383
|
+
raise ValueError("Speed reference must be set")
|
|
384
|
+
w_M_ref = get_value(ext_ref.w_M, self.t)
|
|
385
|
+
tau_M_ref = self.speed_ctrl.compute_output(w_M_ref, fbk.w_M)
|
|
386
|
+
else: # Torque-control mode
|
|
387
|
+
if ext_ref.tau_M is None:
|
|
388
|
+
raise ValueError("Torque reference must be set")
|
|
389
|
+
w_M_ref, tau_M_ref = None, get_value(ext_ref.tau_M, self.t)
|
|
390
|
+
ref = self.vector_ctrl.compute_output(tau_M_ref, fbk)
|
|
391
|
+
ref.w_M = w_M_ref
|
|
392
|
+
|
|
393
|
+
# DM current control
|
|
394
|
+
dm = self.dm
|
|
395
|
+
if ext_ref.k_share is not None:
|
|
396
|
+
self.dm_ctrl.set_sharing(get_value(ext_ref.k_share, self.t))
|
|
397
|
+
dm.i_ref, dm.u_ref = self.dm_ctrl.compute_output(fbk.i_s / self.n_sets, dm.i)
|
|
398
|
+
|
|
399
|
+
# Angle-advanced voltage references of the sets in their own coordinates
|
|
400
|
+
exp_j_theta_c = exp(1j * fbk.theta_c)
|
|
401
|
+
u_modes = exp_j_theta_c * np.array([ref.u_s, *dm.u_ref])
|
|
402
|
+
u_c_ab = self.n_sets * self.D.conj().T @ u_modes
|
|
403
|
+
u_c_ab *= exp(1j * self.k_comp * ref.T_s * fbk.w_c)
|
|
404
|
+
ref.d_abc = [
|
|
405
|
+
d
|
|
406
|
+
for pwm, u in zip(self.pwms, u_c_ab, strict=True)
|
|
407
|
+
for d in pwm(ref.T_s, complex(u), fbk.u_dc, fbk.w_c)
|
|
408
|
+
]
|
|
409
|
+
|
|
410
|
+
# Limited voltages in the controller coordinates
|
|
411
|
+
u_lim_modes = self.D @ [pwm.limited_voltage for pwm in self.pwms]
|
|
412
|
+
u_lim_modes = u_lim_modes / exp_j_theta_c
|
|
413
|
+
ref.u_s_lim, dm.u_lim = complex(u_lim_modes[0]), u_lim_modes[1:]
|
|
414
|
+
return ref
|
|
415
|
+
|
|
416
|
+
def compute_standby_output(self, fbk: Feedbacks) -> References:
|
|
417
|
+
"""Compute the controller output while the gates are blocked."""
|
|
418
|
+
# The multi-three-phase converter model does not support gate blocking. The
|
|
419
|
+
# message is a separate statement, so that the method is not documented as an
|
|
420
|
+
# abstract method.
|
|
421
|
+
msg = "Multi-three-phase control does not support gate blocking"
|
|
422
|
+
raise NotImplementedError(msg)
|
|
423
|
+
|
|
424
|
+
def get_command(self, ref: References) -> tuple[float, Sequence[float]]:
|
|
425
|
+
"""Extract the sampling period and the duty ratios from the references."""
|
|
426
|
+
return ref.T_s, ref.d_abc
|
|
427
|
+
|
|
428
|
+
def save(self, t: float, **signal_groups: Any) -> None:
|
|
429
|
+
"""Extend the base class method by saving the DM signals."""
|
|
430
|
+
super().save(t, dm=self.dm, **signal_groups)
|
|
431
|
+
|
|
432
|
+
def update(self, ref: References, fbk: Feedbacks) -> None:
|
|
433
|
+
"""Update the controller states."""
|
|
434
|
+
super().update(ref, fbk)
|
|
435
|
+
self.vector_ctrl.update(ref, fbk)
|
|
436
|
+
if self.speed_ctrl is not None:
|
|
437
|
+
self.speed_ctrl.update(ref.T_s, ref.tau_M)
|
|
438
|
+
self.dm_ctrl.update(ref.T_s, self.dm, fbk.w_c)
|
|
439
|
+
|
|
440
|
+
def post_process(self) -> TimeSeries:
|
|
441
|
+
"""Extend the post-process method."""
|
|
442
|
+
ts = super().post_process()
|
|
443
|
+
theta_c = np.copy(ts.fbk.theta_c)
|
|
444
|
+
self.vector_ctrl.post_process(ts)
|
|
445
|
+
# The vector controller may rotate its signals to other coordinates, e.g., the
|
|
446
|
+
# induction machine controllers to the estimated rotor flux coordinates, and
|
|
447
|
+
# then updates the angle fbk.theta_c. The DM signals are rotated in the same
|
|
448
|
+
# manner.
|
|
449
|
+
T = np.exp(-1j * (ts.fbk.theta_c - theta_c))[:, np.newaxis]
|
|
450
|
+
for name, value in vars(ts.dm).items():
|
|
451
|
+
setattr(ts.dm, name, T * value)
|
|
452
|
+
return ts
|