atakan
feat: Initial open-source release of ControlAI
9936912
Raw
History Blame Contribute Delete
32.5 kB
"""Family-diverse, solver-backed linear/control SFT generators with Chain of Thought (CoT)."""
from __future__ import annotations
import math
from collections.abc import Callable
import numpy as np
from scipy import linalg
from controlai_data.schema import make_record
SOURCE_LINEAR = ["dahleh_dahleh_verghese_dynamic_systems_control"]
SOURCE_FOUNDATIONS = ["stanford_ee263_course_reader"]
SOURCE_FEEDBACK = ["astrom_murray_feedback_systems_1e"]
SOURCE_OPT = ["boyd_lmi_system_control"]
def n(value: float) -> str:
if abs(value) < 5e-11:
value = 0.0
return f"{value:.6g}"
def c(value: complex) -> str:
if abs(value.imag) < 1e-9:
return n(float(value.real))
return f"{n(float(value.real))} {'+' if value.imag >= 0 else '-'} {n(abs(float(value.imag)))}j"
def mat(value: np.ndarray) -> str:
return repr(np.asarray(value, dtype=float).tolist())
def ordered_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]
PROMPT_OPENERS = (
"Compute the requested quantities and justify the conclusion.",
"Work this control problem from the stated data; show the decisive calculation.",
"Give a compact engineering analysis with the numerical result first.",
"Check the following system without introducing unstated parameters.",
)
def triangular_stability(
rng: np.random.Generator, index: int, discrete: bool
) -> dict:
if discrete:
diagonal_values = [-1.25, -0.8, -0.3, 0.2, 0.65, 0.92, 1.0, 1.15]
else:
diagonal_values = [-4.0, -2.0, -0.75, -0.1, 0.4, 1.5]
diagonal = np.array(
[diagonal_values[(index * step + step) % len(diagonal_values)] for step in (1, 3, 5)],
dtype=float,
)
A = np.diag(diagonal)
A[0, 1] = int(rng.integers(-3, 4))
A[0, 2] = int(rng.integers(-3, 4))
A[1, 2] = int(rng.integers(-3, 4))
poles = np.linalg.eigvals(A)
stable = bool(
np.all(np.abs(poles) < 1) if discrete else np.all(np.real(poles) < 0)
)
criterion = "|lambda_i| < 1" if discrete else "Re(lambda_i) < 0"
family = "discrete_triangular_eigenvalue_stability" if discrete else "continuous_triangular_eigenvalue_stability"
conclusion = "asymptotically stable" if stable else "not asymptotically stable"
time_domain_str = "discrete-time x[k+1] = A x" if discrete else "continuous-time x_dot = A x"
prompt = (
f"{PROMPT_OPENERS[index % len(PROMPT_OPENERS)]} For the "
f"{time_domain_str} system, "
f"A = {mat(A)}. Determine its poles and asymptotic stability."
)
pole_details = "\n".join(
f"- lambda_{idx+1} = {c(p)}: {'|lambda| = ' + n(abs(p)) if discrete else 'Re(lambda) = ' + n(p.real)} "
f"({'satisfies' if (abs(p) < 1 if discrete else p.real < 0) else 'violates'} {criterion})"
for idx, p in enumerate(poles)
)
answer = (
f"### 1. Eigenvalue Calculation\n"
f"Because matrix $A$ is upper triangular, its eigenvalues (poles) are identically its diagonal entries:\n"
f"$$\\lambda(A) = \\text{{diag}}(A) = \\{{{', '.join(c(x) for x in poles)}\\}}$$\n\n"
f"### 2. Stability Criterion Evaluation ({'Discrete-Time' if discrete else 'Continuous-Time'})\n"
f"For a {'discrete-time' if discrete else 'continuous-time'} linear system, asymptotic stability requires **{criterion}** for every pole:\n"
f"{pole_details}\n\n"
f"### 3. Conclusion\n"
f"Result: the poles are {', '.join(c(x) for x in poles)}, so the system is **{conclusion}**."
)
gt = {
"kind": "eigenvalue_stability",
"time_domain": "discrete" if discrete else "continuous",
"A": A.tolist(),
"eigenvalues": ordered_roots(poles),
"stable": stable,
}
return make_record(
record_id=f"{family}_{index:05d}",
domain="sampled_data" if discrete else "linear_systems",
family=family,
task_type="numerical",
difficulty="foundation",
template_id=f"{family}_prompt_{index % 4}",
prompt=prompt,
answer=answer,
ground_truth=gt,
source_refs=SOURCE_LINEAR,
verifier="verify_eigenvalue_stability",
)
def diagonal_transition(rng: np.random.Generator, index: int) -> dict:
del rng
combinations = [
(-0.25 * first, -0.3 * second, time)
for first in range(1, 6)
for second in range(1, 6)
for time in (0.2, 0.5, 1.0, 1.5)
]
first, second, t = combinations[index - 1]
values = np.array([first, second], dtype=float)
A = np.diag(values)
Phi = np.diag(np.exp(values * t))
prompt = (
f"{PROMPT_OPENERS[index % 4]} For x_dot = A x with A = {mat(A)}, calculate the state-transition "
f"matrix Phi(t) = exp(A t) at t = {n(t)} s."
)
answer = (
f"### 1. Matrix Exponential for Diagonal Matrix\n"
f"For a diagonal system matrix $A = \\text{{diag}}({n(first)}, {n(second)})$, the matrix exponential is obtained by exponentiating each diagonal entry independently:\n"
f"$$\\Phi(t) = \\exp(A t) = \\begin{{bmatrix}} \\exp({n(first)} t) & 0 \\\\ 0 & \\exp({n(second)} t) \\end{{bmatrix}}$$\n\n"
f"### 2. Numerical Evaluation at $t = {n(t)}$ s\n"
f"- First diagonal entry: $\\exp({n(first)} \\times {n(t)}) = \\exp({n(first * t)}) = {n(math.exp(first * t))}$\n"
f"- Second diagonal entry: $\\exp({n(second)} \\times {n(t)}) = \\exp({n(second * t)}) = {n(math.exp(second * t))}$\n\n"
f"### 3. Result\n"
f"$$\\Phi({n(t)}) = {mat(Phi)}$$\n"
f"Thus the state trajectory evolves as $x({n(t)}) = \\Phi({n(t)}) x(0)$."
)
return make_record(
record_id=f"diagonal_state_transition_{index:05d}",
domain="linear_systems",
family="diagonal_state_transition_exponential",
task_type="numerical",
difficulty="foundation",
template_id=f"diagonal_state_transition_prompt_{index % 4}",
prompt=prompt,
answer=answer,
ground_truth={
"kind": "matrix_exponential",
"A": A.tolist(),
"time": t,
"Phi": Phi.tolist(),
},
source_refs=SOURCE_FOUNDATIONS,
verifier="verify_matrix_exponential",
)
def jordan_transition(rng: np.random.Generator, index: int) -> dict:
del rng
combinations = [
(a, t)
for a in (
-3.0, -2.0, -1.5, -1.0, -0.5, 0.25, 0.5, 1.0,
-4.0, -0.25, 0.75, 1.5, 2.0,
)
for t in (0.1, 0.25, 0.5, 1.0, 2.0)
]
a, t = combinations[index - 1]
A = np.array([[a, 1.0], [0.0, a]])
exp_at = math.exp(a * t)
Phi = exp_at * np.array([[1.0, t], [0.0, 1.0]])
prompt = (
f"{PROMPT_OPENERS[index % 4]} Find exp(A t) at t = {n(t)} for the Jordan block A = {mat(A)}. "
"Do not diagonalize a defective matrix."
)
answer = (
f"### 1. Matrix Decomposition (Jordan Block)\n"
f"The $2 \\times 2$ defective Jordan block matrix $A$ can be decomposed as $A = {n(a)} I + N$, where:\n"
f"$$N = \\begin{{bmatrix}} 0 & 1 \\\\ 0 & 0 \\end{{bmatrix}}, \\quad N^2 = \\begin{{bmatrix}} 0 & 0 \\\\ 0 & 0 \\end{{bmatrix}}$$\n\n"
f"### 2. Series Expansion\n"
f"Because ${n(a)} I$ and $N$ commute, $\\exp(A t) = \\exp({n(a)} t I) \\exp(N t)$:\n"
f"$$\\exp(N t) = I + N t + \\frac{{1}}{{2!}} N^2 t^2 + \\cdots = \\begin{{bmatrix}} 1 & t \\\\ 0 & 1 \\end{{bmatrix}}$$\n"
f"$$\\exp(A t) = \\exp({n(a)} t) \\begin{{bmatrix}} 1 & t \\\\ 0 & 1 \\end{{bmatrix}} = \\begin{{bmatrix}} e^{{{n(a)} t}} & t e^{{{n(a)} t}} \\\\ 0 & e^{{{n(a)} t}} \\end{{bmatrix}}$$\n\n"
f"### 3. Numerical Evaluation at $t = {n(t)}$\n"
f"With $\\exp({n(a)} \\times {n(t)}) = \\exp({n(a * t)}) = {n(exp_at)}$:\n"
f"$$\\Phi({n(t)}) = {mat(Phi)}$$"
)
return make_record(
record_id=f"jordan_state_transition_{index:05d}",
domain="linear_systems",
family="jordan_block_state_transition",
task_type="derivation",
difficulty="intermediate",
template_id=f"jordan_state_transition_prompt_{index % 4}",
prompt=prompt,
answer=answer,
ground_truth={
"kind": "matrix_exponential",
"A": A.tolist(),
"time": t,
"Phi": Phi.tolist(),
},
source_refs=SOURCE_FOUNDATIONS,
verifier="verify_matrix_exponential",
)
def diagonal_zoh(rng: np.random.Generator, index: int) -> dict:
del rng
p1 = -0.5 * (1 + index % 4)
p2 = -1.0 - 0.2 * (index % 3)
T = 0.05 + 0.01 * (index % 4)
A = np.diag([p1, p2])
B = np.array([[1.0], [2.0]])
Ad = np.diag([math.exp(p1 * T), math.exp(p2 * T)])
Bd = np.array([[(math.exp(p1 * T) - 1.0) / p1], [2.0 * (math.exp(p2 * T) - 1.0) / p2]])
prompt = (
f"{PROMPT_OPENERS[index % 4]} Discretize the diagonal plant with A = {mat(A)}, B = {mat(B)} under "
f"exact zero-order hold at Ts = {n(T)} s."
)
answer = (
f"### 1. Zero-Order Hold (ZOH) Formulation\n"
f"For sampling period $T_s = {n(T)}$ s, the exact discrete-time state-space matrices are:\n"
f"1. $A_d = \\exp(A T_s)$\n"
f"2. $B_d = \\int_0^{{T_s}} \\exp(A \\tau) B d\\tau$\n\n"
f"### 2. Analytical Integration for Diagonal Matrix\n"
f"- For $A = \\text{{diag}}({n(p1)}, {n(p2)})$, $A_d = \\text{{diag}}(\\exp({n(p1)} T_s), \\exp({n(p2)} T_s)) = {mat(Ad)}$\n"
f"- For input matrix $B = [{n(float(B[0,0]))}, {n(float(B[1,0]))}]^T$:\n"
f" $$B_{{d,1}} = \\int_0^{{{n(T)}}} e^{{{n(p1)} \\tau}} ({n(float(B[0,0]))}) d\\tau = \\frac{{{n(float(B[0,0]))}}}{{{n(p1)}}} (e^{{{n(p1 * T)}}} - 1) = {n(float(Bd[0,0]))}$$\n"
f" $$B_{{d,2}} = \\int_0^{{{n(T)}}} e^{{{n(p2)} \\tau}} ({n(float(B[1,0]))}) d\\tau = \\frac{{{n(float(B[1,0]))}}}{{{n(p2)}}} (e^{{{n(p2 * T)}}} - 1) = {n(float(Bd[1,0]))}$$\n\n"
f"### 3. Result\n"
f"$$A_d = {mat(Ad)}$$\n"
f"$$B_d = {mat(Bd)}$$"
)
return make_record(
record_id=f"diagonal_exact_zoh_{index:05d}",
domain="sampled_data",
family="diagonal_exact_zoh_discretization",
task_type="derivation",
difficulty="intermediate",
template_id=f"diagonal_exact_zoh_prompt_{index % 4}",
prompt=prompt,
answer=answer,
ground_truth={
"kind": "zoh_discretization",
"A": A.tolist(),
"B": B.tolist(),
"sample_time": T,
"Ad": Ad.tolist(),
"Bd": Bd.tolist(),
},
source_refs=SOURCE_LINEAR,
verifier="verify_zoh_discretization",
)
def double_integrator_zoh(rng: np.random.Generator, index: int) -> dict:
del rng
T = 0.015 + 0.005 * index
A = np.array([[0.0, 1.0], [0.0, 0.0]])
B = np.array([[0.0], [1.0]])
Ad = np.array([[1.0, T], [0.0, 1.0]])
Bd = np.array([[0.5 * T * T], [T]])
prompt = (
f"{PROMPT_OPENERS[index % 4]} Derive the exact ZOH model of the double integrator with A = {mat(A)}, "
f"B = {mat(B)}, and sample time {n(T)} s."
)
answer = (
f"### 1. Matrix Exponential of Nilpotent System\n"
f"For the double integrator $A = \\begin{{bmatrix}} 0 & 1 \\\\ 0 & 0 \\end{{bmatrix}}$, we have $A^2 = 0$. The Taylor series terminates:\n"
f"$$\\exp(A \\tau) = I + A \\tau = \\begin{{bmatrix}} 1 & \\tau \\\\ 0 & 1 \\end{{bmatrix}}$$\n\n"
f"### 2. State-Space ZOH Matrices\n"
f"1. Discrete state matrix at $T_s = {n(T)}$ s:\n"
f"$$A_d = \\exp(A T_s) = \\begin{{bmatrix}} 1 & {n(T)} \\\\ 0 & 1 \\end{{bmatrix}} = {mat(Ad)}$$\n\n"
f"2. Discrete input matrix:\n"
f"$$B_d = \\int_0^{{T_s}} \\exp(A \\tau) B d\\tau = \\int_0^{{{n(T)}}} \\begin{{bmatrix}} 1 & \\tau \\\\ 0 & 1 \\end{{bmatrix}} \\begin{{bmatrix}} 0 \\\\ 1 \\end{{bmatrix}} d\\tau = \\int_0^{{{n(T)}}} \\begin{{bmatrix}} \\tau \\\\ 1 \\end{{bmatrix}} d\\tau = \\begin{{bmatrix}} \\frac{{1}}{{2}} T_s^2 \\\\ T_s \\end{{bmatrix}} = {mat(Bd)}$$\n\n"
f"### 3. Result\n"
f"$$A_d = {mat(Ad)}, \\quad B_d = {mat(Bd)}$$"
)
return make_record(
record_id=f"double_integrator_zoh_{index:05d}",
domain="sampled_data",
family="double_integrator_exact_zoh",
task_type="derivation",
difficulty="intermediate",
template_id=f"double_integrator_zoh_prompt_{index % 4}",
prompt=prompt,
answer=answer,
ground_truth={
"kind": "zoh_discretization",
"A": A.tolist(),
"B": B.tolist(),
"sample_time": T,
"Ad": Ad.tolist(),
"Bd": Bd.tolist(),
},
source_refs=SOURCE_LINEAR,
verifier="verify_zoh_discretization",
)
def observability_rank(rng: np.random.Generator, index: int) -> dict:
while True:
A = rng.integers(-3, 4, size=(3, 3)).astype(float)
C = rng.integers(-2, 3, size=(1, 3)).astype(float)
if np.any(C):
break
O = np.vstack([C, C @ A, C @ A @ A])
rank = int(np.linalg.matrix_rank(O))
observable = rank == 3
prompt = (
f"{PROMPT_OPENERS[index % 4]} For x_dot = A x, y = Cx with A = {mat(A)} and C = {mat(C)}, form "
"the three-state observability matrix, compute its rank, and conclude."
)
answer = (
f"### 1. Observability Matrix Construction\n"
f"For a 3rd-order linear system ($n=3$), the observability matrix is $\\mathcal{{O}} = \\begin{{bmatrix}} C \\\\ CA \\\\ CA^2 \\end{{bmatrix}}$:\n"
f"- $C = {mat(C)}$\n"
f"- $CA = {mat(C @ A)}$\n"
f"- $CA^2 = {mat(C @ A @ A)}$\n\n"
f"Stacking the rows yields:\n"
f"$$\\mathcal{{O}} = {mat(O)}$$\n\n"
f"### 2. Rank Determination\n"
f"Evaluating the matrix rank: $\\text{{rank}}(\\mathcal{{O}}) = {rank}$.\n\n"
f"### 3. Conclusion\n"
f"Since $\\text{{rank}}(\\mathcal{{O}}) = {rank}$ {'= 3 (full state rank)' if observable else '< 3 (rank deficient)'}, "
f"the realization is **{'observable' if observable else 'not observable'}**."
)
return make_record(
record_id=f"observability_rank_3x3_{index:05d}",
domain="linear_systems",
family="observability_rank_numeric_3x3",
task_type="numerical",
difficulty="intermediate",
template_id=f"observability_rank_3x3_prompt_{index % 4}",
prompt=prompt,
answer=answer,
ground_truth={
"kind": "observability_rank",
"A": A.tolist(),
"C": C.tolist(),
"O": O.tolist(),
"rank": rank,
"observable": observable,
},
source_refs=SOURCE_LINEAR,
verifier="verify_observability_rank",
)
def pbh_observability(rng: np.random.Generator, index: int) -> dict:
del rng
poles = np.array(
[-1.0 - index % 3, -3.0 - (index * 2) % 4, -7.0 - 0.05 * index]
)
C = np.array([[1.0, 0.0 if index % 3 == 0 else 1.0, 0.0 if index % 4 == 0 else 2.0]])
A = np.diag(poles)
pbh_ranks = []
for pole in poles:
pbh_ranks.append(int(np.linalg.matrix_rank(np.vstack([pole * np.eye(3) - A, C]))))
observable = all(rank == 3 for rank in pbh_ranks)
prompt = (
f"{PROMPT_OPENERS[index % 4]} Apply the PBH observability test to A = {mat(A)}, C = {mat(C)}. "
"Report the rank at each distinct eigenvalue and the conclusion."
)
eval_steps = "\n".join(
f"- At $\\lambda = {n(p)}$: $\\text{{rank}}\\begin{{bmatrix}} {n(p)}I - A \\\\ C \\end{{bmatrix}} = {r}$ out of 3"
for p, r in zip(poles, pbh_ranks)
)
answer = (
f"### 1. PBH Observability Test Criterion\n"
f"A realization $(A, C)$ is observable iff $\\text{{rank}}\\begin{{bmatrix}} \\lambda I - A \\\\ C \\end{{bmatrix}} = n = 3$ for every eigenvalue $\\lambda \\in \\sigma(A)$.\n\n"
f"### 2. Modal Rank Checks\n"
f"{eval_steps}\n\n"
f"### 3. Conclusion\n"
f"The realization is **{'observable' if observable else 'not observable'}** because "
f"{'every eigenvalue yields full column rank 3' if observable else 'at least one eigenvalue produces rank deficiency'}."
)
return make_record(
record_id=f"pbh_observability_{index:05d}",
domain="linear_systems",
family="pbh_observability_diagonal_modes",
task_type="numerical",
difficulty="intermediate",
template_id=f"pbh_observability_prompt_{index % 4}",
prompt=prompt,
answer=answer,
ground_truth={
"kind": "pbh_observability",
"A": A.tolist(),
"C": C.tolist(),
"eigenvalues": poles.tolist(),
"pbh_ranks": pbh_ranks,
"observable": observable,
},
source_refs=SOURCE_LINEAR,
verifier="verify_pbh_observability",
)
def state_feedback_check(rng: np.random.Generator, index: int) -> dict:
A = rng.integers(-3, 4, size=(2, 2)).astype(float)
B = rng.integers(-2, 3, size=(2, 1)).astype(float)
K = rng.integers(-2, 4, size=(1, 2)).astype(float)
if not np.any(B):
B[1, 0] = 1.0
BK = B @ K
Acl = A - BK
poles = np.linalg.eigvals(Acl)
stable = bool(np.all(np.real(poles) < 0))
prompt = (
f"{PROMPT_OPENERS[index % 4]} Check the proposed state feedback u = -Kx for A = {mat(A)}, B = "
f"{mat(B)}, K = {mat(K)}. Compute A-BK and its poles; do not assume the "
"proposal stabilizes the plant."
)
answer = (
f"### 1. Closed-Loop System Matrix\n"
f"With state feedback $u = -K x$, the closed-loop dynamics are $\\dot{{x}} = (A - BK)x$:\n"
f"- $BK = {mat(B)} \\times {mat(K)} = {mat(BK)}$\n"
f"- $A_{{cl}} = A - BK = {mat(Acl)}$\n\n"
f"### 2. Eigenvalues / Closed-Loop Poles\n"
f"Solving $\\det(\\lambda I - A_{{cl}}) = 0$:\n"
f"$$\\lambda(A - BK) = \\{{{', '.join(c(x) for x in poles)}\\}}$$\n\n"
f"### 3. Stability Assessment\n"
f"Since all closed-loop poles have {'strictly negative real parts (Re(lambda) < 0)' if stable else 'non-negative real parts (Re(lambda) >= 0)'}, "
f"the proposed state feedback is **{'stabilizing' if stable else 'not stabilizing'}**."
)
return make_record(
record_id=f"state_feedback_check_{index:05d}",
domain="linear_systems",
family="proposed_state_feedback_stability_check",
task_type="critique",
difficulty="intermediate",
template_id=f"state_feedback_check_prompt_{index % 4}",
prompt=prompt,
answer=answer,
ground_truth={
"kind": "state_feedback",
"A": A.tolist(),
"B": B.tolist(),
"K": K.tolist(),
"Acl": Acl.tolist(),
"eigenvalues": ordered_roots(poles),
"stable": stable,
},
source_refs=SOURCE_LINEAR,
verifier="verify_state_feedback",
)
def observer_error_check(rng: np.random.Generator, index: int) -> dict:
A = rng.integers(-3, 4, size=(2, 2)).astype(float)
C = rng.integers(-2, 3, size=(1, 2)).astype(float)
L = rng.integers(-2, 4, size=(2, 1)).astype(float)
if not np.any(C):
C[0, 0] = 1.0
LC = L @ C
Ae = A - LC
poles = np.linalg.eigvals(Ae)
stable = bool(np.all(np.real(poles) < 0))
prompt = (
f"{PROMPT_OPENERS[index % 4]} An observer uses gain L = {mat(L)} for A = {mat(A)}, C = {mat(C)}. "
"Derive the estimation-error matrix, list its poles, and decide whether "
"the error converges exponentially."
)
answer = (
f"### 1. Observer Error Dynamics\n"
f"For estimation error $e(t) = x(t) - \\hat{{x}}(t)$, the error dynamics follow $\\dot{{e}} = (A - LC)e$:\n"
f"- $LC = {mat(L)} \\times {mat(C)} = {mat(LC)}$\n"
f"- $A_e = A - LC = {mat(Ae)}$\n\n"
f"### 2. Error Poles (Eigenvalues of A-LC)\n"
f"Solving $\\det(\\lambda I - (A - LC)) = 0$:\n"
f"$$\\lambda(A - LC) = \\{{{', '.join(c(x) for x in poles)}\\}}$$\n\n"
f"### 3. Exponential Convergence Conclusion\n"
f"Because all poles satisfy {'Re(lambda) < 0' if stable else 'Re(lambda) >= 0 for at least one mode'}, "
f"the estimation error **{'converges exponentially' if stable else 'does not converge exponentially for every initial error'}**."
)
return make_record(
record_id=f"observer_error_check_{index:05d}",
domain="estimation_filtering",
family="observer_error_dynamics_stability_check",
task_type="derivation",
difficulty="intermediate",
template_id=f"observer_error_check_prompt_{index % 4}",
prompt=prompt,
answer=answer,
ground_truth={
"kind": "observer_error",
"A": A.tolist(),
"C": C.tolist(),
"L": L.tolist(),
"Ae": Ae.tolist(),
"eigenvalues": ordered_roots(poles),
"stable": stable,
},
source_refs=SOURCE_LINEAR,
verifier="verify_observer_error",
)
def continuous_lyapunov(rng: np.random.Generator, index: int) -> dict:
del rng
a = np.array([-0.5 * (1 + index % 5), -1.0 * (1 + (index * 2) % 4)])
q = np.array([1.0 + 0.05 * index, 0.5 + (index * 3) % 5])
A = np.diag(a)
Q = np.diag(q)
P = linalg.solve_continuous_lyapunov(A.T, -Q)
residual = A.T @ P + P @ A + Q
prompt = (
f"{PROMPT_OPENERS[index % 4]} Solve A^T P + PA = -Q for A = {mat(A)} and Q = {mat(Q)}. "
"Verify positive definiteness and the residual."
)
answer = (
f"### 1. Continuous Lyapunov Equation\n"
f"The equation $A^T P + P A = -Q$ with diagonal matrices $A = \\text{{diag}}({n(a[0])}, {n(a[1])})$ and $Q = \\text{{diag}}({n(q[0])}, {n(q[1])})$ has diagonal solution $P = \\text{{diag}}(p_1, p_2)$:\n"
f"- $2 a_1 p_1 = -q_1 \\implies p_1 = -\\frac{{{n(q[0])}}}{{2({n(a[0])})}} = {n(float(P[0,0]))}$\n"
f"- $2 a_2 p_2 = -q_2 \\implies p_2 = -\\frac{{{n(q[1])}}}{{2({n(a[1])})}} = {n(float(P[1,1]))}$\n\n"
f"### 2. Solution Matrix and Definiteness\n"
f"$$P = {mat(P)}$$\n"
f"The eigenvalues of $P$ are {', '.join(n(x) for x in np.linalg.eigvalsh(P))}. Since all eigenvalues are strictly positive, $P$ is positive definite ($P > 0$).\n\n"
f"### 3. Residual Verification\n"
f"$$A^T P + PA + Q = {mat(residual)}$$ (zero within floating-point tolerance)."
)
return make_record(
record_id=f"continuous_lyapunov_equation_{index:05d}",
domain="linear_systems",
family="continuous_diagonal_lyapunov_equation",
task_type="numerical",
difficulty="intermediate",
template_id=f"continuous_lyapunov_prompt_{index % 4}",
prompt=prompt,
answer=answer,
ground_truth={
"kind": "continuous_lyapunov",
"A": A.tolist(),
"Q": Q.tolist(),
"P": P.tolist(),
"residual": residual.tolist(),
},
source_refs=SOURCE_OPT,
verifier="verify_continuous_lyapunov",
)
def discrete_lyapunov(rng: np.random.Generator, index: int) -> dict:
del rng
a = np.array([0.1 + 0.1 * (index % 5), -0.2 - 0.1 * ((index * 2) % 5)])
q = np.array([1.0 + 0.05 * index, 0.5 + (index * 2) % 4])
A = np.diag(a)
Q = np.diag(q)
P = linalg.solve_discrete_lyapunov(A.T, Q)
residual = A.T @ P @ A - P + Q
prompt = (
f"{PROMPT_OPENERS[index % 4]} Solve the discrete Lyapunov equation A^T P A - P = -Q for A = "
f"{mat(A)}, Q = {mat(Q)}. Check the residual and definiteness."
)
answer = (
f"### 1. Discrete Lyapunov Equation\n"
f"The equation $A^T P A - P = -Q$ with diagonal $A = \\text{{diag}}({n(a[0])}, {n(a[1])})$ and $Q = \\text{{diag}}({n(q[0])}, {n(q[1])})$ yields $P = \\text{{diag}}(p_1, p_2)$:\n"
f"- $(a_1^2 - 1) p_1 = -q_1 \\implies p_1 = \\frac{{{n(q[0])}}}{{1 - ({n(a[0])})^2}} = {n(float(P[0,0]))}$\n"
f"- $(a_2^2 - 1) p_2 = -q_2 \\implies p_2 = \\frac{{{n(q[1])}}}{{1 - ({n(a[1])})^2}} = {n(float(P[1,1]))}$\n\n"
f"### 2. Solution Matrix and Definiteness\n"
f"$$P = {mat(P)}$$\n"
f"Eigenvalues of $P$ are {', '.join(n(x) for x in np.linalg.eigvalsh(P))}. Since all eigenvalues are strictly positive, $P$ is positive definite ($P > 0$).\n\n"
f"### 3. Residual Verification\n"
f"$$A^T P A - P + Q = {mat(residual)}$$ (zero to numerical rounding)."
)
return make_record(
record_id=f"discrete_lyapunov_equation_{index:05d}",
domain="sampled_data",
family="discrete_diagonal_lyapunov_equation",
task_type="numerical",
difficulty="intermediate",
template_id=f"discrete_lyapunov_prompt_{index % 4}",
prompt=prompt,
answer=answer,
ground_truth={
"kind": "discrete_lyapunov",
"A": A.tolist(),
"Q": Q.tolist(),
"P": P.tolist(),
"residual": residual.tolist(),
},
source_refs=SOURCE_OPT,
verifier="verify_discrete_lyapunov",
)
def controllability_gramian(rng: np.random.Generator, index: int) -> dict:
del rng
a = np.array([-1.0 - index % 3, -2.0 - (index * 2) % 4])
B = np.array([[1.0 + 0.05 * index], [1.0 + (index * 3) % 3]])
A = np.diag(a)
W = linalg.solve_continuous_lyapunov(A, -(B @ B.T))
eig = np.linalg.eigvalsh(W)
prompt = (
f"{PROMPT_OPENERS[index % 4]} For the stable continuous-time pair A = {mat(A)}, B = {mat(B)}, "
"compute the infinite-horizon controllability Gramian and interpret its eigenvalues."
)
answer = (
f"### 1. Controllability Gramian Equation\n"
f"The infinite-horizon controllability Gramian $W_c$ satisfies the Lyapunov equation $A W_c + W_c A^T + B B^T = 0$.\n\n"
f"### 2. Solution Matrix\n"
f"$$W_c = {mat(W)}$$\n\n"
f"### 3. Eigenvalue Analysis\n"
f"Eigenvalues of $W_c$ are $\\lambda(W_c) = \\{{{', '.join(n(x) for x in eig)}\\}}.\n"
f"Since {'all eigenvalues are strictly positive' if np.all(eig > 0) else 'W_c is singular'}, "
f"the Gramian is **{'positive definite and both states are reachable' if np.all(eig > 0) else 'singular, revealing an unreachable direction'}**."
)
return make_record(
record_id=f"controllability_gramian_{index:05d}",
domain="linear_systems",
family="continuous_controllability_gramian",
task_type="numerical",
difficulty="advanced",
template_id=f"controllability_gramian_prompt_{index % 4}",
prompt=prompt,
answer=answer,
ground_truth={
"kind": "controllability_gramian",
"A": A.tolist(),
"B": B.tolist(),
"W": W.tolist(),
"eigenvalues": eig.tolist(),
},
source_refs=SOURCE_LINEAR,
verifier="verify_controllability_gramian",
)
def observability_gramian(rng: np.random.Generator, index: int) -> dict:
del rng
a = np.array([-0.5 - index % 4, -2.0 - (index * 3) % 3])
C = np.array([[1.0 + 0.05 * index, 0.0 if index % 5 == 0 else 1.0]])
A = np.diag(a)
W = linalg.solve_continuous_lyapunov(A.T, -(C.T @ C))
eig = np.linalg.eigvalsh(W)
prompt = (
f"{PROMPT_OPENERS[index % 4]} For A = {mat(A)}, C = {mat(C)}, compute the infinite-horizon "
"observability Gramian and use its eigenvalues to assess observability."
)
answer = (
f"### 1. Observability Gramian Equation\n"
f"The infinite-horizon observability Gramian $W_o$ solves $A^T W_o + W_o A + C^T C = 0$.\n\n"
f"### 2. Solution Matrix\n"
f"$$W_o = {mat(W)}$$\n\n"
f"### 3. Eigenvalue Analysis\n"
f"Eigenvalues: $\\lambda(W_o) = \\{{{', '.join(n(x) for x in eig)}\\}}.\n"
f"Therefore the pair $(A, C)$ is **{'observable' if np.all(eig > 1e-10) else 'not observable'}**."
)
return make_record(
record_id=f"observability_gramian_{index:05d}",
domain="linear_systems",
family="continuous_observability_gramian",
task_type="numerical",
difficulty="advanced",
template_id=f"observability_gramian_prompt_{index % 4}",
prompt=prompt,
answer=answer,
ground_truth={
"kind": "observability_gramian",
"A": A.tolist(),
"C": C.tolist(),
"W": W.tolist(),
"eigenvalues": eig.tolist(),
},
source_refs=SOURCE_LINEAR,
verifier="verify_observability_gramian",
)
def first_order_frequency(rng: np.random.Generator, index: int) -> dict:
del rng
gain = [0.5, 1.0, 2.0, 5.0][index % 4]
tau = [0.1, 0.25, 0.5, 1.0, 2.0][(index * 3) % 5]
omega = 0.15 + 0.075 * index
x = tau * omega
magnitude = gain / math.sqrt(1 + x * x)
phase = -math.degrees(math.atan(x))
db = 20 * math.log10(magnitude)
prompt = (
f"{PROMPT_OPENERS[index % 4]} Evaluate G(j omega) for G(s) = {n(gain)}/({n(tau)}s + 1) at "
f"omega = {n(omega)} rad/s. Report magnitude, dB magnitude, and phase."
)
answer = (
f"### 1. Frequency Response Formula\n"
f"For first-order transfer function $G(s) = \\frac{{{n(gain)}}}{{{n(tau)}s + 1}}$, substituting $s = j\\omega$ at $\\omega = {n(omega)}$ rad/s:\n"
f"$$G(j{n(omega)}) = \\frac{{{n(gain)}}}{{1 + j ({n(tau)} \\times {n(omega)})}} = \\frac{{{n(gain)}}}{{1 + j {n(x)}}}$$\n\n"
f"### 2. Magnitude and Decibels\n"
f"- Linear magnitude: $|G| = \\frac{{{n(gain)}}}{{\\sqrt{{1 + ({n(tau)} \\times {n(omega)})^2}}}} = \\frac{{{n(gain)}}}{{\\sqrt{{1 + {n(x*x)}}}}} = {n(magnitude)}$\n"
f"- Logarithmic magnitude: $20\\log_{{10}}|G| = 20\\log_{{10}}({n(magnitude)}) = {n(db)}\\text{{ dB}}$\n\n"
f"### 3. Phase\n"
f"$$\\angle G = -\\arctan(\\tau \\omega) = -\\arctan({n(x)}) = {n(phase)}^\\circ$$\n\n"
f"Result: |G| = {n(magnitude)}, 20log10|G| = {n(db)} dB, and phase = {n(phase)} degrees."
)
return make_record(
record_id=f"first_order_frequency_{index:05d}",
domain="classical_control",
family="first_order_frequency_response_with_db",
task_type="numerical",
difficulty="foundation",
template_id=f"first_order_frequency_prompt_{index % 4}",
prompt=prompt,
answer=answer,
ground_truth={
"kind": "first_order_frequency",
"gain": gain,
"tau": tau,
"omega": omega,
"magnitude": magnitude,
"magnitude_db": db,
"phase_deg": phase,
},
source_refs=SOURCE_FEEDBACK,
verifier="verify_first_order_frequency",
)
FAMILIES: tuple[tuple[str, Callable[[np.random.Generator, int], dict]], ...] = (
("continuous_triangular_eigenvalue_stability", lambda r, i: triangular_stability(r, i, False)),
("discrete_triangular_eigenvalue_stability", lambda r, i: triangular_stability(r, i, True)),
("diagonal_state_transition_exponential", diagonal_transition),
("jordan_block_state_transition", jordan_transition),
("diagonal_exact_zoh_discretization", diagonal_zoh),
("double_integrator_exact_zoh", double_integrator_zoh),
("observability_rank_numeric_3x3", observability_rank),
("pbh_observability_diagonal_modes", pbh_observability),
("proposed_state_feedback_stability_check", state_feedback_check),
("observer_error_dynamics_stability_check", observer_error_check),
("continuous_diagonal_lyapunov_equation", continuous_lyapunov),
("discrete_diagonal_lyapunov_equation", discrete_lyapunov),
("continuous_controllability_gramian", controllability_gramian),
("continuous_observability_gramian", observability_gramian),
("first_order_frequency_response_with_db", first_order_frequency),
)
def generate_linear_v1(count_per_family: int, seed: int) -> list[dict]:
rng = np.random.default_rng(seed)
records = []
for _, generator in FAMILIES:
for index in range(1, count_per_family + 1):
records.append(generator(rng, index))
return records