ControlAI-Agent / controlai_data /generators /classical_optimal_v1.py
atakan
feat: Initial open-source release of ControlAI
9936912
Raw
History Blame Contribute Delete
39.7 kB
"""Family-diverse, solver-backed classical/optimal SFT generators with Chain of Thought (CoT)."""
from __future__ import annotations
import math
from collections.abc import Callable
import cvxpy as cp
import numpy as np
from scipy import linalg
from controlai_data.schema import make_record
OPENERS = (
"Provide a compact engineering calculation for the following case.",
"Compute the requested quantities and state the decisive criterion.",
"Work this classical/optimal control problem step-by-step with verified values.",
"Derive the requested objects from the stated model; show intermediate equations.",
)
FBS = ["astrom_murray_feedback_systems_1e"]
OPTIMAL = ["boyd_lmi_system_control", "mit_ocw_16_323_optimal_control"]
ESTIMATION = ["stanford_ee263_course_reader"]
MPC = ["cvxpy_docs", "rawlings_mayne_diehl_mpc_2e"]
def n(value: float) -> str:
if abs(value) < 5e-11:
value = 0.0
return f"{value:.6g}"
def mat(value: np.ndarray) -> str:
return repr(np.asarray(value, dtype=float).tolist())
def roots(values: np.ndarray) -> list[list[float]]:
values = np.asarray(values, dtype=complex)
values = values[np.lexsort((values.imag, values.real))]
return [[float(x.real), float(x.imag)] for x in values]
def first_order_step(rng: np.random.Generator, index: int) -> dict:
del rng
gain = 0.5 + 0.075 * index
tau = 0.2 + 0.04 * index
step = 0.5 + 0.25 * (index % 5)
final = gain * step
at_tau = final * (1 - math.exp(-1))
settling_2 = -tau * math.log(0.02)
prompt = (
f"{OPENERS[index % 4]} A stable first-order plant is G(s) = {n(gain)}/"
f"({n(tau)}s+1). For a step of amplitude {n(step)}, find the final value, "
"the output at one time constant, and the exact 2% settling time."
)
answer = (
f"### 1. Step Response Formulation\n"
f"For plant $G(s) = \\frac{{{n(gain)}}}{{{n(tau)}s + 1}}$ with step input $u(t) = {n(step)}$, the output response is:\n"
f"$$y(t) = K \\cdot U_0 (1 - e^{{-t/\\tau}}) = {n(gain)} \\times {n(step)} (1 - e^{{-t/{n(tau)}}}) = {n(final)} (1 - e^{{-t/{n(tau)}}})$$\n\n"
f"### 2. Output Calculations\n"
f"1. **Final steady-state value** ($t \\to \\infty$):\n"
f" $$y_{{ss}} = K U_0 = {n(final)}$$\n"
f"2. **Output at one time constant** ($t = \\tau = {n(tau)}$ s):\n"
f" $$y(\\tau) = y_{{ss}} (1 - e^{{-1}}) = {n(final)} \\times {n(1 - math.exp(-1))} = {n(at_tau)}$$\n"
f"3. **Exact 2% settling time** ($y(t_s) = 0.98 y_{{ss}} \\implies e^{{-t_s/\\tau}} = 0.02$):\n"
f" $$t_s = -\\tau \\ln(0.02) = -{n(tau)} \\times ({n(math.log(0.02))}) = {n(settling_2)}\\text{{ s}}$$\n\n"
f"**Summary:** Final value = {n(final)}, y(tau) = {n(at_tau)}, t_s(2%) = {n(settling_2)} s."
)
return make_record(
record_id=f"first_order_step_metrics_{index:05d}",
domain="classical_control",
family="first_order_exact_step_metrics",
task_type="numerical",
difficulty="foundation",
template_id=f"first_order_step_prompt_{index%4}",
prompt=prompt,
answer=answer,
ground_truth={
"kind": "first_order_step",
"gain": gain,
"tau": tau,
"step": step,
"final": final,
"at_tau": at_tau,
"settling_2": settling_2,
},
source_refs=FBS,
verifier="verify_first_order_step",
)
def second_order_transient(rng: np.random.Generator, index: int) -> dict:
del rng
zeta = 0.12 + 0.012 * index
wn = 0.8 + 0.09 * index
wd = wn * math.sqrt(1 - zeta * zeta)
overshoot = math.exp(-math.pi * zeta / math.sqrt(1 - zeta * zeta)) * 100
peak_time = math.pi / wd
settling = -math.log(0.02) / (zeta * wn)
poles = np.array([-zeta * wn + 1j * wd, -zeta * wn - 1j * wd])
prompt = (
f"{OPENERS[index % 4]} A standard second-order closed loop has zeta={n(zeta)} "
f"and omega_n={n(wn)} rad/s. Compute its poles, damped frequency, percent "
"overshoot, peak time, and exact 2% exponential-envelope time."
)
answer = (
f"### 1. Second-Order System Parameters\n"
f"Given $\\zeta = {n(zeta)}$ and $\\omega_n = {n(wn)}$ rad/s:\n"
f"1. **Damped natural frequency**:\n"
f" $$\\omega_d = \\omega_n \\sqrt{{1 - \\zeta^2}} = {n(wn)} \\times \\sqrt{{1 - {n(zeta)}^2}} = {n(wd)}\\text{{ rad/s}}$$\n"
f"2. **Closed-loop complex conjugate poles**:\n"
f" $$s_{{1,2}} = -\\zeta \\omega_n \\pm j \\omega_d = -{n(zeta * wn)} \\pm {n(wd)}j = {n(poles[0].real)} \\pm {n(abs(poles[0].imag))}j$$\n\n"
f"### 2. Transient Step Specifications\n"
f"1. **Percent overshoot ($M_p$)**:\n"
f" $$M_p = \\exp\\left(-\\frac{{\\pi \\zeta}}{{\\sqrt{{1 - \\zeta^2}}}}\\right) \\times 100\\% = {n(overshoot)}\\%$$\n"
f"2. **Peak time ($t_p$)**:\n"
f" $$t_p = \\frac{{\\pi}}{{\\omega_d}} = \\frac{{\\pi}}{{{n(wd)}}} = {n(peak_time)}\\text{{ s}}$$\n"
f"3. **Exact 2% settling time ($t_s$)**:\n"
f" $$t_s = -\\frac{{\\ln(0.02)}}{{\\zeta \\omega_n}} = \\frac{{{n(-math.log(0.02))}}}{{{n(zeta * wn)}}} = {n(settling)}\\text{{ s}}$$\n\n"
f"**Summary:** Poles = {n(poles[0].real)} ± {n(abs(poles[0].imag))}j, omega_d = {n(wd)} rad/s, M_p = {n(overshoot)}%, t_p = {n(peak_time)} s, t_s(2%) = {n(settling)} s."
)
return make_record(
record_id=f"second_order_transient_{index:05d}",
domain="classical_control",
family="underdamped_second_order_exact_metrics",
task_type="numerical",
difficulty="intermediate",
template_id=f"second_order_transient_prompt_{index%4}",
prompt=prompt,
answer=answer,
ground_truth={
"kind": "second_order_transient",
"zeta": zeta,
"omega_n": wn,
"omega_d": wd,
"overshoot_percent": overshoot,
"peak_time": peak_time,
"settling_2": settling,
"poles": roots(poles),
},
source_refs=FBS,
verifier="verify_second_order_transient",
)
def closed_loop_tf(rng: np.random.Generator, index: int) -> dict:
del rng
a1 = 1.0 + 0.1 * index
a0 = 2.0 + 0.15 * index
b0 = 0.5 + 0.05 * index
k = 0.4 + 0.08 * index
den = [1.0, a1, a0 + k * b0]
num = [k * b0]
poles = np.roots(den)
stable = bool(np.all(np.real(poles) < 0))
prompt = (
f"{OPENERS[index%4]} With unity negative feedback, P(s)={n(b0)}/(s^2+{n(a1)}s+{n(a0)}) and C(s)={n(k)}. "
"Derive T(s), list its poles, and classify stability."
)
answer = (
f"### 1. Closed-Loop Transfer Function Derivation\n"
f"Under unity feedback, the complementary sensitivity / closed-loop transfer function is:\n"
f"$$T(s) = \\frac{{C(s)P(s)}}{{1 + C(s)P(s)}} = \\frac{{{n(k)} \\times \\frac{{{n(b0)}}}{{s^2 + {n(a1)}s + {n(a0)}}}}}{{1 + {n(k)} \\times \\frac{{{n(b0)}}}{{s^2 + {n(a1)}s + {n(a0)}}}}}$$\n"
f"$$T(s) = \\frac{{{n(num[0])}}}{{s^2 + {n(a1)}s + ({n(a0)} + {n(k * b0)})}} = \\frac{{{n(num[0])}}}{{s^2 + {n(a1)}s + {n(den[2])}}}$$\n\n"
f"### 2. Pole Locations and Stability\n"
f"Solving the characteristic equation $s^2 + {n(a1)}s + {n(den[2])} = 0$:\n"
f"$$\\text{{Poles}} = \\{{{', '.join(f'{x.real:.6g} + {x.imag:.6g}j' if abs(x.imag) > 1e-9 else f'{x.real:.6g}' for x in poles)}\\}}$$\n\n"
f"### 3. Conclusion\n"
f"Since all poles have strictly negative real parts, the closed-loop system is **{'asymptotically stable' if stable else 'not asymptotically stable'}**."
)
return make_record(
record_id=f"closed_loop_tf_{index:05d}",
domain="classical_control",
family="unity_feedback_closed_loop_polynomial",
task_type="derivation",
difficulty="foundation",
template_id=f"closed_loop_tf_prompt_{index%4}",
prompt=prompt,
answer=answer,
ground_truth={
"kind": "closed_loop_tf",
"plant_num": [b0],
"plant_den": [1.0, a1, a0],
"gain": k,
"closed_num": num,
"closed_den": den,
"poles": roots(poles),
"stable": stable,
},
source_refs=FBS,
verifier="verify_closed_loop_tf",
)
def sensitivity_complex(rng: np.random.Generator, index: int) -> dict:
del rng
real = -0.7 + 0.06 * index
imag = -1.2 + 0.05 * index
L = complex(real, imag)
S = 1 / (1 + L)
T = L / (1 + L)
prompt = (
f"{OPENERS[index%4]} At one frequency the loop value is L(jw)={n(real)} {'+' if imag>=0 else '-'} {n(abs(imag))}j. "
"Compute S=1/(1+L), T=L/(1+L), their magnitudes, and verify S+T=1."
)
answer = (
f"### 1. Sensitivity Function $S(j\\omega) = \\frac{{1}}{{1 + L(j\\omega)}}$\n"
f"$$1 + L = 1 + ({n(real)} + {n(imag)}j) = {n(1 + real)} + {n(imag)}j$$\n"
f"$$S = \\frac{{1}}{{{n(1 + real)} + {n(imag)}j}} = {n(S.real)} {'+' if S.imag>=0 else '-'} {n(abs(S.imag))}j, \\quad |S| = {n(abs(S))}$$\n\n"
f"### 2. Complementary Sensitivity $T(j\\omega) = \\frac{{L(j\\omega)}}{{1 + L(j\\omega)}}$\n"
f"$$T = \\frac{{{n(real)} + {n(imag)}j}}{{{n(1 + real)} + {n(imag)}j}} = {n(T.real)} {'+' if T.imag>=0 else '-'} {n(abs(T.imag))}j, \\quad |T| = {n(abs(T))}$$\n\n"
f"### 3. Identity Verification\n"
f"$$S + T = ({n(S.real)} + {n(T.real)}) + ({n(S.imag)} + {n(T.imag)})j = 1.0 + 0.0j$$\n"
f"**Summary:** S = {n(S.real)} {'+' if S.imag>=0 else '-'} {n(abs(S.imag))}j (|S|={n(abs(S))}), T = {n(T.real)} {'+' if T.imag>=0 else '-'} {n(abs(T.imag))}j (|T|={n(abs(T))}), S + T = 1."
)
return make_record(
record_id=f"sensitivity_complex_{index:05d}",
domain="robust_control",
family="complex_sensitivity_complementary_sensitivity",
task_type="numerical",
difficulty="intermediate",
template_id=f"sensitivity_complex_prompt_{index%4}",
prompt=prompt,
answer=answer,
ground_truth={
"kind": "sensitivity_complex",
"L": [real, imag],
"S": [S.real, S.imag],
"T": [T.real, T.imag],
"S_mag": abs(S),
"T_mag": abs(T),
},
source_refs=FBS,
verifier="verify_sensitivity_complex",
)
def lead_parameters(rng: np.random.Generator, index: int) -> dict:
del rng
phi_deg = 12.0 + 1.2 * index
wm = 0.6 + 0.08 * index
phi = math.radians(phi_deg)
alpha = (1 - math.sin(phi)) / (1 + math.sin(phi))
tau = 1 / (wm * math.sqrt(alpha))
zero = -1 / tau
pole = -1 / (alpha * tau)
prompt = (
f"{OPENERS[index%4]} Design the pole/zero locations of C(s)=(tau s+1)/(alpha tau s+1) for maximum phase lead "
f"{n(phi_deg)} deg at omega_m={n(wm)} rad/s. Report alpha, tau, zero, and pole."
)
answer = (
f"### 1. Lead Compensator Design Equations\n"
f"For target maximum phase lead $\\phi_m = {n(phi_deg)}^\\circ$ at geometric mean frequency $\\omega_m = {n(wm)}$ rad/s:\n"
f"1. **Alpha parameter**:\n"
f" $$\\alpha = \\frac{{1 - \\sin(\\phi_m)}}{{1 + \\sin(\\phi_m)}} = \\frac{{1 - \\sin({n(phi_deg)}^\\circ)}}{{1 + \\sin({n(phi_deg)}^\\circ)}} = {n(alpha)}$$\n"
f"2. **Time constant $\\tau$**:\n"
f" $$\\tau = \\frac{{1}}{{\\omega_m \\sqrt{{\\alpha}}}} = \\frac{{1}}{{{n(wm)} \\times \\sqrt{{{n(alpha)}}}}} = {n(tau)}\\text{{ s}}$$\n\n"
f"### 2. Pole and Zero Placement\n"
f"- Compensator zero: $z = -1/\\tau = {n(zero)}$ rad/s\n"
f"- Compensator pole: $p = -1/(\\alpha \\tau) = {n(pole)}$ rad/s\n\n"
f"**Summary:** alpha = {n(alpha)}, tau = {n(tau)} s, zero = {n(zero)} rad/s, pole = {n(pole)} rad/s."
)
return make_record(
record_id=f"lead_parameters_{index:05d}",
domain="classical_control",
family="lead_compensator_maximum_phase_parameters",
task_type="design",
difficulty="intermediate",
template_id=f"lead_parameters_prompt_{index%4}",
prompt=prompt,
answer=answer,
ground_truth={
"kind": "lead_parameters",
"phi_deg": phi_deg,
"omega_m": wm,
"alpha": alpha,
"tau": tau,
"zero": zero,
"pole": pole,
},
source_refs=FBS,
verifier="verify_lead_parameters",
)
def pid_characteristic(rng: np.random.Generator, index: int) -> dict:
del rng
a = 0.8 + 0.06 * index
kp = 0.5 + 0.07 * index
ki = 0.2 + 0.025 * index
kd = 0.1 + 0.02 * index
coeff = [1.0, a + kd, kp, ki]
poles = np.roots(coeff)
stable = bool(np.all(np.real(poles) < 0))
prompt = (
f"{OPENERS[index%4]} For P(s)=1/[s(s+{n(a)})] with unity negative feedback and C(s)=Kp+Ki/s+Kd s, "
f"use Kp={n(kp)}, Ki={n(ki)}, Kd={n(kd)}. Derive the characteristic polynomial, compute poles, and assess stability."
)
answer = (
f"### 1. Closed-Loop Characteristic Equation\n"
f"The characteristic equation is $1 + C(s)P(s) = 0$:\n"
f"$$1 + \\left(K_p + \\frac{{K_i}}{{s}} + K_d s\\right) \\frac{{1}}{{s(s + {n(a)})}} = 0$$\n"
f"$$s^2(s + {n(a)}) + (K_d s^2 + K_p s + K_i) = 0$$\n"
f"$$s^3 + ({n(a)} + {n(kd)}) s^2 + {n(kp)} s + {n(ki)} = s^3 + {n(coeff[1])} s^2 + {n(coeff[2])} s + {n(coeff[3])} = 0$$\n\n"
f"### 2. Roots (Closed-Loop Poles)\n"
f"Solving the cubic polynomial:\n"
f"$$\\text{{Poles}} = \\{{{', '.join(f'{x.real:.6g}{x.imag:+.6g}j' for x in poles)}\\}}$$\n\n"
f"### 3. Stability Assessment\n"
f"Since all closed-loop poles have strictly negative real parts, the closed loop is **{'asymptotically stable' if stable else 'not asymptotically stable'}**."
)
return make_record(
record_id=f"pid_characteristic_{index:05d}",
domain="classical_control",
family="pid_closed_loop_characteristic_polynomial",
task_type="derivation",
difficulty="intermediate",
template_id=f"pid_characteristic_prompt_{index%4}",
prompt=prompt,
answer=answer,
ground_truth={
"kind": "polynomial_stability",
"coefficients": coeff,
"poles": roots(poles),
"stable": stable,
},
source_refs=FBS,
verifier="verify_polynomial_stability",
)
def routh_quartic(rng: np.random.Generator, index: int) -> dict:
del rng
a3 = 1.0 + 0.09 * index
a2 = 1.5 + 0.07 * (index % 9)
a1 = 0.3 + 0.08 * (index % 11)
a0 = 0.2 + 0.03 * index
b1 = (a3 * a2 - a1) / a3
c1 = (b1 * a1 - a3 * a0) / b1
first = [1.0, a3, b1, c1, a0]
poles = np.roots([1, a3, a2, a1, a0])
stable = bool(np.all(np.real(poles) < 0))
routh_stable = all(x > 0 for x in first)
assert stable == routh_stable
prompt = (
f"{OPENERS[index%4]} Apply Routh-Hurwitz to p(s)=s^4+{n(a3)}s^3+{n(a2)}s^2+{n(a1)}s+{n(a0)}. "
"Report the full first column and the stability conclusion."
)
answer = (
f"### 1. Routh Array Construction\n"
f"For polynomial $p(s) = s^4 + {n(a3)} s^3 + {n(a2)} s^2 + {n(a1)} s + {n(a0)}$:\n"
f"- Row $s^4$: $[1, {n(a2)}, {n(a0)}]$\n"
f"- Row $s^3$: $[{n(a3)}, {n(a1)}, 0]$\n"
f"- Row $s^2$: $b_1 = \\frac{{{n(a3)} \\times {n(a2)} - 1 \\times {n(a1)}}}{{{n(a3)}}} = {n(b1)}$, $b_2 = {n(a0)}$\n"
f"- Row $s^1$: $c_1 = \\frac{{{n(b1)} \\times {n(a1)} - {n(a3)} \\times {n(a0)}}}{{{n(b1)}}} = {n(c1)}$\n"
f"- Row $s^0$: $d_1 = {n(a0)}$\n\n"
f"### 2. First Column Analysis\n"
f"The Routh first column is **$[1, {n(a3)}, {n(b1)}, {n(c1)}, {n(a0)}]$**.\n\n"
f"### 3. Conclusion\n"
f"The first column entries are **{'strictly positive, so the polynomial is Hurwitz' if stable else 'not strictly positive, so the polynomial is not Hurwitz'}**."
)
return make_record(
record_id=f"routh_quartic_{index:05d}",
domain="classical_control",
family="routh_quartic_numeric_classification",
task_type="derivation",
difficulty="intermediate",
template_id=f"routh_quartic_prompt_{index%4}",
prompt=prompt,
answer=answer,
ground_truth={
"kind": "routh_quartic",
"coefficients": [1, a3, a2, a1, a0],
"first_column": first,
"poles": roots(poles),
"stable": stable,
},
source_refs=FBS,
verifier="verify_routh_quartic",
)
def root_locus_point(rng: np.random.Generator, index: int) -> dict:
del rng
poles = sorted([-1.0 - 0.05 * index, -3.0 - 0.08 * index, -6.0 - 0.03 * index])
zeros = [-2.0 - 0.04 * index]
point = -0.4 - 0.11 * index
count = sum(x > point for x in poles + zeros)
on_locus = count % 2 == 1
prompt = (
f"{OPENERS[index%4]} A positive-gain root locus has real open-loop poles {poles} and real zeros {zeros}. "
f"Is the real-axis point s={n(point)} on the locus? Apply the odd-count rule."
)
answer = (
f"### 1. Real-Axis Root Locus Criterion (Angle Condition)\n"
f"A point $s_0$ on the real axis belongs to the $180^\\circ$ (positive gain) root locus if and only if the total number of real open-loop poles and zeros to its right is **odd**.\n\n"
f"### 2. Point Evaluation at $s = {n(point)}$\n"
f"- Open-loop poles: {poles}\n"
f"- Open-loop zeros: {zeros}\n"
f"- Poles and zeros to the right of $s = {n(point)}$: {count}\n\n"
f"### 3. Conclusion\n"
f"There are {count} real open-loop poles and zeros to the right of {n(point)}. Because this count is **{'odd' if on_locus else 'even'}**, the point **{'is' if on_locus else 'is not'}** on the positive-gain root locus."
)
return make_record(
record_id=f"root_locus_point_{index:05d}",
domain="classical_control",
family="root_locus_real_axis_membership",
task_type="concept",
difficulty="foundation",
template_id=f"root_locus_point_prompt_{index%4}",
prompt=prompt,
answer=answer,
ground_truth={
"kind": "root_locus_real_axis",
"poles": poles,
"zeros": zeros,
"point": point,
"right_count": count,
"on_locus": on_locus,
},
source_refs=FBS,
verifier="verify_root_locus_real_axis",
)
def margins_from_crossovers(rng: np.random.Generator, index: int) -> dict:
del rng
mag_pc = 0.15 + 0.012 * index
phase_gc = -110.0 - 1.1 * index
gm = 1 / mag_pc
gm_db = 20 * math.log10(gm)
pm = 180 + phase_gc
prompt = (
f"{OPENERS[index%4]} At the phase-crossover frequency, |L|={n(mag_pc)}. "
f"At the gain-crossover frequency, phase(L)={n(phase_gc)} deg. "
"Compute gain margin as a ratio and dB, plus phase margin."
)
answer = (
f"### 1. Stability Margin Formulas\n"
f"1. **Gain Margin (GM)**: Defined at phase-crossover frequency $\\omega_{{pc}}$ where $\\angle L(j\\omega_{{pc}}) = -180^\\circ$:\n"
f" $$GM = \\frac{{1}}{{|L(j\\omega_{{pc}})|}} = \\frac{{1}}{{{n(mag_pc)}}} = {n(gm)}$$\n"
f" $$GM_{{dB}} = 20 \\log_{{10}}(GM) = 20 \\log_{{10}}({n(gm)}) = {n(gm_db)}\\text{{ dB}}$$\n\n"
f"2. **Phase Margin (PM)**: Defined at gain-crossover frequency $\\omega_{{gc}}$ where $|L(j\\omega_{{gc}})| = 1$:\n"
f" $$PM = 180^\\circ + \\angle L(j\\omega_{{gc}}) = 180^\\circ + ({n(phase_gc)}^\\circ) = {n(pm)}^\\circ$$\n\n"
f"**Summary:** GM = {n(gm)} ({n(gm_db)} dB), PM = {n(pm)} deg."
)
return make_record(
record_id=f"margins_crossovers_{index:05d}",
domain="classical_control",
family="classical_margins_from_crossover_values",
task_type="numerical",
difficulty="foundation",
template_id=f"margins_crossovers_prompt_{index%4}",
prompt=prompt,
answer=answer,
ground_truth={
"kind": "crossover_margins",
"magnitude_at_phase_crossover": mag_pc,
"phase_at_gain_crossover_deg": phase_gc,
"gain_margin": gm,
"gain_margin_db": gm_db,
"phase_margin_deg": pm,
},
source_refs=FBS,
verifier="verify_crossover_margins",
)
def scalar_kf_predict(rng: np.random.Generator, index: int) -> dict:
del rng
a = 0.7 + 0.01 * index
b = 0.4 + 0.015 * index
x = 0.5 + 0.08 * index
u = -0.3 + 0.025 * index
P = 0.6 + 0.04 * index
Q = 0.05 + 0.005 * index
xm = a * x + b * u
Pm = a * a * P + Q
prompt = (
f"{OPENERS[index%4]} Perform only the scalar Kalman time update for x[k+1]={n(a)}x[k]+{n(b)}u[k]+w[k]. "
f"Given x_hat+={n(x)}, u={n(u)}, P+={n(P)}, Q={n(Q)}, compute x_hat- and P-."
)
answer = (
f"### 1. Kalman State Prediction (Time Update)\n"
f"$$\\hat{{x}}^-_k = a \\hat{{x}}^+_{{k-1}} + b u_{{k-1}} = ({n(a)})({n(x)}) + ({n(b)})({n(u)}) = {n(a * x)} + ({n(b * u)}) = {n(xm)}$$\n\n"
f"### 2. Error Covariance Prediction\n"
f"$$P^-_k = a^2 P^+_{{k-1}} + Q = ({n(a)})^2 ({n(P)}) + {n(Q)} = ({n(a*a)})({n(P)}) + {n(Q)} = {n(Pm)}$$\n\n"
f"**Summary:** Predicted state x_hat- = {n(xm)}, predicted covariance P- = {n(Pm)}."
)
return make_record(
record_id=f"scalar_kf_predict_{index:05d}",
domain="estimation_filtering",
family="scalar_kalman_time_update",
task_type="numerical",
difficulty="foundation",
template_id=f"scalar_kf_predict_prompt_{index%4}",
prompt=prompt,
answer=answer,
ground_truth={
"kind": "scalar_kf_predict",
"a": a,
"b": b,
"x": x,
"u": u,
"P": P,
"Q": Q,
"x_minus": xm,
"P_minus": Pm,
},
source_refs=ESTIMATION,
verifier="verify_scalar_kf_predict",
)
def matrix_kf_update(rng: np.random.Generator, index: int) -> dict:
del rng
x = np.array([0.2 + 0.06 * index, -0.4 + 0.03 * index])
base = np.array([[1.0 + 0.03 * index, 0.15], [0.15, 0.7 + 0.02 * index]])
P = base @ base.T
H = np.array([[1.0, 0.0 if index % 3 == 0 else 0.5]])
R = 0.2 + 0.01 * index
z = 0.5 + 0.05 * index
innovation = float(z - (H @ x)[0])
S = float((H @ P @ H.T)[0, 0] + R)
K = P @ H.T / S
xp = x + K[:, 0] * innovation
I = np.eye(2)
Pp = (I - K @ H) @ P @ (I - K @ H).T + K * R @ K.T
prompt = (
f"{OPENERS[index%4]} Perform a 2-state Kalman measurement update with x-={mat(x)}, P-={mat(P)}, "
f"H={mat(H)}, R={n(R)}, z={n(z)}. Report innovation, S, K, x+, and Joseph-form P+."
)
answer = (
f"### 1. Innovation and Innovation Covariance\n"
f"- Innovation: $\\tilde{{y}} = z - H \\hat{{x}}^- = {n(z)} - {n(float((H @ x)[0]))} = {n(innovation)}$\n"
f"- Innovation covariance: $S = H P^- H^T + R = {n(float((H @ P @ H.T)[0, 0]))} + {n(R)} = {n(S)}$\n\n"
f"### 2. Kalman Gain and State Update\n"
f"- Kalman Gain: $K = P^- H^T S^{{-1}} = {mat(K)}$\n"
f"- Updated State: $\\hat{{x}}^+ = \\hat{{x}}^- + K \\tilde{{y}} = {mat(xp)}$\n\n"
f"### 3. Joseph-Form Covariance Update\n"
f"$$P^+ = (I - KH) P^- (I - KH)^T + K R K^T = {mat(Pp)}$$\n\n"
f"**Summary:** Innovation = {n(innovation)}, S = {n(S)}, K = {mat(K)}, x+ = {mat(xp)}, P+ = {mat(Pp)}."
)
return make_record(
record_id=f"matrix_kf_update_{index:05d}",
domain="estimation_filtering",
family="matrix_kalman_measurement_update_2state",
task_type="numerical",
difficulty="intermediate",
template_id=f"matrix_kf_update_prompt_{index%4}",
prompt=prompt,
answer=answer,
ground_truth={
"kind": "matrix_kf_update",
"x_minus": x.tolist(),
"P_minus": P.tolist(),
"H": H.tolist(),
"R": [[R]],
"z": [z],
"innovation": [innovation],
"S": [[S]],
"K": K.tolist(),
"x_plus": xp.tolist(),
"P_plus": Pp.tolist(),
},
source_refs=ESTIMATION,
verifier="verify_matrix_kf_update",
)
def covariance_propagation(rng: np.random.Generator, index: int) -> dict:
del rng
A = np.array([[1.0, 0.05 + 0.002 * index], [0.0, 0.8 + 0.003 * index]])
P = np.array([[1.0 + 0.02 * index, 0.1], [0.1, 0.5 + 0.01 * index]])
Q = np.diag([0.02 + 0.001 * index, 0.04 + 0.0015 * index])
Pn = A @ P @ A.T + Q
prompt = (
f"{OPENERS[index%4]} Propagate covariance one step using P_next=A P A^T+Q for A={mat(A)}, P={mat(P)}, "
f"Q={mat(Q)}. Report P_next and check symmetry."
)
answer = (
f"### 1. Covariance Time Propagation Formula\n"
f"$$P_{{k+1}} = A P_k A^T + Q$$\n\n"
f"### 2. Matrix Multiplication and Addition\n"
f"- $A P A^T = {mat(A @ P @ A.T)}$\n"
f"- $P_{{next}} = A P A^T + Q = {mat(Pn)}$\n\n"
f"### 3. Symmetry Verification\n"
f"$$\\|P_{{next}} - P_{{next}}^T\\|_{{\\max}} = {n(float(np.max(np.abs(Pn - Pn.T))))} \\approx 0$$\n"
f"**Summary:** P_next = {mat(Pn)}."
)
return make_record(
record_id=f"covariance_propagation_{index:05d}",
domain="estimation_filtering",
family="linear_covariance_time_propagation",
task_type="numerical",
difficulty="foundation",
template_id=f"covariance_propagation_prompt_{index%4}",
prompt=prompt,
answer=answer,
ground_truth={
"kind": "covariance_propagation",
"A": A.tolist(),
"P": P.tolist(),
"Q": Q.tolist(),
"P_next": Pn.tolist(),
},
source_refs=ESTIMATION,
verifier="verify_covariance_propagation",
)
def continuous_lqr_matrix(rng: np.random.Generator, index: int) -> dict:
del rng
a0 = 0.8 + 0.04 * index
a1 = 0.3 + 0.025 * index
A = np.array([[0, 1], [-a0, -a1]], float)
B = np.array([[0], [1]], float)
Q = np.diag([1 + 0.03 * index, 0.5 + 0.02 * index])
R = np.array([[0.5 + 0.015 * index]])
P = linalg.solve_continuous_are(A, B, Q, R)
K = np.linalg.solve(R, B.T @ P)
poles = np.linalg.eigvals(A - B @ K)
prompt = (
f"{OPENERS[index%4]} Solve the continuous-time LQR for A={mat(A)}, B={mat(B)}, Q={mat(Q)}, R={mat(R)}. "
"Report the stabilizing P, K=R^-1 B^T P, and closed-loop poles."
)
answer = (
f"### 1. Continuous Algebraic Riccati Equation (CARE)\n"
f"$$A^T P + P A - P B R^{{-1}} B^T P + Q = 0$$\n"
f"Solving CARE yields the unique positive definite stabilizing matrix:\n"
f"$$P = {mat(P)}$$\n\n"
f"### 2. State-Feedback Gain Synthesis\n"
f"$$K = R^{{-1}} B^T P = {mat(K)}$$\n\n"
f"### 3. Closed-Loop Poles\n"
f"Eigenvalues of $A - BK$:\n"
f"$$\\lambda(A - BK) = \\{{{', '.join(f'{x.real:.6g}{x.imag:+.6g}j' for x in poles)}\\}}$$\n"
f"All closed-loop poles have strictly negative real parts, guaranteeing asymptotic stability."
)
return make_record(
record_id=f"continuous_lqr_matrix_{index:05d}",
domain="optimal_control",
family="continuous_matrix_lqr_care",
task_type="numerical",
difficulty="advanced",
template_id=f"continuous_lqr_matrix_prompt_{index%4}",
prompt=prompt,
answer=answer,
ground_truth={
"kind": "continuous_lqr",
"A": A.tolist(),
"B": B.tolist(),
"Q": Q.tolist(),
"R": R.tolist(),
"P": P.tolist(),
"K": K.tolist(),
"closed_poles": roots(poles),
},
source_refs=OPTIMAL,
verifier="verify_continuous_lqr",
)
def discrete_lqr_matrix(rng: np.random.Generator, index: int) -> dict:
del rng
T = 0.04 + 0.002 * index
A = np.array([[1, T], [0, 1]], float)
B = np.array([[0.5 * T * T], [T]], float)
Q = np.diag([1 + 0.02 * index, 0.2 + 0.01 * index])
R = np.array([[0.1 + 0.006 * index]])
P = linalg.solve_discrete_are(A, B, Q, R)
K = np.linalg.solve(R + B.T @ P @ B, B.T @ P @ A)
poles = np.linalg.eigvals(A - B @ K)
prompt = (
f"{OPENERS[index%4]} Solve the infinite-horizon discrete LQR for A={mat(A)}, B={mat(B)}, Q={mat(Q)}, R={mat(R)}. "
"Give P, K, and closed-loop poles under u=-Kx."
)
answer = (
f"### 1. Discrete Algebraic Riccati Equation (DARE)\n"
f"$$P = A^T P A - (A^T P B)(R + B^T P B)^{{-1}}(B^T P A) + Q$$\n"
f"The stabilizing positive definite solution is:\n"
f"$$P = {mat(P)}$$\n\n"
f"### 2. Discrete LQR Gain\n"
f"$$K = (R + B^T P B)^{{-1}} B^T P A = {mat(K)}$$\n\n"
f"### 3. Closed-Loop Discrete Poles\n"
f"$$\\lambda(A - BK) = \\{{{', '.join(f'{x.real:.6g}{x.imag:+.6g}j' for x in poles)}\\}}$$\n"
f"All poles satisfy $|\\lambda_i| < 1$, strictly inside the unit circle."
)
return make_record(
record_id=f"discrete_lqr_matrix_{index:05d}",
domain="optimal_control",
family="discrete_matrix_lqr_dare",
task_type="numerical",
difficulty="advanced",
template_id=f"discrete_lqr_matrix_prompt_{index%4}",
prompt=prompt,
answer=answer,
ground_truth={
"kind": "discrete_lqr",
"A": A.tolist(),
"B": B.tolist(),
"Q": Q.tolist(),
"R": R.tolist(),
"P": P.tolist(),
"K": K.tolist(),
"closed_poles": roots(poles),
},
source_refs=OPTIMAL,
verifier="verify_discrete_lqr",
)
def finite_horizon_lqr(rng: np.random.Generator, index: int) -> dict:
del rng
a = 0.8 + 0.005 * index
b = 0.6 + 0.01 * (index % 7)
q = 1.0 + 0.03 * index
r = 0.4 + 0.02 * (index % 5)
qf = 2.0 + 0.04 * index
horizon = 3
P = [0.0] * (horizon + 1)
K = [0.0] * horizon
P[horizon] = qf
for k in range(horizon - 1, -1, -1):
den = r + b * b * P[k + 1]
K[k] = b * P[k + 1] * a / den
P[k] = q + a * a * P[k + 1] - (a * b * P[k + 1]) ** 2 / den
prompt = (
f"{OPENERS[index%4]} For x[k+1]={n(a)}x[k]+{n(b)}u[k], minimize sum_0^2(q x_k^2+r u_k^2)+q_f x_3^2 with "
f"q={n(q)}, r={n(r)}, q_f={n(qf)}. Run the scalar Riccati recursion and report K0..K2 and P0..P3."
)
answer = (
f"### 1. Dynamic Programming Backward Riccati Recursion\n"
f"Terminal condition: $P_3 = q_f = {n(qf)}$.\n"
f"Backward recursion: $K_k = \\frac{{b P_{{k+1}} a}}{{r + b^2 P_{{k+1}}}}$, $P_k = q + a^2 P_{{k+1}} - K_k (r + b^2 P_{{k+1}}) K_k$.\n\n"
f"### 2. Step-by-Step Values\n"
f"- At $k=2$: $K_2 = {n(K[2])}$, $P_2 = {n(P[2])}$\n"
f"- At $k=1$: $K_1 = {n(K[1])}$, $P_1 = {n(P[1])}$\n"
f"- At $k=0$: $K_0 = {n(K[0])}$, $P_0 = {n(P[0])}$\n\n"
f"**Summary:** Gains K = {list(map(float, K))}, Riccati sequence P = {list(map(float, P))}."
)
return make_record(
record_id=f"finite_horizon_lqr_{index:05d}",
domain="optimal_control",
family="finite_horizon_scalar_lqr_recursion",
task_type="derivation",
difficulty="intermediate",
template_id=f"finite_horizon_lqr_prompt_{index%4}",
prompt=prompt,
answer=answer,
ground_truth={
"kind": "finite_horizon_lqr",
"a": a,
"b": b,
"q": q,
"r": r,
"qf": qf,
"horizon": horizon,
"K": K,
"P": P,
},
source_refs=OPTIMAL,
verifier="verify_finite_horizon_lqr",
)
def mpc_prediction_matrices(rng: np.random.Generator, index: int) -> dict:
del rng
a = 0.7 + 0.006 * index
b = 0.4 + 0.009 * index
horizon = 3
F = np.array([[a**i] for i in range(1, horizon + 1)])
G = np.zeros((horizon, horizon))
for row in range(horizon):
for col in range(row + 1):
G[row, col] = a ** (row - col) * b
prompt = (
f"{OPENERS[index%4]} For x[k+1]={n(a)}x[k]+{n(b)}u[k] and horizon N=3, form X=F x0+G U where "
"X=[x1,x2,x3]^T and U=[u0,u1,u2]^T."
)
answer = (
f"### 1. State Prediction Recursion over Horizon N=3\n"
f"- $x_1 = a x_0 + b u_0$\n"
f"- $x_2 = a x_1 + b u_1 = a^2 x_0 + a b u_0 + b u_1$\n"
f"- $x_3 = a x_2 + b u_2 = a^3 x_0 + a^2 b u_0 + a b u_1 + b u_2$\n\n"
f"### 2. Matrix Stacking $X = F x_0 + G U$\n"
f"$$F = \\begin{{bmatrix}} a \\\\ a^2 \\\\ a^3 \\end{{bmatrix}} = {mat(F)}$$\n"
f"$$G = \\begin{{bmatrix}} b & 0 & 0 \\\\ ab & b & 0 \\\\ a^2b & ab & b \\end{{bmatrix}} = {mat(G)}$$\n\n"
f"**Summary:** F = {mat(F)}, G = {mat(G)}."
)
return make_record(
record_id=f"mpc_prediction_matrices_{index:05d}",
domain="mpc",
family="scalar_mpc_prediction_matrices_horizon3",
task_type="derivation",
difficulty="intermediate",
template_id=f"mpc_prediction_prompt_{index%4}",
prompt=prompt,
answer=answer,
ground_truth={
"kind": "mpc_prediction",
"a": a,
"b": b,
"horizon": horizon,
"F": F.tolist(),
"G": G.tolist(),
},
source_refs=MPC,
verifier="verify_mpc_prediction",
)
def mpc_unconstrained(rng: np.random.Generator, index: int) -> dict:
del rng
a = 0.8 + 0.004 * index
b = 0.5 + 0.008 * index
x0 = -2.0 + 0.11 * index
r = 0.15 + 0.01 * (index % 9)
F = np.array([[a], [a * a]])
G = np.array([[b, 0], [a * b, b]])
H = G.T @ G + r * np.eye(2)
g = G.T @ (F[:, 0] * x0)
U = -np.linalg.solve(H, g)
X = F[:, 0] * x0 + G @ U
cost = float(X @ X + r * (U @ U))
prompt = (
f"{OPENERS[index%4]} Solve the unconstrained horizon-2 MPC problem for x[k+1]={n(a)}x[k]+{n(b)}u[k], "
f"x0={n(x0)}, J=x1^2+x2^2+{n(r)}(u0^2+u1^2). Report U*, X*, and J*."
)
answer = (
f"### 1. Quadratic Objective in Vector Form\n"
f"With $X = F x_0 + G U$, the cost is $J = \\|X\\|^2 + r \\|U\\|^2 = U^T (G^T G + r I) U + 2 (G^T F x_0)^T U + x_0^T F^T F x_0$.\n\n"
f"### 2. Unconstrained Analytic Minimization\n"
f"Setting gradient to zero: $(G^T G + r I) U^* = -G^T F x_0$\n"
f"- Hessian $H = G^T G + r I = {mat(H)}$\n"
f"- Gradient vector $g = G^T F x_0 = {mat(g)}$\n"
f"- Optimal input vector: $U^* = -H^{{-1}} g = {mat(U)}$\n"
f"- Optimal predicted trajectory: $X^* = F x_0 + G U^* = {mat(X)}$\n"
f"- Optimal cost: $J^* = {n(cost)}$\n\n"
f"**Summary:** U* = {mat(U)}, X* = {mat(X)}, J* = {n(cost)}."
)
return make_record(
record_id=f"mpc_unconstrained_{index:05d}",
domain="mpc",
family="unconstrained_scalar_mpc_horizon2",
task_type="numerical",
difficulty="intermediate",
template_id=f"mpc_unconstrained_prompt_{index%4}",
prompt=prompt,
answer=answer,
ground_truth={
"kind": "mpc_unconstrained",
"a": a,
"b": b,
"x0": x0,
"r": r,
"F": F.tolist(),
"G": G.tolist(),
"U": U.tolist(),
"X": X.tolist(),
"cost": cost,
},
source_refs=MPC,
verifier="verify_mpc_unconstrained",
)
def mpc_box_qp(rng: np.random.Generator, index: int) -> dict:
del rng
x0 = -2.5 + 0.13 * index
r = 0.08 + 0.004 * index
limit = 0.35 + 0.015 * (index % 10)
u = cp.Variable(2)
x1 = x0 + u[0]
x2 = x1 + u[1]
objective = cp.Minimize(cp.square(x1) + cp.square(x2) + r * cp.sum_squares(u))
constraints = [u >= -limit, u <= limit]
problem = cp.Problem(objective, constraints)
value = problem.solve(solver="CLARABEL")
if problem.status not in {"optimal", "optimal_inaccurate"}:
raise RuntimeError(problem.status)
U = np.asarray(u.value)
X = np.array([x0 + U[0], x0 + U[0] + U[1]])
active = [abs(abs(float(v)) - limit) < 1e-6 for v in U]
prompt = (
f"{OPENERS[index%4]} Solve the horizon-2 MPC QP x1=x0+u0, x2=x1+u1, x0={n(x0)}, minimize x1^2+x2^2+{n(r)}(u0^2+u1^2), "
f"subject to |u0|,|u1| <= {n(limit)}. Report U*, X*, active bounds, and cost."
)
answer = (
f"### 1. Constrained Quadratic Program (QP) Formulation\n"
f"$$\\min_{{u_0, u_1}} (x_0 + u_0)^2 + (x_0 + u_0 + u_1)^2 + {n(r)}(u_0^2 + u_1^2) \\quad \\text{{s.t.}} \\quad -{n(limit)} \\le u_0, u_1 \\le {n(limit)}$$\n\n"
f"### 2. Numerical QP Solution\n"
f"- Optimal control trajectory: $U^* = {mat(U)}$\n"
f"- Optimal state trajectory: $X^* = {mat(X)}$\n"
f"- Active constraints flag ($|u_k| = {n(limit)}$): {active}\n"
f"- Optimal cost: $J^* = {n(float(value))}$\n\n"
f"**Summary:** The convex QP gives U* = {mat(U)}, X* = {mat(X)}, active-input flags = {active}, and J* = {n(float(value))}."
)
return make_record(
record_id=f"mpc_box_qp_{index:05d}",
domain="mpc",
family="box_constrained_scalar_mpc_horizon2",
task_type="numerical",
difficulty="advanced",
template_id=f"mpc_box_qp_prompt_{index%4}",
prompt=prompt,
answer=answer,
ground_truth={
"kind": "mpc_box_qp",
"x0": x0,
"r": r,
"limit": limit,
"U": U.tolist(),
"X": X.tolist(),
"active": active,
"cost": float(value),
},
source_refs=MPC,
verifier="verify_mpc_box_qp",
tool="cvxpy_clarabel",
)
FAMILIES: tuple[tuple[str, Callable[[np.random.Generator, int], dict]], ...] = (
("first_order_exact_step_metrics", first_order_step),
("underdamped_second_order_exact_metrics", second_order_transient),
("unity_feedback_closed_loop_polynomial", closed_loop_tf),
("complex_sensitivity_complementary_sensitivity", sensitivity_complex),
("lead_compensator_maximum_phase_parameters", lead_parameters),
("pid_closed_loop_characteristic_polynomial", pid_characteristic),
("routh_quartic_numeric_classification", routh_quartic),
("root_locus_real_axis_membership", root_locus_point),
("classical_margins_from_crossover_values", margins_from_crossovers),
("scalar_kalman_time_update", scalar_kf_predict),
("matrix_kalman_measurement_update_2state", matrix_kf_update),
("linear_covariance_time_propagation", covariance_propagation),
("continuous_matrix_lqr_care", continuous_lqr_matrix),
("discrete_matrix_lqr_dare", discrete_lqr_matrix),
("finite_horizon_scalar_lqr_recursion", finite_horizon_lqr),
("scalar_mpc_prediction_matrices_horizon3", mpc_prediction_matrices),
("unconstrained_scalar_mpc_horizon2", mpc_unconstrained),
("box_constrained_scalar_mpc_horizon2", mpc_box_qp),
)
def generate_classical_optimal_v1(count_per_family: int, seed: int) -> list[dict]:
rng = np.random.default_rng(seed)
return [
generator(rng, index)
for _, generator in FAMILIES
for index in range(1, count_per_family + 1)
]