Files
2026-05-18 19:02:23 +08:00

330 lines
12 KiB
Python

"""HPI Control Validation — Inverted Pendulum (NN edition)
Runs HPI algorithm (NN-based), compares with LQR baseline, validates in
closed loop, outputs trajectories to CSV, and plots results.
"""
import os
import numpy as np
import pandas as pd
import matplotlib
matplotlib.use("TkAgg")
import matplotlib.pyplot as plt
from scipy.integrate import solve_ivp
from scipy.linalg import solve_continuous_are
from hpi.hpi_controller import HPIController
plt.rcParams.update({
"figure.figsize": (12, 8),
"font.size": 12,
"axes.titlesize": 14,
"axes.labelsize": 13,
"legend.fontsize": 11,
"figure.dpi": 120,
})
OUTPUT_DIR = "output"
os.makedirs(OUTPUT_DIR, exist_ok=True)
# ═══════════════════════════════════════════════════════════════════
# LQR Baseline
# ═══════════════════════════════════════════════════════════════════
def compute_lqr():
"""Compute LQR solution for linearized inverted pendulum."""
A = np.array([[0., 1.], [1., 0.]])
B = np.array([[0.], [1.]])
Q = np.eye(2)
R = np.array([[1.]])
P = solve_continuous_are(A, B, Q, R)
K = (np.linalg.solve(R, B.T @ P)).ravel()
return K
# ═══════════════════════════════════════════════════════════════════
# HPI
# ═══════════════════════════════════════════════════════════════════
def run_hpi():
"""Run HPI algorithm, return controller and results."""
print("=" * 60)
print(" HPI Adaptive Optimal Control — Inverted Pendulum (NN)")
print("=" * 60)
controller = HPIController(
J=1.0, mgl=1.0, R=1.0,
pe_amplitude=0.5,
lr=1e-3, epochs=500, lambda_weight=1e-6,
)
results = controller.run(
x0=np.array([0.5, 0.0]),
T=5.0,
dt=0.05,
L_start=0.1,
L_step=1.0,
L_max=10.0,
gamma=0.1,
epsilon=1e-3,
)
print(f"\n Phase 1 L0 = {results['phase1_L0']:.2f}")
print(f" Phase 1 info: {results['phase1_info']}")
print(f" Phase 2 iterations: {len(results['phase2_history'])}")
print(f" Phase 3 converged: {results['phase3_converged']}")
print(f" Phase 3 iterations: {len(results['phase3_history'])}")
return controller, results
# ═══════════════════════════════════════════════════════════════════
# Simulation
# ═══════════════════════════════════════════════════════════════════
def simulate_closed_loop(controller, x0, T=5.0, n_points=500):
"""Closed-loop simulation with HPI controller.
Returns: t, X, U, V
"""
t_eval = np.linspace(0, T, n_points)
def control(t, x):
return controller.trainer.compute_u_hat(x)
def ode(t, x):
return controller.collector.dynamics(t, x, control)
sol = solve_ivp(ode, (0, T), x0, method="RK45", t_eval=t_eval,
rtol=1e-8, atol=1e-10)
t = sol.t
X = sol.y.T
U = np.array([control(ti, xi) for ti, xi in zip(t, X)])
V = controller.trainer.compute_V(X)
return t, X, U, V
def simulate_lqr_closed_loop(J, mgl, K, x0, T=5.0, n_points=500):
"""Closed-loop simulation with LQR controller on nonlinear pendulum."""
t_eval = np.linspace(0, T, n_points)
def u_lqr(t, x):
return float(-np.dot(K, x))
def dynamics(t, x):
return np.array([x[1], (mgl / J) * np.sin(x[0]) + (1.0 / J) * u_lqr(t, x)])
sol = solve_ivp(dynamics, (0, T), x0, method="RK45", t_eval=t_eval,
rtol=1e-8, atol=1e-10)
t = sol.t
X = sol.y.T
U = np.array([u_lqr(ti, xi) for ti, xi in zip(t, X)])
return t, X, U
# ═══════════════════════════════════════════════════════════════════
# I/O
# ═══════════════════════════════════════════════════════════════════
def save_csv(t, X, U, V, label):
df = pd.DataFrame({"time": t, "x1": X[:, 0], "x2": X[:, 1], "u": U, "V": V})
path = os.path.join(OUTPUT_DIR, f"trajectory_{label}.csv")
df.to_csv(path, index=False)
return path
# ═══════════════════════════════════════════════════════════════════
# Plotting
# ═══════════════════════════════════════════════════════════════════
def plot_results(all_data, controller, results):
"""Plot state/control/value trajectories, phase portrait, convergence."""
colors = ["#1f77b4", "#d62728", "#2ca02c", "#ff7f0e", "#9467bd", "#8c564b"]
# ── Figure 1: State + Control + Value ──
fig1, axes1 = plt.subplots(2, 2, figsize=(14, 10))
for idx, (label, (t, X, U, V)) in enumerate(all_data.items()):
c = colors[idx % len(colors)]
axes1[0, 0].plot(t, X[:, 0], color=c, label=f"{label} (x0=[{X[0,0]:.1f},{X[0,1]:.1f}])")
axes1[0, 1].plot(t, X[:, 1], color=c)
axes1[1, 0].plot(t, U, color=c)
axes1[1, 1].plot(t, V, color=c)
axes1[0, 0].set_ylabel("$x_1$ (angle)")
axes1[0, 0].set_title("State $x_1$")
axes1[0, 0].legend(fontsize=9)
axes1[0, 0].grid(True, alpha=0.3)
axes1[0, 0].axhline(y=0, color="k", lw=0.5)
axes1[0, 1].set_ylabel("$x_2$ (ang. velocity)")
axes1[0, 1].set_title("State $x_2$")
axes1[0, 1].grid(True, alpha=0.3)
axes1[0, 1].axhline(y=0, color="k", lw=0.5)
axes1[1, 0].set_xlabel("Time t (s)")
axes1[1, 0].set_ylabel("$u$ (torque)")
axes1[1, 0].set_title("Control Input")
axes1[1, 0].grid(True, alpha=0.3)
axes1[1, 1].set_xlabel("Time t (s)")
axes1[1, 1].set_ylabel("$V(x)$")
axes1[1, 1].set_title("Value Function")
axes1[1, 1].grid(True, alpha=0.3)
fig1.suptitle("HPI Optimal Control — Closed-Loop Validation", fontsize=15, fontweight="bold")
plt.tight_layout()
path1 = os.path.join(OUTPUT_DIR, "trajectories.png")
fig1.savefig(path1, dpi=150, bbox_inches="tight")
print(f" Plot saved: {path1}")
# ── Figure 2: Phase Portrait ──
fig2, ax2 = plt.subplots(figsize=(8, 8))
for idx, (label, (t, X, U, V)) in enumerate(all_data.items()):
c = colors[idx % len(colors)]
ax2.plot(X[:, 0], X[:, 1], color=c, lw=1.5, label=label)
ax2.scatter(X[0, 0], X[0, 1], color=c, s=80, marker="o", zorder=5)
ax2.scatter(X[-1, 0], X[-1, 1], color=c, s=80, marker="x", zorder=5)
ax2.set_xlabel("$x_1$ (angle)")
ax2.set_ylabel("$x_2$ (angular velocity)")
ax2.set_title("Phase Portrait")
ax2.legend()
ax2.grid(True, alpha=0.3)
ax2.axhline(y=0, color="k", lw=0.5)
ax2.axvline(x=0, color="k", lw=0.5)
ax2.set_aspect("equal")
path2 = os.path.join(OUTPUT_DIR, "phase_portrait.png")
fig2.savefig(path2, dpi=150, bbox_inches="tight")
print(f" Plot saved: {path2}")
# ── Figure 3: Convergence ──
fig3, axes3 = plt.subplots(1, 2, figsize=(14, 5))
L_vals = [h[0] for h in results["phase2_history"]]
axes3[0].plot(range(len(L_vals)), L_vals, "o-", color="#1f77b4", markersize=6)
axes3[0].set_xlabel("Iteration")
axes3[0].set_ylabel("L")
axes3[0].set_title("Phase 2: Homotopy Contraction")
axes3[0].grid(True, alpha=0.3)
loss_history = results["phase3_history"]
if len(loss_history) > 1:
loss_diffs = [abs(loss_history[i] - loss_history[i - 1])
for i in range(1, len(loss_history))]
axes3[1].plot(range(len(loss_diffs)), loss_diffs, "o-", color="#d62728", markersize=6)
axes3[1].set_yscale("log")
axes3[1].set_xlabel("Iteration")
axes3[1].set_ylabel("|loss change|")
axes3[1].set_title("Phase 3: Loss Convergence (log)")
axes3[1].grid(True, alpha=0.3)
fig3.suptitle("HPI Convergence History", fontsize=14, fontweight="bold")
plt.tight_layout()
path3 = os.path.join(OUTPUT_DIR, "convergence.png")
fig3.savefig(path3, dpi=150, bbox_inches="tight")
print(f" Plot saved: {path3}")
# ── Figure 4: Value function surface ──
fig4, ax4 = plt.subplots(figsize=(8, 6))
x1_g = np.linspace(-1.2, 1.2, 80)
x2_g = np.linspace(-2.5, 2.5, 80)
X1, X2 = np.meshgrid(x1_g, x2_g)
Xf = np.column_stack([X1.ravel(), X2.ravel()])
Vg = controller.trainer.compute_V(Xf).reshape(80, 80)
vmin, vmax = max(Vg.min(), 1e-6), Vg.max()
if vmax > vmin:
levels = np.logspace(np.log10(vmin), np.log10(vmax), 20)
cs = ax4.contourf(X1, X2, Vg, levels=levels, cmap="RdYlBu_r")
ax4.contour(X1, X2, Vg, levels=levels[:8], colors="k", linewidths=0.3, alpha=0.5)
fig4.colorbar(cs, ax=ax4, label="$V(x)$")
for idx, (label, (t, X, U, V)) in enumerate(all_data.items()):
ax4.plot(X[:, 0], X[:, 1], color=colors[idx % len(colors)], lw=1.5, label=label)
ax4.set_xlabel("$x_1$ (angle)")
ax4.set_ylabel("$x_2$ (ang. velocity)")
ax4.set_title("Value Function with Phase Trajectories")
ax4.legend(loc="upper left", fontsize=9)
path4 = os.path.join(OUTPUT_DIR, "value_function.png")
fig4.savefig(path4, dpi=150, bbox_inches="tight")
print(f" Plot saved: {path4}")
# ═══════════════════════════════════════════════════════════════════
# Main
# ═══════════════════════════════════════════════════════════════════
def main():
# ── LQR baseline ──
print("=" * 60)
print(" LQR Baseline (linearized pendulum)")
print("=" * 60)
K = compute_lqr()
print(f" LQR gain K = {K}")
# ── HPI ──
controller, results = run_hpi()
# ── Validation ──
print("\n" + "=" * 60)
print(" Closed-Loop Validation")
print("=" * 60)
test_cases = [
("IC1_small_angle", np.array([0.5, 0.0])),
("IC2_large_angle", np.array([0.9, 0.1])),
("IC3_neg_angle", np.array([-0.7, -0.2])),
("IC4_pos_vel", np.array([0.2, 1.5])),
("IC5_neg_vel", np.array([-0.3, -1.2])),
]
all_data = {}
summary = []
for label, x0 in test_cases:
# HPI
t_h, X_h, U_h, V_h = simulate_closed_loop(controller, x0)
n_hpi = np.linalg.norm(X_h[-1])
# LQR
t_l, X_l, U_l = simulate_lqr_closed_loop(controller.J, controller.mgl, K, x0)
n_lqr = np.linalg.norm(X_l[-1])
all_data[f"HPI_{label}"] = (t_h, X_h, U_h, V_h)
save_csv(t_h, X_h, U_h, V_h, f"hpi_{label}")
print(f" {label:20s} HPI: ||x(T)||={n_hpi:.2e} | LQR: ||x(T)||={n_lqr:.2e}")
summary.append({
"test_case": label, "x1_0": x0[0], "x2_0": x0[1],
"hpi_norm_xT": n_hpi, "lqr_norm_xT": n_lqr,
})
df_s = pd.DataFrame(summary)
sp = os.path.join(OUTPUT_DIR, "summary.csv")
df_s.to_csv(sp, index=False)
print(f"\n Summary: {sp}")
print(f"\n LQR K = {K}")
# ── Plot ──
print("\n Generating plots...")
plot_results(all_data, controller, results)
print("\n Done! Output files in ./output/")
if __name__ == "__main__":
main()