Spaces:
Running on Zero
Running on Zero
| """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) | |
| ] | |