airfRANS-model-exploration/tests/test_training_loop.py

156 lines
4.9 KiB
Python
Raw Normal View History

2026-07-21 08:32:30 +00:00
from __future__ import annotations
import json
import subprocess
import sys
import tempfile
import unittest
from pathlib import Path
from unittest.mock import patch
from airfrans_frontier.runtime import remove_pythonpath_entries
remove_pythonpath_entries()
import numpy as np
import torch
from airfrans_frontier.training.config import load_training_config
from airfrans_frontier.training.loop import select_device
FEATURE_NAMES = np.array(["re_norm", "aoa_norm", "x", "y", "sdf"])
TARGET_NAMES = np.array(["velocity_x", "velocity_y", "pressure", "turbulent_viscosity"])
def write_toy_simulator_dataset(root: Path, *, cases: int = 4, points: int = 64) -> None:
root.mkdir(parents=True)
rng = np.random.default_rng(123)
for case_index in range(cases):
re_norm = np.full(points, -0.5 + 0.25 * case_index, dtype=np.float32)
aoa_norm = np.full(points, -0.2 + 0.15 * case_index, dtype=np.float32)
x = rng.uniform(-1.0, 1.0, size=points).astype(np.float32)
y = rng.uniform(-1.0, 1.0, size=points).astype(np.float32)
sdf = (np.sqrt(x * x + y * y) - 0.5).astype(np.float32)
features = np.stack([re_norm, aoa_norm, x, y, sdf], axis=1).astype(np.float32)
targets = np.stack(
[
0.5 * x + 0.2 * y + 0.1 * aoa_norm,
-0.3 * x + 0.7 * sdf,
x * y + 0.05 * re_norm,
sdf**2 + 0.1 * y,
],
axis=1,
).astype(np.float32)
np.savez(
root / f"case_{case_index:02d}.npz",
features=features,
targets=targets,
feature_names=FEATURE_NAMES,
target_names=TARGET_NAMES,
)
def write_training_config(
path: Path,
*,
data_root: Path,
artifact_dir: Path,
device_type: str,
allow_cpu_fallback: bool = False,
) -> None:
path.write_text(
f"""
[run]
name = "test_mlp"
seed = 0
artifact_dir = "{artifact_dir}"
[data]
root = "{data_root}"
train_cases = 2
val_cases = 1
test_cases = 1
points_per_case = 64
batch_size = 64
[model]
type = "mlp"
hidden_width = 128
depth = 4
activation = "gelu"
[optim]
lr = 0.01
weight_decay = 0.0
steps = 1000
log_interval = 250
[device]
type = "{device_type}"
allow_cpu_fallback = {str(allow_cpu_fallback).lower()}
benchmark_kernels = true
[loss]
type = "normalized_mse"
""".strip()
+ "\n"
)
class TrainingLoopTests(unittest.TestCase):
def test_cuda_config_fails_clearly_when_cuda_unavailable(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
tmp_path = Path(tmp)
config_path = tmp_path / "config.toml"
write_training_config(
config_path,
data_root=tmp_path / "data",
artifact_dir=tmp_path / "artifacts",
device_type="cuda",
)
config = load_training_config(config_path)
with patch("torch.cuda.is_available", return_value=False):
with self.assertRaisesRegex(RuntimeError, "CUDA requested"):
select_device(config)
def test_mlp_training_smoke_learns_tiny_deterministic_simulator(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
tmp_path = Path(tmp)
data_root = tmp_path / "data"
artifact_dir = tmp_path / "artifacts"
config_path = tmp_path / "config.toml"
write_toy_simulator_dataset(data_root, cases=4, points=64)
device_type = "cuda" if torch.cuda.is_available() else "cpu"
write_training_config(
config_path,
data_root=data_root,
artifact_dir=artifact_dir,
device_type=device_type,
)
result = subprocess.run(
[sys.executable, "-m", "airfrans_frontier.cli", "train", str(config_path)],
text=True,
capture_output=True,
check=False,
)
self.assertEqual(result.returncode, 0, msg=f"stdout={result.stdout}\nstderr={result.stderr}")
run_dir_line = next(line for line in result.stdout.splitlines() if line.startswith("run_dir: "))
run_dir = Path(run_dir_line.removeprefix("run_dir: "))
final_metrics = json.loads((run_dir / "final_metrics.json").read_text())
self.assertTrue(np.isfinite(final_metrics["train_loss"]))
self.assertLess(final_metrics["train_loss"], final_metrics["initial_train_loss"] * 0.1)
self.assertLess(final_metrics["train_loss"], 1e-2)
self.assertTrue((run_dir / "checkpoint.pt").exists())
self.assertTrue((run_dir / "metrics.jsonl").exists())
self.assertEqual(final_metrics["device"].startswith("cuda"), torch.cuda.is_available())
if torch.cuda.is_available():
self.assertIn("T550", final_metrics["gpu_name"])
if __name__ == "__main__":
unittest.main()