motulator-export 0.7.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.
Files changed (102) hide show
  1. motulator_export/__init__.py +13 -0
  2. motulator_export/c/README.md +60 -0
  3. motulator_export/c/common.c +546 -0
  4. motulator_export/c/common.h +237 -0
  5. motulator_export/c/drive.c +266 -0
  6. motulator_export/c/drive.h +97 -0
  7. motulator_export/c/gfl_current_vector.c +226 -0
  8. motulator_export/c/gfl_current_vector.h +158 -0
  9. motulator_export/c/gfm_observer.c +251 -0
  10. motulator_export/c/gfm_observer.h +180 -0
  11. motulator_export/c/gradnet.c +144 -0
  12. motulator_export/c/gradnet.h +60 -0
  13. motulator_export/c/grid.c +198 -0
  14. motulator_export/c/grid.h +74 -0
  15. motulator_export/c/im_current_vector.c +463 -0
  16. motulator_export/c/im_current_vector.h +201 -0
  17. motulator_export/c/im_flux_vector.c +262 -0
  18. motulator_export/c/im_flux_vector.h +156 -0
  19. motulator_export/c/native.c +48 -0
  20. motulator_export/c/native_gfl.c +196 -0
  21. motulator_export/c/native_gfm.c +199 -0
  22. motulator_export/c/native_im.c +294 -0
  23. motulator_export/c/native_sm.c +252 -0
  24. motulator_export/c/sim.c +859 -0
  25. motulator_export/c/sim.h +295 -0
  26. motulator_export/c/sm_control_loci.c +155 -0
  27. motulator_export/c/sm_control_loci.h +37 -0
  28. motulator_export/c/sm_flux_vector.c +521 -0
  29. motulator_export/c/sm_flux_vector.h +210 -0
  30. motulator_export/c/sm_machine.c +91 -0
  31. motulator_export/c/sm_machine.h +54 -0
  32. motulator_export/c/sm_parameters.c +106 -0
  33. motulator_export/c/sm_parameters.h +55 -0
  34. motulator_export/matlab/README.md +40 -0
  35. motulator_export/matlab/common.m +537 -0
  36. motulator_export/matlab/gfl_current_vector.m +78 -0
  37. motulator_export/matlab/gfm_observer.m +68 -0
  38. motulator_export/matlab/gradnet.m +117 -0
  39. motulator_export/matlab/im_current_vector.m +178 -0
  40. motulator_export/matlab/im_flux_vector.m +94 -0
  41. motulator_export/matlab/sm_control_loci.m +137 -0
  42. motulator_export/matlab/sm_flux_vector.m +185 -0
  43. motulator_export/matlab/sm_machine.m +69 -0
  44. motulator_export/matlab/sm_parameters.m +89 -0
  45. motulator_export/native/__init__.py +11 -0
  46. motulator_export/native/_build.py +72 -0
  47. motulator_export/native/_drive.py +126 -0
  48. motulator_export/native/_grid.py +184 -0
  49. motulator_export/native/_im.py +152 -0
  50. motulator_export/native/_simulation.py +224 -0
  51. motulator_export/native/_sm.py +157 -0
  52. motulator_export/native/_system.py +343 -0
  53. motulator_export/plecs/__init__.py +11 -0
  54. motulator_export/plecs/_common.py +932 -0
  55. motulator_export/plecs/_control.py +1300 -0
  56. motulator_export/plecs/_drive.py +380 -0
  57. motulator_export/plecs/_export.py +91 -0
  58. motulator_export/plecs/_grid.py +461 -0
  59. motulator_export/plecs/_grid_control.py +918 -0
  60. motulator_export/plecs/_im.py +340 -0
  61. motulator_export/plecs/_im_control.py +656 -0
  62. motulator_export/plecs/_im_vhz_control.py +556 -0
  63. motulator_export/plecs/_netlist.py +1050 -0
  64. motulator_export/plecs/_render.py +182 -0
  65. motulator_export/plecs/_rpc.py +66 -0
  66. motulator_export/plecs/_schematic.py +378 -0
  67. motulator_export/plecs/_sm.py +574 -0
  68. motulator_export/plecs/_sm_control.py +782 -0
  69. motulator_export/simulink/+blocks/add.m +4 -0
  70. motulator_export/simulink/+blocks/add_control_system.m +54 -0
  71. motulator_export/simulink/+blocks/add_converter.m +22 -0
  72. motulator_export/simulink/+blocks/add_mechanics.m +33 -0
  73. motulator_export/simulink/+blocks/add_mfunction.m +40 -0
  74. motulator_export/simulink/+blocks/add_netlist.m +98 -0
  75. motulator_export/simulink/+blocks/add_outputs.m +41 -0
  76. motulator_export/simulink/+blocks/add_pwm.m +34 -0
  77. motulator_export/simulink/+blocks/add_step.m +28 -0
  78. motulator_export/simulink/+blocks/align.m +6 -0
  79. motulator_export/simulink/+blocks/compile.m +9 -0
  80. motulator_export/simulink/+blocks/connect.m +4 -0
  81. motulator_export/simulink/+blocks/new_model.m +40 -0
  82. motulator_export/simulink/+blocks/num.m +8 -0
  83. motulator_export/simulink/+blocks/port_pos.m +6 -0
  84. motulator_export/simulink/+blocks/port_y.m +5 -0
  85. motulator_export/simulink/+blocks/route.m +23 -0
  86. motulator_export/simulink/README.md +114 -0
  87. motulator_export/simulink/__init__.py +12 -0
  88. motulator_export/simulink/_common.py +341 -0
  89. motulator_export/simulink/_drive.py +133 -0
  90. motulator_export/simulink/_export.py +94 -0
  91. motulator_export/simulink/_grid.py +108 -0
  92. motulator_export/simulink/_im.py +78 -0
  93. motulator_export/simulink/_mfunction.py +505 -0
  94. motulator_export/simulink/_render.py +341 -0
  95. motulator_export/simulink/_sfunction.py +220 -0
  96. motulator_export/simulink/_sm.py +120 -0
  97. motulator_export/simulink/build_drive.m +319 -0
  98. motulator_export/simulink/build_grid.m +221 -0
  99. motulator_export-0.7.0.dist-info/METADATA +96 -0
  100. motulator_export-0.7.0.dist-info/RECORD +102 -0
  101. motulator_export-0.7.0.dist-info/WHEEL +4 -0
  102. motulator_export-0.7.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,13 @@
