Examples

These examples are source files in examples/ and are included directly here to avoid duplicated documentation code.

Synthetic Analytical Example

"""Minimal analytical VorLap example with synthetic airfoil data."""

import numpy as np

from vorlap.computations import compute_thrust_torque_spectrum_optimized
from vorlap.structs import AirfoilFFT, Component, VIV_Params


def build_airfoil_fft() -> AirfoilFFT:
    re_grid = np.array([1.0, 2.0], dtype=float)
    aoa_grid = np.array([-10.0, 10.0], dtype=float)
    shape = (2, 2, 2)

    zeros = np.zeros(shape)
    cl_amp = zeros.copy()
    cd_amp = zeros.copy()
    cf_amp = zeros.copy()
    cl_amp[:, :, 0] = 1.0
    cd_amp[:, :, 0] = 2.0
    cf_amp[:, :, 1] = 0.1

    st = zeros.copy()
    st[:, :, 1] = 0.5

    return AirfoilFFT(
        name="default",
        Re=re_grid,
        AOA=aoa_grid,
        Thickness=0.12,
        CL_ST=st.copy(),
        CD_ST=st.copy(),
        CM_ST=st.copy(),
        CF_ST=st.copy(),
        CL_Amp=cl_amp,
        CD_Amp=cd_amp,
        CM_Amp=zeros.copy(),
        CF_Amp=cf_amp,
        CL_Pha=zeros.copy(),
        CD_Pha=zeros.copy(),
        CM_Pha=zeros.copy(),
        CF_Pha=zeros.copy(),
    )


def build_component() -> Component:
    shape_xyz = np.array([[0.0, 0.0, 0.0], [0.0, 0.0, 2.0]], dtype=float)
    return Component(
        id="blade",
        translation=np.zeros(3),
        rotation=np.zeros(3),
        pitch=np.array([0.0]),
        shape_xyz=shape_xyz,
        shape_xyz_global=shape_xyz.copy(),
        chord=np.ones(2),
        twist=np.zeros(2),
        thickness=np.ones(2) * 0.12,
        offset=np.zeros(2),
        airfoil_ids=["default", "default"],
        chord_vector=np.array([[1.0, 0.0, 0.0], [1.0, 0.0, 0.0]]),
        normal_vector=np.array([[0.0, 1.0, 0.0], [0.0, 1.0, 0.0]]),
    )


def run():
    component = build_component()
    affts = {"default": build_airfoil_fft()}
    viv = VIV_Params(
        fluid_density=1.0,
        fluid_dynamicviscosity=1.0,
        inflow_vec=np.array([1.0, 1.0, 0.0]),
        inflow_speeds=np.array([2.0]),
        azimuths=np.array([0.0]),
        output_time=np.array([0.0, 0.25]),
        output_azimuth_vinf=(0.0, 2.0),
        n_freq_depth=2,
        amplitude_coeff_cutoff=0.05,
    )

    natfreqs = np.array([1.0, 2.0])
    percdiff, _, force, moment, node_forces = compute_thrust_torque_spectrum_optimized(
        [component], affts, viv, natfreqs
    )

    print("Total force:", force[0, 0, :])
    print("Total moment:", moment[0, 0, :])
    print("Worst percent diff:", percdiff[0, 0])
    print("Node 1 reconstructed x-force:", node_forces[:, 0, 0])


if __name__ == "__main__":
    run()

Single-Blade Verification Case

"""Single-blade verification example using repository reference data."""

from pathlib import Path

import numpy as np

from vorlap.computations import compute_thrust_torque_spectrum_optimized
from vorlap.fileio import load_airfoil_fft, load_components_from_csv
from vorlap.graphics import calc_structure_vectors_andplot
from vorlap.structs import VIV_Params


