Spaces:
Running on Zero
Running on Zero
| #!/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.") | |