Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
262 changes: 175 additions & 87 deletions demos/pages/1_VPP_⛵.py
Original file line number Diff line number Diff line change
@@ -1,59 +1,43 @@
import json
import logging
import os
import sys
from typing import Any, Dict, List

import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import streamlit as st
from utils import footer, header
from presets import PRESETS
from utils import (
FIELD_HELP,
KITE_SAIL_TYPES,
JIB_SAIL_TYPES,
MAIN_SAIL_TYPES,
field_label,
footer,
header,
render_data_source,
render_environment_inputs,
render_keel_inputs,
render_roughness_input,
render_sail_type,
render_solver_method,
run_vpp_direct,
validate_ranges,
)

sys.path.append(os.path.realpath("."))
from src.api import app
from src.UtilsMod import KNOTS_TO_MPS, _get_cross, _get_vmg, _polar, cols, stl
from src.UtilsMod import KNOTS_TO_MPS, _get_cross, _get_vmg, _polar, cols, lab, stl

st.set_page_config(page_title="VPP", page_icon="⛵")


def process_yacht_specifications(
tws_range: List[int],
twa_range: List[int],
yacht: Dict,
keel: Dict,
rudder: Dict,
main: Dict,
jib: Dict,
kite: Dict,
):
data = {
"name": yacht["Name"],
"yacht": yacht,
"keel": keel,
"rudder": rudder,
"main": main,
"jib": jib,
"kite": kite,
"tws_range": tws_range,
"twa_range": twa_range,
}

logging.info("Starting VPP simulation")
json_string = json.dumps(data)
headers = {"content-type": "application/json", "Accept-Charset": "UTF-8"}
client = app.test_client()
response = client.post("/api/vpp/", data=json_string, headers=headers)

logging.info("VPP simulation completed")
return response


def plot_single_polar(response: Dict[str, Any]) -> plt.Figure:
name = response.json["name"]
sails = response.json["sails"]
twa_range = np.array(response.json["twa"])
tws_range = np.array(response.json["tws"])
results = np.array(response.json["results"])
def plot_single_polar(data: Dict[str, Any]) -> plt.Figure:
name = data["name"]
sails = data["sails"]
twa_range = np.array(data["twa"])
tws_range = np.array(data["tws"])
results = np.array(data["results"])

n = 1

Expand All @@ -66,7 +50,7 @@ def plot_single_polar(response: Dict[str, Any]) -> plt.Figure:
for j in range(n):
lab = "_nolegend_"
if k == 0:
lab = name + " " + f"{tws_range[i]/KNOTS_TO_MPS:.1f}"
lab = f"{tws_range[i]/KNOTS_TO_MPS:.1f}"

