Skip to content
Merged
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,6 @@ sphinx/_build/
# Project
ORC_VPP_2019.pdf
dev.py

# exported polars (root only, dat/ holds tracked csv data)
/*.csv
4 changes: 4 additions & 0 deletions runVPP.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,5 +41,9 @@

vpp.run()
vpp.write("results")
# polar csv for routing software, e.g. OpenCPN (5 deg TWA, 2 kn TWS steps)
vpp.write_csv(
"polar.csv", tws_range=np.arange(4.0, 22.0, 2.0), twa_range=np.arange(30.0, 181.0, 5.0)
)
vpp.polar(3, True)
vpp.SailChart(True)
47 changes: 47 additions & 0 deletions src/UtilsMod.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,53 @@ def json_write(data, fname):
json.dump(data, json_file, ensure_ascii=False, indent=2, sort_keys=False)


def csv_write(VPP, tws_range=None, twa_range=None, fname="polar.csv"):
"""
Export the best boat speed (knots) to a polar csv file (TWA rows, TWS
columns, ";" separated), the format read by routing software such as OpenCPN.

Parameters
----------
VPP
A VPP (after run) or VPPResults object.
tws_range
Optional TWS (knots) to write. Default: the TWS of the VPP analysis.
twa_range
Optional TWA (degrees) to write. Default: the TWA of the VPP analysis.
fname
Name of the csv file, default "polar.csv".

If tws_range or twa_range is given, the VPP results are linearly
interpolated onto it; points outside the analysed range, or where no sail
set can be carried, are written as 0.
"""
if not hasattr(VPP, "store"):
raise ValueError("VPP object must have a 'store' attribute to export to csv.")
import csv

# best boat speed over the sail sets, the store already holds knots
vpp_tws = np.asarray(VPP.tws_range) / KNOTS_TO_MPS
vpp_twa = np.asarray(VPP.twa_range)
speed = np.max(np.asarray(VPP.store)[:, :, :, 0], axis=2)
tws = vpp_tws if tws_range is None else np.asarray(tws_range, dtype=float)
twa = vpp_twa if twa_range is None else np.asarray(twa_range, dtype=float)
if tws_range is not None or twa_range is not None:
func = RegularGridInterpolator(
(vpp_tws, vpp_twa), speed, bounds_error=False, fill_value=0.0
)
grid = np.stack(np.meshgrid(tws, twa, indexing="ij"), axis=-1)
speed = func(grid)

with open(fname, mode="w", newline="") as file:
writer = csv.writer(file, delimiter=";")
# header with TWS in knots
writer.writerow(["TWA\\TWS"] + [f"{round(ws, 2):g}" for ws in tws])
for j, angle in enumerate(twa):
writer.writerow(
[f"{round(angle, 2):g}"] + [f"{speed[i, j]:.2f}" for i in range(len(tws))]
)


def build_interp_func(fname, i=1, kind="linear"):
"""
build interpolation function and returns it in a list
Expand Down
6 changes: 5 additions & 1 deletion src/VPPMod.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

from src.AeroMod import AeroMod
from src.HydroMod import HydroMod
from src.UtilsMod import KNOTS_TO_MPS, json_write, polar_plot, sail_chart
from src.UtilsMod import KNOTS_TO_MPS, csv_write, json_write, polar_plot, sail_chart
from src.YachtMod import Yacht as YachtClass

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -281,6 +281,10 @@ def results(self):
def write(self, fname):
json_write(self.results(), fname)

def write_csv(self, fname="polar.csv", tws_range=None, twa_range=None):
"""Write the best boat speed to a polar csv file, see UtilsMod.csv_write."""
csv_write(self, tws_range=tws_range, twa_range=twa_range, fname=fname)

def polar(self, n=1, save=False, fname="Polars.pdf"):
polar_plot([self], n, save, fname)

Expand Down
53 changes: 52 additions & 1 deletion tests/test_utils.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
from types import SimpleNamespace

import numpy as np

from src.SailMod import Jib, Kite, Main
from src.UtilsMod import _get_cross
from src.UtilsMod import KNOTS_TO_MPS, _get_cross, csv_write
from src.VPPMod import VPP
from src.YachtMod import Keel, Rudder, Yacht


Expand Down Expand Up @@ -53,3 +56,51 @@ def test_get_cross_pad_is_symmetric():

def test_get_cross_no_pad():
assert tuple(_get_cross(_cross_store(), 1, pad=0)) == (4, 8)


def _fake_vpp():
"""2 TWS x 3 TWA x 2 sails; the store holds boat speed in knots, TWS in m/s."""
store = np.zeros((2, 3, 2, 5))
store[:, :, 0, 0] = [[5.0, 6.0, 0.0], [7.0, 8.0, 0.0]] # jib, not set downwind
store[:, :, 1, 0] = [[0.0, 5.5, 6.5], [0.0, 8.5, 9.5]] # kite
return SimpleNamespace(
tws_range=np.array([6.0, 10.0]) * KNOTS_TO_MPS,
twa_range=np.array([40.0, 90.0, 150.0]),
store=store,
)


def _read_csv(path):
rows = [line.split(";") for line in path.read_text().splitlines()]
return rows[0], {float(r[0]): [float(v) for v in r[1:]] for r in rows[1:]}


def test_csv_write_default_grid(tmp_path):
fname = tmp_path / "polar.csv"
csv_write(_fake_vpp(), fname=fname)
header, rows = _read_csv(fname)
assert header == ["TWA\\TWS", "6", "10"]
# best sail at each point, in knots (no second unit conversion)
assert rows == {40.0: [5.0, 7.0], 90.0: [6.0, 8.5], 150.0: [6.5, 9.5]}


def test_csv_write_interpolates_custom_grid(tmp_path):
fname = tmp_path / "polar.csv"
csv_write(_fake_vpp(), tws_range=[6.0, 8.0, 12.0], twa_range=[40.0, 65.0], fname=fname)
header, rows = _read_csv(fname)
assert header == ["TWA\\TWS", "6", "8", "12"]
assert rows[40.0] == [5.0, 6.0, 0.0] # 12 kn is outside the analysis: 0
assert np.allclose(rows[65.0], [5.5, 6.625, 0.0], atol=0.006) # halfway 40-90 deg


def test_vpp_write_csv(tmp_path):
vpp = VPP(Yacht=return_YD41_particulars())
vpp.set_analysis(tws_range=np.array([8.0]), twa_range=np.array([45.0, 90.0]))
vpp.run()
fname = tmp_path / "polar.csv"
vpp.write_csv(fname)
header, rows = _read_csv(fname)
assert header == ["TWA\\TWS", "8"]
best = vpp.store[0, :, :, 0].max(axis=1)
assert np.allclose([rows[45.0][0], rows[90.0][0]], best, atol=0.006)
assert all(v > 0 for r in rows.values() for v in r)