def run():
    repo = Path(__file__).resolve().parents[1]

    viv_params = VIV_Params(
        fluid_density=1.225,
        fluid_dynamicviscosity=1.81e-5,
        rotation_axis=np.array([0.0, 0.0, 1.0]),
        rotation_axis_offset=np.array([0.0, 0.0, 0.0]),
        inflow_vec=np.array([1.0, 0.0, 0.0]),
        azimuths=np.array([4.0, 52.0]),
        inflow_speeds=np.array([7.3878]),
        output_time=np.array([0.0, 0.01]),
        output_azimuth_vinf=(999.0, 999.0),
        n_harmonic=2,
        amplitude_coeff_cutoff=0.002,
        n_freq_depth=10,
        airfoil_folder=str(repo / "data" / "airfoils") + "/",
    )

    natfreqs = np.loadtxt(repo / "data" / "natural_frequencies.csv", delimiter=",")
    components = load_components_from_csv(str(repo / "data" / "components" / "componentsSingle"))

    afft = load_airfoil_fft(str(repo / "data" / "airfoils" / "NACA0018.h5"))
    affts = {afft.name: afft, "default": afft}

    # Populates component chord/normal/global vectors used by the solver.
    calc_structure_vectors_andplot(components, viv_params, show_plot=False, return_fig=False, save_path=None)

    _, _, total_force, _, _ = compute_thrust_torque_spectrum_optimized(
        components, affts, viv_params, natfreqs
    )

    q = 0.5 * 1.225 * 7.3878**2 * 1.0 * 80.0
    expected_drag_4 = q * 0.024
    expected_lift_4 = q * -0.402
    expected_drag_52 = q * 1.98
    expected_lift_52 = q * -1.5

    print("AOA -4 deg case:")
    print("  Fx (VorLap):", total_force[0, 0, 0], "Expected:", expected_drag_4)
    print("  Fy (VorLap):", total_force[0, 0, 1], "Expected:", expected_lift_4)
    print("AOA -52 deg case:")
    print("  Fx (VorLap):", total_force[0, 1, 0], "Expected:", expected_drag_52)
    print("  Fy (VorLap):", total_force[0, 1, 1], "Expected:", expected_lift_52)


if __name__ == "__main__":
    run()

Time-Varying Inflow Case

"""Minimal VorLap example using a time-varying inflow profile CSV."""

from pathlib import Path

import numpy as np

from vorlap.computations import compute_time_varying_force_history_optimized
from vorlap.fileio import load_inflow_time_series
from vorlap.structs import AirfoilFFT, Component, VIV_Params


def build_airfoil_fft() -> AirfoilFFT:
    re_grid = np.array([1.0, 2.0], dtype=float)
    aoa_grid = np.array([-10.0, 10.0], dtype=float)
    shape = (2, 2, 2)

    zeros = np.zeros(shape)
    cl_amp = zeros.copy()
    cd_amp = zeros.copy()
    cf_amp = zeros.copy()
    cl_amp[:, :, 0] = 1.0
    cd_amp[:, :, 0] = 2.0
    cl_amp[:, :, 1] = 0.2
    cd_amp[:, :, 1] = 0.1
    cf_amp[:, :, 1] = 0.1

    st = zeros.copy()
    st[:, :, 1] = 0.5

    return AirfoilFFT(
        name="default",
        Re=re_grid,
        AOA=aoa_grid,
        Thickness=0.12,
        CL_ST=st.copy(),
        CD_ST=st.copy(),
        CM_ST=st.copy(),
        CF_ST=st.copy(),
        CL_Amp=cl_amp,
        CD_Amp=cd_amp,
        CM_Amp=zeros.copy(),
        CF_Amp=cf_amp,
        CL_Pha=zeros.copy(),
        CD_Pha=zeros.copy(),
        CM_Pha=zeros.copy(),
        CF_Pha=zeros.copy(),
    )


def build_component() -> Component:
    shape_xyz = np.array([[0.0, 0.0, 0.0], [0.0, 0.0, 2.0]], dtype=float)
    return Component(
        id="blade",
        translation=np.zeros(3),
        rotation=np.zeros(3),
        pitch=np.array([0.0]),
        shape_xyz=shape_xyz,
        shape_xyz_global=shape_xyz.copy(),
        chord=np.ones(2),
        twist=np.zeros(2),
        thickness=np.ones(2) * 0.12,
        offset=np.zeros(2),
        airfoil_ids=["default", "default"],
        chord_vector=np.array([[1.0, 0.0, 0.0], [1.0, 0.0, 0.0]]),
        normal_vector=np.array([[0.0, 1.0, 0.0], [0.0, 1.0, 0.0]]),
    )


