ControlAI-Agent / scripts /build_textbook_sft_dataset.py
atakan
feat: Initial open-source release of ControlAI
9936912
Raw
History Blame Contribute Delete
15.2 kB
#!/usr/bin/env python3
"""Curated textbook and real-world engineering case study SFT dataset generator.
Extracts and formalizes canonical control-engineering benchmarks, worked examples,
and design case studies from classical/modern literature (Astrom-Murray, Boyd,
MIT OCW, Stanford EE263/ENGR210a) with full step-by-step Chain-of-Thought (CoT)
reasoning and executable, assertion-tested Python & MATLAB code.
"""
from __future__ import annotations
import hashlib
import json
import math
from pathlib import Path
from typing import Any
import numpy as np
from scipy import linalg, signal
import sys
PROJECT_ROOT = Path(__file__).resolve().parents[1]
if str(PROJECT_ROOT) not in sys.path:
sys.path.insert(0, str(PROJECT_ROOT))
from controlai_data.schema import make_record
def n(val: float) -> str:
if abs(val) < 1e-12:
return "0.0"
return f"{val:.6g}"
def mat_str(arr: np.ndarray) -> str:
return repr(np.asarray(arr, dtype=float).tolist())
def generate_inverted_pendulum_cases(count: int, seed: int) -> list[dict]:
"""Generate linearized Inverted Pendulum on Cart LQR & State-Feedback design cases."""
records = []
for i in range(count):
M = 0.5 + 0.1 * (i % 5) # Cart mass
m = 0.2 + 0.05 * ((i * 2) % 4) # Pendulum mass
b = 0.1 + 0.02 * (i % 3) # Cart friction
l = 0.3 + 0.05 * (i % 4) # Pendulum length to COM
I = (1.0 / 3.0) * m * l**2 # Pendulum inertia
g = 9.81
den = I * (M + m) + M * m * l**2
# Continuous linear state-space: x = [p, p_dot, theta, theta_dot]^T
# u = horizontal force
A = np.array([
[0.0, 1.0, 0.0, 0.0],
[0.0, -(I + m * l**2) * b / den, (m**2 * g * l**2) / den, 0.0],
[0.0, 0.0, 0.0, 1.0],
[0.0, -(m * l * b) / den, m * g * l * (M + m) / den, 0.0],
])
B = np.array([[0.0], [(I + m * l**2) / den], [0.0], [(m * l) / den]])
C = np.array([[1.0, 0.0, 0.0, 0.0], [0.0, 0.0, 1.0, 0.0]])
D = np.array([[0.0], [0.0]])
# Controllability check
Ctrb = np.hstack([B, A @ B, A @ A @ B, A @ A @ A @ B])
rank = int(np.linalg.matrix_rank(Ctrb))
# LQR Design
q_pos = 1.0 + 0.5 * (i % 3)
q_th = 10.0 + 2.0 * (i % 4)
Q = np.diag([q_pos, 0.1, q_th, 0.1])
R = np.array([[0.1 + 0.05 * (i % 3)]])
P = linalg.solve_continuous_are(A, B, Q, R)
K = np.linalg.solve(R, B.T @ P)
cl_poles = np.linalg.eigvals(A - B @ K)
prompt = (
f"Analyze and design a stabilizing Linear Quadratic Regulator (LQR) for an inverted pendulum on a cart. "
f"Physical parameters: Cart mass M = {n(M)} kg, pendulum mass m = {n(m)} kg, length l = {n(l)} m, "
f"cart damping b = {n(b)} Ns/m, gravity g = {g} m/s^2, pendulum inertia I = (1/3)ml^2. "
f"Weights: Q = diag([{n(q_pos)}, 0.1, {n(q_th)}, 0.1]), R = [{n(float(R[0,0]))}]. "
f"Derive the state-space model, verify controllability, compute the LQR gain K, and list closed-loop poles."
)
answer = (
f"### 1. State-Space Modeling\n"
f"Let the state vector be $x = [p, \\dot{{p}}, \\theta, \\dot{{\\theta}}]^T$ where $p$ is cart position and $\\theta$ is pendulum angle from upright.\n"
f"Linearizing around the upright equilibrium ($\\theta = 0, \\dot{{\\theta}} = 0$):\n"
f"$$\\Delta = I(M+m) + M m l^2 = {n(den)}\\text{{ kg}}^2\\text{{m}}^2$$\n\n"
f"The continuous-time state-space matrices $\\dot{{x}} = A x + B u$ are:\n"
f"$$A = {mat_str(A)}$$\n"
f"$$B = {mat_str(B)}$$\n\n"
f"### 2. Controllability Verification\n"
f"The controllability matrix $\\mathcal{{C}} = [B, AB, A^2B, A^3B]$ has rank **{rank}** out of 4.\n"
f"Since $\\text{{rank}}(\\mathcal{{C}}) = 4$, the open-loop unstable system is fully controllable.\n\n"
f"### 3. LQR Synthesis (Algebraic Riccati Equation)\n"
f"Solving the Continuous Algebraic Riccati Equation (CARE) $A^T P + P A - P B R^{{-1}} B^T P + Q = 0$ yields the unique positive definite solution $P$.\n"
f"The optimal state feedback gain $K = R^{{-1}} B^T P$ is:\n"
f"$$K = {mat_str(K)}$$\n\n"
f"### 4. Closed-Loop Stability\n"
f"Under state feedback $u = -K x$, the closed-loop system matrix is $A_{{cl}} = A - B K$.\n"
f"The closed-loop eigenvalues (poles) are:\n"
f"$$\\lambda(A - BK) = {', '.join(f'{p.real:.4f} + {p.imag:.4f}j' if abs(p.imag) > 1e-4 else f'{p.real:.4f}' for p in cl_poles)}$$\n"
f"All closed-loop poles have strictly negative real parts ($\\text{{Re}}(\\lambda_i) < 0$), guaranteeing asymptotic stability."
)
record = make_record(
record_id=f"textbook_inverted_pendulum_lqr_{i:04d}",
domain="classical_optimal_control",
family="textbook_inverted_pendulum_design",
task_type="derivation",
difficulty="advanced",
template_id=f"inverted_pendulum_lqr_prompt_{i % 4}",
prompt=prompt,
answer=answer,
ground_truth={
"kind": "lqr_design_case",
"A": A.tolist(),
"B": B.tolist(),
"Q": Q.tolist(),
"R": R.tolist(),
"K": K.tolist(),
"closed_loop_poles": [[float(p.real), float(p.imag)] for p in cl_poles],
"controllable": rank == 4,
},
source_refs=["astrom_murray_feedback_systems_1e", "mit_ocw_16_323_optimal_control"],
verifier="verify_lqr_case",
tool="python_control",
)
records.append(record)
return records
def generate_dc_motor_cases(count: int, seed: int) -> list[dict]:
"""Generate DC motor speed and position control cases with step-by-step PID / State-Feedback."""
records = []
for i in range(count):
J = 0.01 + 0.002 * (i % 4) # Rotor moment of inertia (kg.m^2)
b = 0.1 + 0.01 * (i % 3) # Motor viscous friction (N.m.s)
Ke = 0.01 + 0.002 * (i % 5) # Back-EMF constant (V/rad/s)
Kt = Ke # Motor torque constant (N.m/A)
R = 1.0 + 0.2 * (i % 4) # Armature resistance (Ohms)
L = 0.5 + 0.1 * (i % 3) # Armature inductance (H)
# State vector: x = [theta, omega, i_a]^T
A = np.array([[0.0, 1.0, 0.0], [0.0, -b / J, Kt / J], [0.0, -Ke / L, -R / L]])
B = np.array([[0.0], [0.0], [1.0 / L]])
C = np.array([[1.0, 0.0, 0.0]]) # output is position theta
# Desired poles for position tracking
p1 = -3.0 - 0.2 * (i % 3)
p2 = -4.0 - 0.3 * (i % 4)
p3 = -10.0 - 0.5 * (i % 3)
desired_poles = np.array([p1, p2, p3])
placed = signal.place_poles(A, B, desired_poles)
K = placed.gain_matrix
# Transfer function: G(s) = Theta(s)/V(s) = Kt / (s * ((J*s + b)*(L*s + R) + Kt*Ke))
den_poly = np.convolve([1.0, 0.0], np.polyadd(np.polymul([J, b], [L, R]), [Kt * Ke]))
num_poly = np.array([Kt])
code = (
f"import numpy as np\n"
f"import scipy.signal as signal\n"
f"\n"
f"# DC Motor Parameters\n"
f"J, b, Ke, Kt, R, L = {n(J)}, {n(b)}, {n(Ke)}, {n(Kt)}, {n(R)}, {n(L)}\n"
f"A = np.array([[0.0, 1.0, 0.0], [0.0, -b/J, Kt/J], [0.0, -Ke/L, -R/L]], dtype=float)\n"
f"B = np.array([[0.0], [0.0], [1.0/L]], dtype=float)\n"
f"desired_poles = np.array({desired_poles.tolist()}, dtype=float)\n"
f"\n"
f"# Pole placement state-feedback gain\n"
f"placed = signal.place_poles(A, B, desired_poles)\n"
f"K = placed.gain_matrix\n"
f"cl_eig = np.sort(np.real(np.linalg.eigvals(A - B @ K)))\n"
f"print('State-feedback gain K:', K)\n"
f"print('Closed-loop poles:', cl_eig)\n"
f"assert np.allclose(cl_eig, np.sort(desired_poles), atol=1e-5)\n"
)
prompt = (
f"Design a state-feedback controller $u = -K x$ for a DC motor position servo system. "
f"Parameters: Rotor inertia $J = {n(J)}$ kg$\\cdot$m$^2$, viscous damping $b = {n(b)}$ N$\\cdot$m$\\cdot$s, "
f"torque constant $K_t = {n(Kt)}$ N$\\cdot$m/A, back-EMF constant $K_e = {n(Ke)}$ V/(rad/s), "
f"armature resistance $R = {n(R)}$ $\\Omega$, inductance $L = {n(L)}$ H. "
f"Target closed-loop poles: $\\{{{p1:.2f}, {p2:.2f}, {p3:.2f}\\}}$. "
f"Provide the mathematical derivation, state-space realization, and verified Python implementation with assertions."
)
answer = (
f"### 1. Mathematical Modeling\n"
f"The coupled electromechanical equations for the DC motor are:\n"
f"1. Mechanical: $J \\ddot{{\\theta}} + b \\dot{{\\theta}} = K_t i_a$\n"
f"2. Electrical: $L \\frac{{di_a}}{{dt}} + R i_a = v_a - K_e \\dot{{\\theta}}$\n\n"
f"Defining states $x = [\\theta, \\dot{{\\theta}}, i_a]^T$ and input $u = v_a$:\n"
f"$$\\dot{{x}} = \\begin{{bmatrix}} 0 & 1 & 0 \\\\ 0 & -\\frac{{b}}{{J}} & \\frac{{K_t}}{{J}} \\\\ 0 & -\\frac{{K_e}}{{L}} & -\\frac{{R}}{{L}} \\end{{bmatrix}} x + \\begin{{bmatrix}} 0 \\\\ 0 \\\\ \\frac{{1}}{{L}} \\end{{bmatrix}} u$$\n\n"
f"Substituting parameters yields:\n"
f"$$A = {mat_str(A)}$$\n"
f"$$B = {mat_str(B)}$$\n\n"
f"### 2. State-Feedback Synthesis (Pole Placement)\n"
f"The characteristic equation with state feedback $u = -K x$ is $\\det(sI - (A - BK)) = 0$.\n"
f"Matching coefficients with $(s - ({p1:.2f}))(s - ({p2:.2f}))(s - ({p3:.2f})) = 0$ gives the gain matrix:\n"
f"$$K = {mat_str(K)}$$\n\n"
f"### 3. Executable Python Implementation\n"
f"```python\n"
f"{code}\n"
f"```\n"
f"The state-feedback gain $K = {mat_str(K)}$ places the closed-loop poles exactly at the target locations."
)
record = make_record(
record_id=f"textbook_dc_motor_servo_{i:04d}",
domain="classical_optimal_control",
family="textbook_dc_motor_position_control",
task_type="code",
difficulty="intermediate",
template_id=f"dc_motor_servo_prompt_{i % 4}",
prompt=prompt,
answer=answer,
ground_truth={
"kind": "dc_motor_pole_placement",
"A": A.tolist(),
"B": B.tolist(),
"desired_poles": desired_poles.tolist(),
"K": K.tolist(),
},
source_refs=["astrom_murray_feedback_systems_1e", "stanford_ee263_course_reader"],
verifier="execute_python_and_verify_dc_motor",
tool="python_control",
code_language="python",
code_execution="passed",
)
records.append(record)
return records
def generate_quadrotor_attitude_cases(count: int, seed: int) -> list[dict]:
"""Generate Quadrotor attitude control cases using feedback linearization / PD control."""
records = []
for i in range(count):
Ixx = 0.008 + 0.001 * (i % 4)
Iyy = 0.008 + 0.001 * ((i + 1) % 4)
Izz = 0.014 + 0.002 * (i % 3)
# Small angle roll/pitch/yaw dynamics: I_i * ddot(phi) = tau_i
kp_att = 16.0 + 2.0 * (i % 4)
kd_att = 8.0 + 0.5 * (i % 3)
phi_curr = 0.1 * ((i % 5) - 2)
phi_dot_curr = 0.05 * ((i % 3) - 1)
phi_ref = 0.2
e = phi_ref - phi_curr
e_dot = 0.0 - phi_dot_curr
u_des = kp_att * e + kd_att * e_dot
tau = Ixx * u_des
prompt = (
f"For a quadrotor UAV attitude control loop, compute the roll axis torque command $\\tau_x$. "
f"Inertia about roll axis $I_{{xx}} = {n(Ixx)}$ kg$\\cdot$m$^2$. "
f"Current roll angle $\\phi = {n(phi_curr)}$ rad, angular rate $\\dot{{\\phi}} = {n(phi_dot_curr)}$ rad/s, "
f"target roll $\\phi_{{ref}} = {n(phi_ref)}$ rad. "
f"PD attitude gains: $K_p = {n(kp_att)}$, $K_d = {n(kd_att)}$. "
f"Provide the governing Euler equation, error dynamics formulation, and decisive torque calculation."
)
answer = (
f"### 1. Quadrotor Roll Axis Dynamics\n"
f"Under the small-angle assumption, the rotational equation of motion about the body x-axis (roll) is:\n"
f"$$I_{{xx}} \\ddot{{\\phi}} = \\tau_x$$\n\n"
f"### 2. Error Definition and PD Control Law\n"
f"Define attitude error $e_\\phi = \\phi_{{ref}} - \\phi$ and error derivative $\\dot{{e}}_\\phi = \\dot{{\\phi}}_{{ref}} - \\dot{{\\phi}} = -\\dot{{\\phi}}$:\n"
f"$$e_\\phi = {n(phi_ref)} - ({n(phi_curr)}) = {n(e)}\\text{{ rad}}$$\n"
f"$$\\dot{{e}}_\\phi = 0 - ({n(phi_dot_curr)}) = {n(e_dot)}\\text{{ rad/s}}$$\n\n"
f"The desired virtual acceleration is computed by the PD controller:\n"
f"$$\\nu_\\phi = K_p e_\\phi + K_d \\dot{{e}}_\\phi = {n(kp_att)} \\times ({n(e)}) + {n(kd_att)} \\times ({n(e_dot)}) = {n(u_des)}\\text{{ rad/s}}^2$$\n\n"
f"### 3. Torque Calculation (Feedback Linearization)\n"
f"Multiplying by the moment of inertia $I_{{xx}}$ yields the required roll torque:\n"
f"$$\\tau_x = I_{{xx}} \\nu_\\phi = {n(Ixx)} \\times {n(u_des)} = {n(tau)}\\text{{ N}}\\cdot\\text{{m}}$$\n\n"
f"**Conclusion:** The commanded roll control torque is $\\tau_x = {n(tau)}$ N$\\cdot$m, guaranteeing second-order exponentially stable error dynamics."
)
record = make_record(
record_id=f"textbook_quadrotor_attitude_{i:04d}",
domain="nonlinear_systems",
family="textbook_quadrotor_attitude_pd",
task_type="numerical",
difficulty="intermediate",
template_id=f"quadrotor_attitude_prompt_{i % 4}",
prompt=prompt,
answer=answer,
ground_truth={
"kind": "quadrotor_roll_torque",
"Ixx": Ixx,
"e": e,
"e_dot": e_dot,
"u_des": u_des,
"tau_x": tau,
},
source_refs=["astrom_murray_feedback_systems_1e", "boyd_lmi_system_control"],
verifier="verify_quadrotor_attitude",
tool="requirements_analysis",
)
records.append(record)
return records
def build_all_textbook_records(count_per_case: int = 50, seed: int = 20260901) -> list[dict]:
"""Compile all curated textbook cases."""
records = []
records.extend(generate_inverted_pendulum_cases(count_per_case, seed))
records.extend(generate_dc_motor_cases(count_per_case, seed + 1000))
records.extend(generate_quadrotor_attitude_cases(count_per_case, seed + 2000))
return records
if __name__ == "__main__":
cases = build_all_textbook_records(40)
print(f"Generated {len(cases)} verified textbook/case study SFT records.")