From a2164101ebac8361264169f2ab29cbbdbd7aad03 Mon Sep 17 00:00:00 2001 From: Evan Nawfal Date: Sat, 17 Feb 2024 15:14:48 -0800 Subject: [PATCH 1/8] Wingsail controller with tests --- controller/common/constants.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/controller/common/constants.py b/controller/common/constants.py index 3dc5c4f..d1bf026 100644 --- a/controller/common/constants.py +++ b/controller/common/constants.py @@ -1 +1,4 @@ """Constants used across the controller package.""" + +CHORD_WIDTH_MAIN_SAIL = 0.14 # meters, trim tab chord width is not included +KINEMATIC_VISCOSITY = 0.000014207 # {m^2 / s at 10degC} and air density at 1.225 {kg / m^3} From b50963c3ec48614cd758461a0309277f054f1c05 Mon Sep 17 00:00:00 2001 From: Evan Nawfal Date: Sat, 17 Feb 2024 15:15:12 -0800 Subject: [PATCH 2/8] Wingsail controller with tests --- controller/wingsail/controllers.py | 75 ++++++++++++++++++++++++ tests/unit/wingsail/test_controllers.py | 76 +++++++++++++++++++++++++ 2 files changed, 151 insertions(+) create mode 100644 controller/wingsail/controllers.py create mode 100644 tests/unit/wingsail/test_controllers.py diff --git a/controller/wingsail/controllers.py b/controller/wingsail/controllers.py new file mode 100644 index 0000000..5f799c3 --- /dev/null +++ b/controller/wingsail/controllers.py @@ -0,0 +1,75 @@ +from controller.common.constants import CHORD_WIDTH_MAIN_SAIL, KINEMATIC_VISCOSITY +from controller.common.lut import LUT + + +class WingsailController: + def __init__(self): + self.lut = LUT() # Placeholder for the LUT class + + def _compute_reynolds_number(self, apparent_wind_speed): + """ + Computes the Reynolds number for the main sail. + + Parameters: + - 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 = (apparent_wind_speed * CHORD_WIDTH_MAIN_SAIL) / KINEMATIC_VISCOSITY + return reynolds_number + + def _compute_angle_of_attack(self, reynolds_number, look_up_table): + """ + Computes the desired angle of attack based on Reynolds number and a lookup table. + + Parameters: + - reynolds_number (float): The Reynolds number. + - look_up_table: A 2D numpy array containing Reynolds numbers in the first column + and corresponding desired angles of attack in the second column. + + Returns: + - desired_alpha (float): The computed desired angle of attack based on the provided + Reynolds number and lookup table. + """ + desired_alpha = self.lut.interpolate(reynolds_number, look_up_table) + return desired_alpha + + def _compute_trim_tab_angle(self, desired_alpha, apparent_wind_direction): + """ + Range: -180 < direction <= 180 for symmetry + + Parameters: + - desired_alpha (float): The desired angle of attack. + - apparent_wind_direction (float): The apparent wind direction in degrees. + + Returns: + - trim_tab_angle (float): The computed trim tab angle based on the provided desired angle + of attack, apparent wind direction, and boat direction. + + """ + if apparent_wind_direction >= 0: + trim_tab_angle = desired_alpha + else: + trim_tab_angle = -desired_alpha + + return trim_tab_angle + + def get_trim_tab_angle(self, apparent_wind_speed, apparent_wind_direction): + """ + Computes and returns the final trim tab angle. + + Range: -180 < direction <= 180 for symmetry + + Parameters: + - 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 = self._compute_reynolds_number(apparent_wind_speed) + desired_alpha = self._compute_angle_of_attack(reynolds_number, self.lut.get_table()) + trim_tab_angle = self._compute_trim_tab_angle(desired_alpha, apparent_wind_direction) + + return trim_tab_angle diff --git a/tests/unit/wingsail/test_controllers.py b/tests/unit/wingsail/test_controllers.py new file mode 100644 index 0000000..dd9a180 --- /dev/null +++ b/tests/unit/wingsail/test_controllers.py @@ -0,0 +1,76 @@ +"""Tests classes and functions in controller/wingsail/controllers.py""" + +import numpy as np +import pytest + +from controller.wingsail.controllers import WingsailController + + +class TestWingsailController: + @pytest.fixture + def wingsail_controller(self): + return WingsailController() + + @pytest.mark.parametrize( + "apparent_wind_speed, expected_reynolds_number", + [(10.0, 985171.4622911938), (20.0, 1970342.9245823876), (5.0, 492585.7311458489)], + ) + def test_compute_reynolds_number( + self, wingsail_controller, apparent_wind_speed, expected_reynolds_number + ): + assert np.isclose( + wingsail_controller._compute_reynolds_number(apparent_wind_speed), + expected_reynolds_number, + ) + + @pytest.mark.parametrize( + "reynolds_number, look_up_table, expected_desired_alpha", + [ + (1000000, np.array([[500000, 3], [1500000, 5]]), 4), # Test case 1 + (2000000, np.array([[1500000, 5], [2500000, 7]]), 6), # Test case 2 + (3000000, np.array([[2500000, 7], [3500000, 9]]), 8), # Test case 3 + ], + ) + def test_compute_angle_of_attack( + self, wingsail_controller, reynolds_number, look_up_table, expected_desired_alpha + ): + assert np.isclose( + wingsail_controller._compute_angle_of_attack(reynolds_number, look_up_table), + expected_desired_alpha, + ) + + @pytest.mark.parametrize( + "desired_alpha, apparent_wind_direction, expected_trim_tab_angle", + [ + (5, 45, 5), # Test case 1 + (10, -30, -10), # Test case 2 + (15, 0, 15), # Test case 3 + ], + ) + def test_compute_trim_tab_angle( + self, wingsail_controller, desired_alpha, apparent_wind_direction, expected_trim_tab_angle + ): + assert ( + wingsail_controller._compute_trim_tab_angle(desired_alpha, apparent_wind_direction) + == expected_trim_tab_angle + ) + + @pytest.mark.parametrize( + "apparent_wind_speed, apparent_wind_direction, expected_trim_tab_angle", + [ + (10, 45, 5), # Test case 1 + (20, -30, -10), # Test case 2 + (5, 0, 15), # Test case 3 + ], + ) + def test_get_trim_tab_angle( + self, + wingsail_controller, + apparent_wind_speed, + apparent_wind_direction, + expected_trim_tab_angle, + ): + assert ( + wingsail_controller.get_trim_tab_angle(apparent_wind_speed, apparent_wind_direction) + == expected_trim_tab_angle + ) From 1cd9acaed9e0f8a6c4b5b9a2c364ecea795cb7c5 Mon Sep 17 00:00:00 2001 From: Evan Nawfal Date: Sat, 24 Feb 2024 14:41:57 -0800 Subject: [PATCH 3/8] Implemented WingsailController class for computing sail trim tab angles --- controller/wingsail/controllers.py | 50 +++++++++++++++++------------- 1 file changed, 28 insertions(+), 22 deletions(-) diff --git a/controller/wingsail/controllers.py b/controller/wingsail/controllers.py index 5f799c3..78608ff 100644 --- a/controller/wingsail/controllers.py +++ b/controller/wingsail/controllers.py @@ -1,29 +1,34 @@ -from controller.common.constants import CHORD_WIDTH_MAIN_SAIL, KINEMATIC_VISCOSITY +import math + from controller.common.lut import LUT class WingsailController: - def __init__(self): - self.lut = LUT() # Placeholder for the LUT class + def __init__(self, chord_width_main_sail: float, kinematic_viscosity: float, lut: LUT) -> None: + 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): + def _compute_reynolds_number(self, apparent_wind_speed: float) -> float: """ Computes the Reynolds number for the main sail. - Parameters: + 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 = (apparent_wind_speed * CHORD_WIDTH_MAIN_SAIL) / KINEMATIC_VISCOSITY + reynolds_number: float = ( + apparent_wind_speed * self.chord_width_main_sail + ) / self.kinematic_viscosity return reynolds_number - def _compute_angle_of_attack(self, reynolds_number, look_up_table): + def _compute_angle_of_attack(self, reynolds_number: float, look_up_table: LUT) -> float: """ Computes the desired angle of attack based on Reynolds number and a lookup table. - Parameters: + Args: - reynolds_number (float): The Reynolds number. - look_up_table: A 2D numpy array containing Reynolds numbers in the first column and corresponding desired angles of attack in the second column. @@ -32,14 +37,16 @@ def _compute_angle_of_attack(self, reynolds_number, look_up_table): - desired_alpha (float): The computed desired angle of attack based on the provided Reynolds number and lookup table. """ - desired_alpha = self.lut.interpolate(reynolds_number, look_up_table) + desired_alpha: float = self.lut(reynolds_number) # Using __call__ method return desired_alpha - def _compute_trim_tab_angle(self, desired_alpha, apparent_wind_direction): + def _compute_trim_tab_angle( + self, desired_alpha: float, apparent_wind_direction: float + ) -> float: """ Range: -180 < direction <= 180 for symmetry - Parameters: + Args: - desired_alpha (float): The desired angle of attack. - apparent_wind_direction (float): The apparent wind direction in degrees. @@ -48,28 +55,27 @@ def _compute_trim_tab_angle(self, desired_alpha, apparent_wind_direction): of attack, apparent wind direction, and boat direction. """ - if apparent_wind_direction >= 0: - trim_tab_angle = desired_alpha - else: - trim_tab_angle = -desired_alpha - - return trim_tab_angle + return math.copysign(desired_alpha, apparent_wind_direction) - def get_trim_tab_angle(self, apparent_wind_speed, apparent_wind_direction): + def get_trim_tab_angle( + self, apparent_wind_speed: float, apparent_wind_direction: float + ) -> float: """ Computes and returns the final trim tab angle. Range: -180 < direction <= 180 for symmetry - Parameters: + 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 = self._compute_reynolds_number(apparent_wind_speed) - desired_alpha = self._compute_angle_of_attack(reynolds_number, self.lut.get_table()) - trim_tab_angle = self._compute_trim_tab_angle(desired_alpha, apparent_wind_direction) + reynolds_number: float = self._compute_reynolds_number(apparent_wind_speed) + desired_alpha: float = self._compute_angle_of_attack(reynolds_number, self.lut.get_table()) + trim_tab_angle: float = self._compute_trim_tab_angle( + desired_alpha, apparent_wind_direction + ) return trim_tab_angle From 6b3aef94ec8c0a4e5d731973b7e83ebc07ff2cfa Mon Sep 17 00:00:00 2001 From: Evan Nawfal Date: Sat, 2 Mar 2024 15:05:08 -0800 Subject: [PATCH 4/8] controllers.py revised and fixed tests for WingsailController --- controller/wingsail/controllers.py | 2 +- tests/unit/wingsail/test_controllers.py | 127 ++++++++++++------------ 2 files changed, 63 insertions(+), 66 deletions(-) diff --git a/controller/wingsail/controllers.py b/controller/wingsail/controllers.py index 78608ff..d64e402 100644 --- a/controller/wingsail/controllers.py +++ b/controller/wingsail/controllers.py @@ -73,7 +73,7 @@ def get_trim_tab_angle( - trim_tab_angle (float): The computed trim tab angle. """ reynolds_number: float = self._compute_reynolds_number(apparent_wind_speed) - desired_alpha: float = self._compute_angle_of_attack(reynolds_number, self.lut.get_table()) + desired_alpha: float = self._compute_angle_of_attack(reynolds_number, self.lut) trim_tab_angle: float = self._compute_trim_tab_angle( desired_alpha, apparent_wind_direction ) diff --git a/tests/unit/wingsail/test_controllers.py b/tests/unit/wingsail/test_controllers.py index dd9a180..c060014 100644 --- a/tests/unit/wingsail/test_controllers.py +++ b/tests/unit/wingsail/test_controllers.py @@ -1,76 +1,73 @@ -"""Tests classes and functions in controller/wingsail/controllers.py""" - -import numpy as np import pytest - +import math +import numpy as np +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 = 0.14 +test_kinematic_viscosity = 0.000014207 class TestWingsailController: + """ + Tests the functionality of the WingsailController class. + """ + @pytest.fixture def wingsail_controller(self): - return WingsailController() + """ + Fixture to create an instance of WingsailController for testing. + """ + return WingsailController(test_chord_width, test_kinematic_viscosity, test_lut) + + def test_compute_reynolds_number(self, wingsail_controller): + """ + Tests the computation of Reynolds number. + + Args: + wingsail_controller: Instance of WingsailController. + """ + apparent_wind_speed = 1.0 + expected_reynolds_number = 9854.297177 + computed_reynolds_number = wingsail_controller._compute_reynolds_number(apparent_wind_speed) + assert math.isclose(computed_reynolds_number, expected_reynolds_number) + + def test_compute_angle_of_attack(self, wingsail_controller): + """ + Tests the computation of angle of attack. + + Args: + wingsail_controller: Instance of WingsailController. + """ + reynolds_number = 75000 + expected_desired_alpha = 6.25 + computed_desired_alpha = wingsail_controller._compute_angle_of_attack(reynolds_number, test_lut) + assert math.isclose(computed_desired_alpha, expected_desired_alpha) - @pytest.mark.parametrize( - "apparent_wind_speed, expected_reynolds_number", - [(10.0, 985171.4622911938), (20.0, 1970342.9245823876), (5.0, 492585.7311458489)], - ) - def test_compute_reynolds_number( - self, wingsail_controller, apparent_wind_speed, expected_reynolds_number - ): - assert np.isclose( - wingsail_controller._compute_reynolds_number(apparent_wind_speed), - expected_reynolds_number, - ) + def test_compute_trim_tab_angle(self, wingsail_controller): + """ + Tests the computation of trim tab angle. - @pytest.mark.parametrize( - "reynolds_number, look_up_table, expected_desired_alpha", - [ - (1000000, np.array([[500000, 3], [1500000, 5]]), 4), # Test case 1 - (2000000, np.array([[1500000, 5], [2500000, 7]]), 6), # Test case 2 - (3000000, np.array([[2500000, 7], [3500000, 9]]), 8), # Test case 3 - ], - ) - def test_compute_angle_of_attack( - self, wingsail_controller, reynolds_number, look_up_table, expected_desired_alpha - ): - assert np.isclose( - wingsail_controller._compute_angle_of_attack(reynolds_number, look_up_table), - expected_desired_alpha, - ) + Args: + wingsail_controller: Instance of WingsailController. + """ + desired_alpha = 10.0 + apparent_wind_direction = 45.0 + expected_trim_tab_angle = 10.0 + computed_trim_tab_angle = wingsail_controller._compute_trim_tab_angle(desired_alpha, apparent_wind_direction) + assert math.isclose(computed_trim_tab_angle, expected_trim_tab_angle) - @pytest.mark.parametrize( - "desired_alpha, apparent_wind_direction, expected_trim_tab_angle", - [ - (5, 45, 5), # Test case 1 - (10, -30, -10), # Test case 2 - (15, 0, 15), # Test case 3 - ], - ) - def test_compute_trim_tab_angle( - self, wingsail_controller, desired_alpha, apparent_wind_direction, expected_trim_tab_angle - ): - assert ( - wingsail_controller._compute_trim_tab_angle(desired_alpha, apparent_wind_direction) - == expected_trim_tab_angle - ) + def test_get_trim_tab_angle(self, wingsail_controller): + """ + Tests the computation of final trim tab angle. - @pytest.mark.parametrize( - "apparent_wind_speed, apparent_wind_direction, expected_trim_tab_angle", - [ - (10, 45, 5), # Test case 1 - (20, -30, -10), # Test case 2 - (5, 0, 15), # Test case 3 - ], - ) - def test_get_trim_tab_angle( - self, - wingsail_controller, - apparent_wind_speed, - apparent_wind_direction, - expected_trim_tab_angle, - ): - assert ( - wingsail_controller.get_trim_tab_angle(apparent_wind_speed, apparent_wind_direction) - == expected_trim_tab_angle - ) + Args: + wingsail_controller: Instance of WingsailController. + """ + apparent_wind_speed = 1.0 + apparent_wind_direction = 45.0 + expected_trim_tab_angle = 5.75 + computed_trim_tab_angle = wingsail_controller.get_trim_tab_angle(apparent_wind_speed, apparent_wind_direction) + assert math.isclose(computed_trim_tab_angle, expected_trim_tab_angle) From bc199c887bda390f264748e2fd35467ad6178b47 Mon Sep 17 00:00:00 2001 From: Evan Nawfal Date: Sat, 2 Mar 2024 15:13:18 -0800 Subject: [PATCH 5/8] fixed formatting --- tests/unit/wingsail/test_controllers.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/tests/unit/wingsail/test_controllers.py b/tests/unit/wingsail/test_controllers.py index c060014..5fe6a9d 100644 --- a/tests/unit/wingsail/test_controllers.py +++ b/tests/unit/wingsail/test_controllers.py @@ -10,6 +10,7 @@ test_chord_width = 0.14 test_kinematic_viscosity = 0.000014207 + class TestWingsailController: """ Tests the functionality of the WingsailController class. @@ -31,7 +32,8 @@ def test_compute_reynolds_number(self, wingsail_controller): """ apparent_wind_speed = 1.0 expected_reynolds_number = 9854.297177 - computed_reynolds_number = wingsail_controller._compute_reynolds_number(apparent_wind_speed) + computed_reynolds_number = wingsail_controller._compute_reynolds_number(apparent_wind_speed + ) assert math.isclose(computed_reynolds_number, expected_reynolds_number) def test_compute_angle_of_attack(self, wingsail_controller): @@ -43,7 +45,8 @@ def test_compute_angle_of_attack(self, wingsail_controller): """ reynolds_number = 75000 expected_desired_alpha = 6.25 - computed_desired_alpha = wingsail_controller._compute_angle_of_attack(reynolds_number, test_lut) + computed_desired_alpha = wingsail_controller._compute_angle_of_attack(reynolds_number + , test_lut) assert math.isclose(computed_desired_alpha, expected_desired_alpha) def test_compute_trim_tab_angle(self, wingsail_controller): @@ -56,7 +59,8 @@ def test_compute_trim_tab_angle(self, wingsail_controller): desired_alpha = 10.0 apparent_wind_direction = 45.0 expected_trim_tab_angle = 10.0 - computed_trim_tab_angle = wingsail_controller._compute_trim_tab_angle(desired_alpha, apparent_wind_direction) + computed_trim_tab_angle = wingsail_controller._compute_trim_tab_angle( + desired_alpha, apparent_wind_direction) assert math.isclose(computed_trim_tab_angle, expected_trim_tab_angle) def test_get_trim_tab_angle(self, wingsail_controller): @@ -69,5 +73,6 @@ def test_get_trim_tab_angle(self, wingsail_controller): apparent_wind_speed = 1.0 apparent_wind_direction = 45.0 expected_trim_tab_angle = 5.75 - computed_trim_tab_angle = wingsail_controller.get_trim_tab_angle(apparent_wind_speed, apparent_wind_direction) + computed_trim_tab_angle = wingsail_controller.get_trim_tab_angle( + apparent_wind_speed, apparent_wind_direction) assert math.isclose(computed_trim_tab_angle, expected_trim_tab_angle) From f51c577dd3ad072d57e0899857255096ff2ada7f Mon Sep 17 00:00:00 2001 From: Evan Nawfal Date: Sat, 2 Mar 2024 15:21:14 -0800 Subject: [PATCH 6/8] small formatting fix --- tests/unit/wingsail/test_controllers.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/unit/wingsail/test_controllers.py b/tests/unit/wingsail/test_controllers.py index 5fe6a9d..9f6b6a1 100644 --- a/tests/unit/wingsail/test_controllers.py +++ b/tests/unit/wingsail/test_controllers.py @@ -5,7 +5,8 @@ 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_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 = 0.14 test_kinematic_viscosity = 0.000014207 From 594ed32748dab3d2d170036a87988e34aa09424f Mon Sep 17 00:00:00 2001 From: Evan Nawfal Date: Fri, 8 Mar 2024 19:41:07 -0800 Subject: [PATCH 7/8] Resolved comments and parameterized tests --- controller/wingsail/controllers.py | 52 ++++++------ tests/unit/wingsail/test_controllers.py | 102 +++++++++++++++--------- 2 files changed, 89 insertions(+), 65 deletions(-) diff --git a/controller/wingsail/controllers.py b/controller/wingsail/controllers.py index d64e402..41faaf7 100644 --- a/controller/wingsail/controllers.py +++ b/controller/wingsail/controllers.py @@ -1,12 +1,23 @@ import math +from controller.common.constants import CHORD_WIDTH_MAIN_SAIL, KINEMATIC_VISCOSITY 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) -> None: - self.chord_width_main_sail = chord_width_main_sail - self.kinematic_viscosity = kinematic_viscosity + 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: @@ -24,37 +35,24 @@ def _compute_reynolds_number(self, apparent_wind_speed: float) -> float: ) / self.kinematic_viscosity return reynolds_number - def _compute_angle_of_attack(self, reynolds_number: float, look_up_table: LUT) -> float: - """ - Computes the desired angle of attack based on Reynolds number and a lookup table. - - Args: - - reynolds_number (float): The Reynolds number. - - look_up_table: A 2D numpy array containing Reynolds numbers in the first column - and corresponding desired angles of attack in the second column. - - Returns: - - desired_alpha (float): The computed desired angle of attack based on the provided - Reynolds number and lookup table. - """ - desired_alpha: float = self.lut(reynolds_number) # Using __call__ method - return desired_alpha - def _compute_trim_tab_angle( - self, desired_alpha: float, apparent_wind_direction: float + self, reynolds_number: float, apparent_wind_direction: float ) -> float: """ - Range: -180 < direction <= 180 for symmetry + Computes the trim tab angle based on Reynolds number and apparent wind direction. Args: - - desired_alpha (float): The desired angle of attack. - - apparent_wind_direction (float): The apparent wind direction in degrees. + - 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 desired angle - of attack, apparent wind direction, and boat direction. - + - 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( @@ -73,9 +71,7 @@ def get_trim_tab_angle( - trim_tab_angle (float): The computed trim tab angle. """ reynolds_number: float = self._compute_reynolds_number(apparent_wind_speed) - desired_alpha: float = self._compute_angle_of_attack(reynolds_number, self.lut) trim_tab_angle: float = self._compute_trim_tab_angle( - desired_alpha, apparent_wind_direction + reynolds_number, apparent_wind_direction ) - return trim_tab_angle diff --git a/tests/unit/wingsail/test_controllers.py b/tests/unit/wingsail/test_controllers.py index 9f6b6a1..d2e2b65 100644 --- a/tests/unit/wingsail/test_controllers.py +++ b/tests/unit/wingsail/test_controllers.py @@ -1,15 +1,17 @@ -import pytest -import math 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_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 = 0.14 -test_kinematic_viscosity = 0.000014207 +test_chord_width = CHORD_WIDTH_MAIN_SAIL +test_kinematic_viscosity = KINEMATIC_VISCOSITY class TestWingsailController: @@ -24,56 +26,82 @@ def wingsail_controller(self): """ return WingsailController(test_chord_width, test_kinematic_viscosity, test_lut) - def test_compute_reynolds_number(self, wingsail_controller): + @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. """ - apparent_wind_speed = 1.0 - expected_reynolds_number = 9854.297177 - computed_reynolds_number = wingsail_controller._compute_reynolds_number(apparent_wind_speed - ) - assert math.isclose(computed_reynolds_number, expected_reynolds_number) - - def test_compute_angle_of_attack(self, wingsail_controller): - """ - Tests the computation of angle of attack. - - Args: - wingsail_controller: Instance of WingsailController. - """ - reynolds_number = 75000 - expected_desired_alpha = 6.25 - computed_desired_alpha = wingsail_controller._compute_angle_of_attack(reynolds_number - , test_lut) - assert math.isclose(computed_desired_alpha, expected_desired_alpha) + computed_reynolds_number = wingsail_controller._compute_reynolds_number( + apparent_wind_speed + ) + assert np.isclose(computed_reynolds_number, expected_reynolds_number) - def test_compute_trim_tab_angle(self, wingsail_controller): + @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. """ - desired_alpha = 10.0 - apparent_wind_direction = 45.0 - expected_trim_tab_angle = 10.0 computed_trim_tab_angle = wingsail_controller._compute_trim_tab_angle( - desired_alpha, apparent_wind_direction) - assert math.isclose(computed_trim_tab_angle, expected_trim_tab_angle) + reynolds_number, apparent_wind_direction + ) + assert np.isclose(computed_trim_tab_angle, expected_trim_tab_angle) - def test_get_trim_tab_angle(self, wingsail_controller): + @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. """ - apparent_wind_speed = 1.0 - apparent_wind_direction = 45.0 - expected_trim_tab_angle = 5.75 computed_trim_tab_angle = wingsail_controller.get_trim_tab_angle( - apparent_wind_speed, apparent_wind_direction) - assert math.isclose(computed_trim_tab_angle, expected_trim_tab_angle) + apparent_wind_speed, apparent_wind_direction + ) + assert np.isclose(computed_trim_tab_angle, expected_trim_tab_angle) From 84e8753f21f7762efd6802902adf0b19dda7b955 Mon Sep 17 00:00:00 2001 From: Devon Friend Date: Sat, 9 Mar 2024 11:10:40 -0800 Subject: [PATCH 8/8] Update constants to reflect actual boat metrics --- controller/common/constants.py | 7 +++++-- controller/wingsail/controllers.py | 7 +++---- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/controller/common/constants.py b/controller/common/constants.py index d1bf026..4a32289 100644 --- a/controller/common/constants.py +++ b/controller/common/constants.py @@ -1,4 +1,7 @@ """Constants used across the controller package.""" -CHORD_WIDTH_MAIN_SAIL = 0.14 # meters, trim tab chord width is not included -KINEMATIC_VISCOSITY = 0.000014207 # {m^2 / s at 10degC} and air density at 1.225 {kg / m^3} +# 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 diff --git a/controller/wingsail/controllers.py b/controller/wingsail/controllers.py index 41faaf7..619f174 100644 --- a/controller/wingsail/controllers.py +++ b/controller/wingsail/controllers.py @@ -1,6 +1,5 @@ import math -from controller.common.constants import CHORD_WIDTH_MAIN_SAIL, KINEMATIC_VISCOSITY from controller.common.lut import LUT @@ -15,9 +14,9 @@ class WingsailController: attack. """ - def __init__(self, chord_width_main_sail: float, kinematic_viscosity: float, lut: LUT) -> None: - self.chord_width_main_sail = CHORD_WIDTH_MAIN_SAIL - self.kinematic_viscosity = KINEMATIC_VISCOSITY + 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: