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()