Source code for aerodemo.openvsp_utils

"""
OpenVSP utility helpers for AeroDemonstrator.

This module provides helper functions to set up and run OpenVSP models
and VSPAero analyses from Python.

Requires the ``openvsp`` package to be installed.
See https://openvsp.org/pyapi_docs/latest/ for the full OpenVSP Python API.

Notes
-----
OpenVSP is an open-source parametric aircraft geometry tool developed by NASA.
The Python API (``openvsp`` package) allows programmatic geometry creation and
aerodynamic analysis through VSPAero.

Installation
------------
Install the OpenVSP Python API with::

    pip install openvsp

or download prebuilt wheels from https://openvsp.org.
"""

from __future__ import annotations

import os
import tempfile
from pathlib import Path
from typing import Optional

try:
    import openvsp as vsp
    HAS_OPENVSP = True
except ImportError:
    HAS_OPENVSP = False
    vsp = None  # type: ignore[assignment]


[docs] def check_openvsp() -> bool: """ Check whether the OpenVSP Python API is available. Returns ------- bool ``True`` if ``openvsp`` can be imported, ``False`` otherwise. """ return HAS_OPENVSP
[docs] def init_vsp(title: str = "AeroDemonstrator") -> None: """ Initialize a fresh OpenVSP model. Parameters ---------- title : str, optional Title for the VSP model. Default is ``'AeroDemonstrator'``. Raises ------ ImportError If the ``openvsp`` package is not installed. """ if not HAS_OPENVSP: raise ImportError( "The 'openvsp' package is not installed. " "Install it with: pip install openvsp" ) vsp.ClearVSPModel() vsp.Update()
[docs] def add_wing( span: float = 10.0, root_chord: float = 2.0, tip_chord: float = 1.0, sweep_deg: float = 0.0, dihedral_deg: float = 0.0, x_offset: float = 0.0, z_offset: float = 0.0, name: str = "Wing", ) -> Optional[str]: """ Add a trapezoidal wing to the current VSP model. Parameters ---------- span : float Full wing span [m]. Default 10.0. root_chord : float Root chord [m]. Default 2.0. tip_chord : float Tip chord [m]. Default 1.0. sweep_deg : float Quarter-chord sweep angle [degrees]. Default 0.0. dihedral_deg : float Dihedral angle [degrees]. Default 0.0. x_offset : float X-position of wing origin [m]. Default 0.0. z_offset : float Z-position of wing [m]. Default 0.0. name : str Geometry name in VSP. Default ``'Wing'``. Returns ------- str or None VSP geometry ID string, or ``None`` if OpenVSP is unavailable. """ if not HAS_OPENVSP: return None wing_id = vsp.AddGeom("WING", "") vsp.SetGeomName(wing_id, name) # Set span (half-span in VSP = full_span/2) vsp.SetParmVal(wing_id, "TotalSpan", "WingGeom", float(span)) vsp.SetParmVal(wing_id, "Root_Chord", "XSec_1", float(root_chord)) vsp.SetParmVal(wing_id, "Tip_Chord", "XSec_1", float(tip_chord)) vsp.SetParmVal(wing_id, "Sweep", "XSec_1", float(sweep_deg)) vsp.SetParmVal(wing_id, "Dihedral", "XSec_1", float(dihedral_deg)) # Position vsp.SetParmVal(wing_id, "X_Rel_Location", "XForm", float(x_offset)) vsp.SetParmVal(wing_id, "Z_Rel_Location", "XForm", float(z_offset)) vsp.Update() return wing_id
[docs] def add_fuselage( length: float = 10.0, max_diameter: float = 1.5, name: str = "Fuselage", ) -> Optional[str]: """ Add a simple fuselage to the current VSP model. Parameters ---------- length : float Fuselage length [m]. Default 10.0. max_diameter : float Maximum diameter [m]. Default 1.5. name : str Geometry name. Default ``'Fuselage'``. Returns ------- str or None VSP geometry ID, or ``None`` if OpenVSP is unavailable. """ if not HAS_OPENVSP: return None fuse_id = vsp.AddGeom("FUSELAGE", "") vsp.SetGeomName(fuse_id, name) vsp.SetParmVal(fuse_id, "Length", "Design", float(length)) vsp.SetParmVal(fuse_id, "Diameter", "Design", float(max_diameter)) vsp.Update() return fuse_id
[docs] def add_horizontal_tail( span: float = 4.0, root_chord: float = 1.2, tip_chord: float = 0.7, x_offset: float = 8.5, z_offset: float = 0.2, name: str = "HTail", ) -> Optional[str]: """ Add a horizontal tail surface. Parameters ---------- span : float Full horizontal tail span [m]. Default 4.0. root_chord : float Root chord [m]. Default 1.2. tip_chord : float Tip chord [m]. Default 0.7. x_offset : float X-position [m]. Default 8.5. z_offset : float Z-position [m]. Default 0.2. name : str Geometry name. Default ``'HTail'``. Returns ------- str or None VSP geometry ID, or ``None`` if OpenVSP is unavailable. """ return add_wing( span=span, root_chord=root_chord, tip_chord=tip_chord, x_offset=x_offset, z_offset=z_offset, name=name, )
[docs] def add_vertical_tail( height: float = 2.5, root_chord: float = 1.5, tip_chord: float = 0.8, x_offset: float = 8.0, z_offset: float = 0.0, name: str = "VTail", ) -> Optional[str]: """ Add a vertical tail surface. Parameters ---------- height : float Vertical tail height [m]. Default 2.5. root_chord : float Root chord [m]. Default 1.5. tip_chord : float Tip chord [m]. Default 0.8. x_offset : float X-position [m]. Default 8.0. z_offset : float Z-position [m]. Default 0.0. name : str Geometry name. Default ``'VTail'``. Returns ------- str or None VSP geometry ID, or ``None`` if OpenVSP is unavailable. """ if not HAS_OPENVSP: return None vtail_id = vsp.AddGeom("WING", "") vsp.SetGeomName(vtail_id, name) vsp.SetParmVal(vtail_id, "TotalSpan", "WingGeom", float(height)) vsp.SetParmVal(vtail_id, "Root_Chord", "XSec_1", float(root_chord)) vsp.SetParmVal(vtail_id, "Tip_Chord", "XSec_1", float(tip_chord)) vsp.SetParmVal(vtail_id, "X_Rel_Location", "XForm", float(x_offset)) vsp.SetParmVal(vtail_id, "Z_Rel_Location", "XForm", float(z_offset)) # Rotate 90 degrees about X to make it vertical vsp.SetParmVal(vtail_id, "X_Rel_Rotation", "XForm", 90.0) vsp.Update() return vtail_id
[docs] def run_vspaero( alpha_start: float = 0.0, alpha_end: float = 10.0, alpha_npts: int = 5, mach: float = 0.1, ref_area: float = 1.0, ref_span: float = 1.0, ref_chord: float = 1.0, analysis_type: str = "VLM", ) -> Optional[dict]: """ Run a VSPAero analysis sweep on the current model. Parameters ---------- alpha_start : float Start angle of attack [degrees]. Default 0.0. alpha_end : float End angle of attack [degrees]. Default 10.0. alpha_npts : int Number of alpha points. Default 5. mach : float Mach number. Default 0.1 (incompressible). ref_area : float Reference area [m^2]. Default 1.0. ref_span : float Reference span [m]. Default 1.0. ref_chord : float Reference chord [m]. Default 1.0. analysis_type : str ``'VLM'`` for Vortex Lattice Method or ``'Panel'`` for panel method. Default ``'VLM'``. Returns ------- dict or None Dictionary with VSPAero result arrays, or ``None`` if OpenVSP is unavailable. Notes ----- The returned dictionary contains:: { "Alpha": array of alpha values, "CL": array of lift coefficients, "CDi": array of induced drag coefficients, "CY": array of side force coefficients, "Cl": array of rolling moment coefficients, "Cm": array of pitching moment coefficients, "Cn": array of yawing moment coefficients, } """ if not HAS_OPENVSP: return None analysis_name = "VSPAEROSweep" vsp.SetAnalysisInputDefaults(analysis_name) # Analysis method: 0=VLM, 1=Panel method = 0 if analysis_type.upper() == "VLM" else 1 vsp.SetIntAnalysisInput(analysis_name, "AnalysisMethod", [method]) # Alpha sweep vsp.SetDoubleAnalysisInput(analysis_name, "AlphaStart", [alpha_start]) vsp.SetDoubleAnalysisInput(analysis_name, "AlphaEnd", [alpha_end]) vsp.SetIntAnalysisInput(analysis_name, "AlphaNpts", [alpha_npts]) # Reference values vsp.SetDoubleAnalysisInput(analysis_name, "Mach", [mach]) vsp.SetDoubleAnalysisInput(analysis_name, "Sref", [ref_area]) vsp.SetDoubleAnalysisInput(analysis_name, "bref", [ref_span]) vsp.SetDoubleAnalysisInput(analysis_name, "cref", [ref_chord]) # Execute the analysis results_id = vsp.ExecAnalysis(analysis_name) # Extract results alpha_res = list(vsp.GetDoubleResults(results_id, "Alpha")) cl_res = list(vsp.GetDoubleResults(results_id, "CL")) cdi_res = list(vsp.GetDoubleResults(results_id, "CDi")) result = { "Alpha": alpha_res, "CL": cl_res, "CDi": cdi_res, } for key in ["CY", "Cl", "Cm", "Cn"]: try: result[key] = list(vsp.GetDoubleResults(results_id, key)) except Exception: result[key] = [0.0] * len(alpha_res) return result
[docs] def save_vsp3_file(filepath: str) -> None: """ Save the current VSP model to a .vsp3 file. Parameters ---------- filepath : str Output file path (should end in ``.vsp3``). Raises ------ ImportError If the ``openvsp`` package is not installed. """ if not HAS_OPENVSP: raise ImportError("The 'openvsp' package is not installed.") vsp.WriteVSPFile(str(filepath))
[docs] def load_vsp3_file(filepath: str) -> None: """ Load a VSP model from a .vsp3 file. Parameters ---------- filepath : str Input ``.vsp3`` file path. Raises ------ ImportError If the ``openvsp`` package is not installed. FileNotFoundError If the specified file does not exist. """ if not HAS_OPENVSP: raise ImportError("The 'openvsp' package is not installed.") if not os.path.exists(filepath): raise FileNotFoundError(f"VSP file not found: {filepath}") vsp.ReadVSPFile(str(filepath)) vsp.Update()