core.py•2.13 kB
"""Core helpers for running XFOIL."""
from __future__ import annotations
import csv
import os
import subprocess
import tempfile
from collections.abc import Iterable
from pathlib import Path
from .models import PolarRequest, PolarResponse
XFOIL_BIN = os.environ.get("XFOIL_BIN", "xfoil")
def compute_polar(request: PolarRequest) -> PolarResponse:
"""Run XFOIL with the provided parameters and return a CSV payload."""
script, polar_path = _prepare_script(request)
try:
result = subprocess.run(
[XFOIL_BIN],
input=script.encode("utf-8"),
cwd=polar_path.parent,
check=False,
capture_output=True,
)
except FileNotFoundError as exc: # pragma: no cover
raise RuntimeError("XFOIL binary not found") from exc
if result.returncode != 0:
raise RuntimeError(
f"XFOIL execution failed: {result.stderr.decode('utf-8', errors='ignore')}"
)
if not polar_path.exists():
raise RuntimeError("XFOIL did not create the polar output")
rows = list(csv.reader(polar_path.read_text(encoding="utf-8").splitlines()))
if rows and rows[0] and "alpha" not in rows[0][0].lower():
rows.insert(0, ["alpha", "CL", "CD", "CM"])
csv_text = "\n".join(",".join(row) for row in rows)
return PolarResponse(csv=csv_text)
def _prepare_script(request: PolarRequest) -> tuple[str, Path]:
tmpdir = Path(tempfile.mkdtemp(prefix="xfoil_mcp_"))
airfoil_path = tmpdir / f"{request.airfoil_name}.dat"
polar_path = tmpdir / "polar.txt"
airfoil_path.write_text(request.airfoil_data, encoding="utf-8")
commands: Iterable[str] = [
f"LOAD {airfoil_path}",
"OPER",
f"VISC {request.reynolds}",
f"MACH {request.mach}",
f"ITER {request.iterations}",
"PACC",
str(polar_path),
"",
]
alpha_cmds = [f"ALFA {alpha}" for alpha in request.alphas]
footer = ["PACC", "", "QUIT"]
script = "\n".join([*commands, *alpha_cmds, *footer]) + "\n"
return script, polar_path
__all__ = ["compute_polar", "XFOIL_BIN"]