Source code for avl_aero_tables.aero_filewrite

"""Convert list[StResult] into structured aero lookup tables."""

from __future__ import annotations

from dataclasses import dataclass, field
from datetime import date as _date
from typing import TYPE_CHECKING

import numpy as np

from avl_aero_tables.avl_fileread import StResult

if TYPE_CHECKING:
    import pandas as pd

COEF_NAMES = ("CLtot", "CYtot", "CDtot", "Cltot", "Cmtot", "Cntot")
REF_FIELDS = ("Sref", "Cref", "Bref", "Xref", "Yref", "Zref")

# Stability-axis derivatives — keys exactly as written in AVL .st files
STAB_DERIV_ALPHA_BETA = (
    "CLa",
    "CLb",
    "CYa",
    "CYb",
    "CDa",
    "CDb",
    "Cla",
    "Clb",
    "Cma",
    "Cmb",
    "Cna",
    "Cnb",
)
STAB_DERIV_RATES = (
    "CLp",
    "CLq",
    "CLr",
    "CYp",
    "CYq",
    "CYr",
    "CDp",
    "CDq",
    "CDr",
    "Clp",
    "Clq",
    "Clr",
    "Cmp",
    "Cmq",
    "Cmr",
    "Cnp",
    "Cnq",
    "Cnr",
)
STAB_DERIV_NAMES = STAB_DERIV_ALPHA_BETA + STAB_DERIV_RATES  # 30 total

# Control-derivative prefixes — AVL writes CLd01, CYd01, CDd01, Cld01, Cmd01, Cnd01
# (no "tot" suffix — these are linearised ∂coef/∂δ, not integrated totals)
CTRL_DERIV_COEFS = ("CL", "CY", "CD", "Cl", "Cm", "Cn")


