"""avl-aero-tables CLI entry point."""
from __future__ import annotations
import argparse
import logging
import re
import sys
import tomllib
from pathlib import Path
from avl_aero_tables._plot_config import MATHJAX_RETYPESET
from avl_aero_tables.avl_bin import verify
from avl_aero_tables.avl_config import load_config
_PYPROJECT = Path(__file__).resolve().parent.parent / "pyproject.toml"
def _package_version() -> str:
if _PYPROJECT.exists():
with _PYPROJECT.open("rb") as f:
return str(tomllib.load(f)["project"]["version"])
from importlib.metadata import version
return version("avl-aero-tables")
def _build_parser() -> argparse.ArgumentParser:
p = argparse.ArgumentParser(
prog="avl-aero-tables",
description="Python wrapper for AVL (Athena Vortex Lattice)",
epilog=(
"Use --help on any subcommand for details, e.g.:\n"
" avl-aero-tables sweep --help"
),
formatter_class=argparse.RawDescriptionHelpFormatter,
)
p.add_argument(
"--version", action="version", version=f"avl-aero-tables {_package_version()}"
)
p.add_argument(
"--quiet", "-q", action="store_true", help="Suppress progress output"
)
sub = p.add_subparsers(dest="command", required=True)
sub.add_parser("verify", help="Check that the AVL binary is installed and works")
sweep_p = sub.add_parser(
"sweep", help="Run an AVL sweep defined by a YAML project file"
)
sweep_p.add_argument("yml", type=Path, help="Path to the .yml project file")
plot_p = sub.add_parser("plot", help="Plot geometry or aero results")
plot_sub = plot_p.add_subparsers(dest="plot_command", required=True)
geom_p = plot_sub.add_parser(
"geometry", help="Four-view geometry plot from the .avl file"
)
geom_p.add_argument("yml", type=Path, help="Path to the .yml project file")
_runs_help = (
"Path to a sweep results directory, or a parent directory (latest run is used)"
)
totals_p = plot_sub.add_parser(
"totals",
help="3-D total-coefficient surfaces (CLtot, CDtot, …) from sweep results",
)
totals_p.add_argument("runs_dir", type=Path, help=_runs_help)
totals_p.add_argument(
"--beta-ref",
type=float,
default=0.0,
metavar="DEG",
help="Sideslip angle (deg) for control-surface slices (default: 0.0)",
)
stabderiv_p = plot_sub.add_parser(
"stab-deriv",
help="3-D stability-derivative surfaces (CLa, CLb, CLp, …) from sweep results",
)
stabderiv_p.add_argument("runs_dir", type=Path, help=_runs_help)
ctrlderiv_p = plot_sub.add_parser(
"ctrl-deriv",
help="3-D control-derivative surfaces (CLd01, CYd01, …) from sweep results",
)
ctrlderiv_p.add_argument("runs_dir", type=Path, help=_runs_help)
all_p = plot_sub.add_parser(
"all",
help="Generate all plots: totals, stab-deriv, and ctrl-deriv",
)
all_p.add_argument("runs_dir", type=Path, help=_runs_help)
all_p.add_argument(
"--beta-ref",
type=float,
default=0.0,
metavar="DEG",
help="Sideslip angle (deg) for control-surface slices (default: 0.0)",
)
return p
def _cmd_sweep(args: argparse.Namespace) -> int:
from avl_aero_tables.avl_sweep import run as _sweep_run
yml = args.yml.resolve()
cfg = load_config(yml)
avl_file = (yml.parent / cfg.input.geometry).resolve()
if cfg.sweep.ctrl_sweeps:
from avl_aero_tables.avl_fileread import avl_fileread
geom = avl_fileread(avl_file)
valid = set(geom.ctrl_names)
bad = [k for k in cfg.sweep.ctrl_sweeps if k not in valid]
if bad:
print(
f"ERROR: ctrl_sweeps keys not found in {avl_file.name}: {bad}\n"
f" Valid control surfaces: {sorted(valid)}",
file=sys.stderr,
)
sys.exit(1)
out_dir = yml.parent.parent / "_runs" / yml.stem
_sweep_run(
avl_file=avl_file,
alpha=cfg.sweep.alpha,
beta=cfg.sweep.beta,
ctrl_sweeps=cfg.sweep.ctrl_sweeps,
out_dir=out_dir,
out_format=cfg.output.format,
yml_file=yml,
)
return 0
def _write_index_html(directory: Path) -> Path:
"""Write index.html listing every *.html in directory (excluding itself)."""
html_files = sorted(p for p in directory.glob("*.html") if p.name != "index.html")
items = "\n".join(
f' <li><a href="{p.name}">{p.stem}</a></li>' for p in html_files
)
index = directory / "index.html"
index.write_text(
f"<!DOCTYPE html>\n"
f"<html><head><meta charset='utf-8'>"
f"<title>{directory.name}</title>"
f"<style>body{{font-family:monospace;padding:2em}}"
f"li{{margin:.4em 0}}a{{text-decoration:none}}"
f"a:hover{{text-decoration:underline}}"
f"</style></head>\n"
f"<body><h2>{directory.name}</h2><ul>\n{items}\n</ul></body></html>\n"
)
return index
def _cmd_plot_geometry(args: argparse.Namespace) -> int:
import webbrowser
from avl_aero_tables.avl_fileplot import avl_fileplot
from avl_aero_tables.avl_fileread import avl_fileread
yml = args.yml.resolve()
cfg = load_config(yml)
avl_file = (yml.parent / cfg.input.geometry).resolve()
geometry = avl_fileread(avl_file)
fig = avl_fileplot(geometry)
out = avl_file.parent / f"{avl_file.stem}_geometry.html"
fig.write_html(
str(out),
include_plotlyjs="cdn",
include_mathjax="cdn",
post_script=MATHJAX_RETYPESET,
config={"displayModeBar": True},
)
print(f"Geometry plot → {out}")
index = _write_index_html(out.parent)
webbrowser.open(index.as_uri())
return 0
_TIMESTAMP_RE = re.compile(r".*\d{4}-\d{2}-\d{2}-\d{6}$")
def _resolve_result_dir(runs_dir: Path) -> Path | None:
"""Return the timestamped run directory, or None if not found."""
is_run_dir = (
re.search(r"\d{4}-\d{2}-\d{2}-\d{6}", runs_dir.name)
or (runs_dir / ".raw").is_dir()
)
if is_run_dir:
return runs_dir
subdirs = (
sorted(
d for d in runs_dir.iterdir() if d.is_dir() and _TIMESTAMP_RE.match(d.name)
)
if runs_dir.exists()
else []
)
return subdirs[-1] if subdirs else None
def _load_aero(runs_dir: Path): # type: ignore[return]
"""Resolve runs_dir to a result directory; return (result_dir, AeroDatabase)."""
from avl_aero_tables.aero_filewrite import aero_filewrite
from avl_aero_tables.avl_fileread import st_fileread
result_dir = _resolve_result_dir(runs_dir)
if result_dir is None:
print(f"ERROR: No sweep results found in {runs_dir}.", file=sys.stderr)
return None, None
results = st_fileread(result_dir / ".raw")
return result_dir, aero_filewrite(results)
def _cmd_plot_totals(args: argparse.Namespace) -> int:
import webbrowser
from avl_aero_tables.aero_fileplot import plot_totals
result_dir, aero = _load_aero(args.runs_dir.resolve())
if aero is None:
return 1
figs = plot_totals(aero, beta_ref=args.beta_ref)
for key, fig in figs.items():
out = result_dir / f"total_{key}.html"
fig.write_html(
str(out),
include_plotlyjs="cdn",
include_mathjax="cdn",
post_script=MATHJAX_RETYPESET,
config={"displayModeBar": True},
)
print(f" → {out.name}")
index = _write_index_html(result_dir)
webbrowser.open(index.as_uri())
return 0
def _cmd_plot_stab_deriv(args: argparse.Namespace) -> int:
import webbrowser
from avl_aero_tables.aero_fileplot import plot_stab_derivs
result_dir, aero = _load_aero(args.runs_dir.resolve())
if aero is None:
return 1
figs = plot_stab_derivs(aero)
for key, fig in figs.items():
out = result_dir / f"deriv_stab_{key}.html"
fig.write_html(
str(out),
include_plotlyjs="cdn",
include_mathjax="cdn",
post_script=MATHJAX_RETYPESET,
config={"displayModeBar": True},
)
print(f" → {out.name}")
index = _write_index_html(result_dir)
webbrowser.open(index.as_uri())
return 0
def _cmd_plot_all(args: argparse.Namespace) -> int:
import webbrowser
from avl_aero_tables.aero_fileplot import plot_ctrl_derivs, plot_stab_derivs, plot_totals
result_dir, aero = _load_aero(args.runs_dir.resolve())
if aero is None:
return 1
all_figs = {}
for key, fig in plot_totals(aero, beta_ref=args.beta_ref).items():
all_figs[f"total_{key}"] = fig
for key, fig in plot_stab_derivs(aero).items():
all_figs[f"deriv_stab_{key}"] = fig
for key, fig in plot_ctrl_derivs(aero).items():
all_figs[f"deriv_ctrl_{key}"] = fig
for stem, fig in all_figs.items():
out = result_dir / f"{stem}.html"
fig.write_html(
str(out),
include_plotlyjs="cdn",
include_mathjax="cdn",
post_script=MATHJAX_RETYPESET,
config={"displayModeBar": True},
)
print(f" → {out.name}")
index = _write_index_html(result_dir)
webbrowser.open(index.as_uri())
return 0
def _cmd_plot_ctrl_deriv(args: argparse.Namespace) -> int:
import webbrowser
from avl_aero_tables.aero_fileplot import plot_ctrl_derivs
result_dir, aero = _load_aero(args.runs_dir.resolve())
if aero is None:
return 1
figs = plot_ctrl_derivs(aero)
for key, fig in figs.items():
out = result_dir / f"deriv_ctrl_{key}.html"
fig.write_html(
str(out),
include_plotlyjs="cdn",
include_mathjax="cdn",
post_script=MATHJAX_RETYPESET,
config={"displayModeBar": True},
)
print(f" → {out.name}")
index = _write_index_html(result_dir)
webbrowser.open(index.as_uri())
return 0
[docs]
def main(argv: list[str] | None = None) -> int:
"""Entry point for the ``avl-aero-tables`` CLI.
Global flags:
``--help``
Show a help message and exit (also works on every subcommand).
``--version``
Print the installed package version and exit.
Subcommands:
``verify``
Check that the AVL binary is installed and runnable.
``sweep <yml>``
Load a YAML project file and run the AVL sweep.
``plot geometry <yml>``
Four-view geometry plot from the .avl file.
``plot totals [--beta-ref DEG] <runs_dir>``
3-D total-coefficient surfaces (CLtot, CDtot, …) from sweep results.
Pass a specific timestamped run directory, or a parent directory to use
the latest run. ``--beta-ref`` slices control-surface plots at the
nearest available sideslip angle (default 0°).
``plot stab-deriv <runs_dir>``
3-D stability-derivative surfaces (CLa, CLb, CLp, …, Cnr) — one
figure per perturbation variable (α, β, p', q', r').
``plot ctrl-deriv <runs_dir>``
3-D control-derivative surfaces (CLd01, CYd01, …) — one figure per
control surface.
``plot all [--beta-ref DEG] <runs_dir>``
Generate all plots (totals, stab-deriv, ctrl-deriv) in one shot.
Example
-------
.. code-block:: shell
avl-aero-tables --help
avl-aero-tables --version
avl-aero-tables verify
avl-aero-tables sweep examples/bd/bd.yml
avl-aero-tables plot geometry examples/bd/bd.yml
avl-aero-tables plot totals _runs/bd/
avl-aero-tables plot totals --beta-ref 5 _runs/bd/
avl-aero-tables plot stab-deriv _runs/bd/
avl-aero-tables plot ctrl-deriv _runs/bd/
avl-aero-tables plot all _runs/bd/
avl-aero-tables plot all --beta-ref 5 _runs/bd/
"""
parser = _build_parser()
args = parser.parse_args(argv)
if not args.quiet:
logging.basicConfig(format="%(message)s", level=logging.INFO)
if args.command == "verify":
try:
binary = verify()
print(f"AVL binary OK: {binary}")
return 0
except (FileNotFoundError, RuntimeError) as exc:
print(f"ERROR: {exc}", file=sys.stderr)
return 1
if args.command == "sweep":
return _cmd_sweep(args)
if args.command == "plot":
if args.plot_command == "geometry":
return _cmd_plot_geometry(args)
if args.plot_command == "totals":
return _cmd_plot_totals(args)
if args.plot_command == "stab-deriv":
return _cmd_plot_stab_deriv(args)
if args.plot_command == "ctrl-deriv":
return _cmd_plot_ctrl_deriv(args)
if args.plot_command == "all":
return _cmd_plot_all(args)
return 0
if __name__ == "__main__":
sys.exit(main())