Chapter 27: Applications in Process and Power Systems
Lesson 2: Adaptive Control in Power Electronics and Inverters
This lesson develops a direct adaptive current controller for a three-phase voltage-source inverter using an averaged synchronous-frame model. The derivation connects converter physics, model-reference matching, Lyapunov stability, digital implementation, modulation constraints, and reproducible simulations in Python, C++, Java, MATLAB/Simulink, and Wolfram Mathematica.
1. Learning Objectives and Control Architecture
After completing this lesson, the student should be able to:
- derive an averaged \(dq\) model of a grid-connected voltage-source inverter with an \(L\) interface;
- identify which converter and grid parameters create a need for adaptation;
- construct a direct model-reference adaptive current controller without explicitly estimating \(R\) and \(L\);
- prove boundedness and asymptotic current-tracking convergence using a Lyapunov function;
- implement normalized, projected, leakage-modified updates in sampled time; and
- recognize where switching ripple, an \(LCL\) filter, a phase-locked loop, and voltage saturation invalidate the nominal proof.
flowchart TD
R["Current commands: id_ref and iq_ref"] --> RM["Stable reference model"]
RM --> E["Tracking error e"]
M["Measured dq current"] --> E
E --> A["Adaptive update law"]
M --> A
A --> K["Controller gains kx, kr, kw"]
K --> C["Adaptive dq voltage command"]
M --> C
G["Measured grid dq voltage"] --> C
C --> PWM["PWM and two-level VSI"]
D["DC source Vdc"] --> PWM
PWM --> F["L or LCL interface"]
F --> P["Grid or AC load"]
P --> M
The inner current loop is the natural adaptive-control target because it is fast, directly affected by filter and grid-impedance uncertainty, and supplies the controlled current variables used by outer DC-link, active-power, reactive-power, or voltage loops.
2. Averaged Model of a Three-Phase Voltage-Source Inverter
Consider a two-level three-phase voltage-source inverter supplied by a DC-link voltage \(V_{\mathrm{dc}}\). For switching state \( s_x \in \left\{0,1\right\} \) of phase \( x \in \left\{a,b,c\right\} \), the pole voltage relative to the DC-link midpoint is
\[ v_{xN}=\left(2s_x-1\right)\frac{V_{\mathrm{dc}}}{2}. \]
A switching model resolves every semiconductor commutation. For controller synthesis, the switching function is replaced by its switching-period average. With modulation vector \(\mathbf{m}_{dq}=[m_d\;m_q]^T\), a convenient linear modulation model is
\[ \mathbf{v}_{c,dq}= \frac{V_{\mathrm{dc}}}{2}\mathbf{m}_{dq}, \qquad \left\|\mathbf{m}_{dq}\right\|\le m_{\max}. \]
The constant \(m_{\max}\) represents the selected linear modulation region. Its numerical value depends on the precise normalization and modulation strategy; therefore, the controller must use the same convention as the PWM implementation.
For a balanced converter connected to a grid through a series resistance \(R\) and inductance \(L\), the synchronous-frame current dynamics are
\[ L\dot{\mathbf{i}}= -R\mathbf{i}+\omega L\mathbf{J}\mathbf{i} +\mathbf{v}_c-\mathbf{v}_g, \qquad \mathbf{J}= \begin{bmatrix}0&1\\-1&0\end{bmatrix}, \]
where \(\mathbf{i}=[i_d\;i_q]^T\), \(\mathbf{v}_c=[v_{cd}\;v_{cq}]^T\), \(\mathbf{v}_g=[v_{gd}\;v_{gq}]^T\), and \(\omega\) is the angular speed of the rotating reference frame. The sign of the cross-coupling term depends on the Park-transform convention; one convention must be used consistently in the model, controller, and software.
Define the controllable voltage increment
\[ \mathbf{u}=\mathbf{v}_c-\mathbf{v}_g, \qquad a=\frac{R}{L}, \qquad b=\frac{1}{L}>0. \]
The plant then becomes
\[ \dot{\mathbf{i}} =-a\mathbf{i}+\omega\mathbf{J}\mathbf{i}+b\mathbf{u}. \tag{1} \]
Equation (1) is linearly parameterized in the unknown combinations \(a=R/L\) and \(b=1/L\). This structure permits either indirect adaptation of physical parameters or direct adaptation of controller gains. The present lesson uses the direct approach.
3. Why Adaptation Is Useful in an Inverter Current Loop
A fixed current controller is designed around nominal values \(R_0,L_0,V_{\mathrm{dc},0},\omega_0\). In operation, the effective plant can change because of:
- inductor tolerance, magnetic saturation, and temperature-dependent winding resistance;
- unknown transformer and grid inductance added to the converter-side filter;
- DC-link variation, dead time, voltage drops, and modulation nonlinearities;
- frequency-estimation and angle-estimation errors in the synchronous frame; and
- unmodeled resonant modes when an \(LCL\) rather than an \(L\) interface is used.
For a nominal decoupled proportional current controller with gain \(K_p\), a simplified closed-loop pole is approximately
\[ s_{\mathrm{cl}}\approx -\frac{R+K_p}{L}. \]
Consequently, an increase in \(L\) reduces bandwidth, while an error in cross-coupling compensation introduces axis interaction. Adaptation is valuable when these changes are large enough to impair performance but slow enough for the adaptive loop to track. It does not remove the need for a physically valid filter model, sufficient DC-link voltage, overcurrent protection, or switching-time constraints.
4. Current References and Their Power-System Meaning
With the synchronous \(d\) axis aligned to the positive sequence of the grid voltage, one obtains approximately \(v_{gq}\approx0\). Under the power-invariant convention used here, instantaneous three-phase active and reactive powers are
\[ p=\frac{3}{2}\left(v_{gd}i_d+v_{gq}i_q\right), \qquad q=\frac{3}{2}\left(v_{gq}i_d-v_{gd}i_q\right). \]
Therefore, when \(v_{gq}\approx0\),
\[ p\approx\frac{3}{2}v_{gd}i_d, \qquad q\approx-\frac{3}{2}v_{gd}i_q. \]
An outer loop may generate \(i_d^\star\) from a DC-link voltage or active-power error and \(i_q^\star\) from a reactive-power or AC-voltage objective. The adaptive inner loop treats the vector \(\mathbf{r}=[i_d^\star\;i_q^\star]^T\) as a bounded reference input. Current magnitude limiting should be applied before the reference enters the inner loop:
\[ \mathbf{r}_{\mathrm{lim}}= \begin{cases} \mathbf{r}, & \|\mathbf{r}\|\le I_{\max},\\ I_{\max}\dfrac{\mathbf{r}}{\|\mathbf{r}\|}, & \|\mathbf{r}\|>I_{\max}. \end{cases} \]
5. Direct Model-Reference Adaptive Current Controller
5.1 Reference Model
Select a stable first-order vector reference model with desired current bandwidth \(\lambda>0\):
\[ \dot{\mathbf{i}}_m =-\lambda\mathbf{i}_m+\lambda\mathbf{r}. \tag{2} \]
The tracking error is \(\mathbf{e}=\mathbf{i}-\mathbf{i}_m\). Choosing \(\lambda\) too high demands excessive converter voltage and makes neglected switching, delay, and resonance dynamics important.
5.2 Adaptive Control Law
Use the direct gain parameterization
\[ \mathbf{u}= \hat{k}_x\mathbf{i} +\hat{k}_r\mathbf{r} +\hat{k}_\omega\mathbf{J}\mathbf{i}. \tag{3} \]
The three scalar gains have a clear role: \(\hat k_x\) shapes the current-state feedback, \(\hat k_r\) supplies reference feedforward, and \(\hat k_\omega\) compensates synchronous-frame cross-coupling.
5.3 Matching Conditions
Substitute (3) into (1). Exact matching with the desired dynamics \(\dot{\mathbf{i}}=-\lambda\mathbf{i}+\lambda\mathbf{r}\) is obtained if constant ideal gains satisfy
\[ -a+b k_x^\star=-\lambda, \qquad b k_r^\star=\lambda, \qquad \omega+b k_\omega^\star=0. \]
Hence,
\[ k_x^\star=\frac{a-\lambda}{b}=R-\lambda L, \qquad k_r^\star=\frac{\lambda}{b}=\lambda L, \qquad k_\omega^\star=-\frac{\omega}{b}=-\omega L. \tag{4} \]
The adaptive controller does not calculate \(R\) or \(L\). It changes the gains in (3) directly. Equation (4) is used only to prove that an ideal constant gain vector exists under the assumed model.
6. Lyapunov Stability Analysis
Define gain errors
\[ \tilde{k}_x=\hat{k}_x-k_x^\star, \qquad \tilde{k}_r=\hat{k}_r-k_r^\star, \qquad \tilde{k}_\omega=\hat{k}_\omega-k_\omega^\star. \]
Using (1)–(4), the exact tracking-error dynamics are
\[ \dot{\mathbf{e}} =-\lambda\mathbf{e} +b\tilde{k}_x\mathbf{i} +b\tilde{k}_r\mathbf{r} +b\tilde{k}_\omega\mathbf{J}\mathbf{i}. \tag{5} \]
Because an inductance is positive, \(b=1/L>0\). Consider
\[ V= \frac{1}{2}\mathbf{e}^T\mathbf{e} +\frac{b}{2\gamma_x}\tilde{k}_x^2 +\frac{b}{2\gamma_r}\tilde{k}_r^2 +\frac{b}{2\gamma_\omega}\tilde{k}_\omega^2, \tag{6} \]
with adaptation gains \(\gamma_x,\gamma_r,\gamma_\omega>0\). Choose
\[ \dot{\hat{k}}_x=-\gamma_x\mathbf{e}^T\mathbf{i}, \qquad \dot{\hat{k}}_r=-\gamma_r\mathbf{e}^T\mathbf{r}, \qquad \dot{\hat{k}}_\omega =-\gamma_\omega\mathbf{e}^T\mathbf{J}\mathbf{i}. \tag{7} \]
Since the ideal gains are constant in the nominal proof, \(\dot{\tilde k}_j=\dot{\hat k}_j\). Differentiating (6), substituting (5) and (7), and grouping equal cross terms gives
\[ \begin{aligned} \dot V &= -\lambda\mathbf{e}^T\mathbf{e} +b\tilde{k}_x\mathbf{e}^T\mathbf{i} +b\tilde{k}_r\mathbf{e}^T\mathbf{r} +b\tilde{k}_\omega\mathbf{e}^T\mathbf{J}\mathbf{i}\\ &\quad +\frac{b}{\gamma_x}\tilde{k}_x\dot{\hat{k}}_x +\frac{b}{\gamma_r}\tilde{k}_r\dot{\hat{k}}_r +\frac{b}{\gamma_\omega}\tilde{k}_\omega \dot{\hat{k}}_\omega\\ &=-\lambda\|\mathbf{e}\|^2\le0. \end{aligned} \tag{8} \]
Thus, \(\mathbf{e}\) and all gain errors are bounded, and \(\mathbf{e}\in L_2\). If the reference, measured current, and their derivatives remain bounded, then \(\dot{\mathbf{e}}\) is bounded. Barbalat's lemma then yields
\[ \lim_{t\to\infty}\mathbf{e}(t)=\mathbf{0}. \]
Tracking convergence does not imply convergence of all adaptive gains to their ideal values. Gain convergence requires sufficient excitation of the regressors \(\mathbf{i},\mathbf{r},\mathbf{J}\mathbf{i}\). Constant current commands can produce excellent tracking while leaving some gain combinations unidentifiable.
7. Robust Sampled-Time Implementation
7.1 Normalization, Leakage, and Projection
A direct forward-Euler implementation of (7) can react strongly to large current commands. Define
\[ \boldsymbol{\psi}_x=\mathbf{i}, \qquad \boldsymbol{\psi}_r=\mathbf{r}, \qquad \boldsymbol{\psi}_\omega=\mathbf{J}\mathbf{i}, \]
and use the practical update
\[ \hat{k}_j[k+1] = \operatorname{Proj}_{[\underline{k}_j,\overline{k}_j]} \left( \hat{k}_j[k] -T_s\frac{\gamma_j\mathbf{e}^T[k]\boldsymbol{\psi}_j[k]} {1+\nu\|\boldsymbol{\Phi}[k]\|^2} -T_s\sigma_j\left(\hat{k}_j[k]-k_{j0}\right) \right), \tag{9} \]
where \(\boldsymbol{\Phi}\) collects the regressors, \(\nu\ge0\) is a normalization weight, \(\sigma_j\ge0\) is leakage, and \(k_{j0}\) is a safe anchor. Normalization limits the update under large signals, leakage reduces drift under disturbances, and projection enforces engineering bounds. These modifications replace the exact asymptotic result (8) by a robustness or ultimate-boundedness result whose residual set depends on disturbances and modification gains.
7.2 Modulation Saturation
The requested converter voltage and modulation are
\[ \mathbf{v}_c^\star=\mathbf{v}_g+\mathbf{u}, \qquad \mathbf{m}^\star=\frac{2}{V_{\mathrm{dc}}} \mathbf{v}_c^\star. \]
Apply a circular limiter:
\[ \mathbf{m}= \begin{cases} \mathbf{m}^\star, & \|\mathbf{m}^\star\|\le m_{\max},\\ m_{\max}\dfrac{\mathbf{m}^\star} {\|\mathbf{m}^\star\|}, & \|\mathbf{m}^\star\|>m_{\max}. \end{cases} \tag{10} \]
Saturation creates an input mismatch and invalidates the cancellation used in (8). A simple safety rule is to freeze the gradient term while saturated and retain only leakage toward safe gains. More advanced designs introduce an auxiliary error driven by the difference between requested and applied voltage.
7.3 Real-Time Sequence
flowchart TD
S["Sample phase currents, grid voltage, Vdc, and angle"] --> T["Transform abc signals to dq"]
T --> R["Limit id_ref and iq_ref"]
R --> M["Update stable reference model"]
M --> E["Compute current error"]
E --> U["Compute adaptive voltage increment"]
U --> V["Add grid-voltage feedforward"]
V --> L["Apply modulation-vector limit"]
L --> G["If saturated: freeze gradient adaptation"]
G --> A["Normalize, leak, and project adaptive gains"]
A --> P["Generate PWM duty ratios"]
P --> S
The current sampling, angle used by the Park transform, control update, and PWM update should be synchronized. A one-sample computational delay is a real dynamic element and should be included in high-bandwidth designs.
8. Numerical Experiment and Tuning Interpretation
The supplied programs simulate an averaged inverter current loop with a step change in filter parameters. They are educational reference implementations, not hardware-ready gate-drive software.
| Quantity | Value | Purpose |
|---|---|---|
| Sampling period | \(T_s=20\,\mu\mathrm{s}\) | Discrete controller and plant integration |
| Grid frequency | \(50\,\mathrm{Hz}\) | \(\omega=2\pi50\) |
| DC-link voltage | \(800\,\mathrm{V}\) | Modulation scaling |
| Grid voltage in aligned frame | \([325\;0]^T\,\mathrm{V}\) | Balanced 230-V phase RMS example |
| Initial interface | \(L=4\,\mathrm{mH}, R=0.50\,\Omega\) | Before \(t=0.12\,\mathrm{s}\) |
| Changed interface | \(L=6\,\mathrm{mH}, R=0.80\,\Omega\) | After \(t=0.12\,\mathrm{s}\) |
| Reference-model pole | \(\lambda=500\,\mathrm{s}^{-1}\) | Desired current response |
| Adaptation gains | \(\gamma_x=\gamma_r=\gamma_\omega=120\) | Normalized gradient speed |
| Leakage | \(\sigma=0.20\) | Drift suppression |
| Modulation limit | \(m_{\max}=0.95\) | Voltage-command feasibility |
From (4), the ideal gains before the parameter change are
\[ k_x^\star=-1.5\,\Omega,\qquad k_r^\star=2.0\,\Omega,\qquad k_\omega^\star\approx-1.257\,\Omega. \]
After the change, they become
\[ k_x^\star=-2.2\,\Omega,\qquad k_r^\star=3.0\,\Omega,\qquad k_\omega^\star\approx-1.885\,\Omega. \]
The simulated gains need not converge exactly to these values because the references are not persistently exciting, leakage biases the estimates toward their anchors, and several gain combinations can generate similar closed-loop trajectories. The primary performance variable is current tracking, not physical-parameter identification.
9. Python Implementation
The Python program requires numpy; plotting is optional and
uses matplotlib. It writes a CSV file containing currents,
model states, errors, adaptive gains, modulation, and saturation status.
Chapter27_Lesson2.py
#!/usr/bin/env python3
"""Chapter 27, Lesson 2: direct adaptive dq-current control of an averaged VSI."""
from __future__ import annotations
import argparse
import csv
import math
from dataclasses import dataclass
from pathlib import Path
import numpy as np
@dataclass(frozen=True)
class Config:
sample_time: float = 20e-6
stop_time: float = 0.30
grid_frequency_hz: float = 50.0
dc_voltage: float = 800.0
modulation_limit: float = 0.95
reference_bandwidth: float = 500.0
gamma_x: float = 120.0
gamma_r: float = 120.0
gamma_w: float = 120.0
sigma: float = 0.20
normalization_weight: float = 1.0
def reference_current(time_s: float) -> np.ndarray:
"""Return [i_d_ref, i_q_ref] in ampere."""
i_d_ref = 8.0 if time_s >= 0.020 else 0.0
i_q_ref = -4.0 if time_s >= 0.160 else 0.0
return np.array([i_d_ref, i_q_ref], dtype=float)
def plant_parameters(time_s: float) -> tuple[float, float]:
"""Return (L, R); the step emulates temperature/grid-interface variation."""
if time_s < 0.120:
return 4.0e-3, 0.50
return 6.0e-3, 0.80
def project(value: float, lower: float, upper: float) -> float:
return min(max(value, lower), upper)
def simulate(cfg: Config) -> dict[str, np.ndarray]:
n_steps = int(round(cfg.stop_time / cfg.sample_time)) + 1
time = np.arange(n_steps, dtype=float) * cfg.sample_time
current = np.zeros((n_steps, 2), dtype=float)
model_current = np.zeros((n_steps, 2), dtype=float)
error = np.zeros((n_steps, 2), dtype=float)
modulation = np.zeros((n_steps, 2), dtype=float)
gains = np.zeros((n_steps, 3), dtype=float)
saturated = np.zeros(n_steps, dtype=bool)
# [k_x, k_r, k_w]; u = k_x i + k_r r + k_w J i
gains[0, :] = np.array([-1.0, 1.5, -1.0])
gain_anchor = gains[0, :].copy()
lower = np.array([-5.0, 0.0, -5.0])
upper = np.array([2.0, 5.0, 2.0])
omega = 2.0 * math.pi * cfg.grid_frequency_hz
grid_voltage_dq = np.array([325.0, 0.0])
J = np.array([[0.0, 1.0], [-1.0, 0.0]])
for k in range(n_steps - 1):
t = time[k]
reference = reference_current(t)
inductance, resistance = plant_parameters(t)
a = resistance / inductance
b = 1.0 / inductance
# Reference model: dot(i_m) = -lambda i_m + lambda r
model_dot = (
-cfg.reference_bandwidth * model_current[k]
+ cfg.reference_bandwidth * reference
)
model_current[k + 1] = model_current[k] + cfg.sample_time * model_dot
error[k] = current[k] - model_current[k]
rotated_current = J @ current[k]
# Direct adaptive voltage increment u = v_converter - v_grid.
u_command = (
gains[k, 0] * current[k]
+ gains[k, 1] * reference
+ gains[k, 2] * rotated_current
)
converter_voltage_command = grid_voltage_dq + u_command
modulation_command = 2.0 * converter_voltage_command / cfg.dc_voltage
command_norm = float(np.linalg.norm(modulation_command))
if command_norm > cfg.modulation_limit:
modulation[k] = (
cfg.modulation_limit * modulation_command / command_norm
)
saturated[k] = True
else:
modulation[k] = modulation_command
converter_voltage_actual = 0.5 * cfg.dc_voltage * modulation[k]
u_actual = converter_voltage_actual - grid_voltage_dq
# Averaged dq plant:
# dot(i) = -(R/L)i + omega J i + (1/L)u_actual
current_dot = -a * current[k] + omega * rotated_current + b * u_actual
current[k + 1] = current[k] + cfg.sample_time * current_dot
regressor_energy = (
np.dot(current[k], current[k])
+ np.dot(reference, reference)
+ np.dot(rotated_current, rotated_current)
)
denominator = 1.0 + cfg.normalization_weight * regressor_energy
# Freeze the gradient term in saturation; retain leakage toward safe gains.
correlations = np.array(
[
np.dot(error[k], current[k]),
np.dot(error[k], reference),
np.dot(error[k], rotated_current),
]
)
gammas = np.array([cfg.gamma_x, cfg.gamma_r, cfg.gamma_w])
if saturated[k]:
gain_dot = -cfg.sigma * (gains[k] - gain_anchor)
else:
gain_dot = (
-gammas * correlations / denominator
- cfg.sigma * (gains[k] - gain_anchor)
)
candidate = gains[k] + cfg.sample_time * gain_dot
gains[k + 1, 0] = project(candidate[0], lower[0], upper[0])
gains[k + 1, 1] = project(candidate[1], lower[1], upper[1])
gains[k + 1, 2] = project(candidate[2], lower[2], upper[2])
error[-1] = current[-1] - model_current[-1]
modulation[-1] = modulation[-2]
saturated[-1] = saturated[-2]
return {
"time": time,
"current": current,
"model_current": model_current,
"error": error,
"modulation": modulation,
"gains": gains,
"saturated": saturated.astype(int),
}
def write_csv(results: dict[str, np.ndarray], output: Path) -> None:
output.parent.mkdir(parents=True, exist_ok=True)
with output.open("w", newline="", encoding="utf-8") as stream:
writer = csv.writer(stream)
writer.writerow(
[
"time_s",
"i_d_A",
"i_q_A",
"i_md_A",
"i_mq_A",
"e_d_A",
"e_q_A",
"k_x_ohm",
"k_r_ohm",
"k_w_ohm",
"m_d",
"m_q",
"saturated",
]
)
for k, t in enumerate(results["time"]):
writer.writerow(
[
t,
*results["current"][k],
*results["model_current"][k],
*results["error"][k],
*results["gains"][k],
*results["modulation"][k],
int(results["saturated"][k]),
]
)
def print_metrics(results: dict[str, np.ndarray], cfg: Config) -> None:
start = int(round(0.050 / cfg.sample_time))
rms = np.sqrt(np.mean(results["error"][start:] ** 2, axis=0))
final_gains = results["gains"][-1]
saturation_fraction = float(np.mean(results["saturated"]))
print(f"RMS tracking error after 50 ms: d={rms[0]:.4f} A, q={rms[1]:.4f} A")
print(
"Final gains: "
f"k_x={final_gains[0]:.4f}, "
f"k_r={final_gains[1]:.4f}, "
f"k_w={final_gains[2]:.4f} ohm"
)
print(f"Saturation fraction: {100.0 * saturation_fraction:.3f}%")
def plot_results(results: dict[str, np.ndarray]) -> None:
import matplotlib.pyplot as plt
time = results["time"]
current = results["current"]
model = results["model_current"]
gains = results["gains"]
plt.figure()
plt.plot(time, current[:, 0], label="i_d")
plt.plot(time, model[:, 0], "--", label="i_md")
plt.plot(time, current[:, 1], label="i_q")
plt.plot(time, model[:, 1], "--", label="i_mq")
plt.xlabel("Time (s)")
plt.ylabel("Current (A)")
plt.grid(True)
plt.legend()
plt.tight_layout()
plt.figure()
plt.plot(time, gains[:, 0], label="k_x")
plt.plot(time, gains[:, 1], label="k_r")
plt.plot(time, gains[:, 2], label="k_w")
plt.xlabel("Time (s)")
plt.ylabel("Adaptive gain (ohm)")
plt.grid(True)
plt.legend()
plt.tight_layout()
plt.show()
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument(
"--output",
type=Path,
default=Path("Chapter27_Lesson2_results.csv"),
help="CSV output path",
)
parser.add_argument("--plot", action="store_true")
args = parser.parse_args()
cfg = Config()
results = simulate(cfg)
write_csv(results, args.output)
print_metrics(results, cfg)
if args.plot:
plot_results(results)
if __name__ == "__main__":
main()
10. C++ Implementation
The C++17 implementation uses only the standard library and follows the same equations and constants as the Python program.
Chapter27_Lesson2.cpp
// Chapter27_Lesson2.cpp
// Direct adaptive dq-current control of an averaged voltage-source inverter.
// Build: g++ -std=c++17 -O2 Chapter27_Lesson2.cpp -o Chapter27_Lesson2
#include <algorithm>
#include <array>
#include <cmath>
#include <fstream>
#include <iomanip>
#include <iostream>
#include <stdexcept>
#include <string>
#include <vector>
struct Vec2 {
double d{};
double q{};
};
Vec2 operator+(const Vec2& a, const Vec2& b) { return {a.d + b.d, a.q + b.q}; }
Vec2 operator-(const Vec2& a, const Vec2& b) { return {a.d - b.d, a.q - b.q}; }
Vec2 operator*(double s, const Vec2& a) { return {s * a.d, s * a.q}; }
Vec2 operator*(const Vec2& a, double s) { return s * a; }
double dot(const Vec2& a, const Vec2& b) { return a.d * b.d + a.q * b.q; }
double norm(const Vec2& a) { return std::sqrt(dot(a, a)); }
Vec2 rotateJ(const Vec2& a) { return {a.q, -a.d}; }
double project(double value, double lower, double upper) {
return std::min(std::max(value, lower), upper);
}
Vec2 referenceCurrent(double time_s) {
return {
time_s >= 0.020 ? 8.0 : 0.0,
time_s >= 0.160 ? -4.0 : 0.0
};
}
std::pair<double, double> plantParameters(double time_s) {
if (time_s < 0.120) {
return {4.0e-3, 0.50}; // L, R
}
return {6.0e-3, 0.80};
}
int main(int argc, char* argv[]) {
const std::string output =
argc >= 2 ? argv[1] : "Chapter27_Lesson2_results_cpp.csv";
constexpr double Ts = 20.0e-6;
constexpr double stopTime = 0.30;
constexpr double omega = 2.0 * 3.14159265358979323846 * 50.0;
constexpr double dcVoltage = 800.0;
constexpr double modulationLimit = 0.95;
constexpr double lambda = 500.0;
constexpr double gammaX = 120.0;
constexpr double gammaR = 120.0;
constexpr double gammaW = 120.0;
constexpr double sigma = 0.20;
const int steps = static_cast<int>(std::llround(stopTime / Ts)) + 1;
const Vec2 gridVoltage{325.0, 0.0};
Vec2 current{};
Vec2 modelCurrent{};
std::array<double, 3> gains{-1.0, 1.5, -1.0};
const std::array<double, 3> anchor = gains;
const std::array<double, 3> lower{-5.0, 0.0, -5.0};
const std::array<double, 3> upper{2.0, 5.0, 2.0};
std::ofstream csv(output);
if (!csv) {
throw std::runtime_error("Cannot open output file: " + output);
}
csv << std::setprecision(12);
csv << "time_s,i_d_A,i_q_A,i_md_A,i_mq_A,e_d_A,e_q_A,"
"k_x_ohm,k_r_ohm,k_w_ohm,m_d,m_q,saturated\n";
double sumEd2 = 0.0;
double sumEq2 = 0.0;
int metricCount = 0;
int saturationCount = 0;
for (int k = 0; k < steps; ++k) {
const double time = k * Ts;
const Vec2 reference = referenceCurrent(time);
const auto [inductance, resistance] = plantParameters(time);
const double a = resistance / inductance;
const double b = 1.0 / inductance;
const Vec2 error = current - modelCurrent;
const Vec2 rotated = rotateJ(current);
const Vec2 uCommand =
gains[0] * current + gains[1] * reference + gains[2] * rotated;
const Vec2 voltageCommand = gridVoltage + uCommand;
const Vec2 modulationCommand = (2.0 / dcVoltage) * voltageCommand;
Vec2 modulation = modulationCommand;
bool saturated = false;
const double modulationNorm = norm(modulationCommand);
if (modulationNorm > modulationLimit) {
modulation = (modulationLimit / modulationNorm) * modulationCommand;
saturated = true;
++saturationCount;
}
csv << time << ',' << current.d << ',' << current.q << ','
<< modelCurrent.d << ',' << modelCurrent.q << ','
<< error.d << ',' << error.q << ','
<< gains[0] << ',' << gains[1] << ',' << gains[2] << ','
<< modulation.d << ',' << modulation.q << ','
<< (saturated ? 1 : 0) << '\n';
if (time >= 0.050) {
sumEd2 += error.d * error.d;
sumEq2 += error.q * error.q;
++metricCount;
}
if (k == steps - 1) {
break;
}
const Vec2 voltageActual = (0.5 * dcVoltage) * modulation;
const Vec2 uActual = voltageActual - gridVoltage;
const Vec2 modelDot =
(-lambda) * modelCurrent + lambda * reference;
const Vec2 currentDot =
(-a) * current + omega * rotated + b * uActual;
const double denominator =
1.0 + dot(current, current) + dot(reference, reference)
+ dot(rotated, rotated);
std::array<double, 3> gainDot{};
if (saturated) {
for (int j = 0; j < 3; ++j) {
gainDot[j] = -sigma * (gains[j] - anchor[j]);
}
} else {
const std::array<double, 3> correlation{
dot(error, current),
dot(error, reference),
dot(error, rotated)
};
const std::array<double, 3> gamma{gammaX, gammaR, gammaW};
for (int j = 0; j < 3; ++j) {
gainDot[j] =
-gamma[j] * correlation[j] / denominator
- sigma * (gains[j] - anchor[j]);
}
}
current = current + Ts * currentDot;
modelCurrent = modelCurrent + Ts * modelDot;
for (int j = 0; j < 3; ++j) {
gains[j] = project(gains[j] + Ts * gainDot[j], lower[j], upper[j]);
}
}
const double rmsD = std::sqrt(sumEd2 / std::max(metricCount, 1));
const double rmsQ = std::sqrt(sumEq2 / std::max(metricCount, 1));
std::cout << std::fixed << std::setprecision(4);
std::cout << "RMS tracking error after 50 ms: d=" << rmsD
<< " A, q=" << rmsQ << " A\n";
std::cout << "Final gains: k_x=" << gains[0]
<< ", k_r=" << gains[1]
<< ", k_w=" << gains[2] << " ohm\n";
std::cout << "Saturation fraction: "
<< 100.0 * saturationCount / steps << "%\n";
std::cout << "Wrote " << output << '\n';
return 0;
}
11. Java Implementation
The Java implementation uses immutable two-component records for vector arithmetic and writes an equivalent CSV result file.
Chapter27_Lesson2.java
// Chapter27_Lesson2.java
// Direct adaptive dq-current control of an averaged voltage-source inverter.
// Build: javac Chapter27_Lesson2.java
// Run: java Chapter27_Lesson2
import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Locale;
public final class Chapter27_Lesson2 {
private record Vec2(double d, double q) {
Vec2 add(Vec2 other) {
return new Vec2(d + other.d, q + other.q);
}
Vec2 subtract(Vec2 other) {
return new Vec2(d - other.d, q - other.q);
}
Vec2 scale(double scalar) {
return new Vec2(scalar * d, scalar * q);
}
double dot(Vec2 other) {
return d * other.d + q * other.q;
}
double norm() {
return Math.sqrt(dot(this));
}
Vec2 rotateJ() {
return new Vec2(q, -d);
}
}
private static Vec2 referenceCurrent(double time) {
return new Vec2(
time >= 0.020 ? 8.0 : 0.0,
time >= 0.160 ? -4.0 : 0.0
);
}
private static double[] plantParameters(double time) {
if (time < 0.120) {
return new double[] {4.0e-3, 0.50}; // L, R
}
return new double[] {6.0e-3, 0.80};
}
private static double project(double value, double lower, double upper) {
return Math.min(Math.max(value, lower), upper);
}
public static void main(String[] args) throws IOException {
Locale.setDefault(Locale.ROOT);
final String output = args.length >= 1
? args[0]
: "Chapter27_Lesson2_results_java.csv";
final double ts = 20.0e-6;
final double stopTime = 0.30;
final double omega = 2.0 * Math.PI * 50.0;
final double dcVoltage = 800.0;
final double modulationLimit = 0.95;
final double lambda = 500.0;
final double[] gamma = {120.0, 120.0, 120.0};
final double sigma = 0.20;
final int steps = (int) Math.round(stopTime / ts) + 1;
final Vec2 gridVoltage = new Vec2(325.0, 0.0);
Vec2 current = new Vec2(0.0, 0.0);
Vec2 modelCurrent = new Vec2(0.0, 0.0);
final double[] gains = {-1.0, 1.5, -1.0};
final double[] anchor = gains.clone();
final double[] lower = {-5.0, 0.0, -5.0};
final double[] upper = {2.0, 5.0, 2.0};
double sumEd2 = 0.0;
double sumEq2 = 0.0;
int metricCount = 0;
int saturationCount = 0;
try (PrintWriter csv = new PrintWriter(
new BufferedWriter(new FileWriter(output)))) {
csv.println(
"time_s,i_d_A,i_q_A,i_md_A,i_mq_A,e_d_A,e_q_A,"
+ "k_x_ohm,k_r_ohm,k_w_ohm,m_d,m_q,saturated"
);
for (int k = 0; k < steps; ++k) {
final double time = k * ts;
final Vec2 reference = referenceCurrent(time);
final double[] params = plantParameters(time);
final double inductance = params[0];
final double resistance = params[1];
final double a = resistance / inductance;
final double b = 1.0 / inductance;
final Vec2 error = current.subtract(modelCurrent);
final Vec2 rotated = current.rotateJ();
final Vec2 uCommand =
current.scale(gains[0])
.add(reference.scale(gains[1]))
.add(rotated.scale(gains[2]));
final Vec2 voltageCommand = gridVoltage.add(uCommand);
final Vec2 modulationCommand =
voltageCommand.scale(2.0 / dcVoltage);
Vec2 modulation = modulationCommand;
boolean saturated = false;
if (modulationCommand.norm() > modulationLimit) {
modulation = modulationCommand.scale(
modulationLimit / modulationCommand.norm()
);
saturated = true;
++saturationCount;
}
csv.printf(
Locale.ROOT,
"%.12g,%.12g,%.12g,%.12g,%.12g,%.12g,%.12g,"
+ "%.12g,%.12g,%.12g,%.12g,%.12g,%d%n",
time, current.d(), current.q(),
modelCurrent.d(), modelCurrent.q(),
error.d(), error.q(),
gains[0], gains[1], gains[2],
modulation.d(), modulation.q(),
saturated ? 1 : 0
);
if (time >= 0.050) {
sumEd2 += error.d() * error.d();
sumEq2 += error.q() * error.q();
++metricCount;
}
if (k == steps - 1) {
break;
}
final Vec2 voltageActual = modulation.scale(0.5 * dcVoltage);
final Vec2 uActual = voltageActual.subtract(gridVoltage);
final Vec2 modelDot =
modelCurrent.scale(-lambda).add(reference.scale(lambda));
final Vec2 currentDot =
current.scale(-a)
.add(rotated.scale(omega))
.add(uActual.scale(b));
final double denominator =
1.0 + current.dot(current) + reference.dot(reference)
+ rotated.dot(rotated);
final double[] correlation = {
error.dot(current),
error.dot(reference),
error.dot(rotated)
};
final double[] gainDot = new double[3];
for (int j = 0; j < 3; ++j) {
final double leakage = -sigma * (gains[j] - anchor[j]);
gainDot[j] = saturated
? leakage
: -gamma[j] * correlation[j] / denominator + leakage;
}
current = current.add(currentDot.scale(ts));
modelCurrent = modelCurrent.add(modelDot.scale(ts));
for (int j = 0; j < 3; ++j) {
gains[j] = project(
gains[j] + ts * gainDot[j], lower[j], upper[j]
);
}
}
}
final int count = Math.max(metricCount, 1);
final double rmsD = Math.sqrt(sumEd2 / count);
final double rmsQ = Math.sqrt(sumEq2 / count);
System.out.printf(
Locale.ROOT,
"RMS tracking error after 50 ms: d=%.4f A, q=%.4f A%n",
rmsD, rmsQ
);
System.out.printf(
Locale.ROOT,
"Final gains: k_x=%.4f, k_r=%.4f, k_w=%.4f ohm%n",
gains[0], gains[1], gains[2]
);
System.out.printf(
Locale.ROOT,
"Saturation fraction: %.3f%%%n",
100.0 * saturationCount / steps
);
System.out.println("Wrote " + output);
}
}
12. MATLAB and Simulink Implementations
The first file is a complete base-MATLAB simulation. The second file is code-generation-compatible controller logic intended for a Simulink MATLAB Function block. Connect measured \(dq\) currents and voltages to that block, place the averaged or switched converter plant outside it, and execute the block at the specified discrete sample time.
Chapter27_Lesson2.m
% Chapter27_Lesson2.m
% Direct adaptive dq-current control of an averaged voltage-source inverter.
% The script uses only base MATLAB functions.
clear; clc; close all;
Ts = 20e-6;
Tstop = 0.30;
t = (0:Ts:Tstop).';
N = numel(t);
omega = 2*pi*50;
Vdc = 800;
mMax = 0.95;
lambda = 500;
gamma = [120, 120, 120];
sigma = 0.20;
vg = [325; 0];
J = [0, 1; -1, 0];
i = zeros(2, N);
im = zeros(2, N);
e = zeros(2, N);
m = zeros(2, N);
gains = zeros(3, N);
gains(:,1) = [-1.0; 1.5; -1.0]; % [kx; kr; kw]
anchor = gains(:,1);
lower = [-5.0; 0.0; -5.0];
upper = [2.0; 5.0; 2.0];
saturated = false(N,1);
for k = 1:N-1
if t(k) < 0.020
r = [0; 0];
elseif t(k) < 0.160
r = [8; 0];
else
r = [8; -4];
end
if t(k) < 0.120
L = 4.0e-3;
R = 0.50;
else
L = 6.0e-3;
R = 0.80;
end
a = R/L;
b = 1/L;
e(:,k) = i(:,k) - im(:,k);
Ji = J*i(:,k);
uCommand = gains(1,k)*i(:,k) + gains(2,k)*r + gains(3,k)*Ji;
vCommand = vg + uCommand;
mCommand = 2*vCommand/Vdc;
if norm(mCommand) > mMax
m(:,k) = mMax*mCommand/norm(mCommand);
saturated(k) = true;
else
m(:,k) = mCommand;
end
vActual = 0.5*Vdc*m(:,k);
uActual = vActual - vg;
imDot = -lambda*im(:,k) + lambda*r;
iDot = -a*i(:,k) + omega*Ji + b*uActual;
denominator = 1 + dot(i(:,k),i(:,k)) + dot(r,r) + dot(Ji,Ji);
correlations = [dot(e(:,k),i(:,k));
dot(e(:,k),r);
dot(e(:,k),Ji)];
if saturated(k)
gainDot = -sigma*(gains(:,k) - anchor);
else
gainDot = -gamma(:).*correlations/denominator ...
- sigma*(gains(:,k) - anchor);
end
i(:,k+1) = i(:,k) + Ts*iDot;
im(:,k+1) = im(:,k) + Ts*imDot;
gains(:,k+1) = min(max(gains(:,k) + Ts*gainDot, lower), upper);
end
e(:,N) = i(:,N) - im(:,N);
m(:,N) = m(:,N-1);
saturated(N) = saturated(N-1);
metricStart = find(t >= 0.050, 1, 'first');
rmsError = sqrt(mean(e(:,metricStart:end).^2, 2));
fprintf('RMS tracking error after 50 ms: d=%.4f A, q=%.4f A\n', ...
rmsError(1), rmsError(2));
fprintf('Final gains: kx=%.4f, kr=%.4f, kw=%.4f ohm\n', ...
gains(1,end), gains(2,end), gains(3,end));
fprintf('Saturation fraction: %.3f%%\n', 100*mean(saturated));
resultTable = table(t, i(1,:).', i(2,:).', im(1,:).', im(2,:).', ...
e(1,:).', e(2,:).', gains(1,:).', gains(2,:).', gains(3,:).', ...
m(1,:).', m(2,:).', saturated, ...
'VariableNames', {'time_s','i_d_A','i_q_A','i_md_A','i_mq_A', ...
'e_d_A','e_q_A','k_x_ohm','k_r_ohm','k_w_ohm','m_d','m_q', ...
'saturated'});
writetable(resultTable, 'Chapter27_Lesson2_results_matlab.csv');
figure;
plot(t, i(1,:), t, im(1,:),'--', t, i(2,:), t, im(2,:),'--', ...
'LineWidth', 1.1);
grid on;
xlabel('Time (s)');
ylabel('Current (A)');
legend('i_d','i_{md}','i_q','i_{mq}','Location','best');
title('Adaptive inverter current tracking');
figure;
plot(t, gains(1,:), t, gains(2,:), t, gains(3,:), 'LineWidth', 1.1);
grid on;
xlabel('Time (s)');
ylabel('Adaptive gain (\Omega)');
legend('k_x','k_r','k_\omega','Location','best');
title('Direct adaptive gains');
Chapter27_Lesson2_Simulink.m
function [md, mq, imd, imq, kx, kr, kw, sat] = ...
Chapter27_Lesson2_Simulink(id, iq, vgd, vgq, idRef, iqRef, Vdc, reset)
%#codegen
% Chapter27_Lesson2_Simulink.m
% MATLAB Function-block implementation of the adaptive controller.
%
% Inputs: measured dq currents, grid dq voltage, dq current references,
% DC-link voltage, and a Boolean reset.
% Outputs: dq modulation command, reference-model states, gains, saturation.
%
% Suggested Simulink rate: discrete sample time Ts = 20 microseconds.
persistent im gains
Ts = 20e-6;
lambda = 500;
gamma = [120; 120; 120];
sigma = 0.20;
mMax = 0.95;
J = [0, 1; -1, 0];
anchor = [-1.0; 1.5; -1.0];
lower = [-5.0; 0.0; -5.0];
upper = [2.0; 5.0; 2.0];
if isempty(im) || reset
im = [0; 0];
gains = anchor;
end
current = [id; iq];
reference = [idRef; iqRef];
gridVoltage = [vgd; vgq];
error = current - im;
rotated = J*current;
uCommand = gains(1)*current + gains(2)*reference + gains(3)*rotated;
vCommand = gridVoltage + uCommand;
mCommand = 2*vCommand/max(Vdc, 1.0);
magnitude = sqrt(mCommand(1)^2 + mCommand(2)^2);
if magnitude > mMax
modulation = mMax*mCommand/magnitude;
sat = true;
else
modulation = mCommand;
sat = false;
end
denominator = 1 + current.'*current + reference.'*reference ...
+ rotated.'*rotated;
correlations = [error.'*current;
error.'*reference;
error.'*rotated];
if sat
gainDot = -sigma*(gains - anchor);
else
gainDot = -gamma.*correlations/denominator - sigma*(gains - anchor);
end
modelDot = -lambda*im + lambda*reference;
im = im + Ts*modelDot;
gains = min(max(gains + Ts*gainDot, lower), upper);
md = modulation(1);
mq = modulation(2);
imd = im(1);
imq = im(2);
kx = gains(1);
kr = gains(2);
kw = gains(3);
end
13. Wolfram Mathematica Implementation
The downloadable package contains both a readable Wolfram Language source file and a notebook containing the same simulation.
Chapter27_Lesson2.nb (source also supplied as
Chapter27_Lesson2.wl)
(* Chapter27_Lesson2.wl
Direct adaptive dq-current control of an averaged voltage-source inverter. *)
ClearAll["Global`*"];
ts = 20.*^-6;
stopTime = 0.30;
steps = Round[stopTime/ts] + 1;
omega = 2. Pi 50.;
vdc = 800.;
mMax = 0.95;
lambda = 500.;
gamma = {120., 120., 120.};
sigma = 0.20;
gridVoltage = {325., 0.};
jMatrix = {{0., 1.}, {-1., 0.}};
anchor = {-1., 1.5, -1.};
lower = {-5., 0., -5.};
upper = {2., 5., 2.};
referenceCurrent[t_] := Which[
t < 0.020, {0., 0.},
t < 0.160, {8., 0.},
True, {8., -4.}
];
plantParameters[t_] := If[t < 0.120, {0.004, 0.50}, {0.006, 0.80}];
projectVector[x_] := MapThread[Clip[#1, {#2, #3}] &, {x, lower, upper}];
current = ConstantArray[{0., 0.}, steps];
modelCurrent = ConstantArray[{0., 0.}, steps];
error = ConstantArray[{0., 0.}, steps];
modulation = ConstantArray[{0., 0.}, steps];
gains = ConstantArray[{0., 0., 0.}, steps];
saturated = ConstantArray[0, steps];
gains[[1]] = anchor;
Do[
time = (k - 1) ts;
reference = referenceCurrent[time];
{inductance, resistance} = plantParameters[time];
a = resistance/inductance;
b = 1./inductance;
error[[k]] = current[[k]] - modelCurrent[[k]];
rotated = jMatrix.current[[k]];
uCommand = gains[[k, 1]] current[[k]] +
gains[[k, 2]] reference +
gains[[k, 3]] rotated;
voltageCommand = gridVoltage + uCommand;
modulationCommand = 2. voltageCommand/vdc;
If[Norm[modulationCommand] > mMax,
modulation[[k]] = mMax modulationCommand/Norm[modulationCommand];
saturated[[k]] = 1,
modulation[[k]] = modulationCommand
];
voltageActual = 0.5 vdc modulation[[k]];
uActual = voltageActual - gridVoltage;
modelDot = -lambda modelCurrent[[k]] + lambda reference;
currentDot = -a current[[k]] + omega rotated + b uActual;
denominator = 1. + current[[k]].current[[k]] +
reference.reference + rotated.rotated;
correlations = {
error[[k]].current[[k]],
error[[k]].reference,
error[[k]].rotated
};
gainDot = If[saturated[[k]] == 1,
-sigma (gains[[k]] - anchor),
-gamma correlations/denominator - sigma (gains[[k]] - anchor)
];
current[[k + 1]] = current[[k]] + ts currentDot;
modelCurrent[[k + 1]] = modelCurrent[[k]] + ts modelDot;
gains[[k + 1]] = projectVector[gains[[k]] + ts gainDot],
{k, 1, steps - 1}
];
error[[-1]] = current[[-1]] - modelCurrent[[-1]];
modulation[[-1]] = modulation[[-2]];
saturated[[-1]] = saturated[[-2]];
times = N@Range[0, steps - 1] ts;
startIndex = First@FirstPosition[times, x_ /; x >= 0.050];
rmsError = Sqrt[Mean /@ Transpose[error[[startIndex ;;]]^2]];
Print["RMS tracking error after 50 ms: d=", NumberForm[rmsError[[1]], {6, 4}],
" A, q=", NumberForm[rmsError[[2]], {6, 4}], " A"];
Print["Final gains {kx, kr, kw} = ", N[gains[[-1]], 6], " ohm"];
Print["Saturation fraction = ", 100. Mean[saturated], "%"];
rows = MapThread[
Join[{#1}, #2, #3, #4, #5, #6, {#7}] &,
{times, current, modelCurrent, error, gains, modulation, saturated}
];
header = {"time_s", "i_d_A", "i_q_A", "i_md_A", "i_mq_A",
"e_d_A", "e_q_A", "k_x_ohm", "k_r_ohm", "k_w_ohm",
"m_d", "m_q", "saturated"};
Export["Chapter27_Lesson2_results_wolfram.csv", Prepend[rows, header]];
currentPlot = ListLinePlot[
{
Transpose[{times, current[[All, 1]]}],
Transpose[{times, modelCurrent[[All, 1]]}],
Transpose[{times, current[[All, 2]]}],
Transpose[{times, modelCurrent[[All, 2]]}]
},
PlotLegends -> {"i_d", "i_md", "i_q", "i_mq"},
AxesLabel -> {"Time (s)", "Current (A)"},
GridLines -> Automatic,
ImageSize -> Large
];
gainPlot = ListLinePlot[
Table[Transpose[{times, gains[[All, j]]}], {j, 1, 3}],
PlotLegends -> {"k_x", "k_r", "k_w"},
AxesLabel -> {"Time (s)", "Gain (ohm)"},
GridLines -> Automatic,
ImageSize -> Large
];
Column[{currentPlot, gainPlot}]
14. Extensions, Limitations, and Engineering Checks
14.1 Extension to Matrix Gains
If the two axes are not symmetric, replace scalar gains by matrices:
\[ \mathbf{u}= \hat{\mathbf{K}}_x\mathbf{i} +\hat{\mathbf{K}}_r\mathbf{r} +\hat{\mathbf{K}}_\omega\mathbf{J}\mathbf{i}. \]
The adaptation laws then use outer products such as \(\dot{\hat{\mathbf{K}}}_x=-\mathbf{\Gamma}_x \mathbf{e}\mathbf{i}^T\). Matrix adaptation offers more freedom but increases the number of parameters and the excitation needed for convergence.
14.2 LCL Filters
An \(LCL\) filter adds capacitor voltage and a second inductor current, creating a resonant mode. Treating that plant as the first-order model (1) can cause the adaptive loop to excite unmodeled dynamics. A valid design must include the higher-order model, active or passive damping, and a reference-model bandwidth comfortably below the relevant resonance and delay limits.
14.3 PLL and Weak-Grid Interaction
In a grid-following inverter, the synchronous frame is generated by a phase-locked loop. Angle and frequency errors modify the apparent cross-coupling and inject disturbances into the current loop. In weak grids, the PLL, current controller, grid impedance, and outer loops form a coupled multivariable system; stability cannot be certified solely by the inner-loop proof in Section 6.
14.4 Hardware Safety
- Apply independent cycle-by-cycle overcurrent protection.
- Enforce duty-ratio, current-reference, gain, and rate limits.
- Include dead time, ADC scaling, PWM delay, and sensor offsets in validation.
- Begin with controller-hardware-in-the-loop or low-voltage testing.
- Disable or reset adaptation after detected faults or invalid measurements.
15. Problems and Solutions
Problem 1 — Derivation of the synchronous-frame model. Starting from \(L\dot{\mathbf{i}}_{\alpha\beta} =-R\mathbf{i}_{\alpha\beta}+ \mathbf{v}_{c,\alpha\beta}-\mathbf{v}_{g,\alpha\beta}\), show why a cross-coupling term appears after transformation to a frame rotating at angular speed \(\omega\).
Solution. Let \(\mathbf{i}_{dq}=\mathbf{T}(\theta) \mathbf{i}_{\alpha\beta}\), with \(\dot\theta=\omega\). Differentiation gives
\[ \dot{\mathbf{i}}_{dq} = \dot{\mathbf{T}}\mathbf{i}_{\alpha\beta} +\mathbf{T}\dot{\mathbf{i}}_{\alpha\beta}. \]
For the convention used in this lesson, \(\dot{\mathbf{T}}\mathbf{T}^T =\omega\mathbf{J}\). Substitution yields
\[ L\dot{\mathbf{i}}_{dq} = -R\mathbf{i}_{dq} +\omega L\mathbf{J}\mathbf{i}_{dq} +\mathbf{v}_{c,dq}-\mathbf{v}_{g,dq}. \]
The cross-coupling is therefore a kinematic consequence of differentiating coordinates that rotate with time.
Problem 2 — Matching and ideal gains. Derive the ideal gains for the plant (1), controller (3), and reference model (2).
Solution. Closed-loop substitution gives
\[ \dot{\mathbf{i}} = \left(-a+b\hat{k}_x\right)\mathbf{i} +b\hat{k}_r\mathbf{r} +\left(\omega+b\hat{k}_\omega\right) \mathbf{J}\mathbf{i}. \]
Equating coefficients with \(-\lambda\mathbf{i}+\lambda\mathbf{r}\) gives \(k_x^\star=(a-\lambda)/b\), \(k_r^\star=\lambda/b\), and \(k_\omega^\star=-\omega/b\). Replacing \(a=R/L\) and \(b=1/L\) gives (4).
Problem 3 — Complete the Lyapunov proof. Show explicitly that every gain-error cross term cancels under (7).
Solution. The term associated with \(\tilde k_x\) in \(\dot V\) is
\[ b\tilde{k}_x\mathbf{e}^T\mathbf{i} +\frac{b}{\gamma_x}\tilde{k}_x \left(-\gamma_x\mathbf{e}^T\mathbf{i}\right)=0. \]
The same calculation applies to the reference and cross-coupling regressors. Only \(-\lambda\|\mathbf{e}\|^2\) remains, proving (8). Positive definiteness of (6) uses \(b>0\) and positive adaptation gains.
Problem 4 — Available converter voltage. For \(V_{\mathrm{dc}}=800\,\mathrm{V}\) and \(m_{\max}=0.95\), compute the maximum converter voltage-vector magnitude. If \(\mathbf{v}_g=[325\;0]^T\,\mathrm{V}\), find (a) the radial positive \(d\)-axis voltage increment and (b) the largest pure \(q\)-axis increment that remains feasible when the \(d\) component stays at 325 V.
Solution.
\[ \|\mathbf{v}_c\|_{\max} =\frac{V_{\mathrm{dc}}}{2}m_{\max} =380\,\mathrm{V}. \]
The radial positive \(d\)-axis headroom is \(380-325=55\,\mathrm{V}\). For a pure \(q\)-axis increment,
\[ u_{q,\max}=\sqrt{380^2-325^2} \approx196.9\,\mathrm{V}. \]
Thus the available control voltage depends strongly on the direction of the requested voltage vector.
Problem 5 — Parameter-change calculation. Compute ideal gains for \(L=4\,\mathrm{mH},R=0.5\,\Omega\) and for \(L=6\,\mathrm{mH},R=0.8\,\Omega\), using \(\lambda=500\,\mathrm{s}^{-1}\) and \(\omega=2\pi50\,\mathrm{rad/s}\).
Solution. Use (4). Before the change,
\[ k_x^\star=0.5-500(0.004)=-1.5\,\Omega, \quad k_r^\star=500(0.004)=2.0\,\Omega, \quad k_\omega^\star=-2\pi50(0.004)\approx-1.257\,\Omega. \]
After the change,
\[ k_x^\star=0.8-500(0.006)=-2.2\,\Omega, \quad k_r^\star=500(0.006)=3.0\,\Omega, \quad k_\omega^\star=-2\pi50(0.006)\approx-1.885\,\Omega. \]
Problem 6 — Tracking versus parameter convergence. Explain why constant \(i_d^\star\) and \(i_q^\star\) commands may produce zero tracking error without convergence of all three gains.
Solution. At steady state, the regressors can become constant and linearly dependent. The adaptive law then receives insufficient independent information to distinguish all gain errors. The closed loop can reach a manifold of gain combinations that generates the required steady voltage. Persistent excitation is needed to collapse this manifold to the unique ideal gain vector. Tracking and identification are therefore distinct objectives.
16. Summary
A grid-connected voltage-source inverter with an \(L\) interface can be represented by a first-order vector plant in the synchronous frame. Direct adaptation of three controller gains gives exact model matching without explicitly estimating resistance or inductance. A quadratic Lyapunov function proves boundedness and asymptotic current tracking for the nominal averaged model. Practical implementation requires normalization, projection, leakage, current limiting, voltage-vector limiting, and saturation-aware adaptation. These safeguards improve robustness but change exact convergence into an ultimate-boundedness problem. Higher-order filters, PLL interaction, switching delay, and weak-grid dynamics require an expanded model and a separate stability analysis.
17. References
- Sanders, S.R., Noworolski, J.M., Liu, X.Z., & Verghese, G.C. (1991). Generalized averaging method for power conversion circuits. IEEE Transactions on Power Electronics, 6(2), 251–259. doi:10.1109/63.76811.
- Harnefors, L. (2007). Modeling of three-phase dynamic systems using complex transfer functions and transfer matrices. IEEE Transactions on Industrial Electronics, 54(4), 2239–2248. doi:10.1109/TIE.2007.894769.
- Kazmierkowski, M.P., & Malesani, L. (1998). Current control techniques for three-phase voltage-source PWM converters: A survey. IEEE Transactions on Industrial Electronics, 45(5), 691–703. doi:10.1109/41.720325.
- Åström, K.J., & Wittenmark, B. (1973). On self-tuning regulators. Automatica, 9(2), 185–199. doi:10.1016/0005-1098(73)90073-3.
- Åström, K.J., Borisson, U., Ljung, L., & Wittenmark, B. (1977). Theory and applications of self-tuning regulators. Automatica, 13(5), 457–476. doi:10.1016/0005-1098(77)90067-X.
- Ioannou, P.A., & Kokotovic, P.V. (1984). Instability analysis and improvement of robustness of adaptive control. Automatica, 20(5), 583–594. doi:10.1016/0005-1098(84)90009-8.
- Narendra, K.S., & Annaswamy, A.M. (1987). A new adaptive law for robust adaptation without persistent excitation. IEEE Transactions on Automatic Control, 32(2), 134–145. doi:10.1109/TAC.1987.1104543.
- Ortega, R., & Tang, Y. (1989). Robustness of adaptive controllers—A survey. Automatica, 25(5), 651–677. doi:10.1016/0005-1098(89)90023-X.
- Pomet, J.-B., & Praly, L. (1992). Adaptive nonlinear regulation: Estimation from the Lyapunov equation. IEEE Transactions on Automatic Control, 37(6), 729–740. doi:10.1109/9.256328.
- Liu, Y., & Chen, Z. (2007). Adaptive control design for VSC-HVDC systems based on backstepping method. Electric Power Systems Research, 77(5–6), 559–565. doi:10.1016/j.epsr.2006.05.006.
Help keep these engineering tutorials free and growing
If these lessons, examples, and project pages help you, a small donation supports the continued creation and improvement of free control, robotics, software, and engineering education resources.
Created and maintained by Abolfazl Mohammadijoo.