1
+ """
2
+ Export motulator systems to PLECS and Simulink, and simulate them natively.
3
+
4
+ The subpackages `plecs` and `simulink` write the models of a system built in
5
+ motulator, and `native` simulates the system in C. All of them run the same C port of
6
+ the control algorithms of motulator.
7
+
8
+ """
9
+
10
+ from motulator_export import native, plecs, simulink
11
+ from motulator_export.plecs._common import StepSignal
12
+
13
+ __all__ = ["StepSignal", "native", "plecs", "simulink"]
@@ -0,0 +1,60 @@
1
+ # C port
2
+
3
+ C port of the *motulator* control algorithms and models, run in the C-Script blocks of the PLECS models, in the S-functions of the Simulink models, and in the libraries of the native simulation (`motulator_export.native`).
4
+ The code follows the Python code of *motulator* closely, including the order of the state updates, and each header lists its Python counterparts.
5
+ Optional parameters (`None` in *motulator*) are represented by `NAN`, and their defaults are resolved in the C code, as in *motulator*.
6
+
7
+ Each class is a type with its functions (e.g., `FluxObserver` with `flux_observer_init`, `flux_observer_compute_output`, and `flux_observer_update`), which the blocks of the classes of the control systems run.
8
+ The measurements, the feedback signals, and the references between the blocks are signal vectors (meas, fbk, and ref), whose layouts are defined in the headers of the control methods (e.g., `MEAS_WIDTH` and `FBK_w_M` in `sm_flux_vector.h`) with their pack and unpack functions.
9
+ The whole-system functions (e.g., `vector_control_system_compute_output`) run the control system in one block; they are used by the reference control systems of the tests.
10
+ Their gate enable (`ControlSystem.set_enable` of *motulator*) selects the standby output of the controllers (e.g., `flux_vector_ctrl_compute_standby_output`) with the measured converter voltage, as in *motulator*; in the blocks of the classes, the block `compute_standby_output` of each controller selects the references.
11
+
12
+ | File | Contents |
13
+ | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
14
+ | `common.c` | `PIController`, `ComplexPIController`, `SpeedController`, `SpeedObserver`, `CurrentLimiter`, `PowerReferenceLimiter`, `RateLimiter`, `PWM` (MPE or MME overmodulation, double or single update) (with the duty-ratio error model `InverterNonlinearity` of the dead time and the threshold voltage, the current direction given by `np.sign` or `tanh(i/i_0)`), the discretization of the observers (`hold_equivalent` and `matched_pole`), root finding (`brentq`), and utility functions |
15
+ | `gradnet.c` | GradNet inference (`FluxMap`, `CurrentMap`, and `CurrentMapWithHarmonics`) |
16
+ | `sm_parameters.c` | `SynchronousMachinePars` and `SaturatedSynchronousMachinePars` (flux linkage, incremental inductances, and current iteration) |
17
+ | `sm_control_loci.c` | `ControlLoci` (MTPA, MTPV, and current-limit loci) |
18
+ | `sm_flux_vector.c` | `FluxObserver`, `SpeedFluxObserver`, `ReferenceGenerator`, `FluxTorqueController`, `FluxVectorController`, and `VectorControlSystem` for synchronous machines |
19
+ | `sm_machine.c` | `SynchronousMachine` with `SpatialSaturatedSynchronousMachinePars` and `MechanicalSystem` |
20
+ | `im_current_vector.c` | `FluxObserver`, `SpeedFluxObserver`, `CurrentReferenceGenerator`, `CurrentController`, `CurrentVectorController`, and `VectorControlSystem` for induction machines |
21
+ | `im_flux_vector.c` | `FluxTorqueController`, `ReferenceGenerator`, `ObserverBasedVHzController` (including open-loop V/Hz control with `L_M = inf`), and `VHzControlSystem` with the `RateLimiter` of the speed reference for induction machines |
22
+ | `gfl_current_vector.c` | Grid-following `CurrentVectorController` (`PLL`, `CurrentController`, and the current and power references) and `GridConverterControlSystem` |
23
+ | `gfm_observer.c` | Disturbance-observer-based grid-forming control (the discrete-time `Observer` and `ObserverBasedGridFormingController`) and `GridConverterControlSystem` |
24
+ | `sim.c` | Native simulation: `Simulation` (the simulation loop, a single model and control system, the gates enabled) with the RK45 solver of scipy, the computational delay (`Delay`), the PWM models `ZOH` and `CarrierComparison`, `VoltageSourceConverter` with a constant DC-bus voltage or a DC-bus model (`DCBus` or `DiodeBridgeDCBus`), `StepSignal`, and the logs of the results |
25
+ | `drive.c` | Native simulation: `Drive` with `SynchronousMachine` (`SynchronousMachinePars` or `SpatialSaturatedSynchronousMachinePars` with a GradNet current map with spatial harmonics) or `InductionMachine` (constant parameters), and `MechanicalSystem` |
26
+ | `grid.c` | Native simulation: `GridConverterSystem` with `LFilter` or `LCLFilter` and `ThreePhaseSource` |
27
+ | `native_sm.c` | Native simulation: the library of synchronous machine drives with flux-vector control (`sm_flux_vector.c` and `drive.c`), with its API for Python |
28
+ | `native_im.c` | Native simulation: the library of induction machine drives with current-vector control or observer-based V/Hz control (`im_current_vector.c`, `im_flux_vector.c`, and `drive.c`) |
29
+ | `native_gfl.c` | Native simulation: the library of grid converter systems with grid-following control (`gfl_current_vector.c` and `grid.c`) |
30
+ | `native_gfm.c` | Native simulation: the library of grid converter systems with grid-forming control (`gfm_observer.c` and `grid.c`) |
31
+ | `native.c` | Native simulation: the helpers of the libraries (the limited voltage reference and the settings of the PWM) |
32
+
33
+ ## Native simulation
34
+
35
+ The libraries of the native simulation (`native_<name>.c`) combine the C port of a control system with C models of the system, which `sim.c` simulates as the simulation of *motulator* (`Simulation.simulate`): the control system is run at its sampling instants, the duty ratios pass through the computational delay and the PWM model, and the states are integrated between the sampling and switching instants by a port of the RK45 solver of scipy, taking the same steps.
36
+ A model (`Model` in `sim.h`) and a control system (`ControlSystem`) are interfaces of function pointers, embedded as the first members of the concrete types (e.g., `Drive` in `drive.h`).
37
+ The API functions setting the converter, its DC bus, and its modulator are in `sim.c`, those of the models in `drive.c` and `grid.c`, and those of the control systems in the libraries.
38
+ The states are complex and in the order of *motulator*, and the control system logs the signal groups of *motulator* (`ref` and `fbk`), described by a string of their fields (e.g., `FVC_FIELDS` in `native_sm.c`), so that the Python module fills the time series of the *motulator* objects and post-processes them as *motulator* does.
39
+ A new type of system needs its C model (e.g., a machine in `drive.c`), a library combining the model with the C port of its control system, and a Python module setting the parameters (e.g., `motulator_export/native/_im.py`).
40
+
41
+ The native simulation takes the same solver steps as *motulator*, so the results agree to the rounding errors: about 1e-12 relative to the signal magnitudes, also with the GradNet models, see `tests/test_native.py`.
42
+ With the carrier comparison (`pwm=True`), a rounding error can change a quantized duty ratio and thereby a switching instant, after which the solutions differ more; with a fine quantization (`CarrierComparison(N=2**24)`), they agree to about 1e-7.
43
+ Similarly, the diode bridge (`DiodeBridgeDCBus`) clamps its inductor current at zero, which makes the step sizes sensitive to the rounding errors (differences of about 1e-7 to 1e-5, depending on the system and the simulated time).
44
+ The closed-loop simulations of *motulator* itself are equally sensitive to the rounding errors.
45
+
46
+ ## Tests
47
+
48
+ The tests in `tests/` compile the C sources with gcc and compare the results with *motulator* step by step, without PLECS.
49
+ They run in GitHub Actions on every push and weekly against the main branch of *motulator*, to detect changes in *motulator* that must be mirrored here.
50
+
51
+ ## GradNets
52
+
53
+ Both *motulator* and the C port evaluate the GradNets in double precision, and the incremental inductances are computed from the analytic Jacobian of the flux map in both, so the tests compare the C port with *motulator* using the relative tolerance of 1e-9.
54
+ The sizes of the networks are limited by `GRADNET_MAX_IN_DIM` and `GRADNET_MAX_EMBED_DIM` in `gradnet.h`, which are checked both when a model is written and at the start of the simulation.
55
+
56
+ ## Portability
57
+
58
+ The C port uses the C99 complex type (`complex.h`) in double precision, mirroring the complex space vectors of *motulator*.
59
+ It compiles with gcc, clang, MinGW-w64 (e.g., for `mex` in MATLAB), and the compiler bundled with PLECS, but not with MSVC, and embedded compilers may lack `complex.h`.
60
+ A conversion to a custom complex type or to single precision is straightforward with the C-port tests, but it is left until a concrete embedded target exists.
@@ -0,0 +1,546 @@
1
+ /*
2
+ * Common control functions and classes, ported from motulator. See common.h.
3
+ */
4
+
5
+ #include "common.h"
6
+
7
+ /* Utility functions -------------------------------------------------------- */
8
+
9
+ static double complex abc2complex(const double u[3])
10
+ {
11
+ return (2.0 / 3.0) * u[0] - (u[1] + u[2]) / 3.0 + I * (u[1] - u[2]) / sqrt(3.0);
12
+ }
13
+
14
+ static void complex2abc(double complex u, double u_abc[3])
15
+ {
16
+ u_abc[0] = creal(u);
17
+ u_abc[1] = 0.5 * (-creal(u) + sqrt(3.0) * cimag(u));
18
+ u_abc[2] = 0.5 * (-creal(u) - sqrt(3.0) * cimag(u));
19
+ }
20
+
21
+ static double complex line2complex(const double u[2])
22
+ {
23
+ return (2.0 * u[0] + u[1]) / 3.0 + I * u[1] / sqrt(3.0);
24
+ }
25
+
26
+ static double complex complex_from(double re, double im)
27
+ {
28
+ /* A complex number has the representation of an array of its real and imaginary
29
+ * parts (C99 6.2.5) */
30
+ double complex z;
31
+ ((double *)&z)[0] = re;
32
+ ((double *)&z)[1] = im;
33
+ return z;
34
+ }
35
+
36
+ static double wrap(double theta)
37
+ {
38
+ /* Limit the angle into the range [-pi, pi), as numpy.mod does */
39
+ double x = fmod(theta + M_PI, 2.0 * M_PI);
40
+ if (x < 0.0) {
41
+ x += 2.0 * M_PI;
42
+ }
43
+ return x - M_PI;
44
+ }
45
+
46
+ static double clip(double value, double min_value, double max_value)
47
+ {
48
+ return fmax(fmin(value, max_value), min_value);
49
+ }
50
+
51
+ static double sign(double x)
52
+ {
53
+ return (x < 0.0) ? -1.0 : ((x > 0.0) ? 1.0 : 0.0);
54
+ }
55
+
56
+ static double lut_interp(const LUT *lut, double x)
57
+ {
58
+ int n = lut->n;
59
+ if (x <= lut->x[0]) {
60
+ return lut->y[0];
61
+ }
62
+ if (x >= lut->x[n - 1]) {
63
+ return lut->y[n - 1];
64
+ }
65
+ /* Binary search for the interval x[k] <= x < x[k + 1] */
66
+ int lo = 0, hi = n - 1;
67
+ while (hi - lo > 1) {
68
+ int mid = (lo + hi) / 2;
69
+ if (lut->x[mid] <= x) {
70
+ lo = mid;
71
+ } else {
72
+ hi = mid;
73
+ }
74
+ }
75
+ double slope = (lut->y[hi] - lut->y[lo]) / (lut->x[hi] - lut->x[lo]);
76
+ return slope * (x - lut->x[lo]) + lut->y[lo];
77
+ }
78
+
79
+ /* Port of scipy/optimize/Zeros/brentq.c with xtol = 2e-12, rtol = 4*eps, and
80
+ * maxiter = 100 (the defaults of scipy.optimize.brentq). The function values at the
81
+ * bracket ends must have opposite signs (or be zero). */
82
+ static double brentq(ScalarFunction f, double xa, double xb, void *data)
83
+ {
84
+ const double xtol = 2e-12;
85
+ const double rtol = 8.881784197001252e-16;
86
+ const int iter = 100;
87
+ double xpre = xa, xcur = xb;
88
+ double xblk = 0.0, fblk = 0.0, spre = 0.0, scur = 0.0;
89
+ double fpre = f(xpre, data);
90
+ double fcur = f(xcur, data);
91
+
92
+ if (fpre == 0.0) {
93
+ return xpre;
94
+ }
95
+ if (fcur == 0.0) {
96
+ return xcur;
97
+ }
98
+ if (signbit(fpre) == signbit(fcur)) {
99
+ return NAN; /* No sign change */
100
+ }
101
+ for (int i = 0; i < iter; i++) {
102
+ if (fpre != 0.0 && fcur != 0.0 && signbit(fpre) != signbit(fcur)) {
103
+ xblk = xpre;
104
+ fblk = fpre;
105
+ spre = scur = xcur - xpre;
106
+ }
107
+ if (fabs(fblk) < fabs(fcur)) {
108
+ xpre = xcur;
109
+ xcur = xblk;
110
+ xblk = xpre;
111
+ fpre = fcur;
112
+ fcur = fblk;
113
+ fblk = fpre;
114
+ }
115
+ double delta = (xtol + rtol * fabs(xcur)) / 2.0;
116
+ double sbis = (xblk - xcur) / 2.0;
117
+ if (fcur == 0.0 || fabs(sbis) < delta) {
118
+ return xcur;
119
+ }
120
+ if (fabs(spre) > delta && fabs(fcur) < fabs(fpre)) {
121
+ double stry;
122
+ if (xpre == xblk) {
123
+ /* Interpolate */
124
+ stry = -fcur * (xcur - xpre) / (fcur - fpre);
125
+ } else {
126
+ /* Extrapolate */
127
+ double dpre = (fpre - fcur) / (xpre - xcur);
128
+ double dblk = (fblk - fcur) / (xblk - xcur);
129
+ stry = -fcur * (fblk * dblk - fpre * dpre) / (dblk * dpre * (fblk - fpre));
130
+ }
131
+ if (2.0 * fabs(stry) < fmin(fabs(spre), 3.0 * fabs(sbis) - delta)) {
132
+ /* Good short step */
133
+ spre = scur;
134
+ scur = stry;
135
+ } else {
136
+ /* Bisect */
137
+ spre = sbis;
138
+ scur = sbis;
139
+ }
140
+ } else {
141
+ /* Bisect */
142
+ spre = sbis;
143
+ scur = sbis;
144
+ }
145
+ xpre = xcur;
146
+ fpre = fcur;
147
+ if (fabs(scur) > delta) {
148
+ xcur += scur;
149
+ } else {
150
+ xcur += (sbis > 0.0 ? delta : -delta);
151
+ }
152
+ fcur = f(xcur, data);
153
+ }
154
+ return xcur;
155
+ }
156
+
157
+ /* PIController ------------------------------------------------------------- */
158
+
159
+ static void pi_init(PIController *self, double k_p, double k_t, double alpha_i,
160
+ double u_max)
161
+ {
162
+ self->k_p = k_p;
163
+ self->k_t = k_t;
164
+ self->alpha_i = alpha_i;
165
+ self->u_max = u_max;
166
+ self->u_i = 0.0;
167
+ self->v = 0.0;
168
+ }
169
+
170
+ static double pi_compute_output(PIController *self, double y_ref, double y,
171
+ double u_ff)
172
+ {
173
+ /* Estimate of a disturbance input */
174
+ self->v = self->u_i - (self->k_p - self->k_t) * y + u_ff;
175
+ double u = self->k_t * (y_ref - y) + self->v;
176
+ /* Limit the controller output */
177
+ return clip(u, -self->u_max, self->u_max);
178
+ }
179
+
180
+ static void pi_update(PIController *self, double T_s, double u)
181
+ {
182
+ self->u_i += T_s * self->alpha_i * (u - self->v);
183
+ }
184
+
185
+ /* ComplexPIController ------------------------------------------------------ */
186
+
187
+ static void complex_pi_init(ComplexPIController *self, double k_p, double k_i,
188
+ double k_t)
189
+ {
190
+ self->k_p = k_p;
191
+ self->k_t = k_t;
192
+ self->alpha_i = k_i / k_t;
193
+ self->u_i = 0.0;
194
+ self->v = 0.0;
195
+ }
196
+
197
+ static double complex complex_pi_compute_output(ComplexPIController *self,
198
+ double complex i_ref,
199
+ double complex i,
200
+ double complex u_ff)
201
+ {
202
+ /* Disturbance input estimate */
203
+ self->v = self->u_i - (self->k_p - self->k_t) * i + u_ff;
204
+ return self->k_t * (i_ref - i) + self->v;
205
+ }
206
+
207
+ static void complex_pi_update(ComplexPIController *self, double T_s,
208
+ double complex u, double w_c)
209
+ {
210
+ self->u_i += T_s * (self->alpha_i + I * w_c) * (u - self->v);
211
+ }
212
+
213
+ /* CurrentLimiter ----------------------------------------------------------- */
214
+
215
+ static double complex current_limiter(double i_max, double complex i)
216
+ {
217
+ if (cabs(i) > i_max) {
218
+ i = i_max * i / cabs(i);
219
+ }
220
+ return i;
221
+ }
222
+
223
+ /* PowerReferenceLimiter ---------------------------------------------------- */
224
+
225
+ static void power_reference_limiter_init(PowerReferenceLimiter *self, double i_d_max,
226
+ double alpha_l, double u_nom)
227
+ {
228
+ self->i_d_max = i_d_max;
229
+ self->alpha_l = alpha_l;
230
+ self->p_max = 1.5 * u_nom * i_d_max;
231
+ }
232
+
233
+ static double power_reference_limiter_compute_output(const PowerReferenceLimiter *self,
234
+ double p_g_ref)
235
+ {
236
+ return copysign(fmin(fabs(p_g_ref), self->p_max), p_g_ref);
237
+ }
238
+
239
+ static void power_reference_limiter_update(PowerReferenceLimiter *self, double T_s,
240
+ double complex u_c, double complex i_c,
241
+ double complex i_cn)
242
+ {
243
+ double abs_u_c = fmax(cabs(u_c), 1e-6);
244
+ double i_q = cimag(u_c * conj(i_c)) / abs_u_c;
245
+ double i_d_max = fmax(self->i_d_max - cabs(i_cn), 0.0);
246
+ double i_d_lim_sqr = fmax(i_d_max * i_d_max - i_q * i_q, 0.0);
247
+ double p_max = 1.5 * abs_u_c * sqrt(i_d_lim_sqr);
248
+ self->p_max += T_s * self->alpha_l * (p_max - self->p_max);
249
+ }
250
+
251
+ /* SpeedController ---------------------------------------------------------- */
252
+
253
+ static PIController speed_controller(double J, double alpha_s, double alpha_i,
254
+ double tau_M_max)
255
+ {
256
+ PIController ctrl;
257
+ alpha_i = isnan(alpha_i) ? alpha_s : alpha_i;
258
+ double k_p = (alpha_s + alpha_i) * J;
259
+ double k_i = alpha_s * alpha_i * J;
260
+ double k_t = alpha_s * J;
261
+ pi_init(&ctrl, k_p, k_t, k_i / k_t, tau_M_max);
262
+ return ctrl;
263
+ }
264
+
265
+ /* SpeedObserver ------------------------------------------------------------ */
266
+
267
+ static void speed_observer_init(SpeedObserver *self, double k_w, double k_tau,
268
+ double J)
269
+ {
270
+ self->k_w = k_w;
271
+ self->k_tau = k_tau;
272
+ self->J = J;
273
+ self->w_M = 0.0;
274
+ self->tau_L = 0.0;
275
+ }
276
+
277
+ static void speed_observer_update(SpeedObserver *self, double T_s, double eps,
278
+ double tau_M)
279
+ {
280
+ double d_w_M, d_tau_L;
281
+ if (self->J <= 0.0) {
282
+ d_w_M = self->k_w * eps;
283
+ d_tau_L = 0.0;
284
+ } else {
285
+ d_w_M = (tau_M - self->tau_L) / self->J + self->k_w * eps;
286
+ d_tau_L = -self->k_tau * eps;
287
+ }
288
+ self->w_M += T_s * d_w_M;
289
+ self->tau_L += T_s * d_tau_L;
290
+ }
291
+
292
+ /* Discretization ----------------------------------------------------------- */
293
+
294
+ static void hold_equivalent(double T_s, double w_c, double complex *Phi,
295
+ double complex *Gamma)
296
+ {
297
+ /* Half-angle form, requiring a single sine-cosine pair. The sinc function is
298
+ * computed as numpy.sinc(x/pi). */
299
+ double x = 0.5 * w_c * T_s;
300
+ double complex exp_jx = cexp(-I * x);
301
+ double y = M_PI * ((x != 0.0) ? x / M_PI : 1e-20);
302
+ *Phi = exp_jx * exp_jx;
303
+ *Gamma = T_s * (sin(y) / y) * exp_jx;
304
+ }
305
+
306
+ static double matched_pole(double alpha, double T_s)
307
+ {
308
+ return -expm1(-alpha * T_s) / T_s;
309
+ }
310
+
311
+ /* RateLimiter -------------------------------------------------------------- */
312
+
313
+ static void rate_limiter_init(RateLimiter *self, double rate_limit)
314
+ {
315
+ self->rate_limit = rate_limit;
316
+ self->old_y = 0.0;
317
+ }
318
+
319
+ static double rate_limiter_compute_output(const RateLimiter *self, double T_s,
320
+ double u)
321
+ {
322
+ double rate = (u - self->old_y) / T_s;
323
+ if (rate > self->rate_limit) {
324
+ return self->old_y + T_s * self->rate_limit;
325
+ }
326
+ if (rate < -self->rate_limit) {
327
+ return self->old_y - T_s * self->rate_limit;
328
+ }
329
+ return u;
330
+ }
331
+
332
+ static void rate_limiter_update(RateLimiter *self, double y)
333
+ {
334
+ self->old_y = y;
335
+ }
336
+
337
+ /* Inverter nonlinearity ------------------------------------------------------ */
338
+
339
+ static void inverter_nonlinearity(double t_d, double u_th, double i_0,
340
+ const double i_abc[3], const double d_abc[3],
341
+ double u_dc, double T_half, double d_err[3])
342
+ {
343
+ for (int k = 0; k < 3; k++) {
344
+ /* Switching-cycle averaged on-times of the upper and lower switches
345
+ * (averaged_gate_signals) */
346
+ double d = d_abc[k];
347
+ double delta = (d > 0.0 && d < 1.0) ? t_d / (2.0 * T_half) : 0.0;
348
+ double q_hi = fmax(d - delta, 0.0);
349
+ double q_lo = fmax(1.0 - d - delta, 0.0);
350
+ double b = 1.0 - q_hi - q_lo;
351
+ /* Current direction: tanh(i/i_0), or the signum function if i_0 = 0 */
352
+ double s = (i_0 > 0.0) ? tanh(i_abc[k] / i_0) : sign(i_abc[k]);
353
+ d_err[k] = d - q_hi - 0.5 * b * (1.0 - s) + s * u_th / u_dc;
354
+ }
355
+ }
356
+
357
+ /* PWM ---------------------------------------------------------------------- */
358
+
359
+ static void pwm_init(PWM *self, double k_pred)
360
+ {
361
+ self->k_pred = k_pred;
362
+ self->mme = 0;
363
+ self->t_d = 0.0;
364
+ self->u_th = 0.0;
365
+ self->i_0 = 0.0;
366
+ self->feedforward = 1;
367
+ self->d_min = 0.0;
368
+ self->single_update = 0;
369
+ for (int k = 0; k < 3; k++) {
370
+ self->d_abc[0][k] = 0.0;
371
+ self->d_abc[1][k] = 0.0;
372
+ }
373
+ /* Before the first duty ratios, the legs do not switch, and the infinite period
374
+ * gives no dead-time error */
375
+ self->T_half = INFINITY;
376
+ }
377
+
378
+ static void pwm_set_nonlinearity(PWM *self, double t_d, double u_th, double i_0,
379
+ int feedforward)
380
+ {
381
+ self->t_d = t_d;
382
+ self->u_th = u_th;
383
+ self->i_0 = i_0;
384
+ self->feedforward = feedforward;
385
+ }
386
+
387
+ static void pwm_set_overmodulation(PWM *self, int mme)
388
+ {
389
+ self->mme = mme;
390
+ }
391
+
392
+ static void pwm_set_min_pulse(PWM *self, double d_min)
393
+ {
394
+ self->d_min = d_min;
395
+ }
396
+
397
+ static void pwm_set_single_update(PWM *self, int single_update)
398
+ {
399
+ self->single_update = single_update;
400
+ }
401
+
402
+ /* Error model of the PWM in use (d_err is not None) */
403
+ static int pwm_has_error_model(const PWM *self)
404
+ {
405
+ return self->t_d > 0.0 || self->u_th > 0.0;
406
+ }
407
+
408
+ /* Half carrier period (s) corresponding to the sampling period */
409
+ static double pwm_half_period(const PWM *self, double T_s)
410
+ {
411
+ return self->single_update ? T_s / 2.0 : T_s;
412
+ }
413
+
414
+ static void pwm_limit_pulses(const PWM *self, double d_abc[3], const double d_ref[3],
415
+ const double *i_abc, double u_dc, double T_half)
416
+ {
417
+ for (int k = 0; k < 3; k++) {
418
+ double candidates[2]; /* The leg switching, the leg clamped */
419
+ if (d_abc[k] > 0.0 && d_abc[k] < self->d_min) {
420
+ candidates[0] = self->d_min;
421
+ candidates[1] = 0.0;
422
+ } else if (d_abc[k] > 1.0 - self->d_min && d_abc[k] < 1.0) {
423
+ candidates[0] = 1.0 - self->d_min;
424
+ candidates[1] = 1.0;
425
+ } else {
426
+ continue;
427
+ }
428
+ double errors[2];
429
+ for (int j = 0; j < 2; j++) {
430
+ d_abc[k] = candidates[j];
431
+ double realized = candidates[j];
432
+ if (i_abc != NULL) {
433
+ double d_err[3];
434
+ inverter_nonlinearity(self->t_d, self->u_th, self->i_0, i_abc, d_abc,
435
+ u_dc, T_half, d_err);
436
+ realized -= d_err[k];
437
+ }
438
+ errors[j] = fabs(realized - d_ref[k]);
439
+ }
440
+ d_abc[k] = (errors[1] < errors[0]) ? candidates[1] : candidates[0];
441
+ }
442
+ }
443
+
444
+ static double complex pwm_realized_voltage(const PWM *self, double complex i_c_ab,
445
+ double u_dc, int average)
446
+ {
447
+ /* A discharged DC bus, e.g., with blocked gates */
448
+ if (u_dc <= 0.0) {
449
+ return 0.0;
450
+ }
451
+ double i_abc[3];
452
+ complex2abc(i_c_ab, i_abc);
453
+ int k0 = average ? 0 : 1;
454
+ double complex q_ab = 0.0;
455
+ for (int k = k0; k < 2; k++) {
456
+ double d[3] = {self->d_abc[k][0], self->d_abc[k][1], self->d_abc[k][2]};
457
+ if (pwm_has_error_model(self)) {
458
+ double d_err[3];
459
+ inverter_nonlinearity(self->t_d, self->u_th, self->i_0, i_abc, d, u_dc,
460
+ self->T_half, d_err);
461
+ for (int j = 0; j < 3; j++) {
462
+ d[j] -= d_err[j];
463
+ }
464
+ }
465
+ q_ab += abc2complex(d);
466
+ }
467
+ return u_dc * q_ab / (double)(2 - k0);
468
+ }
469
+
470
+ static void pwm_duty_ratios(double complex u_c_ref_ab, double u_dc, int mme,
471
+ double d_abc[3])
472
+ {
473
+ double u_abc[3];
474
+ complex2abc(u_c_ref_ab, u_abc);
475
+
476
+ /* Zero-sequence voltage resulting in space-vector PWM */
477
+ double u_max = fmax(fmax(u_abc[0], u_abc[1]), u_abc[2]);
478
+ double u_min = fmin(fmin(u_abc[0], u_abc[1]), u_abc[2]);
479
+ double u_0 = 0.5 * (u_max + u_min);
480
+ for (int k = 0; k < 3; k++) {
481
+ u_abc[k] -= u_0;
482
+ }
483
+
484
+ /* MPE overmodulation (MME results from limiting the duty ratios) */
485
+ double m = (2.0 / u_dc) * fmax(fmax(u_abc[0], u_abc[1]), u_abc[2]);
486
+ if (!mme && m > 1.0) {
487
+ for (int k = 0; k < 3; k++) {
488
+ u_abc[k] = u_abc[k] / m;
489
+ }
490
+ }
491
+
492
+ /* Duty ratios */
493
+ for (int k = 0; k < 3; k++) {
494
+ d_abc[k] = fmax(fmin(u_abc[k] / u_dc + 0.5, 1.0), 0.0);
495
+ }
496
+ }
497
+
498
+ static double complex pwm_compute_output(const PWM *self, double T_s,
499
+ double complex u_c_ref_ab, double u_dc,
500
+ double w, double complex i_c_ab,
501
+ double d_abc[3])
502
+ {
503
+ /* A discharged DC bus, e.g., with blocked gates before the precharge */
504
+ if (u_dc <= 0.0) {
505
+ for (int k = 0; k < 3; k++) {
506
+ d_abc[k] = 0.5;
507
+ }
508
+ return 0.0;
509
+ }
510
+
511
+ /* Duty ratios */
512
+ pwm_duty_ratios(u_c_ref_ab, u_dc, self->mme, d_abc);
513
+ double d_ref[3] = {d_abc[0], d_abc[1], d_abc[2]};
514
+
515
+ /* Compensate for the duty-ratio error using the predicted currents */
516
+ int compensate = pwm_has_error_model(self) && self->feedforward;
517
+ double T_half = pwm_half_period(self, T_s);
518
+ double i_c_abc[3];
519
+ if (compensate) {
520
+ double d_err[3];
521
+ complex2abc(cexp(I * self->k_pred * T_s * w) * i_c_ab, i_c_abc);
522
+ inverter_nonlinearity(self->t_d, self->u_th, self->i_0, i_c_abc, d_abc, u_dc,
523
+ T_half, d_err);
524
+ for (int k = 0; k < 3; k++) {
525
+ d_abc[k] = clip(d_abc[k] + d_err[k], 0.0, 1.0);
526
+ }
527
+ }
528
+
529
+ /* Minimum pulses */
530
+ if (self->d_min > 0.0) {
531
+ pwm_limit_pulses(self, d_abc, d_ref, compensate ? i_c_abc : NULL, u_dc,
532
+ T_half);
533
+ }
534
+
535
+ /* Limited voltage reference, including the compensation */
536
+ return abc2complex(d_abc) * u_dc;
537
+ }
538
+
539
+ static void pwm_update(PWM *self, double T_s, const double d_abc[3])
540
+ {
541
+ for (int k = 0; k < 3; k++) {
542
+ self->d_abc[0][k] = self->d_abc[1][k];
543
+ self->d_abc[1][k] = d_abc[k];
544
+ }
545
+ self->T_half = pwm_half_period(self, T_s);
546
+ }