[docs] @dataclass class StabTable: """2-D coefficient lookup table indexed by (alpha, beta). Populated only for runs where all control surfaces are at neutral (0 deg). """ coef: str alpha: np.ndarray # shape (n_alpha,), sorted unique beta: np.ndarray # shape (n_beta,), sorted unique data: np.ndarray # shape (n_alpha, n_beta), NaN where unfilled
[docs] @dataclass class CtrlTable: """3-D coefficient lookup table indexed by (alpha, beta, deflection).""" coef: str surface: str # e.g. "d01_flap" ctrl_name: str # e.g. "flap" alpha: np.ndarray beta: np.ndarray defl: np.ndarray # shape (n_defl,), sorted unique deflections data: np.ndarray # shape (n_alpha, n_beta, n_defl), NaN where unfilled
[docs] @dataclass class AeroDatabase: """Aero coefficient tables built from a sweep of AVL .st results. total_stab[coef] → StabTable total coefficients, neutral-control, indexed α×β total_ctrl[coef_surface] → CtrlTable total coefficients, all deflections, indexed α×β×δ stab_deriv[key] → StabTable stability derivatives (CLa, CLb, CLp, …, Cnr), neutral-control only, indexed α×β ctrl_deriv[key] → StabTable control derivatives (CL_d01_flap, …), neutral-control only, indexed α×β """ date: str Sref: float = 0.0 Cref: float = 0.0 Bref: float = 0.0 Xref: float = 0.0 Yref: float = 0.0 Zref: float = 0.0 total_stab: dict[str, StabTable] = field(default_factory=dict) total_ctrl: dict[str, CtrlTable] = field(default_factory=dict) stab_deriv: dict[str, StabTable] = field(default_factory=dict) ctrl_deriv: dict[str, StabTable] = field(default_factory=dict)
def _sorted_unique(vals: list[float]) -> np.ndarray: return np.array(sorted({round(v, 8) for v in vals})) def _find_idx(arr: np.ndarray, val: float, atol: float = 1e-6) -> int: i = int(np.searchsorted(arr, val)) if i < len(arr) and abs(arr[i] - val) <= atol: return i if i > 0 and abs(arr[i - 1] - val) <= atol: return i - 1 raise ValueError(f"{val!r} not in breakpoints {arr}")
[docs] def aero_filewrite(results: list[StResult]) -> AeroDatabase: """Pivot list[StResult] into an AeroDatabase of (alpha × beta [× defl]) tables. Stability tables are populated only for neutral-control runs (all deflections zero). Control tables are populated for all runs regardless of deflection. Parameters ---------- results: Output from avl_sweep.run() or st_fileread(). Returns ------- AeroDatabase Structured lookup tables keyed by coefficient name (and surface name for control tables). Example ------- >>> import tempfile >>> from avl_aero_tables import avl_sweep >>> from avl_aero_tables.aero_filewrite import aero_filewrite >>> with tempfile.TemporaryDirectory() as tmp: ... results = avl_sweep( ... "examples/bd/bd.avl", alpha=[-5, 0, 5, 10], beta=[0], out_dir=tmp ... ) >>> db = aero_filewrite(results) >>> db.total_stab["CLtot"].data.shape (4, 1) >>> list(db.total_stab) ['CLtot', 'CYtot', 'CDtot', 'Cltot', 'Cmtot', 'Cntot'] """ if not results: raise ValueError("results is empty") r0 = results[0] for r in results: for key in ("Alpha", "Beta"): if key not in r.data: raise ValueError( f"StResult from {r.filename!r} is missing {key!r} — " "was the .st file parsed correctly?" ) alpha_arr = _sorted_unique([r.data["Alpha"] for r in results]) beta_arr = _sorted_unique([r.data["Beta"] for r in results]) ctrl_map: dict[str, str] = dict(r0.controls) # {d01: flap, d02: aileron, …} surface_defls: dict[str, np.ndarray] = { d_idx: _sorted_unique([r.data.get(ctrl_name, 0.0) for r in results]) for d_idx, ctrl_name in ctrl_map.items() } db = AeroDatabase(date=str(_date.today())) for fld in REF_FIELDS: if fld in r0.data: setattr(db, fld, r0.data[fld]) for coef in COEF_NAMES: db.total_stab[coef] = StabTable( coef=coef, alpha=alpha_arr, beta=beta_arr, data=np.full((len(alpha_arr), len(beta_arr)), np.nan), ) for d_idx, ctrl_name in ctrl_map.items(): surf_key = f"{d_idx}_{ctrl_name}" defl_arr = surface_defls[d_idx] for coef in COEF_NAMES: db.total_ctrl[f"{coef}_{surf_key}"] = CtrlTable( coef=coef, surface=surf_key, ctrl_name=ctrl_name, alpha=alpha_arr, beta=beta_arr, defl=defl_arr, data=np.full((len(alpha_arr), len(beta_arr), len(defl_arr)), np.nan), ) for key in STAB_DERIV_NAMES: db.stab_deriv[key] = StabTable( coef=key, alpha=alpha_arr, beta=beta_arr, data=np.full((len(alpha_arr), len(beta_arr)), np.nan), ) for d_idx, ctrl_name in ctrl_map.items(): for coef in CTRL_DERIV_COEFS: avl_key = f"{coef}{d_idx}" # "CLd01" — key in StResult.data db_key = f"{coef}_{d_idx}_{ctrl_name}" # "CL_d01_flap" — dict key db.ctrl_deriv[db_key] = StabTable( coef=avl_key, alpha=alpha_arr, beta=beta_arr, data=np.full((len(alpha_arr), len(beta_arr)), np.nan), ) for r in results: ai = _find_idx(alpha_arr, r.data["Alpha"]) bi = _find_idx(beta_arr, r.data["Beta"]) all_neutral = all( abs(r.data.get(name, 0.0)) < 1e-6 for name in ctrl_map.values() ) for coef in COEF_NAMES: val = r.data.get(coef, np.nan) if all_neutral: db.total_stab[coef].data[ai, bi] = val for d_idx, ctrl_name in ctrl_map.items(): surf_key = f"{d_idx}_{ctrl_name}" defl_val = r.data.get(ctrl_name, 0.0) di = _find_idx(surface_defls[d_idx], defl_val) db.total_ctrl[f"{coef}_{surf_key}"].data[ai, bi, di] = val if all_neutral: for key in STAB_DERIV_NAMES: db.stab_deriv[key].data[ai, bi] = r.data.get(key, np.nan) for d_idx, ctrl_name in ctrl_map.items(): for coef in CTRL_DERIV_COEFS: avl_key = f"{coef}{d_idx}" db_key = f"{coef}_{d_idx}_{ctrl_name}" db.ctrl_deriv[db_key].data[ai, bi] = r.data.get(avl_key, np.nan) return db
def _is_neutral(r: StResult) -> bool: return all(abs(r.data.get(name, 0.0)) < 1e-6 for name in r.controls.values())
[docs] def stab_deriv_to_dataframe(results: list[StResult]) -> "pd.DataFrame": """Return a DataFrame of stability derivatives for neutral-control cases only. Columns: filename, Alpha, Beta, CLa, CLb, …, Cnr (30 derivative columns). One row per neutral-control (α, β) point. """ import pandas as pd rows = [] for r in results: if not _is_neutral(r): continue row: dict[str, object] = { "filename": r.filename, "Alpha": r.data.get("Alpha"), "Beta": r.data.get("Beta"), } for key in STAB_DERIV_NAMES: row[key] = r.data.get(key, float("nan")) rows.append(row) return pd.DataFrame(rows)
[docs] def ctrl_deriv_to_dataframe(results: list[StResult]) -> "pd.DataFrame": """Return a DataFrame of control derivatives for neutral-control cases only. Columns: filename, Alpha, Beta, CLd01, CYd01, …, Cnd{n} (6 × n_surfaces columns). One row per neutral-control (α, β) point. AVL notation: CLd01 = ∂CL/∂δ_surface1, no "tot" suffix. """ import pandas as pd ctrl_map = dict(results[0].controls) if results else {} rows = [] for r in results: if not _is_neutral(r): continue row: dict[str, object] = { "filename": r.filename, "Alpha": r.data.get("Alpha"), "Beta": r.data.get("Beta"), } for d_idx in ctrl_map: for coef in CTRL_DERIV_COEFS: avl_key = f"{coef}{d_idx}" # e.g. "CLd01" row[avl_key] = r.data.get(avl_key, float("nan")) rows.append(row) return pd.DataFrame(rows)
[docs] def results_to_dataframe(results: list[StResult]) -> "pd.DataFrame": """Convert a list of StResult to a pandas DataFrame (one row per case). Each row contains the filename plus every key from StResult.data (Alpha, Beta, CLtot, control deflections, stability derivatives, etc.). This is the recommended format for saving results to CSV, HDF5, Parquet, or any other tabular format. Example ------- >>> import tempfile >>> from avl_aero_tables import avl_sweep >>> from avl_aero_tables.aero_filewrite import results_to_dataframe >>> with tempfile.TemporaryDirectory() as tmp: ... results = avl_sweep( ... "examples/bd/bd.avl", alpha=[0, 5], beta=[0], out_dir=tmp ... ) >>> df = results_to_dataframe(results) >>> "Alpha" in df.columns and "CLtot" in df.columns True >>> df.to_csv("tests/sweep.csv", index=False) """ import pandas as pd rows = [{"filename": r.filename, **r.data} for r in results] return pd.DataFrame(rows)