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

132 lines
3.9 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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