diff --git a/.gitignore b/.gitignore index cdd06fd..910c9b8 100644 --- a/.gitignore +++ b/.gitignore @@ -177,4 +177,7 @@ cython_debug/ #.idea/ #Ignore old thesis -Aerostructural_model_LEI \ No newline at end of file +Aerostructural_model_LEI + +# Generated cache from the fancy-plot SurfplanAdapter conversion +vsm_processed/ \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..fe3990e --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,46 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Repository purpose + +Python implementation of the Vortex Step Method (VSM): an enhanced lifting-line aerodynamic solver coupled with 2D viscous airfoil polars, aimed at low-aspect-ratio wings and leading-edge inflatable (LEI) kites. Scientific software — physical correctness, numerical stability, and reproducibility outrank abstraction and stylistic churn. **Read `AGENTS.md` before changing solver/aerodynamic logic** (sign conventions, reference frames, and circulation definitions must never change silently). + +## Commands + +```bash +source venv/bin/activate # project venv at ./venv — required; system python lacks deps +pip install -e .[dev] # editable install (dev = pytest, pytest-cov, black) + +pytest # full suite (~35 s), config in pytest.ini (testpaths = tests) +pytest tests/Solver/test_solver.py # one file +pytest tests/Solver/test_solver.py::test_name # one test +black src tests # formatting (used by the project) +``` + +Examples run as plain scripts: `python examples/TUDELFT_V3_KITE/tutorial.py`. CI (`.github/workflows/testing.yml`) runs pytest with coverage on Python 3.10. + +## Architecture (big picture) + +Data flow: **geometry YAML → `BodyAerodynamics.instantiate()` → `Wing`/`Panel` mesh → `Solver.solve()` → results dict → plotting**. + +- `src/VSM/core/BodyAerodynamics.py` — central orchestrator. `BodyAerodynamics.instantiate(n_panels, file_path=, spanwise_panel_distribution=..., bridle_path=...)` reads the YAML's `wing_sections` (headers `[airfoil_id, LE_x..z, TE_x..z]`) and `wing_airfoils` tables, builds polars per airfoil, adds sections to a `Wing`, and refines to `n_panels`. YAML readers index columns **by header name**, so extra columns (e.g. `VUP_x..z` from newer SurfplanAdapter exports) are ignored harmlessly. +- `src/VSM/core/AirfoilAerodynamics.py` — polar generation per airfoil type: `breukels_regression`, `masure_regression` (needs `ml_models_dir`; ~2.3 GB models downloaded from Zenodo into `data/ml_models/`, not in git), `neuralfoil`, `polars` (CSV), `inviscid`. Batch entry point: `from_yaml_entry_batch()`. +- `src/VSM/core/WingGeometry.py` (`Wing`), `Panel.py`, `Filament.py`, `Wake.py` — mesh & vortex elements. **Panels retain only LE/TE points + interpolated polar data**; airfoil `info_dict` parameters (`t`, `chord`, …) are discarded during instantiation — anything needing them later must re-read the YAML. +- `src/VSM/core/Solver.py` — iterative circulation (gamma) loop; optional Anderson acceleration and Li/Gaunaa spanwise artificial viscosity for post-stall stabilization. Returns a results dict (forces, moments, distributions). +- Top-level `src/VSM/` modules build on core: `stability_derivatives.py`, `trim_angle.py`, plus plotting: `plotting.py` (polars/distributions), `plot_geometry_matplotlib.py` (`plot_geometry`), `plot_geometry_plotly.py` (`interactive_plot` — the primary interactive 3D wing view, incl. bridle rendering), `plot_styling.py`. +- Performance-critical vector math is `numba`-jitted (`core/utils.py`). + +## Data layout + +`data//` holds per-kite inputs, e.g. `data/TUDELFT_V3_KITE/`: +- `CAD_derived_geometry/` and `Surfplan_derived_geometry/` — `aero_geometry_*.yaml` variants (one per airfoil-model type) + `struc_geometry_*.yaml` (bridle/particle data, used via `bridle_path`). +- `Surfplan_export/` — raw Surfplan export (`.txt` + `profiles/*.dat`). Converted to VSM YAMLs by the sibling [SurfplanAdapter](https://github.com/awegroup/SurfplanAdapter) project, which also handles the Surfplan→VSM coordinate transform. +- Different YAML sources use different global frames (CAD-derived vs Surfplan-derived origins differ by ~10 m in z) — never mix coordinates across YAML families. + +## Conventions that matter + +- Body-fixed reference frame: **x rearward (LE→TE), y right wing, z up**; α positive nose-up, β positive wind from port. Aircraft-frame conversion via `stability_derivatives.map_derivatives_to_aircraft_frame()`. +- Wing sections are ordered/symmetric tip-to-tip; `airfoil_id` in `wing_sections` must match `wing_airfoils`. +- Style (`docs/style_guide.md`): typed Google docstrings, `snake_case`, path variables end `_path`, directories end `_dir`. +- Tests include verification cases against analytical solutions (`tests/verification_cases/`: elliptical, swept, curved wing, horseshoe vortices) — solver changes must keep these passing; add regression tests when numerical behaviour intentionally changes and explain what changed and why in the commit. diff --git a/README.md b/README.md index 1d42c90..2376db6 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,18 @@ The Vortex Step Method (VSM) is an enhanced lifting line method that improves up A Julia version of this project is available at [VortexStepMethod.jl](https://github.com/Albatross-Kite-Transport/VortexStepMethod.jl) +## Key Features +- **Accurate low-aspect-ratio wing modeling** with enhanced lifting line theory +- **Viscous-inviscid coupling** using 2D airfoil polars (inviscid, CFD, ML-based) +- **Complex geometry support**: sweep, dihedral, anhedral, leading-edge inflatable (LEI) kites +- **Rigid-body stability derivatives**: automatic computation of dCx/dα, dCMy/dq, etc. +- **Trim angle solver**: automatic determination of trimmed angle of attack +- **Non-dimensional rate derivatives**: controls-friendly output (hat_p, hat_q, hat_r) +- **Reference point flexibility**: correctly handles moment reference point for rotational velocities +- **Interactive visualization**: Plotly and Matplotlib geometry and results plotting + +![](docs/plotly_render.png) + ## Reference Frame The VSM uses a body-fixed reference frame with the following conventions: @@ -30,17 +42,6 @@ The reference frame is illustrated below for the open-source example kite, the [ **Aircraft Frame Transformation:** For stability derivatives in standard aircraft coordinates (x-forward, y-right, z-down), use `map_derivatives_to_aircraft_frame()` from the `VSM.stability_derivatives` module. See `examples/TUDELFT_V3_KITE/evaluate_stability_derivatives.py` for usage. -## Key Features - -- **Accurate low-aspect-ratio wing modeling** with enhanced lifting line theory -- **Viscous-inviscid coupling** using 2D airfoil polars (inviscid, CFD, ML-based) -- **Complex geometry support**: sweep, dihedral, anhedral, leading-edge inflatable (LEI) kites -- **Rigid-body stability derivatives**: automatic computation of dCx/dα, dCMy/dq, etc. -- **Trim angle solver**: automatic determination of trimmed angle of attack -- **Non-dimensional rate derivatives**: controls-friendly output (hat_p, hat_q, hat_r) -- **Reference point flexibility**: correctly handles moment reference point for rotational velocities -- **Interactive visualization**: Plotly and Matplotlib geometry and results plotting - ## Documentation For detailed documentation, please refer to the following resources. diff --git a/changelog.md b/changelog.md index 410043c..cd1244e 100644 --- a/changelog.md +++ b/changelog.md @@ -4,6 +4,171 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). +## [2.3.0] - 04/11/2026 + +**Default results change.** The lifting-line coupling now follows the consistent +formulation of Gaunaa, Li & Pirrung (TORQUE 2026): circulation from the +effective velocity including induction, span-perpendicular sections, +quarter-chord force directions, +attached-trailed-vortex forces and a chord-regularised curved bound vortex. +Forces, moments and coefficients computed with the defaults differ from 2.2.0, +most on swept and curved wings (kites). Each new default can be switched back +individually where an option exists (listed below). See +`docs/aerodynamic_model` for the formulation and its validation. + +This release also contains everything shipped in 2.2.0. `v2.2.0` was tagged on +the `improve_plotting` branch, which had not been merged into `develop`/`main`. + +### Changed (results) +- Circulation target is `Gamma = 0.5 |V_eff| c Cl`, instead of + `0.5 |V_eff|^2 / |V_a| c Cl`. `V_eff` is the span-perpendicular effective + velocity: inflow plus induction, without the section's own 2D induction (the + inner velocity of Gaunaa et al.). `V_a` is the span-perpendicular inflow + without induction. On the V3 kite the old map put the circulation 2-6 % low. +- Panel frame and chord follow the Crossflow Principle: the chord direction is + perpendicular to the bound vortex, the normal is chord x span, and the panel + chord is the perpendicular one. Unswept panels are unchanged; on swept panels + the chord shrinks (V3 tip panels by 56 % and 19 %). +- `is_aoa_corrected=True` by default: lift and drag directions come from the + flow at the quarter chord (TAT3), with magnitudes from the 3/4-chord angle. + `is_aoa_corrected=False` restores the 3/4-chord directions. +- The quarter-chord flow includes the influence of a curved or swept bound + vortex through a correction: the actual minus a locally unrolled vortex + system, with the bound vorticity and the leg starting points spread over the + chord using the flat-plate thin-airfoil loading `gamma(x) ~ sqrt((c - x)/x)` + (24 strips, effective cut-off 0.15 c, not a tuning parameter). This is a new + modelling assumption. The correction is zero on a straight wing with + perpendicular legs. On-blade induced drag matches the Trefftz plane within + about 1 % on arc, swept and kite geometries, independently of the panel + count. It is computed once per shape and rotated under rigid motion. +- Drag acts along the local inner flow, not the global span axis. +- `is_with_attached_trailed_vortex_force=True` by default: Kutta-Joukowski force + on the chordwise attached trailed vortex segments (`F_attached_trailed_distribution` + in the results). +- `relaxation_factor=None` by default: 0.8 x the fixed-point stability limit of + Li et al. (2026), evaluated per solve (`Solver.compute_relaxation_factor_limit`). + 2.2.0 used a fixed 0.01; pass a number to keep a fixed value. +- Each wake ring follows its own panel's apparent velocity. Results in uniform + translating inflow are unchanged to round-off; they change under body rates or + distributed inflow. +- Bridle line segments are charged at their own inflow, not the wing's mean. +- With `is_with_viscous_drag_correction=True`, the Gaunaa 2024 correction receives + the full 3D relative velocity (the spanwise flow it models). +- With `is_with_artificial_viscosity=True`, the regularization is also gated on + negative stall. +- `anderson_max_iterations` default raised from 100 to 1000. +- `Wing.preserve_section_order` (default False): skip the section re-sorting; + `update_wing_from_points` sets it, since its arrays already define the arc order. + +### Removed / breaking +- `Solver` options `is_smooth_circulation`, `smoothness_factor`, + `is_artificial_damping`, `artificial_damping`, + `is_with_simonet_artificial_viscosity`, `simonet_artificial_viscosity_fva` + (stored, never read) and the `gamma_loop_type` values `simonet_stall`, + `non_linear_simonet_stall`, `non_linear_simonet_stall_newton_raphson` (they + raised `ImportError`). An unknown `gamma_loop_type` now raises `ValueError`. +- `Solver.compute_aerodynamic_quantities` no longer returns `Umagw`; the relative + velocity is available from `Solver.compute_relative_velocity(gamma)`. +- The Anderson loop no longer falls back to the base loop by default + (`anderson_fallback_to_base=True` restores it). +- `SurfplanAdapter` is now a required dependency. + +### Added +- `gamma_loop_type="casadi_newton"`: Newton solve of the circulation residual + with an exact CasADi Jacobian, globalised by pseudo-transient continuation, + with fallback to the base loop (`newton_max_iterations`, + `newton_pseudo_time_step`, `newton_fallback_to_base`, `polar_interpolation`). + Requires the optional `casadi` package. +- `stagnation_patience` / `stagnation_rtol`: optional early stop of stagnating + circulation loops. +- `BodyAerodynamics(..., leg_direction="rib" | "streamwise" | "perpendicular")` + for the attached-trailed legs (default `"rib"`, unchanged). +- `Panel.set_leg_end_points` and `Panel.reset_legs_to_ribs`. +- `results["drag_induced_trefftz"]`: Trefftz-plane induced drag as a far-wake + reference (uniform inflow only). +- `VSM.core.panel_kernels`: the section force law as functions over an `xp` + namespace (`numpy` or `casadi`): `relative_flow`, `quarter_chord_velocity`, + `direction_angle`, `panel_forces`, `attached_trailed_forces` and row helpers. + `compute_results` uses them itself. +- `BodyAerodynamics.compute_quarter_chord_AIC_matrices`: the induction of the + force-direction flow at the quarter chords, for callers that freeze it. +- `Solver.casadi_panel_force_function()`: per-panel forces as a CasADi function. + With `table_inputs=True` (also on the residual function) the polar tables are + inputs, so one graph serves every deformed shape; + `Solver.casadi_polar_table_inputs()` and `casadi_stall_angle_inputs()` return + them in the input layout. +- Documentation: aerodynamic model document (formulation, conventions, + validation) and the curved bound-vortex validation report. + +### Fixed +- Circulation loops stop on non-finite circulation instead of running on. +- `Solver`: SyntaxError in the pseudo-transient reject condition (hotfix on `main` + after 2.2.0, now also here). +- `Wing.compute_projected_area` no longer normalises the caller's + `z_plane_vector` in place. + +### Performance +- AIC assembly JIT-compiled with numba, then made allocation-free (vectors as + tuples): about 1 us -> 20 ns per filament evaluation, 7.3 -> 0.1 ms for a + 36-panel AIC. +- Curved bound-vortex correction computed once per shape and rotated under + rigid motion. +- Faster Li/Gaunaa artificial viscosity, vectorised force law, Trefftz drag and + projected area, cheaper `casadi_newton` residual calls. A 36-panel V3 + `casadi_newton` solve takes about 17 ms (about 50 ms before these changes). + +## [2.2.0] - 02/09/2026 + +Release cut from the `improve_plotting` branch. This is the version used to +generate the rigid-body stability derivatives (Table 8) of Poland et al. (2026), +Wind Energ. Sci., "Computational aerodynamics for soft-wing kite design". + +### Fixed +- `BodyAerodynamics.va` setter: the rotational inflow due to body rates is now + `va -= omega x (r - r0)` (apparent wind = freestream minus body-point velocity). + v2.1.2 used `+=`, i.e. the body rotating at `-omega`. **This flips the sign of + every rate derivative** (`d/dp_hat`, `d/dq_hat`, `d/dr_hat`) computed with + `stability_derivatives.py`; angle derivatives and trim angles are unaffected. + Roll, pitch and yaw damping derivatives are now negative, as physics requires. +- `stability_derivatives.py::_evaluate_with_angles` now passes `body_axis`, so a + non-zero baseline roll/pitch rate is no longer silently applied about the z-axis. +- `trim_angle.py`: removed leftover debug `matplotlib` plot/`plt.show()` from the + coarse sweep (blocked headless pipelines). +- `Filament`: vortex-core regularization uses the radial projection of the + evaluation point onto the filament. +- `WingGeometry`: section meshing is orientation-agnostic. +- Elliptical-wing y-offset bug in tests fixed; bridle line index handling fixed. + +### Changed +- **Breaking:** `BodyAerodynamics.va_initialize` / `va` setter no longer accept + `roll_rate`, `pitch_rate`, `yaw_rate`. Use `body_rates` (scalar or list of + magnitudes, rad/s), `body_axis` (matching axes, default +z) and + `rates_in_body_frame`. `compute_trim_angle` and + `compute_rigid_body_stability_derivatives` keep their `roll_rate/pitch_rate/yaw_rate` + keyword arguments. +- **Breaking:** bridle-line dictionaries use header key `d` (line diameter) + instead of `diameter`, matching the SurfPlanAdapter export. +- `BodyAerodynamics.instantiate` accepts `aerodynamic_center_location` and + `control_point_location` (chord fractions, defaults 0.25 / 0.75); AC and CP + chord weights are decoupled. +- `Solver`: opt-in Anderson-accelerated inner circulation loop; opt-in + Li/Gaunaa spanwise artificial viscosity for post-stall stabilization; + `relaxation_factor` default restored to 0.01. +- `Solver.solve` results include `q_ref` (reference dynamic pressure); coefficients + are normalised with an area-weighted reference velocity for distributed inflow. +- Quasi-steady-state solver API and examples added and subsequently removed. + +### Added +- `BodyAerodynamics.rotate(angle_deg|angle_rad, axis, point)` to rotate the full + geometry; body rates follow the geometry unless `rates_in_body_frame=True`. +- `BodyAerodynamics.compute_flat_area`. +- `src/VSM/plotly/`: interactive Plotly geometry plots with inflatable tubes, + curved canopy, Cp-scaled force fields (`fancy_interactive_plot.py` example). +- Examples: `compute_trim_angle.py`, `calculate_max_roll.py`, + `attitude_and_yaw_rate_optimize.py`, `cmz_sensitivity_default.py`, + `varying_spanwise_va.py`. +- Tests for gamma initialization, tube geometry, updated filament/solver tests. + ## [2.1.0] - 11/02/2026 ### Added diff --git a/data/TUDELFT_V3_KITE/CAD_derived_geometry/struc_geometry_manually_adjusted.yaml b/data/TUDELFT_V3_KITE/CAD_derived_geometry/struc_geometry_manually_adjusted.yaml index fdc68f0..4987dc6 100644 --- a/data/TUDELFT_V3_KITE/CAD_derived_geometry/struc_geometry_manually_adjusted.yaml +++ b/data/TUDELFT_V3_KITE/CAD_derived_geometry/struc_geometry_manually_adjusted.yaml @@ -1,8 +1,7 @@ - ############################## ## General ################### ############################## -bridle_point_node: [0,0,0] #[x,y,z] --> location of kcu +bridle_point_node: [0, 0, 0] #[x,y,z] --> location of kcu ## Mass pulley_mass: !!float 0.1 #[kg] #TODO: would be better to define these per pulley @@ -15,7 +14,6 @@ dyneema: youngs_modulus: 550000000 #[Pa] -> k = E A / l0 damping_per_stiffness: 0.0 # [/s] -> c = damping_per_stiffness * k - ########################### ## Wing ################### ########################### @@ -117,38 +115,38 @@ wing_elements: # line name, measured rest length "l0" [m], stiffness "k" [N/m], damping "c" [N.s/m], mass "m" [kg], linktype headers: [name, l0, k, c, m, linktype] data: - # leading-edge tube (LE[i] -- LE[i+1]) - - [le_1, 0.984642, 2e3, 0, 1, "default"] - - [le_2, 1.327663, 2e3, 0, 1, "default"] - - [le_3, 1.326240, 2e3, 0, 1, "default"] - - [le_4, 1.323655, 2e3, 0, 1, "default"] - - [le_5, 1.347595, 2e3, 0, 1, "default"] - - # struts (chords) - - [strut_1, 1.074728, 2e3, 0, 1, "default"] - - [strut_2, 1.511911, 2e3, 0, 1, "default"] - - [strut_3, 1.960560, 2e3, 0, 1, "default"] - - [strut_4, 2.154185, 2e3, 0, 1, "default"] - - [strut_5, 2.265501, 2e3, 0, 1, "default"] - - # trailing-edge wire (TE[i] -- TE[i+1]) - - [te_1, 0.903854, 1e3, 0, 0.2, "default"] - - [te_2, 1.351057, 1e3, 0, 0.2, "default"] - - [te_3, 1.350587, 1e3, 0, 0.2, "default"] - - [te_4, 1.350462, 1e3, 0, 0.2, "default"] - - [te_5, 1.321072, 1e3, 0, 0.2, "default"] - - # diagonal springs (both directions per panel) - - [dia_1a, 1.360356, 1e3, 0, 0.1, "default"] - - [dia_1b, 1.780032, 1e3, 0, 0.1, "default"] - - [dia_2a, 2.108078, 1e3, 0, 0.1, "default"] - - [dia_2b, 2.251513, 1e3, 0, 0.1, "default"] - - [dia_3a, 2.449188, 1e3, 0, 0.1, "default"] - - [dia_3b, 2.455523, 1e3, 0, 0.1, "default"] - - [dia_4a, 2.601005, 1e3, 0, 0.1, "default"] - - [dia_4b, 2.563136, 1e3, 0, 0.1, "default"] - - [dia_5a, 2.629214, 1e3, 0, 0.1, "default"] - - [dia_5b, 2.629214, 1e3, 0, 0.1, "default"] + # leading-edge tube (LE[i] -- LE[i+1]) + - [le_1, 0.984642, 2e3, 0, 1, 'default'] + - [le_2, 1.327663, 2e3, 0, 1, 'default'] + - [le_3, 1.326240, 2e3, 0, 1, 'default'] + - [le_4, 1.323655, 2e3, 0, 1, 'default'] + - [le_5, 1.347595, 2e3, 0, 1, 'default'] + + # struts (chords) + - [strut_1, 1.074728, 2e3, 0, 1, 'default'] + - [strut_2, 1.511911, 2e3, 0, 1, 'default'] + - [strut_3, 1.960560, 2e3, 0, 1, 'default'] + - [strut_4, 2.154185, 2e3, 0, 1, 'default'] + - [strut_5, 2.265501, 2e3, 0, 1, 'default'] + + # trailing-edge wire (TE[i] -- TE[i+1]) + - [te_1, 0.903854, 1e3, 0, 0.2, 'default'] + - [te_2, 1.351057, 1e3, 0, 0.2, 'default'] + - [te_3, 1.350587, 1e3, 0, 0.2, 'default'] + - [te_4, 1.350462, 1e3, 0, 0.2, 'default'] + - [te_5, 1.321072, 1e3, 0, 0.2, 'default'] + + # diagonal springs (both directions per panel) + - [dia_1a, 1.360356, 1e3, 0, 0.1, 'default'] + - [dia_1b, 1.780032, 1e3, 0, 0.1, 'default'] + - [dia_2a, 2.108078, 1e3, 0, 0.1, 'default'] + - [dia_2b, 2.251513, 1e3, 0, 0.1, 'default'] + - [dia_3a, 2.449188, 1e3, 0, 0.1, 'default'] + - [dia_3b, 2.455523, 1e3, 0, 0.1, 'default'] + - [dia_4a, 2.601005, 1e3, 0, 0.1, 'default'] + - [dia_4b, 2.563136, 1e3, 0, 0.1, 'default'] + - [dia_5a, 2.629214, 1e3, 0, 0.1, 'default'] + - [dia_5b, 2.629214, 1e3, 0, 0.1, 'default'] ############################# ## Bridle ################### @@ -177,71 +175,71 @@ bridle_particles: bridle_connections: # name, ci, cj, ck (this is the third node, only exist when a pulley) - headers: [name, ci, cj, ck] + headers: [name, ci, cj, ck] data: # front lines - - [a6,25,1,33] # pulley - - [A5,3,21] - - [A3,5,21] - - [A2,7,22] - - [A1,9,22] - - [A1,11,23] - - [A2,13,23] - - [A3,15,24] - - [A5,17,24] - - [a6,26,19,35] # pulley - - [AII,21,25] - - [AI,22,25] - - [AI,23,26] - - [AII,24,26] - - [amain,25,0] - - [amain,26,0] + - [a6, 25, 1, 33] # pulley + - [A5, 3, 21] + - [A3, 5, 21] + - [A2, 7, 22] + - [A1, 9, 22] + - [A1, 11, 23] + - [A2, 13, 23] + - [A3, 15, 24] + - [A5, 17, 24] + - [a6, 26, 19, 35] # pulley + - [AII, 21, 25] + - [AI, 22, 25] + - [AI, 23, 26] + - [AII, 24, 26] + - [amain, 25, 0] + - [amain, 26, 0] # rear lines - - [br6,33,2,36] # pulley - - [br5,4,27] - - [br3,6,27] - - [br2,8,28] - - [br1,10,28] - - [br1,12,29] - - [br2,14,29] - - [br3,16,30] - - [br5,18,30] - - [br6,35,20,37] # pulley - - [BRII,27,31] - - [BRI,28,31] - - [BRI,29,32] - - [BRII,30,32] - - [BR-main-1,31,33] - - [BR-main-1,32,35] - - [M-line,36,33,34] # pulley - - [M-line,34,35,37] # pulley - - [Steering Tape,36,0] - - [Power Tape,34,0] - - [Steering Tape,37,0] + - [br6, 33, 2, 36] # pulley + - [br5, 4, 27] + - [br3, 6, 27] + - [br2, 8, 28] + - [br1, 10, 28] + - [br1, 12, 29] + - [br2, 14, 29] + - [br3, 16, 30] + - [br5, 18, 30] + - [br6, 35, 20, 37] # pulley + - [BRII, 27, 31] + - [BRI, 28, 31] + - [BRI, 29, 32] + - [BRII, 30, 32] + - [BR-main-1, 31, 33] + - [BR-main-1, 32, 35] + - [M-line, 36, 33, 34] # pulley + - [M-line, 34, 35, 37] # pulley + - [Steering Tape, 36, 0] + - [Power Tape, 34, 0] + - [Steering Tape, 37, 0] bridle_lines: # line name, measured rest length "l0" [m], stiffness "k" [N/m], damping "c" [N.s/m], diameter "d" [m], density "rho" [kg/m^3] # dyneema has rho = 970 - headers: [name, l0, diameter, material, linktype] + headers: [name, l0, d, material, linktype] data: - # front lines - - [a6, 11.372, 0.005, dyneema, "pulley"] - - [A5, 2.782, 0.005, dyneema, "noncompressive"] - - [A3, 3.253, 0.005, dyneema, "noncompressive"] - - [A2, 3.352, 0.005, dyneema, "noncompressive"] - - [A1, 3.520, 0.005, dyneema, "noncompressive"] - - [AII, 3.188, 0.01, dyneema, "noncompressive"] - - [AI, 3.420, 0.01, dyneema, "noncompressive"] - - [amain, 4.297, 0.02, dyneema, "noncompressive"] - # rear lines - - [br1, 4.448, 0.002, dyneema, "noncompressive"] - - [br2, 4.300, 0.002, dyneema, "noncompressive"] - - [br3, 4.560, 0.002, dyneema, "noncompressive"] - - [br5, 4.047, 0.002, dyneema, "noncompressive"] - - [br6, 14.172, 0.002, dyneema, "pulley"] - - [BRI, 2.388, 0.002, dyneema, "noncompressive"] - - [BRII, 1.890, 0.002, dyneema, "noncompressive"] - - [BR-main-1, 0.874, 0.002, dyneema, "noncompressive"] - - [M-line, 2.196, 0.002, dyneema, "pulley"] - - [Steering Tape, 1.506, 0.002, dyneema, "noncompressive"] - - [Power Tape, 3.129, 0.002, dyneema, "noncompressive"] \ No newline at end of file + # front lines + - [a6, 11.372, 0.005, dyneema, 'pulley'] + - [A5, 2.782, 0.005, dyneema, 'noncompressive'] + - [A3, 3.253, 0.005, dyneema, 'noncompressive'] + - [A2, 3.352, 0.005, dyneema, 'noncompressive'] + - [A1, 3.520, 0.005, dyneema, 'noncompressive'] + - [AII, 3.188, 0.01, dyneema, 'noncompressive'] + - [AI, 3.420, 0.01, dyneema, 'noncompressive'] + - [amain, 4.297, 0.02, dyneema, 'noncompressive'] + # rear lines + - [br1, 4.448, 0.002, dyneema, 'noncompressive'] + - [br2, 4.300, 0.002, dyneema, 'noncompressive'] + - [br3, 4.560, 0.002, dyneema, 'noncompressive'] + - [br5, 4.047, 0.002, dyneema, 'noncompressive'] + - [br6, 14.172, 0.002, dyneema, 'pulley'] + - [BRI, 2.388, 0.002, dyneema, 'noncompressive'] + - [BRII, 1.890, 0.002, dyneema, 'noncompressive'] + - [BR-main-1, 0.874, 0.002, dyneema, 'noncompressive'] + - [M-line, 2.196, 0.002, dyneema, 'pulley'] + - [Steering Tape, 1.506, 0.002, dyneema, 'noncompressive'] + - [Power Tape, 3.129, 0.002, dyneema, 'noncompressive'] diff --git a/data/TUDELFT_V3_KITE/Surfplan_export/TUDELFT_V3_KITE.txt b/data/TUDELFT_V3_KITE/Surfplan_export/TUDELFT_V3_KITE.txt new file mode 100644 index 0000000..717e097 --- /dev/null +++ b/data/TUDELFT_V3_KITE/Surfplan_export/TUDELFT_V3_KITE.txt @@ -0,0 +1,214 @@ +Filename: TUDELFT_V3.sle +Design name: 14 hydrav1 07 +Revision: 1.0 +Designer: +Export Date: jeu. 13/02/2025 16:57:57 + +3d rib positions + LE X; Y; Z; TE X; Y; Z; VUP X; Y; Z +24 + 4,156287; 0,916618; 0,520832; 4,156287; 0,916618; -0,864107; 1,352509; 0,297952; 0,000000 + 3,967978; 1,791356; 0,709238; 3,955506; 1,787553; -1,011852; 1,646267; 0,501952; -0,013040 + 3,805350; 2,209568; 0,790880; 3,789238; 2,199439; -1,189372; 1,676461; 1,053982; -0,019032 + 3,501223; 2,540205; 0,864219; 3,485499; 2,522089; -1,256822; 1,390325; 1,601815; -0,023989 + 3,134335; 2,796493; 0,929298; 3,116753; 2,773988; -1,310901; 1,379198; 1,765306; -0,028558 + 2,798300; 3,092998; 0,986158; 2,780285; 3,065722; -1,354755; 1,290075; 1,953351; -0,032688 + 2,393198; 3,288677; 1,034831; 2,380855; 3,254511; -1,390237; 0,824015; 2,280780; -0,036327 + 1,959729; 3,398648; 1,075348; 1,946587; 3,361473; -1,418530; 0,831221; 2,351276; -0,039430 + 1,553762; 3,587398; 1,107733; 1,540958; 3,547442; -1,440421; 0,777640; 2,426595; -0,041958 + 1,111239; 3,671702; 1,132004; 1,107796; 3,627957; -1,456442; 0,203071; 2,580468; -0,043881 + 0,664252; 3,658834; 1,148177; 0,660721; 3,613797; -1,466944; 0,204434; 2,607118; -0,045175 + 0,225013; 3,741837; 1,156262; 0,220752; 3,696209; -1,472144; 0,244401; 2,617018; -0,045826 + -0,225013; 3,741837; 1,156262; -0,220752; 3,696209; -1,472144; -0,244401; 2,617018; -0,045826 + -0,664252; 3,658834; 1,148177; -0,660721; 3,613797; -1,466944; -0,204434; 2,607118; -0,045175 + -1,111239; 3,671702; 1,132004; -1,107796; 3,627957; -1,456442; -0,203071; 2,580468; -0,043881 + -1,553762; 3,587398; 1,107733; -1,540958; 3,547442; -1,440421; -0,777640; 2,426595; -0,041958 + -1,959729; 3,398648; 1,075348; -1,946587; 3,361473; -1,418530; -0,831221; 2,351276; -0,039430 + -2,393198; 3,288677; 1,034831; -2,380855; 3,254511; -1,390237; -0,824015; 2,280780; -0,036327 + -2,798300; 3,092998; 0,986158; -2,780285; 3,065722; -1,354755; -1,290075; 1,953351; -0,032688 + -3,134335; 2,796493; 0,929298; -3,116753; 2,773988; -1,310901; -1,379198; 1,765306; -0,028558 + -3,501223; 2,540205; 0,864219; -3,485499; 2,522089; -1,256822; -1,390325; 1,601815; -0,023989 + -3,805350; 2,209568; 0,790880; -3,789238; 2,199439; -1,189372; -1,676461; 1,053982; -0,019032 + -3,967978; 1,791356; 0,709238; -3,955506; 1,787553; -1,011852; -1,646267; 0,501952; -0,013040 + -4,156287; 0,916618; 0,520832; -4,156287; 0,916618; -0,864107; -1,352509; 0,297952; 0,000000 + +3d curved wingtip positions + LE X; Y; Z; TE X; Y; Z; VUP X; Y; Z +7 + 4,100957; 0,921215; -0,522180; 4,152601; 0,932607; -0,866820; 1,357902; 0,301698; -0,000239 + 4,105533; 0,971623; -0,263184; 4,141856; 0,979216; -0,874727; 1,373623; 0,312615; -0,000937 + 4,099444; 1,049881; -0,028112; 4,124523; 1,054402; -0,887481; 1,398983; 0,330226; -0,002063 + 4,085799; 1,154511; 0,183243; 4,101072; 1,156125; -0,904737; 1,433293; 0,354052; -0,003586 + 4,065552; 1,283430; 0,368472; 4,071974; 1,282342; -0,926149; 1,475864; 0,383616; -0,005476 + 4,039112; 1,434330; 0,522929; 4,037700; 1,431014; -0,951369; 1,526010; 0,418440; -0,007702 + 4,006563; 1,604842; 0,639766; 3,998721; 1,600098; -0,980052; 1,583040; 0,458044; -0,010233 + +LE tube + Centre X; Y; Z; Diam(mm) +38 + 4,047384; 0,902987; -0,806316; 0,112452 + 4,053468; 0,918198; -0,576557; 0,113170 + 4,052015; 0,967544; -0,320048; 0,115263 + 4,042557; 1,045392; -0,087800; 0,118639 + 4,025810; 1,149644; 0,119933; 0,123207 + 4,002306; 1,277897; 0,300671; 0,128875 + 3,972396; 1,427448; 0,449839; 0,135551 + 3,936225; 1,595488; 0,560835; 0,143143 + 3,967429; 1,791188; 0,633460; 0,151561 + 3,804690; 2,209154; 0,709853; 0,162061 + 3,500590; 2,539476; 0,778809; 0,170831 + 3,133636; 2,795597; 0,840178; 0,178254 + 2,797590; 3,091923; 0,893902; 0,184529 + 2,392716; 3,287341; 0,939956; 0,189772 + 1,959218; 3,397202; 0,978330; 0,194060 + 1,553266; 3,585851; 1,009025; 0,197442 + 1,111106; 3,670013; 1,032043; 0,199952 + 0,664116; 3,657099; 1,047385; 0,201615 + 0,224849; 3,740081; 1,055068; 0,202418 + -0,224849; 3,740081; 1,055068; 0,202418 + -0,664116; 3,657099; 1,047385; 0,201615 + -1,111106; 3,670013; 1,032043; 0,199952 + -1,553266; 3,585851; 1,009025; 0,197442 + -1,959218; 3,397202; 0,978330; 0,194060 + -2,392716; 3,287341; 0,939956; 0,189772 + -2,797590; 3,091923; 0,893902; 0,184529 + -3,133636; 2,795597; 0,840178; 0,178254 + -3,500590; 2,539476; 0,778809; 0,170831 + -3,804690; 2,209154; 0,709853; 0,162061 + -3,967429; 1,791188; 0,633460; 0,151561 + -3,936225; 1,595488; 0,560835; 0,143143 + -3,972396; 1,427448; 0,449839; 0,135551 + -4,002306; 1,277897; 0,300671; 0,128875 + -4,025810; 1,149644; 0,119933; 0,123207 + -4,042557; 1,045392; -0,087800; 0,118639 + -4,052015; 0,967544; -0,320048; 0,115263 + -4,053468; 0,918198; -0,576557; 0,113170 + -4,047384; 0,902987; -0,806316; 0,112452 + +Strut 2 + Centre X; Y; Z; Diam(m) +7 + 3,992381; 1,798912; 0,562351; 0,105697 + 4,013188; 1,805140; 0,397453; 0,099891 + 4,000806; 1,801365; 0,121209; 0,097265 + 3,980386; 1,795139; -0,157776; 0,092566 + 3,959762; 1,788850; -0,439564; 0,087908 + 3,938932; 1,782499; -0,724157; 0,083203 + 3,919906; 1,776698; -0,984103; 0,078907 + +Strut 5 + Centre X; Y; Z; Diam(m) +7 + 3,165429; 2,835935; 0,767330; 0,110490 + 3,198103; 2,878112; 0,526561; 0,128371 + 3,183835; 2,859850; 0,166347; 0,124894 + 3,159895; 2,829208; -0,197326; 0,117965 + 3,135714; 2,798257; -0,564655; 0,111190 + 3,111292; 2,766998; -0,935639; 0,104346 + 3,088872; 2,738302; -1,276210; 0,098064 + +Strut 8 + Centre X; Y; Z; Diam(m) +8 + 1,979699; 3,454888; 0,903062; 0,131981 + 2,006196; 3,530089; 0,681343; 0,141738 + 2,001981; 3,518165; 0,340947; 0,141175 + 1,987651; 3,477629; -0,004804; 0,133769 + 1,973148; 3,436605; -0,353542; 0,126804 + 1,958521; 3,395229; -0,705269; 0,119783 + 1,943769; 3,353502; -1,059985; 0,112703 + 1,930428; 3,315762; -1,380803; 0,106299 + +Strut 11 + Centre X; Y; Z; Diam(m) +8 + 0,669201; 3,721894; 0,970331; 0,140834 + 0,676930; 3,820506; 0,734465; 0,142401 + 0,675849; 3,806722; 0,377913; 0,142268 + 0,672073; 3,758564; 0,015391; 0,134521 + 0,668249; 3,709798; -0,350263; 0,127292 + 0,664392; 3,660613; -0,719051; 0,120005 + 0,660502; 3,611011; -1,090973; 0,112657 + 0,656965; 3,565894; -1,429263; 0,105972 + +Strut 14 + Centre X; Y; Z; Diam(m) +8 + -0,669201; 3,721894; 0,970331; 0,140834 + -0,676930; 3,820506; 0,734465; 0,142401 + -0,675849; 3,806722; 0,377913; 0,142268 + -0,672073; 3,758564; 0,015391; 0,134521 + -0,668249; 3,709798; -0,350263; 0,127292 + -0,664392; 3,660613; -0,719051; 0,120005 + -0,660502; 3,611011; -1,090973; 0,112657 + -0,656965; 3,565894; -1,429263; 0,105972 + +Strut 17 + Centre X; Y; Z; Diam(m) +8 + -1,979699; 3,454888; 0,903062; 0,131981 + -2,006196; 3,530089; 0,681343; 0,141738 + -2,001981; 3,518165; 0,340947; 0,141175 + -1,987651; 3,477629; -0,004804; 0,133769 + -1,973148; 3,436605; -0,353542; 0,126804 + -1,958521; 3,395229; -0,705269; 0,119783 + -1,943769; 3,353502; -1,059985; 0,112703 + -1,930428; 3,315762; -1,380803; 0,106299 + +Strut 20 + Centre X; Y; Z; Diam(m) +7 + -3,165429; 2,835935; 0,767330; 0,110490 + -3,198103; 2,878112; 0,526561; 0,128371 + -3,183835; 2,859850; 0,166347; 0,124894 + -3,159895; 2,829208; -0,197326; 0,117965 + -3,135714; 2,798257; -0,564655; 0,111190 + -3,111292; 2,766998; -0,935639; 0,104346 + -3,088872; 2,738302; -1,276210; 0,098064 + +Strut 23 + Centre X; Y; Z; Diam(m) +7 + -3,992381; 1,798912; 0,562351; 0,105697 + -4,013188; 1,805140; 0,397453; 0,099891 + -4,000806; 1,801365; 0,121209; 0,097265 + -3,980386; 1,795139; -0,157776; 0,092566 + -3,959762; 1,788850; -0,439564; 0,087908 + -3,938932; 1,782499; -0,724157; 0,083203 + -3,919906; 1,776698; -0,984103; 0,078907 + +3d Bridle + TopX; Y; Z; BottomX; Y; Z; Name ; Length; Material; Diameter + 0,658922; 3,590858; 1,123196; 0,682069; 3,407109; 1,014197; a1 ; 0,214896; ; 2,000000 + 1,937954; 3,337051; 1,051436; 1,898143; 3,164487; 0,942461; a2 ; 0,207939; ; 2,000000 + 3,097783; 2,749708; 0,907650; 3,052266; 2,575994; 0,791988; a3 ; 0,213602; ; 2,000000 + 3,921859; 1,777294; 0,692391; 3,825134; 1,648422; 0,616812; a4 ; 0,177977; ; 2,000000 + 0,682069; 3,407109; 1,014197; 0,887905; 1,721653; 0,769331; A1 ; 1,715543; ; 2,000000 + 1,898143; 3,164487; 0,942461; 1,532504; 1,590653; 0,731702; A2 ; 1,629437; ; 2,000000 + 3,052266; 2,575994; 0,791988; 2,644934; 1,015550; 0,607355; A3 ; 1,623267; ; 2,000000 + 3,825134; 1,648422; 0,616812; 2,172485; -0,743695; 0,618402; A4 ; 2,907486; ; 2,000000 + 0,887905; 1,721653; 0,769331; 1,113164; -0,195037; 0,711827; AI ; 1,930738; ; 3,000000 + 1,532504; 1,590653; 0,731702; 1,113164; -0,195037; 0,711827; AII ; 1,834374; ; 3,000000 + 2,644934; 1,015550; 0,607355; 2,172485; -0,743695; 0,618402; AIII ; 1,821612; ; 3,000000 + 1,113164; -0,195037; 0,711827; 0,941442; -3,901961; 0,633144; amain ; 3,711733; ; 4,000000 + 2,172485; -0,743695; 0,618402; 0,941442; -3,901961; 0,633144; amain2 ; 3,389739; ; 4,000000 + 0,941442; -3,901961; 0,633144; 0,910001; -4,099458; 0,630632; a5.1 ; 0,200000; ; 5,000000 + 0,659453; 3,597625; 0,966125; 0,682069; 3,407109; 1,014197; b1 ; 0,197785; ; 2,000000 + 1,939381; 3,341088; 0,901698; 1,898143; 3,164487; 0,942461; b2 ; 0,185876; ; 2,000000 + 3,097418; 2,749241; 0,773223; 3,052266; 2,575994; 0,791988; b3 ; 0,180014; ; 2,000000 + 3,908574; 1,773243; 0,589225; 3,825134; 1,648422; 0,616812; b4 ; 0,152655; ; 2,000000 + 0,670112; 3,733556; 0,362112; 0,887905; 1,721653; 0,769331; e1 ; 2,064223; ; 2,000000 + 1,977801; 3,449767; 0,326140; 1,532504; 1,590653; 0,731702; e2 ; 1,954245; ; 2,000000 + 3,148563; 2,814703; 0,256835; 2,644934; 1,015550; 0,607355; e3 ; 1,900910; ; 2,000000 + 0,654966; 3,540403; -1,204082; 1,155974; -0,902939; -0,473306; br1 ; 4,530820; ; 2,000000 + 1,920720; 3,288303; -1,167853; 1,155974; -0,902939; -0,473306; br2 ; 4,316681; ; 2,000000 + 3,069960; 2,714095; -1,085876; 1,921865; -1,309919; -0,396111; br3 ; 4,241059; ; 2,000000 + 3,891515; 1,768042; -0,839226; 1,921865; -1,309919; -0,396111; br4 ; 3,680993; ; 2,000000 + 4,142726; 0,875716; -0,615380; 3,821673; 0,392361; -0,630484; br5 ; 0,580461; ; 2,000000 + 4,137746; 0,864315; -0,791897; 3,821673; 0,392361; -0,630484; br6 ; 0,590505; ; 2,000000 + 1,155974; -0,902939; -0,473306; 1,087879; -3,228214; -0,089481; BR1 ; 2,357724; ; 2,000000 + 1,921865; -1,309919; -0,396111; 1,087879; -3,228214; -0,089481; BR2 ; 2,114098; ; 2,000000 + 3,821673; 0,392361; -0,630484; 0,954417; -3,906964; 0,021095; BR3 ; 5,208639; ; 2,000000 + 1,087879; -3,228214; -0,089481; 0,954417; -3,906964; 0,021095; BRI ; 0,700529; ; 3,000000 + 0,954417; -3,906964; 0,021095; 0,910001; -4,099458; 0,052295; brmain ; 0,200000; ; 4,000000 + diff --git a/data/TUDELFT_V3_KITE/Surfplan_export/profiles/V3D_AVL.avl b/data/TUDELFT_V3_KITE/Surfplan_export/profiles/V3D_AVL.avl new file mode 100644 index 0000000..4519c16 --- /dev/null +++ b/data/TUDELFT_V3_KITE/Surfplan_export/profiles/V3D_AVL.avl @@ -0,0 +1,116 @@ +Empty Project +0.0 | Mach +0 0 0.0 | iYsym iZsym Zsym + + 25.000 2.630 11.180 | Sref Cref Bref + 0.00 0.00 0.00 | Xref Yref Zref + 0.00 | CDp (optional) + + + +#================================================= +SURFACE | (keyword) +14 hydrav1 07 +#Nchord Cspace [ Nspan Sspace ] + 10 1.0 + +INDEX | (keyword) +111 | Lsurf + +YDUPLICATE +0.0 + +SCALE +1.0 1.0 1.0 + +TRANSLATE +0.0 0.0 0.0 + +ANGLE +0.0 | dAinc + +#__________________________________ +SECTION | (keyword) + 0.0000 0.2250 0.0000 2.6288 0.9989 3 0 | Xle Yle Zle Chord Ainc [ Nspan Sspace ] + +AFIL 0.0 1.0 +G:\Shared drives\Hardware\01 - Kite and Bridle (KB)\Archived kites\V3.25\V3.25D\VSM Package\prof_1.dat + +#______________ +SECTION | (keyword) + 0.0081 0.6643 -0.0830 2.6155 0.9897 3 0 | Xle Yle Zle Chord Ainc [ Nspan Sspace ] + +AFIL 0.0 1.0 +G:\Shared drives\Hardware\01 - Kite and Bridle (KB)\Archived kites\V3.25\V3.25D\VSM Package\prof_2.dat + +#______________ +SECTION | (keyword) + 0.0243 1.1112 -0.0701 2.5888 0.9712 3 0 | Xle Yle Zle Chord Ainc [ Nspan Sspace ] + +AFIL 0.0 1.0 +G:\Shared drives\Hardware\01 - Kite and Bridle (KB)\Archived kites\V3.25\V3.25D\VSM Package\prof_3.dat + +#______________ +SECTION | (keyword) + 0.0485 1.5538 -0.1544 2.5485 0.9433 3 0 | Xle Yle Zle Chord Ainc [ Nspan Sspace ] + +AFIL 0.0 1.0 +G:\Shared drives\Hardware\01 - Kite and Bridle (KB)\Archived kites\V3.25\V3.25D\VSM Package\prof_4.dat + +#______________ +SECTION | (keyword) + 0.0809 1.9597 -0.3432 2.4942 0.9058 3 0 | Xle Yle Zle Chord Ainc [ Nspan Sspace ] + +AFIL 0.0 1.0 +G:\Shared drives\Hardware\01 - Kite and Bridle (KB)\Archived kites\V3.25\V3.25D\VSM Package\prof_5.dat + +#______________ +SECTION | (keyword) + 0.1214 2.3932 -0.4532 2.4253 0.8582 3 0 | Xle Yle Zle Chord Ainc [ Nspan Sspace ] + +AFIL 0.0 1.0 +G:\Shared drives\Hardware\01 - Kite and Bridle (KB)\Archived kites\V3.25\V3.25D\VSM Package\prof_6.dat + +#______________ +SECTION | (keyword) + 0.1701 2.7983 -0.6488 2.3411 0.8000 3 0 | Xle Yle Zle Chord Ainc [ Nspan Sspace ] + +AFIL 0.0 1.0 +G:\Shared drives\Hardware\01 - Kite and Bridle (KB)\Archived kites\V3.25\V3.25D\VSM Package\prof_7.dat + +#______________ +SECTION | (keyword) + 0.2270 3.1343 -0.9453 2.2404 0.7304 3 0 | Xle Yle Zle Chord Ainc [ Nspan Sspace ] + +AFIL 0.0 1.0 +G:\Shared drives\Hardware\01 - Kite and Bridle (KB)\Archived kites\V3.25\V3.25D\VSM Package\prof_8.dat + +#______________ +SECTION | (keyword) + 0.2920 3.5012 -1.2016 2.1212 0.6480 3 0 | Xle Yle Zle Chord Ainc [ Nspan Sspace ] + +AFIL 0.0 1.0 +G:\Shared drives\Hardware\01 - Kite and Bridle (KB)\Archived kites\V3.25\V3.25D\VSM Package\prof_9.dat + +#______________ +SECTION | (keyword) + 0.3654 3.8053 -1.5323 1.9803 0.5506 3 0 | Xle Yle Zle Chord Ainc [ Nspan Sspace ] + +AFIL 0.0 1.0 +G:\Shared drives\Hardware\01 - Kite and Bridle (KB)\Archived kites\V3.25\V3.25D\VSM Package\prof_10.dat + +#______________ +SECTION | (keyword) + 0.4470 3.9680 -1.9505 1.7211 0.4341 3 0 | Xle Yle Zle Chord Ainc [ Nspan Sspace ] + +AFIL 0.0 1.0 +G:\Shared drives\Hardware\01 - Kite and Bridle (KB)\Archived kites\V3.25\V3.25D\VSM Package\prof_11.dat + +#______________ +SECTION | (keyword) + 0.6354 4.1563 -2.8252 1.3849 0.0000 3 0 | Xle Yle Zle Chord Ainc [ Nspan Sspace ] + +AFIL 0.0 1.0 +G:\Shared drives\Hardware\01 - Kite and Bridle (KB)\Archived kites\V3.25\V3.25D\VSM Package\prof_12.dat + +#______________ diff --git a/data/TUDELFT_V3_KITE/Surfplan_export/profiles/prof_1.dat b/data/TUDELFT_V3_KITE/Surfplan_export/profiles/prof_1.dat new file mode 100644 index 0000000..0c7db24 --- /dev/null +++ b/data/TUDELFT_V3_KITE/Surfplan_export/profiles/prof_1.dat @@ -0,0 +1,151 @@ +rib_1 + 1.0000000 0.0000000 + 0.9577769 0.0053610 + 0.9081751 0.0116589 + 0.8585733 0.0179567 + 0.8089715 0.0242546 + 0.7887696 0.0268196 + 0.7593697 0.0305525 + 0.7097680 0.0368503 + 0.6601662 0.0431482 + 0.6105644 0.0494461 + 0.5983523 0.0509966 + 0.5609626 0.0557439 + 0.5113608 0.0620418 + 0.4617590 0.0683397 + 0.4301315 0.0723554 + 0.4121573 0.0746375 + 0.3625555 0.0809354 + 0.3129537 0.0872333 + 0.2944570 0.0895817 + 0.2864751 0.0899438 + 0.2631344 0.0910025 + 0.2431549 0.0919087 + 0.2231755 0.0928149 + 0.2031960 0.0937211 + 0.1832166 0.0946273 + 0.1750000 0.0950000 + 0.1696407 0.0944874 + 0.1632797 0.0938790 + 0.1433727 0.0919749 + 0.1234657 0.0900708 + 0.1222984 0.0899592 + 0.1039075 0.0859758 + 0.0941392 0.0838600 + 0.0842841 0.0817254 + 0.0818568 0.0807479 + 0.0751055 0.0780291 + 0.0658445 0.0742997 + 0.0565836 0.0705702 + 0.0529896 0.0691229 + 0.0478730 0.0658051 + 0.0395115 0.0603832 + 0.0311500 0.0549613 + 0.0292787 0.0537479 + 0.0290956 0.0535627 + 0.0273482 0.0517953 + 0.0256109 0.0500381 + 0.0256009 0.0500279 + 0.0238535 0.0482605 + 0.0221062 0.0464932 + 0.0203588 0.0447258 + 0.0186115 0.0429584 + 0.0168641 0.0411910 + 0.0151168 0.0394236 + 0.0133694 0.0376563 + 0.0132474 0.0375328 + 0.0121593 0.0355013 + 0.0109895 0.0333173 + 0.0098198 0.0311333 + 0.0086500 0.0289492 + 0.0074877 0.0267791 + 0.0066048 0.0244503 + 0.0057275 0.0221362 + 0.0048502 0.0198222 + 0.0039729 0.0175081 + 0.0030956 0.0151940 + 0.0022184 0.0128799 + 0.0021212 0.0123489 + 0.0013119 0.0099645 + 0.0008206 0.0074949 + 0.0003294 0.0050253 + 0.0001647 0.0025126 + 0.0000000 0.0000000 + 0.0001647 -0.0025126 + 0.0003294 -0.0050253 + 0.0008206 -0.0074949 + 0.0013119 -0.0099645 + 0.0021212 -0.0123489 + 0.0029306 -0.0147333 + 0.0040443 -0.0169917 + 0.0051580 -0.0192500 + 0.0065570 -0.0213437 + 0.0079559 -0.0234373 + 0.0096161 -0.0253305 + 0.0112764 -0.0272236 + 0.0131695 -0.0288839 + 0.0150627 -0.0305441 + 0.0171563 -0.0319430 + 0.0192500 -0.0333420 + 0.0215083 -0.0344557 + 0.0237667 -0.0355694 + 0.0261511 -0.0363788 + 0.0285355 -0.0371881 + 0.0310051 -0.0376794 + 0.0334747 -0.0381706 + 0.0359874 -0.0383353 + 0.0385000 -0.0385000 + 0.0410126 -0.0383353 + 0.0435253 -0.0381706 + 0.0459949 -0.0376794 + 0.0484645 -0.0371881 + 0.0508489 -0.0363788 + 0.0532333 -0.0355694 + 0.0554917 -0.0344557 + 0.0577500 -0.0333420 + 0.0598437 -0.0319430 + 0.0619373 -0.0305441 + 0.0638305 -0.0288839 + 0.0657236 -0.0272236 + 0.0673839 -0.0253305 + 0.0690441 -0.0234373 + 0.0704430 -0.0213437 + 0.0718420 -0.0192500 + 0.0729557 -0.0169917 + 0.0740694 -0.0147333 + 0.0751055 -0.0127256 + 0.0818568 -0.0003134 + 0.0842841 0.0036228 + 0.0941392 0.0180415 + 0.1039075 0.0299185 + 0.1222984 0.0475825 + 0.1234657 0.0485200 + 0.1433727 0.0624018 + 0.1632797 0.0725964 + 0.1696407 0.0752301 + 0.1750000 0.0772601 + 0.1832166 0.0800012 + 0.2031960 0.0851738 + 0.2231755 0.0884966 + 0.2431549 0.0902580 + 0.2631344 0.0907111 + 0.2864751 0.0889438 + 0.2944570 0.0885817 + 0.3129537 0.0862333 + 0.3625555 0.0799354 + 0.4121573 0.0736375 + 0.4301315 0.0713554 + 0.4617590 0.0673397 + 0.5113608 0.0610418 + 0.5609626 0.0547439 + 0.5983523 0.0499966 + 0.6105644 0.0484461 + 0.6601662 0.0421482 + 0.7097680 0.0358503 + 0.7593697 0.0295525 + 0.7887696 0.0258196 + 0.8089715 0.0232546 + 0.8585733 0.0169567 + 0.9081751 0.0106589 + 0.9577769 0.0043610 + 1.0000000 -0.0010000 diff --git a/data/TUDELFT_V3_KITE/Surfplan_export/profiles/prof_1.png b/data/TUDELFT_V3_KITE/Surfplan_export/profiles/prof_1.png new file mode 100644 index 0000000..6694b81 Binary files /dev/null and b/data/TUDELFT_V3_KITE/Surfplan_export/profiles/prof_1.png differ diff --git a/data/TUDELFT_V3_KITE/Surfplan_export/profiles/prof_10.dat b/data/TUDELFT_V3_KITE/Surfplan_export/profiles/prof_10.dat new file mode 100644 index 0000000..5bec58d --- /dev/null +++ b/data/TUDELFT_V3_KITE/Surfplan_export/profiles/prof_10.dat @@ -0,0 +1,150 @@ +rib_10 + 1.0000000 0.0000000 + 0.9577769 0.0036927 + 0.9081751 0.0080307 + 0.8585733 0.0123687 + 0.8089715 0.0167067 + 0.7887696 0.0184735 + 0.7593697 0.0210447 + 0.7097680 0.0253827 + 0.6601662 0.0297207 + 0.6105644 0.0340587 + 0.5983523 0.0351267 + 0.5609626 0.0383967 + 0.5113608 0.0427347 + 0.4617590 0.0470727 + 0.4301315 0.0498387 + 0.4121573 0.0514107 + 0.3625555 0.0557487 + 0.3129537 0.0600867 + 0.2944570 0.0617043 + 0.2864751 0.0620433 + 0.2631344 0.0629229 + 0.2431549 0.0635471 + 0.2231755 0.0641713 + 0.2031960 0.0647955 + 0.1832166 0.0654197 + 0.1750000 0.0656764 + 0.1696407 0.0654557 + 0.1632797 0.0651937 + 0.1433727 0.0642449 + 0.1234657 0.0630378 + 0.1222984 0.0629670 + 0.1039075 0.0607509 + 0.0941392 0.0594096 + 0.0842841 0.0580564 + 0.0818568 0.0575028 + 0.0751055 0.0559631 + 0.0658445 0.0536768 + 0.0565836 0.0513126 + 0.0529896 0.0503951 + 0.0478730 0.0484757 + 0.0395115 0.0451268 + 0.0311500 0.0416897 + 0.0292787 0.0409204 + 0.0290956 0.0408128 + 0.0273482 0.0397851 + 0.0256109 0.0387634 + 0.0256009 0.0387575 + 0.0238535 0.0376639 + 0.0221062 0.0365435 + 0.0203588 0.0354231 + 0.0186115 0.0343027 + 0.0168641 0.0331009 + 0.0151168 0.0317578 + 0.0133694 0.0305365 + 0.0132474 0.0304573 + 0.0121593 0.0292922 + 0.0109895 0.0279934 + 0.0098198 0.0265927 + 0.0086500 0.0251605 + 0.0074877 0.0237382 + 0.0066048 0.0223433 + 0.0057275 0.0208784 + 0.0048502 0.0193234 + 0.0039729 0.0175881 + 0.0038336 0.0172924 + 0.0031147 0.0156584 + 0.0022544 0.0131243 + 0.0013942 0.0105902 + 0.0008721 0.0079655 + 0.0003501 0.0053408 + 0.0001750 0.0026704 + 0.0000000 0.0000000 + 0.0001750 -0.0026704 + 0.0003501 -0.0053408 + 0.0008721 -0.0079655 + 0.0013942 -0.0105902 + 0.0022544 -0.0131243 + 0.0031147 -0.0156584 + 0.0042983 -0.0180585 + 0.0054819 -0.0204587 + 0.0069687 -0.0226838 + 0.0084554 -0.0249089 + 0.0102199 -0.0269209 + 0.0119844 -0.0289330 + 0.0139964 -0.0306975 + 0.0160085 -0.0324619 + 0.0182336 -0.0339487 + 0.0204587 -0.0354355 + 0.0228588 -0.0366191 + 0.0252590 -0.0378027 + 0.0277931 -0.0386629 + 0.0303272 -0.0395232 + 0.0329519 -0.0400452 + 0.0355766 -0.0405673 + 0.0382470 -0.0407424 + 0.0409174 -0.0409174 + 0.0435878 -0.0407424 + 0.0462582 -0.0405673 + 0.0488829 -0.0400452 + 0.0515076 -0.0395232 + 0.0540417 -0.0386629 + 0.0565758 -0.0378027 + 0.0589759 -0.0366191 + 0.0613761 -0.0354355 + 0.0636012 -0.0339487 + 0.0658263 -0.0324619 + 0.0678383 -0.0306975 + 0.0698503 -0.0289330 + 0.0716148 -0.0269209 + 0.0733793 -0.0249089 + 0.0748661 -0.0226838 + 0.0763529 -0.0204587 + 0.0775365 -0.0180585 + 0.0787201 -0.0156584 + 0.0818568 -0.0097543 + 0.0842841 -0.0056869 + 0.0941392 0.0077849 + 0.1039075 0.0179685 + 0.1222984 0.0319512 + 0.1234657 0.0326843 + 0.1433727 0.0430118 + 0.1632797 0.0502675 + 0.1696407 0.0520978 + 0.1750000 0.0534903 + 0.1832166 0.0553847 + 0.2031960 0.0589190 + 0.2231755 0.0611435 + 0.2431549 0.0623109 + 0.2631344 0.0626067 + 0.2864751 0.0610433 + 0.2944570 0.0607043 + 0.3129537 0.0590867 + 0.3625555 0.0547487 + 0.4121573 0.0504107 + 0.4301315 0.0488387 + 0.4617590 0.0460727 + 0.5113608 0.0417347 + 0.5609626 0.0373967 + 0.5983523 0.0341267 + 0.6105644 0.0330587 + 0.6601662 0.0287207 + 0.7097680 0.0243827 + 0.7593697 0.0200447 + 0.7887696 0.0174735 + 0.8089715 0.0157067 + 0.8585733 0.0113687 + 0.9081751 0.0070307 + 0.9577769 0.0026927 + 1.0000000 -0.0010000 diff --git a/data/TUDELFT_V3_KITE/Surfplan_export/profiles/prof_10.png b/data/TUDELFT_V3_KITE/Surfplan_export/profiles/prof_10.png new file mode 100644 index 0000000..0591d8d Binary files /dev/null and b/data/TUDELFT_V3_KITE/Surfplan_export/profiles/prof_10.png differ diff --git a/data/TUDELFT_V3_KITE/Surfplan_export/profiles/prof_11.dat b/data/TUDELFT_V3_KITE/Surfplan_export/profiles/prof_11.dat new file mode 100644 index 0000000..fa35a81 --- /dev/null +++ b/data/TUDELFT_V3_KITE/Surfplan_export/profiles/prof_11.dat @@ -0,0 +1,147 @@ +rib_11 + 1.0000000 0.0000000 + 0.9577769 0.0032589 + 0.9081751 0.0070873 + 0.8585733 0.0109156 + 0.8089715 0.0147440 + 0.7887696 0.0163032 + 0.7593697 0.0185724 + 0.7097680 0.0224008 + 0.6601662 0.0262292 + 0.6105644 0.0300575 + 0.5983523 0.0310001 + 0.5609626 0.0338859 + 0.5113608 0.0377143 + 0.4617590 0.0415427 + 0.4301315 0.0439838 + 0.4121573 0.0453711 + 0.3625555 0.0491994 + 0.3129537 0.0530278 + 0.2944570 0.0544554 + 0.2864751 0.0547884 + 0.2631344 0.0556214 + 0.2431549 0.0561723 + 0.2231755 0.0567232 + 0.2031960 0.0572741 + 0.1832166 0.0578249 + 0.1750000 0.0580515 + 0.1696407 0.0579299 + 0.1632797 0.0577855 + 0.1433727 0.0572106 + 0.1234657 0.0563887 + 0.1222984 0.0563406 + 0.1039075 0.0549167 + 0.0941392 0.0540035 + 0.0842841 0.0530821 + 0.0818568 0.0527233 + 0.0751055 0.0517256 + 0.0658445 0.0501903 + 0.0565836 0.0485806 + 0.0529896 0.0479558 + 0.0478730 0.0466996 + 0.0395115 0.0444437 + 0.0311500 0.0421034 + 0.0292787 0.0414955 + 0.0290956 0.0414245 + 0.0273482 0.0407469 + 0.0256109 0.0399917 + 0.0256009 0.0399871 + 0.0238535 0.0391345 + 0.0221062 0.0381831 + 0.0203588 0.0371252 + 0.0186115 0.0359515 + 0.0168641 0.0346501 + 0.0151168 0.0332060 + 0.0133694 0.0315998 + 0.0132474 0.0314809 + 0.0121593 0.0303789 + 0.0109895 0.0291024 + 0.0098198 0.0277180 + 0.0086500 0.0262085 + 0.0074877 0.0245619 + 0.0066048 0.0231945 + 0.0057275 0.0217152 + 0.0048502 0.0200893 + 0.0041252 0.0186075 + 0.0033515 0.0168492 + 0.0024259 0.0141224 + 0.0015003 0.0113956 + 0.0009385 0.0085713 + 0.0003767 0.0057470 + 0.0001883 0.0028735 + 0.0000000 0.0000000 + 0.0001883 -0.0028735 + 0.0003767 -0.0057470 + 0.0009385 -0.0085713 + 0.0015003 -0.0113956 + 0.0024259 -0.0141224 + 0.0033515 -0.0168492 + 0.0046252 -0.0194319 + 0.0058988 -0.0220146 + 0.0074986 -0.0244089 + 0.0090985 -0.0268033 + 0.0109972 -0.0289683 + 0.0128958 -0.0311333 + 0.0150609 -0.0330320 + 0.0172259 -0.0349307 + 0.0196202 -0.0365305 + 0.0220146 -0.0381304 + 0.0245973 -0.0394040 + 0.0271799 -0.0406776 + 0.0299068 -0.0416033 + 0.0326336 -0.0425289 + 0.0354579 -0.0430907 + 0.0382822 -0.0436525 + 0.0411557 -0.0438408 + 0.0440292 -0.0440292 + 0.0469026 -0.0438408 + 0.0497761 -0.0436525 + 0.0526004 -0.0430907 + 0.0554248 -0.0425289 + 0.0581516 -0.0416033 + 0.0608784 -0.0406776 + 0.0634611 -0.0394040 + 0.0660438 -0.0381304 + 0.0684381 -0.0365305 + 0.0708324 -0.0349307 + 0.0729975 -0.0330320 + 0.0751625 -0.0311333 + 0.0770612 -0.0289683 + 0.0789599 -0.0268033 + 0.0805597 -0.0244089 + 0.0821595 -0.0220146 + 0.0834332 -0.0194319 + 0.0847068 -0.0168492 + 0.0941392 -0.0014548 + 0.1039075 0.0097812 + 0.1222984 0.0245290 + 0.1234657 0.0252929 + 0.1433727 0.0357364 + 0.1632797 0.0429857 + 0.1696407 0.0448194 + 0.1750000 0.0461895 + 0.1832166 0.0480633 + 0.2031960 0.0515148 + 0.2231755 0.0537148 + 0.2431549 0.0549061 + 0.2631344 0.0552536 + 0.2864751 0.0547754 + 0.2944570 0.0534554 + 0.3129537 0.0520278 + 0.3625555 0.0481994 + 0.4121573 0.0443711 + 0.4301315 0.0429838 + 0.4617590 0.0405427 + 0.5113608 0.0367143 + 0.5609626 0.0328859 + 0.5983523 0.0300001 + 0.6105644 0.0290575 + 0.6601662 0.0252292 + 0.7097680 0.0214008 + 0.7593697 0.0175724 + 0.7887696 0.0153032 + 0.8089715 0.0137440 + 0.8585733 0.0099156 + 0.9081751 0.0060873 + 0.9577769 0.0022589 + 1.0000000 -0.0010000 diff --git a/data/TUDELFT_V3_KITE/Surfplan_export/profiles/prof_11.png b/data/TUDELFT_V3_KITE/Surfplan_export/profiles/prof_11.png new file mode 100644 index 0000000..97f608f Binary files /dev/null and b/data/TUDELFT_V3_KITE/Surfplan_export/profiles/prof_11.png differ diff --git a/data/TUDELFT_V3_KITE/Surfplan_export/profiles/prof_12.dat b/data/TUDELFT_V3_KITE/Surfplan_export/profiles/prof_12.dat new file mode 100644 index 0000000..68cc973 --- /dev/null +++ b/data/TUDELFT_V3_KITE/Surfplan_export/profiles/prof_12.dat @@ -0,0 +1,150 @@ +rib_12 + 1.0000000 0.0000000 + 0.9577769 0.0017841 + 0.9081751 0.0038799 + 0.8585733 0.0059757 + 0.8089715 0.0080716 + 0.7887696 0.0089252 + 0.7593697 0.0101674 + 0.7097680 0.0122632 + 0.6601662 0.0143591 + 0.6105644 0.0164549 + 0.5983523 0.0169709 + 0.5609626 0.0185507 + 0.5113608 0.0206466 + 0.4617590 0.0227424 + 0.4301315 0.0240788 + 0.4121573 0.0248383 + 0.3625555 0.0269341 + 0.3129537 0.0290299 + 0.2944570 0.0298115 + 0.2864751 0.0301487 + 0.2631344 0.0311350 + 0.2431549 0.0319791 + 0.2231755 0.0328233 + 0.2031960 0.0336675 + 0.1832166 0.0345117 + 0.1750000 0.0348589 + 0.1696407 0.0350854 + 0.1632797 0.0353541 + 0.1433727 0.0361953 + 0.1234657 0.0370364 + 0.1222984 0.0370857 + 0.1039075 0.0378628 + 0.0941392 0.0382755 + 0.0842841 0.0386920 + 0.0818568 0.0387945 + 0.0751055 0.0390798 + 0.0658445 0.0394711 + 0.0565836 0.0398624 + 0.0529896 0.0400142 + 0.0478730 0.0402304 + 0.0395115 0.0405837 + 0.0311500 0.0394836 + 0.0292787 0.0389883 + 0.0290956 0.0389347 + 0.0273482 0.0383752 + 0.0256109 0.0377306 + 0.0256009 0.0377266 + 0.0238535 0.0369842 + 0.0221062 0.0361423 + 0.0203588 0.0351935 + 0.0186115 0.0341292 + 0.0168641 0.0329380 + 0.0151168 0.0316056 + 0.0133694 0.0301133 + 0.0132474 0.0300025 + 0.0121593 0.0289732 + 0.0109895 0.0277767 + 0.0098198 0.0264746 + 0.0086500 0.0250505 + 0.0074877 0.0234927 + 0.0066048 0.0221960 + 0.0057275 0.0207906 + 0.0048502 0.0192431 + 0.0039729 0.0175158 + 0.0038037 0.0171576 + 0.0030904 0.0155363 + 0.0022369 0.0130220 + 0.0013834 0.0105076 + 0.0008653 0.0079034 + 0.0003473 0.0052991 + 0.0001737 0.0026496 + 0.0000000 0.0000000 + 0.0001737 -0.0026496 + 0.0003473 -0.0052991 + 0.0008653 -0.0079034 + 0.0013834 -0.0105076 + 0.0022369 -0.0130220 + 0.0030904 -0.0155363 + 0.0042648 -0.0179177 + 0.0054391 -0.0202991 + 0.0069143 -0.0225069 + 0.0083895 -0.0247147 + 0.0101402 -0.0267110 + 0.0118910 -0.0287073 + 0.0138873 -0.0304581 + 0.0158836 -0.0322088 + 0.0180914 -0.0336840 + 0.0202991 -0.0351592 + 0.0226806 -0.0363335 + 0.0250620 -0.0375079 + 0.0275763 -0.0383614 + 0.0300907 -0.0392149 + 0.0326949 -0.0397330 + 0.0352992 -0.0402510 + 0.0379487 -0.0404246 + 0.0405983 -0.0405983 + 0.0432479 -0.0404246 + 0.0458974 -0.0402510 + 0.0485017 -0.0397330 + 0.0511059 -0.0392149 + 0.0536202 -0.0383614 + 0.0561346 -0.0375079 + 0.0585160 -0.0363335 + 0.0608974 -0.0351592 + 0.0631052 -0.0336840 + 0.0653130 -0.0322088 + 0.0673093 -0.0304581 + 0.0693056 -0.0287073 + 0.0710563 -0.0267110 + 0.0728071 -0.0247147 + 0.0742823 -0.0225069 + 0.0757574 -0.0202991 + 0.0769318 -0.0179177 + 0.0781062 -0.0155363 + 0.0818568 -0.0093260 + 0.0842841 -0.0062041 + 0.0941392 0.0029782 + 0.1039075 0.0091624 + 0.1222984 0.0170947 + 0.1234657 0.0175025 + 0.1433727 0.0229102 + 0.1632797 0.0265216 + 0.1696407 0.0273820 + 0.1750000 0.0280259 + 0.1832166 0.0288782 + 0.2031960 0.0303387 + 0.2231755 0.0310903 + 0.2431549 0.0312669 + 0.2631344 0.0309707 + 0.2864751 0.0291487 + 0.2944570 0.0288115 + 0.3129537 0.0280299 + 0.3625555 0.0259341 + 0.4121573 0.0238383 + 0.4301315 0.0230788 + 0.4617590 0.0217424 + 0.5113608 0.0196466 + 0.5609626 0.0175507 + 0.5983523 0.0159709 + 0.6105644 0.0154549 + 0.6601662 0.0133591 + 0.7097680 0.0112632 + 0.7593697 0.0091674 + 0.7887696 0.0079252 + 0.8089715 0.0070716 + 0.8585733 0.0049757 + 0.9081751 0.0028799 + 0.9577769 0.0007841 + 1.0000000 -0.0010000 diff --git a/data/TUDELFT_V3_KITE/Surfplan_export/profiles/prof_12.png b/data/TUDELFT_V3_KITE/Surfplan_export/profiles/prof_12.png new file mode 100644 index 0000000..2cdc56c Binary files /dev/null and b/data/TUDELFT_V3_KITE/Surfplan_export/profiles/prof_12.png differ diff --git a/data/TUDELFT_V3_KITE/Surfplan_export/profiles/prof_2.dat b/data/TUDELFT_V3_KITE/Surfplan_export/profiles/prof_2.dat new file mode 100644 index 0000000..625d979 --- /dev/null +++ b/data/TUDELFT_V3_KITE/Surfplan_export/profiles/prof_2.dat @@ -0,0 +1,151 @@ +rib_2 + 1.0000000 0.0000000 + 0.9577769 0.0053268 + 0.9081751 0.0115845 + 0.8585733 0.0178422 + 0.8089715 0.0240999 + 0.7887696 0.0266485 + 0.7593697 0.0303576 + 0.7097680 0.0366152 + 0.6601662 0.0428729 + 0.6105644 0.0491306 + 0.5983523 0.0506713 + 0.5609626 0.0553883 + 0.5113608 0.0616460 + 0.4617590 0.0679037 + 0.4301315 0.0718938 + 0.4121573 0.0741614 + 0.3625555 0.0804191 + 0.3129537 0.0866768 + 0.2944570 0.0890103 + 0.2864751 0.0893718 + 0.2631344 0.0904268 + 0.2431549 0.0913273 + 0.2231755 0.0922277 + 0.2031960 0.0931281 + 0.1832166 0.0940286 + 0.1750000 0.0943989 + 0.1696407 0.0938922 + 0.1632797 0.0932908 + 0.1433727 0.0914058 + 0.1234657 0.0895147 + 0.1222984 0.0894039 + 0.1039075 0.0854549 + 0.0941392 0.0833536 + 0.0842841 0.0812336 + 0.0818568 0.0802643 + 0.0751055 0.0775684 + 0.0658445 0.0738664 + 0.0565836 0.0701625 + 0.0529896 0.0687251 + 0.0478730 0.0654343 + 0.0395115 0.0600516 + 0.0311500 0.0546668 + 0.0292787 0.0534617 + 0.0290956 0.0532780 + 0.0273482 0.0515249 + 0.0256109 0.0497818 + 0.0256009 0.0497717 + 0.0238535 0.0480171 + 0.0221062 0.0462618 + 0.0203588 0.0445065 + 0.0186115 0.0427513 + 0.0168641 0.0409960 + 0.0151168 0.0392407 + 0.0133694 0.0374855 + 0.0132474 0.0373629 + 0.0121593 0.0353485 + 0.0109895 0.0331817 + 0.0098198 0.0310127 + 0.0086500 0.0288436 + 0.0074877 0.0266883 + 0.0066048 0.0243778 + 0.0057275 0.0220801 + 0.0048502 0.0197818 + 0.0039729 0.0174836 + 0.0030956 0.0151854 + 0.0022184 0.0128872 + 0.0021236 0.0123624 + 0.0013133 0.0099754 + 0.0008215 0.0075031 + 0.0003297 0.0050308 + 0.0001649 0.0025154 + 0.0000000 0.0000000 + 0.0001649 -0.0025154 + 0.0003297 -0.0050308 + 0.0008215 -0.0075031 + 0.0013133 -0.0099754 + 0.0021236 -0.0123624 + 0.0029338 -0.0147494 + 0.0040488 -0.0170102 + 0.0051637 -0.0192710 + 0.0065641 -0.0213670 + 0.0079646 -0.0234629 + 0.0096267 -0.0253582 + 0.0112887 -0.0272534 + 0.0131839 -0.0289154 + 0.0150792 -0.0305775 + 0.0171751 -0.0319780 + 0.0192710 -0.0333784 + 0.0215319 -0.0344933 + 0.0237927 -0.0356083 + 0.0261797 -0.0364185 + 0.0285667 -0.0372288 + 0.0310390 -0.0377206 + 0.0335113 -0.0382124 + 0.0360267 -0.0383772 + 0.0385421 -0.0385421 + 0.0410575 -0.0383772 + 0.0435728 -0.0382124 + 0.0460452 -0.0377206 + 0.0485175 -0.0372288 + 0.0509045 -0.0364185 + 0.0532915 -0.0356083 + 0.0555523 -0.0344933 + 0.0578131 -0.0333784 + 0.0599091 -0.0319780 + 0.0620050 -0.0305775 + 0.0639003 -0.0289154 + 0.0657955 -0.0272534 + 0.0674575 -0.0253582 + 0.0691196 -0.0234629 + 0.0705201 -0.0213670 + 0.0719205 -0.0192710 + 0.0730354 -0.0170102 + 0.0741504 -0.0147494 + 0.0751055 -0.0128992 + 0.0818568 -0.0004809 + 0.0842841 0.0034531 + 0.0941392 0.0178500 + 0.1039075 0.0296932 + 0.1222984 0.0472811 + 0.1234657 0.0482146 + 0.1433727 0.0620213 + 0.1632797 0.0721516 + 0.1696407 0.0747674 + 0.1750000 0.0767829 + 0.1832166 0.0795051 + 0.2031960 0.0846408 + 0.2231755 0.0879378 + 0.2431549 0.0896854 + 0.2631344 0.0901347 + 0.2864751 0.0883718 + 0.2944570 0.0880103 + 0.3129537 0.0856768 + 0.3625555 0.0794191 + 0.4121573 0.0731614 + 0.4301315 0.0708938 + 0.4617590 0.0669037 + 0.5113608 0.0606460 + 0.5609626 0.0543883 + 0.5983523 0.0496713 + 0.6105644 0.0481306 + 0.6601662 0.0418729 + 0.7097680 0.0356152 + 0.7593697 0.0293576 + 0.7887696 0.0256485 + 0.8089715 0.0230999 + 0.8585733 0.0168422 + 0.9081751 0.0105845 + 0.9577769 0.0043268 + 1.0000000 -0.0010000 diff --git a/data/TUDELFT_V3_KITE/Surfplan_export/profiles/prof_2.png b/data/TUDELFT_V3_KITE/Surfplan_export/profiles/prof_2.png new file mode 100644 index 0000000..196bda3 Binary files /dev/null and b/data/TUDELFT_V3_KITE/Surfplan_export/profiles/prof_2.png differ diff --git a/data/TUDELFT_V3_KITE/Surfplan_export/profiles/prof_3.dat b/data/TUDELFT_V3_KITE/Surfplan_export/profiles/prof_3.dat new file mode 100644 index 0000000..b4aaa32 --- /dev/null +++ b/data/TUDELFT_V3_KITE/Surfplan_export/profiles/prof_3.dat @@ -0,0 +1,151 @@ +rib_3 + 1.0000000 0.0000000 + 0.9577769 0.0052581 + 0.9081751 0.0114351 + 0.8585733 0.0176122 + 0.8089715 0.0237892 + 0.7887696 0.0263049 + 0.7593697 0.0299662 + 0.7097680 0.0361432 + 0.6601662 0.0423202 + 0.6105644 0.0484972 + 0.5983523 0.0500180 + 0.5609626 0.0546742 + 0.5113608 0.0608512 + 0.4617590 0.0670282 + 0.4301315 0.0709669 + 0.4121573 0.0732053 + 0.3625555 0.0793823 + 0.3129537 0.0855593 + 0.2944570 0.0878627 + 0.2864751 0.0882233 + 0.2631344 0.0892710 + 0.2431549 0.0901598 + 0.2231755 0.0910486 + 0.2031960 0.0919374 + 0.1832166 0.0928263 + 0.1750000 0.0931918 + 0.1696407 0.0926970 + 0.1632797 0.0921097 + 0.1433727 0.0902628 + 0.1234657 0.0883979 + 0.1222984 0.0882886 + 0.1039075 0.0844087 + 0.0941392 0.0823364 + 0.0842841 0.0802458 + 0.0818568 0.0792930 + 0.0751055 0.0766430 + 0.0658445 0.0729958 + 0.0565836 0.0693432 + 0.0529896 0.0679257 + 0.0478730 0.0646890 + 0.0395115 0.0593849 + 0.0311500 0.0540747 + 0.0292787 0.0528862 + 0.0290956 0.0527055 + 0.0273482 0.0509810 + 0.0256109 0.0492664 + 0.0256009 0.0492565 + 0.0238535 0.0475274 + 0.0221062 0.0457964 + 0.0203588 0.0440654 + 0.0186115 0.0423344 + 0.0168641 0.0406034 + 0.0151168 0.0388725 + 0.0133694 0.0371415 + 0.0132474 0.0370206 + 0.0121593 0.0350404 + 0.0109895 0.0329084 + 0.0098198 0.0307693 + 0.0086500 0.0286303 + 0.0074877 0.0265048 + 0.0066048 0.0242309 + 0.0057275 0.0219660 + 0.0048502 0.0196995 + 0.0039729 0.0174331 + 0.0030956 0.0151667 + 0.0022184 0.0129003 + 0.0021278 0.0123869 + 0.0013159 0.0099952 + 0.0008231 0.0075180 + 0.0003304 0.0050407 + 0.0001652 0.0025204 + 0.0000000 0.0000000 + 0.0001652 -0.0025204 + 0.0003304 -0.0050407 + 0.0008231 -0.0075180 + 0.0013159 -0.0099952 + 0.0021278 -0.0123869 + 0.0029397 -0.0147786 + 0.0040568 -0.0170439 + 0.0051739 -0.0193092 + 0.0065771 -0.0214093 + 0.0079804 -0.0235094 + 0.0096457 -0.0254084 + 0.0113111 -0.0273074 + 0.0132101 -0.0289727 + 0.0151090 -0.0306381 + 0.0172091 -0.0320413 + 0.0193092 -0.0334446 + 0.0215745 -0.0345617 + 0.0238398 -0.0356788 + 0.0262315 -0.0364907 + 0.0286233 -0.0373026 + 0.0311005 -0.0377953 + 0.0335777 -0.0382881 + 0.0360981 -0.0384533 + 0.0386185 -0.0386185 + 0.0411388 -0.0384533 + 0.0436592 -0.0382881 + 0.0461364 -0.0377953 + 0.0486137 -0.0373026 + 0.0510054 -0.0364907 + 0.0533971 -0.0356788 + 0.0556624 -0.0345617 + 0.0579277 -0.0334446 + 0.0600278 -0.0320413 + 0.0621279 -0.0306381 + 0.0640269 -0.0289727 + 0.0659258 -0.0273074 + 0.0675912 -0.0254084 + 0.0692565 -0.0235094 + 0.0706598 -0.0214093 + 0.0720630 -0.0193092 + 0.0731801 -0.0170439 + 0.0742973 -0.0147786 + 0.0751055 -0.0132140 + 0.0818568 -0.0007931 + 0.0842841 0.0031413 + 0.0941392 0.0174898 + 0.1039075 0.0292607 + 0.1222984 0.0466897 + 0.1234657 0.0476151 + 0.1433727 0.0612674 + 0.1632797 0.0712660 + 0.1696407 0.0738449 + 0.1750000 0.0758307 + 0.1832166 0.0785142 + 0.2031960 0.0835748 + 0.2231755 0.0868185 + 0.2431549 0.0885374 + 0.2631344 0.0889779 + 0.2864751 0.0872233 + 0.2944570 0.0868627 + 0.3129537 0.0845593 + 0.3625555 0.0783823 + 0.4121573 0.0722053 + 0.4301315 0.0699669 + 0.4617590 0.0660282 + 0.5113608 0.0598512 + 0.5609626 0.0536742 + 0.5983523 0.0490180 + 0.6105644 0.0474972 + 0.6601662 0.0413202 + 0.7097680 0.0351432 + 0.7593697 0.0289662 + 0.7887696 0.0253049 + 0.8089715 0.0227892 + 0.8585733 0.0166122 + 0.9081751 0.0104351 + 0.9577769 0.0042581 + 1.0000000 -0.0010000 diff --git a/data/TUDELFT_V3_KITE/Surfplan_export/profiles/prof_3.png b/data/TUDELFT_V3_KITE/Surfplan_export/profiles/prof_3.png new file mode 100644 index 0000000..d0b09ba Binary files /dev/null and b/data/TUDELFT_V3_KITE/Surfplan_export/profiles/prof_3.png differ diff --git a/data/TUDELFT_V3_KITE/Surfplan_export/profiles/prof_4.dat b/data/TUDELFT_V3_KITE/Surfplan_export/profiles/prof_4.dat new file mode 100644 index 0000000..3da0617 --- /dev/null +++ b/data/TUDELFT_V3_KITE/Surfplan_export/profiles/prof_4.dat @@ -0,0 +1,151 @@ +rib_4 + 1.0000000 0.0000000 + 0.9577769 0.0051544 + 0.9081751 0.0112096 + 0.8585733 0.0172647 + 0.8089715 0.0233199 + 0.7887696 0.0257860 + 0.7593697 0.0293750 + 0.7097680 0.0354302 + 0.6601662 0.0414853 + 0.6105644 0.0475405 + 0.5983523 0.0490313 + 0.5609626 0.0535956 + 0.5113608 0.0596508 + 0.4617590 0.0657060 + 0.4301315 0.0695669 + 0.4121573 0.0717611 + 0.3625555 0.0778163 + 0.3129537 0.0838714 + 0.2944570 0.0861294 + 0.2864751 0.0864886 + 0.2631344 0.0875251 + 0.2431549 0.0883964 + 0.2231755 0.0892677 + 0.2031960 0.0901390 + 0.1832166 0.0910102 + 0.1750000 0.0913686 + 0.1696407 0.0908919 + 0.1632797 0.0903261 + 0.1433727 0.0885375 + 0.1234657 0.0867131 + 0.1222984 0.0866061 + 0.1039075 0.0828322 + 0.0941392 0.0808049 + 0.0842841 0.0787595 + 0.0818568 0.0778321 + 0.0751055 0.0752525 + 0.0658445 0.0716898 + 0.0565836 0.0681163 + 0.0529896 0.0667296 + 0.0478730 0.0635761 + 0.0395115 0.0583931 + 0.0311500 0.0531979 + 0.0292787 0.0520352 + 0.0290956 0.0518591 + 0.0273482 0.0501785 + 0.0256109 0.0485076 + 0.0256009 0.0484979 + 0.0238535 0.0468082 + 0.0221062 0.0451147 + 0.0203588 0.0434212 + 0.0186115 0.0417277 + 0.0168641 0.0400343 + 0.0151168 0.0383408 + 0.0133694 0.0366473 + 0.0132474 0.0365290 + 0.0121593 0.0346014 + 0.0109895 0.0325227 + 0.0098198 0.0304300 + 0.0086500 0.0283373 + 0.0074877 0.0262578 + 0.0066048 0.0240403 + 0.0057275 0.0218259 + 0.0048502 0.0196086 + 0.0039729 0.0173912 + 0.0030956 0.0151739 + 0.0029487 0.0148239 + 0.0021343 0.0124249 + 0.0013199 0.0100258 + 0.0008257 0.0075410 + 0.0003314 0.0050562 + 0.0001657 0.0025281 + 0.0000000 0.0000000 + 0.0001657 -0.0025281 + 0.0003314 -0.0050562 + 0.0008257 -0.0075410 + 0.0013199 -0.0100258 + 0.0021343 -0.0124249 + 0.0029487 -0.0148239 + 0.0040692 -0.0170962 + 0.0051898 -0.0193684 + 0.0065973 -0.0214750 + 0.0080048 -0.0235815 + 0.0096753 -0.0254863 + 0.0113458 -0.0273911 + 0.0132505 -0.0290615 + 0.0151553 -0.0307320 + 0.0172619 -0.0321395 + 0.0193684 -0.0335471 + 0.0216407 -0.0346676 + 0.0239129 -0.0357882 + 0.0263119 -0.0366025 + 0.0287110 -0.0374169 + 0.0311958 -0.0379112 + 0.0336807 -0.0384054 + 0.0362087 -0.0385711 + 0.0387368 -0.0387368 + 0.0412649 -0.0385711 + 0.0437930 -0.0384054 + 0.0462778 -0.0379112 + 0.0487627 -0.0374169 + 0.0511617 -0.0366025 + 0.0535608 -0.0357882 + 0.0558330 -0.0346676 + 0.0581052 -0.0335471 + 0.0602118 -0.0321395 + 0.0623183 -0.0307320 + 0.0642231 -0.0290615 + 0.0661279 -0.0273911 + 0.0677984 -0.0254863 + 0.0694688 -0.0235815 + 0.0708764 -0.0214750 + 0.0722839 -0.0193684 + 0.0734045 -0.0170962 + 0.0745250 -0.0148239 + 0.0751055 -0.0137013 + 0.0818568 -0.0012751 + 0.0842841 0.0026609 + 0.0941392 0.0169366 + 0.1039075 0.0285984 + 0.1222984 0.0457880 + 0.1234657 0.0467011 + 0.1433727 0.0601213 + 0.1632797 0.0699225 + 0.1696407 0.0724465 + 0.1750000 0.0743880 + 0.1832166 0.0770136 + 0.2031960 0.0819619 + 0.2231755 0.0851268 + 0.2431549 0.0868029 + 0.2631344 0.0872307 + 0.2864751 0.0854886 + 0.2944570 0.0851294 + 0.3129537 0.0828714 + 0.3625555 0.0768163 + 0.4121573 0.0707611 + 0.4301315 0.0685669 + 0.4617590 0.0647060 + 0.5113608 0.0586508 + 0.5609626 0.0525956 + 0.5983523 0.0480313 + 0.6105644 0.0465405 + 0.6601662 0.0404853 + 0.7097680 0.0344302 + 0.7593697 0.0283750 + 0.7887696 0.0247860 + 0.8089715 0.0223199 + 0.8585733 0.0162647 + 0.9081751 0.0102096 + 0.9577769 0.0041544 + 1.0000000 -0.0010000 diff --git a/data/TUDELFT_V3_KITE/Surfplan_export/profiles/prof_4.png b/data/TUDELFT_V3_KITE/Surfplan_export/profiles/prof_4.png new file mode 100644 index 0000000..84a484e Binary files /dev/null and b/data/TUDELFT_V3_KITE/Surfplan_export/profiles/prof_4.png differ diff --git a/data/TUDELFT_V3_KITE/Surfplan_export/profiles/prof_5.dat b/data/TUDELFT_V3_KITE/Surfplan_export/profiles/prof_5.dat new file mode 100644 index 0000000..9f4e0b2 --- /dev/null +++ b/data/TUDELFT_V3_KITE/Surfplan_export/profiles/prof_5.dat @@ -0,0 +1,151 @@ +rib_5 + 1.0000000 0.0000000 + 0.9577769 0.0050147 + 0.9081751 0.0109057 + 0.8585733 0.0167967 + 0.8089715 0.0226877 + 0.7887696 0.0250870 + 0.7593697 0.0285787 + 0.7097680 0.0344697 + 0.6601662 0.0403608 + 0.6105644 0.0462518 + 0.5983523 0.0477022 + 0.5609626 0.0521428 + 0.5113608 0.0580338 + 0.4617590 0.0639248 + 0.4301315 0.0676811 + 0.4121573 0.0698158 + 0.3625555 0.0757068 + 0.3129537 0.0815979 + 0.2944570 0.0837946 + 0.2864751 0.0841519 + 0.2631344 0.0851734 + 0.2431549 0.0860211 + 0.2231755 0.0868687 + 0.2031960 0.0877164 + 0.1832166 0.0885641 + 0.1750000 0.0889127 + 0.1696407 0.0884605 + 0.1632797 0.0879237 + 0.1433727 0.0862141 + 0.1234657 0.0844447 + 0.1222984 0.0843410 + 0.1039075 0.0807108 + 0.0941392 0.0787447 + 0.0842841 0.0767610 + 0.0818568 0.0758679 + 0.0751055 0.0733840 + 0.0658445 0.0699363 + 0.0565836 0.0664706 + 0.0529896 0.0651257 + 0.0478730 0.0620851 + 0.0395115 0.0570670 + 0.0311500 0.0520285 + 0.0292787 0.0509009 + 0.0290956 0.0507311 + 0.0273482 0.0491102 + 0.0256109 0.0474986 + 0.0256009 0.0474893 + 0.0238535 0.0458531 + 0.0221062 0.0442107 + 0.0203588 0.0425683 + 0.0186115 0.0409259 + 0.0168641 0.0392835 + 0.0151168 0.0376411 + 0.0133694 0.0359987 + 0.0132474 0.0358840 + 0.0121593 0.0340277 + 0.0109895 0.0320214 + 0.0098198 0.0299918 + 0.0086500 0.0279623 + 0.0074877 0.0259455 + 0.0066048 0.0238045 + 0.0057275 0.0216590 + 0.0048502 0.0195085 + 0.0039729 0.0173581 + 0.0030956 0.0152076 + 0.0029613 0.0148873 + 0.0021434 0.0124780 + 0.0013256 0.0100687 + 0.0008292 0.0075732 + 0.0003328 0.0050778 + 0.0001664 0.0025389 + 0.0000000 0.0000000 + 0.0001664 -0.0025389 + 0.0003328 -0.0050778 + 0.0008292 -0.0075732 + 0.0013256 -0.0100687 + 0.0021434 -0.0124780 + 0.0029613 -0.0148873 + 0.0040866 -0.0171692 + 0.0052119 -0.0194512 + 0.0066255 -0.0215667 + 0.0080390 -0.0236822 + 0.0097166 -0.0255952 + 0.0113942 -0.0275081 + 0.0133072 -0.0291857 + 0.0152201 -0.0308633 + 0.0173356 -0.0322768 + 0.0194512 -0.0336904 + 0.0217331 -0.0348157 + 0.0240150 -0.0359411 + 0.0264244 -0.0367589 + 0.0288337 -0.0375768 + 0.0313291 -0.0380731 + 0.0338246 -0.0385695 + 0.0363634 -0.0387359 + 0.0389023 -0.0389023 + 0.0414412 -0.0387359 + 0.0439801 -0.0385695 + 0.0464755 -0.0380731 + 0.0489710 -0.0375768 + 0.0513803 -0.0367589 + 0.0537896 -0.0359411 + 0.0560715 -0.0348157 + 0.0583535 -0.0336904 + 0.0604690 -0.0322768 + 0.0625846 -0.0308633 + 0.0644975 -0.0291857 + 0.0664104 -0.0275081 + 0.0680880 -0.0255952 + 0.0697656 -0.0236822 + 0.0711792 -0.0215667 + 0.0725927 -0.0194512 + 0.0737181 -0.0171692 + 0.0748434 -0.0148873 + 0.0751055 -0.0143810 + 0.0818568 -0.0019452 + 0.0842841 0.0019951 + 0.0941392 0.0161737 + 0.1039075 0.0276889 + 0.1222984 0.0445574 + 0.1234657 0.0454539 + 0.1433727 0.0585635 + 0.1632797 0.0681022 + 0.1696407 0.0705533 + 0.1750000 0.0724360 + 0.1832166 0.0749851 + 0.2031960 0.0797846 + 0.2231755 0.0828457 + 0.2431549 0.0844658 + 0.2631344 0.0848772 + 0.2864751 0.0831519 + 0.2944570 0.0827946 + 0.3129537 0.0805979 + 0.3625555 0.0747068 + 0.4121573 0.0688158 + 0.4301315 0.0666811 + 0.4617590 0.0629248 + 0.5113608 0.0570338 + 0.5609626 0.0511428 + 0.5983523 0.0467022 + 0.6105644 0.0452518 + 0.6601662 0.0393608 + 0.7097680 0.0334697 + 0.7593697 0.0275787 + 0.7887696 0.0240870 + 0.8089715 0.0216877 + 0.8585733 0.0157967 + 0.9081751 0.0099057 + 0.9577769 0.0040147 + 1.0000000 -0.0010000 diff --git a/data/TUDELFT_V3_KITE/Surfplan_export/profiles/prof_5.png b/data/TUDELFT_V3_KITE/Surfplan_export/profiles/prof_5.png new file mode 100644 index 0000000..025394e Binary files /dev/null and b/data/TUDELFT_V3_KITE/Surfplan_export/profiles/prof_5.png differ diff --git a/data/TUDELFT_V3_KITE/Surfplan_export/profiles/prof_6.dat b/data/TUDELFT_V3_KITE/Surfplan_export/profiles/prof_6.dat new file mode 100644 index 0000000..2b003ea --- /dev/null +++ b/data/TUDELFT_V3_KITE/Surfplan_export/profiles/prof_6.dat @@ -0,0 +1,150 @@ +rib_6 + 1.0000000 0.0000000 + 0.9577769 0.0048375 + 0.9081751 0.0105205 + 0.8585733 0.0162034 + 0.8089715 0.0218863 + 0.7887696 0.0242009 + 0.7593697 0.0275693 + 0.7097680 0.0332522 + 0.6601662 0.0389351 + 0.6105644 0.0446180 + 0.5983523 0.0460172 + 0.5609626 0.0503010 + 0.5113608 0.0559839 + 0.4617590 0.0616668 + 0.4301315 0.0652904 + 0.4121573 0.0673498 + 0.3625555 0.0730327 + 0.3129537 0.0787156 + 0.2944570 0.0808348 + 0.2864751 0.0811896 + 0.2631344 0.0821921 + 0.2431549 0.0830098 + 0.2231755 0.0838275 + 0.2031960 0.0846453 + 0.1832166 0.0854630 + 0.1750000 0.0857993 + 0.1696407 0.0853780 + 0.1632797 0.0848779 + 0.1433727 0.0832681 + 0.1234657 0.0815686 + 0.1222984 0.0814689 + 0.1039075 0.0780208 + 0.0941392 0.0761323 + 0.0842841 0.0742270 + 0.0818568 0.0733774 + 0.0751055 0.0710146 + 0.0658445 0.0677128 + 0.0565836 0.0643840 + 0.0529896 0.0630922 + 0.0478730 0.0601947 + 0.0395115 0.0553859 + 0.0311500 0.0505464 + 0.0292787 0.0494634 + 0.0290956 0.0493014 + 0.0273482 0.0477561 + 0.0256109 0.0462198 + 0.0256009 0.0462109 + 0.0238535 0.0446427 + 0.0221062 0.0430651 + 0.0203588 0.0414876 + 0.0186115 0.0399101 + 0.0168641 0.0383326 + 0.0151168 0.0367551 + 0.0133694 0.0351775 + 0.0132474 0.0350674 + 0.0121593 0.0333014 + 0.0109895 0.0313870 + 0.0098198 0.0294376 + 0.0086500 0.0274882 + 0.0074877 0.0255511 + 0.0066048 0.0235071 + 0.0057275 0.0214489 + 0.0048502 0.0193834 + 0.0039729 0.0173179 + 0.0030956 0.0152525 + 0.0029780 0.0149716 + 0.0021556 0.0125487 + 0.0013331 0.0101257 + 0.0008339 0.0076161 + 0.0003347 0.0051065 + 0.0001674 0.0025533 + 0.0000000 0.0000000 + 0.0001674 -0.0025533 + 0.0003347 -0.0051065 + 0.0008339 -0.0076161 + 0.0013331 -0.0101257 + 0.0021556 -0.0125487 + 0.0029780 -0.0149716 + 0.0041098 -0.0172665 + 0.0052415 -0.0195614 + 0.0066630 -0.0216889 + 0.0080846 -0.0238164 + 0.0097717 -0.0257402 + 0.0114588 -0.0276640 + 0.0133826 -0.0293511 + 0.0153063 -0.0310382 + 0.0174339 -0.0324598 + 0.0195614 -0.0338813 + 0.0218563 -0.0350130 + 0.0241511 -0.0361447 + 0.0265741 -0.0369672 + 0.0289971 -0.0377897 + 0.0315066 -0.0382889 + 0.0340162 -0.0387881 + 0.0365695 -0.0389554 + 0.0391228 -0.0391228 + 0.0416761 -0.0389554 + 0.0442293 -0.0387881 + 0.0467389 -0.0382889 + 0.0492485 -0.0377897 + 0.0516715 -0.0369672 + 0.0540944 -0.0361447 + 0.0563893 -0.0350130 + 0.0586842 -0.0338813 + 0.0608117 -0.0324598 + 0.0629392 -0.0310382 + 0.0648630 -0.0293511 + 0.0667868 -0.0276640 + 0.0684739 -0.0257402 + 0.0701610 -0.0238164 + 0.0715825 -0.0216889 + 0.0730041 -0.0195614 + 0.0741358 -0.0172665 + 0.0752675 -0.0149716 + 0.0818568 -0.0028308 + 0.0842841 0.0011187 + 0.0941392 0.0151761 + 0.1039075 0.0265060 + 0.1222984 0.0429705 + 0.1234657 0.0438457 + 0.1433727 0.0565655 + 0.1632797 0.0657766 + 0.1696407 0.0681373 + 0.1750000 0.0699475 + 0.1832166 0.0724014 + 0.2031960 0.0770165 + 0.2231755 0.0799503 + 0.2431549 0.0815017 + 0.2631344 0.0818934 + 0.2864751 0.0801896 + 0.2944570 0.0798348 + 0.3129537 0.0777156 + 0.3625555 0.0720327 + 0.4121573 0.0663498 + 0.4301315 0.0642904 + 0.4617590 0.0606668 + 0.5113608 0.0549839 + 0.5609626 0.0493010 + 0.5983523 0.0450172 + 0.6105644 0.0436180 + 0.6601662 0.0379351 + 0.7097680 0.0322522 + 0.7593697 0.0265693 + 0.7887696 0.0232009 + 0.8089715 0.0208863 + 0.8585733 0.0152034 + 0.9081751 0.0095205 + 0.9577769 0.0038375 + 1.0000000 -0.0010000 diff --git a/data/TUDELFT_V3_KITE/Surfplan_export/profiles/prof_6.png b/data/TUDELFT_V3_KITE/Surfplan_export/profiles/prof_6.png new file mode 100644 index 0000000..23f1b56 Binary files /dev/null and b/data/TUDELFT_V3_KITE/Surfplan_export/profiles/prof_6.png differ diff --git a/data/TUDELFT_V3_KITE/Surfplan_export/profiles/prof_7.dat b/data/TUDELFT_V3_KITE/Surfplan_export/profiles/prof_7.dat new file mode 100644 index 0000000..546acd6 --- /dev/null +++ b/data/TUDELFT_V3_KITE/Surfplan_export/profiles/prof_7.dat @@ -0,0 +1,150 @@ +rib_7 + 1.0000000 0.0000000 + 0.9577769 0.0046209 + 0.9081751 0.0100494 + 0.8585733 0.0154778 + 0.8089715 0.0209063 + 0.7887696 0.0231172 + 0.7593697 0.0263347 + 0.7097680 0.0317632 + 0.6601662 0.0371916 + 0.6105644 0.0426201 + 0.5983523 0.0439566 + 0.5609626 0.0480485 + 0.5113608 0.0534770 + 0.4617590 0.0589054 + 0.4301315 0.0623667 + 0.4121573 0.0643339 + 0.3625555 0.0697623 + 0.3129537 0.0751908 + 0.2944570 0.0772150 + 0.2864751 0.0775668 + 0.2631344 0.0785461 + 0.2431549 0.0793272 + 0.2231755 0.0801083 + 0.2031960 0.0808894 + 0.1832166 0.0816705 + 0.1750000 0.0819918 + 0.1696407 0.0816080 + 0.1632797 0.0811524 + 0.1433727 0.0796641 + 0.1234657 0.0780503 + 0.1222984 0.0779557 + 0.1039075 0.0747297 + 0.0941392 0.0729364 + 0.0842841 0.0711272 + 0.0818568 0.0703308 + 0.0751055 0.0681158 + 0.0658445 0.0649927 + 0.0565836 0.0618318 + 0.0529896 0.0606051 + 0.0478730 0.0578824 + 0.0395115 0.0533298 + 0.0311500 0.0487344 + 0.0292787 0.0477059 + 0.0290956 0.0475537 + 0.0273482 0.0461008 + 0.0256109 0.0446563 + 0.0256009 0.0446480 + 0.0238535 0.0431630 + 0.0221062 0.0416650 + 0.0203588 0.0401671 + 0.0186115 0.0386691 + 0.0168641 0.0371711 + 0.0151168 0.0356732 + 0.0133694 0.0341752 + 0.0132474 0.0340706 + 0.0121593 0.0324149 + 0.0109895 0.0306127 + 0.0098198 0.0287616 + 0.0086500 0.0269105 + 0.0074877 0.0250712 + 0.0066048 0.0231458 + 0.0057275 0.0211947 + 0.0048502 0.0192333 + 0.0039729 0.0172720 + 0.0030956 0.0153106 + 0.0029999 0.0150816 + 0.0021714 0.0126408 + 0.0013429 0.0102001 + 0.0008400 0.0076721 + 0.0003372 0.0051440 + 0.0001686 0.0025720 + 0.0000000 0.0000000 + 0.0001686 -0.0025720 + 0.0003372 -0.0051440 + 0.0008400 -0.0076721 + 0.0013429 -0.0102001 + 0.0021714 -0.0126408 + 0.0029999 -0.0150816 + 0.0041399 -0.0173933 + 0.0052799 -0.0197050 + 0.0067119 -0.0218482 + 0.0081439 -0.0239913 + 0.0098434 -0.0259292 + 0.0115429 -0.0278671 + 0.0134808 -0.0295666 + 0.0154187 -0.0312661 + 0.0175619 -0.0326981 + 0.0197050 -0.0341301 + 0.0220167 -0.0352701 + 0.0243285 -0.0364101 + 0.0267692 -0.0372386 + 0.0292100 -0.0380672 + 0.0317380 -0.0385700 + 0.0342660 -0.0390729 + 0.0368380 -0.0392414 + 0.0394100 -0.0394100 + 0.0419820 -0.0392414 + 0.0445541 -0.0390729 + 0.0470821 -0.0385700 + 0.0496101 -0.0380672 + 0.0520508 -0.0372386 + 0.0544916 -0.0364101 + 0.0568033 -0.0352701 + 0.0591150 -0.0341301 + 0.0612582 -0.0326981 + 0.0634013 -0.0312661 + 0.0653392 -0.0295666 + 0.0672771 -0.0278671 + 0.0689766 -0.0259292 + 0.0706761 -0.0239913 + 0.0721081 -0.0218482 + 0.0735401 -0.0197050 + 0.0746801 -0.0173933 + 0.0758201 -0.0150816 + 0.0818568 -0.0039728 + 0.0842841 -0.0000056 + 0.0941392 0.0139076 + 0.1039075 0.0250125 + 0.1222984 0.0409883 + 0.1234657 0.0418372 + 0.1433727 0.0540865 + 0.1632797 0.0629056 + 0.1696407 0.0651586 + 0.1750000 0.0668829 + 0.1832166 0.0692235 + 0.2031960 0.0736194 + 0.2231755 0.0764040 + 0.2431549 0.0778748 + 0.2631344 0.0782444 + 0.2864751 0.0765668 + 0.2944570 0.0762150 + 0.3129537 0.0741908 + 0.3625555 0.0687623 + 0.4121573 0.0633339 + 0.4301315 0.0613667 + 0.4617590 0.0579054 + 0.5113608 0.0524770 + 0.5609626 0.0470485 + 0.5983523 0.0429566 + 0.6105644 0.0416201 + 0.6601662 0.0361916 + 0.7097680 0.0307632 + 0.7593697 0.0253347 + 0.7887696 0.0221172 + 0.8089715 0.0199063 + 0.8585733 0.0144778 + 0.9081751 0.0090494 + 0.9577769 0.0036209 + 1.0000000 -0.0010000 diff --git a/data/TUDELFT_V3_KITE/Surfplan_export/profiles/prof_7.png b/data/TUDELFT_V3_KITE/Surfplan_export/profiles/prof_7.png new file mode 100644 index 0000000..adeb036 Binary files /dev/null and b/data/TUDELFT_V3_KITE/Surfplan_export/profiles/prof_7.png differ diff --git a/data/TUDELFT_V3_KITE/Surfplan_export/profiles/prof_8.dat b/data/TUDELFT_V3_KITE/Surfplan_export/profiles/prof_8.dat new file mode 100644 index 0000000..f2d3b06 --- /dev/null +++ b/data/TUDELFT_V3_KITE/Surfplan_export/profiles/prof_8.dat @@ -0,0 +1,150 @@ +rib_8 + 1.0000000 0.0000000 + 0.9577769 0.0043617 + 0.9081751 0.0094856 + 0.8585733 0.0146095 + 0.8089715 0.0197335 + 0.7887696 0.0218203 + 0.7593697 0.0248574 + 0.7097680 0.0299813 + 0.6601662 0.0351052 + 0.6105644 0.0402291 + 0.5983523 0.0414907 + 0.5609626 0.0453531 + 0.5113608 0.0504770 + 0.4617590 0.0556009 + 0.4301315 0.0588681 + 0.4121573 0.0607248 + 0.3625555 0.0658488 + 0.3129537 0.0709727 + 0.2944570 0.0728834 + 0.2864751 0.0732316 + 0.2631344 0.0741830 + 0.2431549 0.0749203 + 0.2231755 0.0756576 + 0.2031960 0.0763949 + 0.1832166 0.0771322 + 0.1750000 0.0774354 + 0.1696407 0.0770967 + 0.1632797 0.0766947 + 0.1433727 0.0753533 + 0.1234657 0.0738451 + 0.1222984 0.0737566 + 0.1039075 0.0708010 + 0.0941392 0.0691251 + 0.0842841 0.0674343 + 0.0818568 0.0667027 + 0.0751055 0.0646679 + 0.0658445 0.0617641 + 0.0565836 0.0588100 + 0.0529896 0.0576636 + 0.0478730 0.0551544 + 0.0395115 0.0509167 + 0.0311500 0.0466220 + 0.0292787 0.0456608 + 0.0290956 0.0455204 + 0.0273482 0.0441804 + 0.0256109 0.0428481 + 0.0256009 0.0428404 + 0.0238535 0.0414578 + 0.0221062 0.0400579 + 0.0203588 0.0386579 + 0.0186115 0.0372580 + 0.0168641 0.0358580 + 0.0151168 0.0344581 + 0.0133694 0.0330582 + 0.0132474 0.0329604 + 0.0121593 0.0314392 + 0.0109895 0.0297743 + 0.0098198 0.0280443 + 0.0086500 0.0263144 + 0.0074877 0.0245954 + 0.0066048 0.0228151 + 0.0057275 0.0209957 + 0.0048502 0.0191627 + 0.0039729 0.0173297 + 0.0037273 0.0168126 + 0.0030282 0.0152240 + 0.0021919 0.0127602 + 0.0013555 0.0102964 + 0.0008479 0.0077445 + 0.0003403 0.0051926 + 0.0001702 0.0025963 + 0.0000000 0.0000000 + 0.0001702 -0.0025963 + 0.0003403 -0.0051926 + 0.0008479 -0.0077445 + 0.0013555 -0.0102964 + 0.0021919 -0.0127602 + 0.0030282 -0.0152240 + 0.0041790 -0.0175575 + 0.0053298 -0.0198911 + 0.0067753 -0.0220544 + 0.0082208 -0.0242178 + 0.0099364 -0.0261740 + 0.0116519 -0.0281302 + 0.0136081 -0.0298457 + 0.0155643 -0.0315613 + 0.0177277 -0.0330068 + 0.0198911 -0.0344523 + 0.0222246 -0.0356031 + 0.0245582 -0.0367539 + 0.0270220 -0.0375902 + 0.0294857 -0.0384266 + 0.0320376 -0.0389342 + 0.0345895 -0.0394418 + 0.0371858 -0.0396119 + 0.0397821 -0.0397821 + 0.0423784 -0.0396119 + 0.0449747 -0.0394418 + 0.0475266 -0.0389342 + 0.0500785 -0.0384266 + 0.0525423 -0.0375902 + 0.0550061 -0.0367539 + 0.0573396 -0.0356031 + 0.0596732 -0.0344523 + 0.0618366 -0.0330068 + 0.0639999 -0.0315613 + 0.0659561 -0.0298457 + 0.0679123 -0.0281302 + 0.0696279 -0.0261740 + 0.0713434 -0.0242178 + 0.0727889 -0.0220544 + 0.0742344 -0.0198911 + 0.0753852 -0.0175575 + 0.0765360 -0.0152240 + 0.0818568 -0.0054326 + 0.0842841 -0.0014328 + 0.0941392 0.0123151 + 0.1039075 0.0231541 + 0.1222984 0.0385548 + 0.1234657 0.0393719 + 0.1433727 0.0510680 + 0.1632797 0.0594310 + 0.1696407 0.0615595 + 0.1750000 0.0631851 + 0.1832166 0.0653948 + 0.2031960 0.0695372 + 0.2231755 0.0721527 + 0.2431549 0.0735319 + 0.2631344 0.0738775 + 0.2864751 0.0722316 + 0.2944570 0.0718834 + 0.3129537 0.0699727 + 0.3625555 0.0648488 + 0.4121573 0.0597248 + 0.4301315 0.0578681 + 0.4617590 0.0546009 + 0.5113608 0.0494770 + 0.5609626 0.0443531 + 0.5983523 0.0404907 + 0.6105644 0.0392291 + 0.6601662 0.0341052 + 0.7097680 0.0289813 + 0.7593697 0.0238574 + 0.7887696 0.0208203 + 0.8089715 0.0187335 + 0.8585733 0.0136095 + 0.9081751 0.0084856 + 0.9577769 0.0033617 + 1.0000000 -0.0010000 diff --git a/data/TUDELFT_V3_KITE/Surfplan_export/profiles/prof_8.png b/data/TUDELFT_V3_KITE/Surfplan_export/profiles/prof_8.png new file mode 100644 index 0000000..9fb8db6 Binary files /dev/null and b/data/TUDELFT_V3_KITE/Surfplan_export/profiles/prof_8.png differ diff --git a/data/TUDELFT_V3_KITE/Surfplan_export/profiles/prof_9.dat b/data/TUDELFT_V3_KITE/Surfplan_export/profiles/prof_9.dat new file mode 100644 index 0000000..146d5b9 --- /dev/null +++ b/data/TUDELFT_V3_KITE/Surfplan_export/profiles/prof_9.dat @@ -0,0 +1,150 @@ +rib_9 + 1.0000000 0.0000000 + 0.9577769 0.0040550 + 0.9081751 0.0088187 + 0.8585733 0.0135823 + 0.8089715 0.0183459 + 0.7887696 0.0202861 + 0.7593697 0.0231096 + 0.7097680 0.0278732 + 0.6601662 0.0326369 + 0.6105644 0.0374005 + 0.5983523 0.0385733 + 0.5609626 0.0421642 + 0.5113608 0.0469278 + 0.4617590 0.0516915 + 0.4301315 0.0547289 + 0.4121573 0.0564551 + 0.3625555 0.0612187 + 0.3129537 0.0659824 + 0.2944570 0.0677588 + 0.2864751 0.0681027 + 0.2631344 0.0690212 + 0.2431549 0.0697067 + 0.2231755 0.0703921 + 0.2031960 0.0710776 + 0.1832166 0.0717630 + 0.1750000 0.0720449 + 0.1696407 0.0717592 + 0.1632797 0.0714200 + 0.1433727 0.0702522 + 0.1234657 0.0688715 + 0.1222984 0.0687906 + 0.1039075 0.0661569 + 0.0941392 0.0646227 + 0.0842841 0.0630749 + 0.0818568 0.0624205 + 0.0751055 0.0606006 + 0.0658445 0.0579605 + 0.0565836 0.0552562 + 0.0529896 0.0542067 + 0.0478730 0.0519526 + 0.0395115 0.0480937 + 0.0311500 0.0441621 + 0.0292787 0.0432822 + 0.0290956 0.0431560 + 0.0273482 0.0419509 + 0.0256109 0.0407528 + 0.0256009 0.0407458 + 0.0238535 0.0394864 + 0.0221062 0.0382048 + 0.0203588 0.0369232 + 0.0186115 0.0356416 + 0.0168641 0.0343601 + 0.0151168 0.0330785 + 0.0133694 0.0317969 + 0.0132474 0.0317074 + 0.0121593 0.0303466 + 0.0109895 0.0288459 + 0.0098198 0.0272622 + 0.0086500 0.0256785 + 0.0074877 0.0241049 + 0.0066048 0.0224983 + 0.0057275 0.0208377 + 0.0048502 0.0191597 + 0.0039729 0.0174408 + 0.0037728 0.0170180 + 0.0030652 0.0154099 + 0.0022187 0.0129160 + 0.0013721 0.0104221 + 0.0008583 0.0078391 + 0.0003445 0.0052560 + 0.0001722 0.0026280 + 0.0000000 0.0000000 + 0.0001722 -0.0026280 + 0.0003445 -0.0052560 + 0.0008583 -0.0078391 + 0.0013721 -0.0104221 + 0.0022187 -0.0129160 + 0.0030652 -0.0154099 + 0.0042301 -0.0177719 + 0.0053949 -0.0201340 + 0.0068581 -0.0223238 + 0.0083212 -0.0245136 + 0.0100577 -0.0264937 + 0.0117942 -0.0284738 + 0.0137743 -0.0302103 + 0.0157544 -0.0319467 + 0.0179442 -0.0334099 + 0.0201340 -0.0348731 + 0.0224960 -0.0360379 + 0.0248581 -0.0372028 + 0.0273520 -0.0380493 + 0.0298459 -0.0388959 + 0.0324289 -0.0394097 + 0.0350120 -0.0399235 + 0.0376400 -0.0400957 + 0.0402680 -0.0402680 + 0.0428960 -0.0400957 + 0.0455240 -0.0399235 + 0.0481071 -0.0394097 + 0.0506901 -0.0388959 + 0.0531840 -0.0380493 + 0.0556779 -0.0372028 + 0.0580399 -0.0360379 + 0.0604020 -0.0348731 + 0.0625918 -0.0334099 + 0.0647816 -0.0319467 + 0.0667617 -0.0302103 + 0.0687418 -0.0284738 + 0.0704782 -0.0264937 + 0.0722147 -0.0245136 + 0.0736779 -0.0223238 + 0.0751411 -0.0201340 + 0.0763059 -0.0177719 + 0.0774708 -0.0154099 + 0.0818568 -0.0073062 + 0.0842841 -0.0032485 + 0.0941392 0.0103179 + 0.1039075 0.0208497 + 0.1222984 0.0355868 + 0.1234657 0.0363657 + 0.1433727 0.0474230 + 0.1632797 0.0552655 + 0.1696407 0.0572531 + 0.1750000 0.0587679 + 0.1832166 0.0608289 + 0.2031960 0.0646844 + 0.2231755 0.0671125 + 0.2431549 0.0683904 + 0.2631344 0.0687109 + 0.2864751 0.0671027 + 0.2944570 0.0667588 + 0.3129537 0.0649824 + 0.3625555 0.0602187 + 0.4121573 0.0554551 + 0.4301315 0.0537289 + 0.4617590 0.0506915 + 0.5113608 0.0459278 + 0.5609626 0.0411642 + 0.5983523 0.0375733 + 0.6105644 0.0364005 + 0.6601662 0.0316369 + 0.7097680 0.0268732 + 0.7593697 0.0221096 + 0.7887696 0.0192861 + 0.8089715 0.0173459 + 0.8585733 0.0125823 + 0.9081751 0.0078187 + 0.9577769 0.0030550 + 1.0000000 -0.0010000 diff --git a/data/TUDELFT_V3_KITE/Surfplan_export/profiles/prof_9.png b/data/TUDELFT_V3_KITE/Surfplan_export/profiles/prof_9.png new file mode 100644 index 0000000..90ebec8 Binary files /dev/null and b/data/TUDELFT_V3_KITE/Surfplan_export/profiles/prof_9.png differ diff --git a/data/TUDELFT_V3_KITE/cpx_distributions/README.md b/data/TUDELFT_V3_KITE/cpx_distributions/README.md new file mode 100644 index 0000000..d328489 --- /dev/null +++ b/data/TUDELFT_V3_KITE/cpx_distributions/README.md @@ -0,0 +1,34 @@ +# TU Delft V3 kite — 2D Cp(x/c) distributions + +Chordwise pressure-coefficient (`cp_AOA_*.dat`) and skin-friction (`cf_AOA_*.dat`) +distributions for the **TU Delft V3 LEI kite midspan airfoil section** (rigid 2D), +from RANS CFD (Thijs/Kasper). + +File format: whitespace columns `# x y Cp` (or `... Cf`), i.e. column 0 = x/c, +column 1 = y/c, column 2 = Cp (or Cf). + +## Contents + +| Angle of attack | Cp file | Cf file | +|---|---|---| +| 2° | `cp_AOA_2.dat` | `cf_AOA_2.dat` | +| 6° | `cp_AOA_6.dat` | `cf_AOA_6.dat` | +| 8° | `cp_AOA_8.dat` | `cf_AOA_8.dat` | + +These are the only angles for which the genuine V3 midspan section Cp distribution +is available locally. + +## Provenance + +Copied from `WES_aero_sim_for_kite_design/data/`: +- AoA 2° and 8°: `cp_cf_kasper/` +- AoA 6°: `THIJS_CFD_alpha_6_V3_turbulent/postProcessing/surfaces/624/` + +The V3 section is identified by its airfoil geometry embedded in the files: +~13.4% thickness, max camber y ≈ 0.0953 (chord normalised to 1). + +## Not included + +`Pointwise-Openfoam-toolchain/.../cp_AOA_10.dat` was **excluded**: it is a generic +demo LEI profile from the Pointwise→OpenFOAM toolchain (~12.4% thickness, +`tube_size=0.08, c_x=0.15, c_y=0.08`), **not** the V3 section. diff --git a/data/TUDELFT_V3_KITE/cpx_distributions/cf_AOA_2.dat b/data/TUDELFT_V3_KITE/cpx_distributions/cf_AOA_2.dat new file mode 100644 index 0000000..2d874b9 --- /dev/null +++ b/data/TUDELFT_V3_KITE/cpx_distributions/cf_AOA_2.dat @@ -0,0 +1,78 @@ +# x y Cf +0.04276388755929555 0.06356556861711357 -5.3685492635529e-05 +0.03309840970977389 0.04825624850833229 -3.582704828999522e-08 +0.06326602268098154 0.03073779662338421 6.167786129151704e-05 +0.07384327410599092 0.01793640188203612 -0.0003192609764164146 +0.07660775307689469 0.01104437306879618 -0.000228764133061211 +0.07819996384396966 4.127780662861551e-05 -2.991057168005374e-06 +0.07202763627549755 -0.02108505542368243 -0.0004650481933519364 +0.06030740026776515 -0.03284898406007216 -0.000575092613061699 +0.05197415418742954 -0.03691972333053082 -0.0009533577132157866 +0.04841011551193194 -0.03797539582415126 -0.0012756171634708763 +0.04106259222822374 -0.03905070100038476 -0.002640959490398662 +0.0392041678823387 -0.03909985971367359 -0.002617383818930308 +0.03734550757143083 -0.03906061142774211 -0.0019512487320989223 +0.03549081466558211 -0.03893305307143092 -0.0004661649781977114 +0.02999235773064849 -0.03802447036198644 0.005671899478682422 +0.02819522910180473 -0.03754856560901278 0.007255265503267772 +0.02642274895181671 -0.03698778874565389 0.008437998523398466 +0.02467892578294952 -0.03634339841365394 0.009272998170146079 +0.02296771065963648 -0.03561683055468438 0.009818887434565653 +0.0212929661235993 -0.034809744871696 0.010121505031131855 +0.01965847119772771 -0.03392397792845034 0.010215736624071617 +0.01652493718577972 -0.03192453294918682 0.00988329670009842 +0.01503298224681096 -0.03081537691891334 0.009500925488249457 +0.01221552866592811 -0.02839075860041346 0.008406802228970986 +0.009641067793166569 -0.0257095339280482 0.00700174990074151 +0.0073328241508335 -0.02279597455813496 0.005439446132599986 +0.0004212849451338721 -0.0057241591407998 -9.821423814390917e-05 +0.0001929063854338941 -0.00387916574954884 -0.0001677770129790615 +3.715759961701779e-07 -0.000167057565479412 8.866677575636765e-06 +0.0003737770984357588 0.005393438845063324 0.0009648191807615032 +0.00106093352703043 0.009046469258067052 0.001906286770070402 +0.002091866108723892 0.01261774277269358 0.002862841031091262 +0.002733467311930074 0.01436259362075018 0.003226329655069884 +0.003457264975454637 0.01607498180921493 0.003373864267305056 +0.005118832334056953 0.01943119745211363 0.003244479859277398 +0.01551627563955031 0.03736815990984928 0.00640642847178748 +0.01999837151218267 0.04343539867217488 0.00746451057835034 +0.02363912034760315 0.04776628085227682 0.008134354582056486 +0.03738856887789012 0.0606405766812684 0.009765309341371863 +0.04832587972692123 0.06818926940941658 0.0116451778915289 +0.05399675986092924 0.07144915477192883 0.012011259579708933 +0.06072447947721454 0.07485584093822345 0.012147363864742813 +0.07472263851285585 0.08065345333266476 0.011970806903965334 +0.08887221045490147 0.08513218823611196 0.011442401741641892 +0.1793062895712234 0.09530077013388079 0.006856086486232446 +0.2117339898587441 0.09495067176705572 0.006628380949299338 +0.2581032333418144 0.09215324744689607 0.005192207194632046 +0.2996809993542806 0.08811305015450889 0.004457266541411632 +0.3567078053717327 0.08168243973732783 0.00394706807980882 +0.4309247818702319 0.07270373419804112 0.003581264429982738 +0.859748396615083 0.01806363700433209 0.002415898765630274 +0.990135014604125 0.00127095875812223 0.0018193027298150943 +0.9980660976826226 0.000249320580680477 0.00202920416730058 +0.9995004786446735 6.447374315074194e-05 0.002407312125848668 +0.9998066004065816 2.499749945161417e-05 0.002663732027943266 +1.0 0.0 0.003108088175078052 +1.000175570344442 -0.0003873748714666402 -0.0005394350261954327 +0.9999361185511577 -0.0004959023699215505 0.007227548955785272 +0.9997426931776413 -0.0004710692940610273 0.006070740947186466 +0.9992898615857166 -0.0004127908100313096 0.00523084111183753 +0.9984027748023451 -0.0002985631337174143 0.004596763351448572 +0.9969850747884935 -0.0001159634273836383 0.004125445965553146 +0.9925102286323966 0.0004604996516486448 0.003608634550155278 +0.9828366693570219 0.001706780446830773 0.003290951256256768 +0.8007931164398834 0.02514688172946307 0.0015438641554223705 +0.6227248105276376 0.04799391426786268 -0.000934765929254101 +0.5467873640304859 0.05766507732352074 -0.0017082583043725305 +0.4557607131227709 0.06912084908337753 -0.002057381475396368 +0.3439219432772413 0.08267370547347265 -0.001909630199411214 +0.2878908640215147 0.0888395395812468 -0.0016403223905841638 +0.2472404119045495 0.0925291412707685 -0.0012439644700339358 +0.2284148315498924 0.09375889647752622 -0.0008998106920897766 +0.1938680582880188 0.09475868486167134 6.862163771827565e-05 +0.1565453765113058 0.0943433883278578 3.083217355968188e-05 +0.1049056581150844 0.0884277941269339 0.0003608467874276816 +0.0596424493908252 0.07383844242791567 -7.284237499479304e-05 +0.04571639419151958 0.06586873418008751 -7.147357452011414e-05 diff --git a/data/TUDELFT_V3_KITE/cpx_distributions/cf_AOA_6.dat b/data/TUDELFT_V3_KITE/cpx_distributions/cf_AOA_6.dat new file mode 100755 index 0000000..a415d17 --- /dev/null +++ b/data/TUDELFT_V3_KITE/cpx_distributions/cf_AOA_6.dat @@ -0,0 +1,103 @@ +# x y Cf +0.04276388755929555 0.06356556861711357 -0.0001879585975700373 +0.03309840970977389 0.04825624850833229 -5.458507980308168e-07 +0.04467938624112796 0.03869402682902678 0.0002106766326490412 +0.06326602268098154 0.03073779662338421 0.00010683943074351959 +0.06739054964049444 0.0269898872199461 -5.94499259365639e-05 +0.07295141860002202 0.01956758285390561 -0.000747380127932433 +0.07465658066274587 0.01626466884529217 -0.000804869041349509 +0.07660775307689469 0.01104437306879618 -0.0005439448493615928 +0.07819996384396966 4.127780662861551e-05 -7.537638609578638e-06 +0.07606735331573551 -0.0127367285437733 -0.0003803835335502698 +0.0698766379503123 -0.02411656177727725 5.957956843802754e-05 +0.06744746907549139 -0.0269300909630248 -6.499428398172114e-05 +0.0647621273625294 -0.02950025730032107 -0.0005852397077900648 +0.0618448413245353 -0.03180378944934589 -0.0014060038402179437 +0.06030740026776515 -0.03284898406007216 -0.001682074499723797 +0.05872201224623478 -0.03381991329602371 -0.0016918762225057683 +0.05709225787431717 -0.03471437303988962 -0.0013994803680989652 +0.05371451517243328 -0.03626603972395559 -0.000359979333924828 +0.05197415418742954 -0.03691972333053082 -0.00011495450593594156 +0.05020468513322063 -0.0374899309873329 -0.000475260545252736 +0.04841011551193194 -0.03797539582415126 -0.0011372551904624323 +0.04659450074871872 -0.03837502119181638 -0.0013395688525546072 +0.04476194045990555 -0.03868788115484954 -0.0008744136341935016 +0.04291657957939413 -0.03891327036966102 9.44628992832468e-05 +0.03734550757143083 -0.03906061142774211 0.004816902116601814 +0.03549081466558211 -0.03893305307143092 0.00608226725442228 +0.03364428007162458 -0.03871749253601962 0.007029206136024282 +0.03181007738878976 -0.03841441105310851 0.007703113708221444 +0.02819522910180473 -0.03754856560901278 0.008392342601560906 +0.02642274895181671 -0.03698778874565389 0.008463133797438408 +0.02296771065963648 -0.03561683055468438 0.008153228574274977 +0.0212929661235993 -0.034809744871696 0.007803711062963378 +0.01806792542873962 -0.03296152056688918 0.006792102588333668 +0.0135954264885147 -0.02963656804659672 0.004726720235922244 +0.004414604365619548 -0.01804807451598938 -0.0008341513405588942 +0.002201042597788599 -0.01293351709306329 -0.0017822205566014335 +0.001139618108146739 -0.009371187244292476 -0.0018498189272529702 +0.0007370940576948314 -0.007556212895409316 -0.0017046704323901173 +0.0001929063854338941 -0.00387916574954884 -0.0010666383323894113 +3.662379381053231e-05 0.001691663021906976 0.0006406888791772092 +0.0003737770984357588 0.005393438845063324 0.00210616838314834 +0.00106093352703043 0.009046469258067052 0.003601831254573682 +0.002091866108723892 0.01261774277269358 0.004786120411857886 +0.002733467311930074 0.01436259362075018 0.00505463342148768 +0.003457264975454637 0.01607498180921493 0.00486538638803568 +0.004261644904234816 0.01775102853981628 0.00428531391161938 +0.005118832334056953 0.01943119745211363 0.004020012931537072 +0.007736073021854499 0.02444803701163283 0.005587124190501672 +0.01144677152350567 0.0310164207846188 0.007294117885428082 +0.02118547842718027 0.04490114709827182 0.011234697506728442 +0.02619677866261684 0.05053895293504266 0.012975279747399983 +0.03022079287608805 0.05451623977328293 0.01396098034110171 +0.03591200766144063 0.05946694354457675 0.01489134876295948 +0.04193582911527939 0.06400735561950383 0.01544564631552075 +0.04832587972692123 0.06818926940941658 0.01567609512254134 +0.05835478956095485 0.07370803105654668 0.015522115657526089 +0.07161579828052492 0.07949795292659076 0.014806722607193528 +0.1266732729257711 0.09241218332941833 0.010715983170303985 +0.1793062895712234 0.09530077013388079 0.007118694040138372 +0.2032346417643243 0.09514644933414897 0.00694971658395249 +0.2117339898587441 0.09495067176705572 0.006746221951628926 +0.2483205990229831 0.09294692974194078 0.005363551999315196 +0.2784114042826095 0.09028704743105777 0.00457644175844203 +0.3448969536063716 0.08306230186370865 0.003728681582367628 +0.4695323861056724 0.06790513097604357 0.003075773887487842 +0.9829114078197247 0.002201372895802811 0.0013510704629247409 +0.9925774208648203 0.000956364758547776 0.0014009684429290416 +0.9972419199369482 0.0003555045445397644 0.0016173392531597238 +0.998466960240747 0.0001976703981867997 0.0018179918899748826 +0.9993538313714398 8.3377965966741e-05 0.002157499247928278 +0.9998066004065816 2.499749945161417e-05 0.002622578724939286 +1.0 0.0 0.003238787035356226 +1.000175570344442 -0.0003873748714666402 -0.0006918343168879954 +0.9999361185511577 -0.0004959023699215505 0.00839085568823664 +0.999687684844556 -0.0004639930247993733 0.007152364057113668 +0.999127051009862 -0.000391829623832507 0.006354581004969732 +0.9978535823824828 -0.0002278312280225523 0.005627694100734124 +0.9961081405376435 -3.002163743061166e-06 0.005163856287339768 +0.9900661326324484 0.0007753784135630709 0.004588401666849688 +0.9775963350348024 0.002381866402965103 0.004222134720252326 +0.9160973581448664 0.01030344679663074 0.003731346418568748 +0.6931632727720849 0.03897495614094905 0.00279072966272386 +0.5978723947463842 0.05116649706761253 0.001947719684416495 +0.5208166631398357 0.06095334182358406 0.0006613558809871008 +0.3922624879189146 0.07693223386512749 -0.002536736281764828 +0.3322904223011696 0.08401082699279248 -0.003535543029570178 +0.2878908640215147 0.0888395395812468 -0.003634970950225882 +0.2570330318015585 0.09174362234289557 -0.003128121342685052 +0.237700137204438 0.09320459962989104 -0.002248277989597068 +0.2284148315498924 0.09375889647752622 -0.001515708017509105 +0.2106187027457731 0.09448229827809655 0.0002175146509524326 +0.2021120932873841 0.09466535260951009 0.0005338654385894822 +0.1781651575110249 0.09480029283794308 0.0003016745162148726 +0.1634966219696773 0.09459572113384887 -0.0001420950660125579 +0.1498463100440565 0.09399700027730548 -0.00026465443382224 +0.1433965052540251 0.09356118961842605 -0.00013273574782980165 +0.1200181280886662 0.09102301972975237 0.0012194013974886953 +0.1097221213345114 0.08934836918478271 0.0014867965630896767 +0.1003032010680039 0.08745876557389623 0.0015071913473745859 +0.08772421797878553 0.08431213223263931 0.0012033990065049374 +0.0596424493908252 0.07383844242791567 -0.0001087650154666008 +0.04571639419151958 0.06586873418008751 -0.000230212940186696 diff --git a/data/TUDELFT_V3_KITE/cpx_distributions/cf_AOA_8.dat b/data/TUDELFT_V3_KITE/cpx_distributions/cf_AOA_8.dat new file mode 100644 index 0000000..8b35b73 --- /dev/null +++ b/data/TUDELFT_V3_KITE/cpx_distributions/cf_AOA_8.dat @@ -0,0 +1,104 @@ +# x y Cf +0.04276388755929555 0.06356556861711357 -7.490305597749076e-05 +0.0332272197474035 0.0464048575148447 5.489757479981662e-06 +0.04832943775882318 0.03799302181036906 7.648519645172773e-05 +0.06326602268098154 0.03073779662338421 -0.0004285934373756168 +0.06739054964049444 0.0269898872199461 -0.00010477936600927875 +0.07465658066274587 0.01626466884529217 0.0008238438806884038 +0.07660775307689469 0.01104437306879618 0.0007641332622482074 +0.07779692838209128 0.005599704515728495 0.0003944887276465724 +0.07815773161604739 -0.001817316543405341 5.485397046001178e-06 +0.07299266376766034 -0.01949606863865545 0.0003637595759240324 +0.06869550509015264 -0.02555220766066108 -0.0002755839837928688 +0.0647621273625294 -0.02950025730032107 -0.000300275706798375 +0.06030740026776515 -0.03284898406007216 -0.001077784609359678 +0.05872201224623478 -0.03381991329602371 -0.0011923736195542543 +0.05709225787431717 -0.03471437303988962 -0.0009765999835591377 +0.05371451517243328 -0.03626603972395559 -0.00017016364552131216 +0.05197415418742954 -0.03691972333053082 -0.000316934051102485 +0.05020468513322063 -0.0374899309873329 -0.0007186906753935714 +0.04841011551193194 -0.03797539582415126 -0.0008223291733746979 +0.04659450074871872 -0.03837502119181638 -0.0003321541027406026 +0.04476194045990555 -0.03868788115484954 0.000708909299360625 +0.04106259222822374 -0.03905070100038476 0.003411477405885782 +0.0392041678823387 -0.03909985971367359 0.004562676628395266 +0.03734550757143083 -0.03906061142774211 0.005450530699843342 +0.03549081466558211 -0.03893305307143092 0.006097005211431964 +0.03181007738878976 -0.03841441105310851 0.006796980228486176 +0.02999235773064849 -0.03802447036198644 0.00689567806772039 +0.02642274895181671 -0.03698778874565389 0.006668905710799834 +0.02467892578294952 -0.03634339841365394 0.006369310395537206 +0.0212929661235993 -0.034809744871696 0.005459364209591542 +0.01806792542873962 -0.03296152056688918 0.00422525671759978 +0.01503298224681096 -0.03081537691891334 0.002783959272415512 +0.006285174856966845 -0.02126020489420191 -0.002040075274130408 +0.004414604365619548 -0.01804807451598938 -0.002852651469250982 +0.002857522257900208 -0.01467282465727422 -0.003236282554861442 +0.001627968848605174 -0.01116497425581786 -0.003138053712873696 +0.001139618108146739 -0.009371187244292476 -0.002901560687168934 +0.0004212849451338721 -0.0057241591407998 -0.0020642851403826 +0.0001929063854338941 -0.00387916574954884 -0.001476146818952331 +3.715759961701779e-07 -0.000167057565479412 -1.644386489545432e-05 +0.0003737770984357588 0.005393438845063324 0.002603987753563134 +0.00106093352703043 0.009046469258067052 0.004308258634942198 +0.002091866108723892 0.01261774277269358 0.005527573864036678 +0.002733467311930074 0.01436259362075018 0.005706970896045456 +0.003457264975454637 0.01607498180921493 0.005309936058614546 +0.004261644904234816 0.01775102853981628 0.004473939386915608 +0.005118832334056953 0.01943119745211363 0.004158086084503602 +0.008635223554445109 0.02610611957064 0.007385511866532802 +0.01242751415010598 0.03262757549417115 0.00978259046760362 +0.01770420762625004 0.04044102676627263 0.012222338373033381 +0.02363912034760315 0.04776628085227682 0.0143235871004763 +0.02885514542407597 0.05321522293646556 0.015645352546840394 +0.03445624798895486 0.05826760669966152 0.016566055170825496 +0.04193582911527939 0.06400735561950383 0.017172923889848403 +0.04832587972692123 0.06818926940941658 0.017252283211256866 +0.05611419408811015 0.07257157145324583 0.01696439047089495 +0.06587533521034071 0.07717347429902713 0.016298119454783315 +0.1266732729257711 0.09241218332941833 0.010882410759841941 +0.1793062895712234 0.09530077013388079 0.006945473027203948 +0.2032346417643243 0.09514644933414897 0.006722973839658798 +0.2117339898587441 0.09495067176705572 0.006496660844552578 +0.2581032333418144 0.09215324744689607 0.004671284441683954 +0.2889286523063009 0.08923439218084046 0.003906579283388574 +0.3567078053717327 0.08168243973732783 0.002987260139238476 +0.4565948443542798 0.06951968856030251 0.002289062476511266 +0.9694031190419137 0.003941412459140006 0.0009043310373034418 +0.9889687389773781 0.001421178244630541 0.0009258776436256234 +0.9964097252760367 0.0004627131567309691 0.0011074669809813652 +0.99820673288432 0.0002312005004618359 0.0013175655760859263 +0.9991910520319729 0.0001043590734837622 0.0016309343514974571 +0.999751597840673 3.209277336096525e-05 0.002140587170106048 +1.0 0.0 0.00296276751106508 +1.000175570344442 -0.0003873748714666402 -0.0007331902544534668 +0.9999361185511577 -0.0004959023699215505 0.008659718621007185 +0.9996297267405945 -0.000456535816824477 0.007355613923448616 +0.999039047920008 -0.0003804987101077882 0.00666339865975267 +0.9976974643155097 -0.0002077232855182258 0.00597884279573205 +0.9958589617720149 2.909655168354372e-05 0.005537713550330318 +0.9894974877848076 0.0008486392223196442 0.00499683301592507 +0.9763814569182623 0.002538361569031684 0.004630244339792308 +0.9076252478663309 0.01139458185905065 0.00387386270301527 +0.6469580977353171 0.04489502959157916 0.00241912394509005 +0.5338234109222841 0.05930804868788935 0.0013982687254335613 +0.468715778144382 0.06750438704698408 0.0003543146735410334 +0.4047377992866207 0.0754159191024797 -0.0009780189748020292 +0.3677534071070601 0.07987434616211128 -0.002030172351450874 +0.2986548580999721 0.08772078502814058 -0.004599280878719982 +0.2773623986528947 0.08988862416845661 -0.005113680899771088 +0.2670749342797846 0.09085981280457657 -0.005208437747751682 +0.2472404119045495 0.0925291412707685 -0.004988841818438576 +0.237700137204438 0.09320459962989104 -0.004674488674257146 +0.2193869169365694 0.09418446720371103 -0.003633045552383154 +0.2106187027457731 0.09448229827809655 -0.002856229566245596 +0.2021120932873841 0.09466535260951009 -0.0018243984739640291 +0.18588626433171 0.09479362983589781 0.0004778349305169992 +0.1781651575110249 0.09480029283794308 0.0008614761575344938 +0.1565453765113058 0.0943433883278578 0.000433684342034735 +0.1371926332586585 0.09304091952856827 0.0007608862155223732 +0.1049056581150844 0.0884277941269339 0.0005783583684360126 +0.08772421797878553 0.08431213223263931 0.0010117873864341426 +0.07686484046880157 0.08091124513785357 0.001067302452619815 +0.06477532213063705 0.07619793801987683 0.0008009974026476422 +0.04571639419151958 0.06586873418008751 3.101420874588672e-06 diff --git a/data/TUDELFT_V3_KITE/cpx_distributions/cp_AOA_2.dat b/data/TUDELFT_V3_KITE/cpx_distributions/cp_AOA_2.dat new file mode 100644 index 0000000..547e619 --- /dev/null +++ b/data/TUDELFT_V3_KITE/cpx_distributions/cp_AOA_2.dat @@ -0,0 +1,45 @@ +# x y Cp +0.04276388755929555 0.06356556861711357 -0.04348704157614704 +0.07295141860002202 0.01956758285390561 -0.05140991436041962 +0.07819996384396966 4.127780662861551e-05 -0.03197150713685026 +0.05197415418742954 -0.03691972333053082 -0.06706167387722287 +0.04476194045990555 -0.03868788115484954 -0.093378926915626 +0.02642274895181671 -0.03698778874565389 -0.3570219353414122 +3.715759961701779e-07 -0.000167057565479412 0.9276509910347256 +0.003457264975454637 0.01607498180921493 0.4005836069123416 +0.03445624798895486 0.05826760669966152 -1.2301611547758755 +0.05399675986092924 0.07144915477192883 -1.6426557894762874 +0.06587533521034071 0.07717347429902713 -1.7332221980026814 +0.07799552636294979 0.08179901473482278 -1.754837005226387 +0.09287072676787013 0.0861933468529139 -1.7249851475023648 +0.1159231761269345 0.09090247468259746 -1.6181096392589778 +0.1870216529466714 0.09529101859089956 -1.1515796372398734 +0.2204942894351738 0.09463860823428381 -1.0426813364294585 +0.2996809993542806 0.08811305015450889 -0.7066174109381166 +0.3332789074473506 0.08439957244416332 -0.6088681071632966 +0.3687000900536647 0.08026370487218884 -0.5287719422111052 +0.4056386418146471 0.07580832798897223 -0.4621736253348554 +0.4565948443542798 0.06951968856030251 -0.3890242262205662 +0.5603789570099812 0.0564429374001441 -0.2790317212774662 +0.6709889816587014 0.04232145397591842 -0.19137133392787326 +0.8985775863007591 0.0130638909219138 -0.036892685638763 +0.9944657367345657 0.000713131800744486 0.0471956570293942 +1.000043909301717 -9.851122780894417e-06 0.04151915164349936 +1.000216009864362 -0.000279890227385287 0.04597681465716156 +0.9955965259210384 6.290342386802663e-05 0.0850038637137151 +0.9837355911010561 0.001590970086407752 0.1131781895823859 +0.9160973581448664 0.01030344679663074 0.1857913336595896 +0.8247212136041724 0.02206815331132695 0.2449938402700558 +0.7835335477130784 0.02736682776276132 0.2578459502494688 +0.7458784011853928 0.03220726993377351 0.2591066500658992 +0.704181333714036 0.03756145616169322 0.2463676060298896 +0.6588094047445807 0.04337787974553729 0.2145042006489572 +0.4817165482691285 0.06587661484209784 0.004772282753139308 +0.4300565639732108 0.07230837544542264 -0.04186437951123492 +0.3799297067584973 0.0784194374404138 -0.07216280351532364 +0.3322904223011696 0.08401082699279248 -0.08709921870404717 +0.2878908640215147 0.0888395395812468 -0.08863313732386507 +0.237700137204438 0.09320459962989104 -0.0728984865243349 +0.1938680582880188 0.09475868486167134 -0.03756026982500518 +0.1781651575110249 0.09480029283794308 -0.03241020524223182 +0.04571639419151958 0.06586873418008751 -0.0433295353537881 diff --git a/data/TUDELFT_V3_KITE/cpx_distributions/cp_AOA_6.dat b/data/TUDELFT_V3_KITE/cpx_distributions/cp_AOA_6.dat new file mode 100755 index 0000000..7da2c81 --- /dev/null +++ b/data/TUDELFT_V3_KITE/cpx_distributions/cp_AOA_6.dat @@ -0,0 +1,55 @@ +# x y Cp +0.04276388755929555 0.06356556861711357 0.2831765988277916 +0.06177764393266522 0.03185174700135322 0.280866185252911 +0.07295141860002202 0.01956758285390561 0.25963219479103 +0.07815773161604739 -0.001817316543405341 0.3226553243711208 +0.07606735331573551 -0.0127367285437733 0.3146385901820192 +0.06744746907549139 -0.0269300909630248 0.342684532314071 +0.05709225787431717 -0.03471437303988962 0.234853067095021 +0.05020468513322063 -0.0374899309873329 0.284736464587777 +0.03734550757143083 -0.03906061142774211 0.15553190031453823 +0.03364428007162458 -0.03871749253601962 0.16954883769738988 +0.004414604365619548 -0.01804807451598938 0.9863929885981783 +0.002091866108723892 0.01261774277269358 -0.572820273285277 +0.03160995824607752 0.05579211544351708 -2.432245537380402 +0.04505807310843256 0.06612426369258909 -2.774111357485588 +0.05199666670865696 0.07034321801435707 -2.844549106106646 +0.06072447947721454 0.07485584093822345 -2.867363498348572 +0.07161579828052492 0.07949795292659076 -2.829796764734134 +0.08887221045490147 0.08513218823611196 -2.690124986734862 +0.1870216529466714 0.09529101859089956 -1.6416426798621342 +0.2889286523063009 0.08923439218084046 -1.0436464025123768 +0.3218640927071186 0.08569020328035809 -0.9017119855459382 +0.3567078053717327 0.08168243973732783 -0.7850028154244542 +0.3931790397022614 0.07732341274896151 -0.6877927119270986 +0.4437200012730869 0.07112018838118041 -0.5809462948976902 +0.5085472107892827 0.06300526498795977 -0.4741340541213232 +0.5731865715040237 0.05481455085516451 -0.388873206174795 +0.7259539967148313 0.03527045550632674 -0.232643572741015 +0.9854751216984708 0.0018711616089445 -0.003723889307472904 +0.9964097252760367 0.0004627131567309691 0.002753800976306192 +1.000216009864362 -0.000279890227385287 -0.004826394769678074 +0.9943997089817618 0.0002170811127827793 0.04868571797704922 +0.9808969261659528 0.001956677923828914 0.09055972385002321 +0.9537275624931341 0.005456518791413356 0.13731493299292824 +0.9119523907158397 0.01083728805011287 0.19026388337497901 +0.6469580977353171 0.04489502959157916 0.4433304082027882 +0.5852500607119644 0.05277533327406693 0.4920978858473208 +0.5467873640304859 0.05766507732352074 0.5105806429851942 +0.5208166631398357 0.06095334182358406 0.5137794956559404 +0.4947454515238927 0.06424033472807196 0.5061160251107084 +0.4557607131227709 0.06912084908337753 0.4691184029843818 +0.4173409525953314 0.07387363386197163 0.4012507759782244 +0.3208625976738804 0.08530086466606873 0.17770486868504992 +0.2986548580999721 0.08772078502814058 0.14367991508003292 +0.2773623986528947 0.08988862416845661 0.12653828962832644 +0.2570330318015585 0.09174362234289557 0.12949031449777215 +0.237700137204438 0.09320459962989104 0.16011249221611834 +0.2106187027457731 0.09448229827809655 0.269821412704075 +0.1938680582880188 0.09475868486167134 0.3075080023769274 +0.1634966219696773 0.09459572113384887 0.3147298735128626 +0.1498463100440565 0.09399700027730548 0.305568189083699 +0.1255076224116574 0.09176672636466693 0.270229612624369 +0.1147579180611961 0.0902151563016211 0.2651942739621602 +0.07359886901176031 0.07974335793549313 0.297013291348983 +0.04571639419151958 0.06586873418008751 0.284032411033685 diff --git a/data/TUDELFT_V3_KITE/cpx_distributions/cp_AOA_8.dat b/data/TUDELFT_V3_KITE/cpx_distributions/cp_AOA_8.dat new file mode 100644 index 0000000..ce2f14c --- /dev/null +++ b/data/TUDELFT_V3_KITE/cpx_distributions/cp_AOA_8.dat @@ -0,0 +1,59 @@ +# x y Cp +0.04276388755929555 0.06356556861711357 0.369754792445869 +0.05363791167027467 0.03629681937611275 0.369512825076681 +0.06023799864411321 0.03289369258862966 0.3803300404656592 +0.07295141860002202 0.01956758285390561 0.4525002946292716 +0.07538949147001935 0.0145561611100228 0.4564693840993162 +0.07801937057148131 0.003753985959382214 0.446882922242024 +0.07780867321095557 -0.005517988661109592 0.4783581156255348 +0.06613535838326418 -0.02824710359527738 0.5311348004808492 +0.05709225787431717 -0.03471437303988962 0.484086890575362 +0.05197415418742954 -0.03691972333053082 0.508181593031596 +0.04106259222822374 -0.03905070100038476 0.42315148127032 +0.03734550757143083 -0.03906061142774211 0.428382483023469 +0.0135954264885147 -0.02963656804659672 0.9678991392350302 +0.009641067793166569 -0.0257095339280482 1.0009297902025698 +0.006285174856966845 -0.02126020489420191 0.9426534686983296 +0.00106093352703043 0.009046469258067052 -1.0429979768839335 +0.02885514542407597 0.05321522293646556 -2.97701516742313 +0.04040120622143255 0.06291072902875927 -3.321950362170818 +0.05399675986092924 0.07144915477192883 -3.429003607616636 +0.06587533521034071 0.07717347429902713 -3.376878702524202 +0.07799552636294979 0.08179901473482278 -3.254197144337922 +0.1870216529466714 0.09529101859089956 -1.8327874863459042 +0.2784114042826095 0.09028704743105777 -1.1996806915789198 +0.3106619665783404 0.08692971356968779 -1.0297320028124064 +0.3448969536063716 0.08306230186370865 -0.8915062484382064 +0.3931790397022614 0.07732341274896151 -0.7482650769295596 +0.4437200012730869 0.07112018838118041 -0.6372226271092232 +0.5085472107892827 0.06300526498795977 -0.5230590187758586 +0.5859002138042717 0.05319596110636245 -0.4121258594586978 +0.6824259822772941 0.04085560183950949 -0.298421444448391 +0.8093373719000714 0.02455167460498058 -0.17313674963583736 +0.9660562316502389 0.004372544645865283 -0.0386406821187513 +0.993768270458061 0.000802973252239986 -0.02128853044019792 +1.000216009864362 -0.000279890227385287 -0.03062803960527396 +0.9943997089817618 0.0002170811127827793 0.0244731662116314 +0.9818912176243451 0.001828584093814853 0.06971150300101164 +0.960286674485311 0.004611622190581761 0.11726533977474018 +0.9274984236403104 0.00883501341243748 0.1677274935281196 +0.8530499807859604 0.01842190075726221 0.2484797697022198 +0.6588094047445807 0.04337787974553729 0.4164065158335706 +0.5852500607119644 0.05277533327406693 0.4895377938887346 +0.5208166631398357 0.06095334182358406 0.571407956570911 +0.4173409525953314 0.07387363386197163 0.7448626251611238 +0.3922624879189146 0.07693223386512749 0.7709991898354575 +0.3799297067584973 0.0784194374404138 0.7726402699823532 +0.3677534071070601 0.07987434616211128 0.7634659759410414 +0.3557467275473164 0.08129361982160917 0.741382005954384 +0.3322904223011696 0.08401082699279248 0.654753629482031 +0.2570330318015585 0.09174362234289557 0.14088055807058458 +0.237700137204438 0.09320459962989104 0.05907810318650006 +0.2284148315498924 0.09375889647752622 0.04303112368943488 +0.2193869169365694 0.09418446720371103 0.04419401824806442 +0.2106187027457731 0.09448229827809655 0.06265088115711293 +0.1707025886246218 0.09474637369056492 0.2715407898968978 +0.1565453765113058 0.0943433883278578 0.2872016129793902 +0.09590926783756276 0.08644641392015195 0.2638217468843654 +0.0596424493908252 0.07383844242791567 0.350286588153118 +0.04571639419151958 0.06586873418008751 0.3689025452585844 diff --git a/docs/Aerodynamic_model.pdf b/docs/Aerodynamic_model.pdf index 68ab9c0..e2383d5 100644 Binary files a/docs/Aerodynamic_model.pdf and b/docs/Aerodynamic_model.pdf differ diff --git a/docs/README.md b/docs/README.md index 0b2bc61..6243a3a 100644 --- a/docs/README.md +++ b/docs/README.md @@ -66,7 +66,7 @@ - Naming conventions - File and directory naming -12. **[Aerodynamic_model.pdf](Aerodynamic_model.pdf)** - Theoretical background and mathematical formulations +12. **[Aerodynamic_model.pdf](Aerodynamic_model.pdf)** - The aerodynamic model: formulation, conventions and validation (source in `aerodynamic_model/aerodynamic_model.tex`; figures from `validation/` and `figures/`) ## Quick Start Examples diff --git a/docs/Solver.md b/docs/Solver.md index 440a434..4007ede 100644 --- a/docs/Solver.md +++ b/docs/Solver.md @@ -244,22 +244,78 @@ non-uniform grid (the paper derives it for uniform rectangular wings). drag directions come from the flow at the quarter chord (TAT3, the paper's LL-Gaunaa), **`is_aoa_corrected=True`**, now the default. False keeps the 3/4-chord directions (the paper's LL-3/4): those overestimate the induced - drag by 20-40% against the Trefftz plane (figure 03), but historically gave - a better match to the V3 wind-tunnel drag, for reasons not yet understood, - which is why it used to be the default. + drag by 20-40% against the Trefftz plane (figure 03). They historically + matched the V3 wind-tunnel drag better, but only because that overshoot has + the same lift-dependent shape as the drag the wind tunnel measures beyond + every simulation, RANS included (Poland et al. 2026 attribute it to the + model's thick canopy and the flow behind the leading-edge tube); a + compensating error, not a physical effect. +- **Curved bound vortex at the quarter chord.** The quarter-chord flow is the + trailed system (Prandtl's downwash, exact on straight wings) plus the + influence of the curved or swept bound vortex. Evaluating the other panels' + bound segments on the line itself is logarithmically singular (Li, Gaunaa, + Pirrung, Ramos-Garcia & Horcas 2020), which showed up as an on-blade induced + drag that kept falling with the panel count on the V3 (0.93, 0.90, 0.86 of + the Trefftz value at 20, 50, 150 panels). The solver instead adds, for each + panel, the difference between the actual vortex system and the same wing + unrolled onto the panel's tangent line, with the bound vorticity and the leg + starting points spread along the chord with the thin-airfoil distribution + `sqrt((c-x)/x)` (`assemble_curved_bound_correction_AIC`). The correction is + identically zero on straight wings, has no tunable length (the distribution + fixes the cut-off at 0.15 c), and brings the on-blade induced drag within 1% + of the Trefftz plane on an arc wing, the V3 and the paper's swept-anhedral + elliptic wing at every resolution (figure 08). The correction depends on + the panel geometry only, so it is computed once per geometry (about 0.5 s at + 50 panels) and reused for every inflow and circulation until the panels are + rebuilt or moved; a coupled trim that solves one deformed shape hundreds of + times pays for it once. It shares its mechanism with + the effective-lifting-line method of Reid & Hunsaker (2020), whose two + empirical lengths it replaces. On the V3 it raises the drag by about 5% of the + induced drag at 10 deg, onto the RANS value (figure 09). - **`is_with_attached_trailed_vortex_force`** (default True): Kutta-Joukowski force on the chordwise vortex legs between the bound vortex and the trailing edge (Sec. 3 of the paper). Zero net effect on unswept wings, needed for swept ones. Reported per section boundary in `F_attached_trailed_distribution` and folded into `F_distribution` and the totals. +- **Direction of the attached-trailed legs**, + `BodyAerodynamics(..., leg_direction=...)` (also on `instantiate`). The paper + states that theory does not fix this direction and chooses the chordwise + direction of CP1, perpendicular to the quarter-chord line, up to the + trailing edge. Three constructions are available (figure 10): `"rib"` + (default) runs the legs along the interpolated input sections to their + trailing-edge points, with the control point on the section, a fixed + vortex system independent of the inflow; `"streamwise"` runs them along + the mean apparent velocity projected onto the surface, up to the + trailing-edge curve, and rebuilds them whenever `va` is set; `"perpendicular"` + is the paper's choice, with the tangent of the quarter-chord line averaged + over one leg length so that the legs do not cross at a planform kink. With + the last two the control point moves with the legs so that it stays between + the panel's own two legs. Against a vortex-lattice method (AeroSandbox, + `docs/validation/vlm_referee.py`, figure 11) rib and streamwise legs + reproduce the lift of straight, yawed, kinked-swept, arc and + swept-anhedral elliptic wings within 1% (streamwise: 2% low on the arc) and + the induced drag within 1-4%; perpendicular legs lose 9% of the lift on the + kinked swept wing and 8% on the V3, where they also cross at the tips above + 80 panels. The rib legs stay the default: same accuracy, a fixed vortex + system, and the code's main use is trim finding with an inflow that varies + along the span, where the streamwise construction can only use the mean + direction. Streamwise legs are the option for uniform-inflow studies of + wings whose sections are drawn obliquely to the flow, or in sideslip: they + give the closest V3 polars to RANS (and the RANS lift slope) at a cost of + 1-3% of lift on the nearly vertical tips of an arc wing at 4-15 deg, where + the chordwise rib legs are within 2%. An inflow without a mean direction + (pure rotation) falls back to the rib legs with a warning. Details in + `docs/validation/curved_bound_vortex_report.pdf`, Section 4.3. Every solve of a wing in one uniform inflow also reports `results["drag_induced_trefftz"]`, the Trefftz-plane induced drag (`BodyAerodynamics.compute_trefftz_plane_induced_drag`): the far-wake value that does not depend on where the forces are evaluated on the blade. With -quarter-chord directions and the attached-trailed force the on-blade drag of -an inviscid straight wing matches it to four digits; with 3/4-chord -directions it does not. It is `None` for per-panel inflow or body rates. +quarter-chord directions, the attached-trailed force and the curved +bound-vortex regularisation the on-blade drag of an inviscid straight wing +matches it to four digits and that of curved wings and the V3 within 1%, +independently of the panel count; with 3/4-chord directions it does not. It +is `None` for per-panel inflow or body rates. Effect on the TUDELFT V3 kite polars (CAD geometry, CFD+NeuralFoil polars, 50 panels) against RANS and wind-tunnel data: the dashed line is the solver @@ -270,7 +326,7 @@ chord directions, the red line with quarter-chord directions ![V3 polars before and after the consistency fixes](consistency_fixes_V3_polars.png) More figures (frame, Trefftz check, attached-trailed force, relaxation, -wake direction) with their descriptions are in [figures/README.md](figures/README.md). +wake direction, leg direction) with their descriptions are in [figures/README.md](figures/README.md). ### Frozen wake direction diff --git a/docs/aerodynamic_model/.gitignore b/docs/aerodynamic_model/.gitignore new file mode 100644 index 0000000..5b4eb77 --- /dev/null +++ b/docs/aerodynamic_model/.gitignore @@ -0,0 +1,9 @@ +# build artefacts; the compiled document is installed as docs/Aerodynamic_model.pdf +*.aux +*.log +*.out +*.toc +*.fls +*.fdb_latexmk +*.synctex.gz +aerodynamic_model.pdf diff --git a/docs/aerodynamic_model/aerodynamic_model.tex b/docs/aerodynamic_model/aerodynamic_model.tex new file mode 100644 index 0000000..8371203 --- /dev/null +++ b/docs/aerodynamic_model/aerodynamic_model.tex @@ -0,0 +1,360 @@ +\documentclass[11pt,a4paper]{article} +\usepackage[margin=24mm]{geometry} +\usepackage{amsmath,amssymb,bm} +\usepackage{graphicx} +\usepackage{booktabs} +\usepackage{array} +\usepackage{enumitem} +\usepackage[hidelinks]{hyperref} +\usepackage{microtype} +\graphicspath{{../figures/}{../}} +\setlength{\parskip}{4pt} +\newcommand{\vect}[1]{\mathbf{#1}} +\newcommand{\code}[1]{\texttt{#1}} + +\title{The aerodynamic model of the Vortex Step Method code\\[4pt]\large formulation, conventions and validation} +\author{Vortex-Step-Method repository, \code{docs/aerodynamic\_model/aerodynamic\_model.tex}} +\date{October 2026} + +\begin{document} +\maketitle + +\begin{abstract} +\noindent This document describes the aerodynamic model implemented in the Vortex-Step-Method (VSM) code as it stands in October 2026: a nonlinear lifting-line method in which an outer three-dimensional vortex system (bound vortex, attached-trailed legs, frozen semi-infinite wake) is coupled to an inner two-dimensional airfoil model (polars) with the consistent coupling rules of Gaunaa, Li and Pirrung (TORQUE 2026). It states every convention the results depend on, gives the equations as they are coded, and collects the validation: analytical wings, the Trefftz-plane consistency of the induced drag, a vortex-lattice referee on planar, swept, curved and kite-like wings, and the TU~Delft V3 kite against RANS and wind-tunnel data. It replaces the thesis chapter that this file used to hold. Each section names the code that implements it and the script that reproduces its figures. +\end{abstract} + +\tableofcontents + +\section{Scope and how to read this document} +\label{sec:scope} +The code computes the steady aerodynamic forces and moments on one or more thin lifting surfaces in a prescribed inflow, including rigid-body rotation rates and an inflow that varies along the span, with optional bridle lines. It is used for kite design, trim and stability analysis and as the aerodynamic module of fluid--structure couplings, where it must be fast (a kite with 50 panels solves in well under a second), robust on unconventional planforms (curved, swept, low aspect ratio) and free of three-dimensional empirical data. Potential-flow vortex methods meet these requirements; among them the lifting line with sectional polars is the fastest, and its known weaknesses on curved and swept wings are what the coupling rules of Section~\ref{sec:coupling} address. + +Sections~\ref{sec:assumptions}--\ref{sec:solver} describe the model: assumptions, geometry and discretisation, the vortex system, the inner--outer coupling, and the solver with its outputs. Section~\ref{sec:validation} is the validation. Section~\ref{sec:conventions} is a table of conventions for people who read results. Every figure is generated by a script in \code{docs/validation} or \code{docs/figures}, named in its caption, and the numbers quoted in the text come from the JSON files those scripts write. The detailed derivation of the curved bound-vortex regularisation and the comparison with the literature are in a separate report, \code{docs/validation/curved\_bound\_vortex\_report.pdf}~\cite{report}; this document only summarises it. + +\section{Assumptions} +\label{sec:assumptions} +\begin{itemize}[nosep] + \item \textbf{Inner and outer region.} The flow is split into an inner region, where each wing section behaves as a two-dimensional airfoil described by its polar $C_l(\alpha)$, $C_d(\alpha)$, $C_m(\alpha)$, and an outer region of inviscid, irrotational, incompressible flow represented by vortex filaments. Viscosity enters only through the polars and the optional spanwise friction correction. + \item \textbf{Thin lifting surface.} The wing is represented by its sections' chord lines: a bound vortex on the quarter-chord line, the chordwise load distribution otherwise ignored. Thickness and camber act through the polars. + \item \textbf{Kutta--Joukowski in every section.} The sectional lift and the bound circulation are tied by $\Gamma=\tfrac12|\vect V_{\mathrm{inner}}|\,c\,C_l$ (Section~\ref{sec:gamma}); the no-penetration condition of the classical lifting line is replaced by this nonlinear relation, which admits any polar. + \item \textbf{Quasi-steady flow.} Every flow condition is solved as a steady problem; the starting vortex is at infinity, and rotation rates enter as a steady velocity distribution. Shed (time-varying) vorticity is not modelled. + \item \textbf{Frozen wake.} The trailed vorticity leaves the trailing edge along the local apparent velocity as straight semi-infinite filaments; wake roll-up and deformation are neglected. On a translating wing this is Prandtl's wake. + \item \textbf{Crossflow principle.} Only the component of the relative velocity perpendicular to the local quarter-chord line drives the inner airfoil model (CP1 of~\cite{gaunaa2026}); the spanwise component does not change the pressure forces. +\end{itemize} + +\section{Geometry and discretisation} +\label{sec:geometry} +\paragraph{Input.} A wing is a list of sections, each a leading-edge point $\vect{LE}$, a trailing-edge point $\vect{TE}$ and a polar (\code{Wing.add\_section}, or the YAML file read by \code{BodyAerodynamics.instantiate}). Coordinates are in a right-handed global frame; the examples and the kite data use $x$ downstream, $y$ to port (left tip), $z$ up. The sections are sorted along the span and need not be parallel: on a kite they are the ribs between inflatable struts, oblique to the flow near the tips. + +\paragraph{Mesh refinement} (\code{WingGeometry.refine\_aerodynamic\_mesh}). The quarter-chord points $\vect{LE}+\tfrac14(\vect{TE}-\vect{LE})$ form a polyline; $N+1$ new sections are placed along its arc length with uniform or cosine spacing (\code{spanwise\_panel\_distribution}), or the provided sections are split or kept. The chord vector of a new section interpolates the direction and the length of the two nearest input ribs, and its polar interpolates their coefficients on a common angle grid. The polars of the two bounding sections are averaged for each panel. + +\paragraph{Panel frame} (\code{BodyAerodynamics.compute\_panel\_properties}, Figure~\ref{fig:frame}). For panel $i$ between sections $j$ and $j+1$, the bound vortex runs between the quarter-chord points $\vect b_1$, $\vect b_2$ of the two sections. The frame is orthonormal and built on it: +\begin{align} +\vect e_{s}&=\frac{\vect b_1-\vect b_2}{|\vect b_1-\vect b_2|} &&\text{span, towards the left tip,}\notag\ +\vect e_{c}&=\frac{\vect t-(\vect t\cdot\vect e_s)\vect e_s}{|\vect t-(\vect t\cdot\vect e_s)\vect e_s|} &&\text{chord, leading to trailing edge,}\ +\vect e_{n}&=\vect e_c\times\vect e_s &&\text{normal, towards the suction side,}\notag +\end{align} +where $\vect t$ is the section chord direction. The chord $c$ of the panel is the length of the section chord vectors with their component along $\vect e_s$ removed, averaged over the two sections: chord and section plane are perpendicular to the local quarter-chord line (CP1). On a straight wing with ribs along the flow this is the plain chord; on the V3 kite tips, where the ribs are skewed by up to $0.77$ in chord-to-span dot product, it differs from the rib chord. In the code the three unit vectors are \code{z\_airf}, \code{y\_airf} and \code{x\_airf}. + +\paragraph{Reference points.} The aerodynamic centre $\vect P_i$ lies on the bound segment, at a spanwise position that weights the neighbouring panel widths (the \code{ncp} rule inherited from~\cite{damiani2019}), and is the point where the force directions are evaluated. The control point $\vect C_i$, where the inner velocity magnitude and angle of attack are evaluated, lies at the control-point chord fraction (default $3/4$) on the interpolated section; the bound vortex at the aerodynamic-centre fraction (default $1/4$). Both fractions are parameters of \code{BodyAerodynamics}. + +\begin{figure}[t]\centering +\includegraphics[width=\textwidth]{02_v3_frame_drag_direction_viscous_beta.png} +\caption{Panel frame on the V3 kite (\code{docs/figures/make\_pr\_figures.py}). Top left: the old frame built from the rib (blue) and the span-perpendicular frame of CP1 now used (red) at the left tip. Top right: the chord-to-span skew of the ribs and the angle by which a drag direction taken from the global span axis missed the local flow, up to $108^\circ$ at the tips. Bottom: the spanwise-flow angle $\beta$ of the viscous correction (Section~\ref{sec:viscous}) from the geometry alone (old) and from the relative velocity (new), at $0$ and $10^\circ$ sideslip.} +\label{fig:frame} +\end{figure} + +\section{The vortex system} +\label{sec:vortex} +\paragraph{Filaments} (\code{Panel}, \code{Filament}). Each panel carries a ring of five filaments of strength $\Gamma_i$ (Figure~\ref{fig:legs}): the bound segment $\vect b_2\to\vect b_1$ on the quarter-chord line, two attached-trailed legs from the bound points to the trailing-edge points $\vect T_1$, $\vect T_2$, and two semi-infinite wake filaments from $\vect T_1$ and $\vect T_2$ along the local apparent velocity, with opposite sense. Neighbouring panels share a section boundary, so the net trailed strength on boundary $j$ is $\Gamma_j-\Gamma_{j-1}$, as Helmholtz' theorems require. + +\paragraph{Induced velocities.} A straight segment $\vect A\to\vect B$ of strength $\Gamma$ induces at $\vect P$, with $\vect r_0=\vect B-\vect A$, $\vect r_1=\vect P-\vect A$, $\vect r_2=\vect P-\vect B$, +\begin{equation} +\vect u=\frac{\Gamma}{4\pi}\,\frac{\vect r_1\times\vect r_2}{|\vect r_1\times\vect r_2|^2}\;\vect r_0\cdot\Big(\frac{\vect r_1}{|\vect r_1|}-\frac{\vect r_2}{|\vect r_2|}\Big), +\label{eq:segment} +\end{equation} +and a semi-infinite filament from $\vect A$ along the unit direction $\vect e$, +\begin{equation} +\vect u=\frac{\Gamma}{4\pi}\,\frac{1+\vect r_1\cdot\vect e/|\vect r_1|}{|\vect r_1\times\vect e|^2}\;\vect r_1\times\vect e . +\label{eq:semiinf} +\end{equation} +Near a filament the velocity is regularised. The bound segments use a Vatistas core of radius $\epsilon_1=\delta\,|\vect r_0|$ with $\delta$ the \code{core\_radius\_fraction} (default $10^{-20}$, i.e.\ no core: the model never evaluates a bound segment on its own line, Section~\ref{sec:curved}); the legs and the wake use the viscous core of~\cite{damiani2019}, $\epsilon_2=\sqrt{4\alpha_0\nu\,r_\perp/U}$ with the Oseen parameter $\alpha_0=1.25643$, $\nu$ the kinematic viscosity and $r_\perp$ the distance along the filament to the foot of the evaluation point. Inside a core the velocity ramps linearly to zero; on the line it is zero. + +\paragraph{Influence matrices} (\code{BodyAerodynamics.compute\_AIC\_matrices}, \code{utils.assemble\_AIC\_matrices}). The velocity induced at every evaluation point by every ring of unit strength is assembled once per inflow into three $N\times N$ matrices, $\vect u=\mathrm{AIC}_x\boldsymbol\Gamma$ etc., evaluated with a numba kernel. For the VSM the evaluation points are the control points and the bound segment of the panel's own ring is included; for the LLT variant they are the aerodynamic centres and the own bound segment is excluded (it induces nothing on its own line). In the VSM the two-dimensional velocity of an infinite vortex of the panel's own strength along its bound direction is subtracted at its control point (TAT1 of~\cite{gaunaa2026}, Section~\ref{sec:coupling}), so that the inner model sees the three-dimensional disturbance only. + +\paragraph{Wake direction} (\code{Wake.frozen\_wake}). Each ring's wake follows that panel's own apparent velocity: the freestream plus the body-rate term, or the distributed inflow. In a uniform inflow this is the classical single straight wake; under yaw or roll rates it is locally aligned. On a translating wing the wake direction is a second-order effect (a per-panel wake changes the V3 lift by under 1\,\%). + +\paragraph{Direction of the attached-trailed legs} (\code{leg\_direction}, Figure~\ref{fig:legs}). Theory does not fix the path of the attached vorticity between the bound vortex and the trailing edge; \cite{gaunaa2026} calls it a modelling choice and takes the chordwise direction of CP1. The code offers three constructions. \code{"rib"} (default) runs the legs along the interpolated sections to their trailing-edge points, with the control point on the section: a fixed vortex system, independent of the inflow. \code{"streamwise"} runs them along the mean apparent velocity projected onto the surface, up to the trailing-edge curve, rebuilt whenever the inflow is set. \code{"perpendicular"} is the paper's construction, with the quarter-chord tangent averaged over one leg length so that the legs fan out instead of crossing at a planform kink. In the last two the control point moves with the legs so that it stays between the panel's own two legs. Section~\ref{sec:val-legs} compares the three against a vortex-lattice method; the rib legs are the default because the code's main use is trim finding with an inflow that varies along the span, where only the rib construction is unambiguous, and their accuracy is the same. + +\begin{figure}[p]\centering +\includegraphics[width=\textwidth]{10_leg_direction_geometry.png} +\caption{The vortex system seen from above (flow downwards) and from the side for the three leg constructions (\code{docs/validation/leg\_direction\_study.py --figures}): bound vortex (black), aerodynamic centres (dots), control points (crosses in the colour of their legs), rib legs (blue), streamwise legs (green), perpendicular legs (red), and the lattice of the vortex-lattice referee of Section~\ref{sec:val-legs} in grey. (a) A straight wing whose ribs are drawn skewed; (b) the root of a kinked swept wing; (c) a swept--anhedral elliptic wing; (d) the arc wing from the side, where streamwise legs climb along the nearly vertical tips; (e, f) the V3 kite and its tip.} +\label{fig:legs} +\end{figure} + +\section{The inner--outer coupling} +\label{sec:coupling} +This section follows the coupling rules of Gaunaa, Li and Pirrung~\cite{gaunaa2026}, which the code implements in full except for the direction of the legs (previous section) and the treatment of the curved bound vortex at the quarter chord (Section~\ref{sec:curved}). The rules are: CP1, only the span-perpendicular velocity drives the inner model; TAT1, that velocity excludes the section's own two-dimensional disturbance; TAT2, the magnitude of the sectional force follows from the flow at the three-quarter chord; TAT3, its direction from the flow at the quarter chord. + +\subsection{Inner velocity and circulation} +\label{sec:gamma} +At the control point $\vect C_i$ the three-dimensional relative velocity is the apparent velocity plus the induced velocity of the whole vortex system, $\vect V_{3D,i}=\vect V_{a,i}+\sum_j \mathrm{AIC}_{ij}\Gamma_j$. The inner velocity is its span-perpendicular part minus the two-dimensional disturbance of the section's own bound vortex (CP1 and TAT1): +\begin{equation} +\vect V_{\mathrm{inner},i}=\vect V_{3D,i}-(\vect V_{3D,i}\cdot\vect e_{s,i})\vect e_{s,i}-\vect V_{2D,\mathrm{bound},i},\qquad +\alpha_{3/4,i}=\operatorname{atan2}(\vect V_{\mathrm{inner},i}\cdot\vect e_{n,i},\ \vect V_{\mathrm{inner},i}\cdot\vect e_{c,i}), +\end{equation} +and the circulation follows from the sectional lift (eq.~4 of~\cite{gaunaa2026}, TAT2): +\begin{equation} +\Gamma_i=\tfrac12\,|\vect V_{\mathrm{inner},i}|\,c_i\,C_{l,i}(\alpha_{3/4,i}). +\label{eq:gamma} +\end{equation} +For a flat plate, $C_l=2\pi\alpha$, this is Weissinger's three-quarter-chord condition: on an infinite wing the bound vortex at $c/4$ induces at $3c/4$ exactly the downwash that makes the flow tangent there. On a swept infinite wing the perpendicular chord and velocity in~\eqref{eq:gamma} give the lift slope $2\pi\cos\Lambda$ of simple sweep theory; a streamwise chord would not. + +\subsection{Force magnitudes and directions} +\label{sec:forces} +The sectional lift, drag and moment per unit span use the inner velocity and the polar at $\alpha_{3/4}$: +\begin{equation} +L'_i=\tfrac12\rho|\vect V_{\mathrm{inner},i}|^2 c_i C_{l,i},\qquad D'_i=\tfrac12\rho|\vect V_{\mathrm{inner},i}|^2 c_i C_{d,i},\qquad M'_i=\tfrac12\rho|\vect V_{\mathrm{inner},i}|^2 c_i^2 C_{m,i}, +\end{equation} +multiplied by the panel width along the bound vortex. Their directions follow TAT3: the lift is perpendicular and the drag parallel to the undisturbed relative flow at the quarter chord, $\vect V_{1/4,i}$, within the span-normal plane (\code{is\_aoa\_corrected=True}, the default). $\vect V_{1/4,i}$ is the apparent velocity plus the induction of the trailed system (legs and wake of every ring) at $\vect P_i$, which for a straight wing is Prandtl's exact downwash, plus the influence of the curved or swept bound vortex of the next section. Taking the directions from the three-quarter-chord flow instead (\code{is\_aoa\_corrected=False}, the paper's LL-3/4) overestimates the induced drag by 20--40\,\%, Section~\ref{sec:val-trefftz}. The moment acts about the bound vortex, with the polar's $C_m$ referred to the quarter chord. + +\subsection{The curved bound vortex at the quarter chord} +\label{sec:curved} +On a curved or swept quarter-chord line the neighbouring bound segments induce a velocity at $\vect P_i$ that grows as the logarithm of the panel width: the lifting line evaluated on itself is singular (Li et al.~\cite{li2020}). The on-blade induced drag is $\rho\Gamma(w\cos\alpha+u\sin\alpha)$ with $w$ an induced angle of about one degree, so a chordwise velocity $u$ of 3\,\% of the freestream moves the drag by 12\,\%; evaluating the other panels' segments on the line made the V3 induced drag fall from 0.93 to 0.86 of the Trefftz value between 20 and 150 panels. The code regularises the term without a free parameter~\cite{report}: for each panel $i$ the wing is unrolled onto the tangent line of panel $i$, and the quarter-chord velocity is the classical downwash of the unrolled wing plus the difference between the actual and the unrolled vortex systems, both with the bound vorticity and the leg starting points spread along the chord with the thin-airfoil distribution $\gamma(x)\propto\sqrt{(c-x)/x}$ (24 strips, effective cut-off $0.15\,c$): +\begin{equation} +\vect V_{1/4,i}=\vect V_{a,i}+\sum_j\Gamma_j\Big[\vect K^{\mathrm{LL}}_{j,\mathrm{unrolled}(i)}(\vect P_i)+\vect K^{\mathrm{sheet}}_{j}(\vect P_i)-\vect K^{\mathrm{sheet}}_{j,\mathrm{unrolled}(i)}(\vect P_i)\Big]. +\label{eq:curved} +\end{equation} +The bracket is identically zero on a straight wing, finite everywhere, and the wake terms cancel between the two systems. Figure~\ref{fig:unroll} shows the unrolled reference wing for two panels of an arc wing and one of a kinked swept wing: it is a different straight wing for every panel, tangent at that panel, and it coincides with the real wing in the panel's neighbourhood, which is why the singular near field cancels in the difference and only the far-field effect of the curvature remains. Figure~\ref{fig:spread} shows the spreading itself: the strips on real and unrolled panels, and the induction of one neighbouring segment at the quarter-chord point as a filament and as a sheet, identical beyond a chord, finite within it only for the sheet, so that the sum over the neighbours settles at every panel count instead of growing with the logarithm. Implementation: \code{utils.assemble\_curved\_bound\_correction\_AIC}, about 0.5\,s at 50 panels. The matrix depends on the panel geometry only, so it is computed once per shape and reused for every inflow and circulation; a body that is the cached shape up to a rigid rotation and translation reuses the stored matrix rotated, which keeps a trim that rotates the kite for every residual evaluation at the cost of one evaluation per deformed shape. It shares its mechanism with the effective-lifting-line method of Reid and Hunsaker~\cite{reid2020}, whose two empirical lengths it replaces with the thin-airfoil distribution. Figure~\ref{fig:curved} shows the problem and the result; the full derivation, the literature and the rejected alternatives are in~\cite{report}. + +\begin{figure}[p]\centering +\includegraphics[width=\textwidth]{08_curved_bound_vortex_regularisation.png} +\caption{The curved bound vortex at the quarter chord (\code{docs/validation/curved\_bound\_vortex\_validation.py --figures}). (a) The log-singular self-induction of a curved line; (b) the chordwise velocity at mid-span of an arc wing from the polyline (grows with the panel count) and from the chordwise-spread sheet (finite); (c) why the drag is sensitive to it; (d) the thin-airfoil distribution; (e) the construction of eq.~\eqref{eq:curved}; (f) on-blade over Trefftz induced drag against panel count on the arc and the V3, before (dashed) and after (solid).} +\label{fig:curved} +\end{figure} + +\begin{figure}[p]\centering +\includegraphics[width=\textwidth]{14_unrolling_per_panel.png} +\caption{Unrolling the wing onto the evaluation panel's tangent (\code{docs/validation/unrolling\_figure.py}). (a, b) Arc wing: the reference wings of a mid-span panel (blue) and a near-tip panel (red), each straight and tangent at its own panel (star), nodes at their arc-length distances. (c) Kinked swept wing from above: the reference of a panel on the right half lays the other half along that panel's line, legs along that panel's chord direction. (d) Distance between actual and unrolled nodes against their distance from the evaluation panel: the near field coincides and cancels, the far field is the curvature effect.} +\label{fig:unroll} +\end{figure} + +\begin{figure}[p]\centering +\includegraphics[width=\textwidth]{15_chord_spread.png} +\caption{The chordwise spreading (\code{docs/validation/chord\_spread\_figure.py}). (a) Three panels of the arc wing from above: the bound vortex of each panel replaced by 24 strips at the thin-airfoil stations, line weight and shade proportional to the strip weight, each strip with its own legs on the node's leg line. (b) The same strips on the real, curved panels (red) and on the unrolled, straight panels of the middle panel's reference (green): in the panel's neighbourhood the two sheets nearly coincide, which is why their difference is small there. (c) The chordwise velocity that one neighbouring bound segment of unit strength induces at the quarter-chord point $\mathbf P_i$ against its arc-length distance, as a single filament and as the spread sheet: beyond a chord the two are the same, within a chord the filament grows without bound and the sheet levels off at the cut-off $0.15\,c$ that the distribution fixes. (d) The running sum of those contributions from the far field inwards for 40 to 320 panels: with filaments every refinement adds a new nearest neighbour and the sum keeps rising, the logarithm of Li et al.; with sheets the sum settles to the same value at every panel count.} +\label{fig:spread} +\end{figure} + +\subsection{The attached-trailed force} +\label{sec:atforce} +The chordwise legs between the bound vortex and the trailing edge are attached to the wing and carry a Kutta--Joukowski force (Section~3 of~\cite{gaunaa2026}). On section boundary $j$, with net strength $\Delta\Gamma_j=\Gamma_j-\Gamma_{j-1}$ and leg vector $\vect l_j$, +\begin{equation} +\vect F_{\mathrm{AT},j}=\rho\,\Delta\Gamma_j\,\vect V_{3D}(\vect Q_j)\times\vect l_j, +\end{equation} +where $\vect V_{3D}(\vect Q_j)$ is the full relative velocity at the leg's three-quarter point, the point where the flow has no component through the wing. The force is spanwise and cancels on an unswept wing; on swept wings and on the curved outboard parts of a kite it changes lift and induced drag (Figure~\ref{fig:at}). It is on by default (\code{is\_with\_attached\_trailed\_vortex\_force}) and reported per boundary in \code{F\_attached\_trailed\_distribution}. + +\begin{figure}[t]\centering +\includegraphics[width=0.9\textwidth]{04_v3_attached_trailed_force.png} +\caption{Attached-trailed force on the inviscid V3 at $10^\circ$ (\code{make\_pr\_figures.py}). Left: force vectors per section boundary. Right: vertical and streamwise components, largest over the curved outboard region and zero at mid-span where the legs follow the flow. Total 18\,N of 760\,N lift.} +\label{fig:at} +\end{figure} + +\subsection{Spanwise-flow viscous correction} +\label{sec:viscous} +With \code{is\_with\_viscous\_drag\_correction} the friction force of each section is corrected for the spanwise flow following Gaunaa, Sørensen and Li~\cite{gaunaa2024}, eqs.~4, 10 and 11: with $\beta=\operatorname{atan2}(\vect V\cdot\vect e_s,\ |\vect V-(\vect V\cdot\vect e_s)\vect e_s|)$ the angle between the full relative velocity at the control point and the span-normal plane, and the Reynolds number and dynamic pressure referred to the span-normal velocity, the correction adds a drag increment along the inner flow direction and a spanwise friction force along $\vect e_s$. On the V3 it is below $0.002$ in $C_D$ even at $20^\circ$ sideslip; it is off by default. + +\subsection{Stall: artificial viscosity} +\label{sec:stall} +Past the stall onset the lift slope of a polar is negative and the fixed-point iteration of Section~\ref{sec:solver} loses stability. With \code{is\_with\_artificial\_viscosity} the spanwise artificial viscosity of Li, Gaunaa, Pirrung and Lønbæk~\cite{li2026} is applied implicitly to the circulation update once any panel is past its positive or negative stall onset, with the coefficient of their eq.~16 written for a non-uniform grid and the factor $k=0.035$; it is a no-op in attached flow. It is off by default; for post-stall solutions the \code{casadi\_newton} loop is the robust alternative. + +\subsection{The lifting-line variant} +\label{sec:llt} +With \code{aerodynamic\_model\_type="LLT"} the inner velocity is evaluated at the aerodynamic centre and the own bound segment is excluded; no two-dimensional subtraction and no quarter-chord correction are needed. This is Prandtl's lifting line with nonlinear polars, exact for straight wings of high aspect ratio and provided for comparison; the VSM is the model for curved, swept and low-aspect-ratio wings. + +\section{Solver, inflow and outputs} +\label{sec:solver} +\paragraph{Iteration} (\code{Solver.solve}). With the influence matrices assembled, the circulation is the fixed point of~\eqref{eq:gamma}. The base loop is an under-relaxed fixed-point iteration, $\boldsymbol\Gamma\leftarrow(1-\omega)\boldsymbol\Gamma+\omega\,\boldsymbol\Gamma_{\mathrm{new}}(\boldsymbol\Gamma)$, stopped when $\max|\Delta\Gamma|/\max|\Gamma|<$ \code{allowed\_error} ($10^{-6}$). The relaxation factor defaults to $0.8$ times the stability limit of~\cite{li2026}, eq.~11, evaluated on the actual panels and polars, +\begin{equation} +\omega_{\max}=\frac{2}{1+\tfrac14\max_i (c_i/\Delta s_i)\,C_{l,i}'}, +\end{equation} +exact for the LLT and halved for the VSM, whose three-quarter-chord control point sees about twice the trailed induction (measured 0.51--0.66, Figure~\ref{fig:relax}). Alternatives: Anderson acceleration (10--25 times fewer iterations in attached flow), a Newton loop with an exact CasADi Jacobian and pseudo-transient continuation (3--10 iterations, converges post-stall), and Broyden solvers. The initial circulation is elliptic, cosine, zero or a previous solution. + +\begin{figure}[t]\centering +\includegraphics[width=0.9\textwidth]{05_relaxation_factor.png} +\caption{Relaxation factor (\code{make\_pr\_figures.py}). Left: iterations on the V3 with the old fixed $0.01$ and with the default derived from the stability limit, for VSM and LLT. Right: the measured stability limit over the bound of~\cite{li2026} on rectangular wings: exact for the LLT, about half for the VSM.} +\label{fig:relax} +\end{figure} + +\paragraph{Inflow} (\code{BodyAerodynamics.va}, \code{va\_initialize}). The apparent velocity is a single vector (speed, angle of attack, sideslip), or one vector per panel, optionally with rigid-body rates about given axes through a reference point: $\vect V_{a,i}=\vect V_\infty-\boldsymbol\omega\times(\vect C_i-\vect r_0)$. The sign conventions are in Section~\ref{sec:conventions}. Each inflow rebuilds the wake (and, with streamwise legs, the legs). + +\paragraph{Forces, moments and coefficients} (\code{BodyAerodynamics.compute\_results}). Panel forces are summed in the global frame; lift, drag and side force are the components along the reference wind axes, built from a reference velocity $\vect V_{\mathrm{ref}}$ (the panel inflow when it is uniform, otherwise an area-weighted root-mean-square of the distribution without the rotational term) and the span direction: drag along $\vect V_{\mathrm{ref}}$, lift along $\vect V_{\mathrm{ref}}\times\vect e_{\mathrm{span}}$, side force completing the triad. Coefficients use $q_{\mathrm{ref}}=\tfrac12\rho|\vect V_{\mathrm{ref}}|^2$ and the projected area $S$ of the wing onto the $z$-plane; moment coefficients are further divided by the largest chord. The results dictionary also carries the spanwise distributions of $\Gamma$, $\alpha$, the sectional coefficients, the panel forces and moments, the centre of pressure, the Reynolds number on the largest chord, and the Trefftz-plane induced drag. + +\paragraph{Trefftz-plane induced drag} (\code{compute\_trefftz\_plane\_induced\_drag}, reported as \code{drag\_induced\_trefftz}). For a wing in one uniform inflow the far-wake induced drag of the converged circulation is computed in the plane perpendicular to the freestream from the trace of the frozen wake: each section boundary sheds a line vortex of strength $\Delta\Gamma_j$, and $D_i=\tfrac12\rho\sum_j\Gamma_j w_{n,j}\Delta s_j$ with $w_n$ the normal induced velocity on the trace. It depends only on the circulation and the wake, not on where the forces are evaluated on the blade, and is the referee of Section~\ref{sec:val-trefftz}. It is \code{None} with body rates or a distributed inflow. + +\paragraph{Bridle lines} (\code{compute\_line\_aerodynamic\_force}). Each line is a cylinder of length $\ell$ and diameter $d$ at angle $\theta$ to the local flow, with the cross-flow drag coefficient $C_{d,c}=1.1$ and the friction coefficient $C_f=0.01$: $C_{d,t}=C_{d,c}\sin^3\theta+\pi C_f\cos^3\theta$ along the flow and $C_{l,t}=C_{d,c}\sin^2\theta\cos\theta-\pi C_f\sin\theta\cos^2\theta$ normal to it, on the area $\ell d$. On the V3 the bridles add a constant $0.036$--$0.040$ to $C_D$; they matter for flight, not for the wind-tunnel model, which had none. + +\section{Validation} +\label{sec:validation} +The validation is layered: analytical results for what theory fixes exactly, an internal consistency check for the induced drag, an independent potential-flow referee (a vortex-lattice method) for lift and induced drag on wings where no analytical result exists, and RANS and wind-tunnel data for the real kite. Inviscid polars ($C_l=2\pi\alpha$, $C_d=0$) are used wherever the comparison is with potential-flow theory, so that the lifting line and the referee solve the same problem. + +\subsection{Analytical cases} +\label{sec:val-analytical} +\paragraph{Horseshoe vortices.} The induced velocity of three horseshoe vortices of prescribed strengths at a prescribed point is compared with the hand-computed Biot--Savart result (the \code{analytical\_horshoe\_vortices} case in \code{tests/verification\_cases}); it checks eqs.~\eqref{eq:segment} and~\eqref{eq:semiinf} and the sign conventions of the ring. + +\paragraph{Elliptic wings.} For an elliptic planform Prandtl's lifting line gives $C_L=2\pi\alpha/(1+2/\mathrm{AR})$ and $C_{D_i}=C_L^2/(\pi\mathrm{AR})$. Figure~\ref{fig:classical} compares the LLT and the VSM with these on wings of aspect ratio 20 and 4 with a straight quarter-chord line (cosine spacing, 60 panels), and with the vortex-lattice method of Section~\ref{sec:val-legs}; the numbers are in Table~\ref{tab:classical}. The LLT reproduces the theory it discretises: lift slope within $0.4$ and $1.4\,\%$, Oswald factor $0.997$ at both aspect ratios. The VSM, which does not assume a high aspect ratio, matches the theory at aspect ratio 20 (slope $-1.5\,\%$, Oswald $0.991$) and at aspect ratio 4 follows the lifting-surface solution instead of the line theory: its lift is within $1\,\%$ of the VLM at $4$, $8$ and $12^\circ$, where Prandtl's result is $11$--$12\,\%$ too high, with the Oswald factor $0.998$. The same elliptic planform drawn with a straight leading edge has a curved quarter-chord line; the LLT variant has no regularisation for that (Section~\ref{sec:curved}) and its on-blade induced drag then exceeds its own Trefftz value by $11\,\%$ at aspect ratio 20 and $36\,\%$ at aspect ratio 4, while the VSM stays within $2\,\%$. The regression test \code{tests/verification\_cases/elliptical\_wing} holds the LLT within $0.01$ in $C_L$ and $0.001$ in $C_{D_i}$ of the theory. + +\begin{figure}[t]\centering +\includegraphics[width=\textwidth]{12_classical_wings.png} +\caption{Classical wings (\code{docs/validation/aerodynamic\_model\_figures.py}). Left and middle: elliptic wings of aspect ratio 20 and 4, LLT and VSM against Prandtl's lifting line and the AeroSandbox vortex-lattice method (stars). Right: the rectangular AR 8 wing, VSM against the VLM at $5^\circ$ and the elliptic reference slope.} +\label{fig:classical} +\end{figure} + +\begin{table}[t]\centering\small +\caption{Classical wings: lift slope relative to Prandtl's $2\pi/(1+2/\mathrm{AR})$ and Oswald factor (inviscid polars, 60 panels, cosine spacing, straight quarter-chord line); rectangular AR 8 wing relative to the vortex-lattice method (80 panels, $5^\circ$, Table~\ref{tab:referee}).} +\label{tab:classical} +\begin{tabular}{lcc} +\toprule +case & lift slope / theory & Oswald factor\\ +\midrule +elliptic AR 20, LLT & 0.996 & 0.997\\ +elliptic AR 20, VSM & 0.985 & 0.991\\ +elliptic AR 4, LLT & 0.987 & 0.997\\ +elliptic AR 4, VSM & 0.886 & 0.998\\ +\midrule + & $C_L$ / VLM & $C_{D_i}$ / VLM\\ +rectangular AR 8, VSM & 0.998 & 0.991\\ +\bottomrule +\end{tabular} +\end{table} + +\subsection{Consistency of the induced drag: the Trefftz plane} +\label{sec:val-trefftz} +For a given circulation and frozen wake the Trefftz-plane drag is the far-field value; a force model that evaluates the forces on the blade with the correct local flow must reproduce it, independently of the panel count. Figure~\ref{fig:trefftz} shows the on-blade over Trefftz ratio on inviscid wings. With quarter-chord directions, the attached-trailed force and the curved bound-vortex regularisation it is $1.000$ on straight wings, within $2\,\%$ on the elliptic swept--anhedral wing and the $30^\circ$ swept wing, and within $2\,\%$ on the V3 at $5$, $10$ and $15^\circ$, at every resolution. Three-quarter-chord directions overshoot by 20--40\,\%. Before the regularisation the V3 ratio fell with the panel count (0.93, 0.90, 0.86 at 20, 50, 150 panels). The check is a consistency test of the force evaluation, not a validation of the circulation; that is what the next subsection does. + +\begin{figure}[t]\centering +\includegraphics[width=\textwidth]{03_trefftz_check.png} +\caption{On-blade over Trefftz-plane induced drag on inviscid wings against panel count (\code{make\_pr\_figures.py}); the ratio must be one for a consistent force model.} +\label{fig:trefftz} +\end{figure} + +\subsection{Vortex-lattice referee: lift and induced drag on planar, swept, curved and kite-like wings} +\label{sec:val-legs} +The Trefftz plane cannot judge the lift. For that the vortex-lattice method of AeroSandbox~\cite{aerosandbox} (horseshoe vortices, 16 chordwise by 80--160 spanwise panels, flat-plate sections, trailing legs along the body axis) was run on the same wings as the VSM with the three leg constructions of Section~\ref{sec:vortex}: a straight AR~8 wing, a yawed wing (uniform $30^\circ$ sweep, no kink) and a kinked $30^\circ$ swept wing at $5^\circ$, a circular-arc wing ($110^\circ$, radius four chords) at $8^\circ$, the swept--anhedral elliptic wing of~\cite{gaunaa2026} at $10^\circ$, and the inviscid V3 at $10^\circ$ (\code{docs/validation/vlm\_referee.py}). For the induced drag the Trefftz drag of the VLM's own circulation is used, because its near-field drag with trailing legs along the body axis is low on non-planar wings. For the V3 the referee is approximate: it takes its sections perpendicular to the local span, which unskews the tip ribs. Figure~\ref{fig:referee} and Table~\ref{tab:referee} give the result. + +\begin{figure}[p]\centering +\includegraphics[width=\textwidth]{11_leg_direction_referee.png} +\caption{Vortex-lattice referee (\code{leg\_direction\_study.py --figures}). Top: strip circulation of the VLM and of the VSM with the three leg constructions. Bottom: lift and on-blade induced drag relative to the VLM, and the V3 viscous drag against RANS for the three constructions.} +\label{fig:referee} +\end{figure} + +\begin{table}[t]\centering\small +\caption{VSM relative to the vortex-lattice referee: lift, and on-blade induced drag over the VLM's Trefftz drag (VSM at 80 panels, elliptic 160).} +\label{tab:referee} +\begin{tabular}{lcc|cc|cc|cc} +\toprule + & \multicolumn{2}{c|}{VLM} & \multicolumn{2}{c|}{rib legs} & \multicolumn{2}{c|}{streamwise} & \multicolumn{2}{c}{perpendicular}\\ +case & $C_L$ & $C_{D_i}$ & $C_L$ & $C_{D_i}$ & $C_L$ & $C_{D_i}$ & $C_L$ & $C_{D_i}$\\ +\midrule +straight AR 8, $5^\circ$ & 0.402 & 0.00654 & $-0.2\,\%$ & $-0.9\,\%$ & $-0.2\,\%$ & $-0.9\,\%$ & $-0.2\,\%$ & $-0.9\,\%$\\ +yawed $30^\circ$, $5^\circ$ & 0.360 & 0.00537 & $0.0\,\%$ & $-0.6\,\%$ & $0.0\,\%$ & $-0.6\,\%$ & $+1.4\,\%$ & $+2.0\,\%$\\ +kinked swept $30^\circ$, $5^\circ$ & 0.363 & 0.00568 & $-0.4\,\%$ & $-3.0\,\%$ & $-0.4\,\%$ & $-3.0\,\%$ & $-8.8\,\%$ & $+6.7\,\%$\\ +arc $110^\circ$, $8^\circ$ & 0.577 & 0.01481 & $+0.3\,\%$ & $-3.4\,\%$ & $-2.1\,\%$ & $-1.2\,\%$ & $+0.3\,\%$ & $-3.4\,\%$\\ +elliptic, swept--anhedral, $10^\circ$ & 0.807 & 0.02643 & $+0.9\,\%$ & $-0.1\,\%$ & $+0.1\,\%$ & $-4.4\,\%$ & $+0.8\,\%$ & $-0.3\,\%$\\ +V3, flat plate, $10^\circ$ (approx.) & 0.624 & 0.02961 & $+2.3\,\%$ & $-4.0\,\%$ & $-1.9\,\%$ & $-8.6\,\%$ & $-7.9\,\%$ & $-4.5\,\%$\\ +\bottomrule +\end{tabular} +\end{table} + +What the referee says: with rib legs the VSM reproduces the lifting-surface lift within $1\,\%$ on every wing except the V3 (2.3\,\%, where the referee itself is approximate) and the induced drag within 4\,\%; streamwise legs are equivalent on the planar wings and on the elliptic wing, 2\,\% low on the arc (the legs climb along the vertical tips) and closest to RANS on the V3; the paper's perpendicular legs lose 9\,\% of the lift on the kinked wing and 8\,\% on the V3, where they also cross at the tips above 80 panels, and are rejected. On the arc wing the rib legs stay within $+0.3$, $+1.6\,\%$ of the VLM lift at $8$ and $15^\circ$ while the streamwise legs drift to $-2.1$, $-3.0\,\%$. The lift distributions of the VLM and the VSM coincide along the span (Figure~\ref{fig:referee}, top), so the small differences in the totals are not local defects. + +\subsection{The TU Delft V3 kite against RANS and the wind tunnel} +\label{sec:val-v3} +The V3 is a 25\,m$^2$ leading-edge inflatable kite with a strongly curved, swept planform. The VSM uses its CAD geometry with two-dimensional RANS polars of the rib sections at Re\,$5\times10^5$ (optionally NeuralFoil polars with transition), 50 panels, and is compared with the three-dimensional RANS of Poland et al.~\cite{poland2026} (Re\,$10^6$, no bridles) and with the wind-tunnel measurements of the same authors on a 1:6.5 rigid model without bridles (Re\,$5\times10^5$, support tare subtracted, open-jet correction applied). Table~\ref{tab:v3} and Figures~\ref{fig:v3polars} and~\ref{fig:v3before} give the comparison. + +\begin{table}[t]\centering\small +\caption{V3 kite, zero sideslip, 50 panels, rib legs, 2D RANS polars: VSM against RANS (\code{leg\_direction\_study.py}) and the wind tunnel (\code{curved\_bound\_vortex\_validation.py}; the measured angles differ slightly from the nominal ones).} +\label{tab:v3} +\begin{tabular}{c|cc|cc|c} +\toprule + & \multicolumn{2}{c|}{VSM} & \multicolumn{2}{c|}{RANS} & wind tunnel\\ +$\alpha$ & $C_L$ & $C_D$ & $C_L$ & $C_D$ & $C_D$\\ +\midrule +$1^\circ$ & 0.207 & 0.0494 & 0.228 & 0.0491 & --\\ +$4^\circ$ & 0.461 & 0.0518 & 0.461 & 0.0517 & --\\ +$5^\circ$ & -- & 0.055 & -- & 0.052 ($4^\circ$) & 0.076 ($5.4^\circ$)\\ +$7^\circ$ & 0.697 & 0.0656 & 0.675 & 0.0644 & 0.090 ($7.3^\circ$)\\ +$9^\circ$ & -- & 0.079 & -- & 0.084 ($10^\circ$) & 0.107 ($9.4^\circ$)\\ +$10^\circ$ & 0.917 & 0.0866 & 0.869 & 0.0842 & --\\ +$13^\circ$ & 1.050 & 0.1206 & 1.043 & 0.1100 & 0.217 ($13.4^\circ$)\\ +\bottomrule +\end{tabular} +\end{table} + +Against RANS the drag is within 3\,\% up to $10^\circ$ and 10\,\% high at $13^\circ$, near the RANS stall; the lift is within $0.02$ on average, with a lift slope of $0.078$ per degree against $0.067$ in RANS and the wind tunnel. The wind tunnel measures more drag than any simulation, RANS included: the gap is lift-dependent, $\Delta C_D\approx0.003+0.037\,C_L^2$, while the zero-lift drag agrees (wind tunnel 0.041, VSM 0.038, RANS 0.043). Poland et al.\ attribute it to the 3--4\,mm model canopy, the flow behind the leading-edge tube and transition. It is not a lifting-line issue; the three-quarter-chord force directions of the old code appeared to close part of it only because they overestimate the induced drag by 20--40\,\% (Figure~\ref{fig:v3before}), a compensating error. The physically defensible levers for the wind tunnel are two-dimensional polars with transition at the test Reynolds number ($+0.009$ in $C_D$ at $9.4^\circ$) and, for flight, the bridles. + +\begin{figure}[p]\centering +\includegraphics[width=\textwidth]{09_v3_polars_curved_bound_vortex_fix.png} +\caption{V3 polars (\code{curved\_bound\_vortex\_validation.py --figures}): the solver before the consistent coupling (grey dashed), quarter-chord directions with the singular bound-on-line term (blue) and with the regularisation (red), against RANS and the wind tunnel.} +\label{fig:v3polars} +\end{figure} + +\begin{figure}[p]\centering +\includegraphics[width=\textwidth]{01_v3_polars_before_after.png} +\caption{V3 polars before and after the consistent coupling (\code{make\_pr\_figures.py}): the old solver (dashed black), the consistent implementation with three-quarter-chord directions (blue) and with quarter-chord directions (red, the default), dotted without the attached-trailed force, against RANS and wind-tunnel data, including sideslip.} +\label{fig:v3before} +\end{figure} + +\subsection{Mesh convergence} +\label{sec:val-mesh} +Figure~\ref{fig:mesh} shows lift, induced drag and the on-blade over Trefftz ratio of the inviscid V3 and of the straight wing against the number of panels, 10 to 160, uniform spacing. On the V3 the lift is within $0.4\,\%$ of its 160-panel value from 10 panels on and the induced drag within $3\,\%$ (within $0.5\,\%$ from 40 panels), with the on-blade over Trefftz ratio between 1.007 and 1.021. On the straight wing the lift converges from above ($5\,\%$ high at 10 panels, $1\,\%$ at 40), because the tip loading needs panels to be resolved, the induced drag is within $1.3\,\%$ throughout, and the ratio is 1.000 at every count. Fifty panels, the usual choice for the V3, put every integral quantity within $1\,\%$ of the converged value. + +\begin{figure}[t]\centering +\includegraphics[width=\textwidth]{13_mesh_convergence.png} +\caption{Mesh convergence (\code{aerodynamic\_model\_figures.py}): lift and induced drag relative to the 160-panel value, and the on-blade over Trefftz ratio, for the inviscid V3 at $10^\circ$ and the straight AR 8 wing at $5^\circ$.} +\label{fig:mesh} +\end{figure} + +\subsection{Regression cases in the test suite} +\label{sec:val-tests} +The test suite (\code{pytest tests}, 182 tests) holds, besides unit tests of every module: the analytical horseshoe case; the elliptic wing against Prandtl; a curved wing of aspect ratio 3.2 and a $30^\circ$ swept wing against the thesis implementation and, for the swept wing, XFLR5 and CFD polars; the V3 against the thesis implementation and the RANS polars; the Trefftz consistency of the regularisation on the arc wing at 20, 40 and 80 panels; the leg constructions (equality where they must coincide, the skewed-rib invariance, the kinked wing, the sideslip rebuild, the pure-rotation fallback); the relaxation-factor bound; body rates and distributed inflow; and the wake direction. The cases that verify the thesis vortex system pass \code{leg\_direction="rib"} explicitly. + +\subsection{Limits of the model} +\label{sec:limits} +The model is a steady, attached-flow, thin-surface method. It does not capture: stall and post-stall flow beyond what the sectional polars and the artificial viscosity provide; wake roll-up and the interaction of the wake with downstream surfaces; unsteady effects (shed vorticity, added mass); thickness effects on the induced flow; and the viscous drag sources that the V3 wind tunnel sees beyond RANS. On curved wings the on-blade induced drag carries a few per cent of uncertainty with respect to a lifting-surface solution, and the lift slope of the V3 is 15\,\% steeper than measured. The direction of the attached-trailed legs is a modelling choice; the rib construction is the default and the streamwise one the alternative for uniform inflow (Section~\ref{sec:val-legs}). + +\section{Conventions} +\label{sec:conventions} +\begin{table}[h]\centering\small +\begin{tabular}{>{\raggedright\arraybackslash}p{0.28\textwidth}>{\raggedright\arraybackslash}p{0.66\textwidth}} +\toprule +item & convention\\ +\midrule +global frame & right-handed; kite data and examples: $x$ downstream, $y$ to the left tip, $z$ up\\ +panel frame & $\vect e_s$ (\code{z\_airf}) along the bound vortex towards the left tip; $\vect e_c$ (\code{y\_airf}) leading to trailing edge, perpendicular to the span; $\vect e_n$ (\code{x\_airf}) $=\vect e_c\times\vect e_s$, suction side\\ +chord & perpendicular to the local quarter-chord line (CP1), averaged over the panel's two sections\\ +bound vortex, control point & at $1/4$ and $3/4$ of the chord by default (\code{aerodynamic\_center\_location}, \code{control\_point\_location})\\ +circulation & positive for positive lift in the panel frame; ring sense bound $\vect b_2\to\vect b_1$, leg $\vect b_1\to\vect T_1$, leg $\vect T_2\to\vect b_2$\\ +angle of attack, sideslip & degrees in \code{va\_initialize}; $\vect V_a=U(\cos\alpha\cos\beta,\ \sin\beta,\ \sin\alpha\cos\beta)$ in the global frame\\ +body rates & about given axes through \code{reference\_point}; $\vect V_{a,i}=\vect V_\infty-\boldsymbol\omega\times(\vect C_i-\vect r_0)$\\ +polars & $[\alpha\ \text{(rad)},\ C_l,\ C_d,\ C_m]$, $C_m$ about the quarter chord\\ +lift, drag, side & along $\vect V_{\mathrm{ref}}\times\vect e_{\mathrm{span}}$, $\vect V_{\mathrm{ref}}$ and their cross product; coefficients on $q_{\mathrm{ref}}$ and the projected area\\ +moments & about \code{reference\_point}, coefficients on projected area times largest chord\\ +units & SI throughout; $\rho=1.225$\,kg/m$^3$, $\mu=1.81\times10^{-5}$\,Pa\,s by default\\ +\bottomrule +\end{tabular} +\end{table} + +\begin{thebibliography}{99}\small +\bibitem{gaunaa2026} M.~Gaunaa, A.~Li, G.~R.~Pirrung, From thin airfoil theory to finite wings: consistent implementation of Lifting Line-based models, J.\ Phys.: Conf.\ Ser.\ 3224 (2026) 042030. +\bibitem{li2026} A.~Li, M.~Gaunaa, G.~R.~Pirrung, K.~L{\o}nb{\ae}k, Relaxation limits and artificial viscosity for lifting line methods, TORQUE 2026. +\bibitem{li2020} A.~Li, M.~Gaunaa, G.~R.~Pirrung, N.~Ramos-Garc\'ia, S.~G.~Horcas, The influence of the bound vortex on the aerodynamics of curved wind turbine blades, J.\ Phys.: Conf.\ Ser.\ 1618 (2020) 052038. +\bibitem{gaunaa2024} M.~Gaunaa, N.~N.~S{\o}rensen, A.~Li, Spanwise-flow correction of the viscous forces in lifting-line methods, J.\ Phys.: Conf.\ Ser.\ 2767 (2024) 022068. +\bibitem{reid2020} J.~T.~Reid, D.~F.~Hunsaker, A general approach to lifting-line theory, applied to wings with sweep, J.\ Aircraft 58 (2021), doi:10.2514/1.C035994. +\bibitem{damiani2019} R.~Damiani, F.~F.~Wendt, J.~M.~Jonkman, J.~Sicard, A vortex step method for nonlinear airfoil polar data as implemented in KiteAeroDyn, AIAA SciTech 2019, paper 2019-0804. +\bibitem{poland2026} J.~A.~W.~Poland, J.~M.~van Spronsen, M.~Gaunaa, R.~Schmehl, Wind tunnel load measurements of a leading-edge inflatable kite rigid-scale model, Wind Energ.\ Sci.\ 11 (2026) 911. +\bibitem{aerosandbox} P.~D.~Sharpe, AeroSandbox: a differentiable framework for aircraft design optimization, MIT (2021), \url{https://github.com/peterdsharpe/AeroSandbox}; version 4.2.8, \code{VortexLatticeMethod}. +\bibitem{report} Vortex-Step-Method repository, The curved bound vortex at the quarter chord of a lifting line: problem, regularisation, literature and the direction of the attached-trailed legs, \code{docs/validation/curved\_bound\_vortex\_report.pdf} (2026). +\bibitem{cayon2022} O.~Cayon, Fast aeroelastic model of a leading-edge inflatable kite, MSc thesis, TU Delft (2022); its chapter~4 was the previous content of this document. +\end{thebibliography} + +\end{document} diff --git a/docs/figures/03_trefftz_check.png b/docs/figures/03_trefftz_check.png index 7003a19..8962185 100644 Binary files a/docs/figures/03_trefftz_check.png and b/docs/figures/03_trefftz_check.png differ diff --git a/docs/figures/08_curved_bound_vortex_regularisation.png b/docs/figures/08_curved_bound_vortex_regularisation.png new file mode 100644 index 0000000..8a586f7 Binary files /dev/null and b/docs/figures/08_curved_bound_vortex_regularisation.png differ diff --git a/docs/figures/09_v3_polars_curved_bound_vortex_fix.png b/docs/figures/09_v3_polars_curved_bound_vortex_fix.png new file mode 100644 index 0000000..863406f Binary files /dev/null and b/docs/figures/09_v3_polars_curved_bound_vortex_fix.png differ diff --git a/docs/figures/10_leg_direction_geometry.png b/docs/figures/10_leg_direction_geometry.png new file mode 100644 index 0000000..87a224a Binary files /dev/null and b/docs/figures/10_leg_direction_geometry.png differ diff --git a/docs/figures/11_leg_direction_referee.png b/docs/figures/11_leg_direction_referee.png new file mode 100644 index 0000000..d159913 Binary files /dev/null and b/docs/figures/11_leg_direction_referee.png differ diff --git a/docs/figures/12_classical_wings.png b/docs/figures/12_classical_wings.png new file mode 100644 index 0000000..c5185f6 Binary files /dev/null and b/docs/figures/12_classical_wings.png differ diff --git a/docs/figures/13_mesh_convergence.png b/docs/figures/13_mesh_convergence.png new file mode 100644 index 0000000..34cef2e Binary files /dev/null and b/docs/figures/13_mesh_convergence.png differ diff --git a/docs/figures/14_unrolling_per_panel.png b/docs/figures/14_unrolling_per_panel.png new file mode 100644 index 0000000..894b74d Binary files /dev/null and b/docs/figures/14_unrolling_per_panel.png differ diff --git a/docs/figures/15_chord_spread.png b/docs/figures/15_chord_spread.png new file mode 100644 index 0000000..9757341 Binary files /dev/null and b/docs/figures/15_chord_spread.png differ diff --git a/docs/figures/16_kink_line_vs_sheet.png b/docs/figures/16_kink_line_vs_sheet.png new file mode 100644 index 0000000..e58cd05 Binary files /dev/null and b/docs/figures/16_kink_line_vs_sheet.png differ diff --git a/docs/figures/README.md b/docs/figures/README.md index 91a5d45..ac8860d 100644 --- a/docs/figures/README.md +++ b/docs/figures/README.md @@ -1,7 +1,8 @@ # Figures: consistent lifting-line coupling Generated by `make_pr_figures.py` (run from the repository root, a few -minutes). They document the changes that align the solver with Gaunaa, Li & +minutes; figures 01-07 pass `leg_direction="rib"` explicitly, see 10 and 11 +for the leg direction). They document the changes that align the solver with Gaunaa, Li & Pirrung (TORQUE 2026, "From thin airfoil theory to finite wings"), Li, Gaunaa, Pirrung & Lønbæk (TORQUE 2026, "Relaxation limits and artificial viscosity") and Gaunaa, Sørensen & Li (2024, spanwise-flow viscous correction). @@ -31,10 +32,12 @@ correction, old (a geometry-only number that never saw the flow) versus new ## 03 Trefftz-plane check Inviscid wings, on-blade induced drag divided by the Trefftz-plane induced drag on the same circulation (`results["drag_induced_trefftz"]`). Quarter- -chord directions with the attached-trailed force reproduce the far-wake drag -exactly on straight wings, within 2% on the elliptic wing, 6% low on the -30 deg swept wing and 7-14% low on the V3. The 3/4-chord directions overshoot -by 20-40%. Without the attached-trailed force the V3 is 11-28% low. +chord directions with the attached-trailed force and the curved bound-vortex +regularisation (figure 08) reproduce the far-wake drag exactly on straight +wings, within 2% on the elliptic wing and the 30 deg swept wing, and within +2% on the V3 at 5, 10 and 15 deg (before the regularisation the V3 was 7-14% +low and drifting with the panel count). The 3/4-chord directions overshoot +by 20-40%. Without the attached-trailed force the V3 is 5-13% low. ## 04 Attached-trailed vortex force on the V3 Kutta-Joukowski force on the chordwise legs between the bound vortex and the @@ -51,6 +54,34 @@ the fixed-point Jacobian) over the paper's bound on rectangular wings: the bound is exact for LLT and about half for VSM, whose 3/4-chord control point sees twice the trailing-vortex induction; the solver halves it for VSM. +## 08 Curved bound vortex at the quarter chord: problem and regularisation +Figures 08 and 09 are generated by `docs/validation/curved_bound_vortex_validation.py --figures`, +which also reproduces every table of `docs/validation/curved_bound_vortex_report.pdf` +(the problem statement, the literature comparison and the verification). +(a) On a spanwise-curved bound line the neighbouring segments induce a +chordwise velocity at the panel's own quarter-chord point that grows as +ln(span / panel width). (b) Arc wing, mid-span: that velocity from the +polyline (grows by 0.05 m/s per doubling of the panel count) and from the +chordwise-spread thin-airfoil sheet (finite, -0.186 m/s); the grey band is +the value at which the on-blade induced drag equals the Trefftz plane. +(c) Why it matters: the on-blade induced drag is rho Gamma (w cos a + u sin a), +and w is only about 1 deg of induced angle, so u = 3% of U moves the drag by +12%. (d) The thin-airfoil distribution used to spread the bound vorticity; +effective cut-off 0.15 c. (e) The construction: classical downwash of the wing +unrolled onto the local tangent line, plus the chord-regularised difference +between the actual and the unrolled systems. (f) On-blade over Trefftz drag +against panel count on the arc and the inviscid V3, before (dashed) and +after (solid). + +## 09 V3 polars with the curved bound-vortex regularisation +TUDELFT V3 kite, CAD geometry, 2D RANS Re 5e5 polars, 50 panels, 2.82 m/s. +Grey dashed: the solver before this work (3/4-chord directions). Blue: +quarter-chord directions with the other panels' bound segments evaluated on +the line (log-singular). Red: with the regularisation. Lift is unchanged; +drag rises by a fraction of the induced drag, most at high lift and in +sideslip, onto the RANS values up to 10 deg. The wind-tunnel gap that remains +is shared by RANS. + ## 06 Wake direction under a yaw rate Top view of the wake origins with the old single wake direction (mean freestream before the rotational term) and the new per-ring direction (each @@ -58,11 +89,88 @@ panel's own apparent velocity). Identical in uniform inflow; under body rates the wake follows the local inflow. A yaw rate mostly changes the local speed, so the direction differences are small even at the exaggerated 1 rad/s. -## 07 Perpendicular chordwise legs, tried and rejected -The paper aligns the attached legs with the local chord, which its CP1 defines -perpendicular to the quarter-chord line; the solver keeps them along the input -ribs. On the V3 tips a perpendicular leg only meets the trailing edge far -inboard, and on a swept wing with streamwise ribs the legs cross neighbouring -panels and their control points (NaN). Against RANS the V3 mean errors are -0.020 in CL and 1.5% in CD with rib legs, 0.031 / 4.7% with fixed-length -perpendicular legs and 0.057 / 9.4% with legs to the trailing edge. +## 07 Perpendicular chordwise legs, first attempt (superseded by 10 and 11) +Made in September with the singular bound-on-line term and perpendicular +legs of fixed length. Kept for the record; the question is settled by +figures 10 and 11 and Section 4.3 of +`docs/validation/curved_bound_vortex_report.pdf`. + +## 10 Direction of the attached-trailed legs: geometry +Generated by `docs/validation/leg_direction_study.py --figures`. Top views +(flow downwards) and one side view of the three leg constructions of +`BodyAerodynamics(leg_direction=...)`: along the input ribs (blue, the +default), along the mean flow projected onto the surface (green), or +perpendicular to the quarter-chord line as in the TORQUE 2026 paper (red), +with the bound vortex (black), the aerodynamic centres (dots), the control +points (crosses in the colour of their legs) and, in grey, the lattice of the +vortex-lattice referee (AeroSandbox, coarse). (a) A straight wing whose ribs +are drawn 22 deg skewed: rib legs follow the drawing, the other two end on +the trailing-edge line and give the straight-wing result. (b) The root of a +kinked 30 deg swept wing: streamwise ribs against the perpendicular fan, +whose legs would cross at the kink without the tangent averaging. (c) The +swept-anhedral elliptic wing. (d) The arc wing from the side: streamwise +legs climb along the nearly vertical tip at the angle of attack. (e, f) The +V3: the ribs are nearly streamwise over the middle and skewed at the tips, +where the perpendicular legs become long and end far inboard. + +## 11 Direction of the attached-trailed legs: referee +Generated by the same script from `vlm_referee.json` +(`docs/validation/vlm_referee.py`, AeroSandbox VLM with flat-plate sections, +chordwise 16, spanwise 80 to 160) and `leg_direction_study.json`. Top: strip +circulation of the VLM and of the VSM with the three leg options on the +kinked swept wing, the elliptic wing and the inviscid V3. Bottom left and +middle: lift and on-blade induced drag of the VSM relative to the VLM (its +Trefftz-plane drag, since its near-field drag with trailing legs along the +body axis is low on the non-planar wings). Rib and streamwise legs are +within 1% in lift except the streamwise arc (2% low); perpendicular legs +lose 9% on the kinked wing and 8% on the V3. Bottom right: V3 viscous drag +against RANS for the three options. + +## 12 Classical wings +Generated by `docs/validation/aerodynamic_model_figures.py` for the +aerodynamic model document (`docs/Aerodynamic_model.pdf`). Elliptic wings of +aspect ratio 20 and 4 with a straight quarter-chord line, LLT and VSM against +Prandtl's lifting line and the AeroSandbox VLM; the rectangular AR 8 wing +against the VLM. The LLT reproduces Prandtl (slope within 1.4%, Oswald +0.997); the VSM matches the VLM within 1% at AR 4, where Prandtl is 11-12% +high. The JSON next to the script also records that the LLT loses its +Trefftz consistency (1.11 and 1.36) when the same planform is drawn with a +straight leading edge, i.e. a curved quarter-chord line, while the VSM stays +within 2%. + +## 13 Mesh convergence +Same script. Lift, induced drag and on-blade over Trefftz ratio of the +inviscid V3 at 10 deg and the straight AR 8 wing at 5 deg against the number +of panels, 10 to 160, uniform spacing. + +## 14 Unrolling the wing onto the evaluation panel's tangent +Generated by `docs/validation/unrolling_figure.py`. What the regularisation's +"unrolled" reference wing is: for each evaluation panel a straight wing on +that panel's own tangent line, nodes at their arc-length distances, legs +along that panel's chord direction, one reference per panel. (a, b) The arc +wing with the references of a mid-span and a near-tip panel; (c) the kinked +swept wing from above with the reference of a panel on the right half; (d) +the distance between actual and unrolled nodes against the distance from the +evaluation panel: the near field coincides (the singular part cancels in the +difference), the far field is the curvature effect. + +## 15 Chordwise spreading of the bound vorticity +Generated by `docs/validation/chord_spread_figure.py`. (a) Three panels of +the arc wing from above with the bound vortex replaced by the 24 +thin-airfoil strips, weights as line shade, each strip with its own legs; +(b) the same strips on the real curved panels and on the unrolled straight +reference, nearly coincident in the neighbourhood; (c) the chordwise +velocity one neighbouring segment induces at the quarter-chord point +against its distance, filament against sheet: equal beyond a chord, the +filament grows and the sheet levels off at the 0.15 c cut-off within it; +(d) the running sum of the neighbours from far to near at 40 to 320 panels: +filaments keep rising with refinement (the logarithm), sheets settle. + +## 16 Two vortices meeting at a kink: line against sheet +Generated by `docs/validation/kink_figure.py`. The chordwise velocity that a +vortex of unit strength, starting where another ends and turned by 2, 5 or +10 deg, induces at a point of the first one at distance r from the kink: +as a single filament it goes like theta / r without limit, as the +chord-spread sheet it equals the filament beyond a third of a chord, is +bounded within, and vanishes at the kink. The reason the curved bound +vortex is spread over the chord (report Section 4.2). diff --git a/docs/figures/make_pr_figures.py b/docs/figures/make_pr_figures.py index 78945d1..78fff5a 100644 --- a/docs/figures/make_pr_figures.py +++ b/docs/figures/make_pr_figures.py @@ -45,7 +45,7 @@ def rect_wing(n, aspect_ratio, sweep_deg=0.0, dist="uniform"): for y in np.linspace(-aspect_ratio / 2, aspect_ratio / 2, n + 1): x = abs(y) * np.tan(np.radians(sweep_deg)) w.add_section(np.array([x, y, 0.0]), np.array([x + 1.0, y, 0.0]), polar) - return BodyAerodynamics([w]) + return BodyAerodynamics([w], leg_direction="rib") def elliptic_wing(n, aspect_ratio): @@ -55,11 +55,11 @@ def elliptic_wing(n, aspect_ratio): for y in np.linspace(-b / 2, b / 2, n + 1): c = np.sqrt(max(1e-6, 1 - (2 * y / b) ** 2)) w.add_section(np.array([-c / 4, y, 0.0]), np.array([3 * c / 4, y, 0.0]), polar) - return BodyAerodynamics([w]) + return BodyAerodynamics([w], leg_direction="rib") def v3(n=50, geom=V3_GEOM): - return BodyAerodynamics.instantiate(n_panels=n, file_path=geom, spanwise_panel_distribution="uniform") + return BodyAerodynamics.instantiate(n_panels=n, file_path=geom, spanwise_panel_distribution="uniform", leg_direction="rib") # -------------------------------------------------------------------------- diff --git a/docs/plotly_render.png b/docs/plotly_render.png new file mode 100644 index 0000000..404ef33 Binary files /dev/null and b/docs/plotly_render.png differ diff --git a/docs/validation/aerodynamic_model_figures.json b/docs/validation/aerodynamic_model_figures.json new file mode 100644 index 0000000..2f6a9fd --- /dev/null +++ b/docs/validation/aerodynamic_model_figures.json @@ -0,0 +1,206 @@ +{ + "figure_12": { + "elliptic AR 20 LLT": { + "lift_slope_over_theory": 0.9963843139117654, + "oswald": 0.9964771750172142, + "CL": [ + 0.0, + 0.19882924048475586, + 0.3976284521518459, + 0.5963677296182592, + 0.7950174135397768, + 0.9935482115627807, + 1.1919313168205519 + ], + "CDi": [ + 0.0, + 0.0006310947348141985, + 0.0025241036212726425, + 0.005678204554120482, + 0.010092040124094925, + 0.015763736550204063, + 0.022690929761209504 + ] + }, + "elliptic AR 20 VSM": { + "lift_slope_over_theory": 0.9849492828535306, + "oswald": 0.9913208731393474, + "CL": [ + 0.0, + 0.1963787703180651, + 0.3927586557998938, + 0.5891407901559308, + 0.7855263440179843, + 0.981916542945562, + 1.1783126848236414 + ], + "CDi": [ + 0.0, + 0.0006192313013003554, + 0.0024769090624717125, + 0.005572987482443277, + 0.009907398938861516, + 0.01548006690481695, + 0.022290923736423246 + ] + }, + "elliptic AR 20 VLM": { + "alphas": [ + 4.0, + 8.0, + 12.0 + ], + "CL": [ + 0.39376001028991775, + 0.7845833143122043, + 1.1695920197908525 + ], + "VSM_CL_over_VLM": [ + 0.997456942137708, + 1.001201949733798, + 1.007456159827722 + ], + "LLT_CL_over_VLM": [ + 1.009824364488105, + 1.013298905338969, + 1.0191000764811085 + ] + }, + "elliptic AR 20 curved quarter chord LLT on-blade over Trefftz": 1.111239027333095, + "elliptic AR 20 curved quarter chord VSM on-blade over Trefftz": 1.0200031482128242, + "elliptic AR 4 LLT": { + "lift_slope_over_theory": 0.9865093148049535, + "oswald": 0.997059231563966, + "CL": [ + 0.0, + 0.14581075576431654, + 0.2913208630219549, + 0.43624207545725313, + 0.5803087292239091, + 0.7232842719182481, + 0.8649638580434403 + ], + "CDi": [ + 0.0, + 0.001693415220085719, + 0.006760656797980737, + 0.015163858508505823, + 0.026843443312675616, + 0.04172254910603149, + 0.05971185569212423 + ] + }, + "elliptic AR 4 VSM": { + "lift_slope_over_theory": 0.8858558416832792, + "oswald": 0.9981313924910079, + "CL": [ + 0.0, + 0.12922212505313213, + 0.25849624387515335, + 0.38787685977792, + 0.5174233826137107, + 0.6472018580228505, + 0.7772862329425175 + ], + "CDi": [ + 0.0, + 0.0013380764921838346, + 0.005351875615546081, + 0.012040547062401949, + 0.021404118063319183, + 0.03344559771838916, + 0.048173835450310926 + ] + }, + "elliptic AR 4 VLM": { + "alphas": [ + 4.0, + 8.0, + 12.0 + ], + "CL": [ + 0.26128869069800176, + 0.5190656580851507, + 0.7699341317782422 + ], + "VSM_CL_over_VLM": [ + 0.9893127910917663, + 0.9968360929954441, + 1.0095490001817362 + ], + "LLT_CL_over_VLM": [ + 1.1149386613087837, + 1.117987137435919, + 1.1234257871458655 + ] + }, + "elliptic AR 4 curved quarter chord LLT on-blade over Trefftz": 1.3559368932945106, + "elliptic AR 4 curved quarter chord VSM on-blade over Trefftz": 0.9902361800899013, + "rectangular AR 8 VSM at 5 deg over VLM": { + "CL": 0.9975115821855666, + "CDi": 1.0306382233133708 + } + }, + "figure_13": { + "V3 kite, inviscid, 10 deg": { + "N": [ + 10, + 20, + 40, + 80, + 160 + ], + "CL": [ + 0.637363986960486, + 0.6402568220108974, + 0.6393171088835797, + 0.6385107288798224, + 0.6379759739912666 + ], + "CD": [ + 0.027675249430394237, + 0.028052909232293376, + 0.028298800310764233, + 0.0284336819387546, + 0.028519468839138445 + ], + "on_blade_over_trefftz": [ + 1.0207406219369242, + 1.00662934510494, + 1.0072015329480584, + 1.008284529459178, + 1.009664834821042 + ] + }, + "straight AR 8, 5 deg": { + "N": [ + 10, + 20, + 40, + 80, + 160 + ], + "CL": [ + 0.4202226251698962, + 0.40961430047730546, + 0.4037683141001728, + 0.4007293936922778, + 0.3991813178631238 + ], + "CD": [ + 0.006556742531394257, + 0.006533838132738166, + 0.006502921040432454, + 0.006483374761696476, + 0.006472575417770192 + ], + "on_blade_over_trefftz": [ + 1.0007278450941295, + 1.000170085790424, + 1.0000241613419563, + 0.9999993387640963, + 0.9999951646012563 + ] + } + } +} \ No newline at end of file diff --git a/docs/validation/aerodynamic_model_figures.py b/docs/validation/aerodynamic_model_figures.py new file mode 100644 index 0000000..95b6395 --- /dev/null +++ b/docs/validation/aerodynamic_model_figures.py @@ -0,0 +1,184 @@ +"""Figures 12 and 13 of docs/figures for the aerodynamic model document +(docs/aerodynamic_model/aerodynamic_model.tex): + +* 12: classical wings against theory and a vortex-lattice method. Elliptic + wings of aspect ratio 20 and 4 (straight quarter-chord line) against + Prandtl's lifting-line result (C_L = 2 pi alpha / (1 + 2/AR), Oswald + factor 1) and the AeroSandbox VLM, with the LLT and the VSM; the + rectangular AR 8 wing against the VLM. The JSON also records what the LLT + and the VSM do when the same planform is drawn with a straight leading + edge, i.e. a curved quarter-chord line. +* 13: mesh convergence on the inviscid V3 kite and the straight wing: lift, + on-blade induced drag and the on-blade over Trefftz ratio against the + number of panels. + +Usage (from the repository root, about 3 minutes):: + + python docs/validation/aerodynamic_model_figures.py +""" + +from __future__ import annotations + +import json +import logging +from pathlib import Path + +import numpy as np + +from VSM.core.BodyAerodynamics import BodyAerodynamics +from VSM.core.Solver import Solver +from VSM.core.WingGeometry import Wing + +import curved_bound_vortex_validation as cbv + +logging.disable(logging.CRITICAL) +OUT = Path(__file__).resolve().parent +FIG = OUT.parent / "figures" +INVISCID = cbv.INVISCID +RHO = 1.225 + + +def elliptic(n, aspect_ratio, root_chord=1.0, straight_quarter_chord=True, **kw): + """Planar elliptic wing, sections along x. With ``straight_quarter_chord`` + the quarter-chord line is the y axis, as Prandtl's theory assumes; otherwise + the leading edge is straight and the quarter-chord line is curved.""" + s = aspect_ratio * (np.pi / 4) * root_chord / 2 + wing = Wing(n_panels=n, spanwise_panel_distribution="cosine") + for y in np.linspace(-s, s, n + 1): + eta = min(abs(y) / s, 0.999999) + c = root_chord * np.sqrt(1 - eta**2) + x_le = -0.25 * c if straight_quarter_chord else 0.0 + wing.add_section(np.array([x_le, y, 0.0]), np.array([x_le + c, y, 0.0]), INVISCID) + body = BodyAerodynamics([wing], **kw) + return body, np.pi * root_chord * s / 2 + + +def elliptic_vlm(aspect_ratio, alphas, root_chord=1.0, n_chord=16, n_seg=2): + """AeroSandbox VLM on the same elliptic wing (None without AeroSandbox).""" + try: + import aerosandbox as asb + except ImportError: + return None + s = aspect_ratio * (np.pi / 4) * root_chord / 2 + flat = asb.Airfoil("naca0010") + xsecs, chords = [], [] + ys = np.linspace(-s, s, 41) + for y in ys: + eta = min(abs(y) / s, 0.999999) + c = root_chord * np.sqrt(1 - eta**2) + chords.append(c) + xsecs.append(asb.WingXSec(xyz_le=[-0.25 * c, y, 0.0], chord=c, twist=0.0, airfoil=flat)) + area = float(np.trapz(chords, ys)) + airplane = asb.Airplane(wings=[asb.Wing(symmetric=False, xsecs=xsecs)], s_ref=area, c_ref=1.0, b_ref=1.0) + cl = [] + for a in alphas: + r = asb.VortexLatticeMethod(airplane=airplane, op_point=asb.OperatingPoint(velocity=20.0, alpha=float(a)), + spanwise_resolution=n_seg, chordwise_resolution=n_chord, + align_trailing_vortices_with_wind=False).run() + cl.append(float(r["CL"])) + return np.array(cl) + + +def polar(body, area, U, alphas, model="VSM"): + cl, cd = [], [] + for a in alphas: + body.va_initialize(U, a, 0.0) + r = Solver(aerodynamic_model_type=model, allowed_error=1e-7, max_iterations=4000).solve(body) + q = 0.5 * RHO * U**2 * area + cl.append(r["lift"] / q) + cd.append(r["drag"] / q) + return np.array(cl), np.array(cd) + + +def figure_12(): + import matplotlib + + matplotlib.use("Agg") + import matplotlib.pyplot as plt + + plt.rcParams.update({"font.size": 9}) + fig, ax = plt.subplots(1, 3, figsize=(15, 4.6)) + alphas = np.arange(0, 13, 2.0) + out = {} + for AR, col in ((20.0, "tab:blue"), (4.0, "tab:red")): + body, area = elliptic(60, AR) + cl_th = 2 * np.pi * np.radians(alphas) / (1 + 2 / AR) + cdi_th = cl_th**2 / (np.pi * AR) + ax[0].plot(alphas, cl_th, "-", color=col, lw=2, label=f"Prandtl, AR {AR:.0f}") + ax[1].plot(cl_th**2, cdi_th, "-", color=col, lw=2, label=f"Prandtl, AR {AR:.0f}") + for model, mk in (("LLT", "s"), ("VSM", "o")): + cl, cd = polar(body, area, 20.0, alphas, model) + ax[0].plot(alphas, cl, mk, color=col, mfc="none" if model == "LLT" else col, ms=5, label=f"{model}, AR {AR:.0f}") + ax[1].plot(cl**2, cd, mk, color=col, mfc="none" if model == "LLT" else col, ms=5, label=f"{model}, AR {AR:.0f}") + slope = np.polyfit(np.radians(alphas), cl, 1)[0] + e = np.polyfit(cl[1:] ** 2, cd[1:], 1)[0] + out[f"elliptic AR {AR:.0f} {model}"] = {"lift_slope_over_theory": float(slope / (2 * np.pi / (1 + 2 / AR))), "oswald": float(1 / (np.pi * AR * e)), "CL": cl.tolist(), "CDi": cd.tolist()} + vlm_alphas = np.array([4.0, 8.0, 12.0]) + cl_vlm = elliptic_vlm(AR, vlm_alphas) + if cl_vlm is not None: + ax[0].plot(vlm_alphas, cl_vlm, "*", color="k", ms=10, label=f"AeroSandbox VLM, AR {AR:.0f}" if AR == 20.0 else None) + out[f"elliptic AR {AR:.0f} VLM"] = {"alphas": vlm_alphas.tolist(), "CL": cl_vlm.tolist(), + "VSM_CL_over_VLM": (np.interp(vlm_alphas, alphas, out[f"elliptic AR {AR:.0f} VSM"]["CL"]) / cl_vlm).tolist(), + "LLT_CL_over_VLM": (np.interp(vlm_alphas, alphas, out[f"elliptic AR {AR:.0f} LLT"]["CL"]) / cl_vlm).tolist()} + # the same planform with a straight leading edge: the quarter-chord line is then curved + body_c, _ = elliptic(60, AR, straight_quarter_chord=False) + body_c.va_initialize(20.0, 8.0, 0.0) + for model in ("LLT", "VSM"): + r = Solver(aerodynamic_model_type=model, allowed_error=1e-7, max_iterations=4000).solve(body_c) + out[f"elliptic AR {AR:.0f} curved quarter chord {model} on-blade over Trefftz"] = float(r["drag"] / r["drag_induced_trefftz"]) + ax[0].set_xlabel("alpha [deg]"); ax[0].set_ylabel("CL"); ax[0].set_title("elliptic wings: lift against Prandtl") + ax[1].set_xlabel("CL^2"); ax[1].set_ylabel("CDi"); ax[1].set_title("elliptic wings: induced drag against Prandtl") + ax[0].legend(fontsize=7, frameon=False, ncol=2); ax[1].legend(fontsize=7, frameon=False, ncol=2) + + # rectangular AR 8 against the VLM referee results (straight AR 8, 5 deg) + ref = json.load(open(OUT / "vlm_referee.json"))["straight AR 8"] + vlm = ref["VLM 16x80"] + alphas_r = np.arange(0, 13, 2.0) + body = cbv.straight(80) + cl, cd = polar(body, 8.0, 20.0, alphas_r) + ax[2].plot(alphas_r, cl, "o-", color="tab:green", ms=4, label="VSM, 80 panels") + ax[2].plot([5.0], [vlm["CL"]], "k*", ms=11, label="AeroSandbox VLM 16x80, 5 deg") + ax[2].plot(alphas_r, 2 * np.pi * np.radians(alphas_r) / (1 + 2 / 8), "--", color="0.5", label="Prandtl elliptic, AR 8 (reference slope)") + ax[2].set_xlabel("alpha [deg]"); ax[2].set_ylabel("CL"); ax[2].set_title("rectangular AR 8 wing") + ax[2].legend(fontsize=7, frameon=False) + cl5, cd5 = polar(body, 8.0, 20.0, [5.0]) + out["rectangular AR 8 VSM at 5 deg over VLM"] = {"CL": float(cl5[0] / vlm["CL"]), "CDi": float(cd5[0] / vlm["CDi_trefftz"])} + fig.tight_layout() + fig.savefig(FIG / "12_classical_wings.png", dpi=150) + print("figure 12 written", json.dumps(out, indent=1)) + return out + + +def figure_13(): + import matplotlib + + matplotlib.use("Agg") + import matplotlib.pyplot as plt + + plt.rcParams.update({"font.size": 9}) + fig, ax = plt.subplots(1, 3, figsize=(15, 4.4)) + Ns = (10, 20, 40, 80, 160) + out = {} + for name, mk, col in (("V3 kite, inviscid, 10 deg", cbv.v3_inviscid, "tab:red"), ("straight AR 8, 5 deg", cbv.straight, "tab:blue")): + cl, cd, ratio = [], [], [] + for N in Ns: + body = mk(N) + r = Solver(allowed_error=1e-7, max_iterations=4000).solve(body) + cl.append(r["cl"]); cd.append(r["cd"]); ratio.append(r["drag"] / r["drag_induced_trefftz"]) + out[name] = {"N": list(Ns), "CL": cl, "CD": cd, "on_blade_over_trefftz": ratio} + ax[0].plot(Ns, np.array(cl) / cl[-1], "o-", color=col, label=name) + ax[1].plot(Ns, np.array(cd) / cd[-1], "o-", color=col, label=name) + ax[2].plot(Ns, ratio, "o-", color=col, label=name) + for A, t in zip(ax, ("CL / CL(160 panels)", "CDi / CDi(160 panels)", "on-blade / Trefftz induced drag")): + A.set_xscale("log", base=2); A.set_xlabel("number of panels"); A.set_ylabel(t); A.legend(fontsize=8, frameon=False) + ax[2].axhline(1.0, color="k", lw=0.8) + fig.suptitle("mesh convergence, uniform spacing", y=1.0) + fig.tight_layout() + fig.savefig(FIG / "13_mesh_convergence.png", dpi=150) + print("figure 13 written", json.dumps(out, indent=1)) + return out + + +if __name__ == "__main__": + res = {"figure_12": figure_12(), "figure_13": figure_13()} + json.dump(res, open(OUT / "aerodynamic_model_figures.json", "w"), indent=1) diff --git a/docs/validation/chord_spread_figure.py b/docs/validation/chord_spread_figure.py new file mode 100644 index 0000000..ef65e66 --- /dev/null +++ b/docs/validation/chord_spread_figure.py @@ -0,0 +1,225 @@ +"""Figure 15 of docs/figures: the chordwise spreading of the bound vorticity in +the curved bound-vortex regularisation (report Section 4, eqs. 4-6). + +(a) Three neighbouring panels of the arc wing with their bound segments + replaced by the 24 thin-airfoil strips along the chord (line weight and + shade proportional to the strip weight), each strip with its own legs on + the node's leg line. +(b) The same panels on the unrolled (straight) reference line of the middle + panel, with the same strips. +(c) Chordwise velocity induced at the quarter-chord point of the middle panel + by ONE neighbouring bound segment of unit strength, against its arc-length + distance, for the single filament and for the spread sheet: far away the + two coincide, within a chord the sheet stays finite where the filament + grows. +(d) The running sum of those contributions from the far field inwards, for + several panel counts: the filament sum keeps rising as the nearest + neighbours get closer (the logarithm), the sheet sum settles. + +Usage (from the repository root, seconds):: + + python docs/validation/chord_spread_figure.py +""" + +from __future__ import annotations + +import logging +from pathlib import Path + +import numpy as np + +import curved_bound_vortex_validation as cbv +from VSM.core.utils import thin_airfoil_strips + +logging.disable(logging.CRITICAL) +FIG = Path(__file__).resolve().parents[1] / "figures" +AC = 0.25 + + +def seg_velocity(a, b, p): + """Unit-strength straight segment a->b at p (Biot-Savart), zero on the line.""" + r1, r2 = p - a, p - b + cr = np.cross(r1, r2) + n = np.dot(cr, cr) + if n < 1e-20: + return np.zeros(3) + return cr / (4 * np.pi * n) * np.dot(b - a, r1 / np.linalg.norm(r1) - r2 / np.linalg.norm(r2)) + + +def geometry(body): + P = body.panels + bound = np.array([p.bound_point_1 for p in P] + [P[-1].bound_point_2]) + te = np.array([p.TE_point_1 for p in P] + [P[-1].TE_point_2]) + ac = np.array([p.aerodynamic_center for p in P]) + chord_dir = np.array([p.y_airf for p in P]) + chords = np.array([p.chord for p in P]) + return bound, te, ac, chord_dir, chords + + +def strips_of_panel(b1, b2, t1, t2, x, w): + """Strip segments of one panel: stations along each node's leg line.""" + f = (x - AC) / (1.0 - AC) + a = b1[None, :] + f[:, None] * (t1 - b1)[None, :] + b = b2[None, :] + f[:, None] * (t2 - b2)[None, :] + return a, b, w + + +def unrolled(bound, te, ac, chord_dir, chords, i): + seg = bound[1:] - bound[:-1] + L = np.linalg.norm(seg, axis=1) + s = np.concatenate(([0.0], np.cumsum(L))) + t = seg[i] / L[i] + s_P = 0.5 * (s[i] + s[i + 1]) + nodes = ac[i][None, :] + (s - s_P)[:, None] * t[None, :] + c_node = np.concatenate(([chords[0]], 0.5 * (chords[1:] + chords[:-1]), [chords[-1]])) + ends = nodes + (1.0 - AC) * c_node[:, None] * chord_dir[i][None, :] + return nodes, ends, s - s_P + + +def draw_strips(ax, bound, te, panels, x, w, ij, color, show_legs=True): + u, v = ij + for j in panels: + a, b, ww = strips_of_panel(bound[j], bound[j + 1], te[j], te[j + 1], x, w) + for k in range(len(ww)): + lw = 0.3 + 6.0 * ww[k] + ax.plot([a[k, u], b[k, u]], [a[k, v], b[k, v]], color=color, lw=lw, alpha=0.35 + 0.65 * ww[k] / ww.max(), solid_capstyle="butt") + if show_legs: + for k in range(0, len(ww), 6): + ax.plot([a[k, u], te[j, u]], [a[k, v], te[j, v]], color=color, lw=0.4, alpha=0.5) + ax.plot([b[k, u], te[j + 1, u]], [b[k, v], te[j + 1, v]], color=color, lw=0.4, alpha=0.5) + + +def main(): + import matplotlib + + matplotlib.use("Agg") + import matplotlib.pyplot as plt + from matplotlib.lines import Line2D + + plt.rcParams.update({"font.size": 9}) + fig = plt.figure(figsize=(14, 10)) + ax = np.empty((2, 2), dtype=object) + ax[0, 0] = fig.add_subplot(2, 2, 1) + ax[0, 1] = fig.add_subplot(2, 2, 2, projection="3d") + ax[1, 0] = fig.add_subplot(2, 2, 3) + ax[1, 1] = fig.add_subplot(2, 2, 4) + x, w = thin_airfoil_strips(24) + + # ---- (a) real panels near the tip of the arc wing, front-ish view (y, z) with the chord along x shown by projection + body = cbv.arc(16) + bound, te, ac, cdir, chords = geometry(body) + i = 3 + panels = [i - 1, i, i + 1] + A = ax[0, 0] + # top view (x along the page downwards, y across): shows the chordwise strips + for j in range(len(bound) - 1): + A.plot([bound[j, 1], bound[j + 1, 1]], [bound[j, 0], bound[j + 1, 0]], color="0.6", lw=1) + for j in panels: + A.plot([bound[j, 1], bound[j + 1, 1]], [bound[j, 0], bound[j + 1, 0]], color="k", lw=2.2) + for k in (j, j + 1): + A.plot([bound[k, 1], te[k, 1]], [bound[k, 0], te[k, 0]], color="k", lw=0.8) + draw_strips(A, bound, te, panels, x, w, (1, 0), "tab:red") + A.plot([ac[i, 1]], [ac[i, 0]], "*", color="tab:blue", ms=13, zorder=5) + A.annotate("P_i (quarter chord of panel i)", (ac[i, 1], ac[i, 0]), textcoords="offset points", xytext=(10, -12), fontsize=8, color="tab:blue") + A.set_xlim(bound[i - 1, 1] - 0.15, bound[i + 2, 1] + 0.15) + A.set_ylim(1.08, -0.08) + A.set_aspect("equal") + A.set_xlabel("y"); A.set_ylabel("x (chordwise, flow downwards)") + A.set_title("(a) three real panels of the arc wing, top view: the bound vortex spread into 24 strips") + + # ---- (b) 3-D view: the real strips (red) and the unrolled copies (green) of the same panels + B = ax[0, 1] + nodes, ends, _ = unrolled(bound, te, ac, cdir, chords, i) + B.plot(bound[:, 0], bound[:, 1], bound[:, 2], color="0.6", lw=1) + B.plot(nodes[:, 0], nodes[:, 1], nodes[:, 2], color="0.6", lw=1, ls=":") + for pts, tes, col in ((bound, te, "tab:red"), (nodes, ends, "tab:green")): + for j in panels: + B.plot([pts[j, 0], pts[j + 1, 0]], [pts[j, 1], pts[j + 1, 1]], [pts[j, 2], pts[j + 1, 2]], color="k", lw=2) + a, b, ww = strips_of_panel(pts[j], pts[j + 1], tes[j], tes[j + 1], x, w) + for k in range(len(ww)): + B.plot([a[k, 0], b[k, 0]], [a[k, 1], b[k, 1]], [a[k, 2], b[k, 2]], color=col, lw=0.3 + 5.0 * ww[k], alpha=0.35 + 0.65 * ww[k] / ww.max()) + for k in (j, j + 1): + B.plot([pts[k, 0], tes[k, 0]], [pts[k, 1], tes[k, 1]], [pts[k, 2], tes[k, 2]], color="k", lw=0.6) + B.plot([ac[i, 0]], [ac[i, 1]], [ac[i, 2]], "*", color="tab:blue", ms=12) + B.set_xlabel("x"); B.set_ylabel("y"); B.set_zlabel("z") + pts_all = np.vstack([bound[i - 1:i + 3], te[i - 1:i + 3], nodes[i - 1:i + 3], ends[i - 1:i + 3]]) + lo, hi = pts_all.min(axis=0) - 0.1, pts_all.max(axis=0) + 0.1 + B.set_xlim(lo[0], hi[0]); B.set_ylim(lo[1], hi[1]); B.set_zlim(lo[2], hi[2]) + B.set_box_aspect(tuple(hi - lo)) + B.view_init(elev=25, azim=-50) + B.set_title("(b) the same strips on the real (red, curved) and on the unrolled (green, straight) panels") + + # ---- (c) one neighbouring segment's chordwise velocity at P_i against distance: filament vs sheet + C = ax[1, 0] + body = cbv.arc(320) + bound, te, ac, cdir, chords = geometry(body) + i = 160 + p = ac[i] + seg = bound[1:] - bound[:-1] + s = np.concatenate(([0.0], np.cumsum(np.linalg.norm(seg, axis=1)))) + s_mid = 0.5 * (s[:-1] + s[1:]) + d = np.abs(s_mid - s_mid[i]) + v_fil, v_sheet = np.zeros(len(seg)), np.zeros(len(seg)) + for j in range(len(seg)): + if j == i: + continue + v_fil[j] = seg_velocity(bound[j], bound[j + 1], p)[0] + a, b, ww = strips_of_panel(bound[j], bound[j + 1], te[j], te[j + 1], x, w) + v_sheet[j] = sum(ww[k] * seg_velocity(a[k], b[k], p)[0] for k in range(len(ww))) + o = np.argsort(d) + m = (d > 0) + C.loglog(d[m], np.abs(v_fil[m]), ".", color="tab:red", ms=4, label="single filament at the quarter chord") + C.loglog(d[m], np.abs(v_sheet[m]), ".", color="tab:green", ms=4, label="chord-spread sheet (24 strips)") + C.axvline(0.15, color="0.5", ls="--", lw=0.8) + C.text(0.16, np.abs(v_fil[m]).max() * 0.5, "effective cut-off 0.15 c", fontsize=8, color="0.3") + C.axvline(1.0, color="0.8", ls=":", lw=0.8); C.text(1.05, np.abs(v_fil[m]).max() * 0.5, "one chord", fontsize=8, color="0.5") + C.set_xlabel("arc-length distance of the neighbouring segment from P_i (chord = 1)") + C.set_ylabel("|chordwise velocity at P_i| per unit circulation, one segment") + C.set_title("(c) what one neighbour induces: the sheet is the filament beyond a chord, finite within it") + C.legend(fontsize=8, frameon=False) + + # ---- (d) running sum from the far field inwards, for several panel counts + D = ax[1, 1] + for N, col in ((40, "0.75"), (80, "0.55"), (160, "0.35"), (320, "0.1")): + body = cbv.arc(N) + bound, te, ac, cdir, chords = geometry(body) + i = N // 2 + p = ac[i] + seg = bound[1:] - bound[:-1] + s = np.concatenate(([0.0], np.cumsum(np.linalg.norm(seg, axis=1)))) + s_mid = 0.5 * (s[:-1] + s[1:]) + d = np.abs(s_mid - s_mid[i]) + vf, vs = np.zeros(N), np.zeros(N) + for j in range(N): + if j == i: + continue + vf[j] = seg_velocity(bound[j], bound[j + 1], p)[0] + a, b, ww = strips_of_panel(bound[j], bound[j + 1], te[j], te[j + 1], x, w) + vs[j] = sum(ww[k] * seg_velocity(a[k], b[k], p)[0] for k in range(len(ww))) + o = np.argsort(-d) # far to near + dd = d[o][d[o] > 0] + D.semilogx(dd, np.cumsum(vf[o][d[o] > 0]), "-", color=col, lw=1.2, label=f"filaments, {N} panels") + D.semilogx(dd, np.cumsum(vs[o][d[o] > 0]), "--", color="tab:green", lw=1.2, alpha=min(1.0, 0.35 + 0.08 * (N // 40)), label=f"sheets, {N} panels") + D.invert_xaxis() + D.set_xlabel("distance from P_i of the segments summed so far (far field on the left)") + D.set_ylabel("running sum of the chordwise velocity at P_i") + D.set_title("(d) summing the neighbours from far to near: filaments keep growing, sheets settle") + D.legend(fontsize=7, frameon=False, ncol=2) + + handles = [ + Line2D([], [], color="k", lw=2.2, label="bound vortex (quarter-chord line)"), + Line2D([], [], color="k", lw=0.8, label="legs to the trailing edge"), + Line2D([], [], color="tab:red", lw=2, label="strips of the real panels (shade = weight)"), + Line2D([], [], color="tab:green", lw=2, label="strips of the unrolled panels"), + Line2D([], [], color="tab:blue", marker="*", ls="", ms=10, label="evaluation point P_i"), + ] + fig.legend(handles=handles, loc="lower center", ncol=5, frameon=False, bbox_to_anchor=(0.5, 0.0)) + fig.suptitle("Chordwise spreading of the bound vorticity: the sheet that replaces the line", y=0.995) + fig.tight_layout(rect=(0, 0.04, 1, 0.98)) + out = FIG / "15_chord_spread.png" + fig.savefig(out, dpi=150) + print("figure 15 written to", out) + + +if __name__ == "__main__": + main() diff --git a/docs/validation/curved_bound_vortex_report.pdf b/docs/validation/curved_bound_vortex_report.pdf new file mode 100644 index 0000000..4488902 Binary files /dev/null and b/docs/validation/curved_bound_vortex_report.pdf differ diff --git a/docs/validation/curved_bound_vortex_report.tex b/docs/validation/curved_bound_vortex_report.tex new file mode 100644 index 0000000..f6bda29 --- /dev/null +++ b/docs/validation/curved_bound_vortex_report.tex @@ -0,0 +1,381 @@ +\documentclass[11pt,a4paper]{article} +\usepackage[margin=2.2cm]{geometry} +\usepackage{amsmath,amssymb} +\usepackage{graphicx} +\usepackage{booktabs} +\usepackage{array} +\usepackage[hidelinks]{hyperref} +\usepackage{caption} +\usepackage{enumitem} +\usepackage{microtype} +\graphicspath{{../figures/}} +\setlength{\parskip}{4pt} +\setlength{\parindent}{0pt} +\title{Curved lifting lines in the Vortex Step Method:\\ a chord-regularised curvature correction at the quarter chord} +\author{Working document, awegroup/Vortex-Step-Method, branch \texttt{feature/curved-bound-vortex-regularisation}} +\date{17 September 2026, rewritten 13 October 2026} + +\begin{document} +\maketitle + +\begin{abstract} +The Vortex Step Method solves the circulation at the three-quarter-chord control point and, following Gaunaa, Li and Pirrung (TORQUE 2026), takes the direction of the sectional force from the velocity at the quarter chord. On a straight wing that velocity is Prandtl's downwash and the on-blade induced drag equals the Trefftz-plane value exactly. On a curved wing the neighbouring bound vortex, seen from a point on the line, induces a chordwise velocity that grows without bound as the sections are refined, and the drag drifts with the mesh: 0.93 to 0.85 of the Trefftz value on the V3 kite between 20 and 160 sections. This document explains why two line vortices meeting at an angle have no finite mutual velocity while two chordwise sheets do, and builds the curvature correction from that: the quarter-chord velocity is Prandtl's downwash of the wing straightened at the section, plus the velocity the real wing induces as chordwise sheets minus the velocity the straightened wing induces as the same sheets. The correction is zero on straight wings, has no parameter, the thin-airfoil loading fixing its cut-off at $0.15\,c$, and works for any direction of the trailed legs. On-blade induced drag is within 1\,\% of the Trefftz plane on an arc wing, a swept wing, the swept--anhedral elliptic wing of Gaunaa et al.\ and the V3, independently of the mesh, and the lift agrees with a vortex lattice within 1\,\% with the legs along the ribs. Alternatives tested along the way are in the appendix. All numbers are reproduced by the scripts in \texttt{docs/validation}. +\end{abstract} + +\section{The baseline: Prandtl's straight wing, and why it is exact} +\label{sec:baseline} + +The VSM couples a 2D section model to a 3D vortex system in the way of~\cite{gaunaa2026}. Each section carries its circulation $\Gamma_i$ as a bound vortex on the quarter-chord line, trailed legs from the quarter chord to the trailing edge and a wake. The section's own 2D model is thin-airfoil theory, or a polar standing in for it, and it is coupled at two points: +\begin{itemize}[nosep] + \item \emph{Magnitude at the three-quarter chord.} A lumped vortex at the quarter chord with the no-penetration condition at the three-quarter chord reproduces thin-airfoil lift exactly (Pistolesi). The control point sees the whole 3D system minus the section's own infinite 2D vortex, which the polar already contains, and the circulation follows from $\Gamma_i=\tfrac12|V_i|c_iC_l(\alpha_i)$. + \item \emph{Direction at the quarter chord.} The sectional force is $\rho\,\Gamma_i\,\mathbf V_{1/4,i}\times\mathbf l_i$ with the velocity at the bound vortex itself, plus the Kutta--Joukowski force on the attached-trailed legs. This is what makes the on-blade induced drag consistent with the far wake. +\end{itemize} + +On a straight wing the quarter-chord velocity is Prandtl's: the bound vortex lies on one straight line and induces nothing at points on that line, so only the trailed vorticity contributes, +\begin{equation} +\mathbf V_{1/4,i}=\mathbf V_\infty+\sum_j\Gamma_j\,\mathbf K^{\mathrm{trailed}}_j(\mathbf P_i). +\label{eq:prandtl} +\end{equation} +Prandtl's lifting line is exact in two senses that matter here. The neighbours' bound vorticity cancels from the velocity at the line by geometry, so the section's own 2D field never has to be subtracted at the quarter chord. And Munk's theorem makes the on-blade induced drag $\sum_i\rho\Gamma_i w_i\,\Delta s_i$, with $w_i$ the downwash of eq.~\eqref{eq:prandtl}, equal to the Trefftz-plane drag of the same circulation. The code reproduces this: the ratio of on-blade to Trefftz induced drag is 1.000 on a straight rectangular wing at every panel count, with the attached-trailed force included (Table~\ref{tab:fix}, first row). This is the baseline every curved-wing result must return to when the curvature is removed, and the control case for anything added. + +\section{The problem: curvature} +\label{sec:problem} + +On a curved wing the neighbouring bound segments are not on the section's line. Seen from the quarter-chord point $\mathbf P_i$, a neighbour bent out of the section's plane induces a velocity perpendicular to the plane of the two segments, i.e.\ along the chord. Let line 2 start where line 1 ends, turned by an angle $\theta$, and let $\mathbf P$ sit on line 1 at distance $r$ from the kink. Line 2 induces at $\mathbf P$ +\begin{equation} +v=\frac{\Gamma\,(1-\cos\theta)}{4\pi\, r\sin\theta}\;\approx\;\frac{\Gamma\theta}{8\pi r}, +\label{eq:kink} +\end{equation} +unbounded as $\mathbf P$ approaches the kink. A smooth line of curvature $\kappa$ is a continuum of such kinks: the element at arc distance $d$ is seen under the angle $\kappa d/2$ and contributes $\Gamma\kappa\,\mathrm{d}s/(8\pi d)$, so the sum from the nearest element, at the panel width $\Delta s$, to the span $L$ is +\begin{equation} +u\simeq\frac{\Gamma\kappa}{8\pi}\ln\frac{L}{\Delta s}\quad\text{per side}, +\label{eq:log} +\end{equation} +one constant more at every mesh doubling, with no limit because the nearest element is always at the panel width. This is the logarithmic self-induction of a curved lifting line of Li et al.~\cite{li2020}. Two line vortices meeting at an angle have no finite mutual velocity. + +The drag is exceptionally sensitive to this velocity. With the force perpendicular to $\mathbf V_{1/4}=(U\cos\alpha+u,\;U\sin\alpha-w)$ in chord and normal components, its freestream component is +\begin{equation} +D_i=\rho\,\Gamma_i\,(w\cos\alpha+u\sin\alpha). +\label{eq:drag} +\end{equation} +The downwash $w$ is about one degree of induced angle, so a chordwise $u$ of 3\,\% of $U$ at $\alpha=8^\circ$ moves the induced drag by 12\,\%. Table~\ref{tab:conv} shows the result with the neighbours' bound segments evaluated on the line (Phillips and Snyder~\cite{phillips2000}, the code before this work): lift converged to four digits, drag drifting by a constant per doubling on every wing with out-of-plane curvature. Leaving the bound segments out does not help: the trailed legs carry a logarithm of their own wherever they are not perpendicular to the line, which the bound segments partly cancel, and the swept wing with streamwise legs then collapses to 0.43 at 40 panels and 0.25 at 160. Figure~\ref{fig:explain} shows the mechanism on the arc wing. + +\begin{table}[h]\centering\small +\caption{On-blade over Trefftz-plane induced drag with the bound segments evaluated on the quarter-chord line, inviscid polars ($C_l=2\pi\alpha$).} +\label{tab:conv} +\begin{tabular}{lcccccc} +\toprule +case & $N=10$ & 20 & 40 & 80 & 160 & trend\\ +\midrule +straight rectangular, AR 8 & -- & 1.000 & 1.000 & 1.000 & -- & exact\\ +swept $30^\circ$, AR 8, streamwise ribs & 0.933 & 0.937 & 0.939 & 0.940 & 0.936 & converged, 6\,\% low\\ +circular arc, $110^\circ$, $R=4c$ & 0.998 & 0.977 & 0.957 & 0.937 & 0.917 & $-0.02$ per doubling\\ +elliptic, 10\,\% sweep, 25\,\% anhedral~\cite{gaunaa2026} & -- & -- & 0.980 & 0.967 & 0.954 & $-0.013$ per doubling\\ +V3 kite, CAD geometry & -- & 0.933 & 0.907 & 0.881 & 0.854 & $-0.026$ per doubling\\ +\bottomrule +\end{tabular} +\end{table} + +\begin{figure}[p]\centering +\includegraphics[width=\textwidth]{08_curved_bound_vortex_regularisation.png} +\caption{Mechanism and fix (\texttt{docs/figures/08}). (a) Front view of the curved bound polyline with the quarter-chord point of panel $i$. (b) Arc wing, mid-span: chordwise self-induction from the polyline, growing logarithmically with $N$, and from the chordwise-spread sheet, finite; grey band: the value at which on-blade drag equals Trefftz. (c) Why a chordwise velocity changes the drag, eq.~\eqref{eq:drag}. (d) The thin-airfoil bound vorticity distribution, effective cut-off $0.15\,c$. (e) The straightened reference wing. (f) On-blade over Trefftz drag before and after.} +\label{fig:explain} +\end{figure} + +\section{What Li and Gaunaa do} +\label{sec:lit} + +Reference~\cite{li2020} identifies the singularity and avoids the quarter-chord point altogether: the bound-vortex induction is evaluated at the three-quarter-chord control point, half a chord from every filament, the section's own infinite 2D vortex is subtracted there, and the difference is assumed constant along the chord and reused at the quarter chord. Reference~\cite{gaunaa2026} cites this for LL-Gaunaa without further detail, and takes the trailed legs perpendicular to the quarter-chord line. + +Implemented literally in the VSM the recipe fails on straight wings, 0.85 of the Trefftz drag on an AR\,8 wing: at the control point of a finite wing with spanwise-varying $\Gamma$ the ``3D minus 2D'' bound difference is not zero, it holds the finite-span tails and the variation of $\Gamma$, and eq.~\eqref{eq:drag} amplifies it. The form that does reproduce the authors' published numbers is ``curved minus straight'': the bound segments where they are minus the same segments laid on the section's tangent, single filaments, at the control point, reused at the quarter chord. It is zero on straight wings by construction and finite everywhere, with the half-chord distance to the control point as its cut-off. Table~\ref{tab:hyp} shows what it does here: with the paper's perpendicular legs it is mesh-independent and within 1--2\,\% of the Trefftz plane; with the legs along the ribs it diverges on the V3 and the kinked wing, because skewed legs carry the leg logarithm of Section~\ref{sec:problem} and a difference of bound segments cannot see it. The authors never meet that case, their legs being perpendicular by construction; a kite with ribs does. Two things are therefore assumed in their construction: that the curvature velocity is constant along the chord, and that the legs are perpendicular to the line. Which form their code uses is to be confirmed with them. + +\begin{table}[h]\centering\small +\caption{The curved-minus-straight bound term at the control point, reused at the quarter chord (the hypothesised LL-Gaunaa form; \texttt{curved\_bound\_vortex\_validation.py --hypothesis}): on-blade over Trefftz induced drag with rib legs and with the paper's perpendicular legs.} +\label{tab:hyp} +\begin{tabular}{llcccl} +\toprule +case & legs & $N=40$ & 80 & 160 & trend\\ +\midrule +straight AR 8 & either & 1.000 & 1.000 & -- & exact\\ +circular arc, $R=4c$ & either & 0.993 & 0.993 & 0.993 & converged, 0.7\,\% low\\ +elliptic, swept--anhedral & perpendicular & 1.020 & 1.017 & 1.012 & converging\\ +elliptic, swept--anhedral & rib & 0.993 & 0.986 & 0.979 & drifting\\ +V3 kite & perpendicular & 1.006 & 1.015 & crosses & converged\\ +V3 kite & rib & 0.809 & 0.772 & 0.735 & diverging\\ +swept $30^\circ$, kinked & perpendicular & 1.129 & 1.138 & -- & fan of legs at the kink\\ +swept $30^\circ$, kinked & rib & 0.908 & 0.840 & -- & diverging\\ +\bottomrule +\end{tabular} +\end{table} + +Other published remedies replace the panel width in eq.~\eqref{eq:log} by a chosen length: a vortex core on the bound vortex, or the effective lifting line of Reid and Hunsaker~\cite{reid2020,goates2023}, straight near the point and blended into the true curve over a prescribed distance, with the legs leaving it perpendicularly over a prescribed joint. Both converge with the mesh and both depend on the length chosen, by 10--20\,\% of the induced drag across reasonable values, because that length is the cut-off of the logarithm (Appendix~\ref{app:alt}). The construction below takes the cut-off from the section itself. + +\section{The curvature correction with chordwise sheets} +\label{sec:fix} + +\subsection{A section is a sheet, and two sheets at an angle see a finite velocity} +Seen from closer than a chord, a section is not a line. Its bound vorticity is spread along the chord, in thin-airfoil theory as $\gamma(x)\propto\sqrt{(c-x)/x}$, and that is the vorticity the section model already uses for itself: its integral is $\Gamma$, its centroid is the quarter chord, and it induces on its own chord the uniform velocity that makes the flat plate a streamline. Give each neighbour that sheet and repeat the calculation of eq.~\eqref{eq:kink} with $\mathbf P$ at chord station $x_P$: the element of sheet 2 at station $x$ and arc distance $d$ sits at distance $\sqrt{d^2+\Delta x^2}$, $\Delta x=x-x_P$, and induces +\begin{equation} +\mathrm{d}v=\frac{\Gamma\gamma(x)\,\mathrm{d}x}{4\pi}\;\frac{\kappa d^2/2}{(d^2+\Delta x^2)^{3/2}}\,\mathrm{d}s , +\label{eq:sheet} +\end{equation} +which is the line's $\Gamma\kappa\,\mathrm{d}s/(8\pi d)$ for $d\gg|\Delta x|$ and vanishes like $d^2$ for $d\ll|\Delta x|$: from more than a chord away the sheet is the line, from within a chord the chordwise offset keeps the element away from $\mathbf P$, and at the kink itself the induced velocity has no chordwise component at all. Integrated over $d$ and averaged over the chord with the thin-airfoil weight, the logarithm of eq.~\eqref{eq:log} returns with the panel width replaced by a length of the section, +\begin{equation} +u\simeq\frac{\Gamma\kappa}{8\pi}\ln\frac{L}{c_{\mathrm{eff}}},\qquad c_{\mathrm{eff}}=\exp\langle\ln|x-c/4|\rangle_\gamma=0.15\,c . +\label{eq:ceff} +\end{equation} +The mesh has dropped out and nothing was chosen. Figure~\ref{fig:kink} shows eqs.~\eqref{eq:kink} and~\eqref{eq:sheet} computed for two vortices at a kink; Figure~\ref{fig:spread} shows the strips on the arc wing and the running sum of the neighbours' contributions, rising without limit as filaments and settling to one value as sheets. The chordwise distribution itself is not a sensitive choice: with the NeuralFoil loading of the V3 section instead of the flat plate the V3 induced drag moves by about 1\,\% (Appendix~\ref{app:strips}). + +\begin{figure}[t]\centering +\includegraphics[width=\textwidth]{16_kink_line_vs_sheet.png} +\caption{Two vortices meeting at a kink of angle $\theta$ (\texttt{docs/validation/kink\_figure.py}): the chordwise velocity that line 2, of unit strength, induces at a point of line 1 at distance $r$ from the kink, as a single filament and as the chord-spread sheet. (a) Kinks of 2, 5 and $10^\circ$: the filament follows eq.~\eqref{eq:kink}, a line of slope $-1$ without limit; the sheet coincides with it beyond about a third of a chord, is bounded within, and vanishes at the kink. (b) The $5^\circ$ case with the small-angle formula.} +\label{fig:kink} +\end{figure} + +\begin{figure}[p]\centering +\includegraphics[width=\textwidth]{15_chord_spread.png} +\caption{The chordwise spreading (\texttt{docs/validation/chord\_spread\_figure.py}). (a) Three panels of the arc wing from above: the bound vortex of each panel replaced by 24 strips at the thin-airfoil stations, line weight and shade proportional to the strip weight, each strip with its own legs on the node's leg line. (b) The same strips on the real, curved panels (red) and on the straightened panels of the middle panel's reference (green): near the panel the two sheets nearly coincide, which is why their difference is small there. (c) The chordwise velocity that one neighbouring bound segment of unit strength induces at $\mathbf P_i$ against its arc-length distance, as a filament and as a sheet: beyond a chord the same, within a chord the filament grows without bound and the sheet levels off at $0.15\,c$. (d) The running sum of those contributions from the far field inwards for 40 to 320 panels: with filaments every refinement adds a nearest neighbour and the sum keeps rising; with sheets it settles to the same value at every panel count.} +\label{fig:spread} +\end{figure} + +\subsection{Isolating the curvature: the real wing minus the wing straightened at the section} +The sheets are needed only where the wing bends. Where it is straight, Prandtl's line is exact and nothing should change. The correction is therefore written as a difference between two wings. For every evaluation panel $i$ a \emph{straightened} reference wing is built on panel $i$'s own tangent line (Figure~\ref{fig:unroll}): every node keeps its arc-length distance from the panel, the legs stand perpendicular to the line in panel $i$'s chord direction with each panel's own chord, the circulations $\Gamma_j$ are the actual ones, and the wake keeps its direction. There is one such reference per panel. The quarter-chord velocity is then +\begin{equation} +\boxed{\; +\mathbf V_{1/4,i}=\underbrace{\mathbf V_\infty+\sum_j\Gamma_j\,\mathbf K^{\mathrm{trailed}}_{j,\mathrm{straight}(i)}(\mathbf P_i)}_{\text{Prandtl, straightened wing}} +\;+\;\underbrace{\sum_j\Gamma_j\Big[\mathbf K^{\mathrm{sheet}}_{j}(\mathbf P_i)-\mathbf K^{\mathrm{sheet}}_{j,\mathrm{straight}(i)}(\mathbf P_i)\Big]}_{\text{curvature and skew}}\;} +\label{eq:fix} +\end{equation} +where $\mathbf K^{\mathrm{sheet}}_j$ is ring $j$ with its bound vorticity and the starting points of its legs spread over the chord with the thin-airfoil strips ($M=24$ stations $x_q=\sin^2\phi_q$, weights $w_q\propto\cos^2\phi_q$), the strip nodes lying on each node's own leg line so that the strip legs are collinear with the actual legs: +\begin{equation} +\mathbf K^{\mathrm{sheet}}_j(\mathbf P)=\sum_{q=1}^{M} w_q\Big[\mathbf K^{\mathrm{seg}}\big(\mathbf n_{2,jq}\to\mathbf n_{1,jq}\big) + \mathbf K^{\mathrm{leg}}\big(\mathbf n_{1,jq}\to\mathbf T_{1,j}\big)+\mathbf K^{\mathrm{leg}}\big(\mathbf T_{2,j}\to\mathbf n_{2,jq}\big)\Big]+\mathbf K^{\mathrm{wake}}_j, +\qquad +\mathbf n_{k,jq}=\mathbf b_{k,j}+\frac{x_q-\tfrac14}{1-\tfrac14}\,(\mathbf T_{k,j}-\mathbf b_{k,j}). +\end{equation} + +What eq.~\eqref{eq:fix} does, term by term: +\begin{itemize}[nosep] + \item \emph{The baseline is Prandtl.} The first term is eq.~\eqref{eq:prandtl} for the straightened wing: exact, finite as filaments because its legs are perpendicular to its line, and the quantity the section coupling is calibrated on. + \item \emph{The bracket is zero where there is nothing to correct.} Wherever the wing is straight and the ribs perpendicular, a panel sits in the same place in both wings and its two sheets cancel identically. On a straight wing the whole bracket vanishes for any circulation distribution, and the result is Prandtl's. + \item \emph{The section's own field is never added, so it never needs subtracting.} Panel $i$ stays where it is when the wing is straightened, so its own sheet appears identically on both sides and drops out; as a filament it induces nothing at its own line. The 2D self-induction the polar contains does not enter eq.~\eqref{eq:fix} anywhere. + \item \emph{Both logarithms are removed at once.} The bracket differences the whole vortex system, bound segments and legs, so the curvature logarithm of the bound line and the skew logarithm of the legs are both taken between sheets. The legs may therefore follow the ribs, the flow, or the perpendicular; the correction is the same construction for each. + \item \emph{Nothing singular meets anything singular.} Each sheet is finite at $\mathbf P_i$ with the cut-off of eq.~\eqref{eq:ceff}; the near neighbours, whose two positions almost coincide (Figure~\ref{fig:unroll}d), contribute a small difference, the far panels the whole curvature effect. The wake is identical in both wings and cancels. +\end{itemize} + +Why the baseline is Prandtl's filaments and not the straightened wing as sheets: both are finite, but a straight wing described by sheets is not Prandtl's wing, its collinear strips with varying $\Gamma_j$ and its legs starting along the chord induce a small extra velocity at the line, and the straight rectangular wing then comes out at 1.019 of the Trefftz drag instead of 1.000, with the same 1.5--2\,\% carried onto every curved case (Appendix~\ref{app:alt}). The difference form keeps the straight wing exact and lets the sheets act only on the curvature. + +The bracket depends on the panel geometry alone, so it is one influence matrix per shape (\texttt{assemble\_curved\_bound\_correction\_AIC}, about 1\,s at 80 panels), reused for every inflow and circulation, carried along under rigid rotation and translation of the body, and recomputed only when the shape deforms. The three-quarter-chord circulation solve, the attached-trailed force and the wake are untouched. + +\begin{figure}[p]\centering +\includegraphics[width=\textwidth]{14_unrolling_per_panel.png} +\caption{The straightened reference wing (\texttt{docs/validation/unrolling\_figure.py}). For an evaluation panel $i$ (star) the reference is built on panel $i$'s own tangent line through its aerodynamic centre: every node keeps its arc-length distance, the legs are perpendicular to the line in panel $i$'s chord direction with each panel's own chord, the wake keeps its direction. One reference per panel. (a, b) The arc wing with the references of a mid-span panel (blue) and a panel near the tip (red). (c) The kinked swept wing from above with the reference of a panel on the right half: the whole wing, the other half included, is laid along that panel's line. (d) Distance between the real and the straightened nodes against their arc-length distance from the evaluation panel, arc wing and V3: within a leg length the two systems coincide to second order, so the near field cancels in the difference and what remains is the far-field effect of the curvature, finite once both are spread over the chord.} +\label{fig:unroll} +\end{figure} + +\section{Results} +\label{sec:results} + +\subsection{Induced drag against the Trefftz plane} +Table~\ref{tab:fix} repeats Table~\ref{tab:conv} with eq.~\eqref{eq:fix} in place. The straight wing is exact, the arc and the V3 are within 1\,\% at every panel count, and nothing drifts. On the arc the chordwise self-induction that makes on-blade equal Trefftz is $u\approx-0.19$\,m/s, interpolated between the trailed-only and the on-line values; the sheets give $u=-0.186$\,m/s at every $N$ from 40 to 320 (Figure~\ref{fig:explain}b), with nothing tuned. + +\begin{table}[h]\centering\small +\caption{On-blade over Trefftz-plane induced drag with eq.~\eqref{eq:fix}, inviscid polars (\texttt{curved\_bound\_vortex\_validation.py}). Rib legs unless stated.} +\label{tab:fix} +\begin{tabular}{lccccc} +\toprule +case & $N=10$ & 20 & 40 & 80 & 160\\ +\midrule +straight rectangular, AR 8 & -- & -- & 1.000 & 1.000 & --\\ +straight, ribs skewed $22^\circ$ & -- & -- & 1.029 & 1.030 & --\\ +swept $30^\circ$, kinked, streamwise ribs & -- & -- & 0.971 & 0.982 & 0.988\\ +circular arc, $110^\circ$, $R=4c$ & 1.008 & 1.001 & 0.999 & 0.999 & 0.999\\ +elliptic, 10\,\% sweep, 25\,\% anhedral & -- & -- & 1.014 & 1.015 & 1.017\\ +elliptic, same, perpendicular legs & -- & -- & 1.025 & 1.024 & 1.020\\ +V3 kite & -- & 1.011 & 1.007 & 1.008 & 1.010\\ +V3 kite, perpendicular legs & -- & -- & 1.015 & 1.018 & crosses\\ +\bottomrule +\end{tabular} +\end{table} + +Two residuals remain and are not the bound vortex: the kinked swept wing converges to 0.99 and the straight wing with skewed ribs sits at 1.03 with or without the correction. Both come from the attached-trailed force on legs that are not aligned with the flow (streamwise and perpendicular legs return the skewed-rib wing to 1.000) and are a separate, smaller item. + +\subsection{Lift against a vortex lattice, and the direction of the legs} +\label{sec:legs} +The Trefftz plane checks the drag of a given circulation but cannot judge the lift. The vortex-lattice method of AeroSandbox~\cite{aerosandbox}, a lifting surface with 16 chordwise by 80--160 spanwise panels and flat-plate sections, was run on the same wings as the referee for the lift (\texttt{vlm\_referee.py}; its near-field drag is unreliable on non-planar wings, so its induced drag is taken from the Trefftz plane of its own circulation). The lattice resolves the attached trailed vorticity on the surface that the VSM lumps into one leg per section, so it also judges the leg direction, which the theory leaves open and~\cite{gaunaa2026} calls a modelling choice. Three constructions are implemented (\texttt{BodyAerodynamics(leg\_direction=...)}, Figure~\ref{fig:legs} in Appendix~\ref{app:legs}): along the input ribs (\texttt{rib}, the default), along the mean inflow projected on the surface (\texttt{streamwise}), and perpendicular to the quarter-chord line as in the paper (\texttt{perpendicular}, with the tangent averaged over one leg length so that the legs fan out instead of crossing at a kink). The correction of eq.~\eqref{eq:fix} follows whichever legs are built. + +\begin{table}[h]\centering\small +\caption{Lift and induced drag of the VSM with eq.~\eqref{eq:fix} relative to the vortex-lattice referee (VSM at $N=80$, elliptic $N=160$). Induced drag: VSM on-blade over VLM Trefftz. The V3 referee is approximate: AeroSandbox unskews the tip ribs when meshing.} +\label{tab:referee} +\begin{tabular}{lcc|cc|cc|cc} +\toprule + & \multicolumn{2}{c|}{VLM} & \multicolumn{2}{c|}{rib} & \multicolumn{2}{c|}{streamwise} & \multicolumn{2}{c}{perpendicular}\\ +case & $C_L$ & $C_{D_i}$ & $C_L$ & $C_{D_i}$ & $C_L$ & $C_{D_i}$ & $C_L$ & $C_{D_i}$\\ +\midrule +straight AR 8, $5^\circ$ & 0.402 & 0.00654 & $-0.2\,\%$ & $-0.9\,\%$ & $-0.2\,\%$ & $-0.9\,\%$ & $-0.2\,\%$ & $-0.9\,\%$\\ +yawed $30^\circ$, $5^\circ$ & 0.360 & 0.00537 & $0.0\,\%$ & $-0.6\,\%$ & $0.0\,\%$ & $-0.6\,\%$ & $+1.4\,\%$ & $+2.0\,\%$\\ +kinked swept $30^\circ$, $5^\circ$ & 0.363 & 0.00568 & $-0.4\,\%$ & $-3.0\,\%$ & $-0.4\,\%$ & $-3.0\,\%$ & $-8.8\,\%$ & $+6.7\,\%$\\ +arc $110^\circ$, $8^\circ$ & 0.577 & 0.01481 & $+0.3\,\%$ & $-3.4\,\%$ & $-2.1\,\%$ & $-1.2\,\%$ & $+0.3\,\%$ & $-3.4\,\%$\\ +elliptic, swept--anhedral, $10^\circ$ & 0.807 & 0.02643 & $+0.9\,\%$ & $-0.1\,\%$ & $+0.1\,\%$ & $-4.4\,\%$ & $+0.8\,\%$ & $-0.3\,\%$\\ +V3, flat plate, $10^\circ$ (approx.) & 0.624 & 0.02961 & $+2.3\,\%$ & $-4.0\,\%$ & $-1.9\,\%$ & $-8.6\,\%$ & $-7.9\,\%$ & $-4.5\,\%$\\ +\bottomrule +\end{tabular} +\end{table} + +Table~\ref{tab:referee} and Figure~\ref{fig:referee} give the result. With rib or streamwise legs the lift is within 1\,\% of the lattice on every wing it can judge properly and the induced drag within 3\,\%. The paper's perpendicular legs are fine on smooth wings and lose 8.8\,\% of the lift on the kinked wing and 7.9\,\% on the V3: at a kink, and at the skewed tips of a kite, the perpendicular legs fan out and reach up to $1.6\,c$ inboard, so the attached vorticity sits far from where the surface sheds it and the downwash at the neighbouring control points changes the circulation itself. They also cross at the V3 tips above 80 panels. The rib legs stay the default: a fixed vortex system, the same accuracy as the streamwise legs, and no dependence on a mean inflow direction, which the main use of the code, trim finding with a spanwise-varying inflow, does not have. The streamwise legs remain an option for uniform-inflow studies of wings whose sections are drawn obliquely to the flow or in sideslip; details, the V3 polars against RANS for the three options and the authors' own Trefftz numbers are in Appendix~\ref{app:legs}. + +\begin{figure}[p]\centering +\includegraphics[width=\textwidth]{11_leg_direction_referee.png} +\caption{Leg direction against the vortex-lattice referee and RANS. Top: strip circulation of the VLM and of the VSM with the three leg options on the kinked swept wing, the elliptic wing and the inviscid V3. Bottom left and middle: lift and on-blade induced drag of the VSM relative to the VLM. Bottom right: V3 viscous drag against RANS~\cite{poland2026} for the three options.} +\label{fig:referee} +\end{figure} + +\subsection{The V3 kite against RANS and the wind tunnel} +Figure~\ref{fig:polars} and Table~\ref{tab:polars} show the V3 (CAD geometry, 2D RANS polars at Re $5\times10^5$, 50 panels, rib legs) with the old three-quarter-chord directions, the quarter-chord directions with the bound segments on the line, and eq.~\eqref{eq:fix}, against RANS and the wind-tunnel data of Poland et al.~\cite{poland2026}. Lift is unchanged. The drag rises by a fraction of the induced drag, most at high lift and in sideslip, and lands on the RANS values up to $10^\circ$. The remaining gap to the wind tunnel is lift-dependent, $\Delta C_D\approx0.003+0.037\,C_L^2$, and is shared by RANS; Poland et al.\ attribute it to the model canopy, the flow behind the leading-edge tube and transition. It is not a lifting-line issue. The old three-quarter-chord directions appeared to close part of it only because they overestimate the induced drag by 20--40\,\%. + +\begin{table}[h]\centering\small +\caption{V3 kite drag coefficient, $\beta=0$.} +\label{tab:polars} +\begin{tabular}{cccccc} +\toprule +$\alpha$ & old 3/4-chord & bound on the line & eq.~\eqref{eq:fix} & RANS (Re $10^6$) & wind tunnel (Re $5\times10^5$)\\ +\midrule +$5^\circ$ & 0.061 & 0.054 & 0.055 & 0.052 ($4^\circ$) & 0.076 ($5.4^\circ$)\\ +$7^\circ$ & 0.075 & 0.063 & 0.065 & 0.064 & 0.090 ($7.3^\circ$)\\ +$9^\circ$ & 0.094 & 0.074 & 0.079 & 0.084 ($10^\circ$) & 0.107 ($9.4^\circ$)\\ +$13^\circ$ & 0.148 & 0.110 & 0.120 & 0.110 & 0.217 ($13.4^\circ$)\\ +\bottomrule +\end{tabular} +\end{table} + +\begin{figure}[p]\centering +\includegraphics[width=\textwidth]{09_v3_polars_curved_bound_vortex_fix.png} +\caption{V3 kite polars (\texttt{docs/figures/09}) with the old three-quarter-chord directions, the quarter-chord directions with the bound segments on the line, and eq.~\eqref{eq:fix}, against RANS and the wind-tunnel data of Poland et al.\ (1:6.5 rigid model, no bridles, open-jet corrections applied).} +\label{fig:polars} +\end{figure} + +\section{Open items} +\begin{itemize}[nosep] + \item To be confirmed with the authors of~\cite{gaunaa2026,li2020}: whether their quarter-chord term is the curved-minus-straight bound difference at the control point of Table~\ref{tab:hyp}, and how it behaves when the number of sections is doubled. + \item With body rates the apparent velocity is built at the control points and the same array feeds the quarter-chord direction and the attached-trailed force; the variation of the rotational term along the chord, $\boldsymbol\omega\times(\mathbf r_{1/4}-\mathbf r_{3/4})$, is the torsional-rate term of Li, Gaunaa and Pirrung~\cite{li2026wes}. On the V3 at 20\,m/s and 0.3\,rad/s of yaw the local angle error reaches $0.8^\circ$ at the tips, antisymmetric across the span, with 0.6\,\% on the total drag; the yawing moment is affected. Fix: evaluate the apparent velocity at the aerodynamic centre for the direction and at the leg points for the attached-trailed force. + \item The attached-trailed force on legs that are not aligned with the flow (kinked wing 0.98, skewed ribs 1.03, streamwise legs on the arc 1.035), and a V3 lift referee with the exact skewed tips. + \item The mechanism, straighten locally and keep the far field, is that of~\cite{reid2020}; the difference form goes back to~\cite{li2018}; the chordwise cut-off is the inner solution of~\cite{guermond1990}. What is new here is that the thin-airfoil loading fixes the cut-off without a parameter, that the legs are included in the difference so that any leg direction converges, and the demonstration on curved kites. Any publication should cite those three. +\end{itemize} + +\appendix +\section{Alternatives tested} +\label{app:alt} +All at the quarter-chord direction velocity, everything else unchanged; on-blade over Trefftz induced drag, inviscid polars, rib legs unless stated. +\begin{itemize}[nosep] + \item \emph{Li et al.\ 2020 literal} (3D bound minus own infinite 2D vortex at the control point, reused at the quarter chord): straight AR\,8 wing 0.848, AR\,4 wing 0.815, arc 0.84, elliptic with perpendicular legs 0.865, kinked swept wing $0.50\to0.35$. The curved-minus-straight reading is in Table~\ref{tab:hyp}. + \item \emph{Reid and Hunsaker} (effective lifting line blended over a distance, jointed legs of length $\delta c$), Table~\ref{tab:rh}: mesh-independent, induced drag depending on the two lengths by 17\,\% on the V3 and 12\,\% on the arc, and failing on the kinked wing with rib legs. + \item \emph{Each neighbour minus its own infinite line at the quarter chord}: the distance from $\mathbf P_i$ to a neighbour's line shrinks with the square of the arc-length separation, so the term grows like one over the panel width squared; arc mid-span sum $-27$, $-110$, $-448$, $-1805$ at 20 to 160 panels. Unusable. + \item \emph{Vortex core on the bound vortex}, with the code's Vatistas kernel: the projection of an on-line point to the core boundary produces spurious velocities of order $\Gamma/(4\pi\epsilon)$. A smooth core would converge to $\ln(L/\epsilon)$ with $\epsilon$ free. + \item \emph{The straightened wing as sheets instead of Prandtl} (all real sheets minus the section's own infinite 2D sheet): finite and convergent, straight rectangular wing 1.016/1.019 at 40/80 panels, skewed ribs 1.046/1.048, arc 1.013/1.014/1.015, elliptic 1.018/1.019, kinked swept 0.983/0.997; i.e.\ eq.~\eqref{eq:fix} plus a constant 1.5--2\,\% that has nothing to do with curvature. + \item \emph{Three-quarter-chord directions} (Weissinger, Ranneberg~\cite{ranneberg2015}, the code's default before September 2026): no singularity, induced drag 20--40\,\% high on the V3. \emph{Trefftz-plane drag only}: exact for the total in steady uniform inflow, unavailable with body rates or distributed inflow. +\end{itemize} + +\begin{table}[h]\centering\small +\caption{Reid--Hunsaker construction in the VSM, $N=80$: joint length and blending distance in local chords, except the MachUpX column (defaults: joint $0.15\,c$, blending one semispan). Last column: eq.~\eqref{eq:fix} at the same $N$.} +\label{tab:rh} +\begin{tabular}{lccccccc} +\toprule +case & 0.25\,/\,1 & 0.5\,/\,1 & 1.0\,/\,1 & 0.5\,/\,0.5 & 0.5\,/\,2 & MachUpX & eq.~\eqref{eq:fix}\\ +\midrule +straight AR 8 & 1.000 & 1.000 & 1.000 & 1.000 & 1.000 & 1.001 & 1.000\\ +kinked swept $30^\circ$, rib legs & 0.642 & 0.471 & 0.198 & 0.191 & 0.637 & 0.688 & 0.982\\ +arc, $R=4c$ & 1.049 & 1.025 & 1.004 & 1.003 & 1.052 & 1.117 & 0.999\\ +elliptic, swept--anhedral & 1.007 & 0.988 & 0.968 & 0.968 & 1.008 & 1.042 & 1.015\\ +V3 kite & 1.003 & 0.966 & 0.930 & 0.885 & 1.032 & 1.053 & 1.008\\ +\bottomrule +\end{tabular} +\end{table} + +\begin{table}[h]\centering\small +\caption{Why the three-quarter chord needs no sheet. Arc wing, mid-span panel, unit circulation on every other panel, bound segments as filaments: the normal component at the control point converges (the neighbours build up the infinite-line value $1/(2\pi\cdot c/2)=0.318$ that the own-vortex subtraction removes); the chordwise component at the quarter-chord point adds a constant per doubling.} +\label{tab:cpconv} +\begin{tabular}{lccccc} +\toprule +panels & 20 & 40 & 80 & 160 & 320\\ +\midrule +at the 3/4-chord control point, normal & 0.198 & 0.252 & 0.282 & 0.297 & 0.305\\ +at the 1/4-chord point, chordwise & 0.075 & 0.089 & 0.103 & 0.117 & 0.130\\ +\bottomrule +\end{tabular} +\end{table} + +\section{Sensitivity to the chordwise loading of the strips} +\label{app:strips} +In thin-airfoil terms a section's bound vorticity is its pressure jump, $\gamma(x)\propto\Delta C_p(x)$, so a real section loads differently from the flat plate. Table~\ref{tab:strips} replaces the strip weights by the NeuralFoil loading of the V3 centre section and of a NACA~4412 (\texttt{strip\_weight\_sensitivity.py}). The LEI profile is nose-heavier, its cut-off falls from $0.16\,c$ to $0.13\,c$, and its centroid sits aft of the quarter chord because of the camber. The arc moves by at most 0.3\,\%, the V3 by 0.5--1.5\,\% of the induced drag ($\Delta C_D\le0.0005$ at $10^\circ$); the moments change by 0.3\,\% or less. Since the loading depends on the angle of attack, using it would make the correction depend on the solution and cost the per-shape cache. The flat-plate weights are kept. + +\begin{table}[h]\centering\small +\caption{Strip weights from real section loadings: effective cut-off, centroid, and on-blade over Trefftz induced drag. NeuralFoil samples the chord from $x/c=0.016$, so the leading-edge peak is partly missed and the centroids are a few percent of chord too far aft.} +\label{tab:strips} +\begin{tabular}{lcccccc} +\toprule +strip loading & $c_{\mathrm{eff}}/c$ & centroid & arc 40 & arc 80 & V3 40 & V3 80\\ +\midrule +flat plate, thin airfoil (implemented) & 0.158 & 0.250 & 0.999 & 0.999 & 1.007 & 1.008\\ +V3 centre section, $5^\circ$ & 0.138 & 0.311 & 1.003 & 1.002 & 1.020 & 1.022\\ +V3 centre section, $10^\circ$ & 0.127 & 0.287 & 0.999 & 0.998 & 1.013 & 1.015\\ +V3 centre section, $15^\circ$ & 0.133 & 0.288 & 1.000 & 0.999 & 1.013 & 1.014\\ +NACA 4412, $10^\circ$ & 0.141 & 0.300 & 1.003 & 1.002 & 1.022 & 1.024\\ +\bottomrule +\end{tabular} +\end{table} + +\section{The leg study in detail} +\label{app:legs} +\emph{Constructions.} \texttt{rib}: along the interpolated input sections to their trailing-edge points, control point on the section at the control-point chord fraction; on a kite the ribs of the real wing, on the test wings drawn along the flow (swept wings), along the chord (arc) or deliberately skewed (straight wing). \texttt{streamwise}: along the mean apparent velocity projected onto the local surface up to the trailing-edge curve, rebuilt whenever the inflow is set; with a spanwise-varying inflow only the mean direction is available, and a pure rotation falls back to the ribs with a warning. \texttt{perpendicular}: the paper's choice, the tangent of the quarter-chord line averaged over plus and minus one leg length before the perpendicular is taken, legs that still cross raising an error. In the last two the leg ends at the point of the trailing-edge curve closest to the leg's ray, the curve extended beyond its ends, and the control point moves with the legs so that it stays between the panel's own two legs (without this the control points of a kinked wing land on neighbouring legs and the solve diverges). + +\begin{figure}[p]\centering +\includegraphics[width=\textwidth]{10_leg_direction_geometry.png} +\caption{The three leg constructions. Top views (flow downwards) and one side view, with the bound vortex (black), the aerodynamic centres (dots), the control points (crosses in the colour of their legs) and, in grey, the lattice of the vortex-lattice referee. (a) A straight wing whose ribs are drawn $22^\circ$ skewed. (b) The root of the kinked swept wing: streamwise ribs against the perpendicular fan. (c) The swept--anhedral elliptic wing. (d) The arc wing from the side: streamwise legs climb along the nearly vertical tip at the angle of attack. (e, f) The V3: the ribs are nearly streamwise over the middle and skewed at the tips, where the perpendicular legs become long and end up to $1.6\,c$ inboard of the section's own trailing-edge point.} +\label{fig:legs} +\end{figure} + +\begin{table}[h]\centering\small +\caption{Top: on-blade over Trefftz induced drag with the attached-trailed force, $N=80$ (\texttt{leg\_direction\_study.py}). Bottom: V3 viscous polars (CAD geometry, 2D RANS polars, 50 panels) against RANS~\cite{poland2026}, $1$--$13^\circ$; the drag error up to $10^\circ$ in brackets.} +\label{tab:legs3} +\begin{tabular}{lccc} +\toprule + & rib & streamwise & perpendicular\\ +\midrule +straight AR 8 & 1.000 & 1.000 & 1.000\\ +straight, ribs skewed $22^\circ$ & 1.030 & 1.000 & 1.000\\ +yawed $30^\circ$ & 1.002 & 1.002 & 0.991\\ +kinked swept $30^\circ$ & 0.982 & 0.982 & 1.093\\ +circular arc & 0.999 & 1.035 & 0.999\\ +elliptic, swept--anhedral & 1.015 & 0.986 & 1.024\\ +V3 kite, $10^\circ$ & 1.008 & 1.008 & 1.018 (crosses at $N=160$)\\ +\midrule +V3 mean $|\Delta C_L|$ & 0.020 & 0.019 & 0.058\\ +V3 mean $|\Delta C_D|/C_D$ & 3.0\,\% (1.4\,\%) & 2.2\,\% (0.3\,\%) & 3.5\,\% (2.5\,\%)\\ +\bottomrule +\end{tabular} +\end{table} + +\emph{What the referee says.} On the planar wings the streamwise legs, which coincide with the ribs there, reproduce the lattice lift to 0.4\,\% and the induced drag within 3\,\%; the perpendicular legs are 1.4\,\% high on the yawed wing and 8.8\,\% low on the kinked one. A perpendicular lattice is a sheared Weissinger lattice; at a kink the fan of legs changes the downwash at the neighbouring control points, and even without a kink the sheared lattice is not the better discretisation. On the arc the rib and perpendicular legs coincide and are within 0.3\,\% in lift; the streamwise legs, climbing along the vertical tips, are 2.1\,\% low at $8^\circ$ and 3\,\% at $15^\circ$. On the elliptic wing all three are within 1\,\%. On the V3 the approximate referee puts the rib legs 2.3\,\% high and the streamwise legs 1.9\,\% low; against RANS the streamwise legs give the closest polars (drag within 0.3\,\% up to $10^\circ$, lift slope 0.065 per degree matching RANS, the ribs 0.078). The skewed-rib straight wing shows what the rib legs cost when the ribs are not aligned with the flow: 1.03 of the Trefftz drag and 0.7\,\% less lift, whereas streamwise and perpendicular legs return the straight wing exactly. + +\emph{The authors' numbers.} For the swept--anhedral elliptic wing Table~2 of~\cite{gaunaa2026} gives $e_{\mathrm{Osw,Blade}}=1.101$ against $e_{\mathrm{Osw,Trefftz}}=1.080$ for LL-Gaunaa, on-blade induced drag 1.9\,\% below the Trefftz plane, and their VLM reference 2.8\,\% below; the appendix~\cite{appendix2026} gives 5\,\% and 1\,\% below for their 15\,\% and 25\,\% swept wings. Here the near-field drag of the vortex lattice is likewise below its own Trefftz drag on the non-planar wings (8\,\% on the elliptic wing), for the same reason: trailing legs that are not aligned with the wind carry a force that the near-field evaluation drops. + +\section{What the code implements of Li and Gaunaa} +\label{app:status} +Since September 2026 (commits \texttt{233a238}\ldots\texttt{62c53af}) the code implements, of~\cite{gaunaa2026,li2026}: CP1, the inner 2D velocity and chord perpendicular to the local quarter-chord line; TAT1, the section's own 2D bound-vortex induction subtracted at the three-quarter-chord point; TAT2, force magnitudes from the three-quarter-chord angle of attack; TAT3, force directions from the quarter-chord flow (\texttt{is\_aoa\_corrected=True}, the default); the attached-trailed force $\rho\,\Delta\Gamma_j\,\mathbf V_{3D,\mathrm{rel},j}\times\mathbf l_{AT,j}$ at the three-quarter point of the leg; the artificial viscosity with tip closures and the relaxation bound of~\cite{li2026}, the latter halved for the VSM after measurement of the fixed-point Jacobian. Two elements differ from the published prescription and are the subject of this document: the curved bound-vortex term at the quarter chord (Sections~\ref{sec:lit} and~\ref{sec:fix}) and the direction of the legs (Section~\ref{sec:legs}). + +\begin{thebibliography}{99}\small +\bibitem{gaunaa2026} M.~Gaunaa, A.~Li, G.~R.~Pirrung, From thin airfoil theory to finite wings: consistent implementation of Lifting Line-based models, J.\ Phys.: Conf.\ Ser.\ 3224 (2026) 042030. +\bibitem{appendix2026} M.~Gaunaa, A.~Li, G.~R.~Pirrung, Internet appendix for ``From thin airfoil theory to finite wings'', Zenodo, doi:10.5281/zenodo.18222326 (2026). +\bibitem{li2026} A.~Li, M.~Gaunaa, G.~R.~Pirrung, K.~L{\o}nb{\ae}k, Relaxation limits and artificial viscosity for lifting line methods, TORQUE 2026. +\bibitem{li2026wes} A.~Li, M.~Gaunaa, G.~R.~Pirrung, Unsteady airfoil aerodynamics in attached flow: from unsteady thin-airfoil theory to wind turbine implementation, Wind Energ.\ Sci.\ Discuss., doi:10.5194/wes-2026-168 (2026). Reference implementation: doi:10.5281/zenodo.22793702. +\bibitem{li2020} A.~Li, M.~Gaunaa, G.~R.~Pirrung, N.~Ramos-Garc\'ia, S.~G.~Horcas, The influence of the bound vortex on the aerodynamics of curved wind turbine blades, J.\ Phys.: Conf.\ Ser.\ 1618 (2020) 052038. +\bibitem{li2018} A.~Li, G.~Pirrung, H.~Aa.~Madsen, M.~Gaunaa, F.~Zahle, Fast trailed and bound vorticity modeling of swept wind turbine blades, J.\ Phys.: Conf.\ Ser.\ 1037 (2018) 062012. +\bibitem{reid2020} J.~T.~Reid, D.~F.~Hunsaker, A general approach to lifting-line theory, applied to wings with sweep, J.\ Aircraft 58 (2021), doi:10.2514/1.C035994. +\bibitem{goates2023} C.~D.~Goates, D.~F.~Hunsaker, Modern implementation and evaluation of lifting-line theory for complex geometries, J.\ Aircraft (2023), doi:10.2514/1.C036748. Code: \url{https://github.com/usuaero/MachUpX}. +\bibitem{guermond1990} J.-L.~Guermond, A generalized lifting-line theory for curved and swept wings, J.\ Fluid Mech.\ 211 (1990) 497--513. +\bibitem{phillips2000} W.~F.~Phillips, D.~O.~Snyder, Modern adaptation of Prandtl's classic lifting-line theory, J.\ Aircraft 37 (2000) 662--670. +\bibitem{ranneberg2015} M.~Ranneberg, Direct wing design and inverse airfoil identification with the nonlinear Weissinger method, arXiv:1501.04983 (2015). +\bibitem{aerosandbox} P.~D.~Sharpe, AeroSandbox: a differentiable framework for aircraft design optimization, MIT (2021), \url{https://github.com/peterdsharpe/AeroSandbox}; version 4.2.8, \texttt{VortexLatticeMethod}. +\bibitem{poland2026} J.~A.~W.~Poland, J.~M.~van Spronsen, M.~Gaunaa, R.~Schmehl, Wind tunnel load measurements of a leading-edge inflatable kite rigid-scale model, Wind Energ.\ Sci.\ 11 (2026) 911. +\end{thebibliography} + +\end{document} diff --git a/docs/validation/curved_bound_vortex_validation.json b/docs/validation/curved_bound_vortex_validation.json new file mode 100644 index 0000000..016c95e --- /dev/null +++ b/docs/validation/curved_bound_vortex_validation.json @@ -0,0 +1,1105 @@ +{ + "variants": { + "straight AR 8 | regularised (this work)": [ + [ + 40, + 1.0000241613419563, + 0.4037683141001728 + ], + [ + 80, + 0.9999993387640963, + 0.4007293936922778 + ] + ], + "straight AR 8 | bound segments on the line (develop before)": [ + [ + 40, + 1.0000241613419556, + 0.4037683141001728 + ], + [ + 80, + 0.9999993387640965, + 0.4007293936922778 + ] + ], + "straight AR 8 | trailed only (no bound term)": [ + [ + 40, + 1.0000241613419556, + 0.4037683141001728 + ], + [ + 80, + 0.9999993387640965, + 0.4007293936922778 + ] + ], + "straight AR 8 | Li 2020 literal (3/4c difference reused)": [ + [ + 40, + 0.8458999525728195, + 0.4037854100744974 + ], + [ + 80, + 0.8485987375268363, + 0.40074600569932317 + ] + ], + "straight, ribs skewed 22 deg | regularised (this work)": [ + [ + 40, + 1.0294871336786302, + 0.40111700937128875 + ], + [ + 80, + 1.0297742412007151, + 0.39805783530229416 + ] + ], + "straight, ribs skewed 22 deg | bound segments on the line (develop before)": [ + [ + 40, + 1.029151614328891, + 0.4011017610348767 + ], + [ + 80, + 1.0294531671672196, + 0.3980296482507103 + ] + ], + "straight, ribs skewed 22 deg | trailed only (no bound term)": [ + [ + 40, + 1.029151614328891, + 0.4011017610348767 + ], + [ + 80, + 1.0294531671672196, + 0.3980296482507103 + ] + ], + "straight, ribs skewed 22 deg | Li 2020 literal (3/4c difference reused)": [ + [ + 40, + 0.8633903274403826, + 0.40110456156770546 + ], + [ + 80, + 0.8664582527511719, + 0.3980288000994686 + ] + ], + "swept 30 deg, streamwise ribs | regularised (this work)": [ + [ + 40, + 0.9710106496264701, + 0.3644656343303096 + ], + [ + 80, + 0.9818703531144978, + 0.36161543443795746 + ], + [ + 160, + 0.9879834819028264, + 0.36020744818304334 + ] + ], + "swept 30 deg, streamwise ribs | bound segments on the line (develop before)": [ + [ + 40, + 0.9391569973868759, + 0.3644214813758505 + ], + [ + 80, + 0.9402284249483108, + 0.3614922461455458 + ], + [ + 160, + 0.9362144447170302, + 0.3599243258232737 + ] + ], + "swept 30 deg, streamwise ribs | trailed only (no bound term)": [ + [ + 40, + 0.3531144221837756, + 0.36442458336836786 + ], + [ + 80, + 0.2759112958084285, + 0.36156098680356485 + ], + [ + 160, + 0.19707600577127668, + 0.3601299242002165 + ] + ], + "swept 30 deg, streamwise ribs | Li 2020 literal (3/4c difference reused)": [ + [ + 40, + 0.44834384387512616, + 0.3643979713151609 + ], + [ + 80, + 0.37690036537429855, + 0.36152546053802814 + ], + [ + 160, + 0.3000084613314567, + 0.3600850809036525 + ] + ], + "arc 110 deg, R = 4c | regularised (this work)": [ + [ + 20, + 1.0008104829418196, + 0.5865492489174156 + ], + [ + 40, + 0.9991128646767465, + 0.5814516426807416 + ], + [ + 80, + 0.9989015501879521, + 0.5787728279860994 + ], + [ + 160, + 0.9992644225273919, + 0.5773969145630061 + ] + ], + "arc 110 deg, R = 4c | bound segments on the line (develop before)": [ + [ + 20, + 0.977444009145902, + 0.5865619807751812 + ], + [ + 40, + 0.9574315377305134, + 0.5814743412337862 + ], + [ + 80, + 0.937182557714954, + 0.5788063070110815 + ], + [ + 160, + 0.9167178901755672, + 0.5774414578038963 + ] + ], + "arc 110 deg, R = 4c | trailed only (no bound term)": [ + [ + 20, + 1.080559238481663, + 0.5865028003556394 + ], + [ + 40, + 1.0793162932537483, + 0.5814049495121314 + ], + [ + 80, + 1.078640326402641, + 0.5787264175444475 + ], + [ + 160, + 1.0782843407724565, + 0.5773509218125135 + ] + ], + "arc 110 deg, R = 4c | Li 2020 literal (3/4c difference reused)": [ + [ + 20, + 0.8323148570519505, + 0.5866704645456157 + ], + [ + 40, + 0.8383166941392419, + 0.5815649406944821 + ], + [ + 80, + 0.8403121891972414, + 0.5788832886144405 + ], + [ + 160, + 0.84089870032553, + 0.5775066468505359 + ] + ], + "elliptic, 10% sweep, 25% anhedral | regularised (this work)": [ + [ + 40, + 1.013843700029167, + 0.8167899906598163 + ], + [ + 80, + 1.0153496307985954, + 0.8151570220744624 + ], + [ + 160, + 1.0165458901739322, + 0.814536072602332 + ] + ], + "elliptic, 10% sweep, 25% anhedral | bound segments on the line (develop before)": [ + [ + 40, + 0.9798851005352586, + 0.8167912476658324 + ], + [ + 80, + 0.9671126383468862, + 0.8151595572045376 + ], + [ + 160, + 0.9543587298350683, + 0.8145350766983026 + ] + ], + "elliptic, 10% sweep, 25% anhedral | trailed only (no bound term)": [ + [ + 40, + 1.0043532897585281, + 0.8167453616403155 + ], + [ + 80, + 0.9951184673336272, + 0.8150965225801483 + ], + [ + 160, + 0.9864727035821242, + 0.8144503559388052 + ] + ], + "elliptic, 10% sweep, 25% anhedral | Li 2020 literal (3/4c difference reused)": [ + [ + 40, + 0.8227774187050402, + 0.8168824225158383 + ], + [ + 80, + 0.8169812200094699, + 0.8152287867504544 + ], + [ + 160, + 0.8101839010880149, + 0.8145758225334953 + ] + ], + "V3 kite, inviscid, 10 deg | regularised (this work)": [ + [ + 20, + 1.00662934510494, + 0.6402568220108974 + ], + [ + 40, + 1.0072015329480584, + 0.6393171088835797 + ], + [ + 80, + 1.008284529459178, + 0.6385107288798224 + ], + [ + 160, + 1.009664834821042, + 0.6379759739912666 + ] + ], + "V3 kite, inviscid, 10 deg | bound segments on the line (develop before)": [ + [ + 20, + 0.9328730896931746, + 0.6399589430996667 + ], + [ + 40, + 0.9070517786630374, + 0.6388697935539668 + ], + [ + 80, + 0.8806687904134649, + 0.6379341485255687 + ], + [ + 160, + 0.854124961444298, + 0.6372508424707297 + ] + ], + "V3 kite, inviscid, 10 deg | trailed only (no bound term)": [ + [ + 20, + 0.8646897514089346, + 0.6396827548606758 + ], + [ + 40, + 0.8379612436635758, + 0.6385482738092294 + ], + [ + 80, + 0.8068376569701363, + 0.637559585665145 + ], + [ + 160, + 0.7735896986582989, + 0.6368220051830247 + ] + ], + "V3 kite, inviscid, 10 deg | Li 2020 literal (3/4c difference reused)": [ + [ + 20, + 0.5968289789400932, + 0.6396337105817096 + ], + [ + 40, + 0.564925798861799, + 0.638585071703887 + ], + [ + 80, + 0.5312959744943544, + 0.6376352176153206 + ], + [ + 160, + 0.4964053087095514, + 0.636907566254002 + ] + ] + }, + "arc_self_induction": { + "N": [ + 10, + 20, + 40, + 80, + 160, + 320 + ], + "polyline": [ + -0.19336551898010107, + -0.24424550999021308, + -0.29382857010626906, + -0.3430880581247514, + -0.39216146038410227, + -0.4412183727952242 + ], + "sheet": [ + -0.16631261468009004, + -0.1822061967093731, + -0.18569605309833728, + -0.18606433707392606, + -0.18510537036639624, + -0.18385689607095052 + ] + }, + "reid_hunsaker": { + "straight AR 8 | RH 0.25/1.0": [ + [ + 80, + 1.0004419247150802 + ] + ], + "straight AR 8 | RH 0.5/1.0": [ + [ + 80, + 1.0001976697955677 + ] + ], + "straight AR 8 | RH 1.0/1.0": [ + [ + 80, + 0.9998342697137015 + ] + ], + "straight AR 8 | RH 0.5/0.5": [ + [ + 80, + 1.000197669795569 + ] + ], + "straight AR 8 | RH 0.5/2.0": [ + [ + 80, + 1.0001976697955683 + ] + ], + "straight AR 8 | RH MachUpX defaults": [ + [ + 80, + 1.0005558094824771 + ] + ], + "swept 30 deg, streamwise ribs | RH 0.25/1.0": [ + [ + 80, + 0.6424804083474445 + ] + ], + "swept 30 deg, streamwise ribs | RH 0.5/1.0": [ + [ + 80, + 0.47064639031429134 + ] + ], + "swept 30 deg, streamwise ribs | RH 1.0/1.0": [ + [ + 80, + 0.1984615436244586 + ] + ], + "swept 30 deg, streamwise ribs | RH 0.5/0.5": [ + [ + 80, + 0.1913572331483738 + ] + ], + "swept 30 deg, streamwise ribs | RH 0.5/2.0": [ + [ + 80, + 0.6367948714596088 + ] + ], + "swept 30 deg, streamwise ribs | RH MachUpX defaults": [ + [ + 80, + 0.6875925480018436 + ] + ], + "arc 110 deg, R = 4c | RH 0.25/1.0": [ + [ + 80, + 1.048918171617377 + ] + ], + "arc 110 deg, R = 4c | RH 0.5/1.0": [ + [ + 80, + 1.0254871773893823 + ] + ], + "arc 110 deg, R = 4c | RH 1.0/1.0": [ + [ + 80, + 1.0035006961079103 + ] + ], + "arc 110 deg, R = 4c | RH 0.5/0.5": [ + [ + 80, + 1.0029454526236823 + ] + ], + "arc 110 deg, R = 4c | RH 0.5/2.0": [ + [ + 80, + 1.0524338205822277 + ] + ], + "arc 110 deg, R = 4c | RH MachUpX defaults": [ + [ + 80, + 1.1171681398116078 + ] + ], + "elliptic, 10% sweep, 25% anhedral | RH 0.25/1.0": [ + [ + 80, + 1.0072993169489264 + ] + ], + "elliptic, 10% sweep, 25% anhedral | RH 0.5/1.0": [ + [ + 80, + 0.9883264872063175 + ] + ], + "elliptic, 10% sweep, 25% anhedral | RH 1.0/1.0": [ + [ + 80, + 0.9684249560143255 + ] + ], + "elliptic, 10% sweep, 25% anhedral | RH 0.5/0.5": [ + [ + 80, + 0.9675468710177102 + ] + ], + "elliptic, 10% sweep, 25% anhedral | RH 0.5/2.0": [ + [ + 80, + 1.0078162439072207 + ] + ], + "elliptic, 10% sweep, 25% anhedral | RH MachUpX defaults": [ + [ + 80, + 1.0421799438268862 + ] + ], + "V3 kite, inviscid, 10 deg | RH 0.25/1.0": [ + [ + 80, + 1.002952535541019 + ] + ], + "V3 kite, inviscid, 10 deg | RH 0.5/1.0": [ + [ + 80, + 0.965772725301266 + ] + ], + "V3 kite, inviscid, 10 deg | RH 1.0/1.0": [ + [ + 80, + 0.930287487105459 + ] + ], + "V3 kite, inviscid, 10 deg | RH 0.5/0.5": [ + [ + 80, + 0.885374811185594 + ] + ], + "V3 kite, inviscid, 10 deg | RH 0.5/2.0": [ + [ + 80, + 1.0324553736619866 + ] + ], + "V3 kite, inviscid, 10 deg | RH MachUpX defaults": [ + [ + 80, + 1.0532331500880745 + ] + ] + }, + "v3_polars": { + "alpha": { + "old34": [ + [ + -3, + -0.09041034072428804, + 0.06472611799274106 + ], + [ + -1, + 0.05331716412927477, + 0.05433426295459796 + ], + [ + 1, + 0.20512145329213485, + 0.05010145238996536 + ], + [ + 3, + 0.37302423595640954, + 0.05206387834521813 + ], + [ + 5, + 0.5403289168265447, + 0.06087519667477277 + ], + [ + 7, + 0.6953383553729355, + 0.07531352524971663 + ], + [ + 9, + 0.8438251741514005, + 0.09428486343752125 + ], + [ + 10, + 0.9142575301356946, + 0.10526978067219678 + ], + [ + 11, + 0.9811280157500538, + 0.11705744624896189 + ], + [ + 12, + 1.0440578642166516, + 0.12954827830360136 + ], + [ + 13, + 1.0471970437475342, + 0.14780305141092046 + ], + [ + 14, + 1.0216963120109757, + 0.16593348952861184 + ], + [ + 15, + 1.0229178003365564, + 0.1832570850555361 + ], + [ + 17, + 1.0252315966294687, + 0.21775767826781292 + ], + [ + 19, + 1.0077220775553686, + 0.2507871475900853 + ] + ], + "on_line": [ + [ + -3, + -0.09051973142396862, + 0.06457080633396048 + ], + [ + -1, + 0.05335120282131424, + 0.054268343196041235 + ], + [ + 1, + 0.20514152745649994, + 0.04934202378153214 + ], + [ + 3, + 0.37291472262129177, + 0.049235358555732474 + ], + [ + 5, + 0.5400824633035918, + 0.05423833748793673 + ], + [ + 7, + 0.6950997805357032, + 0.06309938069322801 + ], + [ + 9, + 0.8438488754435933, + 0.07442135443403403 + ], + [ + 10, + 0.914544856310683, + 0.08080903799755794 + ], + [ + 11, + 0.981783014673668, + 0.08753029683841891 + ], + [ + 12, + 1.0451946261218334, + 0.09452605563822877 + ], + [ + 13, + 1.0488262405179472, + 0.10958953090923362 + ], + [ + 14, + 1.0239866830612823, + 0.12628182852661743 + ], + [ + 15, + 1.0255964963593582, + 0.14219070827698804 + ], + [ + 17, + 1.0292684639434164, + 0.1726512367908856 + ], + [ + 19, + 1.0142338747832476, + 0.20173713136942983 + ] + ], + "fix": [ + [ + -3, + -0.09049448678566321, + 0.06457803440736518 + ], + [ + -1, + 0.05335324758607815, + 0.054267521026767865 + ], + [ + 1, + 0.20522849223446463, + 0.04940356675525017 + ], + [ + 3, + 0.37319683424561395, + 0.049596324590352986 + ], + [ + 5, + 0.5406258812961813, + 0.05531335569769452 + ], + [ + 7, + 0.6958909827320242, + 0.0654402632519665 + ], + [ + 9, + 0.8448280624791582, + 0.07875484900935512 + ], + [ + 10, + 0.9155759672742243, + 0.08645229846401946 + ], + [ + 11, + 0.9828273030914096, + 0.0947042029577697 + ], + [ + 12, + 1.0462066294215902, + 0.10345273509472067 + ], + [ + 13, + 1.049428438693311, + 0.1203497121582245 + ], + [ + 14, + 1.0242189750280708, + 0.1383596888471699 + ], + [ + 15, + 1.0257168660270177, + 0.1546615362575407 + ], + [ + 17, + 1.0289523469718433, + 0.18679413441348552 + ], + [ + 19, + 1.0129500886068572, + 0.2183393271727618 + ] + ] + }, + "beta": { + "old34": [ + [ + 0, + 0.7256703802979119, + 0.07875976421848799, + -6.105387872822413e-17 + ], + [ + 4, + 0.722081450404687, + 0.08199552420157269, + 0.07396920787713378 + ], + [ + 8, + 0.7084882817169895, + 0.0925990676421757, + 0.14507346592691406 + ], + [ + 12, + 0.6861106044313405, + 0.11047774279148943, + 0.20777528964547087 + ], + [ + 14, + 0.6597039358263193, + 0.12226136404387439, + 0.2270937850205488 + ], + [ + 20, + 0.5742126847549058, + 0.16106222169555912, + 0.2646114845244913 + ] + ], + "on_line": [ + [ + 0, + 0.7254602401975874, + 0.06518415424580457, + -4.5790409046168096e-17 + ], + [ + 4, + 0.7214414420978781, + 0.06695960822667764, + 0.07482470104974061 + ], + [ + 8, + 0.7070378615749499, + 0.07336714440318456, + 0.14750256706689863 + ], + [ + 12, + 0.6837980250008591, + 0.08498294688814703, + 0.21258767954984878 + ], + [ + 14, + 0.6566954564747162, + 0.0942932190795109, + 0.23305313129316121 + ], + [ + 20, + 0.5700919743462102, + 0.1270372760027923, + 0.2746187928531166 + ] + ], + "fix": [ + [ + 0, + 0.7262955944038266, + 0.06786039332724632, + -6.575033093808751e-17 + ], + [ + 4, + 0.7224816977165603, + 0.07004494818887488, + 0.07487310470520697 + ], + [ + 8, + 0.7085180321307066, + 0.07753958803375395, + 0.14727359197184037 + ], + [ + 12, + 0.6857549365272401, + 0.09070057365341228, + 0.21176274133385709 + ], + [ + 14, + 0.658883436911058, + 0.1007798798944297, + 0.23198473902143452 + ], + [ + 20, + 0.5726101023045227, + 0.1354288182587225, + 0.27254612505155157 + ] + ] + } + }, + "hypothesis": { + "straight AR 8 | rib": [ + [ + 40, + 1.000024161341956 + ], + [ + 80, + 0.9999993387640974 + ] + ], + "straight AR 8 | perpendicular": [ + [ + 40, + 1.0000241613419563 + ], + [ + 80, + 0.9999993387640969 + ] + ], + "arc 110 deg, R = 4c | rib": [ + [ + 20, + 0.9919889241351971 + ], + [ + 40, + 0.9929117510746198 + ], + [ + 80, + 0.9932188147962667 + ], + [ + 160, + 0.9933325347824923 + ] + ], + "arc 110 deg, R = 4c | perpendicular": [ + [ + 20, + 0.9919889241351972 + ], + [ + 40, + 0.9929117510746205 + ], + [ + 80, + 0.9932188147962674 + ], + [ + 160, + 0.993332534782493 + ] + ], + "elliptic, 10% sweep, 25% anhedral | rib": [ + [ + 40, + 0.9932342067650138 + ], + [ + 80, + 0.9858651181185718 + ], + [ + 160, + 0.9785889361575972 + ] + ], + "elliptic, 10% sweep, 25% anhedral | perpendicular": [ + [ + 40, + 1.0199567023879592 + ], + [ + 80, + 1.0168719114436031 + ], + [ + 160, + 1.0119122471412831 + ] + ], + "V3 kite, inviscid, 10 deg | rib": [ + [ + 40, + 0.8091027651645845 + ], + [ + 80, + 0.7720494570082395 + ], + [ + 160, + 0.7353715823776639 + ] + ], + "V3 kite, inviscid, 10 deg | perpendicular": [ + [ + 40, + 1.0062872487784804 + ], + [ + 80, + 1.014504918905616 + ], + [ + 160, + NaN + ] + ], + "swept 30 deg (kinked) | rib": [ + [ + 40, + 0.9077132879044357 + ], + [ + 80, + 0.8395483504523779 + ] + ], + "swept 30 deg (kinked) | perpendicular": [ + [ + 40, + 1.128765146083736 + ], + [ + 80, + 1.138138258123495 + ] + ] + } +} \ No newline at end of file diff --git a/docs/validation/curved_bound_vortex_validation.py b/docs/validation/curved_bound_vortex_validation.py new file mode 100644 index 0000000..9f7e3c2 --- /dev/null +++ b/docs/validation/curved_bound_vortex_validation.py @@ -0,0 +1,638 @@ +"""Validation of the curved bound-vortex regularisation at the quarter chord. + +Reproduces the tables and figures of docs/validation/curved_bound_vortex_report.tex: + +* on-blade induced drag over Trefftz-plane induced drag, inviscid polars, as a + function of the number of spanwise panels, for a straight wing, a straight + wing with skewed ribs, a 30 deg swept wing with streamwise ribs, a circular + arc wing, the swept-anhedral elliptic wing of Gaunaa, Li & Pirrung (TORQUE + 2026) and the TU Delft V3 kite; +* the same ratio for the alternatives that were tried and rejected: no bound + term at the quarter chord (trailed system only), the other panels' bound + segments evaluated on the line (Phillips & Snyder, "LL-P&S" in Li et al. + 2020; the develop branch before this work), the literal Li et al. 2020 + recipe (3D minus 2D bound induction at the 3/4 chord reused at the quarter + chord) and the effective-lifting-line / jointed-vortex construction of Reid + & Hunsaker (2020) for several joint lengths and blending distances; +* the chordwise self-induction at the mid-span of the arc from the bound + polyline (log-divergent) and from the chordwise-spread sheet (finite); +* the V3 viscous polars (figure 09) and the mechanism figure (figure 08). + +Every alternative is a drop-in replacement of +``assemble_curved_bound_correction_AIC`` installed by monkeypatching the name +in ``VSM.core.BodyAerodynamics``; the solver is otherwise unchanged. + +Usage (from the repository root; the full run takes about 20 minutes, most +of it in the pure-Python Reid-Hunsaker loops):: + + python docs/validation/curved_bound_vortex_validation.py # tables + python docs/validation/curved_bound_vortex_validation.py --quick # regularised + on-line + none only + python docs/validation/curved_bound_vortex_validation.py --figures # tables + figures 08, 09 +""" + +from __future__ import annotations + +import argparse +import json +import logging +import sys +import time +from pathlib import Path + +import numpy as np +import pandas as pd +import yaml + +import VSM.core.BodyAerodynamics as BA +from VSM.core.BodyAerodynamics import BodyAerodynamics +from VSM.core.Solver import Solver +from VSM.core.WingGeometry import Wing +from VSM.core.utils import ( + _vel_bound_vortex, + _vel_semiinfinite, + _vel_trailing_vortex, + thin_airfoil_strips, +) + +logging.disable(logging.CRITICAL) +ROOT = Path(__file__).resolve().parents[2] +OUT = Path(__file__).resolve().parent +FIG = ROOT / "docs" / "figures" +DATA = ROOT / "data" / "TUDELFT_V3_KITE" +REGULARISED = BA.assemble_curved_bound_correction_AIC +# The geometries below use rib legs unless told otherwise: the tables of the +# report's Sections 2-4 isolate the quarter-chord bound-vortex term and were +# made with rib legs (the default until October 2026). The leg direction is +# compared separately in leg_direction_study.py and vlm_referee.py. + +_a = np.radians(np.linspace(-25, 25, 101)) +INVISCID = np.column_stack((_a, 2 * np.pi * _a, 0 * _a, 0 * _a)) + + +# -------------------------------------------------------------------------- +# Geometries +# -------------------------------------------------------------------------- +def straight(n, half_span=4.0, skew=0.0, **body_kwargs): + wing = Wing(n_panels=n, spanwise_panel_distribution="uniform") + for y in np.linspace(-half_span, half_span, n + 1): + wing.add_section(np.array([0.0, y, 0.0]), np.array([1.0, y + skew, 0.0]), INVISCID) + body = BodyAerodynamics([wing], **{"leg_direction": "rib", **body_kwargs}) + body.va_initialize(20.0, 5.0, 0.0) + return body + + +def swept(n, sweep_deg=30.0, half_span=4.0, **body_kwargs): + """Kinked 30 deg sweep, sections (ribs) along the freestream x axis.""" + wing = Wing(n_panels=n, spanwise_panel_distribution="uniform") + for y in np.linspace(-half_span, half_span, n + 1): + x = abs(y) * np.tan(np.radians(sweep_deg)) + wing.add_section(np.array([x, y, 0.0]), np.array([x + 1.0, y, 0.0]), INVISCID) + body = BodyAerodynamics([wing], **{"leg_direction": "rib", **body_kwargs}) + body.va_initialize(20.0, 5.0, 0.0) + return body + + +def yawed(n, sweep_deg=30.0, half_span=4.0, **body_kwargs): + """Uniform 30 deg sweep without a kink (a yawed straight wing), ribs along x.""" + wing = Wing(n_panels=n, spanwise_panel_distribution="uniform") + for y in np.linspace(-half_span, half_span, n + 1): + x = y * np.tan(np.radians(sweep_deg)) + wing.add_section(np.array([x, y, 0.0]), np.array([x + 1.0, y, 0.0]), INVISCID) + body = BodyAerodynamics([wing], **{"leg_direction": "rib", **body_kwargs}) + body.va_initialize(20.0, 5.0, 0.0) + return body + + +def arc(n, radius=4.0, half_angle_deg=55.0, **body_kwargs): + """Circular arc in the y-z plane (kite-like), chord 1 along x.""" + wing = Wing(n_panels=n, spanwise_panel_distribution="uniform") + for th in np.radians(np.linspace(-half_angle_deg, half_angle_deg, n + 1)): + le = np.array([0.0, radius * np.sin(th), radius * np.cos(th)]) + wing.add_section(le, le + np.array([1.0, 0.0, 0.0]), INVISCID) + body = BodyAerodynamics([wing], **{"leg_direction": "rib", **body_kwargs}) + body.va_initialize(10.0, 8.0, 0.0) + return body + + +def elliptic_swept_anhedral(n, aspect_ratio=8.0, sweep=0.10, anhedral=0.25, **body_kwargs): + """Elliptic chord, root chord 1, tip offsets of 10 % (back) and 25 % (down) + of the semispan, parabolic; the TORQUE 2026 combined case.""" + s = aspect_ratio * (np.pi / 4) / 2 + wing = Wing(n_panels=n, spanwise_panel_distribution="uniform") + for y in np.linspace(-s, s, n + 1): + eta = min(abs(y) / s, 0.999999) + c = np.sqrt(1 - eta**2) + x = sweep * s * eta**2 + z = -anhedral * s * eta**2 + wing.add_section(np.array([x, y, z]), np.array([x + c, y, z]), INVISCID) + body = BodyAerodynamics([wing], **{"leg_direction": "rib", **body_kwargs}) + body.va_initialize(10.0, 10.0, 0.0) + return body + + +_V3 = None + + +def v3_inviscid(n, alpha=10.0, **body_kwargs): + global _V3 + if _V3 is None: + _V3 = yaml.safe_load(open(DATA / "CAD_derived_geometry" / "aero_geometry_CAD_CFD_NF_combined.yaml")) + wing = Wing(n_panels=n, spanwise_panel_distribution="uniform") + for row in _V3["wing_sections"]["data"]: + wing.add_section(np.array(row[1:4], float), np.array(row[4:7], float), INVISCID) + body = BodyAerodynamics([wing], **{"leg_direction": "rib", **body_kwargs}) + body.va_initialize(2.82, alpha, 0.0) + return body + + +# -------------------------------------------------------------------------- +# Alternatives for the quarter-chord bound-vortex term (drop-in replacements) +# -------------------------------------------------------------------------- +STATE = {"body": None, "U": None, "wake": None, "delta": 0.15, "blend": 1.0} + + +def _seg(x1, x2, p): + """Unit-strength straight segment x1 -> x2, zero on the line (to round-off).""" + r1 = p - x1 + r2 = p - x2 + r0 = x2 - x1 + c = np.cross(r1, r2) + L = np.linalg.norm(r0) + n = np.linalg.norm(c) + if n < 1e-9 * L * max(np.linalg.norm(r1), L): + return np.zeros(3) + return c / (4 * np.pi * n * n) * (r0 @ (r1 / np.linalg.norm(r1) - r2 / np.linalg.norm(r2))) + + +def _none(eval_points, bp1, bp2, *args): + """Trailed system only at the quarter chord.""" + n = eval_points.shape[0] + return np.zeros((3, n, n)) + + +def _on_line(eval_points, bp1, bp2, te1, te2, cdir, chords, va_norm, ac, core, sx, sw): + """Other panels' bound segments evaluated on the quarter-chord line + (Phillips & Snyder; develop before this work).""" + n = eval_points.shape[0] + A = np.zeros((3, n, n)) + for i in range(n): + for j in range(n): + if i != j: + A[:, i, j] = _vel_bound_vortex(bp2[j], bp1[j], eval_points[i], 1.0, core) + return A + + +def _li2020_literal(eval_points, bp1, bp2, te1, te2, cdir, chords, va_norm, ac, core, sx, sw): + """3D bound induction minus the section's own 2D infinite vortex, evaluated + at the 3/4-chord control point and reused at the quarter chord.""" + P = STATE["body"].panels + cps = np.array([p.control_point for p in P]) + n = eval_points.shape[0] + A = np.zeros((3, n, n)) + for i in range(n): + for j in range(n): + v = _seg(bp2[j], bp1[j], cps[i]) + if i == j: + r0 = bp1[j] - bp2[j] + t = r0 / np.linalg.norm(r0) + r1 = cps[i] - bp2[j] + dp = r1 - (r1 @ t) * t + v = v - np.cross(t, r1) / (2 * np.pi * (dp @ dp)) + A[:, i, j] = v + return A + + +def _ring_trailed(bp1, bp2, te1, te2, ep, wake, U): + return ( + _vel_trailing_vortex(bp1, te1, ep, 1.0, U) + + _vel_trailing_vortex(te2, bp2, ep, 1.0, U) + + _vel_semiinfinite(te1, wake, ep, 1.0, U, 1.0) + + _vel_semiinfinite(te2, wake, ep, 1.0, U, -1.0) + ) + + +def _curved_minus_straight_cp(eval_points, bp1, bp2, te1, te2, cdir, chords, va_norm, ac, core, sx, sw): + """Hypothesised form of the authors' LL-Gaunaa quarter-chord term: the bound + segments of the real wing minus those of the wing unrolled onto panel i's + tangent, single filaments, evaluated at the 3/4-chord control point of + panel i and reused at the quarter chord (constant along the chord). Zero + on straight wings by construction; finite because the control point is + half a chord off every line; no treatment of the legs.""" + n = len(chords) + out = np.zeros((3, n, n)) + fwd = np.linalg.norm(bp1[0] - bp2[1]) < np.linalg.norm(bp2[0] - bp1[1]) if n > 1 else True + L = np.linalg.norm(bp2 - bp1, axis=1) + s = np.concatenate(([0.0], np.cumsum(L))) + sgn = 1.0 if fwd else -1.0 + for i in range(n): + C = eval_points[i] + (0.75 - ac) * chords[i] * cdir[i] # 3/4-chord control point + t = (bp2[i] - bp1[i]) / L[i] + sP = 0.5 * (s[i] + s[i + 1]) + for j in range(n): + ra = eval_points[i] + sgn * (s[j] - sP) * t + rb = eval_points[i] + sgn * (s[j + 1] - sP) * t + ub1, ub2 = (ra, rb) if fwd else (rb, ra) + out[:, i, j] = _seg(bp2[j], bp1[j], C) - _seg(ub2, ub1, C) + return out + + +def _reid_hunsaker(eval_points, bp1, bp2, te1, te2, cdir, chords, va_norm, ac, core, sx, sw): + """Reid & Hunsaker (2020): for each control point the lifting line is + straightened near the point (Gaussian blend of width STATE['blend'] times + the local chord, or an absolute length if STATE['blend'] is a tuple + ('abs', metres)), every trailing vortex gets a joint of STATE['delta'] + chords perpendicular to the effective line before turning along the + freestream, and the bound segments of the other rings are kept.""" + U = STATE["U"] + wake = STATE["wake"] + n = eval_points.shape[0] + seg = bp1 - bp2 + L = np.linalg.norm(seg, axis=1) + s_node = np.concatenate([[0.0], np.cumsum(L)]) + fwd = n == 1 or np.linalg.norm(bp1[0] - bp2[1]) < np.linalg.norm(bp2[0] - bp1[1]) + sgn = 1.0 if fwd else -1.0 + nodes = np.vstack([bp2, bp1[-1:]]) if fwd else np.vstack([bp1, bp2[-1:]]) + A = np.zeros((3, n, n)) + for i in range(n): + ep = eval_points[i] + t = seg[i] / L[i] + s_P = 0.5 * (s_node[i] + s_node[i + 1]) + blend = STATE["blend"] + ds = blend[1] if isinstance(blend, tuple) else blend * chords[i] + w = np.exp(-((s_node - s_P) / ds) ** 2) + eff = (1 - w)[:, None] * nodes + w[:, None] * (ep + sgn * (s_node - s_P)[:, None] * t) + for j in range(n): + e2, e1 = (eff[j], eff[j + 1]) if fwd else (eff[j + 1], eff[j]) + tj = e1 - e2 + tj = tj / np.linalg.norm(tj) + d = cdir[j] - (cdir[j] @ tj) * tj + d = d / np.linalg.norm(d) + j1 = e1 + STATE["delta"] * chords[j] * d + j2 = e2 + STATE["delta"] * chords[j] * d + v = (0.0 if i == j else 1.0) * _seg(e2, e1, ep) + v = v + _ring_trailed(e1, e2, j1, j2, ep, wake, U) + A[:, i, j] = v - _ring_trailed(bp1[j], bp2[j], te1[j], te2[j], ep, wake, U) + return A + + +VARIANTS = { + "regularised (this work)": REGULARISED, + "trailed only (no bound term)": _none, + "bound segments on the line (develop before)": _on_line, + "Li 2020 literal (3/4c difference reused)": _li2020_literal, +} +RH_PARAMS = [ # (joint length / chord, blending distance / chord) + (0.25, 1.0), + (0.5, 1.0), + (1.0, 1.0), + (0.5, 0.5), + (0.5, 2.0), +] + + +def solve(body, variant, **rh): + BA.assemble_curved_bound_correction_AIC = variant + STATE["body"] = body + va = body.va if np.ndim(body.va) == 1 else body.va[0] + STATE["U"] = float(np.linalg.norm(va)) + STATE["wake"] = va / STATE["U"] + STATE.update(rh) + try: + r = Solver(allowed_error=1e-7, max_iterations=4000).solve(body) + finally: + BA.assemble_curved_bound_correction_AIC = REGULARISED + return r + + +def ratio(body, variant, **rh): + r = solve(body, variant, **rh) + return float(r["drag"] / r["drag_induced_trefftz"]), float(r["cl"]) + + +# -------------------------------------------------------------------------- +# Tables +# -------------------------------------------------------------------------- +CASES = [ + ("straight AR 8", straight, (40, 80)), + ("straight, ribs skewed 22 deg", lambda n: straight(n, skew=0.4), (40, 80)), + ("swept 30 deg, streamwise ribs", swept, (40, 80, 160)), + ("arc 110 deg, R = 4c", arc, (20, 40, 80, 160)), + ("elliptic, 10% sweep, 25% anhedral", elliptic_swept_anhedral, (40, 80, 160)), + ("V3 kite, inviscid, 10 deg", v3_inviscid, (20, 40, 80, 160)), +] + + +def table_variants(quick): + names = ["regularised (this work)", "bound segments on the line (develop before)", "trailed only (no bound term)"] + if not quick: + names.append("Li 2020 literal (3/4c difference reused)") + rows = {} + print("\non-blade / Trefftz induced drag (CL), inviscid polars") + for case, mk, Ns in CASES: + for name in names: + vals = [] + for N in Ns: + t0 = time.time() + try: + rt, cl = ratio(mk(N), VARIANTS[name]) + except Exception as e: # degenerate geometry etc. + rt, cl = float("nan"), float("nan") + vals.append((N, rt, cl)) + rows[f"{case} | {name}"] = vals + print(f" {case:36s} {name:44s} " + " ".join(f"N={N}: {rt:.3f}" for N, rt, _ in vals), flush=True) + return rows + + +def table_reid_hunsaker(): + rows = {} + print("\nReid & Hunsaker construction, on-blade / Trefftz (joint/chord, blending/chord)") + for case, mk, Ns in CASES: + if "skewed" in case: + continue + N = min(max(Ns), 80) + for delta, blend in RH_PARAMS: + try: + rt, _ = ratio(mk(N), _reid_hunsaker, delta=delta, blend=blend) + except Exception: + rt = float("nan") + rows[f"{case} | RH {delta}/{blend}"] = [(N, rt)] + print(f" {case:36s} joint {delta:4.2f} c, blend {blend:3.1f} c N={N}: {rt:.3f}", flush=True) + # MachUpX defaults: joint 0.15 c, blending distance one semispan + body = mk(N) + y = np.array([p.aerodynamic_center[1] for p in body.panels]) + semispan = 0.5 * (y.max() - y.min()) + try: + rt, _ = ratio(body, _reid_hunsaker, delta=0.15, blend=("abs", semispan)) + except Exception: + rt = float("nan") + rows[f"{case} | RH MachUpX defaults"] = [(N, rt)] + print(f" {case:36s} MachUpX defaults (0.15 c, semispan) N={N}: {rt:.3f}", flush=True) + return rows + + +def table_hypothesis(quick): + """The hypothesised Li/Gaunaa form with rib legs and with the paper's + perpendicular legs, against the sheet regularisation.""" + print("\non-blade / Trefftz, curved-minus-straight bound segments at the 3/4 chord reused at the 1/4 chord (single filaments)") + cases = [ + ("straight AR 8", straight, (40, 80)), + ("arc 110 deg, R = 4c", arc, (20, 40, 80) if quick else (20, 40, 80, 160)), + ("elliptic, 10% sweep, 25% anhedral", elliptic_swept_anhedral, (40, 80) if quick else (40, 80, 160)), + ("V3 kite, inviscid, 10 deg", v3_inviscid, (40, 80) if quick else (40, 80, 160)), + ("swept 30 deg (kinked)", swept, (40, 80)), + ] + out = {} + for name, mk, Ns in cases: + for legs in ("rib", "perpendicular"): + vals = [] + for N in Ns: + try: + r, cl = ratio(mk(N, leg_direction=legs), _curved_minus_straight_cp) + except ValueError: + r = float("nan") + vals.append((N, r)) + out[f"{name} | {legs}"] = vals + print(f" {name:34s} {legs:13s} " + " ".join(f"N={N}: {r:.3f}" for N, r in vals), flush=True) + return out + + +def arc_self_induction(): + """Chordwise induced velocity at the arc mid-span quarter-chord point from + the other panels' bound vortices: polyline (log-divergent) and chordwise- + spread sheet (finite). Circulation from the 80-panel solution.""" + b80 = arc(80) + r80 = solve(b80, REGULARISED) + gam80 = np.asarray(r80["gamma_distribution"]) + th80 = np.array([np.arctan2(p.aerodynamic_center[1], p.aerodynamic_center[2]) for p in b80.panels]) + order = np.argsort(th80) + sx, sw = thin_airfoil_strips() + out = {"N": [], "polyline": [], "sheet": []} + for N in (10, 20, 40, 80, 160, 320): + b = arc(N) + P = b.panels + i = N // 2 + ep = P[i].aerodynamic_center + th = np.array([np.arctan2(p.aerodynamic_center[1], p.aerodynamic_center[2]) for p in P]) + gam = np.interp(th, th80[order], gam80[order]) + u_poly = np.zeros(3) + u_sheet = np.zeros(3) + for j in range(N): + if j == i: + continue + u_poly += gam[j] * _vel_bound_vortex(P[j].bound_point_2, P[j].bound_point_1, ep, 1.0, 1e-20) + for x, w in zip(sx, sw): + sh = np.array([x - 0.25, 0.0, 0.0]) + u_sheet += gam[j] * w * _vel_bound_vortex(P[j].bound_point_2 + sh, P[j].bound_point_1 + sh, ep, 1.0, 1e-20) + out["N"].append(N) + out["polyline"].append(float(u_poly[0])) + out["sheet"].append(float(u_sheet[0])) + print("\narc mid-span chordwise self-induction u [m/s] (U = 10 m/s):") + for N, up, us in zip(out["N"], out["polyline"], out["sheet"]): + print(f" N={N:4d}: polyline {up:+.4f} sheet {us:+.4f}") + return out + + +# -------------------------------------------------------------------------- +# V3 viscous polars (figure 09) +# -------------------------------------------------------------------------- +def v3_polars(): + ref = np.array([0.422646, 0.0, 9.3667]) + U = 2.82 + body = BodyAerodynamics.instantiate( + n_panels=50, + file_path=DATA / "CAD_derived_geometry" / "aero_geometry_CAD_CFD_NF_combined.yaml", + spanwise_panel_distribution="uniform", + ) + alphas = [-3, -1, 1, 3, 5, 7, 9, 10, 11, 12, 13, 14, 15, 17, 19] + betas = [0, 4, 8, 12, 14, 20] + runs = { + "old34": (dict(is_aoa_corrected=False), REGULARISED), + "on_line": (dict(is_aoa_corrected=True), _on_line), + "fix": (dict(is_aoa_corrected=True), REGULARISED), + } + pol = {"alpha": {}, "beta": {}} + for tag, (kw, variant) in runs.items(): + s = Solver(reference_point=ref, **kw) + BA.assemble_curved_bound_correction_AIC = variant + rows = [] + for al in alphas: + body.va_initialize(U, float(al), 0.0) + r = s.solve(body) + rows.append((al, float(r["cl"]), float(r["cd"]))) + pol["alpha"][tag] = rows + rows = [] + for be in betas: + body.va_initialize(U, 7.4, float(be)) + r = s.solve(body) + rows.append((be, float(r["cl"]), float(r["cd"]), float(r["cs"]))) + pol["beta"][tag] = rows + BA.assemble_curved_bound_correction_AIC = REGULARISED + print(f" V3 polars {tag} done", flush=True) + return pol + + +# -------------------------------------------------------------------------- +# Figures 08 and 09 +# -------------------------------------------------------------------------- +def figure_08(conv, ux): + import matplotlib + + matplotlib.use("Agg") + import matplotlib.pyplot as plt + from matplotlib.patches import FancyArrowPatch + + plt.rcParams.update({"font.size": 9}) + fig, ax = plt.subplots(2, 3, figsize=(15, 9)) + A = ax[0, 0] + R = 4 + th = np.radians(np.linspace(-55, 55, 13)) + y, z = R * np.sin(th), R * np.cos(th) + A.plot(y, z, "o-", color="k", lw=2, ms=4, label="bound vortex (polyline) on the quarter-chord line") + i = 6 + P = (0.5 * (y[i] + y[i + 1]), 0.5 * (z[i] + z[i + 1])) + A.plot(*P, "r*", ms=14, label="quarter-chord point P of panel i") + for k in (4, 5, 7, 8): + m = (0.5 * (y[k] + y[k + 1]), 0.5 * (z[k] + z[k + 1])) + A.annotate("", xy=P, xytext=m, arrowprops=dict(arrowstyle="->", color="tab:blue", lw=1.2)) + A.text(0.0, 1.55, "neighbouring segments induce a velocity at P\nperpendicular to the arc plane, i.e. along the chord:\nu ~ (Gamma kappa / 4 pi) ln(span / panel width)\n-> grows without bound as the panels shrink", + ha="center", va="top", fontsize=8.5, bbox=dict(fc="w", ec="0.7")) + A.set_xlim(-3.6, 3.6); A.set_ylim(0.3, 4.6); A.set_xlabel("y (span)"); A.set_ylabel("z") + A.legend(loc="upper left", fontsize=8); A.set_title("(a) Front view: curved bound vortex evaluated on itself") + B = ax[0, 1] + B.semilogx(ux["N"], ux["polyline"], "o-", color="tab:red", label="polyline segments (bound on the line)") + B.semilogx(ux["N"], ux["sheet"], "s-", color="tab:green", label="chordwise-spread sheet (thin-airfoil weight)") + B.axhspan(-0.20, -0.18, color="0.85", label="value for which on-blade drag = Trefftz") + B.set_xlabel("number of spanwise panels N"); B.set_ylabel("chordwise induced velocity u at mid-span [m/s] (U = 10 m/s)") + B.set_title("(b) Arc wing: the self-induction diverges, the sheet value is finite"); B.legend(fontsize=8); B.grid(alpha=0.3) + C = ax[0, 2] + al = np.radians(20) + C.plot([-0.1, 1.1], [0, 0], "k-", lw=3); C.text(1.12, 0, "chord", va="center") + e = np.array([np.cos(al), np.sin(al)]); nvec = np.array([-np.sin(al), np.cos(al)]); o = np.array([0.25, 0.0]) + + def arr(p, v, c, lab, dy=0.02): + C.add_patch(FancyArrowPatch(p, p + v, arrowstyle="->", mutation_scale=14, color=c, lw=1.8)) + C.text(*(p + v + np.array([0.02, dy])), lab, color=c, fontsize=9) + + arr(o, 0.9 * e, "k", "freestream U\n(angle alpha to the chord)") + w = 0.12; u = -0.10 + arr(o + 0.9 * e, -w * nvec, "tab:blue", "downwash w (from the trailed wake)") + arr(o + 0.9 * e - w * nvec, u * e, "tab:red", "chordwise u (curved bound vortex)", dy=-0.06) + V = 0.9 * e - w * nvec + u * e; arr(o, V, "tab:purple", "quarter-chord flow V", dy=-0.12) + Vh = V / np.linalg.norm(V); Lv = np.array([-Vh[1], Vh[0]]) * 0.55 + arr(o, Lv, "tab:green", "lift, perpendicular to V", dy=0.0) + C.text(-0.1, -0.62, "the lift component along U is the\non-blade induced drag:\nD = rho Gamma (w cos(alpha) + u sin(alpha))\n\nw is small (about 1 deg of induced angle),\nso u = 3% of U changes D by about 12%", + fontsize=8.5, bbox=dict(fc="w", ec="0.7")) + C.set_xlim(-0.15, 1.75); C.set_ylim(-0.7, 0.9); C.set_aspect("equal"); C.axis("off"); C.set_title("(c) Why a chordwise velocity changes the drag") + Dd = ax[1, 0] + x = np.linspace(0.002, 0.998, 400); g = np.sqrt((1 - x) / x); g /= np.trapezoid(g, x) + Dd.plot(x, g, color="tab:green", lw=2, label="thin-airfoil bound vorticity gamma(x) ~ sqrt((c-x)/x)") + Dd.fill_between(x, 0, g, color="tab:green", alpha=0.15) + sx, _ = thin_airfoil_strips() + for xq in sx: + Dd.plot([xq, xq], [0, np.interp(xq, x, g)], color="tab:green", lw=0.6, alpha=0.6) + Dd.plot([0.25], [0], "ro", ms=9, label="single bound vortex at c/4 (lifting line)"); Dd.plot([0, 1], [0, 0], "k-", lw=3) + Dd.text(0.5, 2.2, "same total circulation, same centroid (c/4);\nspreading it over the chord makes the curved\nsheet self-induction finite (effective cut-off 0.15 c)", + ha="center", fontsize=8.5, bbox=dict(fc="w", ec="0.7")) + Dd.set_xlabel("x / c"); Dd.set_ylabel("gamma(x) c / Gamma"); Dd.set_ylim(-0.3, 3.2); Dd.legend(fontsize=8, loc="upper right") + Dd.set_title("(d) The regularisation: bound vortex spread over the chord") + E = ax[1, 1] + s = np.linspace(-1, 1, 9); yy = 3.0 * s; zz = -1.2 * s**2 + E.plot(yy, zz, "o-", color="k", lw=2, ms=4, label="actual curved system (bound + legs), smeared") + for k in range(9): + E.plot([yy[k], yy[k]], [zz[k], zz[k] - 0.6], color="k", lw=0.8) + E.plot(yy, 0 * yy + 0.9, "o-", color="tab:orange", lw=2, ms=4, label="same panels unrolled onto the tangent line of P") + for k in range(9): + E.plot([yy[k], yy[k]], [0.9, 0.3], color="tab:orange", lw=0.8) + E.plot([0], [0], "r*", ms=14); E.plot([0], [0.9], "r*", ms=14) + E.text(0, -2.1, "V(1/4c) = classical lifting-line downwash of the UNROLLED wing\n + [ smeared actual system - smeared unrolled system ]\n\nstraight wing: the bracket is exactly zero (nothing changes)\ncurved wing: the bracket is the finite curvature effect", + ha="center", fontsize=8.5, bbox=dict(fc="w", ec="0.7")) + E.set_xlim(-3.6, 3.6); E.set_ylim(-4.4, 1.5); E.axis("off"); E.legend(fontsize=8, loc="lower left") + E.set_title("(e) The fix: reference straight wing + regularised difference") + F = ax[1, 2] + for key, c, lab in (("arc 110 deg, R = 4c", "tab:blue", "arc wing"), ("V3 kite, inviscid, 10 deg", "tab:red", "V3 kite (inviscid)")): + for name, ls, m, lab2 in (("bound segments on the line (develop before)", "--", "o", "bound on the line"), ("regularised (this work)", "-", "s", "regularised")): + rows = np.array([(N, rt) for N, rt, _ in conv[f"{key} | {name}"]]) + F.semilogx(rows[:, 0], rows[:, 1], ls, marker=m, color=c, label=f"{lab}: {lab2}") + F.axhline(1.0, color="k", lw=0.8); F.set_xlabel("number of spanwise panels N"); F.set_ylabel("on-blade induced drag / Trefftz-plane induced drag") + F.set_title("(f) Result: mesh-independent and equal to Trefftz"); F.legend(fontsize=8); F.grid(alpha=0.3); F.set_ylim(0.8, 1.08) + fig.suptitle("Why the V3 on-blade induced drag disagrees with the Trefftz plane, and the fix", fontsize=13) + fig.tight_layout() + fig.savefig(FIG / "08_curved_bound_vortex_regularisation.png", dpi=150) + plt.close(fig) + + +def figure_09(pol): + import matplotlib + + matplotlib.use("Agg") + import matplotlib.pyplot as plt + + plt.rcParams.update({"font.size": 9}) + D = DATA / "3D_polars_literature" + cfd = pd.read_csv(D / "CFD_RANS_Rey_10e5_Poland2025_alpha_sweep_beta_0.csv") + wt = pd.read_csv(D / "windtunnel_alpha_sweep_beta_00_0_Poland_2025_Rey_5e5.csv") + wtb = pd.read_csv(D / "windtunnel_beta_sweep_alpha_07_4_Poland_2025_Rey_5e5.csv") + fig, ax = plt.subplots(2, 2, figsize=(12, 9)) + sty = { + "old34": dict(color="0.5", ls="--", label="develop before this work (3/4-chord directions)"), + "on_line": dict(color="tab:blue", ls="-", label="quarter-chord directions, bound segments on the line"), + "fix": dict(color="tab:red", ls="-", lw=2, label="quarter-chord directions, regularised curved bound vortex"), + } + for tag in ("old34", "on_line", "fix"): + r = np.array(pol["alpha"][tag]) + ax[0, 0].plot(r[:, 0], r[:, 1], **sty[tag]); ax[0, 1].plot(r[:, 0], r[:, 2], **sty[tag]); ax[1, 0].plot(r[:, 0], r[:, 1] / r[:, 2], **sty[tag]) + rb = np.array(pol["beta"][tag]); ax[1, 1].plot(rb[:, 0], rb[:, 2], **sty[tag]) + ax[0, 0].plot(cfd.alpha, cfd.CL, "k^", label="RANS Re 1e6 (Poland 2025)"); ax[0, 1].plot(cfd.alpha, cfd.CD, "k^"); ax[1, 0].plot(cfd.alpha, cfd.CL / cfd.CD, "k^") + m = (wt.alpha > -4) & (wt.alpha < 20) + ax[0, 0].errorbar(wt.alpha[m], wt.CL[m], yerr=wt.CL_ci[m], fmt="ko", mfc="w", label="wind tunnel Re 5e5 (Poland 2026), 99% CI") + ax[0, 1].errorbar(wt.alpha[m], wt.CD[m], yerr=wt.CD_ci[m], fmt="ko", mfc="w"); ax[1, 0].plot(wt.alpha[m], wt.CL[m] / wt.CD[m], "ko", mfc="w") + mb = wtb.beta >= -0.5 + ax[1, 1].errorbar(wtb.beta[mb], wtb.CD[mb], yerr=wtb.CD_ci[mb], fmt="ko", mfc="w", label="wind tunnel, +beta") + mb2 = wtb.beta <= 0.5 + ax[1, 1].errorbar(-wtb.beta[mb2], wtb.CD[mb2], yerr=wtb.CD_ci[mb2], fmt="kd", mfc="w", label="wind tunnel, -beta (mirrored)") + ax[0, 0].set_ylabel("CL"); ax[0, 1].set_ylabel("CD"); ax[1, 0].set_ylabel("L/D"); ax[1, 1].set_ylabel("CD at alpha 7.4 deg") + for a_ in ax[0]: + a_.set_xlabel("angle of attack [deg]") + ax[1, 0].set_xlabel("angle of attack [deg]"); ax[1, 1].set_xlabel("sideslip [deg]") + for a_ in ax.ravel(): + a_.grid(alpha=0.3) + ax[0, 0].legend(fontsize=8); ax[1, 1].legend(fontsize=8); ax[0, 1].set_ylim(0, 0.35) + fig.suptitle("TU Delft V3 kite, CAD geometry, 2D RANS Re 5e5 polars, 50 panels, 2.82 m/s: effect of the fix", fontsize=12) + fig.tight_layout() + fig.savefig(FIG / "09_v3_polars_curved_bound_vortex_fix.png", dpi=150) + plt.close(fig) + + +# -------------------------------------------------------------------------- +def main(argv=None): + ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) + ap.add_argument("--quick", action="store_true", help="only the regularised, on-line and trailed-only variants") + ap.add_argument("--hypothesis", action="store_true", help="only the table of the hypothesised Li/Gaunaa form") + ap.add_argument("--figures", action="store_true", help="also regenerate docs/figures/08 and 09") + args = ap.parse_args(argv) + out = {} + t0 = time.time() + if args.hypothesis: + path = OUT / "curved_bound_vortex_validation.json" + out = json.load(open(path)) if path.exists() else {} + out["hypothesis"] = table_hypothesis(args.quick) + json.dump(out, open(path, "w"), indent=1) + print(f"\nresults written to {path} ({time.time() - t0:.0f} s)") + return + out["variants"] = table_variants(args.quick) + out["arc_self_induction"] = arc_self_induction() + if not args.quick: + out["reid_hunsaker"] = table_reid_hunsaker() + out["hypothesis"] = table_hypothesis(args.quick) + if args.figures: + print("\nV3 viscous polars for figure 09") + out["v3_polars"] = v3_polars() + figure_08(out["variants"], out["arc_self_induction"]) + figure_09(out["v3_polars"]) + print("figures 08 and 09 written to docs/figures") + json.dump(out, open(OUT / "curved_bound_vortex_validation.json", "w"), indent=1) + print(f"\nresults written to {OUT / 'curved_bound_vortex_validation.json'} ({time.time() - t0:.0f} s)") + + +if __name__ == "__main__": + main() diff --git a/docs/validation/kink_figure.py b/docs/validation/kink_figure.py new file mode 100644 index 0000000..9e1931f --- /dev/null +++ b/docs/validation/kink_figure.py @@ -0,0 +1,78 @@ +"""Figure 16 of docs/figures: why two line vortices meeting at an angle have +no finite mutual velocity and two chordwise sheets do (report Section 4.2). + +Line 1 runs along -y from the kink, line 2 starts at the kink turned by the +angle theta out of the plane. P sits on line 1 (at its quarter chord) at +distance r from the kink. Plotted: the chordwise velocity at P induced by a +unit-strength line 2 (single filament at the quarter chord) and by the same +circulation spread over the chord with the thin-airfoil strips, against r. + +Usage (from the repository root, seconds):: + + python docs/validation/kink_figure.py +""" + +from __future__ import annotations + +from pathlib import Path + +import numpy as np + +from VSM.core.utils import thin_airfoil_strips + +FIG = Path(__file__).resolve().parents[1] / "figures" + + +def seg(a, b, p): + r1, r2 = p - a, p - b + cr = np.cross(r1, r2) + n = np.dot(cr, cr) + if n < 1e-24: + return np.zeros(3) + return cr / (4 * np.pi * n) * np.dot(b - a, r1 / np.linalg.norm(r1) - r2 / np.linalg.norm(r2)) + + +def main(): + import matplotlib + + matplotlib.use("Agg") + import matplotlib.pyplot as plt + + plt.rcParams.update({"font.size": 9}) + x, w = thin_airfoil_strips(24) + L = 50.0 + rs = np.logspace(-3, 1, 80) + fig, ax = plt.subplots(1, 2, figsize=(13, 5)) + for theta_deg, col in ((2.0, "tab:blue"), (5.0, "tab:red"), (10.0, "tab:green")): + t = np.radians(theta_deg) + e2 = np.array([0.0, np.cos(t), np.sin(t)]) + vf, vs = [], [] + for r in rs: + P = np.array([0.25, -r, 0.0]) + a0 = np.array([0.25, 0.0, 0.0]) + vf.append(abs(seg(a0, a0 + L * e2, P)[0])) + vs.append(abs(sum(w[q] * seg(np.array([x[q], 0, 0]), np.array([x[q], 0, 0]) + L * e2, P)[0] for q in range(24)))) + ax[0].loglog(rs, vf, "-", color=col, label=f"line, kink {theta_deg:.0f} deg") + ax[0].loglog(rs, vs, "--", color=col, label=f"sheet, kink {theta_deg:.0f} deg") + if theta_deg == 5.0: + ax[1].loglog(rs, vf, "r-", label="line, computed") + ax[1].loglog(rs, (1 - np.cos(t)) / (4 * np.pi * rs * np.sin(t)), "k:", label="line, (1 - cos theta) / (4 pi r sin theta)") + ax[1].loglog(rs, vs, "r--", label="sheet, computed") + ax[0].axvline(0.15, color="0.6", ls=":") + ax[0].text(0.16, 2.0, "0.15 c", color="0.4") + ax[0].set_xlabel("distance r of P from the kink, along line 1 (chord = 1)") + ax[0].set_ylabel("|chordwise velocity at P| per unit circulation of line 2") + ax[0].set_title("(a) two vortices meeting at a kink: line 2 seen from a point on line 1") + ax[0].legend(fontsize=8, frameon=False) + ax[1].set_xlabel("r (chord = 1), kink 5 deg") + ax[1].set_ylabel("|chordwise velocity at P|") + ax[1].set_title("(b) kink 5 deg: line, small-angle formula, sheet") + ax[1].legend(fontsize=8, frameon=False) + fig.tight_layout() + out = FIG / "16_kink_line_vs_sheet.png" + fig.savefig(out, dpi=150) + print("figure 16 written to", out) + + +if __name__ == "__main__": + main() diff --git a/docs/validation/leg_direction_study.json b/docs/validation/leg_direction_study.json new file mode 100644 index 0000000..3638d2b --- /dev/null +++ b/docs/validation/leg_direction_study.json @@ -0,0 +1,918 @@ +{ + "trefftz": { + "straight AR 8 | rib | AT=True": [ + [ + 40, + 1.0000241613419563, + 0.4037683141001728 + ], + [ + 80, + 0.9999993387640963, + 0.4007293936922778 + ] + ], + "straight AR 8 | rib | AT=False": [ + [ + 40, + 0.9983907808217655, + 0.40364690993768665 + ], + [ + 80, + 0.9983779327314991, + 0.40060923881189003 + ] + ], + "straight AR 8 | streamwise | AT=True": [ + [ + 40, + 1.0000241613419567, + 0.4037683141001729 + ], + [ + 80, + 0.9999993387640965, + 0.4007293936922779 + ] + ], + "straight AR 8 | streamwise | AT=False": [ + [ + 40, + 0.998390780821766, + 0.40364690993768676 + ], + [ + 80, + 0.9983779327314994, + 0.40060923881189014 + ] + ], + "straight AR 8 | perpendicular | AT=True": [ + [ + 40, + 1.0000241613419567, + 0.4037683141001729 + ], + [ + 80, + 0.9999993387640965, + 0.4007293936922779 + ] + ], + "straight AR 8 | perpendicular | AT=False": [ + [ + 40, + 0.998390780821766, + 0.40364690993768676 + ], + [ + 80, + 0.9983779327314994, + 0.40060923881189014 + ] + ], + "straight, ribs skewed 22 deg | rib | AT=True": [ + [ + 40, + 1.0294871336786302, + 0.40111700937128875 + ], + [ + 80, + 1.0297742412007151, + 0.39805783530229416 + ] + ], + "straight, ribs skewed 22 deg | rib | AT=False": [ + [ + 40, + 1.0316932384920274, + 0.40099108184481275 + ], + [ + 80, + 1.0319993363747875, + 0.397932863207271 + ] + ], + "straight, ribs skewed 22 deg | streamwise | AT=True": [ + [ + 40, + 1.0000241613419572, + 0.4037683141001728 + ], + [ + 80, + 0.9999993387640971, + 0.4007293936922781 + ] + ], + "straight, ribs skewed 22 deg | streamwise | AT=False": [ + [ + 40, + 0.9983907808217665, + 0.40364690993768665 + ], + [ + 80, + 0.9983779327315, + 0.4006092388118903 + ] + ], + "straight, ribs skewed 22 deg | perpendicular | AT=True": [ + [ + 40, + 1.0000241613419572, + 0.4037683141001728 + ], + [ + 80, + 0.9999993387640971, + 0.4007293936922781 + ] + ], + "straight, ribs skewed 22 deg | perpendicular | AT=False": [ + [ + 40, + 0.9983907808217665, + 0.40364690993768665 + ], + [ + 80, + 0.9983779327315, + 0.4006092388118903 + ] + ], + "swept 30 deg (kinked), streamwise ribs | rib | AT=True": [ + [ + 40, + 0.9710106496264701, + 0.3644656343303096 + ], + [ + 80, + 0.9818703531144978, + 0.36161543443795746 + ], + [ + 160, + 0.9879834819028264, + 0.36020744818304334 + ] + ], + "swept 30 deg (kinked), streamwise ribs | rib | AT=False": [ + [ + 40, + 0.9694504158113294, + 0.36436531130252064 + ], + [ + 80, + 0.9803166581923606, + 0.36151586467745145 + ], + [ + 160, + 0.9864320689040577, + 0.36010819545938966 + ] + ], + "swept 30 deg (kinked), streamwise ribs | streamwise | AT=True": [ + [ + 40, + 0.9710106496264695, + 0.3644656343303096 + ], + [ + 80, + 0.9818703531144976, + 0.3616154344379573 + ], + [ + 160, + 0.9879834819028285, + 0.3602074481830433 + ] + ], + "swept 30 deg (kinked), streamwise ribs | streamwise | AT=False": [ + [ + 40, + 0.969450415811329, + 0.36436531130252064 + ], + [ + 80, + 0.9803166581923604, + 0.3615158646774512 + ], + [ + 160, + 0.9864320689040598, + 0.3601081954593896 + ] + ], + "swept 30 deg (kinked), streamwise ribs | perpendicular | AT=True": [ + [ + 40, + 1.075283526013048, + 0.33389488862867633 + ], + [ + 80, + 1.0928884195341668, + 0.3310762587735496 + ], + [ + 160, + 1.1018374245390548, + 0.32963367201813265 + ] + ], + "swept 30 deg (kinked), streamwise ribs | perpendicular | AT=False": [ + [ + 40, + 1.1890185783822729, + 0.36350925051649075 + ], + [ + 80, + 1.206182598576028, + 0.3605974451424361 + ], + [ + 160, + 1.2147970219338762, + 0.3591082773865858 + ] + ], + "yawed 30 deg, streamwise ribs | rib | AT=True": [ + [ + 40, + 1.0011070347884317, + 0.36276349332168345 + ], + [ + 80, + 1.0015976445854184, + 0.36010909558679327 + ] + ], + "yawed 30 deg, streamwise ribs | rib | AT=False": [ + [ + 40, + 0.9996456952448897, + 0.3626741740330116 + ], + [ + 80, + 1.0001414766943495, + 0.3600203240957124 + ] + ], + "yawed 30 deg, streamwise ribs | streamwise | AT=True": [ + [ + 40, + 1.0011070347884314, + 0.3627634933216834 + ], + [ + 80, + 1.0015976445854184, + 0.3601090955867935 + ] + ], + "yawed 30 deg, streamwise ribs | streamwise | AT=False": [ + [ + 40, + 0.9996456952448893, + 0.36267417403301155 + ], + [ + 80, + 1.0001414766943495, + 0.3600203240957126 + ] + ], + "yawed 30 deg, streamwise ribs | perpendicular | AT=True": [ + [ + 40, + 0.9919346892329748, + 0.3679187785935869 + ], + [ + 80, + 0.9913254385373634, + 0.3652960399895595 + ] + ], + "yawed 30 deg, streamwise ribs | perpendicular | AT=False": [ + [ + 40, + 0.9191462270215652, + 0.36784942935851384 + ], + [ + 80, + 0.918906974866106, + 0.3652284324375624 + ] + ], + "arc 110 deg, R = 4c | rib | AT=True": [ + [ + 40, + 0.9991128646767465, + 0.5814516426807416 + ], + [ + 80, + 0.9989015501879521, + 0.5787728279860994 + ] + ], + "arc 110 deg, R = 4c | rib | AT=False": [ + [ + 40, + 0.9458967550556092, + 0.576033507658104 + ], + [ + 80, + 0.9459979569325551, + 0.5733831320131534 + ] + ], + "arc 110 deg, R = 4c | streamwise | AT=True": [ + [ + 40, + 1.0317563662763733, + 0.5676924267176111 + ], + [ + 80, + 1.034515599275102, + 0.5650842016459041 + ] + ], + "arc 110 deg, R = 4c | streamwise | AT=False": [ + [ + 40, + 1.032925474448956, + 0.5727108006440447 + ], + [ + 80, + 1.0352828743473412, + 0.5700420584770004 + ] + ], + "arc 110 deg, R = 4c | perpendicular | AT=True": [ + [ + 40, + 0.9991128646767461, + 0.5814516426807415 + ], + [ + 80, + 0.9989015501879521, + 0.5787728279860996 + ] + ], + "arc 110 deg, R = 4c | perpendicular | AT=False": [ + [ + 40, + 0.9458967550556086, + 0.5760335076581039 + ], + [ + 80, + 0.945997956932555, + 0.5733831320131536 + ] + ], + "elliptic, 10% sweep, 25% anhedral | rib | AT=True": [ + [ + 40, + 1.013843700029167, + 0.8167899906598163 + ], + [ + 80, + 1.0153496307985954, + 0.8151570220744624 + ], + [ + 160, + 1.0165458901739322, + 0.814536072602332 + ] + ], + "elliptic, 10% sweep, 25% anhedral | rib | AT=False": [ + [ + 40, + 0.9731676407644867, + 0.8108723667297819 + ], + [ + 80, + 0.9737193321810527, + 0.8090654032696574 + ], + [ + 160, + 0.9743882273675868, + 0.8083501263060516 + ] + ], + "elliptic, 10% sweep, 25% anhedral | streamwise | AT=True": [ + [ + 40, + 0.9823795866644961, + 0.8098316168364474 + ], + [ + 80, + 0.9861943910356977, + 0.8082026699779978 + ], + [ + 160, + 0.9885686955728495, + 0.8075322908895035 + ] + ], + "elliptic, 10% sweep, 25% anhedral | streamwise | AT=False": [ + [ + 40, + 0.9827820276872682, + 0.8103649746134265 + ], + [ + 80, + 0.9872120220424129, + 0.8086895169113965 + ], + [ + 160, + 0.9901126517890435, + 0.8079994330501438 + ] + ], + "elliptic, 10% sweep, 25% anhedral | perpendicular | AT=True": [ + [ + 40, + 1.024908398065889, + 0.8152069020145452 + ], + [ + 80, + 1.0235738570140933, + 0.813652870510948 + ], + [ + 160, + 1.0199596919543235, + 0.8131214542386911 + ] + ], + "elliptic, 10% sweep, 25% anhedral | perpendicular | AT=False": [ + [ + 40, + 0.9983083629064013, + 0.8115592627537922 + ], + [ + 80, + 0.993673014612593, + 0.8096264958523416 + ], + [ + 160, + 0.9879534050396199, + 0.8088419145798732 + ] + ], + "V3 kite, inviscid, 10 deg | rib | AT=True": [ + [ + 40, + 1.0072015329480584, + 0.6393171088835797 + ], + [ + 80, + 1.008284529459178, + 0.6385107288798224 + ], + [ + 160, + 1.009664834821042, + 0.6379759739912666 + ] + ], + "V3 kite, inviscid, 10 deg | rib | AT=False": [ + [ + 40, + 0.9159932530422038, + 0.6242165261490399 + ], + [ + 80, + 0.916747950524289, + 0.6233538674653687 + ], + [ + 160, + 0.9179330164124843, + 0.6227821519114533 + ] + ], + "V3 kite, inviscid, 10 deg | streamwise | AT=True": [ + [ + 40, + 1.0034164099739227, + 0.6125695330732521 + ], + [ + 80, + 1.0084932718443487, + 0.612168103615367 + ], + [ + 160, + 1.0110689556669616, + 0.6117133144714705 + ] + ], + "V3 kite, inviscid, 10 deg | streamwise | AT=False": [ + [ + 40, + 0.9972096954809403, + 0.6149720501989414 + ], + [ + 80, + 1.0010765554449188, + 0.6142725448353201 + ], + [ + 160, + 1.0031384839184732, + 0.6137063989968746 + ] + ], + "V3 kite, inviscid, 10 deg | perpendicular | AT=True": [ + [ + 40, + 1.0154988203805648, + 0.576782685706456 + ], + [ + 80, + 1.0179015019944175, + 0.5747251811156084 + ], + [ + 160, + NaN, + NaN + ] + ], + "V3 kite, inviscid, 10 deg | perpendicular | AT=False": [ + [ + 40, + 0.9699132955894555, + 0.5921698488039037 + ], + [ + 80, + 0.9608857703770551, + 0.5897830624225451 + ], + [ + 160, + NaN, + NaN + ] + ] + }, + "geometry": { + "straight, ribs skewed 22 deg | perpendicular | N=40": { + "max_leg_over_075c": 1.0000000000000002, + "max_reach_over_c": 0.30000000000000066 + }, + "straight, ribs skewed 22 deg | perpendicular | N=80": { + "max_leg_over_075c": 1.0000000000000002, + "max_reach_over_c": 0.3000000000000007 + }, + "straight, ribs skewed 22 deg | streamwise | N=40": { + "max_leg_over_075c": 1.0000000000000002, + "max_reach_over_c": 0.30000000000000066 + }, + "straight, ribs skewed 22 deg | streamwise | N=80": { + "max_leg_over_075c": 1.0000000000000002, + "max_reach_over_c": 0.3000000000000007 + }, + "swept 30 deg (kinked) | perpendicular | N=40": { + "max_leg_over_075c": 1.1547005383792515, + "max_reach_over_c": 0.4330127018922198 + }, + "swept 30 deg (kinked) | perpendicular | N=80": { + "max_leg_over_075c": 1.1547005383792495, + "max_reach_over_c": 0.43301270189222 + }, + "swept 30 deg (kinked) | streamwise | N=40": { + "max_leg_over_075c": 1.154700538379252, + "max_reach_over_c": 0.0 + }, + "swept 30 deg (kinked) | streamwise | N=80": { + "max_leg_over_075c": 1.1547005383792526, + "max_reach_over_c": 0.0 + }, + "elliptic, 10% sweep, 25% anhedral | perpendicular | N=40": { + "max_leg_over_075c": 1.0152333757489216, + "max_reach_over_c": 0.18998498205848294 + }, + "elliptic, 10% sweep, 25% anhedral | perpendicular | N=80": { + "max_leg_over_075c": 1.0132724566900528, + "max_reach_over_c": 0.34231071966664456 + }, + "elliptic, 10% sweep, 25% anhedral | streamwise | N=40": { + "max_leg_over_075c": 1.2257237215870764, + "max_reach_over_c": 0.08301849248461324 + }, + "elliptic, 10% sweep, 25% anhedral | streamwise | N=80": { + "max_leg_over_075c": 1.2777281796716817, + "max_reach_over_c": 0.10979717451076848 + }, + "V3 kite | perpendicular | N=40": { + "max_leg_over_075c": 2.338160052068521, + "max_reach_over_c": 1.2835346388650146 + }, + "V3 kite | perpendicular | N=80": { + "max_leg_over_075c": 2.86820553769903, + "max_reach_over_c": 1.602264771124807 + }, + "V3 kite | streamwise | N=40": { + "max_leg_over_075c": 1.5017296741950685, + "max_reach_over_c": 0.1928294285417315 + }, + "V3 kite | streamwise | N=80": { + "max_leg_over_075c": 1.703181642783287, + "max_reach_over_c": 0.2172598792683667 + } + }, + "v3_vs_rans": { + "rib | AT=True": { + "rows": [ + [ + 1.02, + 0.20679612324459334, + 0.04938123564317739, + 0.2278625272194466, + 0.0490919571607997 + ], + [ + 4.02, + 0.46058767084069085, + 0.05180241203771192, + 0.4610104802890269, + 0.0516566817024969 + ], + [ + 7.02, + 0.6974146118133506, + 0.06555811072905388, + 0.6748642166054709, + 0.0643789856608573 + ], + [ + 10.02, + 0.9169519944573377, + 0.0866118956847433, + 0.8685945393341531, + 0.0841781647387978 + ], + [ + 13.02, + 1.0504322262602785, + 0.12058257126592178, + 1.042518829707241, + 0.1099699784207837 + ] + ], + "mean_abs_dCL": 0.020062092061458238, + "mean_rel_dCD": 0.030489041793617777, + "mean_rel_dCD_to_10deg": 0.01398518846638205 + }, + "rib | AT=False": { + "rows": [ + [ + 1.02, + 0.20522250499237227, + 0.04935594735893374, + 0.2278625272194466, + 0.0490919571607997 + ], + [ + 4.02, + 0.452906906935191, + 0.05129108968328134, + 0.4610104802890269, + 0.0516566817024969 + ], + [ + 7.02, + 0.6797188780787451, + 0.06345589505372712, + 0.6748642166054709, + 0.0643789856608573 + ], + [ + 10.02, + 0.8860438648577043, + 0.08129543252333112, + 0.8685945393341531, + 0.0841781647387978 + ], + [ + 13.02, + 1.0090119085388984, + 0.11122721125275412, + 1.042518829707241, + 0.1099699784207837 + ] + ], + "mean_abs_dCL": 0.01731090074921563, + "mean_rel_dCD": 0.014494260892253188, + "mean_rel_dCD_to_10deg": 0.015259698719947253 + }, + "streamwise | AT=True": { + "rows": [ + [ + 1.02, + 0.2059248972230756, + 0.04938456957203894, + 0.2278625272194466, + 0.0490919571607997 + ], + [ + 4.02, + 0.45255813828253166, + 0.05159014087682369, + 0.4610104802890269, + 0.0516566817024969 + ], + [ + 7.02, + 0.6766157453438133, + 0.06448645726072087, + 0.6748642166054709, + 0.0643789856608573 + ], + [ + 10.02, + 0.8791753030226492, + 0.08387651514063242, + 0.8685945393341531, + 0.0841781647387978 + ], + [ + 13.02, + 0.9905545510822583, + 0.12087960013137622, + 1.042518829707241, + 0.1099699784207837 + ] + ], + "mean_abs_dCL": 0.018937308610937488, + "mean_rel_dCD": 0.02234138219639242, + "mean_rel_dCD_to_10deg": 0.0031253640750156493 + }, + "streamwise | AT=False": { + "rows": [ + [ + 1.02, + 0.2048916876005944, + 0.04938307929051503, + 0.2278625272194466, + 0.0490919571607997 + ], + [ + 4.02, + 0.44967510540389827, + 0.05155684407987055, + 0.4610104802890269, + 0.0516566817024969 + ], + [ + 7.02, + 0.6718792130981575, + 0.06435198087509855, + 0.6748642166054709, + 0.0643789856608573 + ], + [ + 10.02, + 0.8730212693217317, + 0.08354289367632899, + 0.8685945393341531, + 0.0841781647387978 + ], + [ + 13.02, + 0.984370793977973, + 0.11993378265818062, + 1.042518829707241, + 0.1099699784207837 + ] + ], + "mean_abs_dCL": 0.019973196745628186, + "mean_rel_dCD": 0.021286766032247502, + "mean_rel_dCD_to_10deg": 0.003957265868542562 + }, + "perpendicular | AT=True": { + "rows": [ + [ + 1.02, + 0.18383228969965923, + 0.050144635457638635, + 0.2278625272194466, + 0.0490919571607997 + ], + [ + 4.02, + 0.405910998991938, + 0.05271092699801286, + 0.4610104802890269, + 0.0516566817024969 + ], + [ + 7.02, + 0.6196634536160025, + 0.06610903493419854, + 0.6748642166054709, + 0.0643789856608573 + ], + [ + 10.02, + 0.819972470827242, + 0.08687187836811643, + 0.8685945393341531, + 0.0841781647387978 + ], + [ + 13.02, + 0.9540820679771876, + 0.11834544272848965, + 1.042518829707241, + 0.1099699784207837 + ] + ], + "mean_abs_dCL": 0.05827786240866182, + "mean_rel_dCD": 0.03537721729006136, + "mean_rel_dCD_to_10deg": 0.025181178902727072 + }, + "perpendicular | AT=False": { + "rows": [ + [ + 1.02, + 0.1914572947226679, + 0.05016001695238325, + 0.2278625272194466, + 0.0490919571607997 + ], + [ + 4.02, + 0.4196196471612632, + 0.052540613543299856, + 0.4610104802890269, + 0.0516566817024969 + ], + [ + 7.02, + 0.6353101482790231, + 0.06507351694329166, + 0.6748642166054709, + 0.0643789856608573 + ], + [ + 10.02, + 0.833730618179213, + 0.08384491217129221, + 0.8685945393341531, + 0.0841781647387978 + ], + [ + 13.02, + 0.9639945924411776, + 0.11244976731212249, + 1.042518829707241, + 0.1099699784207837 + ] + ], + "mean_abs_dCL": 0.04614765847439874, + "mean_rel_dCD": 0.01523294541343615, + "mean_rel_dCD_to_10deg": 0.013403759338079578 + } + } +} \ No newline at end of file diff --git a/docs/validation/leg_direction_study.py b/docs/validation/leg_direction_study.py new file mode 100644 index 0000000..b1e97a1 --- /dev/null +++ b/docs/validation/leg_direction_study.py @@ -0,0 +1,397 @@ +"""Attached-trailed leg direction: section ribs versus perpendicular to the +quarter-chord line or along the flow (``BodyAerodynamics(leg_direction=...)``). + +Gaunaa, Li & Pirrung (TORQUE 2026, Sec. 3) run the attached part of the +trailed vortex along the local chordwise direction of the Crossflow +Principle, perpendicular to the quarter-chord line, up to the trailing edge. +The code's default keeps the legs along the (interpolated) input ribs, while +the panel frame, chord and control point already follow CP1. This script +compares the two with the curved bound-vortex regularisation in place: + +* on-blade over Trefftz induced drag (inviscid) on a straight wing with ribs + drawn skewed (the same wing as the unskewed one: perpendicular and + streamwise legs must give 1.000), the kinked 30 deg swept wing, a yawed + wing, the arc, the swept-anhedral elliptic wing and the V3 kite, with the + attached-trailed (AT) force on and off; lifting-surface references for the + planar wings, the elliptic wing and the inviscid V3 are produced by + ``vlm_referee.py`` (AeroSandbox); +* the leg geometry on the V3 (how far the perpendicular legs reach along the + trailing edge at the curved tips); +* V3 viscous polars against RANS (Poland 2025). + +An earlier version of this study (commit 118e91d) used perpendicular legs of +fixed length (1 - ac) c, which end on the trailing edge only where it is +parallel to the quarter-chord line; those results are superseded. + +Usage (from the repository root, about 4 minutes; ``--figures`` only redraws +figures 10 and 11 from the stored JSON, which needs ``vlm_referee.json``):: + + python docs/validation/leg_direction_study.py +""" + +from __future__ import annotations + +import json +import logging +import time +from pathlib import Path + +import numpy as np +import pandas as pd + +from VSM.core.BodyAerodynamics import BodyAerodynamics +from VSM.core.Solver import Solver + +import curved_bound_vortex_validation as cbv + +logging.disable(logging.CRITICAL) +OUT = Path(__file__).resolve().parent +DATA = cbv.DATA +LEGS = ("rib", "streamwise", "perpendicular") + + +def ratio(body, at=True): + r = Solver(allowed_error=1e-7, max_iterations=4000, is_with_attached_trailed_vortex_force=at).solve(body) + return float(r["drag"] / r["drag_induced_trefftz"]), float(r["cl"]), bool(r["gamma_converged"]) + + +def leg_geometry(body): + """Leg length over (1 - ac) c and the distance between the leg end and the + section's own trailing-edge point over c, per node (max over the wing).""" + P = body.panels + bound = np.array([p.bound_point_1 for p in P] + [P[-1].bound_point_2]) + te = np.array([p.TE_point_1 for p in P] + [P[-1].TE_point_2]) + sec_te = np.array([p.corner_points[1] for p in P] + [P[-1].corner_points[2]]) + chords = np.array([p.chord for p in P]) + c_node = np.concatenate(([chords[0]], 0.5 * (chords[1:] + chords[:-1]), [chords[-1]])) + leg = np.linalg.norm(te - bound, axis=1) / (0.75 * c_node) + reach = np.linalg.norm(te - sec_te, axis=1) / c_node + return float(leg.max()), float(reach.max()) + + +def trefftz_table(): + print("\non-blade / Trefftz induced drag (CL), inviscid, regularised quarter-chord term") + cases = [ + ("straight AR 8", cbv.straight, (40, 80)), + ("straight, ribs skewed 22 deg", lambda n, **kw: cbv.straight(n, skew=0.4, **kw), (40, 80)), + ("swept 30 deg (kinked), streamwise ribs", cbv.swept, (40, 80, 160)), + ("yawed 30 deg, streamwise ribs", cbv.yawed, (40, 80)), + ("arc 110 deg, R = 4c", cbv.arc, (40, 80)), + ("elliptic, 10% sweep, 25% anhedral", cbv.elliptic_swept_anhedral, (40, 80, 160)), + ("V3 kite, inviscid, 10 deg", cbv.v3_inviscid, (40, 80, 160)), + ] + rows = {} + for case, mk, Ns in cases: + for legs in LEGS: + for at in (True, False): + vals = [] + for N in Ns: + try: + body = mk(N, leg_direction=legs) + rt, cl, conv = ratio(body, at) + if not conv: + rt = float("nan") + except ValueError as err: + rt, cl = float("nan"), float("nan") + print(f" [{case}, {legs}, N={N}] {err}") + vals.append((N, rt, cl)) + rows[f"{case} | {legs} | AT={at}"] = vals + print(f" {case:40s} {legs:13s} AT {'on ' if at else 'off'} ", end="") + print(" ".join(f"N={N}: {rt:.3f} ({cl:.3f})" for N, rt, cl in vals), flush=True) + return rows + + +def geometry_table(): + print("\nleg geometry: max leg length / (0.75 c) and max distance of the leg end from the section's own TE point / c") + out = {} + for case, mk in [ + ("straight, ribs skewed 22 deg", lambda n, **kw: cbv.straight(n, skew=0.4, **kw)), + ("swept 30 deg (kinked)", cbv.swept), + ("elliptic, 10% sweep, 25% anhedral", cbv.elliptic_swept_anhedral), + ("V3 kite", cbv.v3_inviscid), + ]: + for legs in ("perpendicular", "streamwise"): + for N in (40, 80): + try: + body = mk(N, leg_direction=legs) + except ValueError as err: + print(f" {case:36s} {legs:13s} N={N:3d}: {err}") + continue + leg, reach = leg_geometry(body) + out[f"{case} | {legs} | N={N}"] = {"max_leg_over_075c": leg, "max_reach_over_c": reach} + print(f" {case:36s} {legs:13s} N={N:3d}: leg/(0.75c) {leg:.2f} reach/c {reach:.2f}") + return out + + +def v3_vs_rans(): + print("\nV3 viscous polars (CAD, 2D RANS Re 5e5 polars, 50 panels) against RANS") + cfd = pd.read_csv(DATA / "3D_polars_literature" / "CFD_RANS_Rey_10e5_Poland2025_alpha_sweep_beta_0.csv") + cfd = cfd[cfd.alpha < 14] + ref = np.array([0.422646, 0.0, 9.3667]) + out = {} + for legs in LEGS: + body = BodyAerodynamics.instantiate( + n_panels=50, + file_path=DATA / "CAD_derived_geometry" / "aero_geometry_CAD_CFD_NF_combined.yaml", + spanwise_panel_distribution="uniform", + leg_direction=legs, + ) + for at in (True, False): + s = Solver(reference_point=ref, is_with_attached_trailed_vortex_force=at) + rows = [] + for al, cl_ref, cd_ref in zip(cfd.alpha, cfd.CL, cfd.CD): + body.va_initialize(2.82, float(al), 0.0) + r = s.solve(body) + rows.append((float(al), float(r["cl"]), float(r["cd"]), float(cl_ref), float(cd_ref))) + e_cl = float(np.mean([abs(a[1] - a[3]) for a in rows])) + e_cd = float(np.mean([abs(a[2] - a[4]) / a[4] for a in rows])) + e_cd10 = float(np.mean([abs(a[2] - a[4]) / a[4] for a in rows if a[0] <= 10.5])) + out[f"{legs} | AT={at}"] = {"rows": rows, "mean_abs_dCL": e_cl, "mean_rel_dCD": e_cd, "mean_rel_dCD_to_10deg": e_cd10} + print(f" {legs:13s} AT {'on ' if at else 'off'}: " + " ".join(f"{a[0]:.0f}deg CL {a[1]:.3f}/{a[3]:.3f} CD {a[2]:.4f}/{a[4]:.4f}" for a in rows)) + print(f" {'':13s} mean |dCL| {e_cl:.3f} mean |dCD|/CD {100 * e_cd:.1f}% (to 10 deg {100 * e_cd10:.1f}%)", flush=True) + return out + + +# -------------------------------------------------------------------------- +# Figures 10 (geometry of the three leg definitions) and 11 (VLM referee) +# -------------------------------------------------------------------------- +FIG = OUT.parent / "figures" +COLOR = {"rib": "tab:blue", "streamwise": "tab:green", "perpendicular": "tab:red"} + + +def _geometry(body): + P = body.panels + bound = np.array([p.bound_point_1 for p in P] + [P[-1].bound_point_2]) + te = np.array([p.TE_point_1 for p in P] + [P[-1].TE_point_2]) + ac = np.array([p.aerodynamic_center for p in P]) + cp = np.array([p.control_point for p in P]) + le_sec = np.array([p.corner_points[0] for p in P] + [P[-1].corner_points[3]]) + te_sec = np.array([p.corner_points[1] for p in P] + [P[-1].corner_points[2]]) + return bound, te, ac, cp, le_sec, te_sec + + +def _vlm_quads(xsecs, n_chord, n_span): + """Panel corners of the AeroSandbox lattice (None if AeroSandbox is absent).""" + try: + import aerosandbox as asb + except ImportError: + return None + wing = asb.Wing(symmetric=False, xsecs=xsecs) + if n_span > 1: + wing = wing.subdivide_sections(n_span) + points, faces = wing.mesh_thin_surface(method="quad", chordwise_resolution=n_chord, add_camber=False) + return np.asarray(points)[np.asarray(faces)] + + +def _draw(ax, body, legs, ij, lattice=None, outline=True, lw=1.2): + i, j = ij + bound, te, ac, cp, le_sec, te_sec = _geometry(body) + if lattice is not None: + for q in lattice: + q = np.vstack([q, q[:1]]) + ax.plot(q[:, i], q[:, j], color="0.8", lw=0.5, zorder=0) + if outline: + ax.plot(le_sec[:, i], le_sec[:, j], color="0.45", lw=0.8, zorder=1) + ax.plot(te_sec[:, i], te_sec[:, j], color="0.45", lw=0.8, zorder=1) + for a, b in zip(le_sec, te_sec): + ax.plot([a[i], b[i]], [a[j], b[j]], color="0.75", lw=0.5, zorder=1) + ax.plot(bound[:, i], bound[:, j], color="k", lw=2.0, zorder=3) + for a, b in zip(bound, te): + ax.plot([a[i], b[i]], [a[j], b[j]], color=COLOR[legs], lw=lw, zorder=2) + ax.plot(ac[:, i], ac[:, j], "o", color="k", ms=2.5, zorder=4) + ax.plot(cp[:, i], cp[:, j], "+", color=COLOR[legs], ms=6, mew=1.3, zorder=4) + ax.set_aspect("equal") + + +def figure_10(): + import matplotlib + + matplotlib.use("Agg") + import matplotlib.pyplot as plt + from matplotlib.lines import Line2D + + try: + import aerosandbox as asb + + FLAT = asb.Airfoil("naca0010") + xs = lambda pts, c=1.0: [asb.WingXSec(xyz_le=p, chord=c, twist=0.0, airfoil=FLAT) for p in pts] + except ImportError: + asb = None + + plt.rcParams.update({"font.size": 9}) + fig, ax = plt.subplots(2, 3, figsize=(15, 9.5)) + tan30 = np.tan(np.radians(30.0)) + + # (a) straight wing with skewed ribs, tip region: rib vs streamwise (= perpendicular) + A = ax[0, 0] + lat = _vlm_quads(xs([[0, -4, 0], [0, 4, 0]]), 4, 20) if asb else None + _draw(A, cbv.straight(20, skew=0.4), "rib", (1, 0), lattice=lat) + _draw(A, cbv.straight(20, skew=0.4, leg_direction="streamwise"), "streamwise", (1, 0), outline=False) + A.set_xlim(2.0, 4.6); A.set_ylim(1.6, -0.3) + A.set_title("(a) straight wing, ribs drawn 22 deg skewed: tip") + A.set_xlabel("y"); A.set_ylabel("x (flow downwards)") + + # (b) kinked swept wing, root region: rib (= streamwise) vs perpendicular + B = ax[0, 1] + lat = _vlm_quads(xs([[4 * tan30, -4, 0], [0, 0, 0], [4 * tan30, 4, 0]]), 4, 10) if asb else None + _draw(B, cbv.swept(40), "rib", (1, 0), lattice=lat) + _draw(B, cbv.swept(40, leg_direction="perpendicular"), "perpendicular", (1, 0), outline=False) + B.set_xlim(-1.6, 1.6); B.set_ylim(2.0, -0.4) + B.set_title("(b) kinked 30 deg swept wing: root") + B.set_xlabel("y"); B.set_ylabel("x (flow downwards)") + + # (c) elliptic swept-anhedral wing, right half, top view + C = ax[0, 2] + s = 8.0 * (np.pi / 4) / 2 + pts, chords = [], [] + for y in np.linspace(-s, s, 41): + eta = min(abs(y) / s, 0.999999) + pts.append([0.10 * s * eta**2, y, -0.25 * s * eta**2]); chords.append(np.sqrt(1 - eta**2)) + lat = None + if asb: + lat = _vlm_quads([asb.WingXSec(xyz_le=p, chord=c, twist=0.0, airfoil=FLAT) for p, c in zip(pts, chords)], 4, 1) + for legs in ("rib", "streamwise", "perpendicular"): + _draw(C, cbv.elliptic_swept_anhedral(40, leg_direction=legs), legs, (1, 0), lattice=lat if legs == "rib" else None, outline=legs == "rib", lw=1.0) + C.set_xlim(0.0, 3.3); C.set_ylim(1.4, -0.2) + C.set_title("(c) elliptic, 10% sweep, 25% anhedral: right half, top view") + C.set_xlabel("y"); C.set_ylabel("x (flow downwards)") + + # (d) arc wing, side view of the upper tip: streamwise legs climb with the flow + D = ax[1, 0] + for legs in ("rib", "streamwise"): + _draw(D, cbv.arc(40, leg_direction=legs), legs, (0, 2), outline=legs == "rib", lw=1.0) + D.set_xlim(-0.2, 1.4); D.set_ylim(2.1, 4.2) + D.set_title("(d) arc wing, 8 deg: side view of the tip region (x-z)") + D.set_xlabel("x"); D.set_ylabel("z") + + # (e) V3 kite, top view, whole wing, with the VLM lattice + E = ax[1, 1] + lat = None + if asb: + import vlm_referee + + lat = _vlm_quads(vlm_referee.v3_xsecs(), 4, 2) + v3 = {legs: cbv.v3_inviscid(40, leg_direction=legs) for legs in ("rib", "streamwise", "perpendicular")} + for legs, body in v3.items(): + _draw(E, body, legs, (1, 0), lattice=lat if legs == "rib" else None, outline=legs == "rib", lw=0.8) + E.invert_yaxis() + E.set_title("(e) V3 kite, 10 deg: top view") + E.set_xlabel("y"); E.set_ylabel("x (flow downwards)") + + # (f) V3 kite, the +y tip, top view + F = ax[1, 2] + for legs, body in v3.items(): + _draw(F, body, legs, (1, 0), outline=legs == "rib", lw=1.2) + bound = _geometry(v3["rib"])[0] + F.set_xlim(bound[:, 1].max() - 2.2, bound[:, 1].max() + 0.3) + xr = bound[:4, 0] + F.set_ylim(xr.max() + 1.8, xr.min() - 0.5) + F.set_title("(f) V3 kite: tip region") + F.set_xlabel("y"); F.set_ylabel("x (flow downwards)") + + handles = [ + Line2D([], [], color="k", lw=2, label="bound vortex (quarter-chord line)"), + Line2D([], [], color="0.45", lw=0.8, label="section outline (LE, TE, ribs)"), + Line2D([], [], color="0.8", lw=0.6, label="VLM lattice (AeroSandbox, coarse)"), + Line2D([], [], color="k", marker="o", ls="", ms=3, label="aerodynamic centre"), + Line2D([], [], color="tab:blue", lw=1.5, marker="+", ms=7, label="rib legs, control point"), + Line2D([], [], color="tab:green", lw=1.5, marker="+", ms=7, label="streamwise legs, control point"), + Line2D([], [], color="tab:red", lw=1.5, marker="+", ms=7, label="perpendicular legs, control point"), + ] + fig.legend(handles=handles, loc="lower center", ncol=4, frameon=False, bbox_to_anchor=(0.5, 0.0)) + fig.suptitle("Attached-trailed legs: along the ribs, along the flow, or perpendicular to the quarter-chord line", y=0.995) + fig.tight_layout(rect=(0, 0.05, 1, 0.98)) + fig.savefig(FIG / "10_leg_direction_geometry.png", dpi=150) + print(f"figure 10 written to {FIG / '10_leg_direction_geometry.png'}") + + +def figure_11(): + import matplotlib + + matplotlib.use("Agg") + import matplotlib.pyplot as plt + + ref = json.load(open(OUT / "vlm_referee.json")) + plt.rcParams.update({"font.size": 9}) + fig, ax = plt.subplots(2, 3, figsize=(15, 8.5)) + + # top: circulation distributions, finest VLM vs VSM at the finest N + panels = [("kinked swept 30 deg", "kinked 30 deg swept wing, 5 deg", 20.0), + ("elliptic", "elliptic, 10% sweep, 25% anhedral, 10 deg", 10.0), + ("V3", "V3 kite, flat-plate sections, 10 deg", 2.82)] + for A, (case, title, U) in zip(ax[0], panels): + row = ref[case] + vkeys = [k for k in row if k.startswith("VLM")] + v = row[vkeys[-1]] + o = np.argsort(v["y"]) + A.plot(np.array(v["y"])[o], np.array(v["gamma"])[o] / U, "k-", lw=2, label=f"VLM ({vkeys[-1][4:]})") + for legs in LEGS: + keys = [k for k in row if k.startswith(f"VSM {legs}")] + if not keys: + continue + r = row[keys[-1]] + o = np.argsort(r["y"]) + A.plot(np.array(r["y"])[o], np.array(r["gamma"])[o] / U, color=COLOR[legs], lw=1.3, label=f"VSM, {legs} legs ({keys[-1].split()[-1]})") + A.set_title(title); A.set_xlabel("y"); A.set_ylabel("Gamma / U") + A.legend(fontsize=8, frameon=False) + + # bottom: CL and induced drag relative to the VLM (Trefftz drag of the VLM circulation) + cases = ["straight AR 8", "yawed 30 deg", "kinked swept 30 deg", "arc", "elliptic", "V3"] + labels = ["straight", "yawed 30", "kinked 30", "arc", "elliptic", "V3"] + for A, quantity, ylabel in zip(ax[1, :2], ("CL", "CDi"), ("CL / CL_VLM", "CDi (on-blade) / CDi_VLM (Trefftz)")): + x = np.arange(len(cases)) + for k, legs in enumerate(LEGS): + vals = [] + for case in cases: + row = ref[case] + vkeys = [kk for kk in row if kk.startswith("VLM")] + v = row[vkeys[-1]] + keys = [kk for kk in row if kk.startswith(f"VSM {legs}")] + if not keys: + vals.append(np.nan) + continue + r = row[keys[-1]] + vals.append(r["CL"] / v["CL"] if quantity == "CL" else r["CDi_blade"] / v["CDi_trefftz"]) + A.bar(x + (k - 1) * 0.26, vals, 0.26, color=COLOR[legs], label=f"{legs} legs") + A.axhline(1.0, color="k", lw=0.8) + A.set_xticks(x); A.set_xticklabels(labels) + A.set_ylim(0.85, 1.15); A.set_ylabel(ylabel); A.legend(fontsize=8, frameon=False, ncol=3, loc="upper left") + ax[1, 0].set_title("lift relative to the VLM") + ax[1, 1].set_title("induced drag relative to the VLM") + + # bottom right: the V3 viscous polars against RANS for the three leg options + A = ax[1, 2] + st = json.load(open(OUT / "leg_direction_study.json"))["v3_vs_rans"] + for legs in LEGS: + rows = np.array(st[f"{legs} | AT=True"]["rows"]) + A.plot(rows[:, 0], rows[:, 2], "-o", color=COLOR[legs], ms=3, label=f"VSM, {legs} legs") + A.plot(rows[:, 0], rows[:, 4], "k--s", ms=3, label="RANS (Poland 2025)") + A.set_title("V3 viscous drag against RANS (2D RANS polars, 50 panels)") + A.set_xlabel("alpha [deg]"); A.set_ylabel("CD"); A.legend(fontsize=8, frameon=False) + fig.suptitle("Leg direction against a vortex-lattice referee (AeroSandbox) and RANS", y=0.995) + fig.tight_layout(rect=(0, 0, 1, 0.97)) + fig.savefig(FIG / "11_leg_direction_referee.png", dpi=150) + print(f"figure 11 written to {FIG / '11_leg_direction_referee.png'}") + + +def main(argv=None): + import argparse + + ap = argparse.ArgumentParser(description="rib / streamwise / perpendicular attached-trailed legs") + ap.add_argument("--figures", action="store_true", help="only draw figures 10 and 11 from the stored results") + args = ap.parse_args(argv) + if args.figures: + figure_10() + figure_11() + return + t0 = time.time() + out = {"trefftz": trefftz_table(), "geometry": geometry_table(), "v3_vs_rans": v3_vs_rans()} + json.dump(out, open(OUT / "leg_direction_study.json", "w"), indent=1) + print(f"\nresults written to {OUT / 'leg_direction_study.json'} ({time.time() - t0:.0f} s)") + figure_10() + if (OUT / "vlm_referee.json").exists(): + figure_11() + + +if __name__ == "__main__": + main() diff --git a/docs/validation/strip_weight_sensitivity.py b/docs/validation/strip_weight_sensitivity.py new file mode 100644 index 0000000..9d1add4 --- /dev/null +++ b/docs/validation/strip_weight_sensitivity.py @@ -0,0 +1,96 @@ +"""Sensitivity of the curved bound-vortex correction to the chordwise +distribution of the strips (report Section 4.2). + +The implemented strips carry the flat-plate thin-airfoil loading +gamma(x) ~ sqrt((c - x)/x). Here they are replaced by the loading of real +sections from NeuralFoil, gamma(x) = ue_upper - ue_lower at its 32 +boundary-layer stations (the leading-edge peak below x/c = 0.016 is not +sampled, which biases the centroid aft by a few percent of chord): a NACA +0012 as a check, the V3 centre section (LEI profile, data/TUDELFT_V3_KITE) +at 5, 10 and 15 deg, and a NACA 4412. For each distribution the effective +cut-off c_eff = exp and the centroid are printed, then the +on-blade / Trefftz ratio and CD of the arc wing and the V3 (inviscid polars). + +Usage (from the repository root, about two minutes):: + + PYTHONPATH=docs/validation python docs/validation/strip_weight_sensitivity.py +""" +import warnings +import numpy as np + +warnings.filterwarnings("ignore") +import aerosandbox as asb +import neuralfoil as nf +import yaml + +import VSM.core.BodyAerodynamics as BA +import curved_bound_vortex_validation as cbv +from curved_bound_vortex_validation import ratio, arc, v3_inviscid, solve, DATA +from VSM.core.utils import thin_airfoil_strips + +XB = np.asarray(nf.bl_x_points) + + +def nf_weights(airfoil, alpha, Re=5.6e5, **kw): + out = airfoil.get_aero_from_neuralfoil(alpha=alpha, Re=Re, model_size="xxxlarge", **kw) + ue_u = np.array([out[f"upper_bl_ue/vinf_{k}"] for k in range(32)]).ravel() + ue_l = np.array([out[f"lower_bl_ue/vinf_{k}"] for k in range(32)]).ravel() + g = ue_u + ue_l # NeuralFoil lower-surface ue is signed negative; the jump is ue_u - (-ue_l) + dx = np.gradient(XB) + w = g * dx + circ = w.sum() # integral gamma dx / (U c) ~ CL / 2 + return XB.copy(), w / w.sum(), float(out["CL"]), circ + + +def stats(x, w): + return float(np.exp(np.sum(w * np.log(np.abs(x - 0.25))))), float(np.sum(w * x)) + + +def main(): + x0, w0 = thin_airfoil_strips() + dists = {"flat plate, thin airfoil (implemented)": (x0, w0)} + print(f"{'distribution':48s} {'c_eff':>6s} {'centroid':>9s} {'CL_nf':>6s} {'2*int(gamma)':>12s}") + print(f"{'flat plate, thin airfoil (implemented)':48s} {stats(x0, w0)[0]:6.3f} {stats(x0, w0)[1]:9.3f}") + + # flat plate through NeuralFoil, as a check of the pipeline + plate = asb.Airfoil("naca0012") + x, w, cl, circ = nf_weights(plate, 5.0) + print(f"{'naca0012 at 5 deg, NeuralFoil':48s} {stats(x, w)[0]:6.3f} {stats(x, w)[1]:9.3f} {cl:6.3f} {2 * circ:12.3f}") + + d = yaml.safe_load(open(DATA / "CAD_derived_geometry" / "aero_geometry_CAD_neuralfoil.yaml")) + rows = d["wing_sections"]["data"] + ids = [r[0] for r in rows] + print("airfoil ids along the span:", ids) + mid = rows[len(rows) // 2][0] + af_info = {r[0]: r[2] for r in d["wing_airfoils"]["data"]}[mid] + dat = DATA / "CAD_derived_geometry" / af_info["dat_file_path"] + coords = np.loadtxt(dat, delimiter=",") + lei = asb.Airfoil(name=f"V3 section {mid}", coordinates=coords) + nf_kw = dict(xtr_lower=af_info.get("xtr_lower", 1.0), xtr_upper=af_info.get("xtr_upper", 1.0), n_crit=af_info.get("n_crit", 9.0)) + for a in (5.0, 10.0, 15.0): + x, w, cl, circ = nf_weights(lei, a, **nf_kw) + name = f"V3 mid section {mid} at {a:.0f} deg, NeuralFoil" + print(f"{name:48s} {stats(x, w)[0]:6.3f} {stats(x, w)[1]:9.3f} {cl:6.3f} {2 * circ:12.3f}") + dists[name] = (x, w) + x, w, cl, circ = nf_weights(asb.Airfoil("naca4412"), 10.0) + print(f"{'naca4412 at 10 deg, NeuralFoil':48s} {stats(x, w)[0]:6.3f} {stats(x, w)[1]:9.3f} {cl:6.3f} {2 * circ:12.3f}") + dists["naca4412 at 10 deg, NeuralFoil"] = (x, w) + + print("\non-blade / Trefftz (CD) with each distribution as strip weights") + cases = [("arc 110 deg, R = 4c", arc, (40, 80)), ("V3 kite, inviscid, 10 deg", v3_inviscid, (40, 80))] + for name, (x, w) in dists.items(): + xx = np.ascontiguousarray(x, dtype=float) + ww = np.ascontiguousarray(w, dtype=float) + BA.thin_airfoil_strips = lambda n_strips=24, xx=xx, ww=ww: (xx, ww) + line = f" {name:48s}" + for cname, mk, Ns in cases: + for N in Ns: + r = solve(mk(N), cbv.REGULARISED) + rt = float(r["drag"] / r["drag_induced_trefftz"]) + line += f" {cname[:3]} N={N}: {rt:.3f} ({float(r['cd']):.4f})" + print(line, flush=True) + BA.thin_airfoil_strips = thin_airfoil_strips + + +if __name__ == "__main__": + main() diff --git a/docs/validation/unrolling_figure.py b/docs/validation/unrolling_figure.py new file mode 100644 index 0000000..da7ec7c --- /dev/null +++ b/docs/validation/unrolling_figure.py @@ -0,0 +1,147 @@ +"""Figure 14 of docs/figures: what "unrolling the wing onto a panel's tangent" +means in the curved bound-vortex regularisation (report Section 4, eq. 6). + +For an evaluation panel i the reference wing is built on panel i's own +tangent line through its aerodynamic centre: every node keeps its arc-length +coordinate along the bound line, the legs are perpendicular to the line in +panel i's chord direction with each panel's own chord, and the wake keeps its +direction. The reference is different for every panel. The figure shows it +for two panels of the arc wing (3-D and front view), for a panel of the +kinked swept wing (top view), and how far the unrolled nodes move from the +actual ones as a function of the distance from the evaluation panel (the +near field cancels in the difference, the far field is the curvature effect). + +Usage (from the repository root, seconds):: + + python docs/validation/unrolling_figure.py +""" + +from __future__ import annotations + +import logging +from pathlib import Path + +import numpy as np + +import curved_bound_vortex_validation as cbv + +logging.disable(logging.CRITICAL) +FIG = Path(__file__).resolve().parents[1] / "figures" + + +def geometry(body): + P = body.panels + bound = np.array([p.bound_point_1 for p in P] + [P[-1].bound_point_2]) + te = np.array([p.TE_point_1 for p in P] + [P[-1].TE_point_2]) + ac = np.array([p.aerodynamic_center for p in P]) + chord_dir = np.array([p.y_airf for p in P]) + chords = np.array([p.chord for p in P]) + return bound, te, ac, chord_dir, chords + + +def unrolled(bound, te, ac, chord_dir, chords, i, ac_fraction=0.25): + """Nodes and leg ends of the reference wing of panel i (as the kernel builds it).""" + seg = bound[1:] - bound[:-1] + L = np.linalg.norm(seg, axis=1) + s = np.concatenate(([0.0], np.cumsum(L))) + t = seg[i] / L[i] + s_P = 0.5 * (s[i] + s[i + 1]) + nodes = ac[i][None, :] + (s - s_P)[:, None] * t[None, :] + # leg of node k: panel i's chord direction, the adjacent panel's chord + c_node = np.concatenate(([chords[0]], 0.5 * (chords[1:] + chords[:-1]), [chords[-1]])) + ends = nodes + (1.0 - ac_fraction) * c_node[:, None] * chord_dir[i][None, :] + return nodes, ends, s - s_P + + +def draw_wing(ax, bound, te, ij=None, color="k", lw=1.2, alpha=1.0, label=None, legs=True): + if ij is None: + ax.plot(bound[:, 0], bound[:, 1], bound[:, 2], color=color, lw=lw, alpha=alpha, label=label) + if legs: + for a, b in zip(bound, te): + ax.plot([a[0], b[0]], [a[1], b[1]], [a[2], b[2]], color=color, lw=0.6, alpha=alpha) + ax.plot(te[:, 0], te[:, 1], te[:, 2], color=color, lw=0.6, alpha=alpha, ls=":") + else: + u, v = ij + ax.plot(bound[:, u], bound[:, v], color=color, lw=lw, alpha=alpha, label=label) + if legs: + for a, b in zip(bound, te): + ax.plot([a[u], b[u]], [a[v], b[v]], color=color, lw=0.6, alpha=alpha) + ax.plot(te[:, u], te[:, v], color=color, lw=0.6, alpha=alpha, ls=":") + + +def main(): + import matplotlib + + matplotlib.use("Agg") + import matplotlib.pyplot as plt + + plt.rcParams.update({"font.size": 9}) + fig = plt.figure(figsize=(15, 9.5)) + ax3 = fig.add_subplot(2, 2, 1, projection="3d") + axf = fig.add_subplot(2, 2, 2) + axt = fig.add_subplot(2, 2, 3) + axd = fig.add_subplot(2, 2, 4) + + # ---- arc wing, two evaluation panels ---- + body = cbv.arc(20) + bound, te, ac, cdir, chords = geometry(body) + picks = [(10, "tab:blue", "panel 10 (mid-span)"), (2, "tab:red", "panel 2 (near the tip)")] + draw_wing(ax3, bound, te, color="k", label="actual wing (bound line, legs, trailing edge)") + draw_wing(axf, bound, te, ij=(1, 2), color="k", label="actual wing") + for i, col, lab in picks: + nodes, ends, _ = unrolled(bound, te, ac, cdir, chords, i) + draw_wing(ax3, nodes, ends, color=col, lw=1.4, alpha=0.9, label=f"unrolled reference of {lab}") + draw_wing(axf, nodes, ends, ij=(1, 2), color=col, lw=1.4, alpha=0.9, label=f"unrolled reference of {lab}") + ax3.plot([ac[i, 0]], [ac[i, 1]], [ac[i, 2]], "*", color=col, ms=12) + axf.plot([ac[i, 1]], [ac[i, 2]], "*", color=col, ms=12) + axf.plot(nodes[:, 1], nodes[:, 2], "o", color=col, ms=2.5) + ax3.set_xlabel("x"); ax3.set_ylabel("y"); ax3.set_zlabel("z") + ax3.set_title("(a) arc wing: the reference wing is straight, tangent at the evaluation panel") + ax3.view_init(elev=22, azim=-60) + ax3.set_box_aspect((1.0, 3.0, 1.6)) + ax3.legend(fontsize=7, loc="upper left", frameon=False) + axf.set_aspect("equal"); axf.set_xlabel("y"); axf.set_ylabel("z") + axf.set_title("(b) front view: nodes keep their arc-length distance from the evaluation panel") + axf.legend(fontsize=7, frameon=False, loc="lower center") + + # ---- kinked swept wing, one evaluation panel on the right half ---- + body = cbv.swept(20) + bound, te, ac, cdir, chords = geometry(body) + i = 5 + nodes, ends, _ = unrolled(bound, te, ac, cdir, chords, i) + draw_wing(axt, bound, te, ij=(1, 0), color="k", label="actual wing (ribs along the flow)") + draw_wing(axt, nodes, ends, ij=(1, 0), color="tab:red", lw=1.4, label=f"unrolled reference of panel {i}: legs along its chord direction") + axt.plot([ac[i, 1]], [ac[i, 0]], "*", color="tab:red", ms=12) + axt.plot(nodes[:, 1], nodes[:, 0], "o", color="tab:red", ms=2.5) + axt.invert_yaxis(); axt.set_aspect("equal") + axt.set_xlabel("y"); axt.set_ylabel("x (flow downwards)") + axt.set_title("(c) kinked swept wing, top view: the other half is laid along the panel's own line") + axt.legend(fontsize=7, frameon=False, loc="lower left") + + # ---- how far the unrolled nodes sit from the actual ones ---- + body = cbv.arc(40) + bound, te, ac, cdir, chords = geometry(body) + for i, col, lab in ((20, "tab:blue", "panel 20 (mid-span)"), (4, "tab:red", "panel 4 (near the tip)")): + nodes, ends, ds = unrolled(bound, te, ac, cdir, chords, i) + gap = np.linalg.norm(nodes - bound, axis=1) + axd.plot(np.abs(ds), gap, "o-", color=col, ms=3, label=f"arc wing, {lab}") + body = cbv.v3_inviscid(40) + bound, te, ac, cdir, chords = geometry(body) + for i, col, lab in ((20, "tab:green", "V3, panel 20 (mid-span)"), (3, "tab:orange", "V3, panel 3 (tip)")): + nodes, ends, ds = unrolled(bound, te, ac, cdir, chords, i) + gap = np.linalg.norm(nodes - bound, axis=1) + axd.plot(np.abs(ds), gap, "s--", color=col, ms=3, label=lab) + axd.axvspan(0, 0.75, color="0.9", label="within one leg length (0.75 c)") + axd.set_xlabel("arc-length distance of node from the evaluation panel") + axd.set_ylabel("distance between actual and unrolled node") + axd.set_title("(d) the near field coincides and cancels; the far field is the curvature effect") + axd.legend(fontsize=7, frameon=False) + fig.suptitle("Unrolling the wing onto the evaluation panel's tangent: one reference wing per panel", y=0.995) + fig.tight_layout(rect=(0, 0, 1, 0.97)) + out = FIG / "14_unrolling_per_panel.png" + fig.savefig(out, dpi=150) + print("figure 14 written to", out) + + +if __name__ == "__main__": + main() diff --git a/docs/validation/vlm_referee.json b/docs/validation/vlm_referee.json new file mode 100644 index 0000000..d08a292 --- /dev/null +++ b/docs/validation/vlm_referee.json @@ -0,0 +1,7509 @@ +{ + "straight AR 8": { + "VLM 8x40": { + "CL": 0.40416905089811733, + "CDi_near": 0.006535009278368528, + "CDi_trefftz": 0.006539800433079701, + "gamma": [ + 0.6297273825613214, + 1.0823274472513496, + 1.5515325114610397, + 1.9995535099348678, + 2.414886186745283, + 2.7908876104238973, + 3.124088508932673, + 3.413809411566639, + 3.6616805081063744, + 3.870940311147481, + 4.045682389082943, + 4.190229975196359, + 4.308714183452077, + 4.404839515135902, + 4.481782945421413, + 4.542173667205085, + 4.5881146782573285, + 4.6212214484239595, + 4.642663055491198, + 4.653197610379467, + 4.653197610379467, + 4.642663055491199, + 4.6212214484239595, + 4.588114678257329, + 4.542173667205086, + 4.481782945421414, + 4.404839515135903, + 4.308714183452077, + 4.190229975196359, + 4.045682389082943, + 3.8709403111474803, + 3.6616805081063744, + 3.4138094115666386, + 3.1240885089326738, + 2.7908876104238978, + 2.414886186745283, + 1.9995535099348678, + 1.5515325114610397, + 1.0823274472513502, + 0.6297273825613215 + ], + "y": [ + -3.993834667466256, + -3.9692113486565317, + -3.9201165219856287, + -3.8468528733856604, + -3.7498720976128808, + -3.629772113399309, + -3.48729337708492, + -3.3233143174580793, + -3.138845919949957, + -2.935025493573157, + -2.713109659033462, + -2.4744666012453127, + -2.2205676340168434, + -1.952978128910991, + -1.6733478642092727, + -1.3834008534800741, + -1.0849247164617055, + -0.7797596577922722, + -0.4697871215361512, + -0.15691819145568964, + 0.15691819145569008, + 0.469787121536152, + 0.779759657792273, + 1.084924716461706, + 1.3834008534800746, + 1.6733478642092732, + 1.952978128910991, + 2.220567634016844, + 2.4744666012453136, + 2.7131096590334622, + 2.935025493573157, + 3.138845919949957, + 3.3233143174580793, + 3.48729337708492, + 3.629772113399309, + 3.7498720976128808, + 3.8468528733856604, + 3.9201165219856287, + 3.9692113486565317, + 3.993834667466256 + ] + }, + "VLM 16x80": { + "CL": 0.4016965040738487, + "CDi_near": 0.006527916792919473, + "CDi_trefftz": 0.006541496633792225, + "gamma": [ + 0.3258725025897388, + 0.5622159079013166, + 0.813754930751498, + 1.064453444202746, + 1.311069091444503, + 1.5518237472939884, + 1.7853720653469294, + 2.0105975458712413, + 2.226562198445116, + 2.4324968074829756, + 2.627802961202396, + 2.812056617527908, + 2.9850084731210123, + 3.146578743194059, + 3.2968455113502566, + 3.4360270935238613, + 3.564459858368091, + 3.682573525106663, + 3.7908660618674865, + 3.8898800068229824, + 3.9801814972076435, + 4.0623426987745574, + 4.136927815161665, + 4.20448248845656, + 4.2655261857641635, + 4.320547074370076, + 4.369998881309428, + 4.414299275887449, + 4.453829379010784, + 4.488934073721972, + 4.519922857448925, + 4.547071033832694, + 4.570621089401581, + 4.590784138311167, + 4.6077413480890765, + 4.621645282227984, + 4.632621112912091, + 4.640767670321074, + 4.646158304816871, + 4.648841545690605, + 4.648841545690605, + 4.646158304816871, + 4.640767670321074, + 4.6326211129120916, + 4.621645282227984, + 4.6077413480890765, + 4.590784138311165, + 4.57062108940158, + 4.547071033832693, + 4.519922857448925, + 4.488934073721972, + 4.4538293790107835, + 4.4142992758874495, + 4.369998881309428, + 4.320547074370075, + 4.265526185764163, + 4.204482488456559, + 4.136927815161666, + 4.0623426987745574, + 3.9801814972076444, + 3.889880006822982, + 3.7908660618674865, + 3.6825735251066627, + 3.564459858368092, + 3.436027093523861, + 3.2968455113502557, + 3.146578743194058, + 2.9850084731210114, + 2.812056617527908, + 2.627802961202396, + 2.432496807482975, + 2.2265621984451145, + 2.0105975458712377, + 1.7853720653469263, + 1.5518237472939878, + 1.3110690914445025, + 1.0644534442027462, + 0.813754930751498, + 0.5622159079013166, + 0.3258725025897387 + ], + "y": [ + -3.998458072481446, + -3.992292739947702, + -3.979971581376109, + -3.9615135951001283, + -3.9369472419967364, + -3.906310401601814, + -3.8696503137026474, + -3.8270235054976016, + -3.7784957044352754, + -3.7241417368675416, + -3.664045412672736, + -3.5982993960268983, + -3.5270050625223295, + -3.4502723428537783, + -3.368219553313275, + -3.2809732133549856, + -3.1886678505113846, + -3.0914457929615518, + -2.989456950071433, + -2.882858581244466, + -2.771815053438978, + -2.6564975877262498, + -2.5370839952800353, + -2.413758403204614, + -2.2867109706241497, + -2.1561375954711015, + -2.0222396124258077, + -1.8852234824730036, + -1.7453004745539493, + -1.602686339805035, + -1.4576009788851652, + -1.3102681029048808, + -1.1609148884800433, + -1.009771627441959, + -0.857071371744067, + -0.703049574112718, + -0.5479437249961366, + -0.3919929863713647, + -0.23543782297382687, + -0.07851963151813723, + 0.07851963151813734, + 0.23543782297382743, + 0.3919929863713656, + 0.5479437249961374, + 0.7030495741127187, + 0.8570713717440679, + 1.0097716274419595, + 1.1609148884800433, + 1.3102681029048808, + 1.4576009788851656, + 1.6026863398050355, + 1.7453004745539493, + 1.8852234824730036, + 2.0222396124258077, + 2.156137595471102, + 2.2867109706241506, + 2.4137584032046147, + 2.5370839952800357, + 2.65649758772625, + 2.771815053438978, + 2.8828585812444665, + 2.9894569500714336, + 3.0914457929615518, + 3.1886678505113846, + 3.2809732133549856, + 3.368219553313275, + 3.4502723428537783, + 3.5270050625223295, + 3.5982993960268983, + 3.664045412672736, + 3.7241417368675416, + 3.7784957044352754, + 3.827023505497602, + 3.869650313702648, + 3.906310401601814, + 3.9369472419967364, + 3.9615135951001283, + 3.979971581376109, + 3.992292739947702, + 3.998458072481446 + ] + }, + "VSM rib N=40": { + "CL": 0.4037683141001728, + "CDi_blade": 0.006502921040432443, + "CDi_trefftz": 0.006502763924929592, + "converged": true, + "gamma": [ + 1.8062024190837223, + 2.5746631205888657, + 3.0631669173694633, + 3.408901848949248, + 3.6669410211290234, + 3.865973608144754, + 4.0230132699145535, + 4.148967839152865, + 4.251206678481653, + 4.334899616636186, + 4.403776951377747, + 4.460588269346439, + 4.507392668866529, + 4.545749081935775, + 4.576844613149878, + 4.601582895296278, + 4.620645739683732, + 4.634536341590136, + 4.643609285752358, + 4.648090705579536, + 4.648090705579536, + 4.64360928575236, + 4.6345363415901355, + 4.620645739683731, + 4.601582895296278, + 4.576844613149877, + 4.545749081935775, + 4.507392668866529, + 4.460588269346438, + 4.403776951377749, + 4.334899616636187, + 4.251206678481653, + 4.148967839152864, + 4.023013269914552, + 3.8659736081447527, + 3.6669410211290234, + 3.4089018489492484, + 3.0631669173694633, + 2.5746631205888653, + 1.8062024190837231 + ], + "y": [ + 3.8999999999999995, + 3.7, + 3.5, + 3.3, + 3.1, + 2.9, + 2.6999999999999997, + 2.5, + 2.3, + 2.1, + 1.9, + 1.6999999999999997, + 1.4999999999999998, + 1.2999999999999998, + 1.0999999999999999, + 0.8999999999999999, + 0.6999999999999997, + 0.4999999999999998, + 0.2999999999999998, + 0.0999999999999999, + -0.10000000000000028, + -0.30000000000000077, + -0.5000000000000007, + -0.7000000000000006, + -0.9000000000000004, + -1.1000000000000003, + -1.3000000000000007, + -1.5000000000000007, + -1.7000000000000006, + -1.9000000000000004, + -2.1, + -2.3, + -2.5, + -2.700000000000001, + -2.9000000000000004, + -3.1, + -3.3, + -3.5, + -3.7, + -3.8999999999999995 + ] + }, + "VSM rib N=80": { + "CL": 0.4007293936922778, + "CDi_blade": 0.006483374761696468, + "CDi_trefftz": 0.006483379048739473, + "converged": true, + "gamma": [ + 1.3014040611245865, + 1.9027083267672518, + 2.318456061496536, + 2.637465936020142, + 2.894424647064766, + 3.107405206240039, + 3.287353289106202, + 3.441530350902, + 3.5750532251378884, + 3.6916843946532083, + 3.794279272090275, + 3.8850583329641406, + 3.965782097091302, + 4.037868636379483, + 4.102475290798576, + 4.160557144740437, + 4.212909883658103, + 4.260201843426762, + 4.3029983952023265, + 4.341780777761427, + 4.37696083214137, + 4.408892662563267, + 4.437881958162405, + 4.46419351132092, + 4.488057329303756, + 4.509673636848782, + 4.529216995718104, + 4.546839714657046, + 4.562674684142594, + 4.576837740917586, + 4.589429644952607, + 4.600537734289182, + 4.610237309864556, + 4.618592791937283, + 4.6256586814195035, + 4.631480352754136, + 4.636094699561358, + 4.639530649821111, + 4.641809563627118, + 4.6429455233624735, + 4.642945523362473, + 4.641809563627119, + 4.6395306498211095, + 4.636094699561356, + 4.631480352754132, + 4.6256586814195, + 4.618592791937279, + 4.610237309864553, + 4.600537734289178, + 4.589429644952603, + 4.576837740917582, + 4.562674684142589, + 4.546839714657044, + 4.5292169957181, + 4.509673636848778, + 4.488057329303751, + 4.464193511320917, + 4.437881958162404, + 4.408892662563265, + 4.3769608321413696, + 4.341780777761422, + 4.3029983952023265, + 4.260201843426756, + 4.2129098836581, + 4.160557144740433, + 4.102475290798571, + 4.037868636379481, + 3.965782097091297, + 3.885058332964134, + 3.7942792720902707, + 3.691684394653203, + 3.5750532251378853, + 3.4415303509019957, + 3.287353289106198, + 3.1074052062400366, + 2.8944246470647603, + 2.637465936020139, + 2.3184560614965344, + 1.9027083267672484, + 1.301404061124585 + ], + "y": [ + 3.95, + 3.8499999999999996, + 3.75, + 3.6500000000000004, + 3.55, + 3.45, + 3.3499999999999996, + 3.25, + 3.1500000000000004, + 3.05, + 2.95, + 2.8499999999999996, + 2.75, + 2.65, + 2.55, + 2.45, + 2.3499999999999996, + 2.25, + 2.15, + 2.05, + 1.95, + 1.8499999999999999, + 1.7499999999999998, + 1.6499999999999997, + 1.5499999999999998, + 1.45, + 1.3499999999999999, + 1.2499999999999998, + 1.1499999999999997, + 1.0499999999999998, + 0.95, + 0.8499999999999999, + 0.7499999999999998, + 0.6499999999999997, + 0.5499999999999998, + 0.44999999999999996, + 0.34999999999999987, + 0.24999999999999978, + 0.14999999999999966, + 0.04999999999999984, + -0.050000000000000246, + -0.1500000000000006, + -0.2500000000000006, + -0.3500000000000012, + -0.45000000000000173, + -0.550000000000002, + -0.6500000000000024, + -0.7500000000000029, + -0.8500000000000034, + -0.9500000000000035, + -1.0500000000000038, + -1.1500000000000041, + -1.2500000000000047, + -1.3500000000000052, + -1.4500000000000053, + -1.5500000000000056, + -1.650000000000006, + -1.7500000000000064, + -1.8500000000000072, + -1.950000000000007, + -2.0500000000000074, + -2.1500000000000075, + -2.2500000000000075, + -2.3500000000000076, + -2.4500000000000073, + -2.5500000000000074, + -2.6500000000000075, + -2.7500000000000075, + -2.8500000000000076, + -2.9500000000000073, + -3.050000000000008, + -3.1500000000000083, + -3.2500000000000084, + -3.3500000000000085, + -3.450000000000009, + -3.5500000000000096, + -3.65000000000001, + -3.75000000000001, + -3.8500000000000103, + -3.950000000000011 + ] + }, + "VSM streamwise N=40": { + "CL": 0.4037683141001729, + "CDi_blade": 0.006502921040432444, + "CDi_trefftz": 0.006502763924929589, + "converged": true, + "gamma": [ + 1.8062024190837223, + 2.5746631205888657, + 3.0631669173694633, + 3.408901848949248, + 3.6669410211290234, + 3.865973608144754, + 4.0230132699145535, + 4.148967839152865, + 4.251206678481653, + 4.334899616636186, + 4.403776951377747, + 4.460588269346439, + 4.507392668866529, + 4.545749081935775, + 4.576844613149877, + 4.601582895296278, + 4.620645739683731, + 4.634536341590136, + 4.643609285752358, + 4.648090705579536, + 4.648090705579536, + 4.643609285752359, + 4.634536341590135, + 4.620645739683731, + 4.601582895296278, + 4.576844613149877, + 4.545749081935775, + 4.507392668866529, + 4.460588269346437, + 4.40377695137775, + 4.334899616636187, + 4.251206678481653, + 4.148967839152864, + 4.023013269914553, + 3.865973608144753, + 3.666941021129024, + 3.4089018489492484, + 3.063166917369464, + 2.5746631205888657, + 1.8062024190837231 + ], + "y": [ + 3.8999999999999995, + 3.7, + 3.5, + 3.3, + 3.1, + 2.9, + 2.6999999999999997, + 2.5, + 2.3, + 2.1, + 1.9, + 1.6999999999999997, + 1.4999999999999998, + 1.2999999999999998, + 1.0999999999999999, + 0.8999999999999999, + 0.6999999999999997, + 0.4999999999999998, + 0.2999999999999998, + 0.0999999999999999, + -0.10000000000000028, + -0.30000000000000077, + -0.5000000000000007, + -0.7000000000000006, + -0.9000000000000004, + -1.1000000000000003, + -1.3000000000000007, + -1.5000000000000007, + -1.7000000000000006, + -1.9000000000000004, + -2.1, + -2.3, + -2.5, + -2.700000000000001, + -2.9000000000000004, + -3.1, + -3.3, + -3.5, + -3.7, + -3.8999999999999995 + ] + }, + "VSM streamwise N=80": { + "CL": 0.4007293936922779, + "CDi_blade": 0.006483374761696471, + "CDi_trefftz": 0.0064833790487394734, + "converged": true, + "gamma": [ + 1.3014040611245865, + 1.9027083267672518, + 2.3184560614965366, + 2.637465936020142, + 2.8944246470647665, + 3.1074052062400397, + 3.2873532891062025, + 3.4415303509020005, + 3.5750532251378897, + 3.691684394653209, + 3.7942792720902756, + 3.88505833296414, + 3.965782097091304, + 4.037868636379483, + 4.102475290798576, + 4.160557144740437, + 4.212909883658103, + 4.260201843426762, + 4.3029983952023265, + 4.341780777761427, + 4.376960832141371, + 4.408892662563267, + 4.437881958162405, + 4.46419351132092, + 4.488057329303757, + 4.509673636848782, + 4.529216995718104, + 4.546839714657047, + 4.562674684142594, + 4.576837740917586, + 4.589429644952607, + 4.600537734289182, + 4.610237309864556, + 4.618592791937283, + 4.625658681419504, + 4.631480352754136, + 4.636094699561358, + 4.639530649821111, + 4.641809563627118, + 4.642945523362475, + 4.642945523362473, + 4.641809563627119, + 4.63953064982111, + 4.636094699561357, + 4.631480352754133, + 4.625658681419501, + 4.61859279193728, + 4.610237309864554, + 4.600537734289178, + 4.589429644952604, + 4.576837740917583, + 4.562674684142588, + 4.546839714657045, + 4.529216995718101, + 4.509673636848779, + 4.488057329303751, + 4.464193511320918, + 4.437881958162404, + 4.408892662563265, + 4.3769608321413696, + 4.341780777761423, + 4.3029983952023265, + 4.260201843426757, + 4.2129098836581, + 4.160557144740433, + 4.102475290798571, + 4.037868636379481, + 3.9657820970912976, + 3.885058332964134, + 3.794279272090271, + 3.6916843946532034, + 3.5750532251378853, + 3.441530350901996, + 3.287353289106198, + 3.107405206240037, + 2.8944246470647608, + 2.637465936020139, + 2.3184560614965344, + 1.9027083267672489, + 1.301404061124585 + ], + "y": [ + 3.95, + 3.8499999999999996, + 3.75, + 3.6500000000000004, + 3.55, + 3.45, + 3.3499999999999996, + 3.25, + 3.1500000000000004, + 3.05, + 2.95, + 2.8499999999999996, + 2.75, + 2.65, + 2.55, + 2.45, + 2.3499999999999996, + 2.25, + 2.15, + 2.05, + 1.95, + 1.8499999999999999, + 1.7499999999999998, + 1.6499999999999997, + 1.5499999999999998, + 1.45, + 1.3499999999999999, + 1.2499999999999998, + 1.1499999999999997, + 1.0499999999999998, + 0.95, + 0.8499999999999999, + 0.7499999999999998, + 0.6499999999999997, + 0.5499999999999998, + 0.44999999999999996, + 0.34999999999999987, + 0.24999999999999978, + 0.14999999999999966, + 0.04999999999999984, + -0.050000000000000246, + -0.1500000000000006, + -0.2500000000000006, + -0.3500000000000012, + -0.45000000000000173, + -0.550000000000002, + -0.6500000000000024, + -0.7500000000000029, + -0.8500000000000034, + -0.9500000000000035, + -1.0500000000000038, + -1.1500000000000041, + -1.2500000000000047, + -1.3500000000000052, + -1.4500000000000053, + -1.5500000000000056, + -1.650000000000006, + -1.7500000000000064, + -1.8500000000000072, + -1.950000000000007, + -2.0500000000000074, + -2.1500000000000075, + -2.2500000000000075, + -2.3500000000000076, + -2.4500000000000073, + -2.5500000000000074, + -2.6500000000000075, + -2.7500000000000075, + -2.8500000000000076, + -2.9500000000000073, + -3.050000000000008, + -3.1500000000000083, + -3.2500000000000084, + -3.3500000000000085, + -3.450000000000009, + -3.5500000000000096, + -3.65000000000001, + -3.75000000000001, + -3.8500000000000103, + -3.950000000000011 + ] + }, + "VSM perpendicular N=40": { + "CL": 0.4037683141001729, + "CDi_blade": 0.006502921040432444, + "CDi_trefftz": 0.006502763924929589, + "converged": true, + "gamma": [ + 1.8062024190837223, + 2.5746631205888657, + 3.0631669173694633, + 3.408901848949248, + 3.6669410211290234, + 3.865973608144754, + 4.0230132699145535, + 4.148967839152865, + 4.251206678481653, + 4.334899616636186, + 4.403776951377747, + 4.460588269346439, + 4.507392668866529, + 4.545749081935775, + 4.576844613149877, + 4.601582895296278, + 4.620645739683731, + 4.634536341590136, + 4.643609285752358, + 4.648090705579536, + 4.648090705579536, + 4.643609285752359, + 4.634536341590135, + 4.620645739683731, + 4.601582895296278, + 4.576844613149877, + 4.545749081935775, + 4.507392668866529, + 4.460588269346437, + 4.40377695137775, + 4.334899616636187, + 4.251206678481653, + 4.148967839152864, + 4.023013269914553, + 3.865973608144753, + 3.666941021129024, + 3.4089018489492484, + 3.063166917369464, + 2.5746631205888657, + 1.8062024190837231 + ], + "y": [ + 3.8999999999999995, + 3.7, + 3.5, + 3.3, + 3.1, + 2.9, + 2.6999999999999997, + 2.5, + 2.3, + 2.1, + 1.9, + 1.6999999999999997, + 1.4999999999999998, + 1.2999999999999998, + 1.0999999999999999, + 0.8999999999999999, + 0.6999999999999997, + 0.4999999999999998, + 0.2999999999999998, + 0.0999999999999999, + -0.10000000000000028, + -0.30000000000000077, + -0.5000000000000007, + -0.7000000000000006, + -0.9000000000000004, + -1.1000000000000003, + -1.3000000000000007, + -1.5000000000000007, + -1.7000000000000006, + -1.9000000000000004, + -2.1, + -2.3, + -2.5, + -2.700000000000001, + -2.9000000000000004, + -3.1, + -3.3, + -3.5, + -3.7, + -3.8999999999999995 + ] + }, + "VSM perpendicular N=80": { + "CL": 0.4007293936922779, + "CDi_blade": 0.006483374761696471, + "CDi_trefftz": 0.0064833790487394734, + "converged": true, + "gamma": [ + 1.3014040611245865, + 1.9027083267672518, + 2.3184560614965366, + 2.637465936020142, + 2.8944246470647665, + 3.1074052062400397, + 3.2873532891062025, + 3.4415303509020005, + 3.5750532251378897, + 3.691684394653209, + 3.7942792720902756, + 3.88505833296414, + 3.965782097091304, + 4.037868636379483, + 4.102475290798576, + 4.160557144740437, + 4.212909883658103, + 4.260201843426762, + 4.3029983952023265, + 4.341780777761427, + 4.376960832141371, + 4.408892662563267, + 4.437881958162405, + 4.46419351132092, + 4.488057329303757, + 4.509673636848782, + 4.529216995718104, + 4.546839714657047, + 4.562674684142594, + 4.576837740917586, + 4.589429644952607, + 4.600537734289182, + 4.610237309864556, + 4.618592791937283, + 4.625658681419504, + 4.631480352754136, + 4.636094699561358, + 4.639530649821111, + 4.641809563627118, + 4.642945523362475, + 4.642945523362473, + 4.641809563627119, + 4.63953064982111, + 4.636094699561357, + 4.631480352754133, + 4.625658681419501, + 4.61859279193728, + 4.610237309864554, + 4.600537734289178, + 4.589429644952604, + 4.576837740917583, + 4.562674684142588, + 4.546839714657045, + 4.529216995718101, + 4.509673636848779, + 4.488057329303751, + 4.464193511320918, + 4.437881958162404, + 4.408892662563265, + 4.3769608321413696, + 4.341780777761423, + 4.3029983952023265, + 4.260201843426757, + 4.2129098836581, + 4.160557144740433, + 4.102475290798571, + 4.037868636379481, + 3.9657820970912976, + 3.885058332964134, + 3.794279272090271, + 3.6916843946532034, + 3.5750532251378853, + 3.441530350901996, + 3.287353289106198, + 3.107405206240037, + 2.8944246470647608, + 2.637465936020139, + 2.3184560614965344, + 1.9027083267672489, + 1.301404061124585 + ], + "y": [ + 3.95, + 3.8499999999999996, + 3.75, + 3.6500000000000004, + 3.55, + 3.45, + 3.3499999999999996, + 3.25, + 3.1500000000000004, + 3.05, + 2.95, + 2.8499999999999996, + 2.75, + 2.65, + 2.55, + 2.45, + 2.3499999999999996, + 2.25, + 2.15, + 2.05, + 1.95, + 1.8499999999999999, + 1.7499999999999998, + 1.6499999999999997, + 1.5499999999999998, + 1.45, + 1.3499999999999999, + 1.2499999999999998, + 1.1499999999999997, + 1.0499999999999998, + 0.95, + 0.8499999999999999, + 0.7499999999999998, + 0.6499999999999997, + 0.5499999999999998, + 0.44999999999999996, + 0.34999999999999987, + 0.24999999999999978, + 0.14999999999999966, + 0.04999999999999984, + -0.050000000000000246, + -0.1500000000000006, + -0.2500000000000006, + -0.3500000000000012, + -0.45000000000000173, + -0.550000000000002, + -0.6500000000000024, + -0.7500000000000029, + -0.8500000000000034, + -0.9500000000000035, + -1.0500000000000038, + -1.1500000000000041, + -1.2500000000000047, + -1.3500000000000052, + -1.4500000000000053, + -1.5500000000000056, + -1.650000000000006, + -1.7500000000000064, + -1.8500000000000072, + -1.950000000000007, + -2.0500000000000074, + -2.1500000000000075, + -2.2500000000000075, + -2.3500000000000076, + -2.4500000000000073, + -2.5500000000000074, + -2.6500000000000075, + -2.7500000000000075, + -2.8500000000000076, + -2.9500000000000073, + -3.050000000000008, + -3.1500000000000083, + -3.2500000000000084, + -3.3500000000000085, + -3.450000000000009, + -3.5500000000000096, + -3.65000000000001, + -3.75000000000001, + -3.8500000000000103, + -3.950000000000011 + ] + } + }, + "yawed 30 deg": { + "VLM 8x40": { + "CL": 0.36223331625097704, + "CDi_near": 0.005327308106480376, + "CDi_trefftz": 0.005367156019954948, + "gamma": [ + 0.5133648711402848, + 0.8818350972562369, + 1.2629914652967364, + 1.6267690491264424, + 1.965145057431875, + 2.274220407651291, + 2.5523871479113662, + 2.799680985893607, + 3.0173099225391837, + 3.2072473671124464, + 3.3718950737125, + 3.5138237525629368, + 3.6355875954227925, + 3.7396013986048047, + 3.828066376748031, + 3.902931174301444, + 3.965876378598508, + 4.018313025676682, + 4.061387612630295, + 4.095987704943944, + 4.122743225453317, + 4.142018877109449, + 4.153892847544506, + 4.158115928661671, + 4.154043432953722, + 4.140529892675936, + 4.115774021990362, + 4.0771006872834175, + 4.020673286096651, + 3.941158763460895, + 3.8314502418963694, + 3.6827283615792465, + 3.4853594056137975, + 3.2310129544658492, + 2.9154204467489673, + 2.5400932818142663, + 2.111999014688856, + 1.642240691334866, + 1.146497539329494, + 0.6671505139822329 + ], + "y": [ + -3.993834667466256, + -3.9692113486565317, + -3.9201165219856287, + -3.8468528733856604, + -3.7498720976128808, + -3.629772113399309, + -3.48729337708492, + -3.3233143174580793, + -3.138845919949957, + -2.935025493573157, + -2.713109659033462, + -2.4744666012453127, + -2.2205676340168434, + -1.952978128910991, + -1.6733478642092727, + -1.3834008534800741, + -1.0849247164617055, + -0.7797596577922722, + -0.4697871215361512, + -0.15691819145568964, + 0.15691819145569008, + 0.469787121536152, + 0.779759657792273, + 1.084924716461706, + 1.3834008534800746, + 1.6733478642092732, + 1.952978128910991, + 2.220567634016844, + 2.4744666012453136, + 2.7131096590334622, + 2.935025493573157, + 3.138845919949957, + 3.3233143174580793, + 3.48729337708492, + 3.629772113399309, + 3.7498720976128808, + 3.8468528733856604, + 3.9201165219856287, + 3.9692113486565317, + 3.993834667466256 + ] + }, + "VLM 16x80": { + "CL": 0.3601463557106662, + "CDi_near": 0.005339698718473664, + "CDi_trefftz": 0.005372802479771769, + "gamma": [ + 0.26561852224090754, + 0.4581600141682819, + 0.6628296038173253, + 0.8665183220108634, + 1.0666204912150037, + 1.2617839554738954, + 1.4510575044423186, + 1.6337120130792613, + 1.80918530531326, + 1.9770586929174565, + 2.137042630738594, + 2.288964599771845, + 2.432756999069587, + 2.5684445203261186, + 2.6961311511200243, + 2.81598719424798, + 2.9282367239430562, + 3.0331458334045496, + 3.131011923999245, + 3.2221541803290865, + 3.3069052848367937, + 3.385604356795967, + 3.458591052856708, + 3.526200736326407, + 3.5887606059064256, + 3.6465866679144474, + 3.699981436070831, + 3.749232247418044, + 3.794610090179143, + 3.8363688481057125, + 3.874744875220319, + 3.909956824179453, + 3.9422056603156928, + 3.971674801449843, + 3.99853033058599, + 4.022921234479908, + 4.044979625718479, + 4.064820909309153, + 4.082543856812554, + 4.098230551719346, + 4.111946169028952, + 4.12373854977603, + 4.133637527501487, + 4.141653958300273, + 4.147778399023999, + 4.1519793694255505, + 4.154201123530218, + 4.154360843476719, + 4.1523451559466285, + 4.148005858054983, + 4.141154728062214, + 4.131557289878073, + 4.118925405049449, + 4.102908592046411, + 4.083084037384425, + 4.058945394222103, + 4.029890704259232, + 3.9952101887765084, + 3.9540753096020613, + 3.9055314672257877, + 3.8484979756865867, + 3.7817803266447925, + 3.704100638912167, + 3.614151493329282, + 3.5106747416933732, + 3.3925597725347956, + 3.2589468069096963, + 3.1093145684723695, + 2.9435330379415636, + 2.761871899609467, + 2.564968808379258, + 2.3537716632552255, + 2.129472001600864, + 1.893443986257997, + 1.6471997129249722, + 1.3923722625949813, + 1.130754729885475, + 0.8645114345392227, + 0.5972742723346781, + 0.34618369146966216 + ], + "y": [ + -3.998458072481446, + -3.992292739947702, + -3.979971581376109, + -3.9615135951001283, + -3.9369472419967364, + -3.906310401601814, + -3.8696503137026474, + -3.8270235054976016, + -3.7784957044352754, + -3.7241417368675416, + -3.664045412672736, + -3.5982993960268983, + -3.5270050625223295, + -3.4502723428537783, + -3.368219553313275, + -3.2809732133549856, + -3.1886678505113846, + -3.0914457929615518, + -2.989456950071433, + -2.882858581244466, + -2.771815053438978, + -2.6564975877262498, + -2.5370839952800353, + -2.413758403204614, + -2.2867109706241497, + -2.1561375954711015, + -2.0222396124258077, + -1.8852234824730036, + -1.7453004745539493, + -1.602686339805035, + -1.4576009788851652, + -1.3102681029048808, + -1.1609148884800433, + -1.009771627441959, + -0.857071371744067, + -0.703049574112718, + -0.5479437249961366, + -0.3919929863713647, + -0.23543782297382687, + -0.07851963151813723, + 0.07851963151813734, + 0.23543782297382743, + 0.3919929863713656, + 0.5479437249961374, + 0.7030495741127187, + 0.8570713717440679, + 1.0097716274419595, + 1.1609148884800433, + 1.3102681029048808, + 1.4576009788851656, + 1.6026863398050355, + 1.7453004745539493, + 1.8852234824730036, + 2.0222396124258077, + 2.156137595471102, + 2.2867109706241506, + 2.4137584032046147, + 2.5370839952800357, + 2.65649758772625, + 2.771815053438978, + 2.8828585812444665, + 2.9894569500714336, + 3.0914457929615518, + 3.1886678505113846, + 3.2809732133549856, + 3.368219553313275, + 3.4502723428537783, + 3.5270050625223295, + 3.5982993960268983, + 3.664045412672736, + 3.7241417368675416, + 3.7784957044352754, + 3.827023505497602, + 3.869650313702648, + 3.906310401601814, + 3.9369472419967364, + 3.9615135951001283, + 3.979971581376109, + 3.992292739947702, + 3.998458072481446 + ] + }, + "VSM rib N=40": { + "CL": 0.36276349332168345, + "CDi_blade": 0.005353359572097573, + "CDi_trefftz": 0.005347439770242871, + "converged": true, + "gamma": [ + 1.9086962291302907, + 2.6899637465370336, + 3.1535055976732846, + 3.454815081086041, + 3.6601226711661083, + 3.8043985208985136, + 3.9079841196538885, + 3.983429215621027, + 4.038801736284097, + 4.079451232904739, + 4.109011041069328, + 4.129995264754258, + 4.14416748268547, + 4.15277506353798, + 4.156701492399109, + 4.156567136750825, + 4.152796687148989, + 4.145664476279389, + 4.135324686438864, + 4.121830873600278, + 4.105147591566422, + 4.0851558061007855, + 4.061653014829815, + 4.034348385797688, + 4.002852689413254, + 3.966662233442773, + 3.925135318535906, + 3.8774587766457227, + 3.822600722321718, + 3.7592433660417854, + 3.685685923011297, + 3.599700955674948, + 3.4983151184394545, + 3.3774610076210365, + 3.231395628614171, + 3.0516626676125247, + 2.82506762584674, + 2.529189970442679, + 2.1202671675440468, + 1.4866225607240262 + ], + "y": [ + 3.9, + 3.7, + 3.5, + 3.3, + 3.1, + 2.8999999999999995, + 2.7, + 2.5, + 2.3, + 2.0999999999999996, + 1.8999999999999995, + 1.6999999999999995, + 1.4999999999999991, + 1.2999999999999994, + 1.0999999999999994, + 0.8999999999999992, + 0.6999999999999991, + 0.4999999999999993, + 0.2999999999999995, + 0.09999999999999973, + -0.10000000000000026, + -0.30000000000000027, + -0.4999999999999998, + -0.6999999999999997, + -0.8999999999999995, + -1.0999999999999994, + -1.2999999999999994, + -1.499999999999999, + -1.6999999999999988, + -1.8999999999999986, + -2.0999999999999988, + -2.299999999999999, + -2.4999999999999982, + -2.6999999999999984, + -2.8999999999999986, + -3.099999999999998, + -3.299999999999997, + -3.499999999999997, + -3.699999999999996, + -3.899999999999995 + ] + }, + "VSM rib N=80": { + "CL": 0.36010909558679327, + "CDi_blade": 0.005342040067522525, + "CDi_trefftz": 0.005333518999771314, + "converged": true, + "gamma": [ + 1.37843822856084, + 2.0071087802420724, + 2.432565036480029, + 2.749507588467586, + 2.995757344334907, + 3.1917625938814367, + 3.3503592934584323, + 3.4802790196454314, + 3.5877536169214603, + 3.6773759782404656, + 3.7526112874193025, + 3.8161217309472306, + 3.8699826957407897, + 3.915831874753197, + 3.954974965951909, + 3.9884622789607502, + 4.017145281506544, + 4.041718986343983, + 4.06275414560289, + 4.080721984375851, + 4.096013393676625, + 4.108953956358801, + 4.1198158037709485, + 4.128827037737415, + 4.136179265151596, + 4.142033657257627, + 4.14652584683788, + 4.149769903410609, + 4.151861571902955, + 4.1528809190401095, + 4.152894500312004, + 4.1519571362940875, + 4.15011336846631, + 4.147398650148095, + 4.143840316754183, + 4.139458370537425, + 4.1342661077637315, + 4.12827061043952, + 4.121473119959477, + 4.113869306111015, + 4.105449441563423, + 4.0961984891262935, + 4.086096106552653, + 4.07511657137744, + 4.063228626123885, + 4.050395242089026, + 4.036573297745819, + 4.021713165481002, + 4.00575819782221, + 3.9886441013759817, + 3.970298183255964, + 3.9506384506470464, + 3.9295725390943477, + 3.9069964388217446, + 3.8827929804676375, + 3.856830031527258, + 3.828958341759331, + 3.7990089588045524, + 3.7667901128019103, + 3.732083438771789, + 3.694639364909943, + 3.6541714392046805, + 3.6103492892020355, + 3.562789799981996, + 3.511045937362873, + 3.4545924112782207, + 3.392807025963799, + 3.3249460278231173, + 3.250110914435865, + 3.167202784520684, + 3.0748579649002052, + 2.971354506587367, + 2.8544714345366264, + 2.7212673999099555, + 2.5677129348670626, + 2.388034612738452, + 2.1734289298110125, + 1.9091787878472095, + 1.566723472971773, + 1.0724438632897804 + ], + "y": [ + 3.95, + 3.8499999999999996, + 3.75, + 3.6500000000000004, + 3.55, + 3.45, + 3.3499999999999996, + 3.25, + 3.1500000000000004, + 3.05, + 2.95, + 2.8499999999999996, + 2.75, + 2.6500000000000004, + 2.55, + 2.45, + 2.3500000000000005, + 2.25, + 2.15, + 2.05, + 1.9499999999999997, + 1.8499999999999999, + 1.75, + 1.65, + 1.5499999999999998, + 1.45, + 1.35, + 1.2500000000000002, + 1.1500000000000001, + 1.0500000000000003, + 0.9500000000000004, + 0.8500000000000003, + 0.7500000000000004, + 0.6500000000000005, + 0.55, + 0.4500000000000002, + 0.3500000000000001, + 0.24999999999999956, + 0.14999999999999958, + 0.049999999999999545, + -0.050000000000000995, + -0.1500000000000013, + -0.25000000000000094, + -0.35000000000000064, + -0.45000000000000073, + -0.5500000000000006, + -0.6500000000000005, + -0.7500000000000006, + -0.8500000000000003, + -0.95, + -1.0500000000000003, + -1.1500000000000004, + -1.25, + -1.3499999999999996, + -1.45, + -1.5499999999999998, + -1.6499999999999997, + -1.7499999999999998, + -1.8499999999999996, + -1.9499999999999993, + -2.0499999999999994, + -2.15, + -2.25, + -2.3499999999999996, + -2.45, + -2.55, + -2.65, + -2.7500000000000004, + -2.8500000000000005, + -2.950000000000001, + -3.0500000000000016, + -3.1500000000000012, + -3.250000000000002, + -3.350000000000003, + -3.450000000000003, + -3.550000000000004, + -3.650000000000005, + -3.7500000000000044, + -3.850000000000005, + -3.9500000000000064 + ] + }, + "VSM streamwise N=40": { + "CL": 0.3627634933216834, + "CDi_blade": 0.005353359572097575, + "CDi_trefftz": 0.005347439770242875, + "converged": true, + "gamma": [ + 1.9086962291302905, + 2.689963746537034, + 3.1535055976732846, + 3.4548150810860414, + 3.6601226711661083, + 3.8043985208985136, + 3.9079841196538885, + 3.9834292156210265, + 4.038801736284097, + 4.079451232904739, + 4.109011041069329, + 4.129995264754259, + 4.144167482685472, + 4.15277506353798, + 4.156701492399111, + 4.156567136750824, + 4.15279668714899, + 4.145664476279389, + 4.135324686438863, + 4.121830873600278, + 4.105147591566422, + 4.085155806100786, + 4.061653014829815, + 4.0343483857976885, + 4.002852689413254, + 3.966662233442774, + 3.925135318535907, + 3.8774587766457227, + 3.8226007223217193, + 3.7592433660417868, + 3.6856859230112975, + 3.5997009556749497, + 3.4983151184394563, + 3.377461007621039, + 3.231395628614174, + 3.051662667612527, + 2.825067625846743, + 2.5291899704426837, + 2.1202671675440516, + 1.4866225607240282 + ], + "y": [ + 3.9, + 3.7, + 3.5, + 3.3, + 3.1, + 2.8999999999999995, + 2.7, + 2.5, + 2.3, + 2.0999999999999996, + 1.8999999999999995, + 1.6999999999999995, + 1.4999999999999991, + 1.2999999999999994, + 1.0999999999999994, + 0.8999999999999992, + 0.6999999999999991, + 0.4999999999999993, + 0.2999999999999995, + 0.09999999999999973, + -0.10000000000000026, + -0.30000000000000027, + -0.4999999999999998, + -0.6999999999999997, + -0.8999999999999995, + -1.0999999999999994, + -1.2999999999999994, + -1.499999999999999, + -1.6999999999999988, + -1.8999999999999986, + -2.0999999999999988, + -2.299999999999999, + -2.4999999999999982, + -2.6999999999999984, + -2.8999999999999986, + -3.099999999999998, + -3.299999999999997, + -3.499999999999997, + -3.699999999999996, + -3.899999999999995 + ] + }, + "VSM streamwise N=80": { + "CL": 0.3601090955867935, + "CDi_blade": 0.005342040067522525, + "CDi_trefftz": 0.005333518999771314, + "converged": true, + "gamma": [ + 1.3784382285608399, + 2.007108780242072, + 2.4325650364800286, + 2.7495075884675853, + 2.9957573443349066, + 3.1917625938814354, + 3.350359293458431, + 3.480279019645431, + 3.587753616921459, + 3.677375978240465, + 3.7526112874193, + 3.81612173094723, + 3.8699826957407883, + 3.915831874753196, + 3.9549749659519073, + 3.9884622789607493, + 4.017145281506542, + 4.041718986343982, + 4.062754145602891, + 4.080721984375851, + 4.096013393676624, + 4.1089539563588, + 4.1198158037709485, + 4.128827037737413, + 4.136179265151593, + 4.142033657257626, + 4.1465258468378785, + 4.149769903410608, + 4.151861571902954, + 4.1528809190401095, + 4.152894500312005, + 4.151957136294086, + 4.15011336846631, + 4.147398650148094, + 4.14384031675418, + 4.139458370537423, + 4.1342661077637315, + 4.128270610439521, + 4.121473119959478, + 4.113869306111015, + 4.105449441563423, + 4.096198489126293, + 4.086096106552654, + 4.07511657137744, + 4.063228626123886, + 4.050395242089028, + 4.03657329774582, + 4.021713165481003, + 4.00575819782221, + 3.9886441013759835, + 3.970298183255964, + 3.9506384506470473, + 3.9295725390943477, + 3.9069964388217446, + 3.882792980467638, + 3.8568300315272595, + 3.8289583417593316, + 3.7990089588045524, + 3.766790112801911, + 3.732083438771789, + 3.6946393649099436, + 3.6541714392046805, + 3.610349289202037, + 3.5627897999819966, + 3.5110459373628733, + 3.4545924112782207, + 3.392807025963799, + 3.324946027823117, + 3.250110914435865, + 3.167202784520684, + 3.0748579649002057, + 2.9713545065873674, + 2.854471434536627, + 2.721267399909957, + 2.567712934867065, + 2.388034612738456, + 2.1734289298110143, + 1.9091787878472106, + 1.5667234729717743, + 1.0724438632897808 + ], + "y": [ + 3.95, + 3.8499999999999996, + 3.75, + 3.6500000000000004, + 3.55, + 3.45, + 3.3499999999999996, + 3.25, + 3.1500000000000004, + 3.05, + 2.95, + 2.8499999999999996, + 2.75, + 2.6500000000000004, + 2.55, + 2.45, + 2.3500000000000005, + 2.25, + 2.15, + 2.05, + 1.9499999999999997, + 1.8499999999999999, + 1.75, + 1.65, + 1.5499999999999998, + 1.45, + 1.35, + 1.2500000000000002, + 1.1500000000000001, + 1.0500000000000003, + 0.9500000000000004, + 0.8500000000000003, + 0.7500000000000004, + 0.6500000000000005, + 0.55, + 0.4500000000000002, + 0.3500000000000001, + 0.24999999999999956, + 0.14999999999999958, + 0.049999999999999545, + -0.050000000000000995, + -0.1500000000000013, + -0.25000000000000094, + -0.35000000000000064, + -0.45000000000000073, + -0.5500000000000006, + -0.6500000000000005, + -0.7500000000000006, + -0.8500000000000003, + -0.95, + -1.0500000000000003, + -1.1500000000000004, + -1.25, + -1.3499999999999996, + -1.45, + -1.5499999999999998, + -1.6499999999999997, + -1.7499999999999998, + -1.8499999999999996, + -1.9499999999999993, + -2.0499999999999994, + -2.15, + -2.25, + -2.3499999999999996, + -2.45, + -2.55, + -2.65, + -2.7500000000000004, + -2.8500000000000005, + -2.950000000000001, + -3.0500000000000016, + -3.1500000000000012, + -3.250000000000002, + -3.350000000000003, + -3.450000000000003, + -3.550000000000004, + -3.650000000000005, + -3.7500000000000044, + -3.850000000000005, + -3.9500000000000064 + ] + }, + "VSM perpendicular N=40": { + "CL": 0.3679187785935869, + "CDi_blade": 0.005496671773908391, + "CDi_trefftz": 0.0055413646014928234, + "converged": true, + "gamma": [ + 2.0206458364125184, + 2.797553261580665, + 3.239562061133799, + 3.5206590883442317, + 3.71026743853675, + 3.8431030476898806, + 3.9386175086608928, + 4.00851018225088, + 4.060175252958506, + 4.098462143550823, + 4.126643425950266, + 4.146976781755054, + 4.161045276918419, + 4.16997037911227, + 4.174548889270595, + 4.1753427753991135, + 4.172738926675378, + 4.166989105572138, + 4.158236435243492, + 4.146532372014757, + 4.13184660953956, + 4.114071367043815, + 4.093020807858796, + 4.068425775418853, + 4.039923520240493, + 4.007041532796231, + 3.9691738911137264, + 3.925547534590462, + 3.87517435542739, + 3.8167825504799566, + 3.7487165432885186, + 3.668787481071237, + 3.574042732321497, + 3.46039603978418, + 3.3220032999878883, + 3.1501376513755486, + 2.9309756034353454, + 2.6406589746382165, + 2.231917675526434, + 1.5825652684161544 + ], + "y": [ + 3.9, + 3.7, + 3.5, + 3.3, + 3.1, + 2.8999999999999995, + 2.7, + 2.5, + 2.3, + 2.0999999999999996, + 1.8999999999999995, + 1.6999999999999995, + 1.4999999999999991, + 1.2999999999999994, + 1.0999999999999994, + 0.8999999999999992, + 0.6999999999999991, + 0.4999999999999993, + 0.2999999999999995, + 0.09999999999999973, + -0.10000000000000026, + -0.30000000000000027, + -0.4999999999999998, + -0.6999999999999997, + -0.8999999999999995, + -1.0999999999999994, + -1.2999999999999994, + -1.499999999999999, + -1.6999999999999988, + -1.8999999999999986, + -2.0999999999999988, + -2.299999999999999, + -2.4999999999999982, + -2.6999999999999984, + -2.8999999999999986, + -3.099999999999998, + -3.299999999999997, + -3.499999999999997, + -3.699999999999996, + -3.899999999999995 + ] + }, + "VSM perpendicular N=80": { + "CL": 0.3652960399895595, + "CDi_blade": 0.005481911276999518, + "CDi_trefftz": 0.005529880565849014, + "converged": true, + "gamma": [ + 1.469987078604212, + 2.1196665585396954, + 2.5458052800952182, + 2.8551450093005464, + 3.0908420429779193, + 3.2758184798788292, + 3.424005762070587, + 3.5445682605515074, + 3.643855689399488, + 3.726432617343733, + 3.795672388087491, + 3.8541226769139567, + 3.9037417158669023, + 3.94605683602117, + 3.982274145824656, + 4.013356311371456, + 4.040078869946951, + 4.063071724635449, + 4.082850185852586, + 4.099838501218358, + 4.114387899500954, + 4.126790570614452, + 4.137290596688874, + 4.146092569533253, + 4.153368434245825, + 4.159262959867808, + 4.163898138012269, + 4.167376737527818, + 4.169785189524087, + 4.171195937046674, + 4.171669353560065, + 4.171255311514806, + 4.1699944647430955, + 4.167919294877054, + 4.165054961420676, + 4.161419986796508, + 4.157026801086619, + 4.1518821658845955, + 4.145987492359931, + 4.139339065068398, + 4.131928180034117, + 4.123741203031054, + 4.11475955168052, + 4.1049596028518485, + 4.094312524812833, + 4.082784031536085, + 4.070334054440145, + 4.056916324535189, + 4.0424778553480065, + 4.026958313993791, + 4.010289264190739, + 3.9923932606828947, + 3.973182769196492, + 3.9525588793739357, + 3.9304097696644473, + 3.90660887229936, + 3.881012672415579, + 3.8534580569595844, + 3.8237591045789805, + 3.791703174959133, + 3.7570461116052223, + 3.7195063109122506, + 3.678757325012119, + 3.6344185448741584, + 3.586043335563502, + 3.53310373883521, + 3.4749704725041672, + 3.410886362280705, + 3.339930402445708, + 3.2609681084082256, + 3.172581228762971, + 3.072965300736742, + 2.9597750191271106, + 2.8298805829703837, + 2.6789624278952893, + 2.500788135266761, + 2.285794228099807, + 2.017905436378893, + 1.6657844900426468, + 1.1484800236497599 + ], + "y": [ + 3.95, + 3.8499999999999996, + 3.75, + 3.6500000000000004, + 3.55, + 3.45, + 3.3499999999999996, + 3.25, + 3.1500000000000004, + 3.05, + 2.95, + 2.8499999999999996, + 2.75, + 2.6500000000000004, + 2.55, + 2.45, + 2.3500000000000005, + 2.25, + 2.15, + 2.05, + 1.9499999999999997, + 1.8499999999999999, + 1.75, + 1.65, + 1.5499999999999998, + 1.45, + 1.35, + 1.2500000000000002, + 1.1500000000000001, + 1.0500000000000003, + 0.9500000000000004, + 0.8500000000000003, + 0.7500000000000004, + 0.6500000000000005, + 0.55, + 0.4500000000000002, + 0.3500000000000001, + 0.24999999999999956, + 0.14999999999999958, + 0.049999999999999545, + -0.050000000000000995, + -0.1500000000000013, + -0.25000000000000094, + -0.35000000000000064, + -0.45000000000000073, + -0.5500000000000006, + -0.6500000000000005, + -0.7500000000000006, + -0.8500000000000003, + -0.95, + -1.0500000000000003, + -1.1500000000000004, + -1.25, + -1.3499999999999996, + -1.45, + -1.5499999999999998, + -1.6499999999999997, + -1.7499999999999998, + -1.8499999999999996, + -1.9499999999999993, + -2.0499999999999994, + -2.15, + -2.25, + -2.3499999999999996, + -2.45, + -2.55, + -2.65, + -2.7500000000000004, + -2.8500000000000005, + -2.950000000000001, + -3.0500000000000016, + -3.1500000000000012, + -3.250000000000002, + -3.350000000000003, + -3.450000000000003, + -3.550000000000004, + -3.650000000000005, + -3.7500000000000044, + -3.850000000000005, + -3.9500000000000064 + ] + } + }, + "kinked swept 30 deg": { + "VLM 8x40": { + "CL": 0.3658681003196086, + "CDi_near": 0.00568734126058481, + "CDi_trefftz": 0.005656774465273685, + "gamma": [ + 0.9016291437476313, + 1.5398886305035515, + 2.1704882784137354, + 2.720390257573458, + 3.161352558410643, + 3.4864871346054205, + 3.70912745359103, + 3.8527453649583845, + 3.9395589011364596, + 3.9858406145205687, + 4.002149078261223, + 3.9949510034295, + 3.9681910786939225, + 3.924632649447235, + 3.867168867198516, + 3.8002898132557514, + 3.731517159745171, + 3.671744644887962, + 3.632549748335245, + 3.619351859861134, + 3.619351859861127, + 3.632549748335253, + 3.6717446448879625, + 3.731517159745171, + 3.800289813255752, + 3.8671688671985174, + 3.9246326494472363, + 3.968191078693922, + 3.9949510034295, + 4.002149078261221, + 3.9858406145205674, + 3.9395589011364573, + 3.852745364958383, + 3.7091274535910284, + 3.4864871346054205, + 3.161352558410643, + 2.7203902575734573, + 2.1704882784137354, + 1.539888630503551, + 0.9016291437476311 + ], + "y": [ + -3.9876883405951378, + -3.9387448568902914, + -3.8420630404835214, + -3.7000235185633152, + -3.516123775561495, + -3.2948920334790204, + -3.0417757520320197, + -2.763007494114494, + -2.465451459415178, + -2.1564344650402307, + -1.843565534959769, + -1.5345485405848216, + -1.2369925058855058, + -0.9582242479679801, + -0.7051079665209794, + -0.4838762244385051, + -0.29997648143668476, + -0.15793695951647857, + -0.06125514310970859, + -0.01231165940486223, + 0.01231165940486234, + 0.06125514310970881, + 0.15793695951647868, + 0.29997648143668487, + 0.4838762244385052, + 0.7051079665209795, + 0.9582242479679802, + 1.2369925058855058, + 1.5345485405848218, + 1.8435655349597693, + 2.156434465040231, + 2.4654514594151786, + 2.763007494114494, + 3.04177575203202, + 3.2948920334790204, + 3.516123775561495, + 3.7000235185633152, + 3.8420630404835214, + 3.9387448568902914, + 3.9876883405951378 + ] + }, + "VLM 16x80": { + "CL": 0.36287481460389154, + "CDi_near": 0.005708111771498566, + "CDi_trefftz": 0.0056750283991185155, + "gamma": [ + 0.4757052837394662, + 0.8195246525508939, + 1.1816149373900338, + 1.5359283744832468, + 1.8751049206591814, + 2.194028928445583, + 2.488479896994604, + 2.755062174054035, + 2.991415554931128, + 3.1964771770326137, + 3.37063467604959, + 3.5156408934241576, + 3.634268119860568, + 3.729818508305846, + 3.8056620370092222, + 3.8649147837834943, + 3.9102758231285546, + 3.943983449650708, + 3.967840306024871, + 3.9832692021016314, + 3.991377242873436, + 3.9930176081436213, + 3.988845270847559, + 3.9793665517348504, + 3.964984112708117, + 3.946039701080201, + 3.9228571193576327, + 3.895787619125059, + 3.865259061900371, + 3.831828431396973, + 3.796234238385705, + 3.7594408530968306, + 3.7226612935118215, + 3.6873400581019067, + 3.655075939885591, + 3.6274693347409857, + 3.6058905084876844, + 3.591181835133966, + 3.58331811648817, + 3.5810514831925335, + 3.5810514831925717, + 3.583318116488173, + 3.591181835133968, + 3.6058905084876844, + 3.627469334740985, + 3.6550759398855917, + 3.6873400581019067, + 3.722661293511821, + 3.7594408530968293, + 3.796234238385705, + 3.8318284313969735, + 3.8652590619003724, + 3.895787619125061, + 3.9228571193576336, + 3.9460397010802017, + 3.964984112708117, + 3.9793665517348495, + 3.98884527084756, + 3.9930176081436226, + 3.9913772428734378, + 3.983269202101635, + 3.9678403060248764, + 3.943983449650716, + 3.910275823128561, + 3.864914783783501, + 3.805662037009228, + 3.729818508305851, + 3.6342681198605726, + 3.515640893424161, + 3.370634676049595, + 3.1964771770326177, + 2.991415554931131, + 2.755062174054037, + 2.4884798969946043, + 2.1940289284455843, + 1.8751049206591826, + 1.5359283744832477, + 1.181614937390034, + 0.8195246525508939, + 0.4757052837394662 + ], + "y": [ + -3.996917333733128, + -3.984605674328266, + -3.960058260992814, + -3.9234264366928304, + -3.8749360488064406, + -3.8148860566996543, + -3.74364668854246, + -3.6616571587290396, + -3.5694229599749785, + -3.4675127467865785, + -3.356554829516731, + -3.2372333006226564, + -3.1102838170084217, + -2.9764890644554955, + -2.8366739321046364, + -2.691700426740037, + -2.5424623582308525, + -2.3898798288961363, + -2.2348935607680755, + -2.078459095727845, + -1.921540904272155, + -1.765106439231924, + -1.6101201711038635, + -1.457537641769147, + -1.3082995732599627, + -1.1633260678953634, + -1.0235109355445045, + -0.8897161829915781, + -0.7627666993773432, + -0.6434451704832689, + -0.5324872532134215, + -0.4305770400250215, + -0.33834284127096037, + -0.25635331145754003, + -0.18511394330034547, + -0.12506395119355962, + -0.0765735633071698, + -0.03994173900718567, + -0.015394325671734155, + -0.003082666266871925, + 0.003082666266872036, + 0.015394325671734377, + 0.039941739007185784, + 0.07657356330716991, + 0.12506395119355973, + 0.18511394330034547, + 0.25635331145754003, + 0.3383428412709605, + 0.43057704002502173, + 0.5324872532134216, + 0.643445170483269, + 0.7627666993773434, + 0.8897161829915782, + 1.0235109355445045, + 1.1633260678953636, + 1.308299573259963, + 1.4575376417691475, + 1.6101201711038637, + 1.7651064392319245, + 1.9215409042721552, + 2.078459095727845, + 2.234893560768076, + 2.3898798288961363, + 2.542462358230853, + 2.691700426740037, + 2.8366739321046364, + 2.9764890644554955, + 3.1102838170084217, + 3.237233300622657, + 3.356554829516731, + 3.4675127467865785, + 3.5694229599749785, + 3.6616571587290396, + 3.74364668854246, + 3.8148860566996543, + 3.8749360488064406, + 3.9234264366928304, + 3.960058260992814, + 3.984605674328266, + 3.996917333733128 + ] + }, + "VSM rib N=40": { + "CL": 0.3644656343303096, + "CDi_blade": 0.005462440508289193, + "CDi_trefftz": 0.00562552069886205, + "converged": true, + "gamma": [ + 1.8869090644374205, + 2.6570527024002555, + 3.1119023652127384, + 3.405375621148294, + 3.6029848421367743, + 3.7392585429256195, + 3.83420669184189, + 3.9000858809719348, + 3.944669335329199, + 3.972982195673977, + 3.988278635343285, + 3.9926120357384978, + 3.987171451185715, + 3.972474979571984, + 3.948470174724269, + 3.9145750317098513, + 3.869705286423259, + 3.81243681061667, + 3.742010144594441, + 3.664688203172171, + 3.6646882031721706, + 3.742010144594442, + 3.812436810616671, + 3.8697052864232604, + 3.9145750317098544, + 3.9484701747242714, + 3.9724749795719863, + 3.987171451185715, + 3.9926120357384995, + 3.9882786353432826, + 3.972982195673975, + 3.9446693353291957, + 3.9000858809719308, + 3.834206691841887, + 3.739258542925614, + 3.6029848421367694, + 3.4053756211482913, + 3.1119023652127376, + 2.6570527024002564, + 1.8869090644374271 + ], + "y": [ + 3.9, + 3.7, + 3.5, + 3.3, + 3.1, + 2.8999999999999995, + 2.7, + 2.5, + 2.3, + 2.0999999999999996, + 1.8999999999999995, + 1.6999999999999995, + 1.4999999999999991, + 1.2999999999999994, + 1.0999999999999994, + 0.8999999999999992, + 0.6999999999999991, + 0.4999999999999993, + 0.2999999999999995, + 0.09999999999999973, + -0.10000000000000026, + -0.30000000000000027, + -0.4999999999999998, + -0.6999999999999997, + -0.8999999999999995, + -1.0999999999999994, + -1.2999999999999994, + -1.499999999999999, + -1.6999999999999988, + -1.8999999999999986, + -2.0999999999999988, + -2.299999999999999, + -2.4999999999999982, + -2.6999999999999984, + -2.8999999999999986, + -3.099999999999998, + -3.299999999999997, + -3.499999999999997, + -3.699999999999996, + -3.899999999999995 + ] + }, + "VSM rib N=80": { + "CL": 0.36161543443795746, + "CDi_blade": 0.00550513084278147, + "CDi_trefftz": 0.005606779780364248, + "converged": true, + "gamma": [ + 1.3628697295120125, + 1.983677546226311, + 2.4031702969187134, + 2.7150657171479784, + 2.956796935484429, + 3.1486018131375872, + 3.3031813984703895, + 3.4291717841679787, + 3.532732424179296, + 3.6183981975774056, + 3.6895854555704495, + 3.7489133834157156, + 3.798417897581681, + 3.8396990788537635, + 3.8740255854122587, + 3.9024102123662887, + 3.925665533865846, + 3.9444454622436655, + 3.9592766424033137, + 3.9705823747750446, + 3.9787009542114755, + 3.983899768518267, + 3.986386125554743, + 3.986315514762072, + 3.9837978218376264, + 3.978901881813517, + 3.9716586630415827, + 3.9620633172491995, + 3.9500763122031612, + 3.935623900024563, + 3.9185983070973336, + 3.8988583548136493, + 3.8762319484288934, + 3.8505235102077244, + 3.8215332449130046, + 3.7891044911286005, + 3.7532402737154347, + 3.714403762108531, + 3.67437426939289, + 3.6392396569376246, + 3.639239656937624, + 3.674374269392894, + 3.714403762108532, + 3.753240273715439, + 3.7891044911285996, + 3.821533244913006, + 3.850523510207725, + 3.876231948428895, + 3.8988583548136506, + 3.9185983070973345, + 3.935623900024564, + 3.950076312203164, + 3.9620633172492, + 3.971658663041583, + 3.9789018818135173, + 3.983797821837627, + 3.9863155147620706, + 3.986386125554742, + 3.983899768518268, + 3.978700954211479, + 3.970582374775047, + 3.9592766424033194, + 3.94444546224367, + 3.925665533865847, + 3.902410212366292, + 3.8740255854122623, + 3.839699078853766, + 3.7984178975816865, + 3.748913383415722, + 3.6895854555704526, + 3.618398197577411, + 3.5327324241793, + 3.429171784167985, + 3.3031813984703957, + 3.1486018131375952, + 2.9567969354844337, + 2.715065717147984, + 2.40317029691872, + 1.9836775462263183, + 1.3628697295120145 + ], + "y": [ + 3.95, + 3.8499999999999996, + 3.75, + 3.6500000000000004, + 3.55, + 3.45, + 3.3499999999999996, + 3.25, + 3.1500000000000004, + 3.05, + 2.95, + 2.8499999999999996, + 2.75, + 2.6500000000000004, + 2.55, + 2.45, + 2.3500000000000005, + 2.25, + 2.15, + 2.05, + 1.9499999999999997, + 1.8499999999999999, + 1.75, + 1.65, + 1.5499999999999998, + 1.45, + 1.35, + 1.2500000000000002, + 1.1500000000000001, + 1.0500000000000003, + 0.9500000000000004, + 0.8500000000000003, + 0.7500000000000004, + 0.6500000000000005, + 0.55, + 0.4500000000000002, + 0.3500000000000001, + 0.24999999999999956, + 0.14999999999999958, + 0.049999999999999545, + -0.050000000000000974, + -0.1500000000000013, + -0.25000000000000094, + -0.35000000000000064, + -0.45000000000000073, + -0.5500000000000006, + -0.6500000000000004, + -0.7500000000000006, + -0.8500000000000003, + -0.95, + -1.0500000000000003, + -1.1500000000000004, + -1.25, + -1.3499999999999999, + -1.45, + -1.5499999999999998, + -1.6499999999999997, + -1.7499999999999998, + -1.8499999999999996, + -1.9499999999999993, + -2.0499999999999994, + -2.15, + -2.25, + -2.3499999999999996, + -2.45, + -2.55, + -2.65, + -2.750000000000001, + -2.8500000000000005, + -2.950000000000001, + -3.0500000000000016, + -3.1500000000000012, + -3.250000000000002, + -3.350000000000003, + -3.450000000000003, + -3.550000000000004, + -3.650000000000005, + -3.7500000000000044, + -3.850000000000005, + -3.9500000000000064 + ] + }, + "VSM streamwise N=40": { + "CL": 0.3644656343303096, + "CDi_blade": 0.005462440508289194, + "CDi_trefftz": 0.005625520698862054, + "converged": true, + "gamma": [ + 1.886909064437421, + 2.657052702400256, + 3.111902365212739, + 3.4053756211482953, + 3.6029848421367747, + 3.739258542925619, + 3.834206691841891, + 3.9000858809719348, + 3.9446693353292, + 3.9729821956739784, + 3.988278635343286, + 3.9926120357385004, + 3.987171451185717, + 3.9724749795719845, + 3.9484701747242696, + 3.9145750317098513, + 3.869705286423259, + 3.81243681061667, + 3.7420101445944405, + 3.66468820317217, + 3.6646882031721697, + 3.742010144594441, + 3.8124368106166715, + 3.8697052864232604, + 3.9145750317098553, + 3.9484701747242723, + 3.972474979571988, + 3.9871714511857173, + 3.9926120357385013, + 3.988278635343285, + 3.972982195673976, + 3.9446693353291966, + 3.9000858809719317, + 3.834206691841888, + 3.739258542925615, + 3.602984842136771, + 3.405375621148295, + 3.111902365212741, + 2.657052702400262, + 1.8869090644374293 + ], + "y": [ + 3.9, + 3.7, + 3.5, + 3.3, + 3.1, + 2.8999999999999995, + 2.7, + 2.5, + 2.3, + 2.0999999999999996, + 1.8999999999999995, + 1.6999999999999995, + 1.4999999999999991, + 1.2999999999999994, + 1.0999999999999994, + 0.8999999999999992, + 0.6999999999999991, + 0.4999999999999993, + 0.2999999999999995, + 0.09999999999999973, + -0.10000000000000026, + -0.30000000000000027, + -0.4999999999999998, + -0.6999999999999997, + -0.8999999999999995, + -1.0999999999999994, + -1.2999999999999994, + -1.499999999999999, + -1.6999999999999988, + -1.8999999999999986, + -2.0999999999999988, + -2.299999999999999, + -2.4999999999999982, + -2.6999999999999984, + -2.8999999999999986, + -3.099999999999998, + -3.299999999999997, + -3.499999999999997, + -3.699999999999996, + -3.899999999999995 + ] + }, + "VSM streamwise N=80": { + "CL": 0.3616154344379573, + "CDi_blade": 0.005505130842781469, + "CDi_trefftz": 0.005606779780364248, + "converged": true, + "gamma": [ + 1.3628697295120118, + 1.9836775462263103, + 2.4031702969187125, + 2.715065717147977, + 2.956796935484428, + 3.148601813137587, + 3.303181398470388, + 3.429171784167977, + 3.5327324241792932, + 3.618398197577404, + 3.6895854555704473, + 3.7489133834157164, + 3.798417897581681, + 3.8396990788537626, + 3.8740255854122574, + 3.902410212366286, + 3.9256655338658453, + 3.9444454622436655, + 3.9592766424033137, + 3.970582374775044, + 3.9787009542114755, + 3.9838997685182655, + 3.9863861255547417, + 3.98631551476207, + 3.9837978218376247, + 3.9789018818135165, + 3.9716586630415813, + 3.9620633172491977, + 3.95007631220316, + 3.9356239000245625, + 3.918598307097333, + 3.898858354813649, + 3.8762319484288934, + 3.850523510207724, + 3.8215332449130033, + 3.7891044911285996, + 3.7532402737154342, + 3.714403762108531, + 3.67437426939289, + 3.639239656937624, + 3.6392396569376237, + 3.674374269392894, + 3.7144037621085317, + 3.7532402737154396, + 3.7891044911286005, + 3.8215332449130073, + 3.8505235102077258, + 3.876231948428895, + 3.8988583548136506, + 3.9185983070973363, + 3.9356239000245647, + 3.9500763122031652, + 3.9620633172491995, + 3.9716586630415844, + 3.9789018818135182, + 3.983797821837628, + 3.9863155147620697, + 3.9863861255547413, + 3.9838997685182687, + 3.978700954211478, + 3.970582374775048, + 3.95927664240332, + 3.9444454622436713, + 3.925665533865848, + 3.9024102123662936, + 3.874025585412263, + 3.839699078853768, + 3.7984178975816887, + 3.7489133834157236, + 3.6895854555704553, + 3.6183981975774135, + 3.5327324241793026, + 3.429171784167989, + 3.3031813984703993, + 3.1486018131375992, + 2.9567969354844394, + 2.7150657171479873, + 2.4031702969187223, + 1.9836775462263199, + 1.3628697295120153 + ], + "y": [ + 3.95, + 3.8499999999999996, + 3.75, + 3.6500000000000004, + 3.55, + 3.45, + 3.3499999999999996, + 3.25, + 3.1500000000000004, + 3.05, + 2.95, + 2.8499999999999996, + 2.75, + 2.6500000000000004, + 2.55, + 2.45, + 2.3500000000000005, + 2.25, + 2.15, + 2.05, + 1.9499999999999997, + 1.8499999999999999, + 1.75, + 1.65, + 1.5499999999999998, + 1.45, + 1.35, + 1.2500000000000002, + 1.1500000000000001, + 1.0500000000000003, + 0.9500000000000004, + 0.8500000000000003, + 0.7500000000000004, + 0.6500000000000005, + 0.55, + 0.4500000000000002, + 0.3500000000000001, + 0.24999999999999956, + 0.14999999999999958, + 0.049999999999999545, + -0.050000000000000974, + -0.1500000000000013, + -0.25000000000000094, + -0.35000000000000064, + -0.45000000000000073, + -0.5500000000000006, + -0.6500000000000004, + -0.7500000000000006, + -0.8500000000000003, + -0.95, + -1.0500000000000003, + -1.1500000000000004, + -1.25, + -1.3499999999999999, + -1.45, + -1.5499999999999998, + -1.6499999999999997, + -1.7499999999999998, + -1.8499999999999996, + -1.9499999999999993, + -2.0499999999999994, + -2.15, + -2.25, + -2.3499999999999996, + -2.45, + -2.55, + -2.65, + -2.750000000000001, + -2.8500000000000005, + -2.950000000000001, + -3.0500000000000016, + -3.1500000000000012, + -3.250000000000002, + -3.350000000000003, + -3.450000000000003, + -3.550000000000004, + -3.650000000000005, + -3.7500000000000044, + -3.850000000000005, + -3.9500000000000064 + ] + }, + "VSM perpendicular N=40": { + "CL": 0.33389488862867633, + "CDi_blade": 0.0059705633030630485, + "CDi_trefftz": 0.005552547917478836, + "converged": true, + "gamma": [ + 1.9884609724201368, + 2.750052666238737, + 3.1805677147413642, + 3.451497293664314, + 3.6312045785248768, + 3.7537897476777924, + 3.838256269201091, + 3.8959106366564447, + 3.9337494915916373, + 3.9561781859545153, + 3.9659408230649187, + 3.964640695377562, + 3.953030327882875, + 3.9311637010350946, + 3.8984723891952417, + 3.853864537125407, + 3.7962985324628273, + 3.7324405207851927, + 3.673457553318494, + 3.628167445620806, + 3.6281674456208064, + 3.6734575533184954, + 3.732440520785193, + 3.796298532462828, + 3.8538645371254074, + 3.8984723891952435, + 3.9311637010350955, + 3.953030327882878, + 3.964640695377563, + 3.9659408230649187, + 3.9561781859545153, + 3.9337494915916364, + 3.895910636656444, + 3.8382562692010893, + 3.7537897476777897, + 3.6312045785248723, + 3.45149729366431, + 3.1805677147413602, + 2.750052666238738, + 1.9884609724201414 + ], + "y": [ + 3.9, + 3.7, + 3.5, + 3.3, + 3.1, + 2.8999999999999995, + 2.7, + 2.5, + 2.3, + 2.0999999999999996, + 1.8999999999999995, + 1.6999999999999995, + 1.4999999999999991, + 1.2999999999999994, + 1.0999999999999994, + 0.8999999999999992, + 0.6999999999999991, + 0.4999999999999993, + 0.2999999999999995, + 0.09999999999999973, + -0.10000000000000026, + -0.30000000000000027, + -0.4999999999999998, + -0.6999999999999997, + -0.8999999999999995, + -1.0999999999999994, + -1.2999999999999994, + -1.499999999999999, + -1.6999999999999988, + -1.8999999999999986, + -2.0999999999999988, + -2.299999999999999, + -2.4999999999999982, + -2.6999999999999984, + -2.8999999999999986, + -3.099999999999998, + -3.299999999999997, + -3.499999999999997, + -3.699999999999996, + -3.899999999999995 + ] + }, + "VSM perpendicular N=80": { + "CL": 0.3310762587735496, + "CDi_blade": 0.006061274278544326, + "CDi_trefftz": 0.005546105320731539, + "converged": true, + "gamma": [ + 1.4467296986313325, + 2.0851023432325726, + 2.5029531254566364, + 2.8054583291914885, + 3.035147028370476, + 3.214606802683687, + 3.3575618419477933, + 3.473035035502603, + 3.5672721726240955, + 3.6447566175870953, + 3.708794630308729, + 3.761875543982453, + 3.8059043977164304, + 3.8423579258088276, + 3.872392303904549, + 3.8969193674361016, + 3.91666157428591, + 3.93219225118341, + 3.943965410686559, + 3.9523380178796987, + 3.9575866796058525, + 3.9599201305789027, + 3.9594884875660896, + 3.956389968553331, + 3.950675590200881, + 3.9423522458804734, + 3.9313845294665515, + 3.91769573629134, + 3.901168728430678, + 3.881648023008822, + 3.858946184787963, + 3.83286248438932, + 3.8032391703557686, + 3.7701803458483663, + 3.736508050816929, + 3.705390787298346, + 3.676303352529526, + 3.649263326972028, + 3.626302182545813, + 3.6107239003942238, + 3.6107239003942224, + 3.6263021825458153, + 3.64926332697203, + 3.676303352529528, + 3.7053907872983487, + 3.7365080508169313, + 3.770180345848368, + 3.80323917035577, + 3.8328624843893206, + 3.8589461847879667, + 3.8816480230088244, + 3.90116872843068, + 3.9176957362913427, + 3.9313845294665533, + 3.9423522458804747, + 3.950675590200882, + 3.956389968553332, + 3.959488487566092, + 3.9599201305789045, + 3.9575866796058534, + 3.9523380178796987, + 3.9439654106865567, + 3.9321922511834084, + 3.9166615742859108, + 3.8969193674361016, + 3.87239230390455, + 3.8423579258088285, + 3.8059043977164317, + 3.7618755439824554, + 3.7087946303087294, + 3.6447566175870953, + 3.567272172624095, + 3.473035035502601, + 3.3575618419477893, + 3.2146068026836843, + 3.0351470283704707, + 2.805458329191482, + 2.502953125456629, + 2.085102343232564, + 1.4467296986313207 + ], + "y": [ + 3.95, + 3.8499999999999996, + 3.75, + 3.6500000000000004, + 3.55, + 3.45, + 3.3499999999999996, + 3.25, + 3.1500000000000004, + 3.05, + 2.95, + 2.8499999999999996, + 2.75, + 2.6500000000000004, + 2.55, + 2.45, + 2.3500000000000005, + 2.25, + 2.15, + 2.05, + 1.9499999999999997, + 1.8499999999999999, + 1.75, + 1.65, + 1.5499999999999998, + 1.45, + 1.35, + 1.2500000000000002, + 1.1500000000000001, + 1.0500000000000003, + 0.9500000000000004, + 0.8500000000000003, + 0.7500000000000004, + 0.6500000000000005, + 0.55, + 0.4500000000000002, + 0.3500000000000001, + 0.24999999999999956, + 0.14999999999999958, + 0.049999999999999545, + -0.050000000000000974, + -0.1500000000000013, + -0.25000000000000094, + -0.35000000000000064, + -0.45000000000000073, + -0.5500000000000006, + -0.6500000000000004, + -0.7500000000000006, + -0.8500000000000003, + -0.95, + -1.0500000000000003, + -1.1500000000000004, + -1.25, + -1.3499999999999999, + -1.45, + -1.5499999999999998, + -1.6499999999999997, + -1.7499999999999998, + -1.8499999999999996, + -1.9499999999999993, + -2.0499999999999994, + -2.15, + -2.25, + -2.3499999999999996, + -2.45, + -2.55, + -2.65, + -2.750000000000001, + -2.8500000000000005, + -2.950000000000001, + -3.0500000000000016, + -3.1500000000000012, + -3.250000000000002, + -3.350000000000003, + -3.450000000000003, + -3.550000000000004, + -3.650000000000005, + -3.7500000000000044, + -3.850000000000005, + -3.9500000000000064 + ] + } + }, + "arc": { + "projected_area": 6.553216354311932, + "VLM 8x44": { + "CL": 0.5792924649034502, + "CDi_near": 0.013278360007589128, + "CDi_trefftz": 0.014803081291523208, + "gamma": [ + 0.9825437673919295, + 1.4314992169072622, + 1.762262483126888, + 2.009616308940748, + 2.228893672621231, + 2.4040537031724853, + 2.5701307506125577, + 2.7054386836668227, + 2.8379703361304105, + 2.946254787312108, + 3.0537878146967232, + 3.1410404036298614, + 3.2277221225877084, + 3.2969778733240194, + 3.3649355940165346, + 3.4177986097533184, + 3.4681501737444957, + 3.5054814527469453, + 3.538812809920766, + 3.561067094285481, + 3.5776719788297173, + 3.585068413009487, + 3.5850684130094876, + 3.577671978829718, + 3.5610670942854803, + 3.538812809920766, + 3.5054814527469453, + 3.4681501737444944, + 3.417798609753318, + 3.3649355940165337, + 3.2969778733240203, + 3.227722122587707, + 3.141040403629859, + 3.0537878146967223, + 2.9462547873121068, + 2.83797033613041, + 2.705438683666822, + 2.570130750612557, + 2.404053703172485, + 2.228893672621231, + 2.009616308940748, + 1.7622624831268878, + 1.4314992169072616, + 0.9825437673919296 + ], + "y": [ + -3.2235005759859536, + -3.1172853736459256, + -3.0052401105434816, + -2.8873647866786207, + -2.7641079532461816, + -2.635469610246165, + -2.501939265410664, + -2.3635169187396774, + -2.220729309053138, + -2.073576436351046, + -1.9226182617406993, + -1.7678547852220983, + -1.6098749285477671, + -1.4486786917177055, + -1.284879475079527, + -1.1184772786332309, + -0.9501053129744925, + -0.7797635781033116, + -0.6081002757484492, + -0.43511540590990483, + -0.2614672282429745, + -0.08715574274765817, + 0.08715574274765817, + 0.2614672282429745, + 0.43511540590990483, + 0.6081002757484492, + 0.7797635781033116, + 0.9501053129744925, + 1.1184772786332309, + 1.284879475079527, + 1.4486786917177055, + 1.6098749285477671, + 1.7678547852220983, + 1.9226182617406993, + 2.073576436351046, + 2.220729309053138, + 2.3635169187396774, + 2.501939265410664, + 2.635469610246165, + 2.7641079532461816, + 2.8873647866786207, + 3.0052401105434816, + 3.1172853736459256, + 3.2235005759859536 + ] + }, + "VLM 16x88": { + "CL": 0.5770251979274088, + "CDi_near": 0.013271103392878032, + "CDi_trefftz": 0.014814710638627498, + "gamma": [ + 0.614610348626413, + 1.0142118085891385, + 1.32843371741417, + 1.5042422785796352, + 1.637248166899498, + 1.7852455504050802, + 1.9519936078481717, + 2.061145822219151, + 2.148704425536142, + 2.2473831156768647, + 2.3643717506991626, + 2.4448051978722827, + 2.5111077543416798, + 2.5856711865222937, + 2.675622479526875, + 2.738892653337979, + 2.7918497437584193, + 2.8510892762420683, + 2.9228784539980426, + 2.973852492709949, + 3.016862525904579, + 3.064663872037164, + 3.122410496805535, + 3.1634224384044023, + 3.19812138513989, + 3.2363930431264603, + 3.282178117881239, + 3.3144238663309378, + 3.3416450954834134, + 3.3713772963806243, + 3.40629991592166, + 3.430412398724495, + 3.4505907730023266, + 3.4723101318088645, + 3.4969606882487656, + 3.5132766900775536, + 3.5266384592220383, + 3.540624162672029, + 3.5553150290267723, + 3.5640077726375545, + 3.5706655014632407, + 3.577057868924602, + 3.5819398930792037, + 3.5830856779906632, + 3.5830856779906632, + 3.581939893079204, + 3.5770578689246015, + 3.570665501463241, + 3.5640077726375545, + 3.555315029026773, + 3.5406241626720294, + 3.5266384592220374, + 3.513276690077553, + 3.4969606882487665, + 3.472310131808864, + 3.4505907730023266, + 3.430412398724494, + 3.406299915921659, + 3.371377296380624, + 3.341645095483414, + 3.3144238663309373, + 3.282178117881239, + 3.2363930431264603, + 3.19812138513989, + 3.1634224384044023, + 3.122410496805535, + 3.064663872037165, + 3.01686252590458, + 2.9738524927099492, + 2.9228784539980435, + 2.8510892762420688, + 2.7918497437584198, + 2.73889265333798, + 2.675622479526875, + 2.585671186522294, + 2.5111077543416798, + 2.444805197872283, + 2.364371750699163, + 2.247383115676865, + 2.1487044255361423, + 2.061145822219151, + 1.9519936078481712, + 1.7852455504050802, + 1.6372481668994976, + 1.5042422785796354, + 1.3284337174141698, + 1.0142118085891383, + 0.614610348626413 + ], + "y": [ + -3.2610533209058206, + -3.207945719735807, + -3.1328402298960722, + -3.0797326287260587, + -3.0469153309631833, + -2.9879776690307533, + -2.904627228191349, + -2.845689566258919, + -2.809588475574138, + -2.74526930407413, + -2.654308259418217, + -2.589989087918209, + -2.5508789554100684, + -2.4816677820745756, + -2.3837884020757656, + -2.314577228740273, + -2.272755706132503, + -2.199179269781457, + -2.095126475622727, + -2.021550039271681, + -1.977335413603853, + -1.8999536753445527, + -1.7905193716182448, + -1.7131376333589445, + -1.6668664046299115, + -1.586268286214881, + -1.4722853340505917, + -1.391687215635561, + -1.3437115358352827, + -1.2605104376121348, + -1.142846316100623, + -1.059645217877475, + -1.010330210897739, + -0.9251593434621486, + -0.8047095476156556, + -0.7195386801800653, + -0.6692596630012027, + -0.5827672280819305, + -0.4604484535764235, + -0.37395601865715133, + -0.32309564495919385, + -0.23593990221153568, + -0.11268306877909699, + -0.025527326031438823, + 0.025527326031438823, + 0.11268306877909699, + 0.23593990221153568, + 0.32309564495919385, + 0.37395601865715133, + 0.4604484535764235, + 0.5827672280819305, + 0.6692596630012027, + 0.7195386801800653, + 0.8047095476156556, + 0.9251593434621486, + 1.010330210897739, + 1.059645217877475, + 1.142846316100623, + 1.2605104376121348, + 1.3437115358352827, + 1.391687215635561, + 1.4722853340505917, + 1.586268286214881, + 1.6668664046299115, + 1.7131376333589445, + 1.7905193716182448, + 1.8999536753445527, + 1.977335413603853, + 2.021550039271681, + 2.095126475622727, + 2.199179269781457, + 2.272755706132503, + 2.314577228740273, + 2.3837884020757656, + 2.4816677820745756, + 2.5508789554100684, + 2.589989087918209, + 2.654308259418217, + 2.74526930407413, + 2.809588475574138, + 2.845689566258919, + 2.904627228191349, + 2.9879776690307533, + 3.0469153309631833, + 3.0797326287260587, + 3.1328402298960722, + 3.207945719735807, + 3.2610533209058206 + ] + }, + "VSM rib N=40": { + "CL": 0.5814516426807416, + "CDi_blade": 0.014296304216648528, + "CDi_trefftz": 0.014308998234422656, + "converged": true, + "gamma": [ + 0.9730189028255855, + 1.4347061400333432, + 1.764003824018121, + 2.02592885827172, + 2.245344204251138, + 2.4346564828292503, + 2.600926876281286, + 2.7484954636312033, + 2.880164369965378, + 2.997803324618766, + 3.1026880069334664, + 3.195701647807816, + 3.277461191722386, + 3.3483993273145436, + 3.4088194406522385, + 3.4589332672961706, + 3.4988870790823485, + 3.528780000309733, + 3.548676715673329, + 3.558616005485527, + 3.5586160054855265, + 3.548676715673328, + 3.528780000309735, + 3.4988870790823476, + 3.4589332672961692, + 3.408819440652239, + 3.3483993273145436, + 3.277461191722385, + 3.1957016478078155, + 3.102688006933467, + 2.997803324618765, + 2.8801643699653763, + 2.748495463631203, + 2.6009268762812843, + 2.434656482829249, + 2.2453442042511362, + 2.025928858271718, + 1.7640038240181177, + 1.4347061400333385, + 0.9730189028255819 + ], + "y": [ + 3.21968323606567, + 3.1021910786877482, + 2.9775538709648623, + 2.8460586806827957, + 2.708008371118132, + 2.5637209034753767, + 2.413528604550108, + 2.2577774013049234, + 2.096826024121107, + 1.9310451805611206, + 1.7608167015449245, + 1.5865326619066806, + 1.408594477357399, + 1.2274119799334202, + 1.043402474060192, + 0.8569897754054363, + 0.6686032347354487, + 0.47867674902280705, + 0.2876477620831363, + 0.09595625704268829, + -0.09595625704268718, + -0.28764776208313536, + -0.47867674902280666, + -0.6686032347354482, + -0.8569897754054354, + -1.0434024740601906, + -1.2274119799334193, + -1.4085944773573986, + -1.5865326619066797, + -1.7608167015449234, + -1.9310451805611204, + -2.096826024121107, + -2.257777401304923, + -2.413528604550108, + -2.563720903475377, + -2.7080083711181326, + -2.8460586806827957, + -2.977553870964863, + -3.102191078687749, + -3.219683236065671 + ] + }, + "VSM rib N=80": { + "CL": 0.5787728279860994, + "CDi_blade": 0.01430224827629082, + "CDi_trefftz": 0.014317975854176745, + "converged": true, + "gamma": [ + 0.6963036887124913, + 1.0352947833707262, + 1.2828330827111347, + 1.4837644667536498, + 1.6551524963088562, + 1.8056580518782075, + 1.9403981224653055, + 2.0626899154166556, + 2.1748207806579534, + 2.278438102976105, + 2.37476704055966, + 2.464741314706799, + 2.5490863011487845, + 2.6283742076227123, + 2.7030620485527184, + 2.7735185606708637, + 2.8400437557452576, + 2.9028834244654163, + 2.9622400899898436, + 3.018281410030547, + 3.0711467101956953, + 3.1209521255459833, + 3.1677946900487366, + 3.2117556200122643, + 3.2529029725028784, + 3.291293813699111, + 3.32697599903759, + 3.359989642867421, + 3.3903683374946647, + 3.4181401681586023, + 3.443328560389279, + 3.4659529884782043, + 3.486029567829879, + 3.503571549305324, + 3.518589729994368, + 3.531092791921645, + 3.5410875778231468, + 3.5485793111900588, + 3.5535717661610216, + 3.5560673914710828, + 3.5560673914710836, + 3.5535717661610224, + 3.548579311190061, + 3.5410875778231468, + 3.5310927919216457, + 3.5185897299943694, + 3.503571549305325, + 3.4860295678298807, + 3.465952988478207, + 3.443328560389282, + 3.4181401681586054, + 3.3903683374946687, + 3.3599896428674216, + 3.326975999037594, + 3.291293813699112, + 3.2529029725028797, + 3.2117556200122657, + 3.167794690048736, + 3.1209521255459856, + 3.0711467101956957, + 3.018281410030549, + 2.962240089989846, + 2.9028834244654185, + 2.8400437557452616, + 2.7735185606708685, + 2.703062048552725, + 2.628374207622718, + 2.5490863011487876, + 2.4647413147068034, + 2.374767040559663, + 2.278438102976107, + 2.174820780657957, + 2.0626899154166574, + 1.9403981224653077, + 1.8056580518782097, + 1.6551524963088564, + 1.48376446675365, + 1.2828330827111347, + 1.0352947833707264, + 0.6963036887124915 + ], + "y": [ + 3.2486093857037, + 3.1916844446134034, + 3.1329214454223435, + 3.0723542291347186, + 3.010017675784714, + 2.945947684349453, + 2.8801811520751435, + 2.8127559532283373, + 2.743710917284532, + 2.673085806566675, + 2.6009212933464543, + 2.527258936421556, + 2.452141157182382, + 2.375611215182012, + 2.29771318322347, + 2.2184919219786554, + 2.1379930541535432, + 2.056262938214542, + 1.9733486416911274, + 1.8892979140701427, + 1.8041591592973596, + 1.717981407902148, + 1.6308142887612982, + 1.542708000518266, + 1.4537132826742911, + 1.3638813863680412, + 1.2732640448606145, + 1.1819134437428853, + 1.0898821908823626, + 0.9972232861268635, + 0.903990090782447, + 0.8102362968831904, + 0.7160158962705006, + 0.6213831494997692, + 0.526392554592283, + 0.43109881565037234, + 0.3355568113538858, + 0.2398215633561258, + 0.14394820459744476, + 0.04799194755475664, + -0.04799194755475929, + -0.14394820459744742, + -0.23982156335612853, + -0.3355568113538881, + -0.43109881565037383, + -0.5263925545922846, + -0.6213831494997704, + -0.7160158962705009, + -0.8102362968831911, + -0.9039900907824472, + -0.9972232861268631, + -1.0898821908823622, + -1.1819134437428844, + -1.2732640448606132, + -1.3638813863680403, + -1.4537132826742898, + -1.5427080005182647, + -1.6308142887612969, + -1.7179814079021465, + -1.8041591592973576, + -1.8892979140701405, + -1.9733486416911257, + -2.0562629382145396, + -2.137993054153541, + -2.2184919219786527, + -2.297713183223467, + -2.3756112151820084, + -2.4521411571823792, + -2.5272589364215534, + -2.6009212933464516, + -2.6730858065666725, + -2.7437109172845293, + -2.8127559532283346, + -2.8801811520751404, + -2.9459476843494503, + -3.010017675784712, + -3.0723542291347163, + -3.1329214454223413, + -3.191684444613401, + -3.248609385703698 + ] + }, + "VSM streamwise N=40": { + "CL": 0.5676924267176111, + "CDi_blade": 0.01457926910459961, + "CDi_trefftz": 0.014130534669940005, + "converged": true, + "gamma": [ + 0.9651794012844642, + 1.4229356535458, + 1.7497283782023643, + 2.010126991494939, + 2.228593381070561, + 2.4173096955808804, + 2.5832045649295523, + 2.730537180211989, + 2.8620592685123345, + 2.9796087235547497, + 3.0844409354388267, + 3.177426132597031, + 3.2591728919534932, + 3.3301084833055317, + 3.39053272928883, + 3.4406549325452103, + 3.480619564184325, + 3.510524208600106, + 3.530431930950004, + 3.540379300171199, + 3.540379300171199, + 3.5304319309500043, + 3.5105242086001045, + 3.480619564184324, + 3.4406549325452103, + 3.3905327292888283, + 3.3301084833055308, + 3.259172891953493, + 3.1774261325970303, + 3.0844409354388262, + 2.979608723554749, + 2.8620592685123314, + 2.7305371802119875, + 2.58320456492955, + 2.4173096955808755, + 2.2285933810705556, + 2.010126991494935, + 1.7497283782023585, + 1.4229356535457913, + 0.9651794012844606 + ], + "y": [ + 3.21968323606567, + 3.1021910786877482, + 2.9775538709648623, + 2.8460586806827957, + 2.708008371118132, + 2.5637209034753767, + 2.413528604550108, + 2.2577774013049234, + 2.096826024121107, + 1.9310451805611206, + 1.7608167015449245, + 1.5865326619066806, + 1.408594477357399, + 1.2274119799334202, + 1.043402474060192, + 0.8569897754054363, + 0.6686032347354487, + 0.47867674902280705, + 0.2876477620831363, + 0.09595625704268829, + -0.09595625704268718, + -0.28764776208313536, + -0.47867674902280666, + -0.6686032347354482, + -0.8569897754054354, + -1.0434024740601906, + -1.2274119799334193, + -1.4085944773573986, + -1.5865326619066797, + -1.7608167015449234, + -1.9310451805611204, + -2.096826024121107, + -2.257777401304923, + -2.413528604550108, + -2.563720903475377, + -2.7080083711181326, + -2.8460586806827957, + -2.977553870964863, + -3.102191078687749, + -3.219683236065671 + ] + }, + "VSM streamwise N=80": { + "CL": 0.5650842016459041, + "CDi_blade": 0.0146274220056569, + "CDi_trefftz": 0.014139392403465463, + "converged": true, + "gamma": [ + 0.6905403505998313, + 1.0263824909151718, + 1.2716499374405665, + 1.4709416686330996, + 1.641094918989962, + 1.7906513341609323, + 1.924654251633791, + 2.0463703255538626, + 2.158050151516789, + 2.2613135498203634, + 2.357364632904533, + 2.4471208695646145, + 2.5312949846570936, + 2.6104492802133823, + 2.685032979025141, + 2.7554086673364506, + 2.821871488740592, + 2.8846633731045315, + 2.943983777673747, + 2.9999979238366974, + 3.052843200999115, + 3.1026342061706913, + 3.1494667526938462, + 3.193421089471344, + 3.2345645080948717, + 3.272953470065048, + 3.308635353819432, + 3.3416498976194036, + 3.372030396868708, + 3.3998047013672124, + 3.42499604811503, + 3.447623757714238, + 3.4677038165539837, + 3.485249362362292, + 3.5002710870248555, + 3.5127775675213564, + 3.5227755331017065, + 3.5302700738484574, + 3.535264790694528, + 3.5377618693094273, + 3.5377618693094295, + 3.535264790694525, + 3.530270073848458, + 3.5227755331017065, + 3.5127775675213577, + 3.5002710870248537, + 3.4852493623622927, + 3.467703816553981, + 3.4476237577142403, + 3.4249960481150303, + 3.3998047013672132, + 3.3720303968687073, + 3.3416498976194036, + 3.3086353538194344, + 3.2729534700650484, + 3.2345645080948726, + 3.193421089471345, + 3.1494667526938445, + 3.1026342061706904, + 3.0528432009991153, + 2.9999979238366983, + 2.9439837776737496, + 2.884663373104535, + 2.8218714887405945, + 2.755408667336454, + 2.6850329790251464, + 2.6104492802133867, + 2.5312949846570967, + 2.447120869564617, + 2.3573646329045355, + 2.2613135498203643, + 2.1580501515167905, + 2.046370325553866, + 1.9246542516337954, + 1.7906513341609374, + 1.641094918989968, + 1.4709416686331054, + 1.271649937440573, + 1.0263824909151777, + 0.6905403505998338 + ], + "y": [ + 3.2486093857037, + 3.1916844446134034, + 3.1329214454223435, + 3.0723542291347186, + 3.010017675784714, + 2.945947684349453, + 2.8801811520751435, + 2.8127559532283373, + 2.743710917284532, + 2.673085806566675, + 2.6009212933464543, + 2.527258936421556, + 2.452141157182382, + 2.375611215182012, + 2.29771318322347, + 2.2184919219786554, + 2.1379930541535432, + 2.056262938214542, + 1.9733486416911274, + 1.8892979140701427, + 1.8041591592973596, + 1.717981407902148, + 1.6308142887612982, + 1.542708000518266, + 1.4537132826742911, + 1.3638813863680412, + 1.2732640448606145, + 1.1819134437428853, + 1.0898821908823626, + 0.9972232861268635, + 0.903990090782447, + 0.8102362968831904, + 0.7160158962705006, + 0.6213831494997692, + 0.526392554592283, + 0.43109881565037234, + 0.3355568113538858, + 0.2398215633561258, + 0.14394820459744476, + 0.04799194755475664, + -0.04799194755475929, + -0.14394820459744742, + -0.23982156335612853, + -0.3355568113538881, + -0.43109881565037383, + -0.5263925545922846, + -0.6213831494997704, + -0.7160158962705009, + -0.8102362968831911, + -0.9039900907824472, + -0.9972232861268631, + -1.0898821908823622, + -1.1819134437428844, + -1.2732640448606132, + -1.3638813863680403, + -1.4537132826742898, + -1.5427080005182647, + -1.6308142887612969, + -1.7179814079021465, + -1.8041591592973576, + -1.8892979140701405, + -1.9733486416911257, + -2.0562629382145396, + -2.137993054153541, + -2.2184919219786527, + -2.297713183223467, + -2.3756112151820084, + -2.4521411571823792, + -2.5272589364215534, + -2.6009212933464516, + -2.6730858065666725, + -2.7437109172845293, + -2.8127559532283346, + -2.8801811520751404, + -2.9459476843494503, + -3.010017675784712, + -3.0723542291347163, + -3.1329214454223413, + -3.191684444613401, + -3.248609385703698 + ] + }, + "VSM perpendicular N=40": { + "CL": 0.5814516426807415, + "CDi_blade": 0.014296304216648518, + "CDi_trefftz": 0.014308998234422651, + "converged": true, + "gamma": [ + 0.9730189028255853, + 1.434706140033343, + 1.764003824018121, + 2.02592885827172, + 2.245344204251138, + 2.4346564828292503, + 2.6009268762812856, + 2.7484954636312033, + 2.880164369965378, + 2.9978033246187654, + 3.1026880069334664, + 3.195701647807815, + 3.2774611917223857, + 3.3483993273145423, + 3.408819440652238, + 3.45893326729617, + 3.498887079082348, + 3.528780000309732, + 3.548676715673328, + 3.5586160054855256, + 3.5586160054855265, + 3.548676715673327, + 3.5287800003097325, + 3.4988870790823468, + 3.4589332672961692, + 3.408819440652238, + 3.3483993273145427, + 3.2774611917223844, + 3.195701647807815, + 3.1026880069334655, + 2.9978033246187645, + 2.880164369965376, + 2.748495463631203, + 2.6009268762812843, + 2.434656482829249, + 2.2453442042511367, + 2.0259288582717185, + 1.7640038240181177, + 1.4347061400333385, + 0.9730189028255819 + ], + "y": [ + 3.21968323606567, + 3.1021910786877482, + 2.9775538709648623, + 2.8460586806827957, + 2.708008371118132, + 2.5637209034753767, + 2.413528604550108, + 2.2577774013049234, + 2.096826024121107, + 1.9310451805611206, + 1.7608167015449245, + 1.5865326619066806, + 1.408594477357399, + 1.2274119799334202, + 1.043402474060192, + 0.8569897754054363, + 0.6686032347354487, + 0.47867674902280705, + 0.2876477620831363, + 0.09595625704268829, + -0.09595625704268718, + -0.28764776208313536, + -0.47867674902280666, + -0.6686032347354482, + -0.8569897754054354, + -1.0434024740601906, + -1.2274119799334193, + -1.4085944773573986, + -1.5865326619066797, + -1.7608167015449234, + -1.9310451805611204, + -2.096826024121107, + -2.257777401304923, + -2.413528604550108, + -2.563720903475377, + -2.7080083711181326, + -2.8460586806827957, + -2.977553870964863, + -3.102191078687749, + -3.219683236065671 + ] + }, + "VSM perpendicular N=80": { + "CL": 0.5787728279860996, + "CDi_blade": 0.014302248276290817, + "CDi_trefftz": 0.014317975854176743, + "converged": true, + "gamma": [ + 0.6963036887124914, + 1.0352947833707262, + 1.2828330827111347, + 1.4837644667536498, + 1.6551524963088562, + 1.8056580518782077, + 1.9403981224653055, + 2.0626899154166556, + 2.1748207806579534, + 2.278438102976105, + 2.37476704055966, + 2.4647413147067994, + 2.5490863011487845, + 2.628374207622713, + 2.703062048552719, + 2.7735185606708637, + 2.840043755745258, + 2.9028834244654163, + 2.9622400899898436, + 3.0182814100305473, + 3.071146710195695, + 3.1209521255459842, + 3.167794690048736, + 3.2117556200122643, + 3.252902972502879, + 3.2912938136991117, + 3.3269759990375904, + 3.359989642867421, + 3.3903683374946647, + 3.418140168158601, + 3.443328560389279, + 3.465952988478204, + 3.486029567829879, + 3.503571549305324, + 3.518589729994368, + 3.5310927919216453, + 3.5410875778231468, + 3.5485793111900596, + 3.553571766161022, + 3.5560673914710823, + 3.5560673914710836, + 3.5535717661610224, + 3.5485793111900596, + 3.5410875778231468, + 3.5310927919216453, + 3.5185897299943694, + 3.503571549305325, + 3.4860295678298807, + 3.465952988478207, + 3.4433285603892827, + 3.4181401681586054, + 3.3903683374946687, + 3.3599896428674216, + 3.326975999037594, + 3.2912938136991126, + 3.2529029725028797, + 3.2117556200122657, + 3.167794690048737, + 3.120952125545986, + 3.071146710195696, + 3.018281410030549, + 2.962240089989846, + 2.9028834244654185, + 2.840043755745262, + 2.7735185606708685, + 2.7030620485527255, + 2.6283742076227177, + 2.549086301148787, + 2.464741314706802, + 2.3747670405596617, + 2.278438102976106, + 2.174820780657956, + 2.062689915416657, + 1.9403981224653073, + 1.8056580518782097, + 1.6551524963088562, + 1.4837644667536498, + 1.282833082711135, + 1.0352947833707262, + 0.6963036887124914 + ], + "y": [ + 3.2486093857037, + 3.1916844446134034, + 3.1329214454223435, + 3.0723542291347186, + 3.010017675784714, + 2.945947684349453, + 2.8801811520751435, + 2.8127559532283373, + 2.743710917284532, + 2.673085806566675, + 2.6009212933464543, + 2.527258936421556, + 2.452141157182382, + 2.375611215182012, + 2.29771318322347, + 2.2184919219786554, + 2.1379930541535432, + 2.056262938214542, + 1.9733486416911274, + 1.8892979140701427, + 1.8041591592973596, + 1.717981407902148, + 1.6308142887612982, + 1.542708000518266, + 1.4537132826742911, + 1.3638813863680412, + 1.2732640448606145, + 1.1819134437428853, + 1.0898821908823626, + 0.9972232861268635, + 0.903990090782447, + 0.8102362968831904, + 0.7160158962705006, + 0.6213831494997692, + 0.526392554592283, + 0.43109881565037234, + 0.3355568113538858, + 0.2398215633561258, + 0.14394820459744476, + 0.04799194755475664, + -0.04799194755475929, + -0.14394820459744742, + -0.23982156335612853, + -0.3355568113538881, + -0.43109881565037383, + -0.5263925545922846, + -0.6213831494997704, + -0.7160158962705009, + -0.8102362968831911, + -0.9039900907824472, + -0.9972232861268631, + -1.0898821908823622, + -1.1819134437428844, + -1.2732640448606132, + -1.3638813863680403, + -1.4537132826742898, + -1.5427080005182647, + -1.6308142887612969, + -1.7179814079021465, + -1.8041591592973576, + -1.8892979140701405, + -1.9733486416911257, + -2.0562629382145396, + -2.137993054153541, + -2.2184919219786527, + -2.297713183223467, + -2.3756112151820084, + -2.4521411571823792, + -2.5272589364215534, + -2.6009212933464516, + -2.6730858065666725, + -2.7437109172845293, + -2.8127559532283346, + -2.8801811520751404, + -2.9459476843494503, + -3.010017675784712, + -3.0723542291347163, + -3.1329214454223413, + -3.191684444613401, + -3.248609385703698 + ] + } + }, + "elliptic": { + "VLM 8x40": { + "CL": 0.8107606561173057, + "CDi_near": 0.023659940143601316, + "CDi_trefftz": 0.02620149521599144, + "gamma": [ + 0.7571230386688006, + 1.4400363697548353, + 1.8887847701522082, + 2.2403958553071597, + 2.532295007702691, + 2.7818123063455347, + 2.9986717286196094, + 3.1889455350299403, + 3.3566966559735136, + 3.5047770672119136, + 3.6352590956961706, + 3.7496869363810172, + 3.849232065280525, + 3.934793672499392, + 4.007065858706383, + 4.066583776858744, + 4.1137558665059135, + 4.148886527741228, + 4.172191946535738, + 4.183810782152702, + 4.183810782152702, + 4.1721919465357376, + 4.148886527741228, + 4.1137558665059135, + 4.066583776858743, + 4.007065858706384, + 3.934793672499393, + 3.8492320652805248, + 3.7496869363810172, + 3.6352590956961723, + 3.5047770672119136, + 3.3566966559735136, + 3.18894553502994, + 2.998671728619609, + 2.7818123063455342, + 2.532295007702691, + 2.2403958553071597, + 1.8887847701522087, + 1.4400363697548357, + 0.7571230386688006 + ], + "y": [ + -3.0630528372500483, + -2.9059732045705586, + -2.748893571891069, + -2.5918139392115793, + -2.4347343065320897, + -2.2776546738526, + -2.1205750411731104, + -1.9634954084936207, + -1.806415775814131, + -1.6493361431346414, + -1.4922565104551517, + -1.335176877775662, + -1.1780972450961724, + -1.0210176124166828, + -0.8639379797371931, + -0.7068583470577035, + -0.5497787143782138, + -0.39269908169872414, + -0.23561944901923448, + -0.07853981633974483, + 0.07853981633974483, + 0.23561944901923448, + 0.39269908169872414, + 0.5497787143782138, + 0.7068583470577035, + 0.8639379797371931, + 1.0210176124166828, + 1.1780972450961724, + 1.335176877775662, + 1.4922565104551517, + 1.6493361431346414, + 1.806415775814131, + 1.9634954084936207, + 2.1205750411731104, + 2.2776546738526, + 2.4347343065320897, + 2.5918139392115793, + 2.748893571891069, + 2.9059732045705586, + 3.0630528372500483 + ] + }, + "VLM 16x80": { + "CL": 0.8080924068361507, + "CDi_near": 0.023817689735069217, + "CDi_trefftz": 0.026363651573957644, + "gamma": [ + 0.4472269443727107, + 0.9115561696905462, + 1.2725681087605556, + 1.5419900129613588, + 1.7717900139029088, + 1.9701147604928135, + 2.148813692662368, + 2.308985801757968, + 2.4567929672641076, + 2.5916348321523444, + 2.717720472780191, + 2.833843851245228, + 2.9432889902284534, + 3.0446135618588963, + 3.140564023914497, + 3.2296217658109945, + 3.3141694813633014, + 3.392693764645678, + 3.467300412829606, + 3.536527975468525, + 3.6022518894005366, + 3.6630921043122964, + 3.720719698039071, + 3.773855995404889, + 3.823982532553594, + 3.869936368606454, + 3.9130179273023713, + 3.9521907738098268, + 3.9885788888206766, + 4.021280652349857, + 4.051246031989072, + 4.077714716603737, + 4.101463770871317, + 4.12187925596163, + 4.139565699746562, + 4.154059400437609, + 4.165792339428139, + 4.174453858602711, + 4.180303243577446, + 4.18318471245863, + 4.18318471245863, + 4.180303243577446, + 4.174453858602712, + 4.165792339428138, + 4.154059400437609, + 4.139565699746563, + 4.121879255961631, + 4.101463770871316, + 4.077714716603736, + 4.051246031989071, + 4.021280652349857, + 3.988578888820676, + 3.9521907738098254, + 3.9130179273023717, + 3.8699363686064547, + 3.823982532553596, + 3.7738559954048903, + 3.720719698039071, + 3.6630921043122973, + 3.602251889400537, + 3.536527975468526, + 3.4673004128296068, + 3.392693764645678, + 3.314169481363302, + 3.2296217658109945, + 3.140564023914497, + 3.044613561858896, + 2.9432889902284534, + 2.833843851245228, + 2.717720472780191, + 2.5916348321523444, + 2.456792967264108, + 2.3089858017579674, + 2.148813692662369, + 1.9701147604928138, + 1.7717900139029092, + 1.541990012961359, + 1.2725681087605558, + 0.9115561696905465, + 0.4472269443727109 + ], + "y": [ + -3.1023227454199205, + -3.023782929080176, + -2.945243112740431, + -2.8667032964006864, + -2.788163480060941, + -2.709623663721197, + -2.6310838473814515, + -2.552544031041707, + -2.474004214701962, + -2.3954643983622175, + -2.316924582022472, + -2.238384765682728, + -2.1598449493429825, + -2.081305133003238, + -2.002765316663493, + -1.9242255003237483, + -1.8456856839840035, + -1.7671458676442586, + -1.6886060513045138, + -1.610066234964769, + -1.5315264186250241, + -1.4529866022852793, + -1.3744467859455345, + -1.2959069696057897, + -1.2173671532660448, + -1.1388273369263, + -1.0602875205865552, + -0.9817477042468103, + -0.9032078879070655, + -0.8246680715673207, + -0.7461282552275759, + -0.667588438887831, + -0.5890486225480862, + -0.5105088062083414, + -0.43196898986859655, + -0.3534291735288517, + -0.2748893571891069, + -0.19634954084936207, + -0.11780972450961724, + -0.039269908169872414, + 0.039269908169872414, + 0.11780972450961724, + 0.19634954084936207, + 0.2748893571891069, + 0.3534291735288517, + 0.43196898986859655, + 0.5105088062083414, + 0.5890486225480862, + 0.667588438887831, + 0.7461282552275759, + 0.8246680715673207, + 0.9032078879070655, + 0.9817477042468103, + 1.0602875205865552, + 1.1388273369263, + 1.2173671532660448, + 1.2959069696057897, + 1.3744467859455345, + 1.4529866022852793, + 1.5315264186250241, + 1.610066234964769, + 1.6886060513045138, + 1.7671458676442586, + 1.8456856839840035, + 1.9242255003237483, + 2.002765316663493, + 2.081305133003238, + 2.1598449493429825, + 2.238384765682728, + 2.316924582022472, + 2.3954643983622175, + 2.474004214701962, + 2.552544031041707, + 2.6310838473814515, + 2.709623663721197, + 2.788163480060941, + 2.8667032964006864, + 2.945243112740431, + 3.023782929080176, + 3.1023227454199205 + ] + }, + "VLM 16x160": { + "CL": 0.8069329007597826, + "CDi_near": 0.023856407779173177, + "CDi_trefftz": 0.02643125576450513, + "gamma": [ + 0.19956060726620317, + 0.5066692160419115, + 0.8224423082816052, + 1.0167417788701063, + 1.1573997546745536, + 1.312489491943761, + 1.4912424337871861, + 1.6082858206069761, + 1.6966655734428546, + 1.801898645309422, + 1.9329324813141695, + 2.022285628288558, + 2.090842785943032, + 2.1737107408598186, + 2.279318454097864, + 2.3525596820280033, + 2.409206367071219, + 2.4781200188297863, + 2.5669213357153904, + 2.6290676912647424, + 2.6773667156290823, + 2.7363160897048195, + 2.8127327483054487, + 2.866494863013306, + 2.9084107913772024, + 2.9596551949224477, + 3.0262992418077275, + 3.073328698353089, + 3.1100738475277705, + 3.1550304087020633, + 3.2135848629354467, + 3.25496802126591, + 3.287346623193309, + 3.3269658785281684, + 3.3785807721975325, + 3.41507088537401, + 3.44364383382883, + 3.4785939674218698, + 3.5240886791636563, + 3.5562281881987676, + 3.5814009975389753, + 3.612167899286286, + 3.6521442683781054, + 3.680334750098602, + 3.702408376024029, + 3.729354183380999, + 3.7642641389904203, + 3.7888088678846374, + 3.8080108779046986, + 3.831410225491156, + 3.8615983655322763, + 3.8827294811549846, + 3.8992340773039498, + 3.9192975529007783, + 3.945029051539645, + 3.96292555629021, + 3.9768669061282185, + 3.993756753842367, + 4.015236226337836, + 4.030036248739814, + 4.0415176027227755, + 4.055358504553742, + 4.072743045873564, + 4.0845523599544, + 4.093652400056472, + 4.104538999650307, + 4.117947312860018, + 4.126845312381576, + 4.133622606132207, + 4.141624787274832, + 4.1511435794568765, + 4.15718744940609, + 4.161683552241483, + 4.166850103239916, + 4.172538487187786, + 4.175766039185287, + 4.178007586360465, + 4.180368647154102, + 4.182261012381634, + 4.182692475401193, + 4.182692475401193, + 4.182261012381632, + 4.180368647154101, + 4.178007586360465, + 4.175766039185287, + 4.172538487187788, + 4.166850103239915, + 4.161683552241482, + 4.157187449406088, + 4.151143579456876, + 4.141624787274834, + 4.133622606132207, + 4.126845312381576, + 4.11794731286002, + 4.104538999650309, + 4.093652400056472, + 4.084552359954398, + 4.072743045873564, + 4.055358504553742, + 4.0415176027227755, + 4.030036248739814, + 4.0152362263378345, + 3.9937567538423657, + 3.976866906128217, + 3.962925556290209, + 3.9450290515396436, + 3.9192975529007774, + 3.899234077303948, + 3.8827294811549837, + 3.86159836553228, + 3.831410225491156, + 3.808010877904698, + 3.7888088678846374, + 3.764264138990421, + 3.729354183381001, + 3.70240837602403, + 3.6803347500986034, + 3.65214426837811, + 3.6121678992862876, + 3.581400997538976, + 3.5562281881987667, + 3.5240886791636545, + 3.4785939674218653, + 3.443643833828828, + 3.4150708853740115, + 3.378580772197533, + 3.326965878528167, + 3.287346623193308, + 3.2549680212659116, + 3.213584862935449, + 3.155030408702064, + 3.1100738475277696, + 3.073328698353089, + 3.026299241807729, + 2.959655194922448, + 2.9084107913772037, + 2.8664948630133074, + 2.812732748305451, + 2.7363160897048204, + 2.6773667156290815, + 2.6290676912647406, + 2.566921335715387, + 2.4781200188297827, + 2.4092063670712176, + 2.3525596820280033, + 2.2793184540978633, + 2.1737107408598177, + 2.090842785943032, + 2.0222856282885577, + 1.9329324813141695, + 1.8018986453094217, + 1.6966655734428546, + 1.6082858206069766, + 1.491242433787186, + 1.3124894919437606, + 1.1573997546745531, + 1.016741778870106, + 0.8224423082816048, + 0.5066692160419115, + 0.19956060726620314 + ], + "y": [ + -3.130090763783411, + -3.090820855613538, + -3.0352848188865584, + -2.996014910716686, + -2.973011131103921, + -2.9337412229340485, + -2.8782051862070688, + -2.838935278037196, + -2.815931498424431, + -2.776661590254559, + -2.721125553527579, + -2.681855645357707, + -2.658851865744942, + -2.619581957575069, + -2.5640459208480895, + -2.524776012678217, + -2.501772233065452, + -2.4625023248955795, + -2.4069662881686, + -2.367696379998727, + -2.344692600385962, + -2.30542269221609, + -2.24988665548911, + -2.2106167473192375, + -2.1876129677064724, + -2.1483430595366, + -2.0928070228096205, + -2.053537114639748, + -2.030533335026983, + -1.9912634268571106, + -1.9357273901301308, + -1.8964574819602587, + -1.8734537023474933, + -1.834183794177621, + -1.7786477574506412, + -1.7393778492807688, + -1.7163740696680034, + -1.6771041614981312, + -1.6215681247711515, + -1.5822982166012793, + -1.559294436988514, + -1.5200245288186416, + -1.4644884920916619, + -1.4252185839217895, + -1.4022148043090241, + -1.362944896139152, + -1.3074088594121722, + -1.2681389512423, + -1.2451351716295345, + -1.2058652634596623, + -1.1503292267326826, + -1.1110593185628101, + -1.0880555389500448, + -1.0487856307801726, + -0.993249594053193, + -0.9539796858833206, + -0.9309759062705554, + -0.891705998100683, + -0.8361699613737034, + -0.796900053203831, + -0.7738962735910657, + -0.7346263654211933, + -0.6790903286942136, + -0.6398204205243412, + -0.6168166409115761, + -0.5775467327417037, + -0.522010696014724, + -0.48274078784485164, + -0.45973700823208635, + -0.42046710006221394, + -0.36493106333523434, + -0.3256611551653619, + -0.3026573755525967, + -0.2633874673827243, + -0.20785143065574468, + -0.16858152248587227, + -0.14557774287310704, + -0.10630783470323461, + -0.05077179797625504, + -0.011501889806382629, + 0.011501889806382629, + 0.05077179797625504, + 0.10630783470323461, + 0.14557774287310704, + 0.16858152248587227, + 0.20785143065574468, + 0.2633874673827243, + 0.3026573755525967, + 0.3256611551653619, + 0.36493106333523434, + 0.42046710006221394, + 0.45973700823208635, + 0.48274078784485164, + 0.522010696014724, + 0.5775467327417037, + 0.6168166409115761, + 0.6398204205243412, + 0.6790903286942136, + 0.7346263654211933, + 0.7738962735910657, + 0.796900053203831, + 0.8361699613737034, + 0.891705998100683, + 0.9309759062705554, + 0.9539796858833206, + 0.993249594053193, + 1.0487856307801726, + 1.0880555389500448, + 1.1110593185628101, + 1.1503292267326826, + 1.2058652634596623, + 1.2451351716295345, + 1.2681389512423, + 1.3074088594121722, + 1.362944896139152, + 1.4022148043090241, + 1.4252185839217895, + 1.4644884920916619, + 1.5200245288186416, + 1.559294436988514, + 1.5822982166012793, + 1.6215681247711515, + 1.6771041614981312, + 1.7163740696680034, + 1.7393778492807688, + 1.7786477574506412, + 1.834183794177621, + 1.8734537023474933, + 1.8964574819602587, + 1.9357273901301308, + 1.9912634268571106, + 2.030533335026983, + 2.053537114639748, + 2.0928070228096205, + 2.1483430595366, + 2.1876129677064724, + 2.2106167473192375, + 2.24988665548911, + 2.30542269221609, + 2.344692600385962, + 2.367696379998727, + 2.4069662881686, + 2.4625023248955795, + 2.501772233065452, + 2.524776012678217, + 2.5640459208480895, + 2.619581957575069, + 2.658851865744942, + 2.681855645357707, + 2.721125553527579, + 2.776661590254559, + 2.815931498424431, + 2.838935278037196, + 2.8782051862070688, + 2.9337412229340485, + 2.973011131103921, + 2.996014910716686, + 3.0352848188865584, + 3.090820855613538, + 3.130090763783411 + ] + }, + "VSM rib N=80": { + "CL": 0.8151570220744624, + "CDi_blade": 0.02626780208432228, + "CDi_trefftz": 0.025870696445383114, + "converged": true, + "gamma": [ + 0.4666352210613352, + 0.8934069522129371, + 1.2052564406377104, + 1.4594977310178647, + 1.6776887226297639, + 1.8706660524008085, + 2.044582138508643, + 2.2033412991426897, + 2.3496018207301224, + 2.4852660846286256, + 2.611747110448127, + 2.7301250507287946, + 2.841244628416512, + 2.9457787022361996, + 3.0442713457871426, + 3.1371679855506165, + 3.224837061392347, + 3.3075859593325085, + 3.3856729700523696, + 3.459316425062269, + 3.528701787153686, + 3.593987230795058, + 3.6553080894517853, + 3.7127804399341793, + 3.766504020420971, + 3.8165646273895586, + 3.863036100092087, + 3.9059819747762927, + 3.945456871475322, + 3.9815076618072776, + 4.014174455415186, + 4.043491434458829, + 4.069487559250834, + 4.092187163218693, + 4.1116104515069365, + 4.127773914380517, + 4.140690663628921, + 4.150370695879524, + 4.156821075233335, + 4.160045977328302, + 4.160045977328301, + 4.156821075233337, + 4.150370695879526, + 4.140690663628923, + 4.127773914380518, + 4.1116104515069365, + 4.092187163218694, + 4.069487559250833, + 4.043491434458829, + 4.014174455415187, + 3.9815076618072784, + 3.9454568714753218, + 3.9059819747762914, + 3.8630361000920868, + 3.8165646273895586, + 3.766504020420971, + 3.7127804399341775, + 3.6553080894517853, + 3.593987230795056, + 3.5287017871536843, + 3.4593164250622674, + 3.3856729700523687, + 3.3075859593325094, + 3.2248370613923463, + 3.137167985550615, + 3.0442713457871435, + 2.9457787022361983, + 2.8412446284165127, + 2.7301250507287955, + 2.611747110448128, + 2.485266084628626, + 2.3496018207301237, + 2.203341299142694, + 2.044582138508647, + 1.87066605240081, + 1.6776887226297643, + 1.4594977310178638, + 1.2052564406377086, + 0.893406952212934, + 0.4666352210613303 + ], + "y": [ + 3.107972239995647, + 3.0383962575911188, + 2.9651691419850827, + 2.890858926823096, + 2.8162158576487144, + 2.74125494878609, + 2.6659878654815197, + 2.59041774371852, + 2.5145454490663033, + 2.4383715653484197, + 2.361897084144449, + 2.2851236469355145, + 2.208053620275291, + 2.1306901064382107, + 2.0530369296116646, + 1.9750986138491746, + 1.8968803593126329, + 1.8183880192425654, + 1.739628078344844, + 1.6606076325467725, + 1.5813343697908069, + 1.5018165514507342, + 1.4220629939598053, + 1.3420830502810617, + 1.2618865909031585, + 1.1814839840997502, + 1.1008860752425518, + 1.0201041650059917, + 0.9391499863446778, + 0.8580356801639935, + 0.7767737696394339, + 0.6953771331722398, + 0.6138589759978346, + 0.5322328004898165, + 0.45051237522596566, + 0.36871170290404504, + 0.28684498721413626, + 0.2049265987908857, + 0.12297104038331919, + 0.04099289434316754, + -0.04099289434316665, + -0.12297104038331834, + -0.20492659879088462, + -0.28684498721413487, + -0.36871170290404376, + -0.4505123752259643, + -0.5322328004898151, + -0.6138589759978332, + -0.6953771331722385, + -0.7767737696394329, + -0.8580356801639927, + -0.9391499863446773, + -1.020104165005991, + -1.1008860752425509, + -1.1814839840997493, + -1.261886590903158, + -1.3420830502810617, + -1.4220629939598053, + -1.5018165514507347, + -1.5813343697908078, + -1.6606076325467733, + -1.7396280783448448, + -1.8183880192425663, + -1.8968803593126333, + -1.975098613849175, + -2.053036929611665, + -2.1306901064382116, + -2.208053620275291, + -2.2851236469355145, + -2.3618970841444487, + -2.4383715653484197, + -2.5145454490663033, + -2.59041774371852, + -2.6659878654815197, + -2.741254948786091, + -2.8162158576487153, + -2.890858926823097, + -2.9651691419850836, + -3.0383962575911196, + -3.107972239995648 + ] + }, + "VSM rib N=160": { + "CL": 0.814536072602332, + "CDi_blade": 0.0263968471917782, + "CDi_trefftz": 0.025967196805312613, + "converged": true, + "gamma": [ + 0.3024524512550188, + 0.5779092408140526, + 0.7917663715871995, + 0.9714957229409088, + 1.1278288313950984, + 1.2676628452143313, + 1.3950096873383826, + 1.5124605964535949, + 1.6218042805680675, + 1.7243367353947645, + 1.8210323315025452, + 1.9126453031634525, + 1.999773418348942, + 2.0828998107094945, + 2.1624220435991552, + 2.238668842186757, + 2.3119117070689073, + 2.382394002220313, + 2.4503209194114546, + 2.5158694092496283, + 2.5791934916875956, + 2.6404281773873013, + 2.699692524000721, + 2.757092073009543, + 2.8127208213625403, + 2.866662834987069, + 2.9189935819699664, + 2.9697810434430707, + 3.0190866462710813, + 3.0669660515330213, + 3.113469825313615, + 3.158644012705687, + 3.202530631651573, + 3.245168099963382, + 3.2865916063058713, + 3.3268334339215144, + 3.3659232442923472, + 3.4038883266707303, + 3.440753818398316, + 3.476542900115009, + 3.5112769692959724, + 3.5449757950131016, + 3.577657656373925, + 3.6093394667266243, + 3.6400368854215075, + 3.6697644186764475, + 3.698535510901953, + 3.7263626277011106, + 3.7532573316850115, + 3.779230352280361, + 3.804291650989061, + 3.8284504845450082, + 3.8517154719468545, + 3.8740946881206213, + 3.895595983066648, + 3.9162267526429813, + 3.9359891503293976, + 3.954890687626472, + 3.972937160776536, + 3.9901337617307804, + 4.006485232299018, + 4.021995924364897, + 4.03666983626647, + 4.050510639894635, + 4.063521702992571, + 4.07570610840855, + 4.087066671114135, + 4.097605953418494, + 4.107326278634348, + 4.116229743361755, + 4.124318228504976, + 4.131593409104083, + 4.138056763035268, + 4.143709578603272, + 4.148552961004036, + 4.152587837554475, + 4.155814961415158, + 4.158234913108887, + 4.159848097822136, + 4.160654730474237, + 4.160654730474236, + 4.159848097822137, + 4.158234913108887, + 4.155814961415156, + 4.152587837554477, + 4.148552961004035, + 4.143709578603273, + 4.138056763035268, + 4.131593409104081, + 4.124318228504976, + 4.116229743361753, + 4.107326278634347, + 4.097605953418494, + 4.087066671114133, + 4.075706108408546, + 4.06352170299257, + 4.050510639894632, + 4.03666983626647, + 4.021995924364895, + 4.006485232299018, + 3.990133761730779, + 3.9729371607765347, + 3.954890687626473, + 3.9359891503293953, + 3.916226752642979, + 3.895595983066648, + 3.8740946881206213, + 3.8517154719468536, + 3.828450484545008, + 3.8042916509890605, + 3.7792303522803614, + 3.753257331685012, + 3.7263626277011115, + 3.6985355109019546, + 3.6697644186764475, + 3.640036885421509, + 3.609339466726627, + 3.5776576563739226, + 3.544975795013101, + 3.5112769692959693, + 3.4765429001150063, + 3.4407538183983117, + 3.403888326670727, + 3.3659232442923446, + 3.3268334339215113, + 3.286591606305869, + 3.245168099963378, + 3.20253063165157, + 3.158644012705684, + 3.113469825313614, + 3.0669660515330164, + 3.019086646271076, + 2.969781043443066, + 2.9189935819699606, + 2.8666628349870655, + 2.8127208213625408, + 2.7570920730095447, + 2.699692524000722, + 2.640428177387302, + 2.5791934916875956, + 2.5158694092496283, + 2.4503209194114564, + 2.3823940022203116, + 2.311911707068904, + 2.2386688421867564, + 2.1624220435991557, + 2.0828998107094945, + 1.9997734183489433, + 1.9126453031634525, + 1.8210323315025443, + 1.724336735394765, + 1.6218042805680695, + 1.512460596453594, + 1.3950096873383866, + 1.2676628452143326, + 1.1278288313950993, + 0.97149572294091, + 0.791766371587201, + 0.5779092408140556, + 0.3024524512550273 + ], + "y": [ + 3.1264850123695305, + 3.094566760975378, + 3.0592620806116706, + 3.0225577652352547, + 2.9856397604987057, + 2.9485779431831904, + 2.911409049164429, + 2.874148937499747, + 2.8368051883284884, + 2.7993816397778417, + 2.7618802881465987, + 2.724302174006824, + 2.686647828753968, + 2.648917512537588, + 2.6111109581849057, + 2.573227227442428, + 2.53526684809234, + 2.4972309672769857, + 2.4591196658961723, + 2.420933062202751, + 2.3826713155998247, + 2.344334629557331, + 2.305923253109393, + 2.2674374814502336, + 2.228877655962449, + 2.1902441638940005, + 2.1515374378244463, + 2.1127579550115563, + 2.07390623667696, + 2.034982847268055, + 1.9959883937191691, + 1.9569235247255121, + 1.9177889300372122, + 1.8785853397765815, + 1.8393135237790923, + 1.799974290956816, + 1.7605684886820354, + 1.7210970021881171, + 1.6815607539844353, + 1.6419607032820105, + 1.6022978454265595, + 1.562573211335749, + 1.5227878669376118, + 1.4829429126072626, + 1.4430394825992687, + 1.4030787444732324, + 1.3630618985103649, + 1.3229901771190324, + 1.2828648442274657, + 1.2426871946620137, + 1.202458553509514, + 1.1621802754625281, + 1.1218537441463603, + 1.081480371204468, + 1.041061503288891, + 1.0005980945750068, + 0.9600917567174208, + 0.9195445485830858, + 0.8789580181211758, + 0.838333737904974, + 0.7976733030994174, + 0.7569783305856242, + 0.7162504580645401, + 0.6754913431400631, + 0.6347026623820962, + 0.5938861103700683, + 0.5530433987175519, + 0.512176255078681, + 0.47128642213715827, + 0.43037565657870225, + 0.3894457280478578, + 0.348498418090154, + 0.3075355190806509, + 0.2665588331399692, + 0.22557017103894844, + 0.18457135109311534, + 0.14356419804818946, + 0.10255054195788425, + 0.06153221705529026, + 0.02051105845298022, + -0.02051105845298199, + -0.061532217055292035, + -0.10255054195788603, + -0.14356419804819123, + -0.18457135109311712, + -0.22557017103895022, + -0.266558833139971, + -0.30753551908065263, + -0.3484984180901558, + -0.3894457280478596, + -0.430375656578704, + -0.47128642213716004, + -0.5121762550786828, + -0.5530433987175537, + -0.5938861103700701, + -0.6347026623820979, + -0.6754913431400649, + -0.716250458064542, + -0.7569783305856266, + -0.7976733030994199, + -0.838333737904976, + -0.8789580181211777, + -0.9195445485830875, + -0.9600917567174224, + -1.000598094575009, + -1.0410615032888937, + -1.0814803712044703, + -1.1218537441463625, + -1.16218027546253, + -1.202458553509516, + -1.2426871946620155, + -1.282864844227467, + -1.322990177119033, + -1.3630618985103653, + -1.403078744473233, + -1.4430394825992696, + -1.4829429126072635, + -1.5227878669376127, + -1.56257321133575, + -1.6022978454265608, + -1.641960703282012, + -1.6815607539844368, + -1.7210970021881182, + -1.7605684886820359, + -1.799974290956817, + -1.8393135237790936, + -1.878585339776583, + -1.9177889300372137, + -1.956923524725513, + -1.9959883937191698, + -2.034982847268056, + -2.0739062366769607, + -2.112757955011557, + -2.1515374378244467, + -2.1902441638940005, + -2.2288776559624486, + -2.2674374814502327, + -2.3059232531093925, + -2.3443346295573306, + -2.3826713155998247, + -2.4209330622027507, + -2.459119665896172, + -2.4972309672769857, + -2.5352668480923404, + -2.5732272274424277, + -2.611110958184905, + -2.6489175125375874, + -2.6866478287539675, + -2.724302174006824, + -2.7618802881465987, + -2.7993816397778417, + -2.836805188328488, + -2.874148937499747, + -2.911409049164428, + -2.9485779431831904, + -2.9856397604987057, + -3.0225577652352547, + -3.0592620806116706, + -3.094566760975378, + -3.12648501236953 + ] + }, + "VSM streamwise N=80": { + "CL": 0.8082026699779978, + "CDi_blade": 0.025126665623908185, + "CDi_trefftz": 0.025478410597651293, + "converged": true, + "gamma": [ + 0.4960320161851049, + 0.915021691591708, + 1.2216781789417506, + 1.472417776807739, + 1.6880422509596316, + 1.879016866954844, + 2.051298927662926, + 2.20867967540545, + 2.353746868075805, + 2.488356420956506, + 2.6138894472469856, + 2.7314034893610546, + 2.8417268610705984, + 2.9455203009441515, + 3.043318821016366, + 3.135561024786006, + 3.2226102043722338, + 3.3047698756999124, + 3.3822954494848876, + 3.455403154742449, + 3.524276968585401, + 3.5890740727766692, + 3.649929203705461, + 3.706958158767403, + 3.760260650799861, + 3.8099226522541603, + 3.8560183351966053, + 3.898611687495205, + 3.9377578666776856, + 3.9735043389260962, + 4.005891840127401, + 4.034955187871733, + 4.06072396710648, + 4.083223107329532, + 4.102473365364964, + 4.1184917245345485, + 4.1312917177241015, + 4.1408836761478325, + 4.147274886023437, + 4.150469449248998, + 4.150469449248998, + 4.147274886023437, + 4.140883676147833, + 4.131291717724102, + 4.118491724534551, + 4.102473365364966, + 4.083223107329534, + 4.060723967106481, + 4.034955187871734, + 4.0058918401274015, + 3.9735043389260976, + 3.937757866677686, + 3.8986116874952064, + 3.856018335196606, + 3.809922652254161, + 3.7602606507998613, + 3.706958158767403, + 3.6499292037054625, + 3.589074072776671, + 3.524276968585401, + 3.455403154742449, + 3.3822954494848876, + 3.304769875699912, + 3.222610204372233, + 3.1355610247860057, + 3.043318821016365, + 2.9455203009441493, + 2.841726861070597, + 2.731403489361054, + 2.613889447246986, + 2.488356420956507, + 2.3537468680758087, + 2.2086796754054543, + 2.0512989276629296, + 1.8790168669548475, + 1.6880422509596333, + 1.4724177768077389, + 1.2216781789417461, + 0.9150216915917059, + 0.4960320161851043 + ], + "y": [ + 3.107972239995647, + 3.0383962575911188, + 2.9651691419850827, + 2.890858926823096, + 2.8162158576487144, + 2.74125494878609, + 2.6659878654815197, + 2.59041774371852, + 2.5145454490663033, + 2.4383715653484197, + 2.361897084144449, + 2.2851236469355145, + 2.208053620275291, + 2.1306901064382107, + 2.0530369296116646, + 1.9750986138491746, + 1.8968803593126329, + 1.8183880192425654, + 1.739628078344844, + 1.6606076325467725, + 1.5813343697908069, + 1.5018165514507342, + 1.4220629939598053, + 1.3420830502810617, + 1.2618865909031585, + 1.1814839840997502, + 1.1008860752425518, + 1.0201041650059917, + 0.9391499863446778, + 0.8580356801639935, + 0.7767737696394339, + 0.6953771331722398, + 0.6138589759978346, + 0.5322328004898165, + 0.45051237522596566, + 0.36871170290404504, + 0.28684498721413626, + 0.2049265987908857, + 0.12297104038331919, + 0.04099289434316754, + -0.04099289434316665, + -0.12297104038331834, + -0.20492659879088462, + -0.28684498721413487, + -0.36871170290404376, + -0.4505123752259643, + -0.5322328004898151, + -0.6138589759978332, + -0.6953771331722385, + -0.7767737696394329, + -0.8580356801639927, + -0.9391499863446773, + -1.020104165005991, + -1.1008860752425509, + -1.1814839840997493, + -1.261886590903158, + -1.3420830502810617, + -1.4220629939598053, + -1.5018165514507347, + -1.5813343697908078, + -1.6606076325467733, + -1.7396280783448448, + -1.8183880192425663, + -1.8968803593126333, + -1.975098613849175, + -2.053036929611665, + -2.1306901064382116, + -2.208053620275291, + -2.2851236469355145, + -2.3618970841444487, + -2.4383715653484197, + -2.5145454490663033, + -2.59041774371852, + -2.6659878654815197, + -2.741254948786091, + -2.8162158576487153, + -2.890858926823097, + -2.9651691419850836, + -3.0383962575911196, + -3.107972239995648 + ] + }, + "VSM streamwise N=160": { + "CL": 0.8075322908895035, + "CDi_blade": 0.02526980395907066, + "CDi_trefftz": 0.02556201108960614, + "converged": true, + "gamma": [ + 0.3409496809352472, + 0.6082366413822512, + 0.8164336717616485, + 0.9922239429909501, + 1.1456634547669111, + 1.2832461225591685, + 1.408767181122689, + 1.5246917153286745, + 1.6327293837919177, + 1.734123697616134, + 1.829812553277977, + 1.920523881797601, + 2.0068358443469694, + 2.0892158322204897, + 2.168048161039164, + 2.2436547733156598, + 2.3163040770397147, + 2.3862320124759555, + 2.453636916411877, + 2.51869154977223, + 2.5815465368261057, + 2.6423340501086443, + 2.7011707485655183, + 2.75816013241382, + 2.8133944525896992, + 2.8669562754728646, + 2.918919777105305, + 2.9693518225760496, + 3.018312872989045, + 3.065857752764854, + 3.112036302856472, + 3.156893940063162, + 3.2004721385142885, + 3.2428088462273976, + 3.283938847180338, + 3.3238940774030223, + 3.362703902063779, + 3.400395359305067, + 3.4369933756038336, + 3.4725209566405826, + 3.5069993570185, + 3.5404482316490307, + 3.5728857711901423, + 3.6043288235696442, + 3.6347930033350013, + 3.6642927903327336, + 3.6928416190274693, + 3.7204519596189933, + 3.747135392003105, + 3.772902673542116, + 3.7977638015017536, + 3.821728070355583, + 3.844804117616009, + 3.8669998023806005, + 3.8883220382623356, + 3.9087789515286335, + 3.928374789425393, + 3.947116888872817, + 3.9650101761148777, + 3.982059847204629, + 3.9982707105231428, + 4.013647196859324, + 4.028193386420297, + 4.041913033036406, + 4.054809585157412, + 4.066886204310037, + 4.078145781520065, + 4.088590952025394, + 4.0982241084958515, + 4.107047412908803, + 4.115062807186883, + 4.122272022673062, + 4.128676588489827, + 4.134277838794535, + 4.139076918888161, + 4.143074790027399, + 4.146272232540989, + 4.148669846114691, + 4.150268042844549, + 4.151066988220612, + 4.151066988220612, + 4.150268042844551, + 4.14866984611469, + 4.146272232540986, + 4.143074790027397, + 4.1390769188881595, + 4.134277838794535, + 4.128676588489824, + 4.122272022673063, + 4.115062807186883, + 4.107047412908804, + 4.098224108495851, + 4.088590952025393, + 4.078145781520063, + 4.066886204310036, + 4.05480958515741, + 4.041913033036406, + 4.028193386420296, + 4.013647196859321, + 3.9982707105231405, + 3.9820598472046265, + 3.9650101761148773, + 3.9471168888728143, + 3.9283747894253906, + 3.9087789515286286, + 3.888322038262333, + 3.8669998023805983, + 3.8448041176160053, + 3.82172807035558, + 3.797763801501751, + 3.7729026735421134, + 3.7471353920031003, + 3.7204519596189893, + 3.692841619027467, + 3.66429279033273, + 3.6347930033349987, + 3.6043288235696425, + 3.572885771190139, + 3.5404482316490244, + 3.506999357018494, + 3.472520956640577, + 3.436993375603827, + 3.4003953593050626, + 3.362703902063775, + 3.3238940774030183, + 3.283938847180335, + 3.242808846227393, + 3.2004721385142827, + 3.156893940063157, + 3.1120363028564704, + 3.06585775276485, + 3.0183128729890387, + 2.969351822576043, + 2.918919777105299, + 2.866956275472863, + 2.8133944525896974, + 2.75816013241382, + 2.701170748565519, + 2.642334050108644, + 2.5815465368261026, + 2.518691549772228, + 2.453636916411876, + 2.3862320124759537, + 2.316304077039713, + 2.2436547733156598, + 2.1680481610391644, + 2.0892158322204915, + 2.0068358443469725, + 1.9205238817976054, + 1.8298125532779799, + 1.7341236976161416, + 1.632729383791925, + 1.5246917153286805, + 1.4087671811227018, + 1.2832461225591776, + 1.1456634547669169, + 0.9922239429909547, + 0.816433671761653, + 0.6082366413822571, + 0.3409496809352577 + ], + "y": [ + 3.1264850123695305, + 3.094566760975378, + 3.0592620806116706, + 3.0225577652352547, + 2.9856397604987057, + 2.9485779431831904, + 2.911409049164429, + 2.874148937499747, + 2.8368051883284884, + 2.7993816397778417, + 2.7618802881465987, + 2.724302174006824, + 2.686647828753968, + 2.648917512537588, + 2.6111109581849057, + 2.573227227442428, + 2.53526684809234, + 2.4972309672769857, + 2.4591196658961723, + 2.420933062202751, + 2.3826713155998247, + 2.344334629557331, + 2.305923253109393, + 2.2674374814502336, + 2.228877655962449, + 2.1902441638940005, + 2.1515374378244463, + 2.1127579550115563, + 2.07390623667696, + 2.034982847268055, + 1.9959883937191691, + 1.9569235247255121, + 1.9177889300372122, + 1.8785853397765815, + 1.8393135237790923, + 1.799974290956816, + 1.7605684886820354, + 1.7210970021881171, + 1.6815607539844353, + 1.6419607032820105, + 1.6022978454265595, + 1.562573211335749, + 1.5227878669376118, + 1.4829429126072626, + 1.4430394825992687, + 1.4030787444732324, + 1.3630618985103649, + 1.3229901771190324, + 1.2828648442274657, + 1.2426871946620137, + 1.202458553509514, + 1.1621802754625281, + 1.1218537441463603, + 1.081480371204468, + 1.041061503288891, + 1.0005980945750068, + 0.9600917567174208, + 0.9195445485830858, + 0.8789580181211758, + 0.838333737904974, + 0.7976733030994174, + 0.7569783305856242, + 0.7162504580645401, + 0.6754913431400631, + 0.6347026623820962, + 0.5938861103700683, + 0.5530433987175519, + 0.512176255078681, + 0.47128642213715827, + 0.43037565657870225, + 0.3894457280478578, + 0.348498418090154, + 0.3075355190806509, + 0.2665588331399692, + 0.22557017103894844, + 0.18457135109311534, + 0.14356419804818946, + 0.10255054195788425, + 0.06153221705529026, + 0.02051105845298022, + -0.02051105845298199, + -0.061532217055292035, + -0.10255054195788603, + -0.14356419804819123, + -0.18457135109311712, + -0.22557017103895022, + -0.266558833139971, + -0.30753551908065263, + -0.3484984180901558, + -0.3894457280478596, + -0.430375656578704, + -0.47128642213716004, + -0.5121762550786828, + -0.5530433987175537, + -0.5938861103700701, + -0.6347026623820979, + -0.6754913431400649, + -0.716250458064542, + -0.7569783305856266, + -0.7976733030994199, + -0.838333737904976, + -0.8789580181211777, + -0.9195445485830875, + -0.9600917567174224, + -1.000598094575009, + -1.0410615032888937, + -1.0814803712044703, + -1.1218537441463625, + -1.16218027546253, + -1.202458553509516, + -1.2426871946620155, + -1.282864844227467, + -1.322990177119033, + -1.3630618985103653, + -1.403078744473233, + -1.4430394825992696, + -1.4829429126072635, + -1.5227878669376127, + -1.56257321133575, + -1.6022978454265608, + -1.641960703282012, + -1.6815607539844368, + -1.7210970021881182, + -1.7605684886820359, + -1.799974290956817, + -1.8393135237790936, + -1.878585339776583, + -1.9177889300372137, + -1.956923524725513, + -1.9959883937191698, + -2.034982847268056, + -2.0739062366769607, + -2.112757955011557, + -2.1515374378244467, + -2.1902441638940005, + -2.2288776559624486, + -2.2674374814502327, + -2.3059232531093925, + -2.3443346295573306, + -2.3826713155998247, + -2.4209330622027507, + -2.459119665896172, + -2.4972309672769857, + -2.5352668480923404, + -2.5732272274424277, + -2.611110958184905, + -2.6489175125375874, + -2.6866478287539675, + -2.724302174006824, + -2.7618802881465987, + -2.7993816397778417, + -2.836805188328488, + -2.874148937499747, + -2.911409049164428, + -2.9485779431831904, + -2.9856397604987057, + -3.0225577652352547, + -3.0592620806116706, + -3.094566760975378, + -3.12648501236953 + ] + }, + "VSM perpendicular N=80": { + "CL": 0.813652870510948, + "CDi_blade": 0.026314451869388376, + "CDi_trefftz": 0.0257084055919045, + "converged": true, + "gamma": [ + 0.40619879930857367, + 0.8618242238267952, + 1.1907125029828516, + 1.456492532467622, + 1.6857341660788785, + 1.8864899877474222, + 2.0639838641473687, + 2.22347689136698, + 2.3692252222902286, + 2.5040220455300735, + 2.6293931503415604, + 2.74652712846992, + 2.8563637108888273, + 2.9596132812001636, + 3.056830922395899, + 3.1484622979978476, + 3.234889660885642, + 3.316435023179362, + 3.3933580190438017, + 3.465877901443463, + 3.5341810423116926, + 3.5984252132012586, + 3.65874732860512, + 3.715266744932981, + 3.7680849225001407, + 3.817288651334398, + 3.862952836968745, + 3.9051421686874517, + 3.943912476487664, + 3.9793118638988694, + 4.011381646245642, + 4.040156862955613, + 4.0656678167544635, + 4.08794026321096, + 4.106995509647667, + 4.122850985991012, + 4.135520532708095, + 4.145014719206765, + 4.151341031800064, + 4.154503898617142, + 4.1545038986171425, + 4.151341031800064, + 4.145014719206765, + 4.135520532708095, + 4.122850985991012, + 4.106995509647666, + 4.0879402632109585, + 4.065667816754464, + 4.040156862955615, + 4.011381646245645, + 3.9793118638988694, + 3.943912476487665, + 3.905142168687452, + 3.862952836968746, + 3.8172886513343984, + 3.768084922500141, + 3.7152667449329804, + 3.658747328605118, + 3.598425213201258, + 3.5341810423116917, + 3.465877901443461, + 3.3933580190438013, + 3.3164350231793596, + 3.2348896608856412, + 3.148462297997846, + 3.056830922395899, + 2.9596132812001623, + 2.856363710888829, + 2.7465271284699213, + 2.629393150341565, + 2.504022045530074, + 2.36922522229023, + 2.223476891366982, + 2.063983864147369, + 1.8864899877474213, + 1.6857341660788745, + 1.4564925324676157, + 1.190712502982847, + 0.86182422382679, + 0.40619879930856556 + ], + "y": [ + 3.107972239995647, + 3.0383962575911188, + 2.9651691419850827, + 2.890858926823096, + 2.8162158576487144, + 2.74125494878609, + 2.6659878654815197, + 2.59041774371852, + 2.5145454490663033, + 2.4383715653484197, + 2.361897084144449, + 2.2851236469355145, + 2.208053620275291, + 2.1306901064382107, + 2.0530369296116646, + 1.9750986138491746, + 1.8968803593126329, + 1.8183880192425654, + 1.739628078344844, + 1.6606076325467725, + 1.5813343697908069, + 1.5018165514507342, + 1.4220629939598053, + 1.3420830502810617, + 1.2618865909031585, + 1.1814839840997502, + 1.1008860752425518, + 1.0201041650059917, + 0.9391499863446778, + 0.8580356801639935, + 0.7767737696394339, + 0.6953771331722398, + 0.6138589759978346, + 0.5322328004898165, + 0.45051237522596566, + 0.36871170290404504, + 0.28684498721413626, + 0.2049265987908857, + 0.12297104038331919, + 0.04099289434316754, + -0.04099289434316665, + -0.12297104038331834, + -0.20492659879088462, + -0.28684498721413487, + -0.36871170290404376, + -0.4505123752259643, + -0.5322328004898151, + -0.6138589759978332, + -0.6953771331722385, + -0.7767737696394329, + -0.8580356801639927, + -0.9391499863446773, + -1.020104165005991, + -1.1008860752425509, + -1.1814839840997493, + -1.261886590903158, + -1.3420830502810617, + -1.4220629939598053, + -1.5018165514507347, + -1.5813343697908078, + -1.6606076325467733, + -1.7396280783448448, + -1.8183880192425663, + -1.8968803593126333, + -1.975098613849175, + -2.053036929611665, + -2.1306901064382116, + -2.208053620275291, + -2.2851236469355145, + -2.3618970841444487, + -2.4383715653484197, + -2.5145454490663033, + -2.59041774371852, + -2.6659878654815197, + -2.741254948786091, + -2.8162158576487153, + -2.890858926823097, + -2.9651691419850836, + -3.0383962575911196, + -3.107972239995648 + ] + }, + "VSM perpendicular N=160": { + "CL": 0.8131214542386911, + "CDi_blade": 0.026343574855659253, + "CDi_trefftz": 0.025828054837326836, + "converged": true, + "gamma": [ + 0.21350689962639738, + 0.5069109032715534, + 0.7457279930087714, + 0.9401240219917908, + 1.1067395672785993, + 1.2538476656687734, + 1.3867542270228146, + 1.5109852903199155, + 1.62856163239218, + 1.7375068146239625, + 1.8382558414287489, + 1.9320687146759787, + 2.0201656823173955, + 2.1037754213504143, + 2.1833924079669926, + 2.2594810754262644, + 2.332438030740559, + 2.4025125372684366, + 2.469940489159681, + 2.534942399450894, + 2.597685357164721, + 2.6583022562650642, + 2.7169253914934264, + 2.773676854507978, + 2.8286530630943543, + 2.8819353008228843, + 2.933602652437817, + 2.9837305648749908, + 3.0323820712306353, + 3.0796131910349747, + 3.1254726587483876, + 3.1700083325442203, + 3.2132657063950973, + 3.255283529279615, + 3.2960970682331188, + 3.335738293405823, + 3.374236155716185, + 3.4116188165891304, + 3.4479126239924742, + 3.4831407890122774, + 3.5173247297478265, + 3.5504843580765972, + 3.5826379058261875, + 3.613802031560206, + 3.643992839891346, + 3.6732254386108827, + 3.701513407486149, + 3.7288693244026208, + 3.75530486881064, + 3.7808308944867197, + 3.805457491003166, + 3.8291940045632504, + 3.852049058506106, + 3.874030984801652, + 3.895147971383124, + 3.9154075939486375, + 3.9348121416894766, + 3.953369282957926, + 3.9710849674130086, + 3.987964539632578, + 4.004012891951395, + 4.0192345192842, + 4.033633485794655, + 4.047213612828269, + 4.059978499727961, + 4.071931394919548, + 4.083075241585113, + 4.093412722029965, + 4.102946293099698, + 4.1116781752947835, + 4.119610352541952, + 4.126744594592988, + 4.133082469531833, + 4.138625356658411, + 4.143374417849693, + 4.147330618883584, + 4.150494755147101, + 4.152867443137245, + 4.15444911330829, + 4.15523999321515, + 4.155239993215151, + 4.154449113308292, + 4.152867443137246, + 4.1504947551471005, + 4.147330618883585, + 4.143374417849696, + 4.138625356658412, + 4.133082469531836, + 4.126744594592989, + 4.119610352541952, + 4.111678175294785, + 4.102946293099697, + 4.0934127220299645, + 4.083075241585111, + 4.071931394919547, + 4.059978499727959, + 4.047213612828265, + 4.033633485794654, + 4.019234519284193, + 4.004012891951392, + 3.987964539632578, + 3.9710849674130064, + 3.9533692829579232, + 3.934812141689475, + 3.9154075939486375, + 3.8951479713831225, + 3.8740309848016503, + 3.8520490585061022, + 3.829194004563247, + 3.8054574910031636, + 3.780830894486718, + 3.755304868810643, + 3.728869324402623, + 3.701513407486152, + 3.673225438610886, + 3.643992839891346, + 3.6138020315602053, + 3.5826379058261875, + 3.5504843580765955, + 3.5173247297478234, + 3.4831407890122756, + 3.4479126239924716, + 3.4116188165891304, + 3.3742361557161855, + 3.3357382934058224, + 3.2960970682331165, + 3.2552835292796116, + 3.2132657063950925, + 3.1700083325442145, + 3.1254726587483836, + 3.0796131910349716, + 3.0323820712306295, + 2.9837305648749846, + 2.93360265243781, + 2.8819353008228816, + 2.828653063094353, + 2.7736768545079777, + 2.7169253914934264, + 2.6583022562650647, + 2.597685357164721, + 2.534942399450896, + 2.4699404891596832, + 2.402512537268437, + 2.3324380307405566, + 2.2594810754262644, + 2.1833924079669944, + 2.1037754213504174, + 2.0201656823173986, + 1.9320687146759792, + 1.8382558414287486, + 1.7375068146239587, + 1.6285616323921792, + 1.510985290319914, + 1.3867542270228173, + 1.2538476656687725, + 1.1067395672785991, + 0.9401240219917931, + 0.7457279930087781, + 0.5069109032715579, + 0.21350689962640593 + ], + "y": [ + 3.1264850123695305, + 3.094566760975378, + 3.0592620806116706, + 3.0225577652352547, + 2.9856397604987057, + 2.9485779431831904, + 2.911409049164429, + 2.874148937499747, + 2.8368051883284884, + 2.7993816397778417, + 2.7618802881465987, + 2.724302174006824, + 2.686647828753968, + 2.648917512537588, + 2.6111109581849057, + 2.573227227442428, + 2.53526684809234, + 2.4972309672769857, + 2.4591196658961723, + 2.420933062202751, + 2.3826713155998247, + 2.344334629557331, + 2.305923253109393, + 2.2674374814502336, + 2.228877655962449, + 2.1902441638940005, + 2.1515374378244463, + 2.1127579550115563, + 2.07390623667696, + 2.034982847268055, + 1.9959883937191691, + 1.9569235247255121, + 1.9177889300372122, + 1.8785853397765815, + 1.8393135237790923, + 1.799974290956816, + 1.7605684886820354, + 1.7210970021881171, + 1.6815607539844353, + 1.6419607032820105, + 1.6022978454265595, + 1.562573211335749, + 1.5227878669376118, + 1.4829429126072626, + 1.4430394825992687, + 1.4030787444732324, + 1.3630618985103649, + 1.3229901771190324, + 1.2828648442274657, + 1.2426871946620137, + 1.202458553509514, + 1.1621802754625281, + 1.1218537441463603, + 1.081480371204468, + 1.041061503288891, + 1.0005980945750068, + 0.9600917567174208, + 0.9195445485830858, + 0.8789580181211758, + 0.838333737904974, + 0.7976733030994174, + 0.7569783305856242, + 0.7162504580645401, + 0.6754913431400631, + 0.6347026623820962, + 0.5938861103700683, + 0.5530433987175519, + 0.512176255078681, + 0.47128642213715827, + 0.43037565657870225, + 0.3894457280478578, + 0.348498418090154, + 0.3075355190806509, + 0.2665588331399692, + 0.22557017103894844, + 0.18457135109311534, + 0.14356419804818946, + 0.10255054195788425, + 0.06153221705529026, + 0.02051105845298022, + -0.02051105845298199, + -0.061532217055292035, + -0.10255054195788603, + -0.14356419804819123, + -0.18457135109311712, + -0.22557017103895022, + -0.266558833139971, + -0.30753551908065263, + -0.3484984180901558, + -0.3894457280478596, + -0.430375656578704, + -0.47128642213716004, + -0.5121762550786828, + -0.5530433987175537, + -0.5938861103700701, + -0.6347026623820979, + -0.6754913431400649, + -0.716250458064542, + -0.7569783305856266, + -0.7976733030994199, + -0.838333737904976, + -0.8789580181211777, + -0.9195445485830875, + -0.9600917567174224, + -1.000598094575009, + -1.0410615032888937, + -1.0814803712044703, + -1.1218537441463625, + -1.16218027546253, + -1.202458553509516, + -1.2426871946620155, + -1.282864844227467, + -1.322990177119033, + -1.3630618985103653, + -1.403078744473233, + -1.4430394825992696, + -1.4829429126072635, + -1.5227878669376127, + -1.56257321133575, + -1.6022978454265608, + -1.641960703282012, + -1.6815607539844368, + -1.7210970021881182, + -1.7605684886820359, + -1.799974290956817, + -1.8393135237790936, + -1.878585339776583, + -1.9177889300372137, + -1.956923524725513, + -1.9959883937191698, + -2.034982847268056, + -2.0739062366769607, + -2.112757955011557, + -2.1515374378244467, + -2.1902441638940005, + -2.2288776559624486, + -2.2674374814502327, + -2.3059232531093925, + -2.3443346295573306, + -2.3826713155998247, + -2.4209330622027507, + -2.459119665896172, + -2.4972309672769857, + -2.5352668480923404, + -2.5732272274424277, + -2.611110958184905, + -2.6489175125375874, + -2.6866478287539675, + -2.724302174006824, + -2.7618802881465987, + -2.7993816397778417, + -2.836805188328488, + -2.874148937499747, + -2.911409049164428, + -2.9485779431831904, + -2.9856397604987057, + -3.0225577652352547, + -3.0592620806116706, + -3.094566760975378, + -3.12648501236953 + ] + } + }, + "V3": { + "projected_area": 19.413149724247187, + "VLM 8x72": { + "CL": 0.6249834502957544, + "CDi_near": 0.024765199802231058, + "CDi_trefftz": 0.029555022511630297, + "gamma": [ + 0.1970795234878041, + 0.3017627594125359, + 0.4066394624141544, + 0.5140883878580795, + 0.6116263363016126, + 0.703190445631355, + 0.7939550948606542, + 0.8872161447328979, + 1.0130680427705299, + 1.1589096654517017, + 1.2976025161722617, + 1.3950729910098667, + 1.494584311722068, + 1.5834985061097, + 1.6938601724568882, + 1.807608290755648, + 1.8990930283828826, + 1.9905345848751543, + 2.067864831064663, + 2.124301488982896, + 2.183526960194489, + 2.2352754119791047, + 2.2976815509527024, + 2.360574121816527, + 2.4061826725544804, + 2.4525630749455924, + 2.489985858207783, + 2.516859177077073, + 2.5445759269168304, + 2.567666266505471, + 2.594065652131558, + 2.617835838076459, + 2.6307582890465806, + 2.6389751442505056, + 2.642818537010915, + 2.6442833028101154, + 2.6442833028101154, + 2.642818537010915, + 2.638975144250505, + 2.630758289046581, + 2.6178358380764597, + 2.594065652131559, + 2.5676662665054724, + 2.5445759269168313, + 2.5168591770770736, + 2.4899858582077834, + 2.452563074945593, + 2.4061826725544817, + 2.3605741218165277, + 2.2976815509527033, + 2.235275411979105, + 2.1835269601944893, + 2.1243014889828964, + 2.067864831064663, + 1.990534584875155, + 1.8990930283828829, + 1.8076082907556483, + 1.6938601724568887, + 1.5834985061097, + 1.4945843117220683, + 1.3950729910098667, + 1.2976025161722615, + 1.1589096654517017, + 1.0130680427705299, + 0.8872161447328979, + 0.7939550948606542, + 0.7031904456313549, + 0.6116263363016123, + 0.5140883878580794, + 0.4066394624141544, + 0.3017627594125358, + 0.19707952348780414 + ], + "y": [ + -4.112650172742665, + -4.104927954650263, + -4.091005566611716, + -4.0707069858792035, + -4.049383693183927, + -4.027228535842301, + -4.003980412024534, + -3.9797465676896273, + -3.9280524485080943, + -3.8489233276025985, + -3.7757503406780764, + -3.7084530183843407, + -3.6327981335355917, + -3.548855053203945, + -3.413851438897148, + -3.227764214116302, + -3.0504871320528952, + -2.8821467479729233, + -2.750479516481624, + -2.6551805266758333, + -2.5541580138456768, + -2.4476544686690413, + -2.2855482632144204, + -2.0677315673039107, + -1.8571808963475496, + -1.6541812045965665, + -1.4989517029311679, + -1.391165439272446, + -1.2807138265208926, + -1.1677221238015807, + -0.999536245934019, + -0.7758348805308, + -0.5539359798263738, + -0.3343932864044814, + -0.16856718351255603, + -0.05619128576937413, + 0.05619128576937413, + 0.16856718351255603, + 0.3343932864044814, + 0.5539359798263738, + 0.7758348805308, + 0.999536245934019, + 1.1677221238015807, + 1.2807138265208926, + 1.391165439272446, + 1.4989517029311679, + 1.6541812045965665, + 1.8571808963475496, + 2.0677315673039107, + 2.2855482632144204, + 2.4476544686690413, + 2.5541580138456768, + 2.6551805266758333, + 2.750479516481624, + 2.8821467479729233, + 3.0504871320528952, + 3.227764214116302, + 3.413851438897148, + 3.548855053203945, + 3.6327981335355917, + 3.7084530183843407, + 3.7757503406780764, + 3.8489233276025985, + 3.9280524485080943, + 3.9797465676896273, + 4.003980412024534, + 4.027228535842301, + 4.049383693183927, + 4.0707069858792035, + 4.091005566611716, + 4.104927954650263, + 4.112650172742665 + ] + }, + "VLM 16x144": { + "CL": 0.6242045369068796, + "CDi_near": 0.024979035281850366, + "CDi_trefftz": 0.029609711793473936, + "gamma": [ + 0.12549074278315495, + 0.20052881044698415, + 0.2687669733362947, + 0.3121603703423283, + 0.35250774271610347, + 0.41264321861705844, + 0.4852644796623525, + 0.5339838798268132, + 0.5714517053374532, + 0.6175089463655872, + 0.6775077790787203, + 0.7205866552337696, + 0.7548214299709433, + 0.7976349484781806, + 0.8567859030152998, + 0.9045356218186292, + 0.955231651079428, + 1.0292380714817568, + 1.1302917199457905, + 1.2074371376563113, + 1.2641684019579593, + 1.3158305331270315, + 1.376529632222002, + 1.4211841920540491, + 1.4591994291546446, + 1.5029152617551835, + 1.5582295164196056, + 1.6010783485384303, + 1.6445298876853134, + 1.7068366796846117, + 1.7838460918151977, + 1.8334322579521056, + 1.8677059298232623, + 1.913000198859254, + 1.974850462033853, + 2.0198592694658264, + 2.0505201529278496, + 2.079230956064091, + 2.1142244723373538, + 2.140405692787251, + 2.1631625885489365, + 2.188919312715015, + 2.221061716714223, + 2.2454408961047863, + 2.2698025082658564, + 2.3049369767447696, + 2.347426543097953, + 2.3733699071562047, + 2.389978454043784, + 2.412852285207737, + 2.444154492684219, + 2.466337809713223, + 2.4810947211781627, + 2.494914748837525, + 2.5116113125304613, + 2.523864324996263, + 2.5345783950191367, + 2.5463917627257646, + 2.5607667315472984, + 2.5712156704054836, + 2.5815157127306194, + 2.595968423973215, + 2.612058322349723, + 2.6206253585285353, + 2.625269725788632, + 2.630736561849136, + 2.6362294807976956, + 2.6388823227376417, + 2.640417494029526, + 2.6416805502175604, + 2.6425850576691374, + 2.6427641277452247, + 2.642764127745225, + 2.642585057669139, + 2.6416805502175627, + 2.640417494029527, + 2.6388823227376417, + 2.636229480797697, + 2.630736561849137, + 2.625269725788632, + 2.620625358528535, + 2.6120583223497227, + 2.595968423973215, + 2.58151571273062, + 2.5712156704054827, + 2.560766731547297, + 2.546391762725762, + 2.534578395019135, + 2.5238643249962616, + 2.5116113125304587, + 2.4949147488375236, + 2.4810947211781618, + 2.466337809713222, + 2.4441544926842185, + 2.412852285207736, + 2.389978454043783, + 2.373369907156203, + 2.3474265430979537, + 2.3049369767447705, + 2.2698025082658573, + 2.2454408961047854, + 2.221061716714222, + 2.188919312715015, + 2.1631625885489356, + 2.140405692787251, + 2.1142244723373533, + 2.07923095606409, + 2.0505201529278487, + 2.0198592694658264, + 1.9748504620338525, + 1.9130001988592535, + 1.8677059298232628, + 1.8334322579521052, + 1.783846091815198, + 1.7068366796846128, + 1.6445298876853127, + 1.6010783485384303, + 1.5582295164196065, + 1.5029152617551835, + 1.4591994291546446, + 1.4211841920540493, + 1.376529632222003, + 1.315830533127031, + 1.264168401957959, + 1.2074371376563109, + 1.13029171994579, + 1.0292380714817566, + 0.9552316510794278, + 0.9045356218186293, + 0.8567859030153001, + 0.7976349484781807, + 0.7548214299709433, + 0.7205866552337696, + 0.677507779078721, + 0.6175089463655874, + 0.5714517053374534, + 0.5339838798268134, + 0.48526447966235253, + 0.41264321861705855, + 0.35250774271610363, + 0.31216037034232846, + 0.2687669733362946, + 0.20052881044698417, + 0.12549074278315492 + ], + "y": [ + -4.115347884208903, + -4.111550499984791, + -4.106094454341433, + -4.102169620473144, + -4.09810202773998, + -4.088104473569722, + -4.073757179943181, + -4.063456153380926, + -4.057188219005663, + -4.0461659528726415, + -4.030499981523249, + -4.019367090314647, + -4.012552195003726, + -4.000436962394561, + -3.983274761713184, + -3.9711561499874417, + -3.9560599107771983, + -3.916484403321408, + -3.8605160883923926, + -3.820962474942687, + -3.7994974103685357, + -3.765878036893719, + -3.718398000698334, + -3.684720051879415, + -3.662387424444491, + -3.6204104884146604, + -3.561234270814933, + -3.519268126513117, + -3.479555644517088, + -3.386518174512107, + -3.2550633081883955, + -3.16201355341253, + -3.110010289194828, + -3.0257829619072867, + -2.906737941948884, + -2.822624885156453, + -2.784152105521125, + -2.736597930822257, + -2.669257351138164, + -2.6215125360312412, + -2.591732941618729, + -2.5384552438954957, + -2.463305623065936, + -2.410079775612534, + -2.36247721349179, + -2.253596855490571, + -2.099731038270469, + -1.9907947003611781, + -1.9289450118257028, + -1.8273306787786319, + -1.6838022786722422, + -1.5824169199683298, + -1.5371012461996925, + -1.483257103238206, + -1.4069585944079648, + -1.3530164737107295, + -1.320678222689764, + -1.264168730635446, + -1.1842398224345456, + -1.1277576117695518, + -1.078548227855214, + -0.9668445606526462, + -0.8088208834974364, + -0.6968231852967852, + -0.6314667726708917, + -0.5215655701671168, + -0.36650335878674234, + -0.2568618678686248, + -0.20829903436932556, + -0.1521144223955178, + -0.07265071777573867, + -0.016459432006364544, + 0.016459432006364544, + 0.07265071777573867, + 0.1521144223955178, + 0.20829903436932556, + 0.2568618678686248, + 0.36650335878674234, + 0.5215655701671168, + 0.6314667726708917, + 0.6968231852967852, + 0.8088208834974364, + 0.9668445606526462, + 1.078548227855214, + 1.1277576117695518, + 1.1842398224345456, + 1.264168730635446, + 1.320678222689764, + 1.3530164737107295, + 1.4069585944079648, + 1.483257103238206, + 1.5371012461996925, + 1.5824169199683298, + 1.6838022786722422, + 1.8273306787786319, + 1.9289450118257028, + 1.9907947003611781, + 2.099731038270469, + 2.253596855490571, + 2.36247721349179, + 2.410079775612534, + 2.463305623065936, + 2.5384552438954957, + 2.591732941618729, + 2.6215125360312412, + 2.669257351138164, + 2.736597930822257, + 2.784152105521125, + 2.822624885156453, + 2.906737941948884, + 3.0257829619072867, + 3.110010289194828, + 3.16201355341253, + 3.2550633081883955, + 3.386518174512107, + 3.479555644517088, + 3.519268126513117, + 3.561234270814933, + 3.6204104884146604, + 3.662387424444491, + 3.684720051879415, + 3.718398000698334, + 3.765878036893719, + 3.7994974103685357, + 3.820962474942687, + 3.8605160883923926, + 3.916484403321408, + 3.9560599107771983, + 3.9711561499874417, + 3.983274761713184, + 4.000436962394561, + 4.012552195003726, + 4.019367090314647, + 4.030499981523249, + 4.0461659528726415, + 4.057188219005663, + 4.063456153380926, + 4.073757179943181, + 4.088104473569722, + 4.09810202773998, + 4.102169620473144, + 4.106094454341433, + 4.111550499984791, + 4.115347884208903 + ] + }, + "VSM rib N=40": { + "CL": 0.6393171088835797, + "CDi_blade": 0.028298800310764233, + "CDi_trefftz": 0.028096462708842605, + "converged": true, + "gamma": [ + 0.2842196122172311, + 0.4816895523555522, + 0.6755871685978874, + 0.8795257212549099, + 1.0822947892284371, + 1.3142937292227581, + 1.5233070908618531, + 1.694061150698677, + 1.8255854030659049, + 1.9534620051215228, + 2.0894224391215177, + 2.207978951205503, + 2.2981148017542625, + 2.362690614841517, + 2.429522286008258, + 2.4935355847827405, + 2.5418966036891817, + 2.5706871945563248, + 2.5867051672896513, + 2.595781664943045, + 2.5957816649430456, + 2.5867051672896513, + 2.5706871945563243, + 2.541896603689182, + 2.49353558478274, + 2.4295222860082593, + 2.3626906148415188, + 2.2981148017542647, + 2.2079789512055044, + 2.08942243912152, + 1.953462005121522, + 1.8255854030659062, + 1.6940611506986778, + 1.5233070908618538, + 1.3142937292227577, + 1.082294789228436, + 0.879525721254907, + 0.6755871685978847, + 0.4816895523555514, + 0.28421961221722464 + ], + "y": [ + 4.10358337054663, + 4.06975337199596, + 4.0228184923227985, + 3.9557381018714843, + 3.8648070664387006, + 3.7272751746659893, + 3.530336676945344, + 3.302673547651778, + 3.075604735120473, + 2.8561689975947537, + 2.6192389902379736, + 2.3554465888133844, + 2.0802920967329683, + 1.8127340793358573, + 1.5502507882060739, + 1.2765306568089396, + 0.992740272068261, + 0.7077067764616343, + 0.42510416953346886, + 0.14221977635906635, + -0.14221977635906635, + -0.42510416953346897, + -0.7077067764616344, + -0.992740272068261, + -1.2765306568089396, + -1.5502507882060736, + -1.812734079335857, + -2.0802920967329666, + -2.3554465888133826, + -2.619238990237972, + -2.8561689975947537, + -3.075604735120474, + -3.3026735476517772, + -3.530336676945344, + -3.7272751746659893, + -3.8648070664387006, + -3.9557381018714843, + -4.0228184923227985, + -4.06975337199596, + -4.10358337054663 + ] + }, + "VSM rib N=80": { + "CL": 0.6385107288798224, + "CDi_blade": 0.0284336819387546, + "CDi_trefftz": 0.028200057729742035, + "converged": true, + "gamma": [ + 0.18800653873064768, + 0.3031836597923403, + 0.4071839078185746, + 0.5073299481548805, + 0.6073839349634891, + 0.7092750755453681, + 0.8130871825707765, + 0.9251285429812206, + 1.0261043733267223, + 1.1285479543699588, + 1.2510950467879662, + 1.3606709863464592, + 1.4670141692881014, + 1.5645323000727285, + 1.650210682702369, + 1.7255735211365697, + 1.7907597651026774, + 1.8527506276546182, + 1.9163526786504559, + 1.983529508712381, + 2.051123385054607, + 2.1187405209763863, + 2.177368049558184, + 2.230759559259542, + 2.2756114418557027, + 2.3137673595373585, + 2.3444815756545316, + 2.3773493628470765, + 2.410283837054975, + 2.4452640708825175, + 2.4768521391891136, + 2.505860737729557, + 2.530367851435397, + 2.54952086277257, + 2.5643762850331715, + 2.5743783383480787, + 2.582152332261012, + 2.588290134251178, + 2.592806132377936, + 2.595174684370243, + 2.5951746843702432, + 2.5928061323779366, + 2.5882901342511797, + 2.5821523322610136, + 2.5743783383480823, + 2.564376285033174, + 2.5495208627725736, + 2.5303678514353978, + 2.505860737729559, + 2.4768521391891154, + 2.4452640708825197, + 2.410283837054978, + 2.3773493628470788, + 2.3444815756545343, + 2.3137673595373607, + 2.2756114418557054, + 2.230759559259546, + 2.1773680495581873, + 2.11874052097639, + 2.05112338505461, + 1.9835295087123854, + 1.916352678650459, + 1.8527506276546217, + 1.7907597651026788, + 1.7255735211365724, + 1.6502106827023701, + 1.5645323000727285, + 1.4670141692881014, + 1.360670986346461, + 1.2510950467879685, + 1.1285479543699612, + 1.0261043733267257, + 0.9251285429812268, + 0.8130871825707778, + 0.7092750755453677, + 0.6073839349634946, + 0.5073299481548815, + 0.40718390781857583, + 0.30318365979233974, + 0.18800653873064693 + ], + "y": [ + 4.111446653815547, + 4.098041911606868, + 4.080593267564084, + 4.060187903515944, + 4.036893946993906, + 4.010357312989299, + 3.980882665942608, + 3.940297445900068, + 3.889967590827081, + 3.839628463605437, + 3.7733487974654016, + 3.686212103950151, + 3.587417727965049, + 3.477557134112515, + 3.361555560103562, + 3.2437652371385464, + 3.1291454307636113, + 3.019897586449587, + 2.9128460770208564, + 2.802709839947192, + 2.685313455257937, + 2.5584613821673017, + 2.424773505649936, + 2.287757916762652, + 2.148991416353346, + 2.010898830271594, + 1.8774731513388323, + 1.7480546322013988, + 1.6186536188886553, + 1.485582021235576, + 1.3477606985359407, + 1.2069918954267076, + 1.064238838200433, + 0.9213138241241257, + 0.7782881630164236, + 0.6362113438735597, + 0.49534588614696773, + 0.35473928138604766, + 0.21358840139372995, + 0.0713632002183667, + -0.0713632002183667, + -0.21358840139372995, + -0.35473928138604766, + -0.49534588614696773, + -0.6362113438735599, + -0.7782881630164236, + -0.9213138241241257, + -1.064238838200433, + -1.2069918954267076, + -1.3477606985359407, + -1.485582021235576, + -1.6186536188886553, + -1.7480546322013988, + -1.8774731513388323, + -2.0108988302715938, + -2.148991416353345, + -2.287757916762651, + -2.424773505649935, + -2.558461382167301, + -2.685313455257936, + -2.8027098399471915, + -2.9128460770208564, + -3.019897586449587, + -3.1291454307636113, + -3.2437652371385464, + -3.361555560103562, + -3.477557134112515, + -3.587417727965049, + -3.686212103950152, + -3.773348797465402, + -3.839628463605437, + -3.889967590827081, + -3.940297445900068, + -3.980882665942608, + -4.010357312989299, + -4.036893946993906, + -4.060187903515944, + -4.080593267564084, + -4.0980419116068685, + -4.1114466538155465 + ] + }, + "VSM streamwise N=40": { + "CL": 0.6125695330732521, + "CDi_blade": 0.026858743795443483, + "CDi_trefftz": 0.026767295739304784, + "converged": true, + "gamma": [ + 0.2764219854946078, + 0.4782793207268841, + 0.6764734874696218, + 0.8803272845295667, + 1.0760281711912039, + 1.2947773658789246, + 1.4939314194829223, + 1.6646640343738137, + 1.799378768375968, + 1.925194105984506, + 2.052120532371685, + 2.1649567277952966, + 2.2578777098233482, + 2.325926612045967, + 2.391123410554287, + 2.449985966959743, + 2.497222959249689, + 2.528395640263785, + 2.545340728439021, + 2.5536278205787113, + 2.553628461490642, + 2.545340919234991, + 2.5283957474775645, + 2.497223033110435, + 2.449986022397128, + 2.3911234537585564, + 2.325926646121972, + 2.257877736993738, + 2.164956749938806, + 2.05212055072218, + 1.9251941212595816, + 1.7993787810528523, + 1.6646640448407137, + 1.4939314281247578, + 1.294777372999671, + 1.0760281769946156, + 0.8803272891812179, + 0.6764734910587414, + 0.4782793233210272, + 0.2764219870810753 + ], + "y": [ + 4.10358337054663, + 4.06975337199596, + 4.0228184923227985, + 3.9557381018714843, + 3.8648070664387006, + 3.7272751746659893, + 3.530336676945344, + 3.302673547651778, + 3.075604735120473, + 2.8561689975947537, + 2.6192389902379736, + 2.3554465888133844, + 2.0802920967329683, + 1.8127340793358573, + 1.5502507882060739, + 1.2765306568089396, + 0.992740272068261, + 0.7077067764616343, + 0.42510416953346886, + 0.14221977635906635, + -0.14221977635906635, + -0.42510416953346897, + -0.7077067764616344, + -0.992740272068261, + -1.2765306568089396, + -1.5502507882060736, + -1.812734079335857, + -2.0802920967329666, + -2.3554465888133826, + -2.619238990237972, + -2.8561689975947537, + -3.075604735120474, + -3.3026735476517772, + -3.530336676945344, + -3.7272751746659893, + -3.8648070664387006, + -3.9557381018714843, + -4.0228184923227985, + -4.06975337199596, + -4.10358337054663 + ] + }, + "VSM streamwise N=80": { + "CL": 0.612168103615367, + "CDi_blade": 0.027073364463625335, + "CDi_trefftz": 0.026845359527399856, + "converged": true, + "gamma": [ + 0.18576999002313596, + 0.302325127659961, + 0.40838368320353, + 0.5109202640792242, + 0.6129968847441934, + 0.7156780259478986, + 0.8185169498119078, + 0.928175678448985, + 1.025401466664682, + 1.1223787983976778, + 1.2360552272489191, + 1.3382777001008148, + 1.4381733235317289, + 1.5317259281482072, + 1.617159177958248, + 1.6954012015361606, + 1.7647475782093585, + 1.829624348793553, + 1.8930769595920796, + 1.9565410528108296, + 2.0176651901315936, + 2.078199874159081, + 2.132867567898943, + 2.1850015134455507, + 2.231511881816502, + 2.2739738779526046, + 2.3088471022286505, + 2.3437851935689875, + 2.3756394770162044, + 2.4070805699772175, + 2.4350247225198736, + 2.4610698720068687, + 2.4844012488812766, + 2.504274060727889, + 2.521153170115183, + 2.5331306395198347, + 2.5417471159626923, + 2.5476965424682785, + 2.551664706004728, + 2.5535803043491376, + 2.5535802658503903, + 2.5516646938324734, + 2.5476965354529986, + 2.54174711120002, + 2.5331306360128356, + 2.5211531673556453, + 2.504274058447408, + 2.48440124693609, + 2.461069870314811, + 2.435024721030167, + 2.4070805686550125, + 2.375639475840083, + 2.3437851925224473, + 2.3088471013013248, + 2.273973877130605, + 2.2315118810817665, + 2.1850015127824793, + 2.132867567297171, + 2.078199873610162, + 2.017665189629962, + 1.95654105235197, + 1.8930769591731984, + 1.829624348412386, + 1.7647475778637534, + 1.695401201223243, + 1.6171591776745908, + 1.5317259278905937, + 1.4381733232977099, + 1.3382776998885175, + 1.2360552270565979, + 1.1223787982243236, + 1.0254014665087483, + 0.9281756783097319, + 0.8185169496888115, + 0.7156780258402101, + 0.612996884651522, + 0.5109202640011372, + 0.40838368313990686, + 0.3023251276114615, + 0.18576998999209948 + ], + "y": [ + 4.111446653815547, + 4.098041911606868, + 4.080593267564084, + 4.060187903515944, + 4.036893946993906, + 4.010357312989299, + 3.980882665942608, + 3.940297445900068, + 3.889967590827081, + 3.839628463605437, + 3.7733487974654016, + 3.686212103950151, + 3.587417727965049, + 3.477557134112515, + 3.361555560103562, + 3.2437652371385464, + 3.1291454307636113, + 3.019897586449587, + 2.9128460770208564, + 2.802709839947192, + 2.685313455257937, + 2.5584613821673017, + 2.424773505649936, + 2.287757916762652, + 2.148991416353346, + 2.010898830271594, + 1.8774731513388323, + 1.7480546322013988, + 1.6186536188886553, + 1.485582021235576, + 1.3477606985359407, + 1.2069918954267076, + 1.064238838200433, + 0.9213138241241257, + 0.7782881630164236, + 0.6362113438735597, + 0.49534588614696773, + 0.35473928138604766, + 0.21358840139372995, + 0.0713632002183667, + -0.0713632002183667, + -0.21358840139372995, + -0.35473928138604766, + -0.49534588614696773, + -0.6362113438735599, + -0.7782881630164236, + -0.9213138241241257, + -1.064238838200433, + -1.2069918954267076, + -1.3477606985359407, + -1.485582021235576, + -1.6186536188886553, + -1.7480546322013988, + -1.8774731513388323, + -2.0108988302715938, + -2.148991416353345, + -2.287757916762651, + -2.424773505649935, + -2.558461382167301, + -2.685313455257936, + -2.8027098399471915, + -2.9128460770208564, + -3.019897586449587, + -3.1291454307636113, + -3.2437652371385464, + -3.361555560103562, + -3.477557134112515, + -3.587417727965049, + -3.686212103950152, + -3.773348797465402, + -3.839628463605437, + -3.889967590827081, + -3.940297445900068, + -3.980882665942608, + -4.010357312989299, + -4.036893946993906, + -4.060187903515944, + -4.080593267564084, + -4.0980419116068685, + -4.1114466538155465 + ] + }, + "VSM perpendicular N=40": { + "CL": 0.576782685706456, + "CDi_blade": 0.027879737964822984, + "CDi_trefftz": 0.02745422978864207, + "converged": true, + "gamma": [ + 0.25068702909143087, + 0.41931691645914937, + 0.5841459387950705, + 0.7803964041722329, + 0.9736933198392055, + 1.1835290392986577, + 1.3770951136399245, + 1.5457571758863722, + 1.6830455683599828, + 1.8176446053712096, + 1.9597566818351444, + 2.0850049096513454, + 2.1823187043460903, + 2.2532369637038316, + 2.3237177409884078, + 2.389488508530588, + 2.439596323753618, + 2.4703068419484597, + 2.4877286889596433, + 2.4973779404938217, + 2.4973779404938217, + 2.487728688959643, + 2.4703068419484597, + 2.439596323753619, + 2.3894885085305893, + 2.3237177409884087, + 2.253236963703834, + 2.1823187043460917, + 2.0850049096513463, + 1.959756681835146, + 1.8176446053712039, + 1.6830455683599832, + 1.5457571758863726, + 1.3770951136399223, + 1.183529039298651, + 0.9736933198391995, + 0.7803964041722229, + 0.5841459387950639, + 0.4193169164591433, + 0.25068702909142365 + ], + "y": [ + 4.10358337054663, + 4.06975337199596, + 4.0228184923227985, + 3.9557381018714843, + 3.8648070664387006, + 3.7272751746659893, + 3.530336676945344, + 3.302673547651778, + 3.075604735120473, + 2.8561689975947537, + 2.6192389902379736, + 2.3554465888133844, + 2.0802920967329683, + 1.8127340793358573, + 1.5502507882060739, + 1.2765306568089396, + 0.992740272068261, + 0.7077067764616343, + 0.42510416953346886, + 0.14221977635906635, + -0.14221977635906635, + -0.42510416953346897, + -0.7077067764616344, + -0.992740272068261, + -1.2765306568089396, + -1.5502507882060736, + -1.812734079335857, + -2.0802920967329666, + -2.3554465888133826, + -2.619238990237972, + -2.8561689975947537, + -3.075604735120474, + -3.3026735476517772, + -3.530336676945344, + -3.7272751746659893, + -3.8648070664387006, + -3.9557381018714843, + -4.0228184923227985, + -4.06975337199596, + -4.10358337054663 + ] + }, + "VSM perpendicular N=80": { + "CL": 0.5747251811156084, + "CDi_blade": 0.02827168196719301, + "CDi_trefftz": 0.027774477109817702, + "converged": true, + "gamma": [ + 0.18263496282983618, + 0.2773991578260784, + 0.34852945336570595, + 0.42064412787244515, + 0.5069580115939034, + 0.6044762320460764, + 0.7056053627331154, + 0.8156247257432812, + 0.9141483531141921, + 1.0101923922800327, + 1.118001109103845, + 1.2156776559151314, + 1.3138119186613766, + 1.4069719055928807, + 1.49205548977391, + 1.5692627486521014, + 1.6377826835542342, + 1.7037219454915304, + 1.7713069041291885, + 1.8424998859285568, + 1.9140037622332424, + 1.9851000690970282, + 2.0474415219331914, + 2.1046511956369933, + 2.1535463916329696, + 2.1954827343002465, + 2.2298451843746183, + 2.2656695614151774, + 2.300702074396433, + 2.3369511657283426, + 2.3694303296622325, + 2.3991639822286706, + 2.424551116165888, + 2.444742571003708, + 2.460637216172908, + 2.471668515636237, + 2.480236011420122, + 2.4869062345904904, + 2.491726150823936, + 2.4942308606506245, + 2.4942308606506254, + 2.491726150823937, + 2.4869062345904918, + 2.480236011420123, + 2.4716685156362397, + 2.460637216172911, + 2.4447425710037116, + 2.4245511161658917, + 2.399163982228672, + 2.3694303296622348, + 2.3369511657283435, + 2.3007020743964333, + 2.265669561415178, + 2.229845184374619, + 2.195482734300247, + 2.153546391632971, + 2.104651195636997, + 2.0474415219331927, + 1.9851000690970315, + 1.9140037622332442, + 1.8424998859285586, + 1.7713069041291907, + 1.7037219454915344, + 1.6377826835542362, + 1.5692627486521042, + 1.4920554897739124, + 1.4069719055928829, + 1.3138119186613797, + 1.2156776559151339, + 1.118001109103848, + 1.0101923922800355, + 0.9141483531141941, + 0.8156247257432808, + 0.705605362733114, + 0.6044762320460667, + 0.5069580115938986, + 0.4206441278724483, + 0.34852945336570745, + 0.2773991578260705, + 0.182634962829825 + ], + "y": [ + 4.111446653815547, + 4.098041911606868, + 4.080593267564084, + 4.060187903515944, + 4.036893946993906, + 4.010357312989299, + 3.980882665942608, + 3.940297445900068, + 3.889967590827081, + 3.839628463605437, + 3.7733487974654016, + 3.686212103950151, + 3.587417727965049, + 3.477557134112515, + 3.361555560103562, + 3.2437652371385464, + 3.1291454307636113, + 3.019897586449587, + 2.9128460770208564, + 2.802709839947192, + 2.685313455257937, + 2.5584613821673017, + 2.424773505649936, + 2.287757916762652, + 2.148991416353346, + 2.010898830271594, + 1.8774731513388323, + 1.7480546322013988, + 1.6186536188886553, + 1.485582021235576, + 1.3477606985359407, + 1.2069918954267076, + 1.064238838200433, + 0.9213138241241257, + 0.7782881630164236, + 0.6362113438735597, + 0.49534588614696773, + 0.35473928138604766, + 0.21358840139372995, + 0.0713632002183667, + -0.0713632002183667, + -0.21358840139372995, + -0.35473928138604766, + -0.49534588614696773, + -0.6362113438735599, + -0.7782881630164236, + -0.9213138241241257, + -1.064238838200433, + -1.2069918954267076, + -1.3477606985359407, + -1.485582021235576, + -1.6186536188886553, + -1.7480546322013988, + -1.8774731513388323, + -2.0108988302715938, + -2.148991416353345, + -2.287757916762651, + -2.424773505649935, + -2.558461382167301, + -2.685313455257936, + -2.8027098399471915, + -2.9128460770208564, + -3.019897586449587, + -3.1291454307636113, + -3.2437652371385464, + -3.361555560103562, + -3.477557134112515, + -3.587417727965049, + -3.686212103950152, + -3.773348797465402, + -3.839628463605437, + -3.889967590827081, + -3.940297445900068, + -3.980882665942608, + -4.010357312989299, + -4.036893946993906, + -4.060187903515944, + -4.080593267564084, + -4.0980419116068685, + -4.1114466538155465 + ] + } + } +} \ No newline at end of file diff --git a/docs/validation/vlm_referee.py b/docs/validation/vlm_referee.py new file mode 100644 index 0000000..925c91e --- /dev/null +++ b/docs/validation/vlm_referee.py @@ -0,0 +1,253 @@ +"""Lifting-surface referee for the attached-trailed leg direction. + +The Trefftz plane checks the induced drag of a given circulation but cannot +judge the lift. A vortex-lattice method (VLM) can: for inviscid flat-plate +sections it solves the same potential-flow problem as the lifting line with +``Cl = 2 pi alpha``, with full chordwise resolution. This script runs the VLM +of AeroSandbox (Sharpe, MIT, ``pip install aerosandbox``; horseshoe vortices, +trailing legs along the body x axis, near-field forces) and the VSM with the +three leg options (``rib``, ``streamwise``, ``perpendicular``) on + +* a straight AR 8 wing, a yawed wing (uniform 30 deg sweep, no kink) and a + kinked 30 deg swept wing, chord 1, span 8, at 5 deg; +* the circular-arc wing (110 deg, R = 4 c) at 8 deg; +* the TORQUE 2026 swept-anhedral elliptic wing at 10 deg; +* the inviscid V3 kite (CAD leading and trailing edges, flat-plate sections) + at 10 deg. AeroSandbox takes its sections perpendicular to the local span, + so the skewed V3 ribs are unskewed there (chord = rib length, twist from + the rib's x-z angle); the surface differs from the VSM one at the tips. + +For the induced drag two VLM numbers are given: AeroSandbox's near-field +value, and the Trefftz-plane drag of the VLM's own strip circulation on the +wake trace. The two agree on planar wings; on the non-planar wings the +near-field value is low (the trailing legs are not aligned with the wind), +so the Trefftz value is the one to compare with. + +Results go to ``vlm_referee.json`` and into the report. About 4 minutes. +""" + +from __future__ import annotations + +import json +import logging +from pathlib import Path + +import numpy as np +import yaml + +import curved_bound_vortex_validation as cbv +from VSM.core.Solver import Solver + +try: + import aerosandbox as asb +except ImportError as err: # pragma: no cover + raise SystemExit("vlm_referee.py needs AeroSandbox: pip install aerosandbox") from err + +logging.disable(logging.CRITICAL) +OUT = Path(__file__).resolve().parent +FLAT = asb.Airfoil("naca0010") # symmetric: the VLM sees a flat camber line +LEGS = ("rib", "streamwise", "perpendicular") +RHO = 1.225 + + +def strip_circulation(vlm, n_chord, n_segments): + """Chordwise sum of the horseshoe strengths per spanwise strip, with the + strip's trailing-edge corners; handles the three panel orderings + AeroSandbox produces.""" + G = np.asarray(vlm.vortex_strengths) + te = np.asarray(vlm.is_trailing_edge, bool) + n_strip = len(G) // n_chord + if np.all(te.reshape(n_strip, n_chord)[:, -1]): + Gs = G.reshape(n_strip, n_chord).sum(axis=1) + te_idx = np.arange(n_strip) * n_chord + (n_chord - 1) + elif np.all(te.reshape(n_chord, n_strip)[-1]): + Gs = G.reshape(n_chord, n_strip).sum(axis=0) + te_idx = (n_chord - 1) * n_strip + np.arange(n_strip) + else: + ns_seg = n_strip // n_segments + te3 = te.reshape(n_segments, n_chord, ns_seg) + if not np.all(te3[:, -1, :]): + raise RuntimeError("AeroSandbox panel ordering not recognised") + Gs = G.reshape(n_segments, n_chord, ns_seg).sum(axis=1).ravel() + te_idx = np.array( + [(g * n_chord + n_chord - 1) * ns_seg + j for g in range(n_segments) for j in range(ns_seg)] + ) + bl = np.asarray(vlm.back_left_vertices)[te_idx] + br = np.asarray(vlm.back_right_vertices)[te_idx] + return Gs, bl, br + + +def trefftz_drag(G, bl, br, alpha_deg): + """Trefftz-plane induced drag of strip circulations ``G`` whose wake + leaves at the strip corners ``bl -> br``, projected onto the plane + perpendicular to the freestream (sideslip zero). Each strip edge sheds + a line vortex of strength equal to the jump in ``G``.""" + al = np.radians(alpha_deg) + ex = np.array([np.cos(al), 0.0, np.sin(al)]) + ey = np.array([0.0, 1.0, 0.0]) + ez = np.cross(ex, ey) + proj = lambda p: np.column_stack((p @ ey, p @ ez)) + L, R = proj(bl), proj(br) + order = np.argsort(0.5 * (L[:, 0] + R[:, 0])) + G, L, R = G[order], L[order], R[order] + edges = np.vstack([L[:1], 0.5 * (R[:-1] + L[1:]), R[-1:]]) + dG = np.concatenate(([G[0]], G[1:] - G[:-1], [-G[-1]])) + mid = 0.5 * (L + R) + tang = R - L + ds = np.linalg.norm(tang, axis=1) + tang = tang / ds[:, None] + nrm = np.column_stack((-tang[:, 1], tang[:, 0])) + drag = 0.0 + for j in range(len(G)): + r = mid[j][None, :] - edges + r2 = np.einsum("ij,ij->i", r, r) + v = (dG / (2 * np.pi * r2))[:, None] * np.column_stack((-r[:, 1], r[:, 0])) + drag += 0.5 * RHO * G[j] * float(np.sum(v @ nrm[j])) * ds[j] + return drag + + +def run_vlm(xsecs, area, U, alpha, n_chord, n_span_per_segment): + airplane = asb.Airplane( + wings=[asb.Wing(symmetric=False, xsecs=xsecs)], s_ref=area, c_ref=1.0, b_ref=1.0 + ) + vlm = asb.VortexLatticeMethod( + airplane=airplane, + op_point=asb.OperatingPoint(velocity=U, alpha=alpha), + spanwise_resolution=n_span_per_segment, + chordwise_resolution=n_chord, + align_trailing_vortices_with_wind=False, + ) + r = vlm.run() + G, bl, br = strip_circulation(vlm, n_chord, len(xsecs) - 1) + q = 0.5 * RHO * U**2 * area + return { + "CL": float(r["CL"]), + "CDi_near": float(r["CD"]), + "CDi_trefftz": float(trefftz_drag(G, bl, br, alpha) / q), + "gamma": G.tolist(), + "y": (0.5 * (bl[:, 1] + br[:, 1])).tolist(), + } + + +def run_vsm(make, N, legs, area, U): + body = make(N, leg_direction=legs) + r = Solver(allowed_error=1e-7, max_iterations=4000).solve(body) + q = 0.5 * RHO * U**2 * area + return { + "CL": float(r["cl"]), + "CDi_blade": float(r["drag"] / q), + "CDi_trefftz": float(r["drag_induced_trefftz"] / q), + "converged": bool(r["gamma_converged"]), + "gamma": np.asarray(r["gamma_distribution"]).ravel().tolist(), + "y": [float(p.aerodynamic_center[1]) for p in body.panels], + } + + +def show(name, key, v): + if "CDi_near" in v: + print(f" {name:22s} {key:22s}: CL {v['CL']:.4f} CDi near {v['CDi_near']:.5f} Trefftz {v['CDi_trefftz']:.5f}") + else: + print(f" {name:22s} {key:22s}: CL {v['CL']:.4f} CDi blade {v['CDi_blade']:.5f} Trefftz {v['CDi_trefftz']:.5f}") + + +def v3_xsecs(): + data = yaml.safe_load(open(cbv.DATA / "CAD_derived_geometry" / "aero_geometry_CAD_CFD_NF_combined.yaml")) + ribs = [(np.array(r[1:4], float), np.array(r[4:7], float)) for r in data["wing_sections"]["data"]] + ribs.sort(key=lambda t: t[0][1]) + xsecs = [] + for le, te in ribs: + cvec = te - le + xsecs.append(asb.WingXSec(xyz_le=le.tolist(), chord=float(np.linalg.norm(cvec)), + twist=float(np.degrees(np.arctan2(-cvec[2], cvec[0]))), airfoil=FLAT)) + return xsecs + + +def main(): + out = {} + tan30 = np.tan(np.radians(30.0)) + cases = { + "straight AR 8": ([[0.0, -4.0, 0.0], [0.0, 4.0, 0.0]], cbv.straight), + "yawed 30 deg": ([[-4.0 * tan30, -4.0, 0.0], [4.0 * tan30, 4.0, 0.0]], cbv.yawed), + "kinked swept 30 deg": ([[4.0 * tan30, -4.0, 0.0], [0.0, 0.0, 0.0], [4.0 * tan30, 4.0, 0.0]], cbv.swept), + } + print("planar wings, chord 1, span 8, 5 deg, U = 20 m/s") + for name, (le_points, make) in cases.items(): + xsecs = [asb.WingXSec(xyz_le=p, chord=1.0, twist=0.0, airfoil=FLAT) for p in le_points] + n_seg = len(le_points) - 1 + row = {} + for n_chord, n_span in ((8, 40), (16, 80)): + key = f"VLM {n_chord}x{n_span}" + row[key] = run_vlm(xsecs, 8.0, 20.0, 5.0, n_chord, n_span // n_seg) + show(name, key, row[key]) + for legs in LEGS: + for N in (40, 80): + key = f"VSM {legs} N={N}" + row[key] = run_vsm(make, N, legs, 8.0, 20.0) + show(name, key, row[key]) + out[name] = row + + print("\ncircular arc, 110 deg, R = 4 c, 8 deg, U = 10 m/s") + xsecs = [] + for th in np.radians(np.linspace(-55.0, 55.0, 23)): + xsecs.append(asb.WingXSec(xyz_le=[0.0, 4.0 * np.sin(th), 4.0 * np.cos(th)], chord=1.0, twist=0.0, airfoil=FLAT)) + body = cbv.arc(40) + area = float(body.wings[0].compute_projected_area()) + row = {"projected_area": area} + for n_chord, n_seg in ((8, 2), (16, 4)): + key = f"VLM {n_chord}x{22 * n_seg}" + row[key] = run_vlm(xsecs, area, 10.0, 8.0, n_chord, n_seg) + show("arc", key, row[key]) + for legs in LEGS: + for N in (40, 80): + key = f"VSM {legs} N={N}" + row[key] = run_vsm(cbv.arc, N, legs, area, 10.0) + show("arc", key, row[key]) + out["arc"] = row + + print("\nelliptic, 10% sweep, 25% anhedral, 10 deg, U = 10 m/s") + s = 8.0 * (np.pi / 4) / 2 + ys = np.linspace(-s, s, 41) + xsecs, chords = [], [] + for y in ys: + eta = min(abs(y) / s, 0.999999) + c = np.sqrt(1 - eta**2) + chords.append(c) + xsecs.append(asb.WingXSec(xyz_le=[0.10 * s * eta**2, y, -0.25 * s * eta**2], chord=c, twist=0.0, airfoil=FLAT)) + area = float(np.trapz(chords, ys)) + row = {} + for n_chord, n_seg in ((8, 1), (16, 2), (16, 4)): + key = f"VLM {n_chord}x{40 * n_seg}" + row[key] = run_vlm(xsecs, area, 10.0, 10.0, n_chord, n_seg) + show("elliptic", key, row[key]) + for legs in LEGS: + for N in (80, 160): + key = f"VSM {legs} N={N}" + row[key] = run_vsm(cbv.elliptic_swept_anhedral, N, legs, area, 10.0) + show("elliptic", key, row[key]) + out["elliptic"] = row + + print("\nV3 kite, flat-plate sections, 10 deg, U = 2.82 m/s") + xsecs = v3_xsecs() + area = float(cbv.v3_inviscid(40).wings[0].compute_projected_area()) + row = {"projected_area": area} + for n_chord, n_seg in ((8, 2), (16, 4)): + key = f"VLM {n_chord}x{(len(xsecs) - 1) * n_seg}" + row[key] = run_vlm(xsecs, area, 2.82, 10.0, n_chord, n_seg) + show("V3", key, row[key]) + for legs in LEGS: + for N in (40, 80): + key = f"VSM {legs} N={N}" + try: + row[key] = run_vsm(cbv.v3_inviscid, N, legs, area, 2.82) + except ValueError as err: + print(f" V3 {key}: {err}") + continue + show("V3", key, row[key]) + out["V3"] = row + + json.dump(out, open(OUT / "vlm_referee.json", "w"), indent=1) + print(f"\nresults written to {OUT / 'vlm_referee.json'}") + + +if __name__ == "__main__": + main() diff --git a/examples/TUDELFT_V3_KITE/fancy_interactive_plot.py b/examples/TUDELFT_V3_KITE/fancy_interactive_plot.py new file mode 100644 index 0000000..ab0b13a --- /dev/null +++ b/examples/TUDELFT_V3_KITE/fancy_interactive_plot.py @@ -0,0 +1,85 @@ +"""Interactive "fancy" 3D Plotly plot of the TU Delft V3 kite. + +Renders the physical inflatable structure of the LEI kite -- the leading-edge +tube, the chordwise strut tubes and the curved single-skin canopy -- reconstructed +from a raw Surfplan export, together with the VSM aerodynamic loading drawn as +chord-normal force vectors distributed over the chord by a measured Cp(x/c). + +Requirements (already handled by the data shipped in the repo): + - a Surfplan export directory: a ``.txt`` plus a ``profiles/`` directory, + - (optional) a directory of ``cp_AOA_.dat`` chordwise Cp distributions. + +SurfplanAdapter must be installed (a dependency of VSM) -- it converts the raw +export into the VSM geometry the plot needs (cached on first run). +""" + +from pathlib import Path + +from VSM.core.BodyAerodynamics import BodyAerodynamics +from VSM.plot_geometry_plotly import interactive_plot, save_high_res_render + +PROJECT_DIR = Path(__file__).resolve().parents[2] +DATA_DIR = PROJECT_DIR / "data" / "TUDELFT_V3_KITE" +RESULTS_DIR = PROJECT_DIR / "results" / "TUDELFT_V3_KITE" + +# Set True to also write a high-resolution PNG (results/.../plotly_render.png) +SAVE_HIGH_RES_PNG = True + + +def main(): + # --- inflow / operating point ------------------------------------------- + Umag = 10.0 # [m/s] + angle_of_attack = 8.0 # [deg] + side_slip = 0.0 # [deg] + + # --- build the aerodynamic model ---------------------------------------- + # The Breukels-regression CAD geometry needs no ML models, so it runs + # out of the box. Any other aero_geometry_*.yaml works too. + aero_geometry = ( + DATA_DIR / "CAD_derived_geometry" / "aero_geometry_CAD_breukels_regression.yaml" + ) + body_aero = BodyAerodynamics.instantiate( + n_panels=36, + file_path=aero_geometry, + spanwise_panel_distribution="uniform", + ) + + # --- data for the fancy plot -------------------------------------------- + # Raw Surfplan export (tubes + airfoil profiles) and the Cp database. + surfplan_dir = DATA_DIR / "Surfplan_export" + cp_distributions_dir = DATA_DIR / "cpx_distributions" + + # --- interactive fancy plot --------------------------------------------- + # Passing ``surfplan_dir`` switches on the inflatable tubes + curved canopy. + # Passing ``cp_distributions_dir`` distributes each panel's VSM force over the + # chord using the closest-AoA measured Cp(x/c). Omit either to fall back. + fig = interactive_plot( + body_aero, + vel=Umag, + angle_of_attack=angle_of_attack, + side_slip=side_slip, + title="TUDELFT_V3_KITE", + surfplan_dir=surfplan_dir, + cp_distributions_dir=cp_distributions_dir, + is_with_canopy=True, # curved canopy over the tubes (False -> tubes only) + is_with_tube_rings=False, # True -> also draw the tube construction rings + is_show=True, # open the interactive plot in a browser + # is_save=True, # uncomment to also write an interactive .html + # save_path=str(RESULTS_DIR / "fancy_plot.html"), + ) + + # --- optional: high-resolution PNG (matches the interactive view) -------- + if SAVE_HIGH_RES_PNG: + save_high_res_render( + fig, + RESULTS_DIR / "plotly_render.png", + base_size=2000, # square canvas -> undistorted; then cropped to the kite + scale=4, + show_legend=False, + show_axes=False, + show_annotations=False, + ) + + +if __name__ == "__main__": + main() diff --git a/examples/TUDELFT_V3_KITE/tutorial.py b/examples/TUDELFT_V3_KITE/tutorial.py index f5460fd..edf75a0 100644 --- a/examples/TUDELFT_V3_KITE/tutorial.py +++ b/examples/TUDELFT_V3_KITE/tutorial.py @@ -49,7 +49,7 @@ def main(): PROJECT_DIR = Path(__file__).resolve().parents[2] ### 2. defining settings - n_panels = 50 + n_panels = 100 spanwise_panel_distribution = "uniform" solver_base_version = Solver(reference_point=np.array([0.0, 0.0, 0.0])) @@ -62,28 +62,30 @@ def main(): file_path=(cad_derived_geometry_dir / "aero_geometry_CAD_CFD_polars.yaml"), spanwise_panel_distribution=spanwise_panel_distribution, ) - body_aero_CAD_CFD_polars_with_bridles = BodyAerodynamics.instantiate( - n_panels=n_panels, - # file_path=(cad_derived_geometry_dir / "config_kite_CAD_CFD_polars.yaml"), - file_path=(cad_derived_geometry_dir / "config_kite_CAD_CFD_polars.yaml"), - spanwise_panel_distribution=spanwise_panel_distribution, - bridle_path=( - cad_derived_geometry_dir / "struc_geometry_manually_adjusted.yaml" - ), - ) - body_aero_CAD_neuralfoil = BodyAerodynamics.instantiate( - n_panels=n_panels, - file_path=(cad_derived_geometry_dir / "aero_geometry_CAD_neuralfoil.yaml"), - spanwise_panel_distribution=spanwise_panel_distribution, - ) - body_aero_masure_regression = BodyAerodynamics.instantiate( - n_panels=n_panels, - file_path=( - cad_derived_geometry_dir / "aero_geometry_CAD_masure_regression.yaml" - ), - ml_models_dir=(Path(PROJECT_DIR) / "data" / "ml_models"), - spanwise_panel_distribution=spanwise_panel_distribution, - ) + # body_aero_CAD_CFD_polars_with_bridles = BodyAerodynamics.instantiate( + # n_panels=n_panels, + # file_path=( + # cad_derived_geometry_dir + # / "config_kite_CAD_CFD_polars_converged_fitted_pchip.yaml" + # ), + # spanwise_panel_distribution=spanwise_panel_distribution, + # bridle_path=( + # cad_derived_geometry_dir / "struc_geometry_manually_adjusted.yaml" + # ), + # ) + # body_aero_CAD_neuralfoil = BodyAerodynamics.instantiate( + # n_panels=n_panels, + # file_path=(cad_derived_geometry_dir / "aero_geometry_CAD_neuralfoil.yaml"), + # spanwise_panel_distribution=spanwise_panel_distribution, + # ) + # body_aero_masure_regression = BodyAerodynamics.instantiate( + # n_panels=n_panels, + # file_path=( + # cad_derived_geometry_dir / "aero_geometry_CAD_masure_regression.yaml" + # ), + # ml_models_dir=(Path(PROJECT_DIR) / "data" / "ml_models"), + # spanwise_panel_distribution=spanwise_panel_distribution, + # ) # Step 2: Set inflow conditions for each aerodynamic object """ @@ -97,40 +99,40 @@ def main(): body_aero_CAD_CFD_polars.va_initialize( Umag, angle_of_attack, side_slip, body_rates=yaw_rate ) - body_aero_CAD_CFD_polars_with_bridles.va_initialize( - Umag, - angle_of_attack, - side_slip, - body_rates=yaw_rate, - body_axis=np.array([0, 0, 1]), - ) - body_aero_CAD_neuralfoil.va_initialize( - Umag, - angle_of_attack, - side_slip, - body_rates=yaw_rate, - body_axis=np.array([0, 0, 1]), - ) - body_aero_masure_regression.va_initialize( - Umag, - angle_of_attack, - side_slip, - body_rates=yaw_rate, - body_axis=np.array([0, 0, 1]), - ) + # body_aero_CAD_CFD_polars_with_bridles.va_initialize( + # Umag, + # angle_of_attack, + # side_slip, + # body_rates=yaw_rate, + # body_axis=np.array([0, 0, 1]), + # ) + # body_aero_CAD_neuralfoil.va_initialize( + # Umag, + # angle_of_attack, + # side_slip, + # body_rates=yaw_rate, + # body_axis=np.array([0, 0, 1]), + # ) + # body_aero_masure_regression.va_initialize( + # Umag, + # angle_of_attack, + # side_slip, + # body_rates=yaw_rate, + # body_axis=np.array([0, 0, 1]), + # ) # Step 3: Plot the kite geometry using Matplotlib """ Visualize the panel mesh, control points, and aerodynamic centers for the selected BodyAerodynamics object. """ - plot_geometry( - body_aero_CAD_CFD_polars, - title="TUDELFT_V3_KITE", - data_type=".pdf", - save_path=".", - is_save=False, - is_show=True, - ) + # plot_geometry( + # body_aero_CAD_CFD_polars, + # title="TUDELFT_V3_KITE", + # data_type=".pdf", + # save_path=".", + # is_save=False, + # is_show=True, + # ) # Step 4: Create an interactive plot using Plotly """ @@ -138,7 +140,7 @@ def main(): This allows for interactive exploration of the geometry and panel arrangement. """ interactive_plot( - body_aero_CAD_CFD_polars_with_bridles, + body_aero_CAD_CFD_polars, vel=Umag, angle_of_attack=angle_of_attack, side_slip=side_slip, @@ -154,105 +156,128 @@ def main(): # / "interactive_plot.html", ) - # Step 5: Plot polar curves for different angles of attack and side slip angles, and save results + # Step 4b: "Fancy" plot -- render the inflatable leading-edge and strut tubes + # and the curved single-skin canopy from a raw Surfplan export. """ - Compare the aerodynamic performance of different models by plotting lift, drag, and side force coefficients - as a function of angle of attack (alpha sweep) and side slip (beta sweep). - Literature/CFD data can be included for validation. + Passing ``surfplan_dir`` (a directory holding the Surfplan '.txt' export + and its 'profiles/' directory) switches on the physically-detailed plot. The + export is converted with SurfplanAdapter (cached under the export directory), + and the tube diameters + airfoil top-surfaces are drawn on the wing. The plain + plot above is unaffected when ``surfplan_dir`` is omitted. """ - save_folder = Path(PROJECT_DIR) / "results" / "TUDELFT_V3_KITE" + # interactive_plot( + # body_aero_CAD_CFD_polars, + # vel=Umag, + # angle_of_attack=angle_of_attack, + # side_slip=side_slip, + # title="TUDELFT_V3_KITE (inflatable tubes + canopy)", + # surfplan_dir=Path(PROJECT_DIR) + # / "data" + # / "TUDELFT_V3_KITE" + # / "Surfplan_export", + # is_with_canopy=True, # curved canopy over the tubes; False -> tubes only + # is_with_tube_rings=False, # True -> also show tube construction rings + # ) - # Step 5a: Plot alpha sweep (angle of attack) - path_cfd_lebesque_alpha = ( - Path(PROJECT_DIR) - / "data" - / "TUDELFT_V3_KITE" - / "3D_polars_literature" - / "CFD_RANS_Rey_10e5_Poland2025_alpha_sweep_beta_0.csv" - ) - path_wt_data = ( - Path(PROJECT_DIR) - / "data" - / "TUDELFT_V3_KITE" - / "3D_polars_literature" - / "V3_CL_CD_CS_alpha_sweep_for_beta_0_WindTunnel_Poland_2025_Rey_560e4.csv" - ) - plot_polars( - solver_list=[ - solver_base_version, - solver_base_version, - solver_base_version, - solver_base_version, - ], - body_aero_list=[ - body_aero_CAD_CFD_polars, - body_aero_CAD_CFD_polars_with_bridles, - body_aero_CAD_neuralfoil, - body_aero_masure_regression, - ], - label_list=[ - "VSM CAD CFD Polars", - "VSM CAD CFD Polars with Bridles", - "VSM CAD NeuralFoil", - "VSM CAD Masure Regression", - "CFD_RANS_Rey_10e5_Poland2025_alpha_sweep_beta_0", - "Wind Tunnel Data Poland 2025", - ], - literature_path_list=[path_cfd_lebesque_alpha, path_wt_data], - angle_range=[0, 5, 8, 10, 12, 15, 20, 25], - angle_type="angle_of_attack", - angle_of_attack=0, - side_slip=0, - yaw_rate=0, - Umag=Umag, - title="alphasweep", - data_type=".pdf", - save_path=Path(save_folder), - is_save=True, - is_show=False, - ) + # # Step 5: Plot polar curves for different angles of attack and side slip angles, and save results + # """ + # Compare the aerodynamic performance of different models by plotting lift, drag, and side force coefficients + # as a function of angle of attack (alpha sweep) and side slip (beta sweep). + # Literature/CFD data can be included for validation. + # """ + # save_folder = Path(PROJECT_DIR) / "results" / "TUDELFT_V3_KITE" - # Step 5b: Plot beta sweep (side slip) - path_cfd_lebesque_beta = ( - Path(PROJECT_DIR) - / "data" - / "TUDELFT_V3_KITE" - / "3D_polars_literature" - / "CFD_RANS_Rey_10e5_Poland2025_beta_sweep_alpha_13_02.csv" - ) - plot_polars( - solver_list=[ - solver_base_version, - solver_base_version, - solver_base_version, - solver_base_version, - ], - body_aero_list=[ - body_aero_CAD_CFD_polars, - body_aero_CAD_CFD_polars_with_bridles, - body_aero_CAD_neuralfoil, - body_aero_masure_regression, - ], - label_list=[ - "VSM CAD CFD Polars", - "VSM CAD CFD Polars with Bridles", - "VSM CAD NeuralFoil", - "VSM CAD Masure Regression", - "CFD_RANS_Rey_10e5_Poland2025_beta_sweep_alpha_13_02", - ], - literature_path_list=[path_cfd_lebesque_beta], - angle_range=[0, 3, 6, 9, 12], - angle_type="side_slip", - angle_of_attack=6.8, - side_slip=0, - yaw_rate=0, - Umag=3.15, - title="betasweep", - data_type=".pdf", - save_path=Path(save_folder), - is_save=True, - is_show=True, - ) + # # Step 5a: Plot alpha sweep (angle of attack) + # path_cfd_lebesque_alpha = ( + # Path(PROJECT_DIR) + # / "data" + # / "TUDELFT_V3_KITE" + # / "3D_polars_literature" + # / "CFD_RANS_Rey_10e5_Poland2025_alpha_sweep_beta_0.csv" + # ) + # path_wt_data = ( + # Path(PROJECT_DIR) + # / "data" + # / "TUDELFT_V3_KITE" + # / "3D_polars_literature" + # / "windtunnel_alpha_sweep_beta_00_0_Poland_2025_Rey_5e5.csv" + # ) + # plot_polars( + # solver_list=[ + # solver_base_version, + # solver_base_version, + # solver_base_version, + # solver_base_version, + # ], + # body_aero_list=[ + # body_aero_CAD_CFD_polars, + # body_aero_CAD_CFD_polars_with_bridles, + # body_aero_CAD_neuralfoil, + # body_aero_masure_regression, + # ], + # label_list=[ + # "VSM CAD CFD Polars", + # "VSM CAD CFD Polars with Bridles", + # "VSM CAD NeuralFoil", + # "VSM CAD Masure Regression", + # "CFD_RANS_Rey_10e5_Poland2025_alpha_sweep_beta_0", + # "Wind Tunnel Data Poland 2025", + # ], + # literature_path_list=[path_cfd_lebesque_alpha, path_wt_data], + # angle_range=[0, 5, 8, 10, 12, 15, 20, 25], + # angle_type="angle_of_attack", + # angle_of_attack=0, + # side_slip=0, + # yaw_rate=0, + # Umag=Umag, + # title="alphasweep", + # data_type=".pdf", + # save_path=Path(save_folder), + # is_save=True, + # is_show=False, + # ) + + # # Step 5b: Plot beta sweep (side slip) + # path_cfd_lebesque_beta = ( + # Path(PROJECT_DIR) + # / "data" + # / "TUDELFT_V3_KITE" + # / "3D_polars_literature" + # / "CFD_RANS_Re1e6_beta_sweep_alpha_13_Vire2022_CorrectedByPoland2025.csv" + # ) + # plot_polars( + # solver_list=[ + # solver_base_version, + # solver_base_version, + # solver_base_version, + # solver_base_version, + # ], + # body_aero_list=[ + # body_aero_CAD_CFD_polars, + # body_aero_CAD_CFD_polars_with_bridles, + # body_aero_CAD_neuralfoil, + # body_aero_masure_regression, + # ], + # label_list=[ + # "VSM CAD CFD Polars", + # "VSM CAD CFD Polars with Bridles", + # "VSM CAD NeuralFoil", + # "VSM CAD Masure Regression", + # "CFD_RANS_Re1e6_beta_sweep_alpha_13_Vire2022_CorrectedByPoland2025", + # ], + # literature_path_list=[path_cfd_lebesque_beta], + # angle_range=[0, 3, 6, 9, 12], + # angle_type="side_slip", + # angle_of_attack=6.8, + # side_slip=0, + # yaw_rate=0, + # Umag=3.15, + # title="betasweep", + # data_type=".pdf", + # save_path=Path(save_folder), + # is_save=True, + # is_show=True, + # ) if __name__ == "__main__": diff --git a/pyproject.toml b/pyproject.toml index 0f938d5..e7eea64 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -51,6 +51,7 @@ dependencies = [ "numba", "screeninfo", "odfpy", + "SurfplanAdapter", ] [project.optional-dependencies] dev = [ diff --git a/src/VSM/core/BodyAerodynamics.py b/src/VSM/core/BodyAerodynamics.py index facc684..a4d190e 100644 --- a/src/VSM/core/BodyAerodynamics.py +++ b/src/VSM/core/BodyAerodynamics.py @@ -12,9 +12,31 @@ point_in_quad, assemble_AIC_matrices, induced_velocity_at_points, - assemble_bound_vortex_AIC, + assemble_curved_bound_correction_AIC, + perpendicular_leg_end_points, + thin_airfoil_strips, ) from . import jit_cross, jit_norm, jit_dot +from VSM.core import panel_kernels + + +def _rigid_rotation_between(points_0, points_1, dirs_0, dirs_1, rel_tol=1e-9): + """Rotation matrix R with points_1 = R points_0 + t and dirs_1 = R dirs_0 + (Kabsch), or None when the two point sets are not the same rigid body + to within ``rel_tol`` of the geometry's size.""" + c0 = points_0.mean(axis=0) + c1 = points_1.mean(axis=0) + q0 = points_0 - c0 + q1 = points_1 - c1 + scale = max(float(np.abs(q0).max()), 1e-300) + u, _, vt = np.linalg.svd(q0.T @ q1) + d = np.sign(np.linalg.det(vt.T @ u.T)) + R = vt.T @ np.diag([1.0, 1.0, d]) @ u.T + if np.abs(q0 @ R.T - q1).max() > rel_tol * scale: + return None + if np.abs(dirs_0 @ R.T - dirs_1).max() > rel_tol: + return None + return R class BodyAerodynamics: @@ -59,10 +81,45 @@ def __init__( bridle_line_system: list = None, aerodynamic_center_location: float = 0.25, control_point_location: float = 0.75, + leg_direction: str = "rib", ): + """ + Args: + wings (list): Wing instances. + bridle_line_system (list, optional): bridle lines as [p1, p2, diameter]. + aerodynamic_center_location (float): chord fraction of the bound vortex. + control_point_location (float): chord fraction of the control point. + leg_direction (str): direction of the attached-trailed legs, the + chordwise vortex segments between the bound vortex and the + trailing edge. ``"rib"`` (default): along the (interpolated) + input sections, ending at their trailing-edge points; a fixed + vortex system, independent of the inflow. ``"streamwise"``: + along the mean apparent velocity projected onto the local + surface, up to the trailing-edge curve, rebuilt whenever ``va`` + is set; depends only on the quarter-chord line, the trailing + edge and the flow, not on how the input sections were drawn, + and follows the inflow in sideslip, but with a spanwise-varying + inflow (body rates, distributed ``va``) it uses the mean + direction only. ``"perpendicular"``: + perpendicular to the local quarter-chord line, up to the + trailing-edge curve, as in Gaunaa, Li & Pirrung (TORQUE 2026, + Sec. 3), with the control point between the panel's two legs; + see ``utils.perpendicular_leg_end_points``. Streamwise and rib + legs coincide on wings whose ribs are drawn along the flow. + The panel frame + and the chord length are the span-perpendicular ones (CP1) in + both cases; with ``"rib"`` the control point lies on the + interpolated rib at the control-point chord fraction. + """ + if leg_direction not in ("rib", "perpendicular", "streamwise"): + raise ValueError( + "leg_direction must be 'rib', 'perpendicular' or 'streamwise', " + f"got {leg_direction!r}" + ) self._wings = wings self._aerodynamic_center_location = aerodynamic_center_location self._control_point_location = control_point_location + self._leg_direction = leg_direction self._bridle_line_system = bridle_line_system self._reference_point = np.zeros(3, dtype=float) @@ -85,6 +142,7 @@ def __init__( def _build_panels(self): """Helper method to build the panel list from the current wing geometry.""" panels = [] + wing_panel_groups = [] for wing in self.wings: section_list = wing.refine_aerodynamic_mesh() n_panels_per_wing = len(section_list) - 1 @@ -104,8 +162,9 @@ def _build_panels(self): self._aerodynamic_center_location, self._control_point_location, ) + wing_panels = [] for j in range(n_panels_per_wing): - panels.append( + wing_panels.append( Panel( section_list[j], section_list[j + 1], @@ -118,9 +177,91 @@ def _build_panels(self): z_airf_list[j], ) ) + if self._leg_direction == "perpendicular": + self._apply_perpendicular_legs(wing_panels, section_list) + wing_panel_groups.append((wing_panels, section_list)) + panels.extend(wing_panels) + self._wing_panel_groups = wing_panel_groups self._panels = panels + # after a rebuild (rotate, update_from_points) streamwise legs must + # follow the inflow already set; before the first va they are ribs + va_distribution = getattr(self, "_va_distribution", None) + if self._leg_direction == "streamwise" and va_distribution is not None: + if len(va_distribution) == len(panels): + self._apply_streamwise_legs(va_distribution) + else: # panel count changed: only the mean direction is meaningful + self._apply_streamwise_legs(np.mean(va_distribution, axis=0, keepdims=True)) self._n_panels = len(panels) + def _apply_perpendicular_legs(self, wing_panels, section_list, direction=None): + """Replace the rib legs of one wing by legs perpendicular to the + quarter-chord line (or along ``direction``, projected onto the + surface) that end on the trailing-edge curve.""" + bound_nodes = np.array( + [p.bound_point_1 for p in wing_panels] + [wing_panels[-1].bound_point_2], + dtype=float, + ) + te_nodes = np.array([sec.TE_point for sec in section_list], dtype=float) + normals = None + if direction is not None: + normals = np.array([p.x_airf for p in wing_panels], dtype=float) + ends = perpendicular_leg_end_points( + bound_nodes, + te_nodes, + np.array([p.y_airf for p in wing_panels], dtype=float), + np.array([p.chord for p in wing_panels], dtype=float), + float(self._aerodynamic_center_location), + direction=direction, + normals=normals, + ) + # The control point goes between the panel's own two legs, at the + # control-point fraction along them, with the same spanwise weights + # that place the aerodynamic centre on the bound segment. Between + # parallel perpendicular legs this is the 3/4-chord point of the CP1 + # section (the paper's control point); where the legs fan out near a + # kink or a tip it stays between them, as it does between the rib + # legs in the default construction. + t_leg = (self._control_point_location - self._aerodynamic_center_location) / ( + 1.0 - self._aerodynamic_center_location + ) + for j, panel in enumerate(wing_panels): + b1, b2 = panel.bound_point_1, panel.bound_point_2 + w1 = np.linalg.norm(panel.aerodynamic_center - b2) / np.linalg.norm(b1 - b2) + control_point = w1 * (b1 + t_leg * (ends[j] - b1)) + (1.0 - w1) * ( + b2 + t_leg * (ends[j + 1] - b2) + ) + panel.set_leg_end_points(ends[j], ends[j + 1], control_point) + + def _apply_streamwise_legs(self, va_distribution): + """Streamwise legs: along the mean apparent velocity, projected onto + the local surface, up to the trailing-edge curve. Called whenever + ``va`` is set, since the legs follow the inflow. + + An inflow without a usable mean direction (pure rotation, a flow + that does not run from the bound vortex towards the trailing edge) + has no streamwise legs; the rib legs are then used and a warning is + logged, so that such a case still solves.""" + va = np.atleast_2d(np.asarray(va_distribution, dtype=float)) + va_mean = np.mean(va, axis=0) + norm = np.linalg.norm(va_mean) + speed = float(np.mean(np.linalg.norm(va, axis=1))) + try: + if speed <= 0.0 or norm < 1e-3 * speed: + raise ValueError("the mean apparent velocity is zero (pure rotation?)") + for wing_panels, section_list in self._wing_panel_groups: + self._apply_perpendicular_legs(wing_panels, section_list, direction=va_mean / norm) + except ValueError as err: + logging.warning( + "streamwise attached-trailed legs cannot be built for this inflow " + f"({err}); using the rib legs for this inflow" + ) + for panel in self._panels: + panel.reset_legs_to_ribs() + + @property + def leg_direction(self): + return self._leg_direction + #################### ## CLASS METHODS ### #################### @@ -136,6 +277,7 @@ def instantiate( ml_models_dir=None, aerodynamic_center_location: float = 0.25, control_point_location: float = 0.75, + leg_direction: str = "rib", ): """ Instantiate a BodyAerodynamics object from either a provided wing_instance or a YAML config file. @@ -150,6 +292,7 @@ def instantiate( center / bound vortex (default: 0.25). control_point_location (float): Chord fraction (from LE) of the control point (default: 0.75). + leg_direction (str): ``"rib"`` (default), ``"streamwise"`` or ``"perpendicular"``, see ``BodyAerodynamics.__init__``. Returns: BodyAerodynamics instance. @@ -347,12 +490,14 @@ def instantiate( bridle_lines, aerodynamic_center_location=aerodynamic_center_location, control_point_location=control_point_location, + leg_direction=leg_direction, ) else: return cls( [wing_instance], aerodynamic_center_location=aerodynamic_center_location, control_point_location=control_point_location, + leg_direction=leg_direction, ) def rotate( @@ -561,6 +706,11 @@ def va( va_distribution += -np.cross(self._body_rates, control_points - r0) # push to panels + # kept for a rebuild of the panels (rotate, update_from_points) + self._va_distribution = np.array(va_distribution, dtype=float) + if self._leg_direction == "streamwise": + self._apply_streamwise_legs(va_distribution) + for i, panel in enumerate(self.panels): panel.va = va_distribution[i] @@ -1149,17 +1299,17 @@ def compute_trefftz_plane_induced_drag(self, gamma, rho): ).astype(float) d_gamma = np.concatenate(([gamma[0]], gamma[1:] - gamma[:-1], [-gamma[-1]])) q = origins - np.outer(origins @ e_inf, e_inf) # projected onto the plane - drag = 0.0 - for i in range(n): - mid = 0.5 * (q[i] + q[i + 1]) - d = mid - q - r2 = np.sum(d * d, axis=1) - v_t = np.sum( - (d_gamma / (2.0 * np.pi * r2))[:, None] * np.cross(e_inf, d), axis=0 - ) - # trace segment in the sense of the bound vortex (section i+1 -> i) - drag += gamma[i] * np.dot(np.cross(v_t, q[i] - q[i + 1]), e_inf) - return 0.5 * rho * drag + # velocity of all wake vortices k at every trace-segment midpoint i + mid = 0.5 * (q[:-1] + q[1:]) # (n, 3) + d = mid[:, None, :] - q[None, :, :] # (n, n+1, 3) + r2 = np.sum(d * d, axis=2) + v_t = np.sum( + (d_gamma[None, :] / (2.0 * np.pi * r2))[:, :, None] * np.cross(e_inf, d), + axis=1, + ) + # trace segment in the sense of the bound vortex (section i+1 -> i) + drag = np.sum(gamma[:n] * (np.cross(v_t, q[:-1] - q[1:]) @ e_inf)) + return 0.5 * rho * float(drag) def compute_attached_trailed_vortex_forces( self, gamma, rho, core_radius_fraction, va_array @@ -1182,11 +1332,7 @@ def compute_attached_trailed_vortex_forces( (forces, points): ``(n+1, 3)`` force vectors and the points they act at. """ panels = self.panels - n = len(panels) gamma = np.asarray(gamma, dtype=float).ravel() - gamma_net = np.zeros(n + 1) - gamma_net[:n] += gamma # leg bound_1 -> TE_1 of panel j sits on boundary j - gamma_net[1:] -= gamma # leg TE_2 -> bound_2 of panel j sits on boundary j+1 bound_point_1 = np.ascontiguousarray( [p.bound_point_1 for p in panels], dtype=float @@ -1206,13 +1352,7 @@ def compute_attached_trailed_vortex_forces( t = (cp - ac) / (1.0 - ac) points = np.ascontiguousarray(bound + t * legs) - # inflow at a boundary: mean of the neighbouring panels' inflow va_array = np.asarray(va_array, dtype=float) - va_boundary = np.zeros((n + 1, 3)) - va_boundary[:n] += va_array - va_boundary[1:] += va_array - va_boundary[1:n] *= 0.5 - va_speeds = np.linalg.norm(va_array, axis=1) wake_units, wake_speeds = self._wake_directions( va_speeds, va_array / va_speeds[:, None] @@ -1228,8 +1368,9 @@ def compute_attached_trailed_vortex_forces( wake_speeds, float(core_radius_fraction), ) - v_rel = va_boundary + v_ind - forces = rho * gamma_net[:, None] * np.cross(v_rel, legs) + # net leg circulation and boundary inflow (mean of the neighbouring + # panels') are formed in the kernel + forces = panel_kernels.attached_trailed_forces(gamma, va_array, v_ind, legs, rho) return forces, points def compute_results( @@ -1267,10 +1408,6 @@ def compute_results( cl_array[icp] = panel_i.compute_cl(alpha_array[icp]) cd_array[icp], cm_array[icp] = panel_i.compute_cd_cm(alpha_array[icp]) panel_width_array[icp] = panel_i.width - lift = (cl_array * 0.5 * rho * Umag_array**2 * chord_array)[:, np.newaxis] - drag = (cd_array * 0.5 * rho * Umag_array**2 * chord_array)[:, np.newaxis] - moment = (cm_array * 0.5 * rho * Umag_array**2 * chord_array**2)[:, np.newaxis] - # Quarter-chord force directions (TAT3) apply to the VSM model, whose # circulation loop evaluates at 3/4 chord. The LLT model already # evaluates on the quarter chord without any bound-vortex influence @@ -1291,6 +1428,28 @@ def compute_results( alpha_corrected = alpha_array[:, np.newaxis] alpha_uncorrected = alpha_array[:, np.newaxis] + # Section force law (VSM.core.panel_kernels): magnitudes from the polar + # at the 3/4-chord angle and section speed, directions from + # alpha_corrected (the quarter-chord angle, or the 3/4-chord one). + lift_2D_array, drag_2D_array, moment_2D_array, dir_flow_array = ( + panel_kernels.section_force_vectors( + alpha_array, + Umag_array, + cl_array, + cd_array, + cm_array, + chord_array, + x_airf_array, + y_airf_array, + z_airf_array, + rho, + alpha_dir=np.ravel(alpha_corrected), + ) + ) + moment_local_3D_array = panel_kernels.panel_moment_local( + moment_2D_array, panel_width_array, z_airf_array + ) + # Initializing variables cl_prescribed_va_list = [] cd_prescribed_va_list = [] @@ -1347,45 +1506,19 @@ def compute_results( ### Defining panel_variables # Defining directions of airfoil that defines current panel_i z_airf_span = panel_i.z_airf # along the span - y_airf_chord = panel_i.y_airf # along the chord - x_airf_normal_to_chord = panel_i.x_airf # normal to the chord - # TODO: implement these - alpha_corrected_i = alpha_corrected[i] panel_chord = panel_i.chord panel_width = panel_i.width panel_area = panel_chord * panel_width area_all_panels += panel_area - ### Calculate the direction of the induced apparent wind speed to the airfoil orientation - # this is done using the CORRECTED CALCULATED (comes from gamma distribution) angle of attack - # For VSM the correction is applied, and it is the angle of attack, from calculating induced velocities at the 1/4c aerodynamic center location - # For LTT the correction is NOT applied, and it is the angle of attack, from calculating induced velocities at the 3/4c control point - induced_va_airfoil = ( - np.cos(alpha_corrected_i) * y_airf_chord - + np.sin(alpha_corrected_i) * x_airf_normal_to_chord - ) - dir_induced_va_airfoil = induced_va_airfoil / jit_norm(induced_va_airfoil) - - ### Calculate the direction of the lift and drag vectors - # lift is perpendical/normal to induced apparent wind speed - dir_lift_induced_va = jit_cross(dir_induced_va_airfoil, z_airf_span) - dir_lift_induced_va = dir_lift_induced_va / jit_norm(dir_lift_induced_va) - # drag is parallel/tangential to induced apparent wind speed - # (TAT3: the local inner flow direction, which lies in the panel's - # span-perpendicular plane; the global span axis is NOT used here) - dir_drag_induced_va = dir_induced_va_airfoil - - ### Calculating the MAGNITUDE of the lift and drag - # The VSM and LTT methods do NOT differ here, both use the uncorrected angle of attack - # i.e. evaluate the magnitude at the (3/4c) control point - # 2D AIRFOIL aerodynamic forces, so multiplied by chord - lift_induced_va_mag = lift[i] - drag_induced_va_mag = drag[i] - - # panel force VECTOR NORMAL to CALCULATED induced velocity - lift_induced_va = lift_induced_va_mag * dir_lift_induced_va - # panel force VECTOR TANGENTIAL to CALCULATED induced velocity - drag_induced_va = drag_induced_va_mag * dir_drag_induced_va + ### Section force vectors (per unit span) from the force law above: + # lift normal and drag along the direction-defining flow + # dir_induced_va_airfoil (TAT3: the local inner flow direction, in + # the panel's span-perpendicular plane; the global span axis is NOT + # used), magnitudes at the 3/4c control point for VSM and LLT alike. + dir_induced_va_airfoil = dir_flow_array[i] + lift_induced_va = lift_2D_array[i] + drag_induced_va = drag_2D_array[i] ftotal_induced_va = lift_induced_va + drag_induced_va ### Converting forces to prescribed wing va @@ -1540,13 +1673,10 @@ def compute_results( # (1) Panel aerodynamic center in global frame: panel_ac_global = panel_i.aerodynamic_center # 3D [x, y, z] - # (2) Convert local (2D) pitching moment to a 3D vector in global coords. - # Use the axis around which the moment is defined, - # which is the z-axis pointing "spanwise" - moment_axis_global = panel_i.z_airf - - # Scale by panel width if your 'moment[i]' is 2D moment-per-unit-span: - M_local_3D = moment[i] * moment_axis_global * panel_i.width + # (2) Local (2D) pitching moment as a 3D vector in global coords: + # along the z-axis pointing "spanwise", times the width + # (panel_kernels.panel_moment_local). + M_local_3D = moment_local_3D_array[i] # (3) Force application point used for r x F: # use aerodynamic center (1/4c) for all models. @@ -1788,7 +1918,7 @@ def compute_results( ( "cm_panel_dist", np.divide( - moment[:, 0], + moment_2D_array, 0.5 * rho * Umag_array**2 * chord_array**2, out=np.zeros_like(chord_array), where=(Umag_array > 0.0) & (chord_array > 0.0), @@ -1959,37 +2089,75 @@ def va_initialize( rates_in_body_frame=rates_in_body_frame, ) - def update_effective_angle_of_attack_if_VSM( - self, - gamma, - core_radius_fraction, - x_airf_array, - y_airf_array, - va_array, - va_norm_array, - va_unit_array, + def _curved_bound_correction_cached( + self, eval_points, bp1, bp2, te1, te2, chord_dir, chords, va_norm, core_radius_fraction ): - """Updates the angle of attack at the aerodynamic center of each panel, - Calculated at the AERODYNAMIC CENTER, which needs an update for VSM - And can just use the old value for the LLT + """The curved bound-vortex correction (3, N, N) for the current panels, + from the cache when the panels are the cached ones up to a rigid + rotation and translation, otherwise recomputed and stored.""" + ac = float(self._aerodynamic_center_location) + points = np.vstack([eval_points, bp1, bp2, te1, te2]) + cache = getattr(self, "_curved_bound_cache", None) + if ( + cache is not None + and cache["ac"] == ac + and cache["core"] == float(core_radius_fraction) + and cache["points"].shape == points.shape + and np.allclose(cache["chords"], chords, rtol=0.0, atol=1e-12) + ): + R = _rigid_rotation_between(cache["points"], points, cache["chord_dir"], chord_dir) + if R is not None: + if np.allclose(R, np.eye(3), atol=1e-14): + return cache["AIC"] + return np.ascontiguousarray(np.einsum("ab,bij->aij", R, cache["AIC"])) + strip_x, strip_w = thin_airfoil_strips() + AIC_corr = assemble_curved_bound_correction_AIC( + eval_points, bp1, bp2, te1, te2, chord_dir, chords, va_norm, + ac, float(core_radius_fraction), strip_x, strip_w, + ) + self._curved_bound_cache = { + "ac": ac, + "core": float(core_radius_fraction), + "points": points.copy(), + "chord_dir": np.array(chord_dir, dtype=float), + "chords": np.array(chords, dtype=float), + "AIC": AIC_corr, + } + return AIC_corr + + def compute_quarter_chord_AIC_matrices( + self, core_radius_fraction, va_norm_array, va_unit_array + ): + """Induction matrices of the force-direction flow at the panel + quarter chords (aerodynamic centres): ``v_qc = va + AIC_qc gamma``. + + The direction-defining flow is the undisturbed relative flow at the + quarter chord (TAT3 in Gaunaa, Li & Pirrung, TORQUE 2026). The LLT + AIC holds the freestream-independent trailed system (legs + wake) + evaluated on the quarter chord, which is Prandtl's exact downwash for + a straight line but misses the influence of a curved or swept bound + vortex. Evaluating the other panels' bound segments on the line itself + is logarithmically singular (Li et al. 2020, "LL-P&S"), so the + curvature/sweep/skew influence is added as the chord-regularised + difference between the actual and the locally unrolled vortex + systems; see assemble_curved_bound_correction_AIC. + + The matrices depend on the geometry and the wake (``va_norm_array``, + ``va_unit_array``, as in :meth:`compute_AIC_matrices`), not on the + circulation, so a caller can freeze them numerically, e.g. as + parameters of a CasADi graph (see ``VSM.core.panel_kernels``). Args: - None + core_radius_fraction (float): Vortex core radius fraction. + va_norm_array (np.ndarray): (n,) panel inflow speeds [m/s]. + va_unit_array (np.ndarray): (n, 3) panel inflow directions. Returns: - None + Tuple[np.ndarray, np.ndarray, np.ndarray]: the x, y and z + components, each (n, n): ``AIC_LLT + AIC_curved_correction``. """ - # The direction-defining flow is the undisturbed relative flow at the - # quarter chord (TAT3 in Gaunaa, Li & Pirrung, TORQUE 2026): freestream - # plus every trailed vortex plus the bound vortices of the OTHER - # panels (the curved/swept bound-vortex influence of Li et al. 2020). - # The LLT AIC drops all bound vortices, which is exact only for a - # straight lifting line; the other panels' bound segments are added - # back here. The panel's own straight bound segment induces nothing on - # its own quarter-chord point. - aerodynamic_model_type = "LLT" AIC_x, AIC_y, AIC_z = self.compute_AIC_matrices( - aerodynamic_model_type, core_radius_fraction, va_norm_array, va_unit_array + "LLT", core_radius_fraction, va_norm_array, va_unit_array ) eval_points = np.ascontiguousarray( [panel.aerodynamic_center for panel in self.panels], dtype=float @@ -2000,24 +2168,58 @@ def update_effective_angle_of_attack_if_VSM( bound_point_2 = np.ascontiguousarray( [panel.bound_point_2 for panel in self.panels], dtype=float ) - AIC_bound = assemble_bound_vortex_AIC( - eval_points, bound_point_1, bound_point_2, float(core_radius_fraction) + TE_point_1 = np.ascontiguousarray( + [panel.TE_point_1 for panel in self.panels], dtype=float ) - AIC_x = AIC_x + AIC_bound[0] - AIC_y = AIC_y + AIC_bound[1] - AIC_z = AIC_z + AIC_bound[2] - induced_velocity_all = np.array( - [ - np.matmul(AIC_x, gamma), - np.matmul(AIC_y, gamma), - np.matmul(AIC_z, gamma), - ] - ).T - relative_velocity_array = va_array + induced_velocity_all - v_normal_array = np.sum(x_airf_array * relative_velocity_array, axis=1) - v_tangential_array = np.sum(y_airf_array * relative_velocity_array, axis=1) - alpha_array = np.arctan2(v_normal_array, v_tangential_array) + TE_point_2 = np.ascontiguousarray( + [panel.TE_point_2 for panel in self.panels], dtype=float + ) + chords = np.ascontiguousarray([panel.chord for panel in self.panels], dtype=float) + chord_dir = np.ascontiguousarray( + [panel.y_airf for panel in self.panels], dtype=float + ) + # The correction depends on the panel geometry only (the wake terms + # cancel between the actual and the unrolled systems, and the leg core + # radius has no measurable effect), so it is computed once per shape + # and reused for every inflow and circulation. A trim that rotates + # the body for each residual evaluation keeps the shape: the stored + # matrix is then rotated instead of rebuilt (its entries are velocity + # vectors, so they rotate with the geometry and ignore translations). + # The kernel costs about 0.5 s at 50 panels; a coupled trim solves + # one deformed shape hundreds of times. + AIC_corr = self._curved_bound_correction_cached( + eval_points, bound_point_1, bound_point_2, TE_point_1, TE_point_2, + chord_dir, chords, np.ascontiguousarray(va_norm_array, dtype=float).ravel(), + float(core_radius_fraction), + ) + return AIC_x + AIC_corr[0], AIC_y + AIC_corr[1], AIC_z + AIC_corr[2] + def update_effective_angle_of_attack_if_VSM( + self, + gamma, + core_radius_fraction, + x_airf_array, + y_airf_array, + va_array, + va_norm_array, + va_unit_array, + ): + """Force-direction angle of each panel: the angle of the relative flow + at the quarter chord, ``va + AIC_qc gamma`` with the matrices of + :meth:`compute_quarter_chord_AIC_matrices`. + + Returns: + np.ndarray: (n, 1) angles [rad]. + """ + AIC_x, AIC_y, AIC_z = self.compute_quarter_chord_AIC_matrices( + core_radius_fraction, va_norm_array, va_unit_array + ) + relative_velocity_array = panel_kernels.quarter_chord_velocity( + va_array, gamma, AIC_x, AIC_y, AIC_z + ) + alpha_array = panel_kernels.direction_angle( + relative_velocity_array, x_airf_array, y_airf_array + ) return alpha_array[:, np.newaxis] def compute_line_aerodynamic_force( diff --git a/src/VSM/core/Panel.py b/src/VSM/core/Panel.py index c71f4fe..7aca671 100644 --- a/src/VSM/core/Panel.py +++ b/src/VSM/core/Panel.py @@ -92,12 +92,49 @@ def __init__( # Conceptually, you should mulitply by the width of the bound vortex and thus take the average width. self._width = jit_norm(bound_point_2 - bound_point_1) + # rib legs and control point as built from the sections, kept so that + # another leg construction can be undone (set_leg_end_points) + self._rib_TE_point_1 = np.array(self._TE_point_1, dtype=float) + self._rib_TE_point_2 = np.array(self._TE_point_2, dtype=float) + self._rib_control_point = np.array(control_point, dtype=float) + ### Setting up the filaments (order used to reversed for right-to-left input) self._filaments = [] self._filaments.append(BoundFilament(x1=bound_point_2, x2=bound_point_1)) self._filaments.append(BoundFilament(x1=bound_point_1, x2=self._TE_point_1)) self._filaments.append(BoundFilament(x1=self._TE_point_2, x2=bound_point_2)) + def set_leg_end_points(self, te_point_1, te_point_2, control_point=None): + """Move the ends of the two attached-trailed legs, and with them the + start of the wake, to the given points on the trailing-edge curve, + and optionally the control point. + + The section corners, chord and frame are kept. Used when the legs are + built perpendicular to the quarter-chord line instead of along the + input ribs (``BodyAerodynamics(..., leg_direction="perpendicular")``); + the control point then moves with the legs so that it stays between + the panel's own two legs. + """ + self._TE_point_1 = np.asarray(te_point_1, dtype=float) + self._TE_point_2 = np.asarray(te_point_2, dtype=float) + if control_point is not None: + self._control_point = np.asarray(control_point, dtype=float) + self._rebuild_filaments() + + def reset_legs_to_ribs(self): + """Restore the rib legs and the section control point.""" + self._TE_point_1 = self._rib_TE_point_1.copy() + self._TE_point_2 = self._rib_TE_point_2.copy() + self._control_point = self._rib_control_point.copy() + self._rebuild_filaments() + + def _rebuild_filaments(self): + self._filaments = [ + BoundFilament(x1=self._bound_point_2, x2=self._bound_point_1), + BoundFilament(x1=self._bound_point_1, x2=self._TE_point_1), + BoundFilament(x1=self._TE_point_2, x2=self._bound_point_2), + ] + ########################### ## GETTER FUNCTIONS ########################### diff --git a/src/VSM/core/Solver.py b/src/VSM/core/Solver.py index 8b7457e..bb03468 100644 --- a/src/VSM/core/Solver.py +++ b/src/VSM/core/Solver.py @@ -3,8 +3,29 @@ import logging from scipy.linalg import solve_banded from . import jit_cross +from VSM.core import panel_kernels + +def _table_symbols(n_panels: int, alpha_tables) -> dict: + """Symbolic ``(n_panels, n_alpha)`` polar tables for ``table_inputs``.""" + import casadi as ca + + n_alpha = int(np.asarray(alpha_tables[0]).size) + return {name: ca.MX.sym(f"{name}_table", n_panels, n_alpha) for name in ("cl", "cd", "cm")} + + +def _with_table_inputs(inputs: list, input_names: list, table_syms): + """Append the table inputs (if any) and default them to NaN, so a call + that omits them (CasADi fills omitted inputs with 0 by default) gives NaN + rather than a silently zero polar.""" + if table_syms is None: + return inputs, input_names, {} + inputs = inputs + [table_syms[k] for k in ("cl", "cd", "cm")] + input_names = input_names + ["cl_table", "cd_table", "cm_table"] + defaults = [0.0] * (len(inputs) - 3) + [float("nan")] * 3 + return inputs, input_names, {"default_in": defaults} + class Solver: """Solver for aerodynamic circulation distribution and force computation. @@ -201,6 +222,8 @@ def __init__( # rejected and the step quartered (0.5 = residual may at most double). self._newton_reject_ratio = 0.5 self._casadi_newton_cache: dict = {} + self._casadi_newton_loop_cache: dict = {} + self._casadi_table_input_cache: dict = {} # polar tables as inputs #: Diagnostic: did the last casadi_newton solve fall back to base? self.last_fallback = False #: Diagnostic: residual evaluations used by the last casadi_newton solve. @@ -1028,56 +1051,91 @@ def _casadi_newton_key(self, alpha_tables, cl_tables, cd_tables, cm_tables) -> t digest.hexdigest(), ) - def _build_casadi_newton_function(self, alpha_tables, cl_tables, cd_tables=None, cm_tables=None): - """Build the CasADi function of the PER-PANEL section physics that - :meth:`gamma_loop_casadi_newton` assembles its residual and exact - Jacobian from. - - The circulation residual whose root the Newton loop finds is - - R(gamma) = (I - diag(mu(alpha)) L) gamma - G_raw(gamma), - - exactly the fixed point the ``base`` and ``anderson`` loops iterate - towards (``gamma = (I - diag(mu) L)^-1 G_raw(gamma)``, see - :meth:`_regularize_gamma_target`), including the Li/Gaunaa viscosity - ``mu_i = max(0, -k S Cl'_i / dz_i^2)`` gated on any panel being past - its stall onset (``mu = 0`` without artificial viscosity). Everything - that is not the dense induction ``v_ind = AIC gamma`` is local to a - panel: ``G_raw_i``, ``mu_i`` and ``alpha_i`` depend on ``gamma`` only - through the panel's own relative velocity ``v_rel_i``. So the symbolic - function takes ``v_rel`` (and ``L gamma``) as INPUTS and returns - - h_i = G_raw_i + mu_i (L gamma)_i, c_i = dh_i / dv_rel_i (3 values) - - plus ``mu``, ``alpha`` and ``|v_rel x z|``; the Newton loop then forms - ``R = gamma - h`` and, by the chain rule through ``v_rel = va + AIC - gamma`` (with ``dh_i/d(L gamma)_i = mu_i``), + def _casadi_table_input_key(self, alpha_tables) -> tuple: + """Cache key of the functions that take the polar tables as INPUTS + (``table_inputs=True``): the graph depends on the panel count, the + shared alpha grid, the interpolation and the regularization settings, + not on the table values, so one function serves every shape whose + blended polars change but keep the grid.""" + self._check_table_inputs_supported(alpha_tables) + grid = np.ascontiguousarray(alpha_tables[0], dtype=float) + return ( + "table_inputs", + self.n_panels, + bool(self.is_with_artificial_viscosity), + float(self.artificial_viscosity_factor), + self.polar_interpolation, + hashlib.blake2b(grid.tobytes(), digest_size=16).hexdigest(), + ) - J = I - diag(mu) L - sum_k diag(c_k) AIC_k + def _check_table_inputs_supported(self, alpha_tables) -> None: + if self.polar_interpolation != "linear": + # A parametric cubic spline refits its coefficients on every call + # (~0.1 s per evaluation), which defeats the purpose. + raise ValueError( + "Polar tables as CasADi inputs need polar_interpolation='linear' " + f"(got {self.polar_interpolation!r})." + ) + if not all( + table.shape == alpha_tables[0].shape and np.array_equal(table, alpha_tables[0]) + for table in alpha_tables[1:] + ): + raise ValueError( + "Polar tables as CasADi inputs need every panel polar on one " + "shared alpha grid." + ) - in NumPy. This keeps the CasADi graph O(n) -- independent of the - n x n induction matrices -- so it costs ~0.1 ms per evaluation and - builds in tens of milliseconds, while the dense algebra runs in BLAS. + def casadi_polar_table_inputs(self) -> dict: + """The current panels' polar tables in the layout of the ``table_inputs`` + CasADi functions (:meth:`_casadi_newton_function` and + :meth:`casadi_panel_force_function` with ``table_inputs=True``): + ``{"cl_table", "cd_table", "cm_table"}``, each ``(n_panels, n_alpha)``, + row ``i`` = panel ``i``'s coefficients on the shared alpha grid + (``panel.panel_polar_data[:, 0]``, radians). + + Pass them on every call, next to the geometry: a deformed shape blends + its section polars with new weights, so the values change while the + grid, and with it the function, stays the same. + """ + alpha_tables, cl_tables, cd_tables, cm_tables = self._polar_tables() + self._check_table_inputs_supported(alpha_tables) + return { + "cl_table": np.vstack(cl_tables), + "cd_table": np.vstack(cd_tables), + "cm_table": np.vstack(cm_tables), + } - The 2-D polars are ``ca.interpolant`` tables. Queries are clamped to - the table range first, so the lookup reproduces ``np.interp`` (constant - beyond both ends) and the lift slope is the same central difference of - the same piecewise-linear table that :meth:`_lift_slope_from_ctx` - evaluates. When every panel shares one alpha grid (the normal outcome - of batch polar generation) the lookup is a single 2-D interpolant over - (alpha, panel index) evaluated exactly at the integer panel nodes; - otherwise one 1-D interpolant per panel. ``polar_interpolation = - "bspline"`` swaps the piecewise-linear table for a cubic spline - through the same nodes (degree 1 along the panel-index axis, so panel - rows never mix). + def casadi_stall_angle_inputs(self) -> dict: + """``{"stall_angles", "stall_angles_neg"}`` [rad] of the current panel + polars, the gate inputs of the residual function as + :meth:`gamma_loop_casadi_newton` passes them (plus / minus infinity + without artificial viscosity). They are derived from the tables, so a + caller passing new tables passes these new values with them.""" + if self.is_with_artificial_viscosity: + return { + "stall_angles": self._panel_stall_angles(), + "stall_angles_neg": self._panel_negative_stall_angles(), + } + return { + "stall_angles": np.full(self.n_panels, np.inf), + "stall_angles_neg": np.full(self.n_panels, -np.inf), + } - Outputs, in order: ``h``, ``dh/dv_rel`` (n x 3, from the corner- - averaged polar), ``mu``, ``alpha``, ``umag``, ``gate``, then for - callers that build FORCES from the same lookups: ``h_avg`` (h with the - corner-averaged Cl -- differentiate this for a kink-tolerant - Jacobian), ``cl``, ``cd``, ``cm`` at ``alpha`` and their corner- - averaged counterparts ``cl_avg``, ``cd_avg``, ``cm_avg``. With - "bspline" polars the averaged outputs equal the exact ones. + def _casadi_polar_lookups( + self, alpha_tables, cl_tables, cd_tables=None, cm_tables=None, table_symbols=None + ): + """Clamped CasADi lookups ``alpha (n) -> cl, cd, cm (n)`` of the + per-panel polars, shared by :meth:`_build_casadi_newton_function` and + :meth:`_build_casadi_panel_force_function` (see the former for the + interpolation). Returns ``(cl_of, cd_of, cm_of, smooth)``, ``smooth`` + True for "bspline" polars. + + ``table_symbols`` (``{"cl": MX, "cd": MX, "cm": MX}``, each + ``(n, n_alpha)``) makes the table VALUES symbolic inputs instead of + constants: one 1-D parametric linear interpolant on the shared grid, + mapped over the panels. It reproduces the constant table exactly + (same nodes, same clamping); only the shared-grid linear case is + supported (see :meth:`_check_table_inputs_supported`). """ import casadi as ca @@ -1089,16 +1147,6 @@ def _build_casadi_newton_function(self, alpha_tables, cl_tables, cd_tables=None, f"(panel {i} violates this)." ) - v_rel = ca.MX.sym("v_rel", n, 3) - x_airf = ca.MX.sym("x_airf", n, 3) - y_airf = ca.MX.sym("y_airf", n, 3) - z_airf = ca.MX.sym("z_airf", n, 3) - chord = ca.MX.sym("chord", n) - width = ca.MX.sym("width", n) - stall_angles = ca.MX.sym("stall_angles", n) - stall_angles_neg = ca.MX.sym("stall_angles_neg", n) - l_gamma = ca.MX.sym("l_gamma", n) # (L gamma), a parameter here - shared_grid = all( table.shape == alpha_tables[0].shape and np.array_equal(table, alpha_tables[0]) @@ -1111,6 +1159,33 @@ def _build_casadi_newton_function(self, alpha_tables, cl_tables, cd_tables=None, kind = self.polar_interpolation smooth = kind == "bspline" + if table_symbols is not None: + self._check_table_inputs_supported(alpha_tables) + grid = np.asarray(alpha_tables[0], dtype=float) + lo, hi = float(grid[0]), float(grid[-1]) + row_interpolant = ca.interpolant("polar_row", "linear", [grid], 1) + alpha_row = ca.MX.sym("alpha_row") + values_row = ca.MX.sym("values_row", grid.size) + # One panel's lookup; .map(n) evaluates it column-wise over panels. + row_lookup = ca.Function( + "polar_row_lookup", + [alpha_row, values_row], + [row_interpolant(ca.fmin(ca.fmax(alpha_row, lo), hi), values_row)], + ).map(n) + + def make_parametric_lookup(table): + def lookup(alpha_vec): + return row_lookup(alpha_vec.T, table.T).T + + return lookup + + return ( + make_parametric_lookup(table_symbols["cl"]), + make_parametric_lookup(table_symbols["cd"]), + make_parametric_lookup(table_symbols["cm"]), + smooth, + ) + def make_lookup(name, value_tables): """Clamped lookup ``alpha_vec (n) -> values (n)`` of one polar column.""" if shared_grid: @@ -1154,6 +1229,83 @@ def lookup(alpha_vec): cl_of = make_lookup("cl", cl_tables) cd_of = make_lookup("cd", cd_tables) cm_of = make_lookup("cm", cm_tables) + return cl_of, cd_of, cm_of, smooth + + def _build_casadi_newton_function( + self, alpha_tables, cl_tables, cd_tables=None, cm_tables=None, table_inputs=False + ): + """Build the CasADi function of the PER-PANEL section physics that + :meth:`gamma_loop_casadi_newton` assembles its residual and exact + Jacobian from. + + The circulation residual whose root the Newton loop finds is + + R(gamma) = (I - diag(mu(alpha)) L) gamma - G_raw(gamma), + + exactly the fixed point the ``base`` and ``anderson`` loops iterate + towards (``gamma = (I - diag(mu) L)^-1 G_raw(gamma)``, see + :meth:`_regularize_gamma_target`), including the Li/Gaunaa viscosity + ``mu_i = max(0, -k S Cl'_i / dz_i^2)`` gated on any panel being past + its stall onset (``mu = 0`` without artificial viscosity). Everything + that is not the dense induction ``v_ind = AIC gamma`` is local to a + panel: ``G_raw_i``, ``mu_i`` and ``alpha_i`` depend on ``gamma`` only + through the panel's own relative velocity ``v_rel_i``. So the symbolic + function takes ``v_rel`` (and ``L gamma``) as INPUTS and returns + + h_i = G_raw_i + mu_i (L gamma)_i, c_i = dh_i / dv_rel_i (3 values) + + plus ``mu``, ``alpha`` and ``|v_rel x z|``; the Newton loop then forms + ``R = gamma - h`` and, by the chain rule through ``v_rel = va + AIC + gamma`` (with ``dh_i/d(L gamma)_i = mu_i``), + + J = I - diag(mu) L - sum_k diag(c_k) AIC_k + + in NumPy. This keeps the CasADi graph O(n) -- independent of the + n x n induction matrices -- so it costs ~0.1 ms per evaluation and + builds in tens of milliseconds, while the dense algebra runs in BLAS. + + The 2-D polars are ``ca.interpolant`` tables. Queries are clamped to + the table range first, so the lookup reproduces ``np.interp`` (constant + beyond both ends) and the lift slope is the same central difference of + the same piecewise-linear table that :meth:`_lift_slope_from_ctx` + evaluates. When every panel shares one alpha grid (the normal outcome + of batch polar generation) the lookup is a single 2-D interpolant over + (alpha, panel index) evaluated exactly at the integer panel nodes; + otherwise one 1-D interpolant per panel. ``polar_interpolation = + "bspline"`` swaps the piecewise-linear table for a cubic spline + through the same nodes (degree 1 along the panel-index axis, so panel + rows never mix). + + Outputs, in order: ``h``, ``dh/dv_rel`` (n x 3, from the corner- + averaged polar), ``mu``, ``alpha``, ``umag``, ``gate``, then for + callers that build FORCES from the same lookups: ``h_avg`` (h with the + corner-averaged Cl -- differentiate this for a kink-tolerant + Jacobian), ``cl``, ``cd``, ``cm`` at ``alpha`` and their corner- + averaged counterparts ``cl_avg``, ``cd_avg``, ``cm_avg``. With + "bspline" polars the averaged outputs equal the exact ones. + + ``table_inputs=True`` appends the inputs ``cl_table``, ``cd_table``, + ``cm_table`` (each ``(n, n_alpha)``, :meth:`casadi_polar_table_inputs`) + and takes the table values from them instead of embedding them; see + :meth:`_casadi_newton_function`. + """ + import casadi as ca + + n = self.n_panels + table_syms = _table_symbols(n, alpha_tables) if table_inputs else None + cl_of, cd_of, cm_of, smooth = self._casadi_polar_lookups( + alpha_tables, cl_tables, cd_tables, cm_tables, table_symbols=table_syms + ) + + v_rel = ca.MX.sym("v_rel", n, 3) + x_airf = ca.MX.sym("x_airf", n, 3) + y_airf = ca.MX.sym("y_airf", n, 3) + z_airf = ca.MX.sym("z_airf", n, 3) + chord = ca.MX.sym("chord", n) + width = ca.MX.sym("width", n) + stall_angles = ca.MX.sym("stall_angles", n) + stall_angles_neg = ca.MX.sym("stall_angles_neg", n) + l_gamma = ca.MX.sym("l_gamma", n) # (L gamma), a parameter here def cross_rows(a, b): return ca.horzcat( @@ -1227,9 +1379,12 @@ def averaged(lookup): seeds = [ca.DM(np.eye(3)[k][None, :].repeat(n, axis=0)) for k in range(3)] dh_dvrel = ca.horzcat(*[ca.jtimes(h_for_jacobian, v_rel, seed) for seed in seeds]) + inputs = [v_rel, x_airf, y_airf, z_airf, chord, width, stall_angles, stall_angles_neg, l_gamma] + input_names = ["v_rel", "x_airf", "y_airf", "z_airf", "chord", "width", "stall_angles", "stall_angles_neg", "l_gamma"] + inputs, input_names, options = _with_table_inputs(inputs, input_names, table_syms) return ca.Function( - "vsm_panel_residual", - [v_rel, x_airf, y_airf, z_airf, chord, width, stall_angles, stall_angles_neg, l_gamma], + "vsm_panel_residual_tables" if table_inputs else "vsm_panel_residual", + inputs, [ h, dh_dvrel, @@ -1245,11 +1400,29 @@ def averaged(lookup): averaged(cd_of), averaged(cm_of), ], - ["v_rel", "x_airf", "y_airf", "z_airf", "chord", "width", "stall_angles", "stall_angles_neg", "l_gamma"], + input_names, ["h", "dh_dvrel", "mu", "alpha", "umag", "gate", "h_avg", "cl", "cd", "cm", "cl_avg", "cd_avg", "cm_avg"], + options, ) - def _casadi_newton_function(self): + def _casadi_newton_function(self, table_inputs: bool = False): + """The per-panel residual function (:meth:`_build_casadi_newton_function`) + of the current panels, cached. + + ``table_inputs=False`` (default, what :meth:`gamma_loop_casadi_newton` + uses): the polar tables are constants of the graph, cached per table + set. ``table_inputs=True``: the tables are the extra inputs + ``cl_table, cd_table, cm_table``, cached per alpha grid, so a caller + that re-trims deformed shapes builds the function once and passes + :meth:`casadi_polar_table_inputs` (and :meth:`casadi_stall_angle_inputs`) + per shape. Same outputs to round-off; needs ``polar_interpolation= + "linear"`` and one shared alpha grid. Omitted table inputs default to + NaN, so a call that forgets them fails loudly instead of using zeros. + """ + if table_inputs: + return self._cached_table_input_function( + "newton", self._build_casadi_newton_function + ) alpha_tables, cl_tables, cd_tables, cm_tables = self._polar_tables() key = self._casadi_newton_key(alpha_tables, cl_tables, cd_tables, cm_tables) fn = self._casadi_newton_cache.get(key) @@ -1264,6 +1437,138 @@ def _casadi_newton_function(self): self._casadi_newton_cache[key] = fn return fn + def _build_casadi_panel_force_function( + self, alpha_tables, cl_tables, cd_tables=None, cm_tables=None, table_inputs=False + ): + """CasADi function of the per-panel section forces, the force law of + ``compute_results`` (``VSM.core.panel_kernels.panel_forces``) on the + polar lookups of the circulation solve. See + :meth:`casadi_panel_force_function`.""" + import casadi as ca + + n = self.n_panels + table_syms = _table_symbols(n, alpha_tables) if table_inputs else None + cl_of, cd_of, cm_of, _ = self._casadi_polar_lookups( + alpha_tables, cl_tables, cd_tables, cm_tables, table_symbols=table_syms + ) + v_rel = ca.MX.sym("v_rel", n, 3) + v_qc = ca.MX.sym("v_qc", n, 3) + x_airf = ca.MX.sym("x_airf", n, 3) + y_airf = ca.MX.sym("y_airf", n, 3) + z_airf = ca.MX.sym("z_airf", n, 3) + chord = ca.MX.sym("chord", n) + width = ca.MX.sym("width", n) + rho = ca.MX.sym("rho") + + alpha, umag = panel_kernels.relative_flow(v_rel, x_airf, y_airf, z_airf, xp=ca) + alpha_dir = panel_kernels.direction_angle(v_qc, x_airf, y_airf, xp=ca) + cl, cd, cm = cl_of(alpha), cd_of(alpha), cm_of(alpha) + force, moment_local = panel_kernels.panel_forces( + alpha, umag, cl, cd, cm, chord, width, x_airf, y_airf, z_airf, rho, + alpha_dir=alpha_dir, xp=ca, + ) + inputs, input_names, options = _with_table_inputs( + [v_rel, v_qc, x_airf, y_airf, z_airf, chord, width, rho], + ["v_rel", "v_qc", "x_airf", "y_airf", "z_airf", "chord", "width", "rho"], + table_syms, + ) + return ca.Function( + "vsm_panel_forces_tables" if table_inputs else "vsm_panel_forces", + inputs, + [force, moment_local, alpha, alpha_dir, umag, cl, cd, cm], + input_names, + ["force", "moment_local", "alpha", "alpha_dir", "umag", "cl", "cd", "cm"], + options, + ) + + def casadi_panel_force_function(self, table_inputs: bool = False): + """CasADi function of the per-panel section forces of the current + panels (the ones of the last :meth:`solve`), cached per polar set like + the ``casadi_newton`` residual function. + + Inputs (numeric or symbolic), ``n = n_panels``: + + * ``v_rel`` (n, 3): relative velocity at the 3/4-chord control points, + ``va + AIC gamma`` -- sets ``alpha``, ``umag`` and so the polar + magnitudes ``cl, cd, cm``; + * ``v_qc`` (n, 3): the force-direction flow. For the quarter-chord law + (``is_aoa_corrected=True``) ``va + AIC_qc gamma`` with + ``BodyAerodynamics.compute_quarter_chord_AIC_matrices`` (or + ``panel_kernels.quarter_chord_velocity``); for ``False`` pass + ``v_rel`` again; + * ``x_airf, y_airf, z_airf`` (n, 3), ``chord, width`` (n), ``rho``. + + Outputs: ``force`` (n, 3) [N], ``moment_local`` (n, 3) [Nm] (the + quarter-chord pitching moment along ``z_airf``), ``alpha``, + ``alpha_dir`` [rad], ``umag`` [m/s], ``cl, cd, cm`` (n). + + Same as ``compute_results`` per panel WITHOUT the attached-trailed + vortex force (``panel_kernels.attached_trailed_forces``) and the + viscous drag correction. Taking ``v_qc`` rather than the matrices keeps + the graph O(n); the caller forms the dense product. + + ``table_inputs=True`` appends ``cl_table, cd_table, cm_table`` as + inputs (see :meth:`_casadi_newton_function`). + """ + import casadi # noqa: F401 (clear error when the optional dep is missing) + + if table_inputs: + return self._cached_table_input_function( + "panel_forces", self._build_casadi_panel_force_function + ) + alpha_tables, cl_tables, cd_tables, cm_tables = self._polar_tables() + key = ("panel_forces",) + self._casadi_newton_key( + alpha_tables, cl_tables, cd_tables, cm_tables + ) + fn = self._casadi_newton_cache.get(key) + if fn is None: + fn = self._build_casadi_panel_force_function( + alpha_tables, cl_tables, cd_tables, cm_tables + ) + if len(self._casadi_newton_cache) >= 32: + self._casadi_newton_cache.pop(next(iter(self._casadi_newton_cache))) + self._casadi_newton_cache[key] = fn + return fn + + def _cached_table_input_function(self, kind: str, builder): + """Function of ``kind`` with the polar tables as inputs, built once + per (panel count, alpha grid, settings) and then reused for any + table values.""" + alpha_tables, cl_tables, cd_tables, cm_tables = self._polar_tables() + key = (kind,) + self._casadi_table_input_key(alpha_tables) + fn = self._casadi_table_input_cache.get(key) + if fn is None: + fn = builder(alpha_tables, cl_tables, cd_tables, cm_tables, table_inputs=True) + if len(self._casadi_table_input_cache) >= 32: + self._casadi_table_input_cache.pop(next(iter(self._casadi_table_input_cache))) + self._casadi_table_input_cache[key] = fn + return fn + + def _casadi_newton_loop_function(self): + """The residual function restricted to the six outputs the Newton loop + reads (``h, dh_dvrel, mu, alpha, umag, gate``): the same graph, minus + the force-side lookups it would otherwise evaluate on every call. + Cached per polar set alongside the full function.""" + import casadi as ca + + alpha_tables, cl_tables, cd_tables, cm_tables = self._polar_tables() + key = self._casadi_newton_key(alpha_tables, cl_tables, cd_tables, cm_tables) + loop_fn = self._casadi_newton_loop_cache.get(key) + if loop_fn is None: + fn = self._casadi_newton_function() + inputs = fn.mx_in() + loop_fn = ca.Function( + "vsm_panel_residual_loop", + inputs, + fn.call(inputs)[:6], + fn.name_in(), + fn.name_out()[:6], + ) + if len(self._casadi_newton_loop_cache) >= 32: + self._casadi_newton_loop_cache.pop(next(iter(self._casadi_newton_loop_cache))) + self._casadi_newton_loop_cache[key] = loop_fn + return loop_fn + def gamma_loop_casadi_newton(self, gamma_initial: np.ndarray) -> tuple: """Newton solve of the circulation residual with an exact CasADi Jacobian, globalised by pseudo-transient continuation @@ -1312,7 +1617,9 @@ def gamma_loop_casadi_newton(self, gamma_initial: np.ndarray) -> tuple: fallback to decide. """ try: - fn = self._casadi_newton_function() + import casadi as ca + + fn = self._casadi_newton_loop_function() except ImportError as exc: # pragma: no cover - depends on optional dep raise ImportError( "gamma_loop_type='casadi_newton' requires the optional 'casadi' " @@ -1329,14 +1636,19 @@ def gamma_loop_casadi_newton(self, gamma_initial: np.ndarray) -> tuple: stall_angles = np.full(n, np.inf) stall_angles_neg = np.full(n, -np.inf) laplacian = np.zeros((n, n)) - params = ( - self.x_airf_array, - self.y_airf_array, - self.z_airf_array, - self.chord_array, - self.width_array, - stall_angles, - stall_angles_neg, + # Constant within the solve: converted to CasADi matrices once, which + # halves the cost of each residual evaluation. + params = tuple( + ca.DM(np.asarray(value, dtype=float)) + for value in ( + self.x_airf_array, + self.y_airf_array, + self.z_airf_array, + self.chord_array, + self.width_array, + stall_angles, + stall_angles_neg, + ) ) aic = (self.AIC_x, self.AIC_y, self.AIC_z) @@ -1345,7 +1657,7 @@ def evaluate(x): # v_rel = va + AIC gamma (see _build_casadi_newton_function). v_rel = self.va_array + np.column_stack([a @ x for a in aic]) l_gamma = laplacian @ x - h, dh_dvrel, mu, alpha, umag, gate = fn(v_rel, *params, l_gamma)[:6] + h, dh_dvrel, mu, alpha, umag, gate = fn(v_rel, *params, l_gamma) h = np.asarray(h, dtype=float).ravel() dh_dvrel = np.asarray(dh_dvrel, dtype=float) mu = np.asarray(mu, dtype=float).ravel() @@ -1444,6 +1756,10 @@ def finite(state): trial = evaluate(current["x"] + direction) evaluations += 1 ratio = np.inf if finite(trial) and norm(trial) == 0 else (norm(current) / norm(trial) if finite(trial) else 0.0) + crossed_gate = finite(trial) and trial["gate"] != current["gate"] + if not finite(trial) or ( + ratio < self._newton_reject_ratio + and not (crossed_gate and dt <= dt_restart) ): # Residual grew too much: reject and shrink. EXCEPT across # the viscosity gate, the residual's one discontinuity: diff --git a/src/VSM/core/WingGeometry.py b/src/VSM/core/WingGeometry.py index ecc56c4..81fb1fd 100644 --- a/src/VSM/core/WingGeometry.py +++ b/src/VSM/core/WingGeometry.py @@ -627,22 +627,18 @@ def compute_projected_area(self, z_plane_vector=np.array([0.0, 0.0, 1.0])): """ sections = self.sections n = np.asarray(z_plane_vector, dtype=float) - n /= np.linalg.norm(n) - - def proj(P): - # orthogonal projection onto plane with normal n - return P - np.dot(P, n) * n - - S = 0.0 - for i in range(len(sections) - 1): - A = proj(sections[i].LE_point) - B = proj(sections[i].TE_point) - C = proj(sections[i + 1].TE_point) - D = proj(sections[i + 1].LE_point) - # two triangles: (A,B,C) and (A,C,D) - S += 0.5 * np.linalg.norm(np.cross(B - A, C - A)) - S += 0.5 * np.linalg.norm(np.cross(C - A, D - A)) - return S + n = n / np.linalg.norm(n) # not in place: never modify the caller's array + + LE = np.array([section.LE_point for section in sections], dtype=float) + TE = np.array([section.TE_point for section in sections], dtype=float) + # orthogonal projection onto the plane with normal n + LE = LE - np.outer(LE @ n, n) + TE = TE - np.outer(TE @ n, n) + A, B, C, D = LE[:-1], TE[:-1], TE[1:], LE[1:] + # two triangles per strip: (A,B,C) and (A,C,D) + area_ABC = 0.5 * np.linalg.norm(np.cross(B - A, C - A), axis=1) + area_ACD = 0.5 * np.linalg.norm(np.cross(C - A, D - A), axis=1) + return float(np.sum(np.column_stack([area_ABC, area_ACD]))) def compute_flat_area(self) -> float: """Calculate the total flat (undeformed) wing area in 3D space. diff --git a/src/VSM/core/panel_kernels.py b/src/VSM/core/panel_kernels.py new file mode 100644 index 0000000..f074f61 --- /dev/null +++ b/src/VSM/core/panel_kernels.py @@ -0,0 +1,236 @@ +"""Section force law of the VSM as ``xp`` kernels. + +Pure functions over a math namespace ``xp``, either ``numpy`` (the default) +or ``casadi``, so the same formula serves ``BodyAerodynamics.compute_results`` +and a CasADi graph (``Solver.casadi_panel_force_function``, or a caller's own +graph such as AWETrim's CasADi trim). The formula lives here only; callers do +not restate it. + +Conventions: + +* Per-panel vectors are rows of an ``(n, 3)`` array; per-panel scalars are + ``(n,)`` arrays in NumPy and ``(n, 1)`` columns in CasADi. +* ``x_airf`` is the chord normal, ``y_airf`` the chord (leading to trailing + edge), ``z_airf`` the span direction of each panel (``Panel`` axes). +* Angles are in radians. ``alpha = atan2(v . x_airf, v . y_airf)``. +* ``umag = |v_rel x z_airf|`` is the speed the 2-D section sees; the dynamic + pressure of the section force is ``0.5 rho umag^2``, not the freestream one. + +The force law (Gaunaa, Li & Pirrung, TORQUE 2026): the MAGNITUDES of lift, +drag and moment come from the polar at the 3/4-chord control-point angle +``alpha``; the DIRECTIONS come from the direction angle ``alpha_dir``. With +``is_aoa_corrected=True`` (TAT3, VSM model) ``alpha_dir`` is the angle of the +flow at the quarter chord, ``va + AIC_qc gamma`` with the matrices of +``BodyAerodynamics.compute_quarter_chord_AIC_matrices``; with ``False`` it is +``alpha`` itself. +""" + +from __future__ import annotations + +from typing import Any + +import numpy as np + + +# --------------------------------------------------------------------- xp rows +def _is_casadi(xp: Any) -> bool: + return hasattr(xp, "horzcat") + + +def hstack_columns(xp: Any, *cols): + """``(n, k)`` from ``k`` per-row columns.""" + if _is_casadi(xp): + return xp.horzcat(*cols) + return np.column_stack(cols) + + +def cross_rows(a, b, xp=np): + """Row-wise cross product of two ``(n, 3)`` arrays.""" + return hstack_columns( + xp, + a[:, 1] * b[:, 2] - a[:, 2] * b[:, 1], + a[:, 2] * b[:, 0] - a[:, 0] * b[:, 2], + a[:, 0] * b[:, 1] - a[:, 1] * b[:, 0], + ) + + +def dot_rows(a, b, xp=np): + """Row-wise dot product of two ``(n, 3)`` arrays -> per-row scalar.""" + if _is_casadi(xp): + return xp.sum2(a * b) + return np.sum(a * b, axis=1) + + +def norm_rows(a, xp=np): + return xp.sqrt(dot_rows(a, a, xp)) + + +def scale_rows(s, a, xp=np): + """Per-row scalar ``s`` times the ``(n, 3)`` rows of ``a``.""" + if _is_casadi(xp): + return xp.repmat(s, 1, a.shape[1]) * a + return np.asarray(s)[:, None] * a + + +def unit_rows(a, xp=np): + """Rows of ``a`` divided by their norms.""" + norms = norm_rows(a, xp) + if _is_casadi(xp): + return a / xp.repmat(norms, 1, a.shape[1]) + return a / norms[:, None] + + +def _atan2(y, x, xp): + return xp.atan2(y, x) if _is_casadi(xp) else np.arctan2(y, x) + + +def _matvec(matrix, vector, xp): + if _is_casadi(xp): + return xp.mtimes(matrix, vector) + return matrix @ vector + + +# ------------------------------------------------------------------ section flow +def relative_flow(v_rel, x_airf, y_airf, z_airf, xp=np): + """Angle of attack and section speed of the relative velocity ``v_rel`` + (``Solver.compute_aerodynamic_quantities``): + ``alpha = atan2(v_rel . x_airf, v_rel . y_airf)``, ``umag = |v_rel x z_airf|``.""" + alpha = direction_angle(v_rel, x_airf, y_airf, xp) + umag = norm_rows(cross_rows(v_rel, z_airf, xp), xp) + return alpha, umag + + +def quarter_chord_velocity(va, gamma, AIC_x, AIC_y, AIC_z, xp=np): + """Relative flow at the panel quarter chords, ``va + AIC_qc gamma``. + + ``AIC_*`` are the ``(n, n)`` quarter-chord induction matrices of + ``BodyAerodynamics.compute_quarter_chord_AIC_matrices`` (trailed system on + the quarter chord plus the curved bound-vortex correction), ``va`` the + ``(n, 3)`` panel inflow and ``gamma`` the ``(n,)`` circulation. + """ + induced = hstack_columns( + xp, + _matvec(AIC_x, gamma, xp), + _matvec(AIC_y, gamma, xp), + _matvec(AIC_z, gamma, xp), + ) + return va + induced + + +def direction_angle(v, x_airf, y_airf, xp=np): + """Angle of the flow ``v`` (``(n, 3)``) in each panel's section plane, + ``atan2(v . x_airf, v . y_airf)``. Applied to the quarter-chord flow it is + the TAT3 direction angle of the force law.""" + return _atan2(dot_rows(x_airf, v, xp), dot_rows(y_airf, v, xp), xp) + + +# ------------------------------------------------------------------- force law +def section_force_directions(alpha_dir, x_airf, y_airf, z_airf, xp=np): + """Unit flow and lift directions of each section at direction angle + ``alpha_dir``: ``dir_flow = unit(cos(a) y_airf + sin(a) x_airf)`` (in the + plane normal to ``z_airf``; drag acts along it, TAT3's local inner flow, + not the global span axis) and ``dir_lift = unit(dir_flow x z_airf)``.""" + dir_flow = scale_rows(xp.cos(alpha_dir), y_airf, xp) + scale_rows( + xp.sin(alpha_dir), x_airf, xp + ) + dir_flow = unit_rows(dir_flow, xp) + dir_lift = unit_rows(cross_rows(dir_flow, z_airf, xp), xp) + return dir_flow, dir_lift + + +def section_force_magnitudes(umag, cl, cd, cm, chord, rho): + """2-D lift, drag [N/m] and pitching moment [Nm/m] about the quarter + chord, ``c_x 0.5 rho umag^2 chord`` (``chord^2`` for the moment). The + coefficients are the polar values at the 3/4-chord angle.""" + lift = cl * 0.5 * rho * umag**2 * chord + drag = cd * 0.5 * rho * umag**2 * chord + moment = cm * 0.5 * rho * umag**2 * chord**2 + return lift, drag, moment + + +def section_force_vectors( + alpha, umag, cl, cd, cm, chord, x_airf, y_airf, z_airf, rho, alpha_dir=None, xp=np +): + """2-D (per unit span) lift and drag vectors and the flow direction. + + ``alpha_dir`` None uses ``alpha`` (the ``is_aoa_corrected=False`` law). + Returns ``(lift_2d (n, 3), drag_2d (n, 3), moment (n,), dir_flow (n, 3))``. + """ + if alpha_dir is None: + alpha_dir = alpha + lift, drag, moment = section_force_magnitudes(umag, cl, cd, cm, chord, rho) + dir_flow, dir_lift = section_force_directions(alpha_dir, x_airf, y_airf, z_airf, xp) + return scale_rows(lift, dir_lift, xp), scale_rows(drag, dir_flow, xp), moment, dir_flow + + +def panel_forces( + alpha, + umag, + cl, + cd, + cm, + chord, + width, + x_airf, + y_airf, + z_airf, + rho, + alpha_dir=None, + xp=np, +): + """Per-panel 3-D force [N] and local pitching moment [Nm]. + + ``force = width (lift_2d + drag_2d)`` (see :func:`section_force_vectors`); + ``moment_local = width (cm q chord^2) z_airf``, the quarter-chord moment as + a vector along the span axis. ``alpha_dir`` None gives the 3/4-chord + directions (``is_aoa_corrected=False``); pass :func:`direction_angle` of + :func:`quarter_chord_velocity` for the quarter-chord law (``True``). + + Not included: the attached-trailed vortex force + (:func:`attached_trailed_forces`) and the viscous drag correction + (``BodyAerodynamics.viscous_drag_correction``, NumPy only). + + Returns ``(force (n, 3), moment_local (n, 3))``. + """ + lift_2d, drag_2d, moment, _ = section_force_vectors( + alpha, umag, cl, cd, cm, chord, x_airf, y_airf, z_airf, rho, alpha_dir, xp + ) + force = scale_rows(width, lift_2d + drag_2d, xp) + return force, panel_moment_local(moment, width, z_airf, xp) + + +def panel_moment_local(moment, width, z_airf, xp=np): + """3-D local pitching moment [Nm] of each panel: the 2-D quarter-chord + moment [Nm/m] (:func:`section_force_magnitudes`) as a vector along the + span axis ``z_airf``, times the panel width. ``(n, 3)``.""" + return scale_rows(width, scale_rows(moment, z_airf, xp), xp) + + +# ---------------------------------------------------- attached trailed vortices +def attached_trailed_forces(gamma, va_panels, v_induced, legs, rho, xp=np): + """Kutta-Joukowski force on the attached trailed (AT) legs (Gaunaa, Li & + Pirrung 2026, Sec. 3), ``rho (gamma_j - gamma_{j-1}) (v_rel_j x l_j)`` on + boundary ``j = 0..n`` with ``gamma_{-1} = gamma_n = 0``. + + ``v_rel_j`` is the mean of the neighbouring panels' inflow ``va_panels`` + (the tip boundaries take their one panel's) plus ``v_induced``, the full + vortex system's induction at the AT force points, ``(n+1, 3)``. ``legs`` + are the ``(n+1, 3)`` leg vectors, bound vortex to trailing edge. Returns + the ``(n+1, 3)`` boundary forces. + """ + n = va_panels.shape[0] + to_boundary = np.zeros((n + 1, n)) # gamma_net = to_boundary @ gamma + to_boundary[np.arange(n), np.arange(n)] += 1.0 + to_boundary[np.arange(1, n + 1), np.arange(n)] -= 1.0 + # boundary inflow = mean of the neighbouring panels (halving is exact) + mean = np.abs(to_boundary) + mean[1:n] *= 0.5 + if _is_casadi(xp): + gamma_net = xp.mtimes(xp.DM(to_boundary), gamma) + va_boundary = xp.mtimes(xp.DM(mean), va_panels) + if isinstance(legs, np.ndarray): + legs = xp.DM(legs) + else: + gamma_net = to_boundary @ np.asarray(gamma, dtype=float).ravel() + va_boundary = mean @ va_panels + return scale_rows(rho * gamma_net, cross_rows(va_boundary + v_induced, legs, xp), xp) diff --git a/src/VSM/core/utils.py b/src/VSM/core/utils.py index 5db59c7..10c3630 100644 --- a/src/VSM/core/utils.py +++ b/src/VSM/core/utils.py @@ -30,156 +30,210 @@ def jit_dot(a, b): # round-off; Filament.py remains the readable reference implementation. # The only intentional difference: the bound-vortex in-core branch cannot # emit the logging.info diagnostic from compiled code. +# +# Vectors are carried as 3-tuples of floats, not length-3 arrays: numba +# keeps tuples on the stack, whereas every small-array temporary (np.cross, +# r1 - r0, np.zeros(3)) is a heap allocation, and those allocations were +# ~95% of the kernel cost (~1.1 us -> ~25 ns per filament evaluation). +# error_model="numpy" keeps NumPy's inf/nan semantics for a zero +# denominator (numba's default would raise ZeroDivisionError on scalars), +# so degenerate geometry behaves as it did with the array kernels. # ===================================================================== _ALPHA0_OSEEN = 1.25643 # Oseen parameter (Filament._alpha0) _NU_AIR = 1.48e-5 # kinematic viscosity of air [m^2/s] (Filament._nu) +_ZERO3 = (0.0, 0.0, 0.0) + + +@jit(nopython=True, cache=True, error_model="numpy") +def _row(a, i): + """Row ``i`` of an (n, 3) array as a 3-tuple.""" + return (a[i, 0], a[i, 1], a[i, 2]) + + +@jit(nopython=True, cache=True, error_model="numpy") +def _add(a, b): + return (a[0] + b[0], a[1] + b[1], a[2] + b[2]) + + +@jit(nopython=True, cache=True, error_model="numpy") +def _sub(a, b): + return (a[0] - b[0], a[1] - b[1], a[2] - b[2]) + + +@jit(nopython=True, cache=True, error_model="numpy") +def _scale(s, a): + return (s * a[0], s * a[1], s * a[2]) + + +@jit(nopython=True, cache=True, error_model="numpy") +def _div(a, s): + return (a[0] / s, a[1] / s, a[2] / s) + + +@jit(nopython=True, cache=True, error_model="numpy") +def _dot(a, b): + return a[0] * b[0] + a[1] * b[1] + a[2] * b[2] -@jit(nopython=True, cache=True) + +@jit(nopython=True, cache=True, error_model="numpy") +def _cross(a, b): + return ( + a[1] * b[2] - a[2] * b[1], + a[2] * b[0] - a[0] * b[2], + a[0] * b[1] - a[1] * b[0], + ) + + +@jit(nopython=True, cache=True, error_model="numpy") +def _norm(a): + return np.sqrt(a[0] * a[0] + a[1] * a[1] + a[2] * a[2]) + + +@jit(nopython=True, cache=True, error_model="numpy") def _vel_bound_vortex(XV1, XV2, XVP, gamma, core_radius_fraction): """Bound-filament induced velocity (Vatistas core), as in Filament.velocity_3D_bound_vortex.""" - r0 = XV2 - XV1 - r1 = XVP - XV1 - r2 = XVP - XV2 + r0 = _sub(XV2, XV1) + r1 = _sub(XVP, XV1) + r2 = _sub(XVP, XV2) - r1Xr0 = np.cross(r1, r0) - epsilon = core_radius_fraction * np.linalg.norm(r0) - dist = np.linalg.norm(r1Xr0) / np.linalg.norm(r0) + r1Xr0 = _cross(r1, r0) + epsilon = core_radius_fraction * _norm(r0) + dist = _norm(r1Xr0) / _norm(r0) if dist > epsilon: - r1Xr2 = np.cross(r1, r2) - return ( + r1Xr2 = _cross(r1, r2) + return _scale( gamma / (4 * np.pi) - * r1Xr2 - / (np.linalg.norm(r1Xr2) ** 2) - * np.dot(r0, r1 / np.linalg.norm(r1) - r2 / np.linalg.norm(r2)) + / (_norm(r1Xr2) ** 2) + * _dot(r0, _sub(_div(r1, _norm(r1)), _div(r2, _norm(r2)))), + r1Xr2, ) elif dist < 1e-12 * epsilon: - return np.zeros(3) + return _ZERO3 else: - r1_radial = r1 - np.dot(r1, r0) * r0 / (np.linalg.norm(r0) ** 2) - r2_radial = r2 - np.dot(r2, r0) * r0 / (np.linalg.norm(r0) ** 2) - r1_proj = np.dot(r1, r0) * r0 / ( - np.linalg.norm(r0) ** 2 - ) + epsilon * r1_radial / np.linalg.norm(r1_radial) - r2_proj = np.dot(r2, r0) * r0 / ( - np.linalg.norm(r0) ** 2 - ) + epsilon * r2_radial / np.linalg.norm(r2_radial) - r1Xr2_proj = np.cross(r1_proj, r2_proj) - vel_ind_proj = ( + r0_sq = _norm(r0) ** 2 + r1_axial = _div(_scale(_dot(r1, r0), r0), r0_sq) + r2_axial = _div(_scale(_dot(r2, r0), r0), r0_sq) + r1_radial = _sub(r1, r1_axial) + r2_radial = _sub(r2, r2_axial) + r1_proj = _add(r1_axial, _div(_scale(epsilon, r1_radial), _norm(r1_radial))) + r2_proj = _add(r2_axial, _div(_scale(epsilon, r2_radial), _norm(r2_radial))) + r1Xr2_proj = _cross(r1_proj, r2_proj) + vel_ind_proj = _scale( gamma / (4 * np.pi) - * r1Xr2_proj - / (np.linalg.norm(r1Xr2_proj) ** 2) - * np.dot( + / (_norm(r1Xr2_proj) ** 2) + * _dot( r0, - r1_proj / np.linalg.norm(r1_proj) - r2_proj / np.linalg.norm(r2_proj), - ) + _sub(_div(r1_proj, _norm(r1_proj)), _div(r2_proj, _norm(r2_proj))), + ), + r1Xr2_proj, ) - return dist / epsilon * vel_ind_proj + return _scale(dist / epsilon, vel_ind_proj) -@jit(nopython=True, cache=True) +@jit(nopython=True, cache=True, error_model="numpy") def _vel_trailing_vortex(XV1, XV2, XVP, gamma, Uinf): """Finite trailing-leg induced velocity (viscous core), as in Filament.velocity_3D_trailing_vortex.""" - r0 = XV2 - XV1 - r1 = XVP - XV1 - r2 = XVP - XV2 + r0 = _sub(XV2, XV1) + r1 = _sub(XVP, XV1) + r2 = _sub(XVP, XV2) - r_perp = r1 - np.dot(r1, r0) * r0 / (np.linalg.norm(r0) ** 2) - epsilon = np.sqrt(4 * _ALPHA0_OSEEN * _NU_AIR * np.linalg.norm(r_perp) / Uinf) + r0_sq = _norm(r0) ** 2 + r_perp = _sub(r1, _div(_scale(_dot(r1, r0), r0), r0_sq)) + epsilon = np.sqrt(4 * _ALPHA0_OSEEN * _NU_AIR * _norm(r_perp) / Uinf) - r1Xr0 = np.cross(r1, r0) - dist = np.linalg.norm(r1Xr0) / np.linalg.norm(r0) + r1Xr0 = _cross(r1, r0) + dist = _norm(r1Xr0) / _norm(r0) if dist > epsilon: - r1Xr2 = np.cross(r1, r2) - return ( + r1Xr2 = _cross(r1, r2) + return _scale( gamma / (4 * np.pi) - * r1Xr2 - / (np.linalg.norm(r1Xr2) ** 2) - * np.dot(r0, r1 / np.linalg.norm(r1) - r2 / np.linalg.norm(r2)) + / (_norm(r1Xr2) ** 2) + * _dot(r0, _sub(_div(r1, _norm(r1)), _div(r2, _norm(r2)))), + r1Xr2, ) elif dist < 1e-12 * epsilon: - return np.zeros(3) + return _ZERO3 else: - r1_radial = r1 - np.dot(r1, r0) * r0 / (np.linalg.norm(r0) ** 2) - r2_radial = r2 - np.dot(r2, r0) * r0 / (np.linalg.norm(r0) ** 2) - r1_proj = np.dot(r1, r0) * r0 / ( - np.linalg.norm(r0) ** 2 - ) + epsilon * r1_radial / np.linalg.norm(r1_radial) - r2_proj = np.dot(r2, r0) * r0 / ( - np.linalg.norm(r0) ** 2 - ) + epsilon * r2_radial / np.linalg.norm(r2_radial) - r1Xr2_proj = np.cross(r1_proj, r2_proj) - vel_ind_proj = ( + r1_axial = _div(_scale(_dot(r1, r0), r0), r0_sq) + r2_axial = _div(_scale(_dot(r2, r0), r0), r0_sq) + r1_radial = _sub(r1, r1_axial) + r2_radial = _sub(r2, r2_axial) + r1_proj = _add(r1_axial, _div(_scale(epsilon, r1_radial), _norm(r1_radial))) + r2_proj = _add(r2_axial, _div(_scale(epsilon, r2_radial), _norm(r2_radial))) + r1Xr2_proj = _cross(r1_proj, r2_proj) + vel_ind_proj = _scale( gamma / (4 * np.pi) - * r1Xr2_proj - / (np.linalg.norm(r1Xr2_proj) ** 2) - * np.dot( + / (_norm(r1Xr2_proj) ** 2) + * _dot( r0, - r1_proj / np.linalg.norm(r1_proj) - r2_proj / np.linalg.norm(r2_proj), - ) + _sub(_div(r1_proj, _norm(r1_proj)), _div(r2_proj, _norm(r2_proj))), + ), + r1Xr2_proj, ) - return dist / epsilon * vel_ind_proj + return _scale(dist / epsilon, vel_ind_proj) -@jit(nopython=True, cache=True) +@jit(nopython=True, cache=True, error_model="numpy") def _vel_semiinfinite(XV1, Vf, XVP, gamma, Uinf, filament_direction): """Semi-infinite trailing-vortex induced velocity, as in Filament.velocity_3D_trailing_vortex_semiinfinite.""" GAMMA = -gamma * filament_direction - r1 = XVP - XV1 - r1XVf = np.cross(r1, Vf) - r_perp = r1 - np.dot(r1, Vf) * Vf + r1 = _sub(XVP, XV1) + r1XVf = _cross(r1, Vf) + r1_axial = _scale(_dot(r1, Vf), Vf) + r_perp = _sub(r1, r1_axial) - if np.linalg.norm(r_perp) < 1e-16: - return np.zeros(3) + if _norm(r_perp) < 1e-16: + return _ZERO3 - epsilon = np.sqrt(4 * _ALPHA0_OSEEN * _NU_AIR * np.linalg.norm(r_perp) / Uinf) + epsilon = np.sqrt(4 * _ALPHA0_OSEEN * _NU_AIR * _norm(r_perp) / Uinf) - dist = np.linalg.norm(r1XVf) / np.linalg.norm(Vf) + dist = _norm(r1XVf) / _norm(Vf) if dist > epsilon: K = ( GAMMA / 4 / np.pi - / np.linalg.norm(r1XVf) ** 2 - * (1 + np.dot(r1, Vf) / np.linalg.norm(r1)) + / _norm(r1XVf) ** 2 + * (1 + _dot(r1, Vf) / _norm(r1)) ) - return K * r1XVf + return _scale(K, r1XVf) elif dist < 1e-12 * epsilon: - return np.zeros(3) + return _ZERO3 else: - r1_radial = r1 - np.dot(r1, Vf) * Vf - r1_proj = np.dot(r1, Vf) * Vf + epsilon * r1_radial / np.linalg.norm(r1_radial) - r1XVf_proj = np.cross(r1_proj, Vf) + r1_radial = r_perp + r1_proj = _add(r1_axial, _div(_scale(epsilon, r1_radial), _norm(r1_radial))) + r1XVf_proj = _cross(r1_proj, Vf) K = ( GAMMA / 4 / np.pi - / np.linalg.norm(r1XVf_proj) ** 2 - * (1 + np.dot(r1_proj, Vf) / np.linalg.norm(r1_proj)) + / _norm(r1XVf_proj) ** 2 + * (1 + _dot(r1_proj, Vf) / _norm(r1_proj)) ) - return K * r1XVf_proj + return _scale(K, r1XVf_proj) -@jit(nopython=True, cache=True) +@jit(nopython=True, cache=True, error_model="numpy") def _vel_bound_2D(XVP, bound_point_1, bound_point_2): """2D bound-vortex correction at the evaluation point, as in Panel.compute_velocity_induced_bound_2D (gamma = 1).""" - r3 = XVP - (bound_point_1 + bound_point_2) / 2 - r0 = bound_point_1 - bound_point_2 - r0_unit_X_r3 = np.cross(r0 / np.linalg.norm(r0), r3) - return (1 / (2 * np.pi)) * (r0_unit_X_r3 / (np.linalg.norm(r0_unit_X_r3) ** 2)) + r3 = _sub(XVP, _div(_add(bound_point_1, bound_point_2), 2.0)) + r0 = _sub(bound_point_1, bound_point_2) + r0_unit_X_r3 = _cross(_div(r0, _norm(r0)), r3) + return _scale(1 / (2 * np.pi), _div(r0_unit_X_r3, _norm(r0_unit_X_r3) ** 2)) -@jit(nopython=True, cache=True) +@jit(nopython=True, cache=True, error_model="numpy") def assemble_AIC_matrices( eval_points, bound_point_1, @@ -212,78 +266,187 @@ def assemble_AIC_matrices( n = eval_points.shape[0] AIC = np.empty((3, n, n)) for icp in range(n): - ep = eval_points[icp] + ep = _row(eval_points, icp) for jring in range(n): + bp1 = _row(bound_point_1, jring) + bp2 = _row(bound_point_2, jring) + te1 = _row(TE_point_1, jring) + te2 = _row(TE_point_2, jring) if evaluation_point_on_bound: - velocity_induced = np.zeros(3) + velocity_induced = _ZERO3 else: velocity_induced = _vel_bound_vortex( - bound_point_2[jring], - bound_point_1[jring], - ep, - 1.0, - core_radius_fraction, + bp2, bp1, ep, 1.0, core_radius_fraction ) - wake_unit = wake_units[jring] + wake_unit = _row(wake_units, jring) wake_speed = wake_speeds[jring] - velocity_induced = velocity_induced + _vel_trailing_vortex( - bound_point_1[jring], TE_point_1[jring], ep, 1.0, wake_speed + velocity_induced = _add( + velocity_induced, _vel_trailing_vortex(bp1, te1, ep, 1.0, wake_speed) ) - velocity_induced = velocity_induced + _vel_trailing_vortex( - TE_point_2[jring], bound_point_2[jring], ep, 1.0, wake_speed + velocity_induced = _add( + velocity_induced, _vel_trailing_vortex(te2, bp2, ep, 1.0, wake_speed) ) - velocity_induced = velocity_induced + _vel_semiinfinite( - TE_point_1[jring], wake_unit, ep, 1.0, wake_speed, 1.0 + velocity_induced = _add( + velocity_induced, + _vel_semiinfinite(te1, wake_unit, ep, 1.0, wake_speed, 1.0), ) - velocity_induced = velocity_induced + _vel_semiinfinite( - TE_point_2[jring], wake_unit, ep, 1.0, wake_speed, -1.0 + velocity_induced = _add( + velocity_induced, + _vel_semiinfinite(te2, wake_unit, ep, 1.0, wake_speed, -1.0), ) if subtract_bound_2D and icp == jring: - velocity_induced = velocity_induced - _vel_bound_2D( - ep, bound_point_1[jring], bound_point_2[jring] - ) + velocity_induced = _sub(velocity_induced, _vel_bound_2D(ep, bp1, bp2)) AIC[0, icp, jring] = velocity_induced[0] AIC[1, icp, jring] = velocity_induced[1] AIC[2, icp, jring] = velocity_induced[2] return AIC -@jit(nopython=True, cache=True) -def assemble_bound_vortex_AIC(eval_points, bound_point_1, bound_point_2, core_radius_fraction): - """Unit-circulation induced velocity of every panel's BOUND vortex alone at - every evaluation point, (3, N, N), with the panel's own bound vortex left - out (a straight segment induces nothing on its own line, and the quarter - chord point sits on it). Used to add the curved/swept bound-vortex - influence to a trailed-vortex-only AIC evaluated on the quarter chord. +@jit(nopython=True, cache=True, error_model="numpy") +def assemble_curved_bound_correction_AIC( + eval_points, + bound_point_1, + bound_point_2, + TE_point_1, + TE_point_2, + chord_dir, + chords, + va_norm, + ac_fraction, + core_radius_fraction, + strip_x, + strip_w, +): + """Curvature, sweep and rib-skew correction to the quarter-chord (direction) + velocity, (3, N, N), to be ADDED to the trailed-vortex-only (LLT) AIC + evaluated on the quarter chord. + + A lifting line evaluated on itself has a logarithmically singular + self-induction wherever it is curved (Li, Gaunaa, Pirrung, Ramos-Garcia & + Horcas 2020), so neither adding the other panels' bound segments at the + quarter chord (Phillips & Snyder) nor leaving them out converges with the + panel count on a curved or swept wing. Here, for every evaluation panel i, + the wing is UNROLLED onto the tangent line of panel i (nodes keep their + arc-length coordinate, legs perpendicular to the line in panel i's chord + direction), and the quarter-chord velocity is built as + + V(1/4c) = LLT downwash of the unrolled wing + + [ actual vortex system - unrolled vortex system ], + + where in the bracket the bound vorticity of every ring and the starting + points of its legs are spread along the chord with the thin-airfoil + distribution gamma(x) ~ sqrt((c - x)/x) (stations strip_x, weights strip_w; + strip nodes lie on each node's own leg line so the strip legs are collinear + with the actual legs). The first term is Prandtl's classical downwash, exact + for straight wings; the bracket is identically zero on a straight wing with + perpendicular legs and finite on curved ones, with the cut-off fixed at the + chord scale by the distribution itself (effective radius 0.15 c). This is the + discrete counterpart of the inner solution of curved lifting-line theory + (Guermond 1990) and shares its mechanism with the effective-lifting-line / + jointed-vortex method of Reid & Hunsaker (2020), without their two tuning + lengths. On-blade induced drag then matches the Trefftz plane within 1 % on + arc, swept and kite geometries independently of the panel count. + + Since the LLT AIC already holds the actual legs and wake, the returned + matrix is (unrolled legs - actual legs) + (smeared actual - smeared + unrolled); the semi-infinite wake filaments cancel identically. """ n = eval_points.shape[0] - AIC = np.zeros((3, n, n)) - for icp in range(n): - ep = eval_points[icp] - for jring in range(n): - if icp == jring: - continue - vel = _vel_bound_vortex( - bound_point_2[jring], bound_point_1[jring], ep, 1.0, core_radius_fraction + n_strips = strip_x.shape[0] + A = np.zeros((3, n, n)) + seg = bound_point_1 - bound_point_2 + L = np.empty(n) + for j in range(n): + L[j] = np.linalg.norm(seg[j]) + # Panel sequence direction relative to the filament direction bp2 -> bp1. + fwd = True + if n > 1: + fwd = np.linalg.norm(bound_point_1[0] - bound_point_2[1]) < np.linalg.norm( + bound_point_2[0] - bound_point_1[1] + ) + sgn = 1.0 if fwd else -1.0 + s_node = np.zeros(n + 1) + for j in range(n): + s_node[j + 1] = s_node[j] + L[j] + for i in range(n): + ep = _row(eval_points, i) + t = _div(_row(seg, i), L[i]) + c_i = _row(chord_dir, i) + s_P = 0.5 * (s_node[i] + s_node[i + 1]) + U = va_norm[i] + for j in range(n): + ra = _add(ep, _scale(sgn * (s_node[j] - s_P), t)) + rb = _add(ep, _scale(sgn * (s_node[j + 1] - s_P), t)) + if fwd: + rb2 = ra + rb1 = rb + else: + rb2 = rb + rb1 = ra + leg = _scale((1.0 - ac_fraction) * chords[j], c_i) + rte1 = _add(rb1, leg) + rte2 = _add(rb2, leg) + bp1 = _row(bound_point_1, j) + bp2 = _row(bound_point_2, j) + te1 = _row(TE_point_1, j) + te2 = _row(TE_point_2, j) + # unrolled legs (single filaments) minus actual legs (single filaments) + v = _add( + _vel_trailing_vortex(rb1, rte1, ep, 1.0, U), + _vel_trailing_vortex(rte2, rb2, ep, 1.0, U), ) - AIC[0, icp, jring] = vel[0] - AIC[1, icp, jring] = vel[1] - AIC[2, icp, jring] = vel[2] - return AIC - - -@jit(nopython=True, cache=True) + v = _sub( + _sub(v, _vel_trailing_vortex(bp1, te1, ep, 1.0, U)), + _vel_trailing_vortex(te2, bp2, ep, 1.0, U), + ) + # chordwise-spread actual system minus chordwise-spread unrolled system + for q in range(n_strips): + f = (strip_x[q] - ac_fraction) / (1.0 - ac_fraction) + w = strip_w[q] + n1 = _add(bp1, _scale(f, _sub(te1, bp1))) + n2 = _add(bp2, _scale(f, _sub(te2, bp2))) + u1 = _add(rb1, _scale(f, _sub(rte1, rb1))) + u2 = _add(rb2, _scale(f, _sub(rte2, rb2))) + strip = _sub( + _vel_bound_vortex(n2, n1, ep, 1.0, core_radius_fraction), + _vel_bound_vortex(u2, u1, ep, 1.0, core_radius_fraction), + ) + strip = _add(strip, _vel_trailing_vortex(n1, te1, ep, 1.0, U)) + strip = _add(strip, _vel_trailing_vortex(te2, n2, ep, 1.0, U)) + strip = _sub(strip, _vel_trailing_vortex(u1, rte1, ep, 1.0, U)) + strip = _sub(strip, _vel_trailing_vortex(rte2, u2, ep, 1.0, U)) + v = _add(v, _scale(w, strip)) + A[0, i, j] = v[0] + A[1, i, j] = v[1] + A[2, i, j] = v[2] + return A + + +def thin_airfoil_strips(n_strips=24): + """Chordwise stations x/c and weights of the thin-airfoil bound vorticity + distribution gamma(x) ~ sqrt((c - x)/x), via x = c sin^2(phi) with phi at + the midpoints of n_strips equal intervals on (0, pi/2), so that + sqrt((c - x)/x) dx = 2 c cos^2(phi) dphi. The centroid of the weights is at + x/c = 1/4, so the far field of the spread vorticity equals that of the + single quarter-chord filament.""" + phi = (np.arange(n_strips) + 0.5) * (np.pi / 2) / n_strips + x = np.sin(phi) ** 2 + w = np.cos(phi) ** 2 + return np.ascontiguousarray(x, dtype=float), np.ascontiguousarray(w / w.sum(), dtype=float) + + +@jit(nopython=True, cache=True, error_model="numpy") def _on_segment_line(XV1, XV2, XVP): """True if XVP lies on the line through XV1 and XV2 (to round-off).""" - r0 = XV2 - XV1 - r1 = XVP - XV1 - len0 = np.linalg.norm(r0) + r0 = _sub(XV2, XV1) + r1 = _sub(XVP, XV1) + len0 = _norm(r0) if len0 == 0.0: return True - return np.linalg.norm(np.cross(r1, r0)) <= 1e-10 * len0 * max(np.linalg.norm(r1), len0) + return _norm(_cross(r1, r0)) <= 1e-10 * len0 * max(_norm(r1), len0) -@jit(nopython=True, cache=True) +@jit(nopython=True, cache=True, error_model="numpy") def induced_velocity_at_points( points, bound_point_1, @@ -307,31 +470,25 @@ def induced_velocity_at_points( n = bound_point_1.shape[0] out = np.zeros((m, 3)) for ip in range(m): - ep = points[ip] - vel = np.zeros(3) + ep = _row(points, ip) + vel = _ZERO3 for j in range(n): g = gamma[j] - wake_unit = wake_units[j] + bp1 = _row(bound_point_1, j) + bp2 = _row(bound_point_2, j) + te1 = _row(TE_point_1, j) + te2 = _row(TE_point_2, j) + wake_unit = _row(wake_units, j) wake_speed = wake_speeds[j] - vel = vel + _vel_bound_vortex( - bound_point_2[j], bound_point_1[j], ep, g, core_radius_fraction - ) + vel = _add(vel, _vel_bound_vortex(bp2, bp1, ep, g, core_radius_fraction)) # a straight filament induces nothing on its own line; skip the # legs the point lies on (the kernel would divide by zero there) - if not _on_segment_line(bound_point_1[j], TE_point_1[j], ep): - vel = vel + _vel_trailing_vortex( - bound_point_1[j], TE_point_1[j], ep, g, wake_speed - ) - if not _on_segment_line(TE_point_2[j], bound_point_2[j], ep): - vel = vel + _vel_trailing_vortex( - TE_point_2[j], bound_point_2[j], ep, g, wake_speed - ) - vel = vel + _vel_semiinfinite( - TE_point_1[j], wake_unit, ep, g, wake_speed, 1.0 - ) - vel = vel + _vel_semiinfinite( - TE_point_2[j], wake_unit, ep, g, wake_speed, -1.0 - ) + if not _on_segment_line(bp1, te1, ep): + vel = _add(vel, _vel_trailing_vortex(bp1, te1, ep, g, wake_speed)) + if not _on_segment_line(te2, bp2, ep): + vel = _add(vel, _vel_trailing_vortex(te2, bp2, ep, g, wake_speed)) + vel = _add(vel, _vel_semiinfinite(te1, wake_unit, ep, g, wake_speed, 1.0)) + vel = _add(vel, _vel_semiinfinite(te2, wake_unit, ep, g, wake_speed, -1.0)) out[ip, 0] = vel[0] out[ip, 1] = vel[1] out[ip, 2] = vel[2] @@ -378,3 +535,148 @@ def point_in_quad(pt, corners): return point_in_triangle( pt, corners[0], corners[1], corners[2] ) or point_in_triangle(pt, corners[0], corners[2], corners[3]) + + +def perpendicular_leg_end_points( + bound_nodes, te_nodes, chord_dir, chords, ac_fraction, direction=None, normals=None +): + """End points of attached-trailed legs perpendicular to the quarter-chord line. + + Gaunaa, Li & Pirrung (TORQUE 2026, Sec. 3) run the attached part of the + trailed vortex from the bound vortex "along the local chordwise direction + up to the trailing edge", the chordwise direction being that of the + Crossflow Principle (CP1): perpendicular to the local quarter-chord line. + The legs then depend only on the quarter-chord line and the trailing-edge + curve, not on where the input ribs were drawn. + + Where the line turns by more than about one radian within one leg length + (a planform kink, a tip whose curvature radius is below a chord) plain + perpendiculars cross. The line tangent is therefore averaged over plus and + minus one leg length, ``(1 - ac) c``, of arc length around each node before + the perpendicular is taken. On a straight or uniformly swept line this is + the plain perpendicular. At a kink it turns the leg at the kink into the + bisector (streamwise at the root of a symmetric swept wing) and blends to + the perpendicular within one leg length. The averaging length is the leg's + own scale, not a free parameter. Legs that still cross raise an error. + + Args: + bound_nodes (np.ndarray): (n+1, 3) bound-vortex nodes in panel order + (node ``k`` is ``bound_point_1`` of panel ``k``; node ``n`` is + ``bound_point_2`` of the last panel). + te_nodes (np.ndarray): (n+1, 3) section trailing-edge points in the + same order; their polyline is the trailing-edge curve. + chord_dir (np.ndarray): (n, 3) span-perpendicular chord directions of + the panels, leading edge to trailing edge (``Panel.y_airf``). + chords (np.ndarray): (n,) perpendicular chords of the panels. + ac_fraction (float): chord fraction of the bound vortex. + direction (np.ndarray, optional): a fixed leg direction (3,), e.g. the + apparent-velocity direction for streamwise legs. It is projected + onto the local surface (perpendicular to ``normals``) at each + node and used instead of the perpendicular to the quarter-chord + line; the tangent averaging is then not needed. + normals (np.ndarray, optional): (n, 3) panel normals (``Panel.x_airf``), + required with ``direction``. + + Returns: + np.ndarray: (n+1, 3) leg end points, on the trailing-edge polyline + (or on the straight extension of its first or last segment, for a + leg that leaves a skewed tip through the tip edge). + + Raises: + ValueError: if a leg does not reach the trailing-edge curve or two + neighbouring legs cross (end points not ordered along the edge). + """ + B = np.asarray(bound_nodes, dtype=float) + T = np.asarray(te_nodes, dtype=float) + Y = np.asarray(chord_dir, dtype=float) + c = np.asarray(chords, dtype=float).ravel() + n = len(c) + if B.shape != (n + 1, 3) or T.shape != (n + 1, 3) or Y.shape != (n, 3): + raise ValueError("perpendicular_leg_end_points: inconsistent array shapes") + + seg = B[1:] - B[:-1] + seg_len = np.linalg.norm(seg, axis=1) + if np.any(seg_len < 1e-12): + raise ValueError("repeated bound-vortex nodes") + tangent = seg / seg_len[:, None] + s_node = np.concatenate(([0.0], np.cumsum(seg_len))) + + te_seg = T[1:] - T[:-1] + te_len = np.linalg.norm(te_seg, axis=1) + if np.any(te_len < 1e-12): + raise ValueError("repeated trailing-edge points") + s_te = np.concatenate(([0.0], np.cumsum(te_len))) + + ends = np.zeros_like(B) + s_end = np.zeros(n + 1) + for k in range(n + 1): + adjacent = [j for j in (k - 1, k) if 0 <= j < n] + leg_length = (1.0 - ac_fraction) * float(np.mean(c[adjacent])) + + # tangent of the quarter-chord line averaged over +- one leg length + lo, hi = s_node[k] - leg_length, s_node[k] + leg_length + overlap = np.minimum(s_node[1:], hi) - np.maximum(s_node[:-1], lo) + overlap = np.clip(overlap, 0.0, None) + t_avg = overlap @ tangent + if np.linalg.norm(t_avg) < 1e-12: + t_avg = tangent[adjacent].sum(axis=0) + t_avg = t_avg / np.linalg.norm(t_avg) + + if direction is None: + # leg direction: the chord direction with its component along the + # averaged tangent removed (points from the bound vortex aft) + y = Y[adjacent].sum(axis=0) + d = y - np.dot(y, t_avg) * t_avg + else: + # fixed direction projected onto the local surface + nrm = np.asarray(normals, dtype=float)[adjacent].sum(axis=0) + nrm = nrm / np.linalg.norm(nrm) + d = np.asarray(direction, dtype=float) + d = d - np.dot(d, nrm) * nrm + d_norm = np.linalg.norm(d) + if d_norm < 1e-9: + raise ValueError(f"leg direction at node {k} is degenerate (along the span or the normal)") + d = d / d_norm + + # The leg ends at the point of the trailing-edge curve closest to the + # ray B_k + t d, t > 0. A ray in 3D does not meet a polyline on a + # curved surface exactly, so the closest point is used rather than an + # intersection; it lies on a segment, or on the straight extension of + # the first or last segment, so that a leg leaving a skewed tip + # through the tip edge still ends on the trailing-edge line. + w0 = B[k][None, :] - T[:-1] + b = te_seg @ d + cc = te_len**2 + dd = w0 @ d + e0 = np.einsum("ij,ij->i", te_seg, w0) + denom = cc - b * b + regular = denom > 1e-10 * cc + u = np.where(regular, (e0 - b * dd) / np.where(regular, denom, 1.0), 0.0) + lower = np.zeros(len(te_len)) + upper = np.ones(len(te_len)) + lower[0], upper[-1] = -np.inf, np.inf + u = np.clip(u, lower, upper) + P = T[:-1] + u[:, None] * te_seg + t = (P - B[k][None, :]) @ d + ahead = t > 0.0 + if not np.any(ahead): + raise ValueError( + f"the perpendicular attached-trailed leg of node {k} does not reach " + "the trailing-edge curve; use leg_direction='rib'" + ) + dist = np.linalg.norm(B[k][None, :] + t[:, None] * d[None, :] - P, axis=1) + dist[~ahead] = np.inf + m = int(np.argmin(dist)) + u_clip = u + ends[k] = P[m] + s_end[k] = s_te[m] + u_clip[m] * te_len[m] + + crossing = np.where(np.diff(s_end) <= 0.0)[0] + if crossing.size: + k = int(crossing[0]) + raise ValueError( + f"attached-trailed legs cross between nodes {k} and {k + 1}: the " + "quarter-chord line turns by more than one leg length there; use " + "leg_direction='rib'" + ) + return ends diff --git a/src/VSM/plot_geometry_plotly.py b/src/VSM/plot_geometry_plotly.py index 236b62b..0d99386 100644 --- a/src/VSM/plot_geometry_plotly.py +++ b/src/VSM/plot_geometry_plotly.py @@ -1,46 +1,62 @@ import os -from typing import List, Tuple, Dict, Any +from pathlib import Path +from typing import List, Tuple, Dict, Any, Optional import numpy as np import plotly.graph_objects as go from VSM.core.Solver import Solver import yaml -def add_panel_edges(fig: go.Figure, panel: Any, is_first: bool, is_last: bool) -> None: - """Add panel edges to the figure with different thicknesses.""" - leadinge_edge_line_color = "black" - leadinge_edge_line_width = 15 - # Leading edge - if is_first: - # Include 1-side edge - fig.add_trace( - go.Scatter3d( - x=[panel.LE_point_1[0], panel.TE_point_1[0]], - y=[panel.LE_point_1[1], panel.TE_point_1[1]], - z=[panel.LE_point_1[2], panel.TE_point_1[2]], - mode="lines", - line=dict( - color=leadinge_edge_line_color, width=leadinge_edge_line_width - ), - name="Leading Edge", - showlegend=False, +def add_panel_edges( + fig: go.Figure, + panel: Any, + is_first: bool, + is_last: bool, + is_thin_outline: bool = False, +) -> None: + """Add panel edges to the figure. + + By default the leading edge is drawn as a thick black line. With + ``is_thin_outline`` (used by the fancy plot, where the inflatable leading-edge + tube already marks the leading edge) all panel edges are thin grey outlines + and the thick leading-edge line is omitted. + """ + edge_color = "blue" if is_thin_outline else "black" + if is_thin_outline: + leadinge_edge_line_color = "blue" + leadinge_edge_line_width = 1.5 + else: + leadinge_edge_line_color = "black" + leadinge_edge_line_width = 15 + # Thick tip edges only make sense for the thick leading-edge style. + if is_first: + fig.add_trace( + go.Scatter3d( + x=[panel.LE_point_1[0], panel.TE_point_1[0]], + y=[panel.LE_point_1[1], panel.TE_point_1[1]], + z=[panel.LE_point_1[2], panel.TE_point_1[2]], + mode="lines", + line=dict( + color=leadinge_edge_line_color, width=leadinge_edge_line_width + ), + name="Leading Edge", + showlegend=False, + ) ) - ) - elif is_last: - # Include 1-side edge - fig.add_trace( - go.Scatter3d( - x=[panel.LE_point_2[0], panel.TE_point_2[0]], - y=[panel.LE_point_2[1], panel.TE_point_2[1]], - z=[panel.LE_point_2[2], panel.TE_point_2[2]], - mode="lines", - line=dict( - color=leadinge_edge_line_color, width=leadinge_edge_line_width - ), - name="Leading Edge", - showlegend=False, + elif is_last: + fig.add_trace( + go.Scatter3d( + x=[panel.LE_point_2[0], panel.TE_point_2[0]], + y=[panel.LE_point_2[1], panel.TE_point_2[1]], + z=[panel.LE_point_2[2], panel.TE_point_2[2]], + mode="lines", + line=dict( + color=leadinge_edge_line_color, width=leadinge_edge_line_width + ), + name="Leading Edge", + showlegend=False, + ) ) - ) # Standard leading edge fig.add_trace( go.Scatter3d( @@ -61,7 +77,7 @@ def add_panel_edges(fig: go.Figure, panel: Any, is_first: bool, is_last: bool) - y=[panel.TE_point_1[1], panel.TE_point_2[1]], z=[panel.TE_point_1[2], panel.TE_point_2[2]], mode="lines", - line=dict(color="black", width=2), + line=dict(color=edge_color, width=2), name="Trailing Edge", showlegend=is_first, ) @@ -77,7 +93,7 @@ def add_panel_edges(fig: go.Figure, panel: Any, is_first: bool, is_last: bool) - y=[points[0][1], points[1][1]], z=[points[0][2], points[1][2]], mode="lines", - line=dict(color="black", width=0.8), + line=dict(color=edge_color, width=0.8), name=( "Side Edge" if is_first and i == 0 else None ), # Legend only for the first side edge @@ -175,6 +191,7 @@ def add_aerodynamic_vectors( force_vector: np.ndarray, scale: float = 0.5, max_force: float = 1.0, + origin: np.ndarray = None, ): """ Add aerodynamic force vectors to a given panel on the plot. @@ -184,9 +201,12 @@ def add_aerodynamic_vectors( panel: Panel object force_of_panel: Aerodynamic force vector scale: Scaling factor for the force vector + origin: Optional vector origin. Defaults to the panel aerodynamic centre; + pass a point on the canopy surface to make the vectors emanate from + the surface when the panels are hidden. """ # Compute vector endpoint - aerodynamic_center = panel.aerodynamic_center + aerodynamic_center = panel.aerodynamic_center if origin is None else origin vector = (force_vector / max_force) * scale * 0.5 vector_endpoint = aerodynamic_center + vector @@ -411,7 +431,7 @@ def add_bridle_system(fig: go.Figure, wing_aero: object, is_first: bool = True) elif hasattr(wing_aero, "_bridle_line_system") and wing_aero._bridle_line_system: # Handle basic bridle line system (backward compatibility) for i, bridle_line in enumerate(wing_aero._bridle_line_system): - p1, p2, _, _, diameter, _ = bridle_line + p1, p2, diameter = bridle_line # Calculate thickness based on diameter (simple scaling) thickness = max(2, min(12, diameter * 1000)) # Assume diameter in meters @@ -435,6 +455,11 @@ def create_3D_plot( forces_of_panels: List[np.ndarray], is_with_aerodynamic_details: bool, is_with_bridles: bool = False, + tube_data: Optional[Dict[str, Any]] = None, + canopy_grid: Optional[Tuple[np.ndarray, np.ndarray, np.ndarray]] = None, + is_with_tube_rings: bool = False, + is_with_panels: bool = True, + cp_magnitude_fn=None, ) -> go.Figure: """ Creates an interactive 3D plot of wing geometry using Plotly. @@ -444,10 +469,26 @@ def create_3D_plot( forces_of_panels: List of force vectors for each panel is_with_aerodynamic_details: Boolean to show/hide aerodynamic visualization details is_with_bridles: Boolean to show/hide bridle system visualization + tube_data: Optional inflatable-tube geometry from + ``VSM.plotly.build_tube_data``; when given, the leading-edge and strut + tubes are drawn as surfaces. + canopy_grid: Optional ``(X, Y, Z)`` curved-canopy surface from + ``VSM.plotly.build_canopy_grid``; when given, it replaces the flat + panel surfaces. + is_with_tube_rings: Also draw the tube construction rings (for inspection). + is_with_panels: Draw the panel outlines. When ``False`` and a canopy is + present, the force vectors are lifted onto the canopy surface instead + of starting at the flat panel aerodynamic centres. Returns: plotly.graph_objects.Figure """ + from VSM.plotly.tube_geometry import ( + add_strut_surfaces, + add_le_tube_surface, + add_tube_rings, + ) + panels = wing_aero.panels chord_average = np.max([panel.chord for panel in panels]) @@ -460,6 +501,14 @@ def create_3D_plot( if is_with_bridles: add_bridle_system(fig, wing_aero, is_first=True) + draw_flat_surface = canopy_grid is None + # In the fancy plot the inflatable LE tube marks the leading edge, so draw the + # panels as thin grey outlines instead of the thick black leading-edge line. + is_thin_outline = tube_data is not None + # On the canopy the per-panel force vectors are replaced by a set of equal, + # surface-normal vectors distributed across the span (see below). + use_distributed_vectors = canopy_grid is not None + # Add geometric elements for i, panel in enumerate(panels): is_first = False @@ -472,20 +521,158 @@ def create_3D_plot( if is_with_aerodynamic_details: add_filaments(fig, panel, is_first) - add_panel_edges(fig, panel, is_first, is_last) - add_panel_surface(fig, panel, is_first) - add_aerodynamic_vectors( + if is_with_panels: + add_panel_edges( + fig, panel, is_first, is_last, is_thin_outline=is_thin_outline + ) + if draw_flat_surface and is_with_panels: + add_panel_surface(fig, panel, is_first) + + if not use_distributed_vectors: + add_aerodynamic_vectors( + fig, + panel, + is_first, + np.array(forces_of_panels[i]), + scale=chord_average, + max_force=max_force, + ) + + # Draw order (so the leading-edge tube ends up rendered on top): struts sit + # under the canopy, then the canopy, then the force vectors, and finally the + # leading-edge tube last so it is never occluded by the semi-transparent canopy. + if tube_data is not None: + add_strut_surfaces(fig, tube_data) + + if canopy_grid is not None: + add_canopy_surface(fig, canopy_grid) + + if use_distributed_vectors: + add_distributed_surface_vectors( fig, - panel, - is_first, - np.array(forces_of_panels[i]), + canopy_grid, + forces_of_panels, scale=chord_average, - max_force=max_force, + cp_magnitude_fn=cp_magnitude_fn, ) + if tube_data is not None: + add_le_tube_surface(fig, tube_data) + if is_with_tube_rings: + add_tube_rings(fig, tube_data) + return fig +def add_canopy_surface( + fig: go.Figure, canopy_grid: Tuple[np.ndarray, np.ndarray, np.ndarray] +) -> None: + """Add the curved single-skin canopy as a uniformly coloured surface.""" + canopy_color = "lightgrey" + x, y, z = canopy_grid + fig.add_trace( + go.Surface( + x=x, + y=y, + z=z, + surfacecolor=np.zeros_like(z), + colorscale=[[0, canopy_color], [1, canopy_color]], + showscale=False, + opacity=0.85, + name="Canopy", + showlegend=True, + lighting=dict(ambient=0.75, diffuse=0.8, specular=0.05, roughness=0.9), + ) + ) + + +def add_distributed_surface_vectors( + fig: go.Figure, + canopy_grid: Tuple[np.ndarray, np.ndarray, np.ndarray], + forces_of_panels: List[np.ndarray], + scale: float, + n_chord: int = 10, + n_span: int = None, + cp_magnitude_fn=None, +) -> None: + """Draw a grid of chord-normal force vectors on the canopy. + + One row of ``n_chord`` nodes is placed at each panel centre (uniformly over + the chord); every node carries one arrow oriented normal to the local chord + line (the flat-panel "up", perpendicular to chord and span), pointing to the + outward/suction side. This gives a regular ``n_span x n_chord`` grid of + vectors covering the whole canopy. + + Each panel's VSM force is distributed over its chord nodes: the arrow lengths + reflect both the spanwise loading (the panel force from VSM) and the chordwise + shape (the ``|Cp|`` weighting, or an even split without a Cp distribution). + The node forces of a panel sum back to that panel's force. + + Args: + fig: Plotly figure. + canopy_grid: ``(X, Y, Z)`` canopy surface arrays, each ``(S, P)`` with + ``S`` spanwise stations and ``P`` chordwise points. + forces_of_panels: Per-panel aerodynamic force vectors from VSM; their + magnitudes set the total per panel that is distributed chordwise. + scale: Length scale (typically the average chord). + n_chord: Nodes per row, distributed over the chord. Defaults to 10. + n_span: Number of spanwise rows. Defaults to one row per panel (the + midpoints between the ``S`` canopy stations, i.e. ``S - 1`` rows). + cp_magnitude_fn: Optional callable mapping chord fraction ``x/c`` to a + (positive) pressure-coefficient magnitude, used as the chordwise + weighting of each panel's force. Without it the force is spread evenly. + """ + from VSM.plotly.canopy_geometry import distributed_surface_vector_field + + origins, normals, lengths_1d = distributed_surface_vector_field( + canopy_grid, + forces_of_panels, + scale, + n_chord=n_chord, + n_span=n_span, + cp_magnitude_fn=cp_magnitude_fn, + ) + if len(origins) == 0: + return + lengths = lengths_1d[:, None] + endpoints = origins + normals * lengths + + # All stems as one trace (None-separated segments), all heads as one cone trace. + stem_x, stem_y, stem_z = [], [], [] + for start, end in zip(origins, endpoints): + stem_x += [start[0], end[0], None] + stem_y += [start[1], end[1], None] + stem_z += [start[2], end[2], None] + fig.add_trace( + go.Scatter3d( + x=stem_x, + y=stem_y, + z=stem_z, + mode="lines", + line=dict(color="red", width=3.9), + name="Aerodynamic Vectors", + showlegend=True, + ) + ) + fig.add_trace( + go.Cone( + x=endpoints[:, 0], + y=endpoints[:, 1], + z=endpoints[:, 2], + u=normals[:, 0] * lengths[:, 0], + v=normals[:, 1] * lengths[:, 0], + w=normals[:, 2] * lengths[:, 0], + sizemode="absolute", + sizeref=0.048 * scale, + anchor="tip", + colorscale=[[0, "red"], [1, "red"]], + showscale=False, + name="Arrowheads", + showlegend=False, + ) + ) + + def compute_kite_geometry_ranges(panels: List[Any]) -> Tuple[Dict[str, float], float]: """Calculate axis ranges and tick spacing.""" all_points = [] @@ -507,14 +694,18 @@ def compute_axis_parameters_from_fig( fig: go.Figure, ) -> Tuple[Dict[str, float], float]: """Calculate axis ranges and tick spacing based on the figure's data.""" - # Extract all data points from the figure traces + # Extract all data points from the figure traces, skipping gap separators + # (``None``/NaN) that traces may use to draw multiple segments at once. all_points = [] for trace in fig.data: if isinstance(trace, go.Scatter3d): - all_points.extend(zip(trace.x, trace.y, trace.z)) + for px, py, pz in zip(trace.x, trace.y, trace.z): + if px is None or py is None or pz is None: + continue + all_points.append((px, py, pz)) # Convert to numpy array for calculations - all_points = np.array(all_points) + all_points = np.array(all_points, dtype=float) # Calculate ranges for each axis ranges = { @@ -598,12 +789,15 @@ def update_fig_layout( f"{kite_geometry_ranges['z'][1]:.2f}", ], # Label for the ticks ), - aspectmode="data", # Allow different axis lengths - aspectratio=dict(x=1, y=1, z=1), # Keep aspect ratio 1:1:1 + # "data" keeps the scene true-to-scale (equal units on every axis); + # do NOT force a cube aspectratio here, that squeezes the wide wing. + aspectmode="data", camera=dict( up=dict(x=0, y=0, z=1), center=dict(x=0, y=0, z=0), - eye=dict(x=1.5, y=1.5, z=1.5), + eye=dict( + x=-1.92, y=-1.44, z=0.30 + ), # rotated ~180 deg, low and further out ), bgcolor="white", ), @@ -754,6 +948,11 @@ def update_plot( is_with_aerodynamic_details: bool, is_with_bridles: bool, title: str, + tube_data: Optional[Dict[str, Any]] = None, + canopy_grid: Optional[Tuple[np.ndarray, np.ndarray, np.ndarray]] = None, + is_with_tube_rings: bool = False, + is_with_panels: bool = True, + cp_magnitude_fn=None, ): # Update AoA and rerun VSM results = running_VSM( @@ -773,6 +972,11 @@ def update_plot( results["F_distribution"], is_with_aerodynamic_details, is_with_bridles, + tube_data=tube_data, + canopy_grid=canopy_grid, + is_with_tube_rings=is_with_tube_rings, + is_with_panels=is_with_panels, + cp_magnitude_fn=cp_magnitude_fn, ) fig = add_case_information( fig, @@ -804,6 +1008,11 @@ def interactive_plot( is_save: bool = False, filename="wing_geometry", is_show: bool = True, + surfplan_dir: Optional[Path] = None, + is_with_canopy: bool = True, + is_with_tube_rings: bool = False, + is_with_panels: Optional[bool] = None, + cp_distributions_dir: Optional[Path] = None, ): """ Creates and optionally saves multiple views of the wing geometry with interactive AoA slider. @@ -823,7 +1032,57 @@ def interactive_plot( is_save: Whether to save the plot filename: Base filename for saved files is_show: Whether to display the plot + surfplan_dir: Optional path to a raw Surfplan export (a ``.txt`` plus + a ``profiles/`` directory). When given, the "fancy" plot is drawn: the + export is converted with SurfplanAdapter (cached) and the inflatable + leading-edge and strut tubes -- and, by default, the curved single-skin + canopy -- are rendered on the wing. Leaving it ``None`` keeps the plain + plot unchanged. + is_with_canopy: When a Surfplan export is given, draw the curved canopy + (lofted airfoil top-surfaces) in place of the flat panel surfaces. + is_with_tube_rings: Also draw the tube construction rings (for inspection). + is_with_panels: Draw the panel outlines. Defaults to ``False`` for the + fancy plot (a Surfplan export is given) and ``True`` otherwise. When + off, the force vectors are lifted onto the canopy surface. + cp_distributions_dir: Optional directory of ``cp_AOA_.dat`` chordwise + pressure-coefficient files. When given, the closest available angle of + attack is selected and its ``|Cp|(x/c)`` scales the chordwise force + vector magnitudes (the single mid-span distribution is applied at + every span station). + + Returns: + plotly.graph_objects.Figure: The created figure. """ + # Default: hide the panel outlines for the fancy plot, show them otherwise. + if is_with_panels is None: + is_with_panels = surfplan_dir is None + + # Build the optional fancy-plot geometry once (reused on every slider update). + tube_data = None + canopy_grid = None + if surfplan_dir is not None: + from VSM.plotly.surfplan_runner import ensure_surfplan_processed + from VSM.plotly.tube_geometry import build_tube_data + from VSM.plotly.canopy_geometry import load_contour_table, build_canopy_grid + + processed_dir = ensure_surfplan_processed(Path(surfplan_dir)) + tube_data = build_tube_data(wing_aero.panels, processed_dir) + if is_with_canopy: + contour_table = load_contour_table(processed_dir) + canopy_grid = build_canopy_grid(wing_aero.panels, contour_table) + + # Optional Cp-based scaling of the chordwise force vectors. + cp_magnitude_fn = None + if cp_distributions_dir is not None: + from VSM.plotly.cp_distributions import build_cp_magnitude_fn + + cp_magnitude_fn, matched_aoa = build_cp_magnitude_fn( + Path(cp_distributions_dir), angle_of_attack + ) + print( + f"Cp loading from closest available AoA = {matched_aoa:g} deg " + f"(operating AoA = {angle_of_attack:g} deg)" + ) # Create the figure with a default orientation fig = go.Figure() @@ -848,6 +1107,11 @@ def interactive_plot( is_with_aerodynamic_details, is_with_bridles, title, + tube_data=tube_data, + canopy_grid=canopy_grid, + is_with_tube_rings=is_with_tube_rings, + is_with_panels=is_with_panels, + cp_magnitude_fn=cp_magnitude_fn, ) # Save or show the plot if requested @@ -859,4 +1123,95 @@ def interactive_plot( fig.write_html(save_path) if is_show: - fig.show() + import tempfile + import webbrowser + + with tempfile.NamedTemporaryFile( + suffix=".html", delete=False, mode="w", encoding="utf-8" + ) as f: + tmp_path = f.name + fig.write_html(f, include_plotlyjs=True, full_html=True) + print(f"Interactive plot saved to: {tmp_path}") + webbrowser.open(f"file://{tmp_path}") + + return fig + + +def save_high_res_render( + fig: go.Figure, + save_path, + base_size: int = 2000, + scale: int = 4, + show_legend: bool = False, + show_axes: bool = False, + show_annotations: bool = False, + transparent_background: bool = False, + pad_frac: float = 0.02, +) -> str: + """Export a clean, to-scale high-resolution PNG of the kite. + + A Plotly 3D scene is only rendered undistorted on a **square** canvas (on a + non-square canvas Plotly stretches it, which squeezes the kite). So this + renders on a square canvas -- guaranteeing correct proportions -- then crops + tightly to the drawn content. The legend, axes and annotations are removed by + default for a clean figure. + + Args: + fig: The Plotly figure (e.g. from :func:`interactive_plot`). + save_path: Output ``.png`` path; parent directories are created. + base_size: Square canvas size in pixels before supersampling. + scale: Supersampling factor (final render is ``base_size * scale`` square, + then cropped). Defaults to 4. + show_legend: Keep the trace legend. Defaults to False. + show_axes: Keep the 3D axes. Defaults to False. + show_annotations: Keep the corner case-info text. Defaults to False. + transparent_background: Transparent instead of white background. + pad_frac: Padding around the cropped content, as a fraction of its size. + + Returns: + str: The path the PNG was written to. + """ + import io + from PIL import Image + + render_fig = go.Figure(fig) + render_fig.update_layout(showlegend=show_legend, margin=dict(l=0, r=0, t=0, b=0)) + if not show_annotations: + render_fig.layout.annotations = () + render_fig.layout.title = None + if not show_axes: + render_fig.update_scenes( + xaxis_visible=False, yaxis_visible=False, zaxis_visible=False + ) + bg = "rgba(0,0,0,0)" if transparent_background else "white" + render_fig.update_layout(paper_bgcolor=bg, plot_bgcolor=bg) + + # Square canvas => Plotly does not distort the 3D scene. + png = render_fig.to_image( + format="png", width=base_size, height=base_size, scale=scale + ) + image = Image.open(io.BytesIO(png)).convert("RGBA") + + # Crop tightly to the drawn content (non-background pixels). + rgb = np.asarray(image)[:, :, :3].astype(int) + alpha = np.asarray(image)[:, :, 3] + if transparent_background: + content = alpha > 0 + else: + content = rgb.sum(axis=2) < 3 * 250 # anything not near-white + ys, xs = np.where(content) + if len(xs): + pad = int(pad_frac * max(xs.max() - xs.min(), ys.max() - ys.min())) + left = max(xs.min() - pad, 0) + right = min(xs.max() + pad, image.width) + top = max(ys.min() - pad, 0) + bottom = min(ys.max() + pad, image.height) + image = image.crop((left, top, right, bottom)) + + save_path = Path(save_path) + save_path.parent.mkdir(parents=True, exist_ok=True) + image.save(str(save_path)) + print( + f"High-resolution render saved to: {save_path} ({image.width} x {image.height} px)" + ) + return str(save_path) diff --git a/src/VSM/plotly/__init__.py b/src/VSM/plotly/__init__.py new file mode 100644 index 0000000..a204699 --- /dev/null +++ b/src/VSM/plotly/__init__.py @@ -0,0 +1,24 @@ +"""Helpers for the interactive Plotly wing plot. + +The public ``interactive_plot`` entry point lives in ``VSM.plot_geometry_plotly`` +(unchanged location for backward compatibility). This subpackage holds the +supporting machinery for the opt-in "fancy" plot that renders the inflatable +tubes and the curved single-skin canopy of a LEI kite from a raw Surfplan export. +""" + +from VSM.plotly.surfplan_runner import ensure_surfplan_processed +from VSM.plotly.tube_geometry import ( + add_tube_rings, + add_tube_surfaces, + build_tube_data, +) +from VSM.plotly.canopy_geometry import build_canopy_grid, load_contour_table + +__all__ = [ + "ensure_surfplan_processed", + "build_tube_data", + "add_tube_surfaces", + "add_tube_rings", + "build_canopy_grid", + "load_contour_table", +] diff --git a/src/VSM/plotly/canopy_geometry.py b/src/VSM/plotly/canopy_geometry.py new file mode 100644 index 0000000..419c6bc --- /dev/null +++ b/src/VSM/plotly/canopy_geometry.py @@ -0,0 +1,332 @@ +"""Curved single-skin canopy geometry for the interactive Plotly wing plot. + +The canopy is rendered as a loft of the real airfoil top-surfaces (the ``.dat`` +profiles produced by SurfplanAdapter) placed on the *drawn* wing panels. Only the +upper contour is used (LEI kites are single-skin), so the inflatable tubes stay +visible from below. See ``VSM.plotly.tube_geometry`` for the tubes and +``VSM.plotly.surfplan_runner`` for how the source files are generated. + +All geometry is anchored on the drawn ``Panel`` objects (their LE/TE points and +local frame), so the result is independent of the coordinate frame of the source +Surfplan export. +""" + +from pathlib import Path +from typing import Any, List, Tuple + +import numpy as np +import yaml + + +def read_upper_contour(dat_file_path: Path) -> np.ndarray: + """Read an airfoil ``.dat`` file and return its upper contour, LE-to-TE. + + The ``.dat`` files use the standard ordering starting at the trailing edge + ``(1, 0)``, running forward over the upper surface to the leading edge + ``(0, 0)``, then back over the lower surface to the trailing edge. The upper + surface is the run up to the minimum-x (leading edge) point. + + Args: + dat_file_path (Path): Path to the airfoil coordinate file. The first line + is a name label and is skipped; remaining lines are ``x y`` pairs + normalised by the chord. + + Returns: + np.ndarray: Upper contour as an ``(n, 2)`` array ordered from the leading + edge ``(0, 0)`` to the trailing edge ``(1, 0)``. + """ + points = [] + for line in Path(dat_file_path).read_text().splitlines(): + parts = line.split() + if len(parts) != 2: + continue + try: + points.append((float(parts[0]), float(parts[1]))) + except ValueError: + continue + points = np.asarray(points, dtype=float) + le_index = int(np.argmin(points[:, 0])) + upper = points[: le_index + 1] # TE -> LE + return upper[::-1] # LE -> TE + + +def resample_contour(contour: np.ndarray, n_points: int = 40) -> np.ndarray: + """Resample a contour to a fixed point count by arc length. + + Uses cumulative chord-length parameterisation so that the leading- and + trailing-edge endpoints are preserved. This lets contours from different + airfoils be blended point-by-point. + + Args: + contour (np.ndarray): ``(n, 2)`` contour ordered LE-to-TE. + n_points (int): Number of output points. Defaults to 40. + + Returns: + np.ndarray: ``(n_points, 2)`` resampled contour. + """ + segment_lengths = np.linalg.norm(np.diff(contour, axis=0), axis=1) + arc_length = np.concatenate([[0.0], np.cumsum(segment_lengths)]) + total = arc_length[-1] + if total <= 0: + return np.repeat(contour[:1], n_points, axis=0) + sample = np.linspace(0.0, total, n_points) + x = np.interp(sample, arc_length, contour[:, 0]) + y = np.interp(sample, arc_length, contour[:, 1]) + return np.column_stack([x, y]) + + +def load_contour_table( + processed_dir: Path, n_points: int = 40 +) -> List[Tuple[float, np.ndarray]]: + """Build a span-fraction -> upper-contour table from the generated aero yaml. + + Reads ``aero_geometry.yaml`` written by SurfplanAdapter: each wing section + provides its span position (``LE_y``) and an ``airfoil_id`` whose + ``info_dict`` carries the ``dat_file_path``. Span positions are normalised to + fractions of the half-span so the table can be sampled against any drawn wing + regardless of scale or frame. + + Args: + processed_dir (Path): Directory holding ``aero_geometry.yaml`` and the + ``profiles/`` referenced by relative ``dat_file_path`` entries. + n_points (int): Point count each contour is resampled to. Defaults to 40. + + Returns: + List[Tuple[float, np.ndarray]]: ``(span_fraction, contour)`` pairs sorted + by ascending span fraction, each contour an ``(n_points, 2)`` array. + """ + processed_dir = Path(processed_dir) + aero = yaml.safe_load((processed_dir / "aero_geometry.yaml").read_text()) + sections = aero["wing_sections"] + airfoils = aero["wing_airfoils"] + + section_headers = sections["headers"] + idx_id = section_headers.index("airfoil_id") + idx_le_y = section_headers.index("LE_y") + + airfoil_headers = airfoils["headers"] + a_idx_id = airfoil_headers.index("airfoil_id") + a_idx_info = airfoil_headers.index("info_dict") + dat_by_id = { + row[a_idx_id]: row[a_idx_info]["dat_file_path"] for row in airfoils["data"] + } + + contour_cache = {} + span_y = np.array([abs(row[idx_le_y]) for row in sections["data"]]) + half_span = span_y.max() if span_y.max() > 0 else 1.0 + + table = [] + for row in sections["data"]: + airfoil_id = row[idx_id] + if airfoil_id not in contour_cache: + dat_path = processed_dir / dat_by_id[airfoil_id] + contour_cache[airfoil_id] = resample_contour( + read_upper_contour(dat_path), n_points + ) + fraction = abs(row[idx_le_y]) / half_span + table.append((fraction, contour_cache[airfoil_id])) + + table.sort(key=lambda item: item[0]) + return table + + +def interpolate_contour_at( + span_fraction: float, contour_table: List[Tuple[float, np.ndarray]] +) -> np.ndarray: + """Linearly blend the two contours bracketing a span fraction. + + Args: + span_fraction (float): Target fraction of the half-span, in ``[0, 1]``. + contour_table (List[Tuple[float, np.ndarray]]): Output of + :func:`load_contour_table`; contours share a common point count. + + Returns: + np.ndarray: Interpolated contour, clamped to the end contours outside the + table's fraction range. + """ + fractions = [f for f, _ in contour_table] + if span_fraction <= fractions[0]: + return contour_table[0][1] + if span_fraction >= fractions[-1]: + return contour_table[-1][1] + upper = int(np.searchsorted(fractions, span_fraction)) + lower = upper - 1 + f_lo, c_lo = contour_table[lower] + f_hi, c_hi = contour_table[upper] + weight = (span_fraction - f_lo) / (f_hi - f_lo) if f_hi > f_lo else 0.0 + return (1.0 - weight) * c_lo + weight * c_hi + + +def _edge_stations(panels: List[Any]) -> List[dict]: + """Collect the ``n_panels + 1`` spanwise edge stations of the drawn wing. + + Each station carries its leading- and trailing-edge points, the chord vector + and length, and the local up-vector (averaged across adjacent panels), all + taken from the ``Panel`` objects so the canopy sits exactly on the drawn wing. + """ + stations = [] + + def make_station(le_point, te_point, up_vectors): + chord_vector = np.asarray(te_point) - np.asarray(le_point) + chord_length = float(np.linalg.norm(chord_vector)) + chord_hat = chord_vector / max(chord_length, 1e-12) + up = np.mean(up_vectors, axis=0) + up = up / max(np.linalg.norm(up), 1e-12) + return { + "le_point": np.asarray(le_point, dtype=float), + "te_point": np.asarray(te_point, dtype=float), + "chord_length": chord_length, + "chord_hat": chord_hat, + "up": up, + } + + stations.append( + make_station(panels[0].LE_point_1, panels[0].TE_point_1, [panels[0].x_airf]) + ) + for index, panel in enumerate(panels): + neighbours = [panel.x_airf] + if index + 1 < len(panels): + neighbours.append(panels[index + 1].x_airf) + stations.append(make_station(panel.LE_point_2, panel.TE_point_2, neighbours)) + return stations + + +def build_canopy_grid( + panels: List[Any], contour_table: List[Tuple[float, np.ndarray]] +) -> Tuple[np.ndarray, np.ndarray, np.ndarray]: + """Build the lofted canopy surface grid over the drawn wing panels. + + For every panel-edge station the local upper contour (blended from the rib + profiles at that span fraction) is placed on the drawn chord: the contour's + chordwise coordinate runs along the station chord and its height along the + local up-vector, both scaled by the station chord length. + + Args: + panels (List[Any]): Drawn ``Panel`` objects, ordered tip to tip. + contour_table (List[Tuple[float, np.ndarray]]): Output of + :func:`load_contour_table`. + + Returns: + Tuple[np.ndarray, np.ndarray, np.ndarray]: ``X``, ``Y``, ``Z`` arrays of + shape ``(n_panels + 1, n_points)`` suitable for a Plotly surface. + """ + stations = _edge_stations(panels) + all_y = np.array([abs(station["le_point"][1]) for station in stations]) + half_span = all_y.max() if all_y.max() > 0 else 1.0 + + rows = [] + for station in stations: + fraction = abs(station["le_point"][1]) / half_span + contour = interpolate_contour_at(fraction, contour_table) + chord_length = station["chord_length"] + points = ( + station["le_point"][None, :] + + contour[:, 0:1] * chord_length * station["chord_hat"][None, :] + + contour[:, 1:2] * chord_length * station["up"][None, :] + ) + rows.append(points) + + grid = np.stack(rows, axis=0) # (n_stations, n_points, 3) + return grid[:, :, 0], grid[:, :, 1], grid[:, :, 2] + + +def distributed_surface_vector_field( + canopy_grid: Tuple[np.ndarray, np.ndarray, np.ndarray], + forces_of_panels: List[np.ndarray], + scale: float, + n_chord: int = 10, + n_span: int = None, + cp_magnitude_fn=None, +) -> Tuple[np.ndarray, np.ndarray, np.ndarray]: + """Chord-normal force-vector field over the canopy (origins, directions, lengths). + + One row of ``n_chord`` nodes is placed at each panel centre, uniformly over + the chord (at the true 5%-95% chord fractions). Every node's arrow points + normal to the local chord line (the flat-panel "up", outward/suction side). + Each panel's VSM force is distributed over its chord nodes, weighted by the + ``|Cp|`` chordwise loading (an even split without a Cp distribution) and + normalised to length so the largest panel force plots at the maximum chord + length (``scale``). + + Args: + canopy_grid: ``(X, Y, Z)`` canopy arrays, each ``(S, P)``. + forces_of_panels: Per-panel VSM force vectors. + scale: Length scale (typically the maximum chord); the largest panel + force plots at this length. + n_chord: Nodes per row over the chord. Defaults to 10. + n_span: Number of spanwise rows. Defaults to one per panel (``S - 1``). + cp_magnitude_fn: Optional ``x/c -> |Cp|`` weighting callable. + + Returns: + Tuple[np.ndarray, np.ndarray, np.ndarray]: ``origins`` ``(N, 3)``, + unit ``directions`` ``(N, 3)`` and ``lengths`` ``(N,)``. Empty arrays + if there is no force. + """ + x, y, z = canopy_grid + grid = np.stack([x, y, z], axis=-1) # (S, P, 3) + + force_magnitudes = np.linalg.norm(np.asarray(forces_of_panels), axis=1) + if force_magnitudes.size == 0 or np.max(force_magnitudes) <= 0: + return np.empty((0, 3)), np.empty((0, 3)), np.empty((0,)) + + panel_grid = 0.5 * (grid[:-1] + grid[1:]) # (S - 1, P, 3), one row per panel + n_rows, n_points, _ = panel_grid.shape + + if n_span is None: + n_span = n_rows + span_rows = np.unique( + np.clip(np.round(np.linspace(0, n_rows - 1, n_span)), 0, n_rows - 1).astype(int) + ) + d_span = np.gradient(panel_grid, axis=0) # (R, P, 3) + + targets = np.linspace(0.05, 0.95, n_chord) + if cp_magnitude_fn is not None: + chord_weights = np.asarray(cp_magnitude_fn(targets), dtype=float) + else: + chord_weights = np.ones(n_chord) + chord_weights = chord_weights / max(chord_weights.sum(), 1e-12) + + origins = [] + directions = [] + node_forces = [] + for row in span_rows: + pts = panel_grid[row] + chord_vector = pts[-1] - pts[0] + chord_length = np.linalg.norm(chord_vector) + chord_hat = chord_vector / max(chord_length, 1e-12) + + span_vector = d_span[row].mean(axis=0) + chord_normal = np.cross(chord_vector, span_vector) + cn = np.linalg.norm(chord_normal) + if cn < 1e-9: + continue + chord_normal = chord_normal / cn + if chord_normal[2] < 0: # outward (suction) side + chord_normal = -chord_normal + + panel_force = force_magnitudes[row] if row < len(force_magnitudes) else 0.0 + + chord_fraction = ((pts - pts[0]) @ chord_hat) / max(chord_length, 1e-12) + order = np.argsort(chord_fraction) + frac_sorted = chord_fraction[order] + pts_sorted = pts[order] + for k, target in enumerate(targets): + point = np.array( + [np.interp(target, frac_sorted, pts_sorted[:, j]) for j in range(3)] + ) + origins.append(point) + directions.append(chord_normal) + node_forces.append(panel_force * chord_weights[k]) + + if not origins: + return np.empty((0, 3)), np.empty((0, 3)), np.empty((0,)) + + origins = np.array(origins) + directions = np.array(directions) + node_forces = np.array(node_forces) + newton_to_metre = scale / max(float(force_magnitudes.max()), 1e-12) + # Anchor the per-vector size to a base of 10 chordwise nodes, so adding more + # nodes makes the field denser without shrinking each arrow (at n_chord=10 the + # factor is 1 and the largest panel's arrows still sum to the max chord). + lengths = node_forces * newton_to_metre * (n_chord / 10.0) + return origins, directions, lengths diff --git a/src/VSM/plotly/cp_distributions.py b/src/VSM/plotly/cp_distributions.py new file mode 100644 index 0000000..29c1ad0 --- /dev/null +++ b/src/VSM/plotly/cp_distributions.py @@ -0,0 +1,97 @@ +"""Chordwise pressure-coefficient (Cp) database for the fancy Plotly plot. + +A directory of ``cp_AOA_.dat`` files (whitespace columns ``x/c, y/c, Cp`` +around the 2D airfoil section) is treated as a small database keyed by angle of +attack. Given an operating angle of attack, the closest available distribution is +selected and its **upper (suction) surface** ``|Cp|(x/c)`` is used to scale the +magnitude of the chordwise force vectors on the canopy. + +Per the modelling assumption in use, the single mid-span section distribution is +applied to every spanwise station (all airfoils share this geometry). +""" + +import re +from pathlib import Path +from typing import Callable, Dict, Tuple + +import numpy as np + + +def load_upper_surface_cp(dat_file_path: Path) -> Tuple[np.ndarray, np.ndarray]: + """Extract the upper (suction) surface ``Cp(x/c)`` from a Cp ``.dat`` loop. + + The file lists ``x/c, y/c, Cp`` around the closed airfoil section. The upper + surface is the leading-edge-to-trailing-edge branch with the more negative + (suction) mean Cp; it is returned sorted by ``x/c``. + + Args: + dat_file_path (Path): Path to a ``cp_AOA_*.dat`` file. + + Returns: + Tuple[np.ndarray, np.ndarray]: ``x/c`` (ascending) and ``Cp`` arrays. + """ + data = np.loadtxt(dat_file_path) + x = data[:, 0] + cp = data[:, 2] + n = len(x) + i_le = int(np.argmin(x)) + i_te = int(np.argmax(x)) + + def branch(a: int, b: int) -> np.ndarray: + if a <= b: + return np.arange(a, b + 1) + return np.concatenate([np.arange(a, n), np.arange(0, b + 1)]) + + branch_a = branch(i_le, i_te) + branch_b = branch(i_te, i_le) + upper = branch_a if cp[branch_a].mean() < cp[branch_b].mean() else branch_b + + x_upper = x[upper] + cp_upper = cp[upper] + order = np.argsort(x_upper) + return x_upper[order], cp_upper[order] + + +def load_cp_database(cp_dir: Path) -> Dict[float, Tuple[np.ndarray, np.ndarray]]: + """Load all ``cp_AOA_.dat`` files, keyed by angle of attack in degrees. + + Args: + cp_dir (Path): Directory of ``cp_AOA_*.dat`` files. + + Returns: + Dict[float, Tuple[np.ndarray, np.ndarray]]: Mapping ``aoa_deg -> (x/c, Cp)`` + for the upper surface. + """ + database = {} + for path in sorted(Path(cp_dir).glob("cp_AOA_*.dat")): + match = re.search(r"AOA_(-?\d+(?:\.\d+)?)", path.stem) + if not match: + continue + database[float(match.group(1))] = load_upper_surface_cp(path) + if not database: + raise FileNotFoundError(f"No 'cp_AOA_*.dat' files found in {cp_dir}.") + return database + + +def build_cp_magnitude_fn( + cp_dir: Path, angle_of_attack: float +) -> Tuple[Callable[[np.ndarray], np.ndarray], float]: + """Return a ``|Cp|(x/c)`` interpolator for the closest available AoA. + + Args: + cp_dir (Path): Directory of ``cp_AOA_*.dat`` files. + angle_of_attack (float): Operating angle of attack in degrees. + + Returns: + Tuple[Callable, float]: A function mapping ``x/c`` (array) to ``|Cp|`` and + the matched database angle of attack. + """ + database = load_cp_database(cp_dir) + available = np.array(sorted(database)) + matched_aoa = float(available[np.argmin(np.abs(available - angle_of_attack))]) + x_upper, cp_upper = database[matched_aoa] + + def magnitude(x_over_c: np.ndarray) -> np.ndarray: + return np.abs(np.interp(x_over_c, x_upper, cp_upper)) + + return magnitude, matched_aoa diff --git a/src/VSM/plotly/surfplan_runner.py b/src/VSM/plotly/surfplan_runner.py new file mode 100644 index 0000000..f7680fb --- /dev/null +++ b/src/VSM/plotly/surfplan_runner.py @@ -0,0 +1,95 @@ +"""Run SurfplanAdapter to produce the geometry files the fancy plot needs. + +The interactive "fancy" plot needs physical tube diameters and airfoil profiles +that are not present in a plain VSM aero-geometry yaml. They live in a raw +Surfplan export (a ``.txt`` plus a ``profiles/`` directory). This module drives +SurfplanAdapter to convert that export into VSM-readable yaml files, caching the +result so the conversion only runs when the export changes. +""" + +from pathlib import Path +from typing import Optional + + +def _find_surfplan_txt(surfplan_dir: Path) -> Path: + """Return the single ``.txt`` export in ``surfplan_dir`` (error if not one).""" + txt_files = sorted(surfplan_dir.glob("*.txt")) + if not txt_files: + raise FileNotFoundError( + f"No Surfplan '.txt' export found in {surfplan_dir}. Expected a raw " + "Surfplan export (a '.txt' plus a 'profiles/' directory)." + ) + if len(txt_files) > 1: + raise ValueError( + f"Multiple '.txt' files found in {surfplan_dir}: " + f"{[f.name for f in txt_files]}. Keep only the Surfplan export." + ) + return txt_files[0] + + +def ensure_surfplan_processed( + surfplan_dir: Path, output_dir: Optional[Path] = None +) -> Path: + """Convert a raw Surfplan export into VSM geometry yamls, with caching. + + Args: + surfplan_dir (Path): Directory containing the Surfplan ``.txt`` export and + a ``profiles/`` subdirectory of airfoil ``.dat`` files. + output_dir (Optional[Path]): Where to write the generated files. Defaults + to ``surfplan_dir / "vsm_processed"``. + + Returns: + Path: The directory holding the generated ``aero_geometry.yaml``, + ``struc_geometry_all_in_surfplan.yaml`` and ``profiles/``. + + Raises: + ImportError: If SurfplanAdapter is not installed. + FileNotFoundError: If the export ``.txt`` or ``profiles/`` is missing. + """ + surfplan_dir = Path(surfplan_dir) + if output_dir is None: + output_dir = surfplan_dir / "vsm_processed" + output_dir = Path(output_dir) + + txt_path = _find_surfplan_txt(surfplan_dir) + profile_load_dir = surfplan_dir / "profiles" + if not profile_load_dir.is_dir(): + raise FileNotFoundError( + f"No 'profiles/' directory in {surfplan_dir}; the fancy plot needs the " + "airfoil '.dat' files from the Surfplan export." + ) + + aero_yaml = output_dir / "aero_geometry.yaml" + struc_yaml = output_dir / "struc_geometry_all_in_surfplan.yaml" + if ( + aero_yaml.exists() + and struc_yaml.exists() + and aero_yaml.stat().st_mtime >= txt_path.stat().st_mtime + and struc_yaml.stat().st_mtime >= txt_path.stat().st_mtime + ): + return output_dir + + try: + from SurfplanAdapter.process_wing import main_process_wing + from SurfplanAdapter.process_bridle_lines import main_process_bridle_lines + from SurfplanAdapter.generate_yaml import main_generate_yaml + except ImportError as error: + raise ImportError( + "The fancy plot requires SurfplanAdapter to convert the raw Surfplan " + "export. Install it with 'pip install SurfplanAdapter'." + ) from error + + output_dir.mkdir(parents=True, exist_ok=True) + ribs_data = main_process_wing.main( + surfplan_txt_file_path=txt_path, + profile_load_dir=profile_load_dir, + profile_save_dir=output_dir / "profiles", + is_make_plots=False, + ) + bridle_lines = main_process_bridle_lines.main(txt_path) + main_generate_yaml.main( + ribs_data=ribs_data, + bridle_lines=bridle_lines, + yaml_file_path=output_dir / "config_kite.yaml", + ) + return output_dir diff --git a/src/VSM/plotly/tube_geometry.py b/src/VSM/plotly/tube_geometry.py new file mode 100644 index 0000000..0609302 --- /dev/null +++ b/src/VSM/plotly/tube_geometry.py @@ -0,0 +1,557 @@ +"""Inflatable-tube geometry for the interactive Plotly wing plot. + +Builds lofted 3D surfaces for the leading-edge tube and the chordwise strut tubes +of a LEI kite, using the physical tube-diameter tables produced by SurfplanAdapter +(``leading_edge_tubes`` and ``strut_tubes`` in +``struc_geometry_all_in_surfplan.yaml``) together with the airfoil top-surface +contours (see ``VSM.plotly.canopy_geometry``). + +Diameters are physical (metres) and sourced from the generated tables; positions +are anchored on the *drawn* ``Panel`` objects, so tubes always sit on the wing +that is actually plotted, independent of the Surfplan export's coordinate frame: + +* Leading-edge tube: centred just aft of the drawn LE polyline, diameter + interpolated from the ``leading_edge_tubes`` table by span fraction (this + captures the tapering tip, which ``t * chord`` does not). +* Struts: placed at the ``is_strut`` rib span fractions, shaped to follow the + local airfoil top-surface and offset downward so the tube top touches the + canopy underside. Per-strut LE/TE diameters come from the ``strut_tubes`` table + paired tip-to-tip with the strut ribs (both are symmetric and span-ordered, so + the pairing does not rely on the table's node indices). +""" + +from pathlib import Path +from typing import Any, Callable, Dict, List, Tuple + +import numpy as np +import plotly.graph_objects as go +import yaml + +from VSM.plotly.canopy_geometry import interpolate_contour_at, load_contour_table + +TUBE_COLOR = "black" +TUBE_OPACITY = 1.0 +RING_COLOR = "#08306b" + + +def circle_points( + center: np.ndarray, + axis: np.ndarray, + diameter: float, + n: int = 24, + ref: np.ndarray = None, +) -> np.ndarray: + """Ring of ``n`` points of the given diameter, perpendicular to ``axis``. + + Args: + center (np.ndarray): Ring centre, shape ``(3,)``. + axis (np.ndarray): Tube axis at this station; the ring lies in the plane + normal to it. + diameter (float): Ring diameter. + n (int): Number of points around the ring. Defaults to 24. + ref (np.ndarray): Optional reference vector fixing the azimuthal origin of + the ring. Passing the *same* ``ref`` for every ring of a tube keeps + consecutive rings consistently oriented, which avoids the surface + twisting where the tube tangent swings (e.g. at a wingtip). Falls back + to an automatic helper when ``ref`` is parallel to the axis. + + Returns: + np.ndarray: ``(n, 3)`` ring points, closed (first point repeated at end). + """ + axis = np.asarray(axis, dtype=float) + axis = axis / max(np.linalg.norm(axis), 1e-12) + if ref is not None: + helper = np.asarray(ref, dtype=float) + if abs(axis @ (helper / max(np.linalg.norm(helper), 1e-12))) > 0.98: + helper = np.array([0.0, 0.0, 1.0]) + if abs(axis @ helper) > 0.9: + helper = np.array([0.0, 1.0, 0.0]) + else: + helper = np.array([0.0, 0.0, 1.0]) + if abs(axis @ helper) > 0.9: + helper = np.array([0.0, 1.0, 0.0]) + e1 = np.cross(axis, helper) + e1 = e1 / max(np.linalg.norm(e1), 1e-12) + e2 = np.cross(axis, e1) + theta = np.linspace(0.0, 2.0 * np.pi, n) + radius = 0.5 * float(diameter) + return ( + np.asarray(center, dtype=float)[None, :] + + radius * np.cos(theta)[:, None] * e1[None, :] + + radius * np.sin(theta)[:, None] * e2[None, :] + ) + + +def rings_along_centerline( + centers: np.ndarray, diameters: np.ndarray, n: int = 24 +) -> List[np.ndarray]: + """Sweep circular rings along a centreline with twist-free framing. + + The ring frame is parallel-transported from one station to the next (the + previous in-plane axis is projected onto the new normal plane) instead of + being chosen independently per ring. This keeps the swept surface from + twisting or pinching where the centreline curves sharply -- e.g. where the + leading-edge tube turns chordwise at a wingtip. + + Args: + centers (np.ndarray): ``(m, 3)`` centreline points. + diameters (np.ndarray): ``(m,)`` local tube diameters. + n (int): Number of points around each ring. Defaults to 24. + + Returns: + List[np.ndarray]: One ``(n, 3)`` ring per centreline station. + """ + centers = np.asarray(centers, dtype=float) + diameters = np.asarray(diameters, dtype=float) + m = len(centers) + + tangents = np.zeros_like(centers) + tangents[1:-1] = centers[2:] - centers[:-2] + tangents[0] = centers[1] - centers[0] + tangents[-1] = centers[-1] - centers[-2] + tangents /= np.linalg.norm(tangents, axis=1, keepdims=True).clip(1e-12) + + def _seed_axis(tangent): + ref = np.array([0.0, 0.0, 1.0]) + if abs(tangent @ ref) > 0.9: + ref = np.array([0.0, 1.0, 0.0]) + axis = ref - (ref @ tangent) * tangent + return axis / max(np.linalg.norm(axis), 1e-12) + + theta = np.linspace(0.0, 2.0 * np.pi, n) + e1 = _seed_axis(tangents[0]) + rings = [] + for index in range(m): + # Parallel-transport the previous in-plane axis onto this normal plane. + e1 = e1 - (e1 @ tangents[index]) * tangents[index] + if np.linalg.norm(e1) < 1e-8: + e1 = _seed_axis(tangents[index]) + e1 /= max(np.linalg.norm(e1), 1e-12) + e2 = np.cross(tangents[index], e1) + radius = 0.5 * diameters[index] + rings.append( + centers[index][None, :] + + radius * np.cos(theta)[:, None] * e1[None, :] + + radius * np.sin(theta)[:, None] * e2[None, :] + ) + return rings + + +def load_le_diameter_interp(processed_dir: Path) -> Callable[[float], float]: + """Return a leading-edge diameter(span-fraction) interpolator. + + Builds the interpolator from the ``leading_edge_tubes`` segment table: each + segment midpoint gives a ``(|y| fraction, diameter)`` sample. Fractions are + of the half-span so the curve can be sampled against any drawn wing. + + Args: + processed_dir (Path): Directory holding + ``struc_geometry_all_in_surfplan.yaml``. + + Returns: + Callable[[float], float]: Function mapping a span fraction in ``[0, 1]`` + to a leading-edge tube diameter in metres. + """ + struc = yaml.safe_load( + (Path(processed_dir) / "struc_geometry_all_in_surfplan.yaml").read_text() + ) + particles = { + int(row[0]): np.asarray(row[1:4], dtype=float) + for row in struc["wing_particles"]["data"] + } + le = struc["leading_edge_tubes"] + idx_ci = le["headers"].index("ci") + idx_cj = le["headers"].index("cj") + idx_d = le["headers"].index("diameter") + + y_mid = [] + diam = [] + for row in le["data"]: + ci = particles[int(row[idx_ci])] + cj = particles[int(row[idx_cj])] + y_mid.append(0.5 * (ci[1] + cj[1])) + diam.append(float(row[idx_d])) + y_mid = np.abs(np.asarray(y_mid)) + diam = np.asarray(diam) + half_span = y_mid.max() if y_mid.max() > 0 else 1.0 + fractions = y_mid / half_span + + order = np.argsort(fractions) + frac_sorted = fractions[order] + diam_sorted = diam[order] + + def interp(span_fraction: float) -> float: + return float(np.interp(span_fraction, frac_sorted, diam_sorted)) + + return interp + + +def load_strut_diameter_table(processed_dir: Path) -> List[Tuple[float, float]]: + """Per-strut ``(diam_le, diam_te)`` pairs in span order (tip to tip). + + Args: + processed_dir (Path): Directory holding + ``struc_geometry_all_in_surfplan.yaml``. + + Returns: + List[Tuple[float, float]]: Leading- and trailing-edge strut-tube diameters + in metres, in the table's listed (span) order. + """ + struc = yaml.safe_load( + (Path(processed_dir) / "struc_geometry_all_in_surfplan.yaml").read_text() + ) + st = struc["strut_tubes"] + idx_le = st["headers"].index("strut_diam_le") + idx_te = st["headers"].index("strut_diam_te") + return [(float(row[idx_le]), float(row[idx_te])) for row in st["data"]] + + +def load_strut_span_fractions(processed_dir: Path) -> np.ndarray: + """Signed span fractions of the strut ribs, from the generated aero yaml. + + Uses the ``is_strut`` flag in each section's airfoil ``info_dict``. Fractions + are ``LE_y`` normalised by the half-span, returned sorted by descending span + (``+tip`` to ``-tip``) to match the ``strut_tubes`` table order. + + Args: + processed_dir (Path): Directory holding ``aero_geometry.yaml``. + + Returns: + np.ndarray: Signed span fractions of the strut ribs, high to low. + """ + aero = yaml.safe_load((Path(processed_dir) / "aero_geometry.yaml").read_text()) + sections = aero["wing_sections"] + airfoils = aero["wing_airfoils"] + idx_id = sections["headers"].index("airfoil_id") + idx_le_y = sections["headers"].index("LE_y") + a_idx_id = airfoils["headers"].index("airfoil_id") + a_idx_info = airfoils["headers"].index("info_dict") + is_strut_by_id = { + row[a_idx_id]: bool(row[a_idx_info].get("is_strut", False)) + for row in airfoils["data"] + } + + span_y = np.array([abs(row[idx_le_y]) for row in sections["data"]]) + half_span = span_y.max() if span_y.max() > 0 else 1.0 + strut_y = [ + row[idx_le_y] + for row in sections["data"] + if is_strut_by_id.get(row[idx_id], False) + ] + fractions = np.array(sorted(strut_y, reverse=True)) / half_span + return fractions + + +def _drawn_stations(panels: List[Any]) -> Dict[str, np.ndarray]: + """Spanwise edge stations of the drawn wing as stacked arrays. + + Returns leading/trailing points, chord lengths, chord and up unit vectors, and + signed span fractions for the ``n_panels + 1`` panel edges, sorted by span. + """ + le = [panels[0].LE_point_1] + [p.LE_point_2 for p in panels] + te = [panels[0].TE_point_1] + [p.TE_point_2 for p in panels] + up = [panels[0].x_airf] + for index, panel in enumerate(panels): + neighbours = [panel.x_airf] + if index + 1 < len(panels): + neighbours.append(panels[index + 1].x_airf) + up.append(np.mean(neighbours, axis=0)) + + le = np.asarray(le, dtype=float) + te = np.asarray(te, dtype=float) + up = np.asarray(up, dtype=float) + up = up / np.linalg.norm(up, axis=1, keepdims=True).clip(1e-12) + chord_vec = te - le + chord_len = np.linalg.norm(chord_vec, axis=1) + chord_hat = chord_vec / chord_len[:, None].clip(1e-12) + + half_span = np.abs(le[:, 1]).max() + half_span = half_span if half_span > 0 else 1.0 + fraction = le[:, 1] / half_span + + order = np.argsort(fraction) + return { + "le": le[order], + "te": te[order], + "up": up[order], + "chord_len": chord_len[order], + "chord_hat": chord_hat[order], + "fraction": fraction[order], + } + + +def _station_at(stations: Dict[str, np.ndarray], signed_fraction: float) -> dict: + """Interpolate a drawn-wing station at a signed span fraction.""" + frac = stations["fraction"] + le = np.array( + [np.interp(signed_fraction, frac, stations["le"][:, i]) for i in range(3)] + ) + te = np.array( + [np.interp(signed_fraction, frac, stations["te"][:, i]) for i in range(3)] + ) + up = np.array( + [np.interp(signed_fraction, frac, stations["up"][:, i]) for i in range(3)] + ) + up = up / max(np.linalg.norm(up), 1e-12) + chord_vec = te - le + chord_len = float(np.linalg.norm(chord_vec)) + chord_hat = chord_vec / max(chord_len, 1e-12) + return { + "le": le, + "te": te, + "up": up, + "chord_len": chord_len, + "chord_hat": chord_hat, + } + + +def _tip_extension(le_center, te_point, le_diameter, n=8, taper=1.0): + """Centreline points + diameters continuing from a tip LE to the TE. + + Extends the leading-edge tube at the wingtip so it runs on to the trailing + edge of the outer panel, at (by default) constant diameter for a clean tube. + """ + ts = np.linspace(0.0, 1.0, n + 1)[1:] # exclude 0 (the LE centre already exists) + le_center = np.asarray(le_center, dtype=float) + te_point = np.asarray(te_point, dtype=float) + points = [le_center + t * (te_point - le_center) for t in ts] + diams = [le_diameter * (1.0 - (1.0 - taper) * t) for t in ts] + return points, diams + + +def build_le_tube( + panels: List[Any], + le_diameter_interp: Callable[[float], float], + extend_tips: bool = True, +) -> List[np.ndarray]: + """Build the leading-edge tube as a list of cross-section rings. + + The tube runs spanwise along the drawn LE polyline; each ring is centred just + aft of the LE point (so the tube front touches the drawn leading edge) and its + diameter is looked up from the LE table at the local span fraction. With + ``extend_tips`` the tube wraps around each wingtip and continues toward the + trailing edge, tapering closed. + + Args: + panels (List[Any]): Drawn ``Panel`` objects, ordered tip to tip. + le_diameter_interp (Callable[[float], float]): Output of + :func:`load_le_diameter_interp`. + extend_tips (bool): Wrap the tube from each tip toward the trailing edge. + + Returns: + List[np.ndarray]: One ``(n, 3)`` ring per centreline station. + """ + stations = _drawn_stations(panels) + le = stations["le"] + te = stations["te"] + fraction = np.abs(stations["fraction"]) + diameters = np.array([le_diameter_interp(f) for f in fraction]) + centers = le + 0.5 * diameters[:, None] * stations["chord_hat"] + + center_list = list(centers) + diam_list = list(diameters) + if extend_tips: + neg_pts, neg_d = _tip_extension(centers[0], te[0], diameters[0]) + pos_pts, pos_d = _tip_extension(centers[-1], te[-1], diameters[-1]) + # Prepend the -tip wrap (reversed: TE -> LE) and append the +tip wrap. + center_list = neg_pts[::-1] + center_list + pos_pts + diam_list = neg_d[::-1] + diam_list + pos_d + + return rings_along_centerline(np.array(center_list), np.array(diam_list)) + + +def build_strut( + stations: Dict[str, np.ndarray], + contour_table, + signed_fraction: float, + diam_le: float, + diam_te: float, + le_tube_diameter: float = 0.0, +) -> List[np.ndarray]: + """Build one chordwise strut tube hugging the canopy underside. + + The local airfoil top-surface is placed on the drawn wing at the strut span, + then each chord station is offset downward by the local strut radius so the + tube top touches the canopy. Diameter tapers linearly from ``diam_le`` at the + leading edge to ``diam_te`` at the trailing edge. + + The strut starts at the aft edge of the leading-edge tube (chord fraction + ``le_tube_diameter / chord`` -- the LE tube spans that far back from the drawn + LE) rather than at the leading edge, so it butts against the LE tube instead + of passing straight through it. + + Args: + stations (Dict[str, np.ndarray]): Output of :func:`_drawn_stations`. + contour_table: Output of ``canopy_geometry.load_contour_table``. + signed_fraction (float): Signed span fraction of the strut rib. + diam_le (float): Strut-tube diameter at the leading edge, metres. + diam_te (float): Strut-tube diameter at the trailing edge, metres. + le_tube_diameter (float): Local leading-edge tube diameter, metres; sets + where the strut starts. Zero means start at the leading edge. + + Returns: + List[np.ndarray]: One ``(n, 3)`` ring per chordwise station. + """ + station = _station_at(stations, signed_fraction) + contour = interpolate_contour_at(abs(signed_fraction), contour_table) + chord_len = station["chord_len"] + + # Start the strut partway into the leading-edge tube (half its depth) so the + # forward cap is buried inside the LE tube -- no gap, no through-poke. + le_tube_depth_fraction = le_tube_diameter / max(chord_len, 1e-12) + start_fraction = min(0.5 * le_tube_depth_fraction, 0.9) + cx = contour[:, 0] + cy = contour[:, 1] + keep = cx > start_fraction + if start_fraction > 0 and keep.any(): + cx = np.concatenate([[start_fraction], cx[keep]]) + cy = np.concatenate( + [[float(np.interp(start_fraction, contour[:, 0], cy))], cy[keep]] + ) + + canopy_points = ( + station["le"][None, :] + + cx[:, None] * chord_len * station["chord_hat"][None, :] + + cy[:, None] * chord_len * station["up"][None, :] + ) + diameters = diam_le + (diam_te - diam_le) * cx + centers = canopy_points - 0.5 * diameters[:, None] * station["up"][None, :] + return rings_along_centerline(centers, diameters) + + +def build_tube_data(panels: List[Any], processed_dir: Path) -> Dict[str, Any]: + """Assemble leading-edge and strut tube rings for the drawn wing. + + Args: + panels (List[Any]): Drawn ``Panel`` objects, ordered tip to tip. + processed_dir (Path): Directory of SurfplanAdapter-generated files. + + Returns: + Dict[str, Any]: ``{"le": [rings], "struts": [[rings], ...]}``. + """ + processed_dir = Path(processed_dir) + le_interp = load_le_diameter_interp(processed_dir) + contour_table = load_contour_table(processed_dir) + strut_fractions = load_strut_span_fractions(processed_dir) + strut_table = load_strut_diameter_table(processed_dir) + + le_rings = build_le_tube(panels, le_interp) + + stations = _drawn_stations(panels) + struts = [] + for index, signed_fraction in enumerate(strut_fractions): + if index < len(strut_table): + diam_le, diam_te = strut_table[index] + else: + # Fallback: size the strut from the local LE tube diameter. + diam_le = le_interp(abs(signed_fraction)) + diam_te = 0.6 * diam_le + le_tube_diameter = le_interp(abs(signed_fraction)) + struts.append( + build_strut( + stations, + contour_table, + signed_fraction, + diam_le, + diam_te, + le_tube_diameter=le_tube_diameter, + ) + ) + return {"le": le_rings, "struts": struts} + + +def _rings_to_surface( + rings: List[np.ndarray], +) -> Tuple[np.ndarray, np.ndarray, np.ndarray]: + """Stack a list of rings into ``X``, ``Y``, ``Z`` surface arrays.""" + grid = np.stack(rings, axis=0) # (n_rings, n_circle, 3) + return grid[:, :, 0], grid[:, :, 1], grid[:, :, 2] + + +def _add_surface(fig: go.Figure, rings: List[np.ndarray], name: str, show_legend: bool): + """Add one tube as a uniformly coloured, semi-transparent surface.""" + x, y, z = _rings_to_surface(rings) + fig.add_trace( + go.Surface( + x=x, + y=y, + z=z, + surfacecolor=np.zeros_like(z), + colorscale=[[0, TUBE_COLOR], [1, TUBE_COLOR]], + showscale=False, + opacity=TUBE_OPACITY, + name=name, + showlegend=show_legend, + lighting=dict(ambient=0.75, diffuse=0.8, specular=0.05, roughness=0.9), + ) + ) + + +def _add_ring_cap(fig: go.Figure, ring: np.ndarray) -> None: + """Close an open tube end with a filled (fan-triangulated) cap disc.""" + ring = np.asarray(ring)[:-1] # drop the duplicated closing point + n = len(ring) + center = ring.mean(axis=0) + verts = np.vstack([center[None, :], ring]) # index 0 is the centre + i = [0] * n + j = [1 + t for t in range(n)] + k = [1 + (t + 1) % n for t in range(n)] + fig.add_trace( + go.Mesh3d( + x=verts[:, 0], + y=verts[:, 1], + z=verts[:, 2], + i=i, + j=j, + k=k, + color=TUBE_COLOR, + opacity=TUBE_OPACITY, + name="Tube cap", + showlegend=False, + ) + ) + + +def _add_tube_with_caps( + fig: go.Figure, rings: List[np.ndarray], name: str, show_legend: bool +): + """Add a tube surface and close both open ends with caps.""" + _add_surface(fig, rings, name, show_legend) + _add_ring_cap(fig, rings[0]) + _add_ring_cap(fig, rings[-1]) + + +def add_strut_surfaces(fig: go.Figure, tube_data: Dict[str, Any]) -> None: + """Add the strut tube surfaces (with end caps) to the figure.""" + for index, strut in enumerate(tube_data["struts"]): + _add_tube_with_caps(fig, strut, "Strut tube", show_legend=(index == 0)) + + +def add_le_tube_surface(fig: go.Figure, tube_data: Dict[str, Any]) -> None: + """Add the leading-edge tube surface (with end caps) to the figure.""" + _add_tube_with_caps(fig, tube_data["le"], "Leading-edge tube", show_legend=True) + + +def add_tube_surfaces(fig: go.Figure, tube_data: Dict[str, Any]) -> None: + """Add the leading-edge and strut tube surfaces to the figure.""" + add_le_tube_surface(fig, tube_data) + add_strut_surfaces(fig, tube_data) + + +def add_tube_rings(fig: go.Figure, tube_data: Dict[str, Any]) -> None: + """Add the construction rings (wireframe circles) for inspection.""" + all_rings = list(tube_data["le"]) + [ + r for strut in tube_data["struts"] for r in strut + ] + for index, ring in enumerate(all_rings): + fig.add_trace( + go.Scatter3d( + x=ring[:, 0], + y=ring[:, 1], + z=ring[:, 2], + mode="lines", + line=dict(color=RING_COLOR, width=1), + name="Tube rings", + showlegend=(index == 0), + ) + ) diff --git a/src/VSM/stability_derivatives.py b/src/VSM/stability_derivatives.py index 0489689..527008c 100644 --- a/src/VSM/stability_derivatives.py +++ b/src/VSM/stability_derivatives.py @@ -183,6 +183,7 @@ def _evaluate_with_angles(alpha_deg: float, beta_deg: float) -> CoeffDict: angle_of_attack=alpha_deg, side_slip=beta_deg, body_rates=[rates["r"], rates["q"], rates["p"]], + body_axis=[[0, 0, 1], [0, 1, 0], [1, 0, 0]], reference_point=reference_point, ) return _solve_and_extract() diff --git a/src/VSM/trim_angle.py b/src/VSM/trim_angle.py index 1207c8c..cb2402e 100644 --- a/src/VSM/trim_angle.py +++ b/src/VSM/trim_angle.py @@ -132,10 +132,6 @@ def _compute_derivative(alpha_deg: float, delta_deg: float) -> float: cmy_coarse.append(cmy_value) cmy_coarse_array = np.asarray(cmy_coarse) - import matplotlib.pyplot as plt - - plt.plot(alpha_coarse, cmy_coarse_array, marker="o") - plt.show() valid_mask = ~np.isnan(cmy_coarse_array) valid_alphas = alpha_coarse[valid_mask] valid_cmy = cmy_coarse_array[valid_mask] diff --git a/tests/AirfoilAerodynamics/test_masure.py b/tests/AirfoilAerodynamics/test_masure.py index 76abfc7..f4e9148 100644 --- a/tests/AirfoilAerodynamics/test_masure.py +++ b/tests/AirfoilAerodynamics/test_masure.py @@ -6,6 +6,14 @@ from pathlib import Path from VSM.core.AirfoilAerodynamics import AirfoilAerodynamics +# The regression models are git-ignored, so they are absent on a clean checkout (CI). +pytestmark = pytest.mark.skipif( + not Path(__file__) + .parent.parent.parent.joinpath("data", "ml_models", "ET_re1e6.pkl") + .exists(), + reason="Real model files not available", +) + class TestMasureRegressionReal: """Test cases for masure_regression with real model file integration.""" @@ -196,12 +204,6 @@ def test_masure_regression_different_parameters( assert airfoil.CD is not None, f"CD should not be None for param set {i}" assert airfoil.CM is not None, f"CM should not be None for param set {i}" - @pytest.mark.skipif( - not Path(__file__) - .parent.parent.parent.joinpath("data", "models", "ET_re1e6.pkl") - .exists(), - reason="Real model files not available", - ) def test_masure_regression_requires_model_files( self, masure_params, small_alpha_range, test_reynolds, masure_config_path ): diff --git a/tests/BodyAerodynamics/data/panel_kernel_reference.npz b/tests/BodyAerodynamics/data/panel_kernel_reference.npz new file mode 100644 index 0000000..897a218 Binary files /dev/null and b/tests/BodyAerodynamics/data/panel_kernel_reference.npz differ diff --git a/tests/BodyAerodynamics/panel_kernel_cases.py b/tests/BodyAerodynamics/panel_kernel_cases.py new file mode 100644 index 0000000..7a8431f --- /dev/null +++ b/tests/BodyAerodynamics/panel_kernel_cases.py @@ -0,0 +1,115 @@ +"""Geometry and solver cases for the force-law regression in +``test_panel_kernels.py``. + +``python tests/BodyAerodynamics/panel_kernel_cases.py OUT.npz`` writes every +numeric output of ``Solver.solve`` for every case. The committed reference +``data/panel_kernel_reference.npz`` was written with VSM at ``1201f6f``, the +last commit before the force law moved into ``VSM.core.panel_kernels``, so +the regression test pins the refactor to the original per-panel loop. +""" + +import sys +from pathlib import Path + +import numpy as np + +from VSM.core.BodyAerodynamics import BodyAerodynamics +from VSM.core.Solver import Solver +from VSM.core.WingGeometry import Wing + +REPO = Path(__file__).resolve().parents[2] +V3_FILE = REPO / "data/TUDELFT_V3_KITE/CAD_derived_geometry/aero_geometry_CAD_CFD_polars.yaml" +REFERENCE_FILE = Path(__file__).resolve().parent / "data" / "panel_kernel_reference.npz" + +_alpha = np.radians(np.linspace(-25, 25, 101)) +# Thin-airfoil lift with a small drag and moment, so all three magnitudes are exercised. +POLAR = np.column_stack( + (_alpha, 2 * np.pi * _alpha, 0.01 + 0.05 * _alpha**2, -0.1 - 0.02 * _alpha) +) + + +def _wing_body(leading_edges, chord_vector, n_panels): + wing = Wing(n_panels=n_panels, spanwise_panel_distribution="uniform") + for le in leading_edges: + wing.add_section(le, le + chord_vector, POLAR) + return BodyAerodynamics([wing]) + + +def straight_body(n_panels=16): + ys = np.linspace(-4.0, 4.0, n_panels + 1) + body = _wing_body([np.array([0.0, y, 0.0]) for y in ys], np.array([1.0, 0.0, 0.0]), n_panels) + body.va_initialize(20.0, 5.0, 0.0) + return body + + +def swept_body(n_panels=16): + """Symmetric 30 deg swept-back wing.""" + ys = np.linspace(-4.0, 4.0, n_panels + 1) + sweep = np.tan(np.radians(30.0)) + body = _wing_body( + [np.array([sweep * abs(y), y, 0.0]) for y in ys], np.array([1.0, 0.0, 0.0]), n_panels + ) + body.va_initialize(15.0, 6.0, 2.0) + return body + + +def arc_body(n_panels=16): + """Circular-arc wing (kite-like anhedral), constant chord along x.""" + th = np.radians(np.linspace(-55.0, 55.0, n_panels + 1)) + les = [np.array([0.0, 4.0 * np.sin(t), 4.0 * np.cos(t)]) for t in th] + body = _wing_body(les, np.array([1.0, 0.0, 0.0]), n_panels) + body.va_initialize(10.0, 8.0, 0.0) + return body + + +def v3_body(n_panels=20, yaw_rate=0.0): + body = BodyAerodynamics.instantiate(n_panels=n_panels, file_path=V3_FILE) + body.va_initialize(10.0, 8.0, 3.0, np.array([0.0, 0.0, yaw_rate])) + return body + + +# name -> (body factory, solver kwargs) +CASES = { + "straight": (straight_body, {}), + "swept": (swept_body, {}), + "arc": (arc_body, {}), + "v3": (v3_body, {}), + "v3_viscous_noAT": ( + v3_body, + {"is_with_viscous_drag_correction": True, "is_with_attached_trailed_vortex_force": False}, + ), + "v3_yaw_rate": (lambda: v3_body(yaw_rate=0.4), {"reference_point": [0.1, 0.0, 0.5]}), +} + + +def numeric_outputs(results: dict) -> dict: + """Every numeric entry of a ``solve`` result, as float arrays.""" + out = {} + for key, value in results.items(): + try: + array = np.asarray(value, dtype=float) + except (TypeError, ValueError): + continue + out[key] = array + return out + + +def run_case(name: str, is_aoa_corrected: bool) -> dict: + factory, kwargs = CASES[name] + solver = Solver(allowed_error=1e-10, is_aoa_corrected=is_aoa_corrected, **kwargs) + results = solver.solve(factory()) + assert results["gamma_converged"] + return numeric_outputs(results) + + +def all_outputs() -> dict: + flat = {} + for name in CASES: + for corrected in (True, False): + for key, value in run_case(name, corrected).items(): + flat[f"{name}|{corrected}|{key}"] = value + return flat + + +if __name__ == "__main__": + np.savez(sys.argv[1], **all_outputs()) diff --git a/tests/BodyAerodynamics/test_curved_bound_vortex.py b/tests/BodyAerodynamics/test_curved_bound_vortex.py new file mode 100644 index 0000000..574682c --- /dev/null +++ b/tests/BodyAerodynamics/test_curved_bound_vortex.py @@ -0,0 +1,141 @@ +"""The quarter-chord direction velocity on curved lifting lines. + +A lifting line evaluated on itself has a logarithmically singular +self-induction wherever it is curved. The regularised correction in +``assemble_curved_bound_correction_AIC`` must (a) vanish on a straight wing, +so the classical exact result is kept, and (b) make the on-blade induced drag +equal to the Trefftz-plane value on curved wings independently of the panel +count. Inviscid polars, so the on-blade drag is pure induced drag. +""" + +import numpy as np +import pytest + +from VSM.core.BodyAerodynamics import BodyAerodynamics +from VSM.core.Solver import Solver +from VSM.core.WingGeometry import Wing +from VSM.core.utils import assemble_curved_bound_correction_AIC, thin_airfoil_strips + +_alpha = np.radians(np.linspace(-25, 25, 101)) +INVISCID = np.column_stack((_alpha, 2 * np.pi * _alpha, 0 * _alpha, 0 * _alpha)) + + +def _straight(n_panels, half_span=4.0): + wing = Wing(n_panels=n_panels, spanwise_panel_distribution="uniform") + for y in np.linspace(-half_span, half_span, n_panels + 1): + wing.add_section(np.array([0.0, y, 0.0]), np.array([1.0, y, 0.0]), INVISCID) + body = BodyAerodynamics([wing], leg_direction="rib") # chordwise legs isolate the bound-vortex term + body.va_initialize(20.0, 5.0, 0.0) + return body + + +def _arc(n_panels, radius=4.0, half_angle_deg=55.0): + """Circular-arc wing (kite-like anhedral), constant chord 1 along x.""" + wing = Wing(n_panels=n_panels, spanwise_panel_distribution="uniform") + for th in np.radians(np.linspace(-half_angle_deg, half_angle_deg, n_panels + 1)): + le = np.array([0.0, radius * np.sin(th), radius * np.cos(th)]) + wing.add_section(le, le + np.array([1.0, 0.0, 0.0]), INVISCID) + body = BodyAerodynamics([wing], leg_direction="rib") # chordwise legs isolate the bound-vortex term + body.va_initialize(10.0, 8.0, 0.0) + return body + + +def _ratio(body): + results = Solver(allowed_error=1e-7, max_iterations=4000).solve(body) + assert results["gamma_converged"] + return results["drag"] / results["drag_induced_trefftz"] + + +def test_strip_weights_are_thin_airfoil_with_quarter_chord_centroid(): + x, w = thin_airfoil_strips(24) + assert np.isclose(w.sum(), 1.0) + assert np.isclose(np.sum(w * x), 0.25, atol=2e-3) + # effective cut-off exp() of the distribution is 0.15 c + assert np.isclose(np.exp(np.sum(w * np.log(np.abs(x - 0.25)))), 0.15, atol=0.02) + + +def test_correction_vanishes_on_a_straight_wing(): + body = _straight(20) + panels = body.panels + ep = np.array([p.aerodynamic_center for p in panels]) + bp1 = np.array([p.bound_point_1 for p in panels]) + bp2 = np.array([p.bound_point_2 for p in panels]) + te1 = np.array([p.TE_point_1 for p in panels]) + te2 = np.array([p.TE_point_2 for p in panels]) + y_airf = np.array([p.y_airf for p in panels]) + chords = np.array([p.chord for p in panels]) + x, w = thin_airfoil_strips() + A = assemble_curved_bound_correction_AIC( + ep, bp1, bp2, te1, te2, y_airf, chords, np.full(20, 20.0), 0.25, 0.05, x, w + ) + assert np.max(np.abs(A)) < 1e-9 + + +def test_straight_wing_on_blade_drag_equals_trefftz(): + assert np.isclose(_ratio(_straight(40)), 1.0, atol=2e-3) + + +@pytest.mark.parametrize("n_panels", [20, 40, 80]) +def test_arc_wing_on_blade_drag_equals_trefftz_at_every_resolution(n_panels): + # Before the regularisation this ratio was 0.977 / 0.957 / 0.937 at + # 20 / 40 / 80 panels and kept falling (log-singular self-induction). + assert np.isclose(_ratio(_arc(n_panels)), 1.0, atol=1e-2) + + +def test_arc_wing_ratio_is_mesh_independent(): + r40 = _ratio(_arc(40)) + r80 = _ratio(_arc(80)) + assert abs(r80 - r40) < 3e-3 + + +def test_curved_bound_correction_is_cached_per_geometry(): + """The quarter-chord correction depends on the geometry only: a second + inflow on the same panels reuses it, a moved geometry recomputes it.""" + import VSM.core.BodyAerodynamics as BA + + calls = [] + original = BA.assemble_curved_bound_correction_AIC + + def counting(*args, **kwargs): + calls.append(1) + return original(*args, **kwargs) + + BA.assemble_curved_bound_correction_AIC = counting + try: + body = _arc(16) + solver = Solver(allowed_error=1e-7, max_iterations=4000) + body.va_initialize(10.0, 8.0, 0.0) + r1 = solver.solve(body) + body.va_initialize(20.0, 4.0, 5.0) + r2 = solver.solve(body) + assert len(calls) == 1, "same geometry, different inflow: one kernel evaluation" + # a fresh body with the same geometry gives the same answer as the cached one + body_ref = _arc(16) + body_ref.va_initialize(20.0, 4.0, 5.0) + r_ref = solver.solve(body_ref) + assert np.isclose(r2["cl"], r_ref["cl"], rtol=1e-12) + assert np.isclose(r2["cd"], r_ref["cd"], rtol=1e-12) + # a rigid rotation keeps the shape: the stored matrix is rotated, not rebuilt, + # and the result equals that of a body built in the rotated position + body.rotate(angle_deg=5.0, axis=[0.0, 1.0, 0.0]) + body.va_initialize(20.0, 4.0, 5.0) + r_rot = solver.solve(body) + assert len(calls) == 2 + body_rot = _arc(16) + body_rot.rotate(angle_deg=5.0, axis=[0.0, 1.0, 0.0]) + body_rot.va_initialize(20.0, 4.0, 5.0) + r_rot_ref = solver.solve(body_rot) + assert len(calls) == 3 + assert np.isclose(r_rot["cl"], r_rot_ref["cl"], rtol=1e-10) + assert np.isclose(r_rot["cd"], r_rot_ref["cd"], rtol=1e-10) + # a deformation (different shape) recomputes + wing = body.wings[0] + le = np.array([sec.LE_point for sec in wing.sections]) + te = np.array([sec.TE_point for sec in wing.sections]) + te[len(te) // 2] += np.array([0.1, 0.0, 0.0]) + body.update_from_points(le, te, "reuse_initial_polar_data", [sec.polar_data for sec in wing.sections]) + body.va_initialize(20.0, 4.0, 5.0) + solver.solve(body) + assert len(calls) == 4 + finally: + BA.assemble_curved_bound_correction_AIC = original diff --git a/tests/BodyAerodynamics/test_leg_direction.py b/tests/BodyAerodynamics/test_leg_direction.py new file mode 100644 index 0000000..95618fa --- /dev/null +++ b/tests/BodyAerodynamics/test_leg_direction.py @@ -0,0 +1,194 @@ +"""Direction of the attached-trailed legs: along the ribs (default), along +the mean flow, or perpendicular to the quarter-chord line +(``BodyAerodynamics(leg_direction=...)``). + +Perpendicular legs depend only on the quarter-chord line and the +trailing-edge curve, so (a) they coincide with the ribs wherever the ribs are +perpendicular, (b) a straight wing whose ribs are drawn skewed gives the same +vortex system and the same forces as the same wing with straight ribs, and +(c) a kinked swept wing builds without crossing legs, with the root leg on the +symmetry plane. Inviscid polars. +""" + +import numpy as np +import pytest + +from VSM.core.BodyAerodynamics import BodyAerodynamics +from VSM.core.Solver import Solver +from VSM.core.WingGeometry import Wing +from VSM.core.utils import perpendicular_leg_end_points + +_alpha = np.radians(np.linspace(-25, 25, 101)) +INVISCID = np.column_stack((_alpha, 2 * np.pi * _alpha, 0 * _alpha, 0 * _alpha)) + + +def _straight(n, skew=0.0, **kw): + wing = Wing(n_panels=n, spanwise_panel_distribution="uniform") + for y in np.linspace(-4.0, 4.0, n + 1): + wing.add_section(np.array([0.0, y, 0.0]), np.array([1.0, y + skew, 0.0]), INVISCID) + body = BodyAerodynamics([wing], **{"leg_direction": "rib", **kw}) + body.va_initialize(20.0, 5.0, 0.0) + return body + + +def _swept(n, sweep_deg, **kw): + """Kinked swept wing, chord 1, ribs along the freestream.""" + wing = Wing(n_panels=n, spanwise_panel_distribution="uniform") + for y in np.linspace(-4.0, 4.0, n + 1): + x = abs(y) * np.tan(np.radians(sweep_deg)) + wing.add_section(np.array([x, y, 0.0]), np.array([x + 1.0, y, 0.0]), INVISCID) + body = BodyAerodynamics([wing], **{"leg_direction": "rib", **kw}) + body.va_initialize(20.0, 5.0, 0.0) + return body + + +def _arc(n, **kw): + wing = Wing(n_panels=n, spanwise_panel_distribution="uniform") + for th in np.radians(np.linspace(-55.0, 55.0, n + 1)): + le = np.array([0.0, 4.0 * np.sin(th), 4.0 * np.cos(th)]) + wing.add_section(le, le + np.array([1.0, 0.0, 0.0]), INVISCID) + body = BodyAerodynamics([wing], **{"leg_direction": "rib", **kw}) + body.va_initialize(10.0, 8.0, 0.0) + return body + + +def _legs(body): + P = body.panels + bound = np.array([p.bound_point_1 for p in P] + [P[-1].bound_point_2]) + te = np.array([p.TE_point_1 for p in P] + [P[-1].TE_point_2]) + return bound, te + + +def _solve(body): + r = Solver(allowed_error=1e-7, max_iterations=4000).solve(body) + assert r["gamma_converged"] + return r + + +def test_unknown_leg_direction_raises(): + with pytest.raises(ValueError): + _straight(8, leg_direction="diagonal") + + +@pytest.mark.parametrize("make", [_straight, _arc]) +def test_perpendicular_legs_equal_ribs_where_ribs_are_perpendicular(make): + rib = make(16) + perp = make(16, leg_direction="perpendicular") + assert perp.leg_direction == "perpendicular" + for p_rib, p_perp in zip(rib.panels, perp.panels): + assert np.allclose(p_rib.TE_point_1, p_perp.TE_point_1, atol=1e-10) + assert np.allclose(p_rib.TE_point_2, p_perp.TE_point_2, atol=1e-10) + assert np.allclose(p_rib.control_point, p_perp.control_point, atol=1e-10) + assert np.isclose(p_rib.chord, p_perp.chord) + + +def test_skewed_ribs_give_the_straight_wing(): + """The surface is the same rectangle whichever way the ribs are drawn.""" + reference = _straight(20) + skewed = _straight(20, skew=0.4, leg_direction="perpendicular") + bound, te = _legs(skewed) + # legs along x, ending on the trailing-edge line x = 1 at the node's own y + assert np.allclose(te[:, 0], 1.0, atol=1e-10) + assert np.allclose(te[:, 1], bound[:, 1], atol=1e-10) + r_ref = _solve(reference) + r_skw = _solve(skewed) + assert np.isclose(r_skw["cl"], r_ref["cl"], rtol=1e-6) + assert np.isclose(r_skw["cd"], r_ref["cd"], rtol=1e-5) + assert np.isclose(r_skw["drag"] / r_skw["drag_induced_trefftz"], 1.0, atol=1e-3) + + +def test_skewed_rib_legs_are_not_the_straight_wing(): + """The reference behaviour: rib legs carry the skew into the result.""" + reference = _solve(_straight(20)) + skewed = _solve(_straight(20, skew=0.4)) + assert abs(skewed["cd"] / reference["cd"] - 1.0) > 5e-3 + + +def test_kinked_swept_wing_builds_without_crossing_legs(): + body = _swept(40, 30.0, leg_direction="perpendicular") + bound, te = _legs(body) + # ends ordered along the trailing edge (panels run from +y to -y) + assert np.all(np.diff(te[:, 1]) < 0.0) + # root leg on the symmetry plane, streamwise; away from the root the legs + # turn towards the perpendicular (aft and inboard), one leg length away + k = len(bound) // 2 + assert np.isclose(bound[k, 1], 0.0, atol=1e-12) + assert np.isclose(te[k, 1], 0.0, atol=1e-10) + leg = te - bound + j = k - 6 # 1.2 chords from the root at 0.2 panel width, outside the averaging window + inboard = -np.sign(bound[j, 1]) * leg[j, 1] + assert np.isclose(inboard / np.linalg.norm(leg[j]), np.sin(np.radians(30.0)), atol=1e-6) + r = _solve(body) + assert np.isfinite(r["cl"]) and np.isfinite(r["cd"]) + assert 0.9 < r["drag"] / r["drag_induced_trefftz"] < 1.1 + + +def test_crossing_legs_raise(): + with pytest.raises(ValueError, match="cross|does not reach"): + _swept(40, 75.0, leg_direction="perpendicular") + + +def test_leg_end_points_function_on_a_uniformly_swept_line(): + """On a line of constant sweep the legs are the plain perpendiculars.""" + sweep = np.radians(30.0) + n = 10 + s = np.linspace(0.0, 5.0, n + 1) + bound = np.column_stack((s * np.sin(sweep), s * np.cos(sweep), 0 * s)) + perp = np.array([np.cos(sweep), -np.sin(sweep), 0.0]) + te = bound + 0.75 * perp + np.array([0.0, 0.0, 0.0]) + chord_dir = np.tile(perp, (n, 1)) + ends = perpendicular_leg_end_points(bound, te, chord_dir, np.ones(n), 0.25) + assert np.allclose(ends, te, atol=1e-10) + + +def test_streamwise_legs_equal_ribs_drawn_along_the_flow(): + """On the kinked swept wing the ribs are along x, as are streamwise legs.""" + rib = _swept(40, 30.0) + stream = _swept(40, 30.0, leg_direction="streamwise") + for p_rib, p_s in zip(rib.panels, stream.panels): + assert np.allclose(p_rib.TE_point_1, p_s.TE_point_1, atol=1e-10) + assert np.allclose(p_rib.control_point, p_s.control_point, atol=1e-10) + + +def test_streamwise_legs_on_skewed_ribs_give_the_straight_wing(): + reference = _solve(_straight(20)) + skewed = _solve(_straight(20, skew=0.4, leg_direction="streamwise")) + assert np.isclose(skewed["cl"], reference["cl"], rtol=1e-6) + assert np.isclose(skewed["cd"], reference["cd"], rtol=1e-5) + + +def test_streamwise_legs_follow_the_inflow(): + """Setting a sideslip rebuilds the legs along the new apparent velocity.""" + body = _straight(16, skew=0.4, leg_direction="streamwise") + te_0 = np.array([p.TE_point_1 for p in body.panels]) + body.va_initialize(20.0, 5.0, 15.0) + te_1 = np.array([p.TE_point_1 for p in body.panels]) + bound = np.array([p.bound_point_1 for p in body.panels]) + legs = te_1 - bound + # legs lie in the plane of the flat wing and point along the yawed flow + assert np.allclose(legs[:, 2], 0.0, atol=1e-12) + va = np.atleast_2d(body.va)[0] + expected_yaw = np.degrees(np.arctan2(va[1], va[0])) + yaw = np.degrees(np.arctan2(legs[:, 1], legs[:, 0])) + assert np.allclose(yaw, expected_yaw, atol=1e-9) + assert not np.allclose(te_0, te_1) + + +def test_streamwise_legs_fall_back_to_ribs_for_pure_rotation(caplog): + """A body that only rotates has no mean inflow direction: the rib legs + are used, with a warning, and the solve goes through.""" + import logging + + body = _straight(12, skew=0.4, leg_direction="streamwise") + rib_te = np.array([p._rib_TE_point_1 for p in body.panels]) + centre = np.mean([p.control_point for p in body.panels], axis=0) + with caplog.at_level(logging.WARNING): + body.va_initialize(Umag=0.0, angle_of_attack=0.0, side_slip=0.0, + body_rates=[0.3, -0.2, 0.1], + body_axis=[[0, 0, 1], [0, 1, 0], [1, 0, 0]], + reference_point=centre) + assert any("rib legs" in m for m in caplog.messages) + assert np.allclose(np.array([p.TE_point_1 for p in body.panels]), rib_te) + # a proper inflow restores the streamwise legs + body.va_initialize(20.0, 5.0, 0.0) + assert not np.allclose(np.array([p.TE_point_1 for p in body.panels]), rib_te) diff --git a/tests/BodyAerodynamics/test_panel_kernels.py b/tests/BodyAerodynamics/test_panel_kernels.py new file mode 100644 index 0000000..cc20943 --- /dev/null +++ b/tests/BodyAerodynamics/test_panel_kernels.py @@ -0,0 +1,303 @@ +"""The section force law as ``xp`` kernels (``VSM.core.panel_kernels``). + +Contract: + +* moving the force law into the kernels changed no output: every numeric + result of ``Solver.solve`` equals the one of VSM ``1201f6f`` (the commit + before the move) to round-off, for ``is_aoa_corrected`` True and False, on + a straight, a swept, a curved (arc) wing and the LEI V3 kite; +* the public quarter-chord induction matrices are the ones the force + directions are computed from; +* every kernel gives the same numbers with ``xp=casadi`` as with + ``xp=numpy``; +* a CasADi graph with the induction matrices as parameters reproduces the + per-panel forces of ``compute_results``. +""" + +import sys +from pathlib import Path + +import numpy as np +import pytest + +sys.path.insert(0, str(Path(__file__).resolve().parent)) + +from panel_kernel_cases import CASES, REFERENCE_FILE, run_case, v3_body, arc_body # noqa: E402 +from VSM.core import panel_kernels # noqa: E402 +from VSM.core.Solver import Solver # noqa: E402 +from VSM.core.utils import thin_airfoil_strips, assemble_curved_bound_correction_AIC # noqa: E402 + +RTOL = 1e-12 + +_FORCE_KEYS = ("Fx", "Fy", "Fz") +_MOMENT_KEYS = ("Mx", "My", "Mz") + + +# ----------------------------------------------------------- no-change regression +@pytest.fixture(scope="module") +def reference(): + return np.load(REFERENCE_FILE) + + +@pytest.mark.parametrize("is_aoa_corrected", [True, False]) +@pytest.mark.parametrize("case", list(CASES)) +def test_solve_outputs_match_reference_commit(reference, case, is_aoa_corrected): + """Every numeric output equals VSM 1201f6f to round-off. Entries that + vanish by symmetry (side force of a symmetric wing, ...) are compared + against the case's force or moment scale instead of their own.""" + outputs = run_case(case, is_aoa_corrected) + prefix = f"{case}|{is_aoa_corrected}|" + expected_keys = {k[len(prefix):] for k in reference.files if k.startswith(prefix)} + assert set(outputs) == expected_keys + family_scale = { + "force": max(abs(float(reference[prefix + k])) for k in _FORCE_KEYS), + "moment": max(abs(float(reference[prefix + k])) for k in _MOMENT_KEYS), + "other": max(abs(float(reference[prefix + k])) for k in ("cl", "cd")), + } + for key, value in outputs.items(): + ref = reference[prefix + key] + assert value.shape == ref.shape, key + if not ref.size: + continue + if key[0] == "F" or key in ("lift", "drag", "side", "drag_induced_trefftz", "bridle_line_forces"): + family = "force" + elif key[0] == "M": + family = "moment" + else: + family = "other" + scale = max(np.max(np.abs(ref)), 1e-3 * family_scale[family]) + np.testing.assert_allclose(value, ref, rtol=0.0, atol=RTOL * scale, err_msg=key) + + +# ------------------------------------------------------ quarter-chord induction +@pytest.mark.parametrize("factory", [arc_body, v3_body]) +def test_quarter_chord_matrices_are_llt_plus_curved_correction(factory): + """The public matrices equal the trailed system on the quarter chord plus + the curved bound-vortex correction, assembled independently here.""" + body = factory() + va = np.array([p.va for p in body.panels]) + va_norm = np.linalg.norm(va, axis=1) + va_unit = va / va_norm[:, None] + core = 0.05 + public = body.compute_quarter_chord_AIC_matrices(core, va_norm, va_unit) + + llt = body.compute_AIC_matrices("LLT", core, va_norm, va_unit) + rows = lambda name: np.ascontiguousarray([getattr(p, name) for p in body.panels], dtype=float) # noqa: E731 + strip_x, strip_w = thin_airfoil_strips() + correction = assemble_curved_bound_correction_AIC( + rows("aerodynamic_center"), rows("bound_point_1"), rows("bound_point_2"), + rows("TE_point_1"), rows("TE_point_2"), rows("y_airf"), + np.array([p.chord for p in body.panels]), va_norm, + float(body._aerodynamic_center_location), core, strip_x, strip_w, + ) + for k in range(3): + assert public[k].shape == (body.n_panels, body.n_panels) + np.testing.assert_allclose(public[k], llt[k] + correction[k], rtol=1e-13, atol=1e-15) + if factory is arc_body: # curved: the correction is not negligible + assert np.max(np.abs(correction)) > 1e-3 * np.max(np.abs(llt)) + + +def test_quarter_chord_direction_angle_uses_the_public_matrices(): + body = v3_body() + solver = Solver(allowed_error=1e-10) + results = solver.solve(body) + va_norm = np.linalg.norm(solver.va_array, axis=1) + AIC_qc = body.compute_quarter_chord_AIC_matrices( + solver.core_radius_fraction, va_norm, solver.va_array / va_norm[:, None] + ) + gamma = results["gamma_distribution"] + v_qc = solver.va_array + np.column_stack([A @ gamma for A in AIC_qc]) + alpha_qc = np.arctan2( + np.sum(solver.x_airf_array * v_qc, axis=1), np.sum(solver.y_airf_array * v_qc, axis=1) + ) + np.testing.assert_allclose(np.ravel(results["alpha_at_ac"]), alpha_qc, rtol=0, atol=1e-14) + + +# --------------------------------------------------------- casadi == numpy +@pytest.fixture(scope="module") +def solved_v3(): + """A converged V3 solve and the arrays a force-law caller needs.""" + body = v3_body() + solver = Solver( + allowed_error=1e-10, + is_aoa_corrected=True, + is_with_attached_trailed_vortex_force=True, + ) + results = solver.solve(body) + va_norm = np.linalg.norm(solver.va_array, axis=1) + AIC_qc = body.compute_quarter_chord_AIC_matrices( + solver.core_radius_fraction, va_norm, solver.va_array / va_norm[:, None] + ) + return body, solver, results, AIC_qc + + +def _numpy_inputs(solver, results, AIC_qc): + gamma = np.asarray(results["gamma_distribution"], dtype=float) + aic = (solver.AIC_x, solver.AIC_y, solver.AIC_z) + v_rel = panel_kernels.quarter_chord_velocity(solver.va_array, gamma, *aic) + alpha, umag = panel_kernels.relative_flow( + v_rel, solver.x_airf_array, solver.y_airf_array, solver.z_airf_array + ) + panels = solver.panels + return { + "gamma": gamma, + "v_rel": v_rel, + "alpha": alpha, + "umag": umag, + "cl": np.array([p.compute_cl(a) for p, a in zip(panels, alpha)]), + "cd": np.array([p.compute_cd_cm(a)[0] for p, a in zip(panels, alpha)]), + "cm": np.array([p.compute_cd_cm(a)[1] for p, a in zip(panels, alpha)]), + "AIC_qc": AIC_qc, + } + + +def test_kernels_casadi_equals_numpy(solved_v3): + ca = pytest.importorskip("casadi") + body, solver, results, AIC_qc = solved_v3 + d = _numpy_inputs(solver, results, AIC_qc) + x, y, z = solver.x_airf_array, solver.y_airf_array, solver.z_airf_array + chord, width, rho = solver.chord_array, solver.width_array, solver.rho + + col = lambda v: ca.DM(np.asarray(v, dtype=float).reshape(-1, 1)) # noqa: E731 + mat = lambda a: ca.DM(np.asarray(a, dtype=float)) # noqa: E731 + + def same(ca_value, np_value): + ca_array = np.array(ca_value, dtype=float).reshape(np.shape(np_value)) + scale = max(np.max(np.abs(np_value)), 1e-300) + np.testing.assert_allclose(ca_array, np_value, rtol=0, atol=RTOL * scale) + + # flow + same(panel_kernels.relative_flow(mat(d["v_rel"]), mat(x), mat(y), mat(z), xp=ca)[0], d["alpha"]) + same(panel_kernels.relative_flow(mat(d["v_rel"]), mat(x), mat(y), mat(z), xp=ca)[1], d["umag"]) + v_qc_np = panel_kernels.quarter_chord_velocity(solver.va_array, d["gamma"], *AIC_qc) + v_qc_ca = panel_kernels.quarter_chord_velocity( + mat(solver.va_array), col(d["gamma"]), *[mat(A) for A in AIC_qc], xp=ca + ) + same(v_qc_ca, v_qc_np) + alpha_dir_np = panel_kernels.direction_angle(v_qc_np, x, y) + same(panel_kernels.direction_angle(v_qc_ca, mat(x), mat(y), xp=ca), alpha_dir_np) + + # force law, both direction laws + for alpha_dir in (None, alpha_dir_np): + np_out = panel_kernels.panel_forces( + d["alpha"], d["umag"], d["cl"], d["cd"], d["cm"], chord, width, x, y, z, rho, + alpha_dir=alpha_dir, + ) + ca_out = panel_kernels.panel_forces( + col(d["alpha"]), col(d["umag"]), col(d["cl"]), col(d["cd"]), col(d["cm"]), + col(chord), col(width), mat(x), mat(y), mat(z), rho, + alpha_dir=None if alpha_dir is None else col(alpha_dir), xp=ca, + ) + for ca_value, np_value in zip(ca_out, np_out): + same(ca_value, np_value) + np_vec = panel_kernels.section_force_vectors( + d["alpha"], d["umag"], d["cl"], d["cd"], d["cm"], chord, x, y, z, rho, + alpha_dir=alpha_dir, + ) + ca_vec = panel_kernels.section_force_vectors( + col(d["alpha"]), col(d["umag"]), col(d["cl"]), col(d["cd"]), col(d["cm"]), + col(chord), mat(x), mat(y), mat(z), rho, + alpha_dir=None if alpha_dir is None else col(alpha_dir), xp=ca, + ) + for ca_value, np_value in zip(ca_vec, np_vec): + same(ca_value, np_value) + + # attached trailed vortex force + legs = np.random.default_rng(0).normal(size=(body.n_panels + 1, 3)) + v_ind = np.random.default_rng(1).normal(size=(body.n_panels + 1, 3)) + same( + panel_kernels.attached_trailed_forces( + col(d["gamma"]), mat(solver.va_array), mat(v_ind), legs, rho, xp=ca + ), + panel_kernels.attached_trailed_forces(d["gamma"], solver.va_array, v_ind, legs, rho), + ) + + +# ------------------------------------------- casadi graph == compute_results +def _boundary_shares(n): + """(n, n+1) split of boundary loads onto panels, compute_results' rule.""" + shares = np.zeros((n, n + 1)) + shares[0, 0] = 1.0 + shares[n - 1, n] = 1.0 + for j in range(1, n): + shares[j - 1, j] = shares[j, j] = 0.5 + return shares + + +def _compute_results_at(body, solver, gamma): + """compute_results for this ``gamma`` with ``alpha``/``umag`` evaluated at + the same ``gamma``. (``solve`` passes the loop's last alpha, which lags the + returned gamma by one iteration, i.e. agrees with it only to the + convergence tolerance.)""" + alpha, umag, _ = solver.compute_aerodynamic_quantities(gamma) + va_norm = np.linalg.norm(solver.va_array, axis=1) + return body.compute_results( + gamma, solver.rho, solver.aerodynamic_model_type, solver.core_radius_fraction, + solver.mu, alpha, umag, solver.chord_array, solver.x_airf_array, + solver.y_airf_array, solver.z_airf_array, solver.va_array, va_norm, + solver.va_array / va_norm[:, None], solver.panels, False, + solver.is_with_viscous_drag_correction, solver.reference_point, + solver.is_aoa_corrected, + relative_velocity_array=solver.compute_relative_velocity(gamma), + is_with_attached_trailed_vortex_force=solver.is_with_attached_trailed_vortex_force, + ) + + +@pytest.mark.parametrize("is_aoa_corrected", [True, False]) +def test_casadi_graph_reproduces_compute_results_forces(is_aoa_corrected): + """Graph: gamma, va and the 3/4-chord and quarter-chord matrices are MX + parameters; ``v_rel`` and ``v_qc`` come from ``quarter_chord_velocity``, + the section forces from ``Solver.casadi_panel_force_function``. Evaluated + at a converged gamma it reproduces compute_results' panel forces and + moments (the attached-trailed part removed, as it is not in the + function).""" + ca = pytest.importorskip("casadi") + body = v3_body() + solver = Solver(allowed_error=1e-10, is_aoa_corrected=is_aoa_corrected) + gamma_converged = solver.solve(body)["gamma_distribution"] + results = _compute_results_at(body, solver, gamma_converged) + n = solver.n_panels + va_norm = np.linalg.norm(solver.va_array, axis=1) + AIC_qc = body.compute_quarter_chord_AIC_matrices( + solver.core_radius_fraction, va_norm, solver.va_array / va_norm[:, None] + ) + + gamma = ca.MX.sym("gamma", n) + va = ca.MX.sym("va", n, 3) + A_cp = [ca.MX.sym(f"A_cp_{k}", n, n) for k in range(3)] + A_qc = [ca.MX.sym(f"A_qc_{k}", n, n) for k in range(3)] + v_rel = panel_kernels.quarter_chord_velocity(va, gamma, *A_cp, xp=ca) + v_qc = panel_kernels.quarter_chord_velocity(va, gamma, *A_qc, xp=ca) if is_aoa_corrected else v_rel + forces_fn = solver.casadi_panel_force_function() + force, moment_local = forces_fn( + v_rel, v_qc, solver.x_airf_array, solver.y_airf_array, solver.z_airf_array, + solver.chord_array, solver.width_array, solver.rho, + )[:2] + graph = ca.Function("forces", [gamma, va, *A_cp, *A_qc], [force, moment_local]) + + force_num, moment_num = graph( + gamma_converged, solver.va_array, + solver.AIC_x, solver.AIC_y, solver.AIC_z, *AIC_qc, + ) + force_num = np.array(force_num) + moment_num = np.array(moment_num) + + shares = _boundary_shares(n) + f_at = np.array(results["F_attached_trailed_distribution"]) + assert f_at.shape == (n + 1, 3) # AT force on (the default) + expected_force = np.array(results["F_distribution"]) - shares @ f_at + reference_point = np.zeros(3) + ac = np.array([p.aerodynamic_center for p in solver.panels]) + at_points = body.compute_attached_trailed_vortex_forces( + gamma_converged, solver.rho, solver.core_radius_fraction, solver.va_array + )[1] + expected_moment = ( + np.array(results["M_distribution"]) + - shares @ np.cross(at_points - reference_point, f_at) + - np.cross(ac - reference_point, expected_force) + ) + f_scale = np.max(np.abs(expected_force)) + np.testing.assert_allclose(force_num, expected_force, rtol=0, atol=RTOL * f_scale) + np.testing.assert_allclose( + moment_num, expected_moment, rtol=0, atol=RTOL * np.max(np.abs(expected_moment)) + ) diff --git a/tests/BodyAerodynamics/test_wing_aero_object_against_create_geometry_general.py b/tests/BodyAerodynamics/test_wing_aero_object_against_create_geometry_general.py index 4bd33fb..5c86206 100644 --- a/tests/BodyAerodynamics/test_wing_aero_object_against_create_geometry_general.py +++ b/tests/BodyAerodynamics/test_wing_aero_object_against_create_geometry_general.py @@ -216,7 +216,7 @@ def create_geometry(model="VSM", wing_type="rectangular", plotting=False, N=40): coord_left_to_right[2 * idx + 1], polar_data, ) - wing_aero = BodyAerodynamics([wing]) + wing_aero = BodyAerodynamics([wing], leg_direction="rib") # thesis vortex system wing_aero.va = Uinf if plotting: wing_aero.plot() diff --git a/tests/Solver/test_casadi_polar_table_inputs.py b/tests/Solver/test_casadi_polar_table_inputs.py new file mode 100644 index 0000000..34ed806 --- /dev/null +++ b/tests/Solver/test_casadi_polar_table_inputs.py @@ -0,0 +1,168 @@ +"""Polar tables as inputs of the CasADi panel functions (``table_inputs=True``). + +Contract: with the tables passed as inputs, the residual and panel-force +functions give the same outputs as the functions with the tables embedded; +one function, built once, serves a second shape whose blended polars differ +(no rebuild); omitted tables give NaN, not a silently zero polar; and the +function still expands to SX. +""" + +import sys +from pathlib import Path + +import numpy as np +import pytest + +ca = pytest.importorskip("casadi") + +REPO = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(REPO / "tests" / "BodyAerodynamics")) + +from panel_kernel_cases import V3_FILE # noqa: E402 +from VSM.core.BodyAerodynamics import BodyAerodynamics # noqa: E402 +from VSM.core.Solver import Solver # noqa: E402 + +RTOL = 1e-12 + + +def _v3(distribution): + body = BodyAerodynamics.instantiate( + n_panels=20, file_path=V3_FILE, spanwise_panel_distribution=distribution + ) + body.va_initialize(10.0, 8.0, 3.0) + return body + + +def _solved(body, **solver_kwargs): + solver = Solver(gamma_loop_type="casadi_newton", allowed_error=1e-10, **solver_kwargs) + results = solver.solve(body) + return solver, np.asarray(results["gamma_distribution"], dtype=float) + + +def _newton_inputs(solver, gamma): + v_rel = solver.compute_relative_velocity(gamma) + n = solver.n_panels + laplacian = ( + solver._build_spanwise_laplacian() + if solver.is_with_artificial_viscosity + else np.zeros((n, n)) + ) + return { + "v_rel": v_rel, + "x_airf": solver.x_airf_array, + "y_airf": solver.y_airf_array, + "z_airf": solver.z_airf_array, + "chord": solver.chord_array, + "width": solver.width_array, + "l_gamma": laplacian @ gamma, + **solver.casadi_stall_angle_inputs(), + } + + +def _force_inputs(solver, gamma): + v_rel = solver.compute_relative_velocity(gamma) + return { + "v_rel": v_rel, + "v_qc": v_rel, + "x_airf": solver.x_airf_array, + "y_airf": solver.y_airf_array, + "z_airf": solver.z_airf_array, + "chord": solver.chord_array, + "width": solver.width_array, + "rho": solver.rho, + } + + +def _assert_outputs_equal(out_tables, out_const): + assert set(out_tables) == set(out_const) + for key, value in out_const.items(): + expected = np.array(value, dtype=float) + actual = np.array(out_tables[key], dtype=float) + scale = max(np.max(np.abs(expected)), 1e-300) + np.testing.assert_allclose(actual, expected, rtol=0, atol=RTOL * scale, err_msg=key) + + +@pytest.mark.parametrize("viscosity", [False, True]) +def test_table_inputs_reproduce_embedded_tables(viscosity): + """Residual function (all 13 outputs, incl. the corner-averaged ones and + the viscosity lift slope) and panel-force function, against the + embedded-table functions of the same shape.""" + solver, gamma = _solved(_v3("uniform"), is_with_artificial_viscosity=viscosity) + tables = solver.casadi_polar_table_inputs() + assert tables["cl_table"].shape == (solver.n_panels, solver.panels[0].panel_polar_data.shape[0]) + + args = _newton_inputs(solver, gamma) + if viscosity: # put a panel past stall so the gate and mu are exercised + args["stall_angles"] = args["stall_angles"] - 0.3 + _assert_outputs_equal( + solver._casadi_newton_function(table_inputs=True)(**args, **tables), + solver._casadi_newton_function()(**args), + ) + f_args = _force_inputs(solver, gamma) + _assert_outputs_equal( + solver.casadi_panel_force_function(table_inputs=True)(**f_args, **tables), + solver.casadi_panel_force_function()(**f_args), + ) + + +def test_one_function_serves_a_second_shape_without_rebuild(): + """Shapes A and B have the same panel count and alpha grid but differently + blended polars. The table-input function built for A, given B's tables and + geometry, equals a fresh embedded-table build for B, and no new + table-input function is built.""" + solver_a, gamma_a = _solved(_v3("uniform")) + fn = solver_a._casadi_newton_function(table_inputs=True) + force_fn = solver_a.casadi_panel_force_function(table_inputs=True) + cache_size = len(solver_a._casadi_table_input_cache) + + body_b = _v3("cosine") + tables_a = solver_a.casadi_polar_table_inputs() + solver_a.solve(body_b) # same solver object, now on shape B + gamma_b = solver_a.solve(body_b)["gamma_distribution"] + tables_b = solver_a.casadi_polar_table_inputs() + assert np.max(np.abs(tables_b["cl_table"] - tables_a["cl_table"])) > 1e-3 # the polars differ + + assert solver_a._casadi_newton_function(table_inputs=True) is fn + assert solver_a.casadi_panel_force_function(table_inputs=True) is force_fn + assert len(solver_a._casadi_table_input_cache) == cache_size + + fresh_b, _ = _solved(_v3("cosine")) + _assert_outputs_equal( + fn(**_newton_inputs(solver_a, gamma_b), **tables_b), + fresh_b._casadi_newton_function()(**_newton_inputs(solver_a, gamma_b)), + ) + _assert_outputs_equal( + force_fn(**_force_inputs(solver_a, gamma_b), **tables_b), + fresh_b.casadi_panel_force_function()(**_force_inputs(solver_a, gamma_b)), + ) + + +def test_omitted_tables_give_nan_not_zero(): + solver, gamma = _solved(_v3("uniform")) + out = solver._casadi_newton_function(table_inputs=True)(**_newton_inputs(solver, gamma)) + assert np.all(np.isnan(np.array(out["h"], dtype=float))) + out = solver.casadi_panel_force_function(table_inputs=True)(**_force_inputs(solver, gamma)) + assert np.all(np.isnan(np.array(out["force"], dtype=float))) + + +def test_table_input_functions_expand_to_sx(): + solver, gamma = _solved(_v3("uniform"), is_with_artificial_viscosity=True) + tables = solver.casadi_polar_table_inputs() + for fn, args in ( + (solver._casadi_newton_function(table_inputs=True), _newton_inputs(solver, gamma)), + (solver.casadi_panel_force_function(table_inputs=True), _force_inputs(solver, gamma)), + ): + _assert_outputs_equal(fn.expand()(**args, **tables), fn(**args, **tables)) + + +def test_newton_loop_is_unchanged_and_does_not_build_table_functions(): + solver, _ = _solved(_v3("uniform")) + assert solver._casadi_table_input_cache == {} + + +def test_unsupported_cases_raise_clearly(): + solver, _ = _solved(_v3("uniform"), polar_interpolation="bspline") + with pytest.raises(ValueError, match="polar_interpolation='linear'"): + solver._casadi_newton_function(table_inputs=True) + with pytest.raises(ValueError, match="polar_interpolation='linear'"): + solver.casadi_polar_table_inputs() diff --git a/tests/plotting/test_tube_geometry.py b/tests/plotting/test_tube_geometry.py new file mode 100644 index 0000000..a518998 --- /dev/null +++ b/tests/plotting/test_tube_geometry.py @@ -0,0 +1,261 @@ +"""Tests for the fancy Plotly plot: inflatable tubes and curved canopy. + +Unit tests cover the pure geometry helpers (ring math, contour handling, +interpolation). The integration tests exercise the full SurfplanAdapter +conversion and figure assembly against the bundled TUDELFT V3 Surfplan export; +they are skipped if SurfplanAdapter or the export data are unavailable. +""" + +from pathlib import Path + +import numpy as np +import pytest + +from VSM.plotly.canopy_geometry import ( + interpolate_contour_at, + read_upper_contour, + resample_contour, +) +from VSM.plotly.tube_geometry import circle_points + +REPO_ROOT = Path(__file__).resolve().parents[2] +SURFPLAN_DIR = REPO_ROOT / "data" / "TUDELFT_V3_KITE" / "Surfplan_export" +CP_DIR = REPO_ROOT / "data" / "TUDELFT_V3_KITE" / "cpx_distributions" +DRAWN_YAML = ( + REPO_ROOT + / "data" + / "TUDELFT_V3_KITE" + / "CAD_derived_geometry" + / "aero_geometry_CAD_breukels_regression.yaml" +) + + +def _has_surfplan(): + try: + import SurfplanAdapter # noqa: F401 + except ImportError: + return False + return SURFPLAN_DIR.is_dir() + + +requires_surfplan = pytest.mark.skipif( + not _has_surfplan(), reason="SurfplanAdapter or Surfplan export data unavailable" +) + + +# --------------------------------------------------------------------------- # +# Unit tests -- pure geometry helpers +# --------------------------------------------------------------------------- # + + +def test_circle_points_radius_and_orthogonality(): + center = np.array([1.0, 2.0, 3.0]) + axis = np.array([0.0, 1.0, 0.0]) + diameter = 0.4 + ring = circle_points(center, axis, diameter, n=32) + + # Every point lies at the radius from the centre. + radii = np.linalg.norm(ring - center, axis=1) + assert np.allclose(radii, 0.5 * diameter, atol=1e-9) + + # The ring lies in the plane normal to the axis (constant projection). + projections = (ring - center) @ axis + assert np.allclose(projections, 0.0, atol=1e-9) + + +def test_circle_points_axis_normalisation_invariant(): + center = np.zeros(3) + ring_unit = circle_points(center, np.array([0.0, 0.0, 1.0]), 1.0) + ring_scaled = circle_points(center, np.array([0.0, 0.0, 5.0]), 1.0) + assert np.allclose(ring_unit, ring_scaled, atol=1e-9) + + +def test_resample_contour_count_and_endpoints(): + contour = np.array([[0.0, 0.0], [0.3, 0.08], [0.7, 0.05], [1.0, 0.0]]) + resampled = resample_contour(contour, n_points=25) + assert resampled.shape == (25, 2) + assert np.allclose(resampled[0], contour[0], atol=1e-9) + assert np.allclose(resampled[-1], contour[-1], atol=1e-9) + + +def test_interpolate_contour_at_blends_and_clamps(): + c_lo = np.array([[0.0, 0.0], [1.0, 0.0]]) + c_hi = np.array([[0.0, 0.0], [1.0, 1.0]]) + table = [(0.0, c_lo), (1.0, c_hi)] + + # Midpoint is the average of the two contours. + mid = interpolate_contour_at(0.5, table) + assert np.allclose(mid, 0.5 * (c_lo + c_hi)) + + # Outside the range clamps to the end contours. + assert np.allclose(interpolate_contour_at(-1.0, table), c_lo) + assert np.allclose(interpolate_contour_at(2.0, table), c_hi) + + +def test_read_upper_contour_ordering(tmp_path): + # Minimal Selig-style .dat: TE -> upper -> LE -> lower -> TE. + dat = tmp_path / "prof.dat" + dat.write_text( + "test_profile\n" "1.0 0.0\n" "0.5 0.08\n" "0.0 0.0\n" "0.5 -0.04\n" "1.0 0.0\n" + ) + upper = read_upper_contour(dat) + # Ordered LE (x=0) to TE (x=1), all upper-surface points. + assert upper[0, 0] == pytest.approx(0.0) + assert upper[-1, 0] == pytest.approx(1.0) + assert np.all(np.diff(upper[:, 0]) > 0) + assert np.all(upper[:, 1] >= 0.0) + + +# --------------------------------------------------------------------------- # +# Integration tests -- full conversion + figure assembly +# --------------------------------------------------------------------------- # + + +@requires_surfplan +def test_ensure_surfplan_processed_and_cache(): + from VSM.plotly.surfplan_runner import ensure_surfplan_processed + + processed = ensure_surfplan_processed(SURFPLAN_DIR) + aero = processed / "aero_geometry.yaml" + struc = processed / "struc_geometry_all_in_surfplan.yaml" + assert aero.exists() and struc.exists() + + # Second call is a cache hit: the generated file is not rewritten. + first_mtime = aero.stat().st_mtime + ensure_surfplan_processed(SURFPLAN_DIR) + assert aero.stat().st_mtime == first_mtime + + +@requires_surfplan +def test_build_tube_and_canopy_shapes(): + from VSM.core.BodyAerodynamics import BodyAerodynamics + from VSM.plotly.surfplan_runner import ensure_surfplan_processed + from VSM.plotly.tube_geometry import build_tube_data, load_strut_span_fractions + from VSM.plotly.canopy_geometry import build_canopy_grid, load_contour_table + + n_panels = 36 + body = BodyAerodynamics.instantiate( + n_panels=n_panels, + file_path=DRAWN_YAML, + spanwise_panel_distribution="uniform", + ) + panels = body.panels + processed = ensure_surfplan_processed(SURFPLAN_DIR) + + tube = build_tube_data(panels, processed) + n_struts = len(load_strut_span_fractions(processed)) + # LE tube has one ring per panel edge, plus the wrap-around tip extensions. + assert len(tube["le"]) >= n_panels + 1 + assert len(tube["struts"]) == n_struts + + x, y, z = build_canopy_grid(panels, load_contour_table(processed)) + assert x.shape[0] == n_panels + 1 + assert x.shape == y.shape == z.shape + + # Tubes track the drawn wing span (within a tube radius at the tips). + wing_le = np.array([p.LE_point_1 for p in panels] + [p.LE_point_2 for p in panels]) + tube_pts = np.vstack(list(tube["le"])) + assert abs(tube_pts[:, 1].max() - wing_le[:, 1].max()) < 0.2 + + +@requires_surfplan +def test_interactive_plot_fancy_traces(): + import plotly.graph_objects as go + from VSM.core.BodyAerodynamics import BodyAerodynamics + from VSM.plot_geometry_plotly import interactive_plot + from VSM.plotly.tube_geometry import load_strut_span_fractions + from VSM.plotly.surfplan_runner import ensure_surfplan_processed + + body = BodyAerodynamics.instantiate( + n_panels=36, + file_path=DRAWN_YAML, + spanwise_panel_distribution="uniform", + ) + n_struts = len(load_strut_span_fractions(ensure_surfplan_processed(SURFPLAN_DIR))) + + # Fancy plot with canopy (panels default off): surfaces = canopy + LE + struts. + fig = interactive_plot(body, surfplan_dir=SURFPLAN_DIR, is_show=False) + surfaces = [t for t in fig.data if isinstance(t, go.Surface)] + flat_panels = [ + t for t in fig.data if isinstance(t, go.Mesh3d) and t.name == "Panel Surface" + ] + le_outline = [t for t in fig.data if getattr(t, "name", None) == "Leading Edge"] + assert len(surfaces) == 1 + 1 + n_struts + assert len(flat_panels) == 0 + assert len(le_outline) == 0 # panel outlines hidden by default in fancy mode + + # Tubes only with panels explicitly on: flat panels kept, no canopy surface. + fig2 = interactive_plot( + body, + surfplan_dir=SURFPLAN_DIR, + is_with_canopy=False, + is_with_panels=True, + is_show=False, + ) + surfaces2 = [t for t in fig2.data if isinstance(t, go.Surface)] + flat_panels2 = [ + t for t in fig2.data if isinstance(t, go.Mesh3d) and t.name == "Panel Surface" + ] + assert len(surfaces2) == 1 + n_struts + assert len(flat_panels2) == len(body.panels) + + # Plain plot is unchanged: panels shown, no surfaces at all. + fig3 = interactive_plot(body, is_show=False) + assert not [t for t in fig3.data if isinstance(t, go.Surface)] + assert [t for t in fig3.data if getattr(t, "name", None) == "Leading Edge"] + + +# --------------------------------------------------------------------------- # +# Cp distribution database +# --------------------------------------------------------------------------- # + + +@pytest.mark.skipif(not CP_DIR.is_dir(), reason="Cp distributions unavailable") +def test_load_upper_surface_cp_ordering(): + from VSM.plotly.cp_distributions import load_upper_surface_cp + + x, cp = load_upper_surface_cp(CP_DIR / "cp_AOA_8.dat") + assert np.all(np.diff(x) >= 0) # ascending x/c + assert x.min() >= 0.0 and x.max() <= 1.01 # normalised chord (small CFD overshoot) + assert cp.min() < 0 # suction side has negative Cp + + +@pytest.mark.skipif(not CP_DIR.is_dir(), reason="Cp distributions unavailable") +def test_build_cp_magnitude_fn_closest_aoa(): + from VSM.plotly.cp_distributions import build_cp_magnitude_fn + + # Exact and nearest matches over the available {2, 6, 8} degrees. + assert build_cp_magnitude_fn(CP_DIR, 8)[1] == 8 + assert build_cp_magnitude_fn(CP_DIR, 5)[1] == 6 + assert build_cp_magnitude_fn(CP_DIR, 3)[1] == 2 + assert build_cp_magnitude_fn(CP_DIR, 100)[1] == 8 + + magnitude, _ = build_cp_magnitude_fn(CP_DIR, 8) + values = magnitude(np.array([0.05, 0.5, 0.95])) + assert np.all(values >= 0) + # Loading is front-heavy: near-LE |Cp| exceeds near-TE |Cp|. + assert values[0] > values[-1] + + +@pytest.mark.skipif( + not (_has_surfplan() and CP_DIR.is_dir()), + reason="SurfplanAdapter or data unavailable", +) +def test_interactive_plot_cp_scaled_vectors_run(): + from VSM.core.BodyAerodynamics import BodyAerodynamics + from VSM.plot_geometry_plotly import interactive_plot + + body = BodyAerodynamics.instantiate( + n_panels=36, + file_path=DRAWN_YAML, + spanwise_panel_distribution="uniform", + ) + fig = interactive_plot( + body, + angle_of_attack=8, + surfplan_dir=SURFPLAN_DIR, + cp_distributions_dir=CP_DIR, + is_show=False, + ) + # The vector grid is still drawn (stems + arrowheads present). + assert [t for t in fig.data if getattr(t, "name", None) == "Aerodynamic Vectors"] diff --git a/tests/utils.py b/tests/utils.py index 92d3844..e44e0c2 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -508,10 +508,10 @@ def compute_new_for_alpha_range( # airfoil_input, # ) - wing_aero = BodyAerodynamics([wing]) + wing_aero = BodyAerodynamics([wing], leg_direction="rib") # thesis vortex system # initializing zero lists - wing_aero = BodyAerodynamics([wing]) + wing_aero = BodyAerodynamics([wing], leg_direction="rib") # thesis vortex system CL_LLT_new = np.zeros(len(aoas)) CD_LLT_new = np.zeros(len(aoas)) gamma_LLT_new = np.zeros((len(aoas), N - 1))