def run(profile_csv: str = ""):
    repo = Path(__file__).resolve().parents[1]
    profile_path = Path(profile_csv) if profile_csv else (repo / "data" / "inflow_profile.csv")
    inflow_profile = load_inflow_time_series(str(profile_path))

    component = build_component()
    affts = {"default": build_airfoil_fft()}
    viv = VIV_Params(
        fluid_density=1.0,
        fluid_dynamicviscosity=1.0,
        rotation_axis=np.array([0.0, 0.0, 1.0]),
        rotation_axis_offset=np.array([0.0, 0.0, 0.0]),
        inflow_vec=np.array([1.0, 0.0, 0.0]),
        azimuths=np.array([0.0]),
        inflow_speeds=np.array([2.0]),
        output_time=np.array([0.0, 0.25]),
        output_azimuth_vinf=(0.0, 2.0),
        n_freq_depth=2,
        amplitude_coeff_cutoff=0.05,
    )

    time, total_force, total_moment, node_forces = compute_time_varying_force_history_optimized(
        [component],
        affts,
        viv,
        inflow_profile,
        azimuth_deg=0.0,
        smoothing_cycles=0.25,
    )

    print(f"Inflow profile file: {profile_path}")
    print(f"Time samples: {time.size}")
    print("Total force at first sample:", total_force[0, :])
    print("Total moment at first sample:", total_moment[0, :])
    print("Node 1 x-force (first 5 samples):", node_forces[:5, 0, 0])


if __name__ == "__main__":
    run()

QBlade Fast-Path Loading Export

"""Generate a QBlade LOADINGFILE from VorLap time-varying inflow reconstruction."""

from __future__ import annotations

import argparse
import glob
import os
from pathlib import Path

import vorlap


def _load_airfoils(airfoil_folder: str):
    affts = {}
    for file in glob.glob(os.path.join(airfoil_folder, "*.h5")):
        afft = vorlap.load_airfoil_fft(file)
        affts[afft.name] = afft
    if "default" not in affts and affts:
        affts["default"] = next(iter(affts.values()))
    return affts


def parse_args():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--sim", required=True, help="Path to a QBlade .sim file.")
    parser.add_argument(
        "--inflow",
        default=str(Path(vorlap.repo_dir) / "data" / "inflow_profile.csv"),
        help="CSV with time-varying inflow: time,inflow_speed,inflow_direction_deg",
    )
    parser.add_argument(
        "--airfoils",
        default=str(Path(vorlap.repo_dir) / "data" / "airfoils"),
        help="Folder containing VorLap airfoil FFT .h5 files.",
    )
    parser.add_argument(
        "--output",
        default="qblade_external_loading.txt",
        help="Output QBlade loading file path.",
    )
    parser.add_argument(
        "--azimuth",
        type=float,
        default=None,
        help="Optional fixed azimuth for reconstruction (deg).",
    )
    return parser.parse_args()


def main():
    args = parse_args()

    components, viv_params, qblade_node_ids = vorlap.convert_qblade_to_vorlap_inputs(args.sim)
    inflow_profile = vorlap.load_inflow_time_series(args.inflow)
    affts = _load_airfoils(args.airfoils)
    if not affts:
        raise RuntimeError(f"No airfoil FFT .h5 files found in: {args.airfoils}")
    viv_params.airfoil_folder = os.path.join(args.airfoils, "")

    azimuth = args.azimuth if args.azimuth is not None else viv_params.output_azimuth_vinf[0]

    # Populate global geometry and local chord/normal vectors needed by reconstruction.
    vorlap.calc_structure_vectors_andplot(components, viv_params, show_plot=False)

    time, _total_force, _total_moment, node_forces = vorlap.compute_time_varying_force_history_optimized(
        components=components,
        affts=affts,
        viv_params=viv_params,
        inflow_profile=inflow_profile,
        azimuth_deg=azimuth,
        smoothing_cycles=0.25,
    )

    output_path = Path(args.output)
    output_path.parent.mkdir(parents=True, exist_ok=True)
    vorlap.write_qblade_loading_file(
        str(output_path),
        time,
        node_forces,
        qblade_node_ids,
        local=False,
    )
    print(f"Wrote QBlade loading file: {output_path}")
    print(f"Load targets: {len(qblade_node_ids)}")
    print("Set this file in your QBlade .sim TURB_1 LOADINGFILE field.")


if __name__ == "__main__":
    main()