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
2 changes: 1 addition & 1 deletion Procfile
Original file line number Diff line number Diff line change
@@ -1 +1 @@
web: gunicorn src.api:app --log-file=-
web: gunicorn --chdir src python_vpp.api:app --log-file=-
41 changes: 39 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,39 @@

Please see the demo hosted at [https://yacht-vpp.streamlit.app/](https://yacht-vpp.streamlit.app/) and go for a test sail!

## Installation

Python-VPP is on PyPI and needs Python 3.11 or newer:

```bash
pip install python-vpp
```

Then build a yacht and run the VPP:

```python
import numpy as np
from python_vpp import VPP, Yacht, Keel, Rudder, Main, Jib, Kite

yacht = 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,
App=[Keel(Cu=1.00, Cl=0.78, Span=1.90), Rudder(Cu=0.48, Cl=0.22, Span=1.15)],
Sails=[
Main("MN1", P=16.60, E=5.60, Roach=0.1, BAD=1.0),
Jib("J1", I=16.20, J=5.10, LPG=5.40, HBI=1.8),
Kite("A2", area=150.0, vce=9.55),
],
)
vpp = VPP(Yacht=yacht)
vpp.set_analysis(tws_range=np.arange(4.0, 22.0, 2.0), twa_range=np.linspace(30.0, 180.0, 31))
vpp.run()
vpp.polar(n=3, save=True) # Polars.pdf
vpp.write_csv("polar.csv") # best boat speed, TWA rows x TWS columns
```

The righting arm curve is passed with `Yacht(..., GZ={"Heel": [...], "GZ": [...]})`. Without it, `righting_moment.json` is read from the working directory, falling back to the YD-41 curve shipped with the package. The ORC sail and resistance coefficients are part of the package.

## Using the code

The project is managed with [uv](https://docs.astral.sh/uv/). Install it once (see the [uv docs](https://docs.astral.sh/uv/getting-started/installation/) for other methods):
Expand All @@ -20,7 +53,7 @@ cd Python-VPP
uv sync
```

The main files that are used are `runVPP.py` and `righting_moment.json`. These have to be filled with the data of your boat. By default they are using the YD-41 (from Principle of Yacht Design).
From a clone of the repository, the main files that are used are `runVPP.py` and `righting_moment.json`. These have to be filled with the data of your boat. By default they are using the YD-41 (from Principle of Yacht Design). The code lives in `src/python_vpp/`, with the model coefficient data in `src/python_vpp/dat/`; `uv sync` installs it in editable mode.

The VPP is run from the root of the repository with

Expand All @@ -37,7 +70,7 @@ uv sync --extra demo --extra api
uv run streamlit run demos/Home.py
```

The run writes the results to `results.json` and saves the polars (`Polars.pdf`) and the sail chart (`SailChart.pdf`) as PDF, so the curves stay sharp when zoomed. For the YD-41 the polars look like this (`Figure.png` is a PNG copy of `Polars.pdf` for this page)
The run writes the results to `results.json`, the best boat speed to `polar.csv` (TWA rows, TWS columns, `;` separated, for routing software such as OpenCPN; `tws_range`/`twa_range` interpolate it onto another grid, e.g. the 5° / 2 kn steps OpenCPN expects) and saves the polars (`Polars.pdf`) and the sail chart (`SailChart.pdf`) as PDF, so the curves stay sharp when zoomed. For the YD-41 the polars look like this (`Figure.png` is a PNG copy of `Polars.pdf` for this page)

<p align="center">
<img src="Figure.png" alt="YD-41 VPP results" width="1024">
Expand Down Expand Up @@ -111,6 +144,10 @@ You can run tests with
uv run pytest -vv
```

### Release

Bump `version` in `pyproject.toml`, then create a GitHub release with a matching tag (e.g. `v1.0.0`). The `python-publish.yml` workflow builds the package with `uv build` and uploads it to PyPI through trusted publishing. Check the build locally first with `uv build`.

You can run a benchmark against the YD-41 results from WinVPP by running the `benchmark.py` script (`-g` saves the polars as one `Figure_<label>_<TWS>.pdf` per wind speed plus a `Figure_<label>_stats.pdf` error summary, `-s` saves the results).

```bash
Expand Down
8 changes: 3 additions & 5 deletions benchmark/benchmark.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,9 @@
import matplotlib.pyplot as plt
from datetime import datetime

sys.path.append(os.path.realpath("."))

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

tws = np.array([3, 4, 5, 6, 7, 8, 10]) # TWS in m/s
data_file = "dat/YD-41/YD41.dat"
Expand Down
7 changes: 4 additions & 3 deletions demos/pages/1_VPP_⛵.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,10 @@
import streamlit as st
from utils import footer, header

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
# the demo may run without the package installed (e.g. Streamlit Cloud)
sys.path.append(os.path.realpath("src"))
from python_vpp.api import app
from python_vpp.UtilsMod import KNOTS_TO_MPS, _get_cross, _get_vmg, _polar, cols, stl

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

Expand Down
15 changes: 13 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[project]
name = "python-vpp"
version = "1.0.0"
description = "OOP Velocity Prediction Program"
description = "3-DOF sailing yacht Velocity Prediction Program based on the ORC models"
readme = "README.md"
license = "MIT"
requires-python = ">=3.11"
Expand All @@ -10,8 +10,11 @@ authors = [
{ name = "Otto Villani" },
{ name = "Thomas Dickson" },
]
keywords = ["VPP", "velocity prediction", "sailing", "yacht", "ORC", "naval architecture"]
classifiers = [
"Programming Language :: Python :: 3",
"Intended Audience :: Science/Research",
"Topic :: Scientific/Engineering",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
]
Expand All @@ -21,6 +24,11 @@ dependencies = [
"scipy>=1.12",
]

[project.urls]
Homepage = "https://github.com/marinlauber/Python-VPP"
Documentation = "https://marinlauber.github.io/Python-VPP/"
Demo = "https://yacht-vpp.streamlit.app/"

[project.optional-dependencies]
api = ["flask", "gunicorn"]
demo = ["streamlit>=1.37"]
Expand All @@ -35,7 +43,10 @@ requires = ["hatchling"]
build-backend = "hatchling.build"

[tool.hatch.build.targets.wheel]
packages = ["src"]
packages = ["src/python_vpp"]

[tool.hatch.build.targets.sdist]
include = ["src/python_vpp", "tests", "README.md", "LICENSE"]

[tool.pytest.ini_options]
testpaths = ["tests"]
Expand Down
6 changes: 3 additions & 3 deletions runVPP.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@

logging.basicConfig(level=logging.INFO)

from src.SailMod import Jib, Kite, Main
from src.VPPMod import VPP
from src.YachtMod import Keel, Rudder, Yacht
from python_vpp.SailMod import Jib, Kite, Main
from python_vpp.VPPMod import VPP
from python_vpp.YachtMod import Keel, Rudder, Yacht

YD41 = Yacht(
Name="YD41",
Expand Down
4 changes: 2 additions & 2 deletions scripts/Resistance.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
__email__ = "M.Lauber@soton.ac.uk"

import numpy as np
from src.HydroMod import HydroMod
from src.YachtMod import Yacht, Keel, Rudder
from python_vpp.HydroMod import HydroMod
from python_vpp.YachtMod import Yacht, Keel, Rudder

if __name__ == "__main__":

Expand Down
2 changes: 1 addition & 1 deletion sphinx/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
import os
import sys

sys.path.insert(0, os.path.abspath(".."))
sys.path.insert(0, os.path.abspath("../src"))


# -- Project information -----------------------------------------------------
Expand Down
2 changes: 1 addition & 1 deletion sphinx/index.rst
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
Welcome to Python-VPP's documentation!
======================================

.. automodule:: src.VPPMod
.. automodule:: python_vpp.VPPMod
:members:

.. toctree::
Expand Down
1 change: 0 additions & 1 deletion src/__init__.py

This file was deleted.

2 changes: 1 addition & 1 deletion src/AeroMod.py → src/python_vpp/AeroMod.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import matplotlib.pyplot as plt
import numpy as np

from src.UtilsMod import build_interp_func
from .UtilsMod import build_interp_func


Z_REF = 10.0 # m, height the true wind speed is given at (ORC 7.1)
Expand Down
4 changes: 3 additions & 1 deletion src/HydroMod.py → src/python_vpp/HydroMod.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
import numpy as np
from scipy.interpolate import RegularGridInterpolator

from .UtilsMod import DATA_DIR


class HydroMod(object):
def __init__(self, Yacht, rho=1025.0, mu=0.00119, g=9.81):
Expand Down Expand Up @@ -61,7 +63,7 @@ def __load_data(self):
"""
Loads ORC resistance surfaces from file and build interpolation function
"""
surf = np.genfromtxt("dat/ORCi_Drag_Surfaces.csv", delimiter=",", skip_header=1)
surf = np.genfromtxt(DATA_DIR / "ORCi_Drag_Surfaces.csv", delimiter=",", skip_header=1)
surf = surf.reshape((24, 43, 42))
# x is Fn := [0.,0.7], y is btr := [2.5,9], z is lvr := [3,9]
# we add zero Fn resistance (0.)
Expand Down
11 changes: 6 additions & 5 deletions src/SailMod.py → src/python_vpp/SailMod.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
import numpy as np
from scipy import interpolate

from .UtilsMod import DATA_DIR


SAIL_TYPES = {
"main": "main",
Expand Down Expand Up @@ -68,12 +70,11 @@ def _build_interp_func(self, fname, data_source="orc"):
"""Build cubic B-spline interpolation functions for CL and CD.

Falls back to linear (k=1) if fewer than 4 data points are available.
Tries ``dat/{data_source}/{fname}.dat`` first, then ``dat/{fname}.dat``.
Tries ``DATA_DIR/{data_source}/{fname}.dat`` first, then ``DATA_DIR/{fname}.dat``.
"""
import os
path = os.path.join("dat", data_source, fname + ".dat")
if not os.path.exists(path):
path = os.path.join("dat", fname + ".dat")
path = DATA_DIR / data_source / (fname + ".dat")
if not path.exists():
path = DATA_DIR / (fname + ".dat")
a = np.genfromtxt(path, delimiter=",", skip_header=1)
self.kp = a[0, 0]
# Filter NaN values (trailing commas in CSV create NaNs)
Expand Down
5 changes: 4 additions & 1 deletion src/UtilsMod.py → src/python_vpp/UtilsMod.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
# -*- coding: utf-8 -*-

import json
from pathlib import Path

import matplotlib.pyplot as plt
import numpy as np
Expand All @@ -10,6 +11,8 @@
from scipy.interpolate import RegularGridInterpolator

KNOTS_TO_MPS = 0.5144
# model coefficient data shipped with the package
DATA_DIR = Path(__file__).parent / "dat"
stl = [
(0, ()),
(0, (1.1, 1.1)),
Expand Down Expand Up @@ -90,7 +93,7 @@ def build_interp_func(fname, i=1, kind="linear"):
"""
build interpolation function and returns it in a list
"""
a = np.genfromtxt("dat/" + fname + ".dat", delimiter=",", skip_header=1)
a = np.genfromtxt(DATA_DIR / (fname + ".dat"), delimiter=",", skip_header=1)
# Filter out NaN values for make_interp_spline compatibility
mask = ~(np.isnan(a[0, :]) | np.isnan(a[i, :]))
x = a[0, mask]
Expand Down
8 changes: 4 additions & 4 deletions src/VPPMod.py → src/python_vpp/VPPMod.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@
import numpy as np
from scipy.optimize import minimize

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

logger = logging.getLogger(__name__)

Expand Down
14 changes: 10 additions & 4 deletions src/YachtMod.py → src/python_vpp/YachtMod.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import os

import numpy as np
from scipy import interpolate

from src.UtilsMod import build_interp_func, json_read, json_write
from .UtilsMod import DATA_DIR, build_interp_func, json_read, json_write


def _zero_cr(fn):
Expand Down Expand Up @@ -248,8 +250,9 @@ def __init__(self, Name, Lwl, Vol, Bwl, Tc, WSA, Tmax,
Sail inventory. Default is [].
GZ : dict, optional
Righting arm curve as ``{"Heel": [...], "GZ": [...]}``.
Heel in degrees, GZ in metres. If *None*, loads from
``righting_moment.json`` (backward compatible).
Heel in degrees, GZ in metres. If *None*, loads
``righting_moment.json`` from the working directory, or the
packaged YD-41 curve if there is none.
crew_weight : float, optional
Total crew weight (kg). If *None*, uses empirical formula
``25.8 * Lwl ** 1.4262``.
Expand Down Expand Up @@ -316,7 +319,10 @@ def _build_rm_interp(self):
if self._gz_data is not None:
a = self._gz_data
else:
a = json_read('righting_moment')
fname = 'righting_moment'
if not os.path.exists(fname + '.json'):
fname = str(DATA_DIR / fname)
a = json_read(fname)
return interpolate.interp1d(np.array(a["Heel"]), np.array(a["GZ"]),
kind="linear", fill_value="extrapolate")

Expand Down
14 changes: 14 additions & 0 deletions src/python_vpp/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
"""Python-VPP: a 3-DOF velocity prediction program based on the ORC models."""

from importlib.metadata import PackageNotFoundError, version

from .SailMod import Jib, Kite, Main
from .VPPMod import VPP
from .YachtMod import Bulb, Keel, Rudder, ShortKeel, Yacht

try:
__version__ = version("python-vpp")
except PackageNotFoundError: # running from a source checkout
__version__ = "unknown"

__all__ = ["VPP", "Yacht", "Keel", "ShortKeel", "Rudder", "Bulb", "Main", "Jib", "Kite"]
9 changes: 3 additions & 6 deletions src/api.py → src/python_vpp/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,14 @@

"""
import logging
import os
import sys
from typing import Any, Dict

import numpy as np
from flask import Flask, jsonify, request

sys.path.append(os.path.realpath("."))
from src.SailMod import Jib, Kite, Main
from src.VPPMod import VPP
from src.YachtMod import Keel, Rudder, ShortKeel, Yacht
from .SailMod import Jib, Kite, Main
from .VPPMod import VPP
from .YachtMod import Keel, Rudder, ShortKeel, Yacht

app = Flask(__name__)

Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
4 changes: 4 additions & 0 deletions src/python_vpp/dat/righting_moment.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"Heel": [0.00,10.0,20.0,30.0,40.0],
"GZ": [0.00,0.400,0.730,0.960,1.100]
}
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
Loading
Loading