ax[j].plot(
twa_range[idx[0] : idx[1]] / 180 * np.pi,
Expand Down Expand Up @@ -94,80 +78,184 @@ def plot_single_polar(response: Dict[str, Any]) -> plt.Figure:
return fig


yacht = {
"Name": "YD41",
"Lwl": 11.90,
"Vol": 6.05,
"Bwl": 3.18,
"Tc": 0.4,
"WSA": 28.20,
"Tmax": 2.30,
"Amax": 1.051,
"Mass": 6500,
"Ff": 1.5,
"Fa": 1.5,
"Boa": 4.2,
"Loa": 12.5,
}

keel = {"Cu": 1.00, "Cl": 0.78, "Span": 1.90}
rudder = {"Cu": 0.48, "Cl": 0.22, "Span": 1.15}
main = {"Name": "MN1", "P": 16.60, "E": 5.60, "Roach": 0.1, "BAD": 1.0}
jib = {"Name": "J1", "I": 16.20, "J": 5.10, "LPG": 5.40, "HBI": 1.8}
kite = {"Name": "A2", "area": 150.0, "vce": 9.55}
def plot_depowering_polar(data: Dict[str, Any]) -> plt.Figure:
"""Plot flat and red depowering values on polar axes."""
name = data["name"]
sails = data["sails"]
twa_range = np.array(data["twa"])
tws_range = np.array(data["tws"])
results = np.array(data["results"])

fig, axes = plt.subplots(1, 2, subplot_kw=dict(polar=True), figsize=(12, 6))
for ax_i, (idx, title) in enumerate([(3, "Flat"), (4, "RED")]):
ax = axes[ax_i]
ax.set_xticks(np.linspace(0, np.pi, 5))
ax.set_theta_direction(-1)
ax.set_theta_offset(np.pi / 2.0)
ax.set_thetamin(0)
ax.set_thetamax(180)
ax.set_rmin(0.0)
ax.set_xlabel(r"TWA ($^\circ$)")
ax.set_ylabel(title, labelpad=-40)

for i in range(len(tws_range)):
for k in range(len(sails)):
cross = _get_cross(results[i, :, :, :], k)
label = "_nolegend_"
if k == 0:
label = f"{tws_range[i]/KNOTS_TO_MPS:.1f}"
ax.plot(
twa_range[cross[0] : cross[1]] / 180 * np.pi,
results[i, cross[0] : cross[1], k, idx],
color=cols[k % 7],
lw=np.where(i < 7, 1.5, 2.5),
linestyle=stl[i % 7],
label=label,
)
if ax_i == 0:
ax.legend(title=r"TWS (kts)", loc=1, bbox_to_anchor=(1.05, 1.05))
plt.tight_layout()
return fig


def build_depowering_table(data: Dict[str, Any]) -> pd.DataFrame:
"""Build a table of depowering values for the best sail at each TWS/TWA."""
sails = data["sails"]
twa_range = np.array(data["twa"])
tws_range = np.array(data["tws"])
results = np.array(data["results"])

rows = []
for i, tws in enumerate(tws_range):
tws_kts = tws / KNOTS_TO_MPS
for j, twa in enumerate(twa_range):
best_sail = int(np.argmax(results[i, j, :, 0]))
vb = results[i, j, best_sail, 0]
flat = results[i, j, best_sail, 3]
red = results[i, j, best_sail, 4]
if flat < 1.0 or red < 2.0:
rows.append(
{
"TWS (kts)": f"{tws_kts:.0f}",
"TWA (°)": f"{twa:.0f}",
"Sail": sails[best_sail],
"Vb (kts)": f"{vb:.2f}",
"Flat": f"{flat:.2f}",
"RED": f"{red:.2f}",
}
)
if not rows:
return pd.DataFrame({"Info": ["No depowering applied at these wind speeds"]})
return pd.DataFrame(rows)


header()

st.markdown(
"""
# Yacht VPP

This is a 3 D.O.F. VPP for a mono hull displacement sailing yacht.

The default parameters are pre-set particulars for the YD-41 yacht.
This is a 3 D.O.F. VPP for a mono hull displacement sailing yacht.
The performance model is based on the
[ORC VPP documentation](https://www.orc.org/rules/ORC%20VPP%20Documentation%202024.pdf).

"""
)

with st.popover("ℹ️ What is a VPP?"):
st.markdown(
"A Velocity Prediction Program computes the "
"equilibrium speed of a sailing yacht at each combination of true "
"wind speed (TWS) and true wind angle (TWA). It balances "
"aerodynamic driving force against hydrodynamic resistance, side "
"force against keel lift, and heeling moment against righting moment."
)

preset_name = st.selectbox("Yacht preset", list(PRESETS.keys()), index=1)
preset = PRESETS[preset_name]
yacht = dict(preset["yacht"])
keel = dict(preset["keel"])
rudder = dict(preset["rudder"])
main = dict(preset["main"])
jib = dict(preset["jib"])
kite = dict(preset["kite"])

st.subheader("Yacht particulars")
for key, value in yacht.items():
yacht[key] = st.text_input(f"{key}:", value)
yacht[key] = st.text_input(field_label(key), value, help=FIELD_HELP.get(key, ""))
roughness = render_roughness_input(key_prefix="vpp")

st.subheader("Keel")
for key, value in keel.items():
keel[key] = st.text_input(f"{key}:", value)
keel = render_keel_inputs(keel, key_prefix="vpp")

st.subheader("Rudder")
for key, value in rudder.items():
rudder[key] = st.text_input(f"{key}:", value)
rudder[key] = st.text_input(field_label(key), value, help=FIELD_HELP.get(key, ""))

st.subheader("Main Sail")
main_sail_type = render_sail_type("Main sail", MAIN_SAIL_TYPES, key_prefix="vpp_main")
for key, value in main.items():
main[key] = st.text_input(f"{key}:", value)
main[key] = st.text_input(field_label(key), value, help=FIELD_HELP.get(key, ""))

st.subheader("Jib")
jib_sail_type = render_sail_type("Jib", JIB_SAIL_TYPES, key_prefix="vpp_jib")
for key, value in jib.items():
jib[key] = st.text_input(f"{key}:", value)
jib[key] = st.text_input(field_label(key), value, help=FIELD_HELP.get(key, ""))

st.subheader("Kite (Spinnaker)")
kite_sail_type = render_sail_type("Kite", KITE_SAIL_TYPES, key_prefix="vpp_kite")
for key, value in kite.items():
kite[key] = st.text_input(f"{key}:", value)
kite[key] = st.text_input(field_label(key), value, help=FIELD_HELP.get(key, ""))

st.subheader("Environment")
twa_slider = st.slider(
"True wind angle (TWA) range", 35.0, 175.0, (35.0, 175.0), step=2.0
)
twa_range = np.arange(twa_slider[0], twa_slider[1], 2.0).tolist()
tws_range, twa_range, env_params = render_environment_inputs(key_prefix="vpp")

tws_slider = st.slider("True wind speed (TWS) range", 2.0, 25.0, (8.0, 12.0), step=2.0)
tws_range = np.arange(tws_slider[0], tws_slider[1], 2.0).tolist()
st.subheader("Solver Settings")
solver_method = render_solver_method(key_prefix="vpp")
data_source = render_data_source(key_prefix="vpp")

if st.button("Process Specifications"):
with st.spinner("Running optimisation, this can take a minute or two."):
response = process_yacht_specifications(
tws_range, twa_range, yacht, keel, rudder, main, jib, kite
)
fig = plot_single_polar(response)
st.pyplot(fig)
if validate_ranges(tws_range, twa_range):
config = {"yacht": yacht, "keel": keel, "rudder": rudder, "main": main, "jib": jib, "kite": kite}
sail_types = {"main": main_sail_type, "jib": jib_sail_type, "kite": kite_sail_type}
env_params["roughness"] = roughness
with st.status("Running VPP optimisation...", expanded=True) as status:
def _on_tws(i, tws_kts, n_tws):
st.write(f"TWS {tws_kts:.0f} kts complete ({i + 1}/{n_tws})")

result, error = run_vpp_direct(
config, tws_range, twa_range, method=solver_method,
data_source=data_source, sail_types=sail_types,
env_params=env_params, progress_callback=_on_tws,
)
status.update(label="Optimisation complete!", state="complete", expanded=False)
if error:
st.error(f"Simulation failed: {error}")
logging.error("VPP failed: %s", error)
else:
with st.popover("ℹ️ What is a polar plot?"):
st.markdown(
"The polar plot shows boat speed (radial "
"axis) vs true wind angle. Each curve is a different wind "
"speed. Dots mark the best VMG (velocity made good) angles "
"upwind and downwind."
)
fig = plot_single_polar(result)
st.pyplot(fig)

st.subheader("Depowering (Flat & RED)")
with st.popover("ℹ️ What is depowering?"):
st.markdown(
"*Flat* controls how much the sails are "
"flattened (1.0 = full power, 0.62 = maximum depower). "
"*RED* is the reef/reduction factor (2.0 = full sail, "
"lower = reefed). The VPP depowers automatically when "
"heel exceeds the limit."
)
dep_fig = plot_depowering_polar(result)
st.pyplot(dep_fig)

with st.expander("Depowering data table"):
df = build_depowering_table(result)
st.dataframe(df, use_container_width=True)

footer()
Loading