Spaces:
Running on Zero
Running on Zero
| """Generate verified SFT examples for continuous-time LTI stability.""" | |
| from __future__ import annotations | |
| import argparse | |
| import json | |
| from pathlib import Path | |
| import numpy as np | |
| SYSTEM_PROMPT = ( | |
| "You are a concise control-systems engineering assistant. " | |
| "State assumptions, show the decisive calculation, and provide executable code " | |
| "when requested." | |
| ) | |
| def format_number(value: complex) -> str: | |
| """Format a real or complex eigenvalue without numerical noise.""" | |
| real = float(np.real(value)) | |
| imag = float(np.imag(value)) | |
| if abs(imag) < 1e-10: | |
| return f"{real:.6g}" | |
| sign = "+" if imag >= 0 else "-" | |
| return f"{real:.6g} {sign} {abs(imag):.6g}j" | |
| def make_record(index: int, matrix: np.ndarray, seed: int) -> dict: | |
| eigenvalues = np.linalg.eigvals(matrix) | |
| stable = bool(np.all(np.real(eigenvalues) < 0.0)) | |
| matrix_text = repr(matrix.tolist()) | |
| eigenvalue_text = ", ".join(format_number(value) for value in eigenvalues) | |
| conclusion = ( | |
| "All eigenvalues have strictly negative real parts; therefore, the origin " | |
| "is asymptotically stable" | |
| if stable | |
| else "At least one eigenvalue has a positive real part; therefore, the origin " | |
| "is unstable" | |
| ) | |
| prompt = ( | |
| "For the continuous-time autonomous LTI system x_dot = A x with " | |
| f"A = {matrix_text}, determine asymptotic stability from the eigenvalues. " | |
| "Provide minimal NumPy code that verifies the conclusion." | |
| ) | |
| answer = ( | |
| f"The eigenvalues are {eigenvalue_text}. {conclusion}.\n\n" | |
| "```python\n" | |
| "import numpy as np\n\n" | |
| f"A = np.array({matrix_text}, dtype=float)\n" | |
| "eigenvalues = np.linalg.eigvals(A)\n" | |
| "stable = np.all(np.real(eigenvalues) < 0.0)\n" | |
| "print(eigenvalues)\n" | |
| "print(\"asymptotically stable:\", stable)\n" | |
| "```" | |
| ) | |
| return { | |
| "messages": [ | |
| {"role": "system", "content": SYSTEM_PROMPT}, | |
| {"role": "user", "content": prompt}, | |
| {"role": "assistant", "content": answer}, | |
| ], | |
| "metadata": { | |
| "id": f"lti_eigenvalue_stability_{index:04d}", | |
| "domain": "state_space", | |
| "family": "continuous_lti_eigenvalue_stability", | |
| "tool": "python_numpy", | |
| "source_type": "programmatically_generated", | |
| "generator": "scripts/generate_stability_sft.py", | |
| "seed": seed, | |
| "verification": ["numpy.linalg.eigvals"], | |
| "ground_truth": { | |
| "A": matrix.tolist(), | |
| "eigenvalues": [ | |
| [float(np.real(value)), float(np.imag(value))] | |
| for value in eigenvalues | |
| ], | |
| "asymptotically_stable": stable, | |
| }, | |
| "status": "draft", | |
| }, | |
| } | |
| def generate(count: int, seed: int) -> list[dict]: | |
| """Generate a roughly balanced set of stable and unstable examples.""" | |
| rng = np.random.default_rng(seed) | |
| stable_target = count // 2 | |
| unstable_target = count - stable_target | |
| stable_count = 0 | |
| unstable_count = 0 | |
| seen: set[tuple[int, ...]] = set() | |
| records: list[dict] = [] | |
| while len(records) < count: | |
| integer_matrix = rng.integers(-4, 5, size=(2, 2)) | |
| key = tuple(int(value) for value in integer_matrix.flat) | |
| if key in seen: | |
| continue | |
| seen.add(key) | |
| matrix = integer_matrix.astype(float) | |
| eigenvalues = np.linalg.eigvals(matrix) | |
| real_parts = np.real(eigenvalues) | |
| # Avoid marginal or numerically ambiguous examples in this first dataset. | |
| if np.any(np.abs(real_parts) < 0.25): | |
| continue | |
| stable = bool(np.all(real_parts < 0.0)) | |
| if stable and stable_count >= stable_target: | |
| continue | |
| if not stable and unstable_count >= unstable_target: | |
| continue | |
| records.append(make_record(len(records) + 1, matrix, seed)) | |
| stable_count += int(stable) | |
| unstable_count += int(not stable) | |
| return records | |
| def main() -> None: | |
| parser = argparse.ArgumentParser() | |
| parser.add_argument("--count", type=int, default=6) | |
| parser.add_argument("--seed", type=int, default=42) | |
| parser.add_argument( | |
| "--output", | |
| type=Path, | |
| default=Path("data/sft_draft/generated_stability.jsonl"), | |
| ) | |
| args = parser.parse_args() | |
| if args.count < 2: | |
| parser.error("--count must be at least 2") | |
| records = generate(args.count, args.seed) | |
| args.output.parent.mkdir(parents=True, exist_ok=True) | |
| with args.output.open("w", encoding="utf-8") as handle: | |
| for record in records: | |
| handle.write(json.dumps(record, ensure_ascii=False) + "\n") | |
| stable_count = sum( | |
| record["metadata"]["ground_truth"]["asymptotically_stable"] | |
| for record in records | |
| ) | |
| print(f"wrote: {args.output}") | |
| print(f"records: {len(records)}") | |
| print(f"stable: {stable_count}") | |
| print(f"unstable: {len(records) - stable_count}") | |
| if __name__ == "__main__": | |
| main() | |