Skip to content
This repository was archived by the owner on Mar 23, 2024. It is now read-only.
6 changes: 6 additions & 0 deletions controller/common/constants.py
Original file line number Diff line number Diff line change
@@ -1 +1,7 @@
"""Constants used across the controller package."""

# meters, trim tab chord width is not included
CHORD_WIDTH_MAIN_SAIL = 0.23

# {m^2 / s at 10degC} and air density at 1.225 {kg / m^3}
KINEMATIC_VISCOSITY = 0.000014207
76 changes: 76 additions & 0 deletions controller/wingsail/controllers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import math

from controller.common.lut import LUT


class WingsailController:
"""
The controller class for computing trim tab angles for controlling the mainsail.

Args:
- chord_width_main_sail (float): The chord width of the main sail.
- kinematic_viscosity (float): The kinematic viscosity of the fluid.
- lut (LUT): A lookup table containing Reynolds numbers and corresponding desired angles of
attack.
"""

def __init__(self, chord_width_main_sail: float, kinematic_viscosity: float, lut: LUT):
self.chord_width_main_sail = chord_width_main_sail
self.kinematic_viscosity = kinematic_viscosity
self.lut: LUT = lut

def _compute_reynolds_number(self, apparent_wind_speed: float) -> float:
"""
Computes the Reynolds number for the main sail.

Args:
- apparent_wind_speed (float): The apparent wind speed in meters per second.

Returns:
- reynolds_number (float): The computed Reynolds number for the main sail.
"""
reynolds_number: float = (
apparent_wind_speed * self.chord_width_main_sail
) / self.kinematic_viscosity
return reynolds_number

def _compute_trim_tab_angle(
self, reynolds_number: float, apparent_wind_direction: float
) -> float:
"""
Computes the trim tab angle based on Reynolds number and apparent wind direction.

Args:
- reynolds_number (float): The Reynolds number.
- apparent_wind_direction (float): The absolute bearing (true heading) of the wind.
Degrees, 0° means the apparent wind is blowing from the bow to the stern of the boat,
increase CW
Range: -180 < direction <= 180 for symmetry

Returns:
- trim_tab_angle (float): The computed trim tab angle based on the provided
Reynolds number and apparent wind direction.
"""
desired_alpha: float = self.lut(reynolds_number) # Using __call__ method
return math.copysign(desired_alpha, apparent_wind_direction)

def get_trim_tab_angle(
self, apparent_wind_speed: float, apparent_wind_direction: float
) -> float:
Comment on lines +57 to +59
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I recommend overloading this function where you accept the apparent wind vector, calculate the wind speed and direction, and then call this function to get the trim tab angle.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's hold off doing this until we figured out the angle convention.

"""
Computes and returns the final trim tab angle.

Range: -180 < direction <= 180 for symmetry

Args:
- apparent_wind_speed (float): The apparent wind speed in meters per second.
- apparent_wind_direction (float): The apparent wind direction in degrees.

Returns:
- trim_tab_angle (float): The computed trim tab angle.
"""
reynolds_number: float = self._compute_reynolds_number(apparent_wind_speed)
trim_tab_angle: float = self._compute_trim_tab_angle(
reynolds_number, apparent_wind_direction
)
return trim_tab_angle
107 changes: 107 additions & 0 deletions tests/unit/wingsail/test_controllers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import numpy as np
import pytest

from controller.common.constants import CHORD_WIDTH_MAIN_SAIL, KINEMATIC_VISCOSITY
from controller.common.lut import LUT
from controller.wingsail.controllers import WingsailController

# Define test data
test_lut_data = np.array(
[[50000, 5.75], [100000, 6.75], [200000, 7], [500000, 9.25], [1000000, 10]]
)
test_lut = LUT(test_lut_data)
test_chord_width = CHORD_WIDTH_MAIN_SAIL
test_kinematic_viscosity = KINEMATIC_VISCOSITY


class TestWingsailController:
"""
Tests the functionality of the WingsailController class.
"""

@pytest.fixture
def wingsail_controller(self):
"""
Fixture to create an instance of WingsailController for testing.
"""
return WingsailController(test_chord_width, test_kinematic_viscosity, test_lut)

@pytest.mark.parametrize(
"apparent_wind_speed, expected_reynolds_number",
[
(0, 0),
(3, 3 * CHORD_WIDTH_MAIN_SAIL / KINEMATIC_VISCOSITY),
(5, 5 * CHORD_WIDTH_MAIN_SAIL / KINEMATIC_VISCOSITY),
(10, 10 * CHORD_WIDTH_MAIN_SAIL / KINEMATIC_VISCOSITY),
(20, 20 * CHORD_WIDTH_MAIN_SAIL / KINEMATIC_VISCOSITY),
],
)
def test_compute_reynolds_number(
self, wingsail_controller, apparent_wind_speed, expected_reynolds_number
):
"""
Tests the computation of Reynolds number.

Args:
wingsail_controller: Instance of WingsailController.
"""
computed_reynolds_number = wingsail_controller._compute_reynolds_number(
apparent_wind_speed
)
assert np.isclose(computed_reynolds_number, expected_reynolds_number)

@pytest.mark.parametrize(
"reynolds_number, apparent_wind_direction, expected_trim_tab_angle",
[
(1250, 45.0, 5.75),
(15388, -90.0, -5.75),
(210945, 170.0, 7.0820875),
(824000, -120.0, -9.736),
(2000000, 0, 10.0),
],
)
def test_compute_trim_tab_angle(
self,
wingsail_controller,
reynolds_number,
apparent_wind_direction,
expected_trim_tab_angle,
):
"""
Tests the computation of trim tab angle.

Args:
wingsail_controller: Instance of WingsailController.
"""
computed_trim_tab_angle = wingsail_controller._compute_trim_tab_angle(
reynolds_number, apparent_wind_direction
)
assert np.isclose(computed_trim_tab_angle, expected_trim_tab_angle)

@pytest.mark.parametrize(
"apparent_wind_speed, apparent_wind_direction, expected_trim_tab_angle",
[
(10.0, 0, 6.720859435489),
(4.0, 90.0, 5.75),
(10.0, 180.0, 6.720859435489),
(15.0, -50.0, -6.869536144),
(20.0, -120.0, -6.99271485887),
],
)
def test_get_trim_tab_angle(
self,
wingsail_controller,
apparent_wind_speed,
apparent_wind_direction,
expected_trim_tab_angle,
):
"""
Tests the computation of final trim tab angle.

Args:
wingsail_controller: Instance of WingsailController.
"""
computed_trim_tab_angle = wingsail_controller.get_trim_tab_angle(
apparent_wind_speed, apparent_wind_direction
)
assert np.isclose(computed_trim_tab_angle, expected_trim_tab_angle)