first commit

This commit is contained in:
2026-05-18 19:02:23 +08:00
commit 69c677e6da
16 changed files with 2454 additions and 0 deletions
+131
View File
@@ -0,0 +1,131 @@
"""Module A: ODE simulation and trajectory data collection.
Implements inverted pendulum dynamics and persistent excitation signal
generation for data-driven adaptive optimal control.
"""
import numpy as np
from scipy.integrate import solve_ivp
class DataCollector:
"""Inverted pendulum simulator with data collection capabilities.
The pendulum dynamics are given by Eq. (27):
dx₁/dt = x₂
dx₂/dt = (mgl/J)·sin(x₁) + (1/J)·u
"""
def __init__(self, J: float = 1.0, mgl: float = 1.0, n_freqs: int = 100):
"""
Args:
J: Moment of inertia (default 1.0).
mgl: Mass × gravity × length product (default 1.0).
n_freqs: Number of frequency components in PE signal.
"""
self.J = float(J)
self.mgl = float(mgl)
self.n_freqs = n_freqs
# Pre-compute PE signal parameters
self._rng = np.random.RandomState(42)
self._omegas = self._rng.uniform(0.5, 50.0, size=n_freqs)
self._a = self._rng.uniform(-1.0, 1.0, size=n_freqs)
self._b = self._rng.uniform(-1.0, 1.0, size=n_freqs)
# Scale amplitudes so that max |pe_signal| ≈ 0.1
raw_max = np.sum(np.abs(self._a) + np.abs(self._b))
self._scale = 0.1 / raw_max if raw_max > 0 else 1.0
def pe_signal(self, t):
"""Persistent excitation signal.
u_e(t) = scale · Σ (a_k·sin(ω_k·t) + b_k·cos(ω_k·t))
Args:
t: Time value (scalar).
Returns:
float: PE signal value at time t.
"""
t = float(t)
signals = self._a * np.sin(self._omegas * t) + self._b * np.cos(self._omegas * t)
return self._scale * np.sum(signals)
def f(self, x):
"""Drift dynamics f(x) — Eq. (27).
f(x) = [x₂, (mgl/J)·sin(x₁)]ᵀ
Args:
x: State vector (2,) or (N, 2).
Returns:
np.ndarray: Drift dynamics values.
"""
x = np.asarray(x, dtype=np.float64)
if x.ndim == 1:
return np.array([x[1], (self.mgl / self.J) * np.sin(x[0])])
return np.column_stack([x[:, 1], (self.mgl / self.J) * np.sin(x[:, 0])])
def g(self, x):
"""Input matrix g(x) — Eq. (27).
g(x) = [0, 1/J]ᵀ (constant)
Args:
x: State vector (2,) or (N, 2) — ignored, kept for interface consistency.
Returns:
np.ndarray: Input matrix [0, 1/J]ᵀ.
"""
x = np.asarray(x, dtype=np.float64)
if x.ndim == 1:
return np.array([0.0, 1.0 / self.J])
N = x.shape[0]
result = np.zeros((N, 2))
result[:, 1] = 1.0 / self.J
return result
def dynamics(self, t, x, u_func):
"""ODE right-hand side: ẋ = f(x) + g(x)·u(t).
Args:
t: Current time.
x: State vector (2,).
u_func: Callable u_func(t, x) → control input.
Returns:
np.ndarray: State derivative (2,).
"""
u_val = u_func(t, x)
return self.f(x) + self.g(x) * u_val
def collect_trajectory(self, x0, T, dt, u_func):
"""Collect a trajectory by simulating the system.
Args:
x0: Initial state (2,).
T: Simulation duration.
dt: Time step for output sampling.
u_func: Control function u_func(t, x) → float.
Returns:
tuple: (t, X, U) where
t: Time points (M,).
X: State trajectory (M, 2).
U: Control inputs (M,).
"""
t_eval = np.arange(0, T, dt)
t_span = (0.0, T)
def ode_rhs(t, x):
return self.dynamics(t, x, u_func)
sol = solve_ivp(ode_rhs, t_span, x0, method="RK45",
t_eval=t_eval, rtol=1e-8, atol=1e-10)
t = sol.t
X = sol.y.T # (M, 2)
U = np.array([u_func(ti, xi) for ti, xi in zip(t, X)])
return t, X, U