codextracking-reference-errors-responses.py
tracking-reference-errors-responses.py
"""Exact synthetic first-order P/PI step and ramp responses; no lab data.
Plant: tau*y' + y = K*u, with tau=2 s and K=1 degree/percentage point.
Controller: u=kp*(r-y) [+ ki*z], z'=r-y, kp=1 pp/degree,
ki=0.5 pp/(degree*s). Initial y=z=0; exact, unbiased measurement.
Tested with Python 3.14.4, NumPy 2.3.5, and Matplotlib 3.10.7.
Run from _media; the PNG is written to the current working directory.
"""
from pathlib import Path
import matplotlib.pyplot as plt
import numpy as np
def main():
plt.rcParams.update({"font.size": 11, "font.family": "DejaVu Sans"})
t = np.linspace(0, 12, 601)
decay_p, decay_pi = np.exp(-t), np.exp(-t / 2)
step_r = np.ones_like(t)
step_p = 0.5 * (1 - decay_p)
step_pi = 1 - decay_pi
ramp_r = 0.2 * t
ramp_p = 0.1 * (t - 1 + decay_p)
ramp_pi = 0.2 * (t - 2 + 2 * decay_pi)
rows = [
(step_r, step_p, step_pi, step_r - step_p, np.ones_like(t)),
(ramp_r, ramp_p, ramp_pi, ramp_r - ramp_p, 0.2 * t),
]
fig, axes = plt.subplots(2, 3, figsize=(11.5, 6.8), dpi=110, facecolor="white")
blue, orange = "#2166ac", "#b35806"
for row, (ref, yp, ypi, up, upi) in enumerate(rows):
ax_y, ax_e, ax_u = axes[row]
ax_y.plot(t, ref, "--", color="#303030", lw=1.8, label="Reference r")
for ax, pval, pival in [(ax_y, yp, ypi), (ax_e, ref - yp, ref - ypi),
(ax_u, up, upi)]:
ax.plot(t, pval, color=blue, lw=2.1, label="P control")
ax.plot(t, pival, color=orange, lw=2.1, label="PI control")
ax.set(xlim=(0, 12), xlabel="Time (s)")
ax.grid(alpha=0.2)
ax.spines[["top", "right"]].set_visible(False)
ax_y.set_ylabel("Output rise y (°C)")
ax_e.set_ylabel("Signed error r − y (°C)")
ax_u.set_ylabel("Heater command u (pp)")
ax_y.set_title("Unit step" if row == 0 else "Ramp: 0.2 °C/s", weight="bold")
ax_e.set_title("Tracking error")
ax_u.set_title("Control input")
if row == 0:
ax_y.legend(loc="lower right", frameon=False, fontsize=10)
ax_e.axhline(0.5, color=blue, ls=":", lw=1)
ax_e.text(5.5, 0.56, "P limit: 0.5 °C", color=blue)
ax_e.text(5.5, 0.1, "PI limit: 0 °C", color=orange)
ax_e.set_ylim(-0.03, 1.1)
else:
ax_e.axhline(0.4, color=orange, ls=":", lw=1)
ax_e.text(4.7, 0.28, "PI limit: 0.4 °C", color=orange)
ax_e.text(3.5, 1.2, "P error keeps growing", color=blue)
ax_e.set_ylim(-0.03, 1.45)
fig.suptitle("Synthetic model: 2 dy/dt + y = u; P: u = e; PI: u = e + 0.5 z, z′ = e",
fontsize=13, weight="bold", y=0.98)
fig.text(0.5, 0.025, "Analytical illustrations, not measurements. y(0) = z(0) = 0; no bias, disturbance, delay, or limits.\n"
"pp = heater-command percentage points; numerical time is in seconds.",
ha="center", va="center", fontsize=10, color="#444444")
fig.tight_layout(rect=(0, 0.065, 1, 0.945), h_pad=2, w_pad=2)
fig.savefig(Path(__file__).with_suffix(".png").name, facecolor="white", transparent=False)
plt.close(fig)
if __name__ == "__main__":
main()