"""Command-line interface for running and inspecting panel cases."""
from __future__ import annotations
import argparse
from pathlib import Path
import buffalo_wings.airfoil as bwa
from buffalo_panel.config import load_panel_case, solve_panel_case_artifact
from buffalo_panel.post import (
export_surface_quantities_csv,
load_solved_case_artifact,
save_solved_case_artifact,
solution_from_artifact,
)
[docs]
def build_parser() -> argparse.ArgumentParser:
"""Build the command-line parser for the CLI workflows."""
parser = argparse.ArgumentParser(
prog="panel2d_cli",
description=(
"Run one Buffalo Panel case or inspect one solved artifact."
),
)
subparsers = parser.add_subparsers(dest="command")
run_parser = subparsers.add_parser(
"run",
help="Solve one structured case file and save a solved artifact.",
)
run_parser.add_argument(
"case_file",
help="Path to the input case YAML or JSON file.",
)
run_parser.add_argument(
"-o",
"--output",
help=(
"Output YAML path for the solved artifact. Defaults to a file "
"next to the input case."
),
)
inspect_parser = subparsers.add_parser(
"inspect",
help="Inspect metadata and key outputs from one solved artifact.",
)
inspect_parser.add_argument(
"artifact_file",
help="Path to the solved artifact YAML file.",
)
inspect_parser.add_argument(
"--include-results",
action="store_true",
help=(
"Reconstruct the runtime post-processing solution and print "
"integrated coefficients."
),
)
return parser
def _default_output_path(case_path: Path) -> Path:
"""Return the default solved-artifact path for one case file."""
return case_path.with_name(f"{case_path.stem}.solution.yaml")
def _default_surface_output_path(artifact_path: Path) -> Path:
"""Return the default surface-export CSV path for one artifact."""
return artifact_path.with_name(f"{artifact_path.stem}.surface.csv")
[docs]
def run_case(args: argparse.Namespace) -> None:
"""Solve one structured case file and persist the solved artifact."""
case_path = Path(args.case_file)
output_path = (
Path(args.output)
if args.output is not None
else _default_output_path(case_path)
)
spec = load_panel_case(case_path)
artifact = solve_panel_case_artifact(spec)
save_solved_case_artifact(artifact, output_path)
loaded_artifact = load_solved_case_artifact(output_path)
results = solution_from_artifact(loaded_artifact)
surface_output_path: Path | None = None
if (
spec.post is not None
and spec.post.surface is not None
and spec.post.surface.quantities
):
surface_output_path = _default_surface_output_path(output_path)
export_surface_quantities_csv(
results,
surface_output_path,
case_name=loaded_artifact.case.case_name,
quantities=spec.post.surface.quantities,
)
print(f"Saved solved artifact: {output_path}")
if surface_output_path is not None:
print(f"Saved surface CSV: {surface_output_path}")
print(f"Case: {loaded_artifact.case.case_name}")
print(
"Geometry: "
f"{bwa.AirfoilFactory.describe_spec(spec.geometry.bodies[0].airfoil).long}"
)
print(f"Panels: {results.geometry.n_panels}")
print(f"C_l pressure = {results.integrated.cl_pressure:.6e}")
print(f"C_l circulation = {results.integrated.cl_circulation:.6e}")
print(f"C_d pressure = {results.integrated.cd_pressure:.6e}")
print(f"C_m pressure = {results.integrated.cm_pressure:.6e}")
[docs]
def inspect_artifact(args: argparse.Namespace) -> None:
"""Print one solved artifact's metadata and optional recovered outputs."""
artifact = load_solved_case_artifact(args.artifact_file)
print(f"Artifact: {args.artifact_file}")
print(f"Artifact version: {artifact.artifact_version}")
print(f"Case: {artifact.case.case_name}")
print(f"Formulation: {artifact.formulation}")
print(f"Backend: {artifact.backend}")
print(
"Units: "
f"{artifact.case.length_unit}, "
f"{artifact.case.time_unit}, "
f"{artifact.case.angle_unit}"
)
print(f"Geometry: {artifact.geometry.geometry_name}")
print(f"Nodes: {len(artifact.geometry.x)}")
print(f"Families: {len(artifact.families)}")
print(f"Unknowns: {len(artifact.solution_vector)}")
if args.include_results:
results = solution_from_artifact(artifact)
print(f"Panels: {results.geometry.n_panels}")
print(f"C_l pressure = {results.integrated.cl_pressure:.6e}")
print(f"C_l circulation = {results.integrated.cl_circulation:.6e}")
print(f"C_d pressure = {results.integrated.cd_pressure:.6e}")
print(f"C_m pressure = {results.integrated.cm_pressure:.6e}")
def main() -> None:
"""Dispatch the CLI command for running or inspecting artifacts."""
parser = build_parser()
args = parser.parse_args()
if args.command == "run":
run_case(args)
return
if args.command == "inspect":
inspect_artifact(args)
return
parser.print_help()
if __name__ == "__main__":
main()