Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implemented Sail Class With Tests #327

Merged
merged 15 commits into from
Mar 29, 2024
Merged
Show file tree
Hide file tree
Changes from 8 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
17 changes: 17 additions & 0 deletions src/controller/controller/common/constants.py
Original file line number Diff line number Diff line change
@@ -1 +1,18 @@
"""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

# lookup_table (List[List[Scalar]]): A list of lists or NDArray containing x-y
# data points for interpolation. Shape should be (n, 2).
# x units is reynolds number, y units is degrees
REYNOLDS_NUMBER_ALPHA_TABLE = [
[50000, 5.75],
[100000, 6.75],
[200000, 7],
[500000, 9.25],
[1000000, 10],
]
78 changes: 78 additions & 0 deletions src/controller/controller/wingsail/controllers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
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.
alberto-escobar marked this conversation as resolved.
Show resolved Hide resolved
"""
desired_alpha: float = self.lut(reynolds_number) # Using __call__ method
# If the controller convention seems reversed, flip the sign of this return statement
# EN, AE - 2024/03/16
return math.copysign(desired_alpha, 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
alberto-escobar marked this conversation as resolved.
Show resolved Hide resolved

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.
alberto-escobar marked this conversation as resolved.
Show resolved Hide resolved
"""
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
33 changes: 28 additions & 5 deletions src/controller/controller/wingsail/wingsail_ctrl_node.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,14 @@
from custom_interfaces.msg import GPS, SailCmd, WindSensor
from rclpy.node import Node

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


def main(args=None):
rclpy.init(args=args)
Expand Down Expand Up @@ -47,8 +55,14 @@ def __init_private_attributes(self):
during the initialization process.
"""
self.__trim_tab_angle = 0.0
self.__filtered_wind_sensor = None
self.__gps = None
self.__filtered_wind_sensor = WindSensor()
self.__gps = GPS()
# pull hardcoded table from the right place later...
# right location should be config.py
lut = LUT(REYNOLDS_NUMBER_ALPHA_TABLE)
self.__wingsailController = WingsailController(
CHORD_WIDTH_MAIN_SAIL, KINEMATIC_VISCOSITY, lut
)

def __declare_ros_parameters(self):
"""Declares ROS parameters from the global configuration file that will be used in this
Expand Down Expand Up @@ -124,11 +138,20 @@ def __init_timer_callbacks(self):
def __publish(self):
"""Publishes a SailCmd message with the trim tab angle using the designated publisher.
It also logs information about the publication to the logger."""

msg = SailCmd()
msg.trim_tab_angle_degrees = 0.0

self.__trim_tab_angle = self.__wingsailController.get_trim_tab_angle(
self.__filtered_wind_sensor.speed.speed, self.__filtered_wind_sensor.direction
)
alberto-escobar marked this conversation as resolved.
Show resolved Hide resolved

msg.trim_tab_angle_degrees = self.__trim_tab_angle

self.__trim_tab_angle_pub.publish(msg)
self.get_logger().info(f"Published to {self.__trim_tab_angle_pub.topic}")

self.get_logger().info(
f"Published to {self.__trim_tab_angle_pub.topic} \
the following angle: {msg.trim_tab_angle_degrees}"
)

@property
def pub_period(self) -> float:
Expand Down
107 changes: 107 additions & 0 deletions src/controller/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.9047300626451),
(4.0, 90.0, 6.045136200464),
(10.0, 180.0, 6.904730062645),
(15.0, -50.0, -7.3212852819032),
(20.0, -120.0, -7.928380375871),
],
)
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)
2 changes: 1 addition & 1 deletion src/custom_interfaces/msg/SailCmd.msg
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Angle to rotate the trim tab relative to the mailsail
# Unit: degrees, 0° is the neutral position, Increases CW
# Unit: degrees, 0° is the neutral position, Increases CCW
# Range: -40° <= angle <= 40°
float32 trim_tab_angle_degrees