From ce7b2bcff2329cf1345d18b01785dd00f96ce619 Mon Sep 17 00:00:00 2001 From: misi9170 Date: Tue, 19 Aug 2025 10:13:42 -0600 Subject: [PATCH 1/8] Initial structure for yaw controller and estimators --- whoc/controllers/wind_farm_yaw_controller.py | 41 ++++++++++++++++++++ whoc/estimators/__init__.py | 0 whoc/estimators/estimator_base.py | 33 ++++++++++++++++ 3 files changed, 74 insertions(+) create mode 100644 whoc/controllers/wind_farm_yaw_controller.py create mode 100644 whoc/estimators/__init__.py create mode 100644 whoc/estimators/estimator_base.py diff --git a/whoc/controllers/wind_farm_yaw_controller.py b/whoc/controllers/wind_farm_yaw_controller.py new file mode 100644 index 00000000..d12b5e0c --- /dev/null +++ b/whoc/controllers/wind_farm_yaw_controller.py @@ -0,0 +1,41 @@ +from __future__ import annotations + +from whoc.controllers.controller_base import ControllerBase +from whoc.estimators.estimator_base import EstimatorBase +from whoc.interfaces.interface_base import InterfaceBase + + +class WindFarmYawController(ControllerBase): + def __init__( + self, + interface: InterfaceBase, + yaw_setpoint_controller: ControllerBase | None = None, + wind_estimator: EstimatorBase | None = None, + verbose: bool = False + ): + """ + Constructor for WindFarmYawController. + + WindFarmYawController is a top-level controller that manages a combined wind estimator + and yaw setpoint controller for a wind farm. + + Args: + interface (InterfaceBase): Interface object for communicating with the plant. + input_dict (dict): Optional dictionary of input parameters. + controller_parameters (dict): Optional dictionary of controller parameters. + yaw_setpoint_controller (ControllerBase): Optional yaw controller to set control + setpoints of individual wind turbines. + wind_estimator (EstimatorBase): Optional wind estimator to provide wind direction + estimates for individual turbines. + """ + super().__init__(interface, verbose=verbose) + + # Store controller and estimator + self.yaw_setpoint_controller = yaw_setpoint_controller + self.wind_estimator = wind_estimator + + def compute_controls(self, measurements_dict): + estimates_dict = self.wind_estimator.compute_estimates(measurements_dict) + controls_dict = self.yaw_setpoint_controller.compute_controls(estimates_dict) + + return controls_dict diff --git a/whoc/estimators/__init__.py b/whoc/estimators/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/whoc/estimators/estimator_base.py b/whoc/estimators/estimator_base.py new file mode 100644 index 00000000..80a33e86 --- /dev/null +++ b/whoc/estimators/estimator_base.py @@ -0,0 +1,33 @@ +from abc import ABCMeta, abstractmethod + + +class EstimatorBase(metaclass=ABCMeta): + def __init__(self, interface, verbose = True): + self._s = interface + self.verbose = verbose + + self._measurements_dict = {} + self._estimates_dict = {} + + def _receive_measurements(self, input_dict=None): + self._measurements_dict = self._s.get_measurements(input_dict) + return None + + def _send_estimates(self, input_dict=None): + self._s.check_estimates(self._estimates_dict) + output_dict = self._s.send_estimates(input_dict, **self._controls_dict) + + return output_dict + + def step(self, input_dict=None): + self._receive_measurements(input_dict) + + output_dict = self.compute_estimates(self._measurements_dict) + + return output_dict + + @abstractmethod + def compute_estimates(self, measurements_dict: dict) -> dict: + raise NotImplementedError( + "compute_estimates method must be implemented in the child class of EstimatorBase." + ) \ No newline at end of file From 874bcca8c8fef69f1ff11c23e1ef57d88875fa3b Mon Sep 17 00:00:00 2001 From: misi9170 Date: Tue, 19 Aug 2025 10:27:14 -0600 Subject: [PATCH 2/8] Establish passthrough controllers --- whoc/controllers/__init__.py | 5 ++++- .../lookup_based_wake_steering_controller.py | 13 +++++++++++++ whoc/controllers/wind_farm_yaw_controller.py | 15 +++++++++++---- whoc/estimators/__init__.py | 1 + whoc/estimators/wind_direction_estimator.py | 15 +++++++++++++++ 5 files changed, 44 insertions(+), 5 deletions(-) create mode 100644 whoc/estimators/wind_direction_estimator.py diff --git a/whoc/controllers/__init__.py b/whoc/controllers/__init__.py index 79a2f941..fd9f7b02 100644 --- a/whoc/controllers/__init__.py +++ b/whoc/controllers/__init__.py @@ -1,7 +1,10 @@ from whoc.controllers.battery_controller import BatteryController, BatteryPassthroughController from whoc.controllers.hybrid_supervisory_controller import HybridSupervisoryControllerBaseline from whoc.controllers.hydrogen_plant_controller import HydrogenPlantController -from whoc.controllers.lookup_based_wake_steering_controller import LookupBasedWakeSteeringController +from whoc.controllers.lookup_based_wake_steering_controller import ( + LookupBasedWakeSteeringController, + YawSetpointPassthroughController, +) from whoc.controllers.solar_passthrough_controller import SolarPassthroughController from whoc.controllers.wake_steering_rosco_standin import WakeSteeringROSCOStandin from whoc.controllers.wind_farm_power_tracking_controller import ( diff --git a/whoc/controllers/lookup_based_wake_steering_controller.py b/whoc/controllers/lookup_based_wake_steering_controller.py index 455d31d3..9b27bb75 100644 --- a/whoc/controllers/lookup_based_wake_steering_controller.py +++ b/whoc/controllers/lookup_based_wake_steering_controller.py @@ -119,3 +119,16 @@ def wake_steering_angles(self, wind_directions): self.controls_dict = {"yaw_angles": yaw_setpoint} return {"yaw_angles": yaw_setpoint} + + +class YawSetpointPassthroughController(ControllerBase): + """ + YawSetpointPassthroughController is a simple controller that passes through wind directions + as yaw setpoints without modification. + """ + def __init__(self, interface: InterfaceBase, verbose: bool = False): + super().__init__(interface, verbose=verbose) + + def compute_controls(self, measurements_dict): + # Simply pass through the yaw setpoints as the received wind directions + return {"yaw_angles": measurements_dict["wind_directions"]} diff --git a/whoc/controllers/wind_farm_yaw_controller.py b/whoc/controllers/wind_farm_yaw_controller.py index d12b5e0c..ef13ba26 100644 --- a/whoc/controllers/wind_farm_yaw_controller.py +++ b/whoc/controllers/wind_farm_yaw_controller.py @@ -1,11 +1,17 @@ from __future__ import annotations +from whoc.controllers import YawSetpointPassthroughController from whoc.controllers.controller_base import ControllerBase +from whoc.estimators import WindDirectionPassthroughEstimator from whoc.estimators.estimator_base import EstimatorBase from whoc.interfaces.interface_base import InterfaceBase class WindFarmYawController(ControllerBase): + """ + WindFarmYawController is a top-level controller that manages a combined wind estimator + and yaw setpoint controller for a wind farm. + """ def __init__( self, interface: InterfaceBase, @@ -16,9 +22,6 @@ def __init__( """ Constructor for WindFarmYawController. - WindFarmYawController is a top-level controller that manages a combined wind estimator - and yaw setpoint controller for a wind farm. - Args: interface (InterfaceBase): Interface object for communicating with the plant. input_dict (dict): Optional dictionary of input parameters. @@ -30,7 +33,11 @@ def __init__( """ super().__init__(interface, verbose=verbose) - # Store controller and estimator + # Establish defaults for yaw setpoint controller and wind estimator and store on self + if yaw_setpoint_controller is None: + yaw_setpoint_controller = YawSetpointPassthroughController(interface, verbose=verbose) + if wind_estimator is None: + wind_estimator = WindDirectionPassthroughEstimator(interface, verbose=verbose) self.yaw_setpoint_controller = yaw_setpoint_controller self.wind_estimator = wind_estimator diff --git a/whoc/estimators/__init__.py b/whoc/estimators/__init__.py index e69de29b..bcc027be 100644 --- a/whoc/estimators/__init__.py +++ b/whoc/estimators/__init__.py @@ -0,0 +1 @@ +from whoc.estimators.wind_direction_estimator import WindDirectionPassthroughEstimator diff --git a/whoc/estimators/wind_direction_estimator.py b/whoc/estimators/wind_direction_estimator.py new file mode 100644 index 00000000..8f838aaf --- /dev/null +++ b/whoc/estimators/wind_direction_estimator.py @@ -0,0 +1,15 @@ +from whoc.estimators.estimator_base import EstimatorBase +from whoc.interfaces.interface_base import InterfaceBase + + +class WindDirectionPassthroughEstimator(EstimatorBase): + """ + WindDirectionPassthroughEstimator is a simple estimator that passes through the wind + direction measurements without modification. + """ + def __init__(self, interface: InterfaceBase, verbose: bool = False): + super().__init__(interface, verbose=verbose) + + def compute_estimates(self, measurements_dict): + # Simply pass through the wind directions as estimates + return {"wind_directions": measurements_dict["wind_directions"]} From 1084fd7c559405dbee3adb4b362c4ce8f8189203 Mon Sep 17 00:00:00 2001 From: misi9170 Date: Wed, 20 Aug 2025 11:56:26 -0600 Subject: [PATCH 3/8] Add tests for estimator base --- tests/controller_library_test.py | 19 ++++++ tests/estimator_base_test.py | 67 +++++++++++++++++++ .../lookup_based_wake_steering_controller.py | 1 + whoc/estimators/estimator_base.py | 14 ++-- 4 files changed, 97 insertions(+), 4 deletions(-) create mode 100644 tests/estimator_base_test.py diff --git a/tests/controller_library_test.py b/tests/controller_library_test.py index 2b522403..04fb5de5 100644 --- a/tests/controller_library_test.py +++ b/tests/controller_library_test.py @@ -12,6 +12,7 @@ SolarPassthroughController, WindFarmPowerDistributingController, WindFarmPowerTrackingController, + YawSetpointPassthroughController, ) from whoc.controllers.wind_farm_power_tracking_controller import POWER_SETPOINT_DEFAULT from whoc.interfaces import ( @@ -92,6 +93,7 @@ def test_controller_instantiation(): _ = SolarPassthroughController(interface=test_interface, input_dict=test_hercules_dict) _ = BatteryPassthroughController(interface=test_interface, input_dict=test_hercules_dict) _ = BatteryController(interface=test_interface, input_dict=test_hercules_dict) + _ = YawSetpointPassthroughController(interface=test_interface) def test_LookupBasedWakeSteeringController(): @@ -646,3 +648,20 @@ def test_HydrogenPlantController(): generator_controller=hybrid_controller, controller_parameters=external_controller_parameters ) + +def test_YawSetpointPassthroughController(): + """ + Tests that the YawSetpointPassthroughController simply passes through the yaw setpoints + from the interface. + """ + test_interface = HerculesADInterface(test_hercules_dict) + test_controller = YawSetpointPassthroughController(test_interface, test_hercules_dict) + + # Check that the controller can be stepped + test_hercules_dict["time"] = 20 + test_hercules_dict_out = test_controller.step(input_dict=test_hercules_dict) + + assert np.allclose( + test_hercules_dict_out["hercules_comms"]["amr_wind"]["test_farm"]["turbine_yaw_angles"], + test_hercules_dict["hercules_comms"]["amr_wind"]["test_farm"]["turbine_wind_directions"] + ) diff --git a/tests/estimator_base_test.py b/tests/estimator_base_test.py new file mode 100644 index 00000000..3cd6e520 --- /dev/null +++ b/tests/estimator_base_test.py @@ -0,0 +1,67 @@ +import pytest +from whoc.estimators.estimator_base import EstimatorBase +from whoc.interfaces.interface_base import InterfaceBase + + +class StandinInterface(InterfaceBase): + """ + Empty class to test controllers. + """ + + def __init__(self): + super().__init__() + + def get_measurements(self): + pass + + def check_controls(self): + pass + + def send_controls(self): + pass + + +class InheritanceTestClassBad(EstimatorBase): + """ + Class that is missing necessary methods (compute_estimates). + """ + + def __init__(self, interface): + super().__init__(interface) + + +class InheritanceTestClassGood(EstimatorBase): + """ + Class that is missing necessary methods. + """ + + def __init__(self, interface): + super().__init__(interface) + + def compute_estimates(self): + pass + + +def test_EstimatorBase_methods(): + """ + Check that the base interface class establishes the correct methods. + """ + test_interface = StandinInterface() + + estimator_base = InheritanceTestClassGood(test_interface) + assert hasattr(estimator_base, "_receive_measurements") + # assert hasattr(estimator_base, "_send_estimates") # Not yet sure we want this. + assert hasattr(estimator_base, "step") + assert hasattr(estimator_base, "compute_estimates") + + +def test_inherited_methods(): + """ + Check that a subclass of InterfaceBase inherits methods correctly. + """ + test_interface = StandinInterface() + + with pytest.raises(TypeError): + _ = InheritanceTestClassBad(test_interface) + + _ = InheritanceTestClassGood(test_interface) diff --git a/whoc/controllers/lookup_based_wake_steering_controller.py b/whoc/controllers/lookup_based_wake_steering_controller.py index 9b27bb75..b6614bae 100644 --- a/whoc/controllers/lookup_based_wake_steering_controller.py +++ b/whoc/controllers/lookup_based_wake_steering_controller.py @@ -100,6 +100,7 @@ def wake_steering_angles(self, wind_directions): wind_speeds, None ) + # TODO: option to return as offsets, rather than absolute angles yaw_offsets = np.diag(interpolated_angles) yaw_setpoint = (np.array(wind_directions) - yaw_offsets).tolist() diff --git a/whoc/estimators/estimator_base.py b/whoc/estimators/estimator_base.py index 80a33e86..a8559220 100644 --- a/whoc/estimators/estimator_base.py +++ b/whoc/estimators/estimator_base.py @@ -14,15 +14,21 @@ def _receive_measurements(self, input_dict=None): return None def _send_estimates(self, input_dict=None): - self._s.check_estimates(self._estimates_dict) - output_dict = self._s.send_estimates(input_dict, **self._controls_dict) + # TODO: how should I use this? Could just return an empty dict for estimates, presumably. + self._s.check_estimates(self._estimates_dict) # _s.check_controls? + output_dict = self._s.send_estimates(input_dict, **self._estimates_dict) # send_controls? - return output_dict + return output_dict # Or simply return input_dict? May work. def step(self, input_dict=None): + """ + Only used if an estimator alone is being run (without an overarching controller). + """ self._receive_measurements(input_dict) - output_dict = self.compute_estimates(self._measurements_dict) + self._estimates_dict = self.compute_estimates(self._measurements_dict) + + output_dict = self._send_estimates(input_dict) return output_dict From 7a939a910dbe5e444c9a40df759dedcaad89376a Mon Sep 17 00:00:00 2001 From: misi9170 Date: Wed, 20 Aug 2025 12:06:12 -0600 Subject: [PATCH 4/8] Basic test for passthrough estimator --- whoc/estimators/estimator_base.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/whoc/estimators/estimator_base.py b/whoc/estimators/estimator_base.py index a8559220..dcb52c7d 100644 --- a/whoc/estimators/estimator_base.py +++ b/whoc/estimators/estimator_base.py @@ -15,10 +15,10 @@ def _receive_measurements(self, input_dict=None): def _send_estimates(self, input_dict=None): # TODO: how should I use this? Could just return an empty dict for estimates, presumably. - self._s.check_estimates(self._estimates_dict) # _s.check_controls? - output_dict = self._s.send_estimates(input_dict, **self._estimates_dict) # send_controls? + #self._s.check_estimates(self._estimates_dict) # _s.check_controls? + #output_dict = self._s.send_estimates(input_dict, **self._estimates_dict) # send_controls? - return output_dict # Or simply return input_dict? May work. + return input_dict # output_dict # Or simply return input_dict? May work. def step(self, input_dict=None): """ From 31e5da237095880dee069808e339e6b8307aecc6 Mon Sep 17 00:00:00 2001 From: misi9170 Date: Wed, 20 Aug 2025 13:03:37 -0600 Subject: [PATCH 5/8] Basic test for passthrough estimator --- tests/estimator_library_test.py | 94 +++++++++++++++++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 tests/estimator_library_test.py diff --git a/tests/estimator_library_test.py b/tests/estimator_library_test.py new file mode 100644 index 00000000..0fe737c1 --- /dev/null +++ b/tests/estimator_library_test.py @@ -0,0 +1,94 @@ +import numpy as np +import pytest + +from whoc.estimators import WindDirectionPassthroughEstimator +from whoc.interfaces import HerculesADInterface +from whoc.interfaces.interface_base import InterfaceBase + + +@pytest.fixture +def test_hercules_dict(): + return { + "dt": 1, + "time": 0, + "controller": { + "num_turbines": 2, + "initial_conditions": {"yaw": [270.0, 270.0]}, + "nominal_plant_power_kW": 10000, + "nominal_hydrogen_rate_kgps": 0.1, + "hydrogen_controller_gain": 1.0, + }, + "hercules_comms": { + "amr_wind": { + "test_farm": { + "turbine_wind_directions": [271.0, 272.5], + "turbine_powers": [4000.0, 4001.0], + "wind_speed": 10.0, + } + } + }, + "py_sims": { + "test_battery": { + "outputs": {"power": 10.0, "soc": 0.3}, + "charge_rate":20, + "discharge_rate":20 + }, + "test_solar": {"outputs": {"power_mw": 1.0, "dni": 1000.0, "aoi": 30.0}}, + "test_hydrogen": {"outputs": {"H2_mfr": 0.03}}, + "inputs": {}, + }, + "external_signals": {"wind_power_reference": 1000.0, "plant_power_reference": 1000.0, + "hydrogen_reference": 0.02}, + } + +class StandinInterface(InterfaceBase): + """ + Empty class to test controllers. + """ + + def __init__(self): + super().__init__() + + def get_measurements(self): + pass + + def check_controls(self): + pass + + def send_controls(self): + pass + +def test_estimator_instantiation(): + """ + Tests whether all controllers can be imported correctly and that they + each implement the required methods specified by ControllerBase. + """ + test_interface = StandinInterface() + + _ = WindDirectionPassthroughEstimator(interface=test_interface) + +def test_YawSetpointPassthroughController(test_hercules_dict): + """ + Tests that the YawSetpointPassthroughController simply passes through the yaw setpoints + from the interface. + """ + test_interface = HerculesADInterface(test_hercules_dict) + test_estimator = WindDirectionPassthroughEstimator(test_interface, test_hercules_dict) + + # Check that the controller can be stepped (simply returns inputs) + test_hercules_dict["time"] = 20 + test_hercules_dict_out = test_estimator.step(input_dict=test_hercules_dict) + + assert np.allclose( + test_hercules_dict_out["hercules_comms"]["amr_wind"]["test_farm"] + ["turbine_wind_directions"], + test_hercules_dict["hercules_comms"]["amr_wind"]["test_farm"]["turbine_wind_directions"] + ) + + # Test that estimates are also computed (for passthrough, these are simply a match) + estimates_dict = test_estimator.compute_estimates(test_estimator._measurements_dict) + + assert np.allclose( + estimates_dict["wind_directions"], + test_estimator._measurements_dict["wind_directions"] + ) From c83a860fc3d46ded03e8f93db41750c480b1814d Mon Sep 17 00:00:00 2001 From: misi9170 Date: Wed, 20 Aug 2025 14:32:40 -0600 Subject: [PATCH 6/8] Reconfigure tests to use fixtures and conftest --- tests/conftest.py | 140 +++++++++++++++++++++ tests/controller_base_test.py | 33 +---- tests/controller_library_test.py | 190 ++++++++++++----------------- tests/estimator_base_test.py | 33 +---- tests/estimator_library_test.py | 70 ++--------- tests/interface_library_test.py | 37 +----- tests/wake_steering_design_test.py | 52 ++++---- 7 files changed, 269 insertions(+), 286 deletions(-) create mode 100644 tests/conftest.py diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 00000000..2fedb474 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,140 @@ +import pytest +from whoc.interfaces import HerculesADInterface, HerculesHybridADInterface +from whoc.interfaces.interface_base import InterfaceBase + + +@pytest.fixture +def test_hercules_dict(): + return { + "dt": 1, + "time": 0, + "controller": { + "num_turbines": 2, + "initial_conditions": {"yaw": [270.0, 270.0]}, + "nominal_plant_power_kW": 10000, + "nominal_hydrogen_rate_kgps": 0.1, + "hydrogen_controller_gain": 1.0, + }, + "hercules_comms": { + "amr_wind": { + "test_farm": { + "turbine_wind_directions": [271.0, 272.5], + "turbine_powers": [4000.0, 4001.0], + "wind_speed": 10.0, + } + } + }, + "py_sims": { + "test_battery": { + "outputs": {"power": 10.0, "soc": 0.3}, + "charge_rate":20, + "discharge_rate":20 + }, + "test_solar": {"outputs": {"power_mw": 1.0, "dni": 1000.0, "aoi": 30.0}}, + "test_hydrogen": {"outputs": {"H2_mfr": 0.03}}, + "inputs": {}, + }, + "external_signals": {"wind_power_reference": 1000.0, "plant_power_reference": 1000.0, + "hydrogen_reference": 0.02}, + } + +class StandinInterface(InterfaceBase): + """ + Empty class to test controllers. + """ + + def __init__(self): + super().__init__() + + def get_measurements(self): + pass + + def check_controls(self): + pass + + def send_controls(self): + pass + +@pytest.fixture +def test_interface_standin(): + return StandinInterface() + +@pytest.fixture +def test_interface_hercules_ad(test_hercules_dict): + """ + Fixture to create a HerculesADInterface for testing. + """ + return HerculesADInterface(test_hercules_dict) + +@pytest.fixture +def test_interface_hercules_hybrid_ad(test_hercules_dict): + """ + Fixture to create a HerculesHybridADInterface for testing. + """ + test_hercules_dict["controller"]["num_batteries"] = 1 + test_hercules_dict["controller"]["num_solar"] = 1 + return HerculesHybridADInterface(test_hercules_dict) + +@pytest.fixture +def floris_dict(): + """ + Fixture to create a FLORIS dictionary for testing. + """ + return { + "name": "test_input", + "description": "Two-turbine farm for testing", + "floris_version": "v4", + "logging": { + "console": {"enable": False, "level": "WARNING"}, + "file": {"enable": False, "level": "WARNING"}, + }, + "solver": {"type": "turbine_grid", "turbine_grid_points": 3}, + "farm": { + "layout_x": [0.0, 500.0], + "layout_y": [0.0, 0.0], + "turbine_type": ["nrel_5MW"], + }, + "flow_field": { + "air_density": 1.225, + "reference_wind_height": 90.0, + "turbulence_intensities": [0.06], + "wind_directions": [270.0], + "wind_shear": 0.12, + "wind_speeds": [8.0], + "wind_veer": 0.0, + }, + "wake": { + "model_strings": { + "combination_model": "sosfs", + "deflection_model": "gauss", + "turbulence_model": "crespo_hernandez", + "velocity_model": "gauss", + }, + "enable_secondary_steering": True, + "enable_yaw_added_recovery": True, + "enable_active_wake_mixing": True, + "enable_transverse_velocities": True, + "wake_deflection_parameters": { + "gauss": { + "ad": 0.0, + "alpha": 0.58, + "bd": 0.0, + "beta": 0.077, + "dm": 1.0, + "ka": 0.38, + "kb": 0.004, + }, + }, + "wake_velocity_parameters": { + "gauss": {"alpha": 0.58, "beta": 0.077, "ka": 0.38, "kb": 0.004}, + }, + "wake_turbulence_parameters": { + "crespo_hernandez": { + "initial": 0.01, + "constant": 0.9, + "ai": 0.83, + "downstream": -0.25, + } + }, + }, + } diff --git a/tests/controller_base_test.py b/tests/controller_base_test.py index 60e8c1ae..fc607ded 100644 --- a/tests/controller_base_test.py +++ b/tests/controller_base_test.py @@ -1,24 +1,5 @@ import pytest from whoc.controllers.controller_base import ControllerBase -from whoc.interfaces.interface_base import InterfaceBase - - -class StandinInterface(InterfaceBase): - """ - Empty class to test controllers. - """ - - def __init__(self): - super().__init__() - - def get_measurements(self): - pass - - def check_controls(self): - pass - - def send_controls(self): - pass class InheritanceTestClassBad(ControllerBase): @@ -42,26 +23,22 @@ def compute_controls(self): pass -def test_ControllerBase_methods(): +def test_ControllerBase_methods(test_interface_standin): """ Check that the base interface class establishes the correct methods. """ - test_interface = StandinInterface() - - controller_base = InheritanceTestClassGood(test_interface) + controller_base = InheritanceTestClassGood(test_interface_standin) assert hasattr(controller_base, "_receive_measurements") assert hasattr(controller_base, "_send_controls") assert hasattr(controller_base, "step") assert hasattr(controller_base, "compute_controls") -def test_inherited_methods(): +def test_inherited_methods(test_interface_standin): """ Check that a subclass of InterfaceBase inherits methods correctly. """ - test_interface = StandinInterface() - with pytest.raises(TypeError): - _ = InheritanceTestClassBad(test_interface) + _ = InheritanceTestClassBad(test_interface_standin) - _ = InheritanceTestClassGood(test_interface) + _ = InheritanceTestClassGood(test_interface_standin) diff --git a/tests/controller_library_test.py b/tests/controller_library_test.py index 04fb5de5..6f5fe940 100644 --- a/tests/controller_library_test.py +++ b/tests/controller_library_test.py @@ -16,92 +16,42 @@ ) from whoc.controllers.wind_farm_power_tracking_controller import POWER_SETPOINT_DEFAULT from whoc.interfaces import ( - HerculesADInterface, HerculesBatteryInterface, - HerculesHybridADInterface, ) -from whoc.interfaces.interface_base import InterfaceBase -class StandinInterface(InterfaceBase): - """ - Empty class to test controllers. - """ - - def __init__(self): - super().__init__() - - def get_measurements(self): - pass - - def check_controls(self): - pass - - def send_controls(self): - pass - - -test_hercules_dict = { - "dt": 1, - "time": 0, - "controller": { - "num_turbines": 2, - "initial_conditions": {"yaw": [270.0, 270.0]}, - "nominal_plant_power_kW": 10000, - "nominal_hydrogen_rate_kgps": 0.1, - "hydrogen_controller_gain": 1.0, - }, - "hercules_comms": { - "amr_wind": { - "test_farm": { - "turbine_wind_directions": [271.0, 272.5], - "turbine_powers": [4000.0, 4001.0], - "wind_speed": 10.0, - } - } - }, - "py_sims": { - "test_battery": { - "outputs": {"power": 10.0, "soc": 0.3}, - "charge_rate":20, - "discharge_rate":20 - }, - "test_solar": {"outputs": {"power_mw": 1.0, "dni": 1000.0, "aoi": 30.0}}, - "test_hydrogen": {"outputs": {"H2_mfr": 0.03}}, - "inputs": {}, - }, - "external_signals": {"wind_power_reference": 1000.0, "plant_power_reference": 1000.0, - "hydrogen_reference": 0.02}, -} - - -def test_controller_instantiation(): +def test_controller_instantiation(test_interface_standin, test_hercules_dict): """ Tests whether all controllers can be imported correctly and that they each implement the required methods specified by ControllerBase. """ - test_interface = StandinInterface() - - _ = LookupBasedWakeSteeringController(interface=test_interface, input_dict=test_hercules_dict) - _ = WindFarmPowerDistributingController(interface=test_interface, input_dict=test_hercules_dict) - _ = WindFarmPowerTrackingController(interface=test_interface, input_dict=test_hercules_dict) + _ = LookupBasedWakeSteeringController( + interface=test_interface_standin, input_dict=test_hercules_dict + ) + _ = WindFarmPowerDistributingController( + interface=test_interface_standin, input_dict=test_hercules_dict + ) + _ = WindFarmPowerTrackingController( + interface=test_interface_standin, input_dict=test_hercules_dict + ) _ = HybridSupervisoryControllerBaseline( - interface=test_interface, + interface=test_interface_standin, input_dict=test_hercules_dict, wind_controller=1, # Override error raised for empty controllers ) - _ = SolarPassthroughController(interface=test_interface, input_dict=test_hercules_dict) - _ = BatteryPassthroughController(interface=test_interface, input_dict=test_hercules_dict) - _ = BatteryController(interface=test_interface, input_dict=test_hercules_dict) - _ = YawSetpointPassthroughController(interface=test_interface) + _ = SolarPassthroughController(interface=test_interface_standin, input_dict=test_hercules_dict) + _ = BatteryPassthroughController( + interface=test_interface_standin, input_dict=test_hercules_dict + ) + _ = BatteryController(interface=test_interface_standin, input_dict=test_hercules_dict) + _ = YawSetpointPassthroughController(interface=test_interface_standin) -def test_LookupBasedWakeSteeringController(): - test_interface = HerculesADInterface(test_hercules_dict) +def test_LookupBasedWakeSteeringController(test_hercules_dict, test_interface_hercules_ad): # No lookup table passed; simply passes through wind direction to yaw angles test_controller = LookupBasedWakeSteeringController( - interface=test_interface, + interface=test_interface_hercules_ad, input_dict=test_hercules_dict ) @@ -126,7 +76,7 @@ def test_LookupBasedWakeSteeringController(): "turbulence_intensity":[0.06]*4 }) test_controller = LookupBasedWakeSteeringController( - interface=test_interface, + interface=test_interface_hercules_ad, input_dict=test_hercules_dict, df_yaw=df_opt_test ) @@ -141,10 +91,9 @@ def test_LookupBasedWakeSteeringController(): ) assert np.allclose(test_angles, wind_directions - test_offsets) -def test_WindFarmPowerDistributingController(): - test_interface = HerculesADInterface(test_hercules_dict) +def test_WindFarmPowerDistributingController(test_hercules_dict, test_interface_hercules_ad): test_controller = WindFarmPowerDistributingController( - interface=test_interface, + interface=test_interface_hercules_ad, input_dict=test_hercules_dict ) @@ -168,10 +117,9 @@ def test_WindFarmPowerDistributingController(): ) assert np.allclose(test_power_setpoints, 500) -def test_WindFarmPowerTrackingController(): - test_interface = HerculesADInterface(test_hercules_dict) +def test_WindFarmPowerTrackingController(test_hercules_dict, test_interface_hercules_ad): test_controller = WindFarmPowerTrackingController( - interface=test_interface, + interface=test_interface_hercules_ad, input_dict=test_hercules_dict ) @@ -208,7 +156,7 @@ def test_WindFarmPowerTrackingController(): # Test that more aggressive control leads to faster response test_controller = WindFarmPowerTrackingController( - interface=test_interface, + interface=test_interface_hercules_ad, input_dict=test_hercules_dict, proportional_gain=2 ) @@ -219,16 +167,21 @@ def test_WindFarmPowerTrackingController(): ) assert (test_power_setpoints_a < test_power_setpoints).all() -def test_HybridSupervisoryControllerBaseline(): - test_interface = HerculesHybridADInterface(test_hercules_dict) +def test_HybridSupervisoryControllerBaseline(test_hercules_dict, test_interface_hercules_hybrid_ad): # Establish lower controllers - wind_controller = WindFarmPowerTrackingController(test_interface, test_hercules_dict) - solar_controller = SolarPassthroughController(test_interface, test_hercules_dict) - battery_controller = BatteryPassthroughController(test_interface, test_hercules_dict) + wind_controller = WindFarmPowerTrackingController( + test_interface_hercules_hybrid_ad, test_hercules_dict + ) + solar_controller = SolarPassthroughController( + test_interface_hercules_hybrid_ad, test_hercules_dict + ) + battery_controller = BatteryPassthroughController( + test_interface_hercules_hybrid_ad, test_hercules_dict + ) test_controller = HybridSupervisoryControllerBaseline( - interface=test_interface, + interface=test_interface_hercules_hybrid_ad, input_dict=test_hercules_dict, wind_controller=wind_controller, solar_controller=solar_controller, @@ -262,21 +215,29 @@ def test_HybridSupervisoryControllerBaseline(): [wind_power_cmd, solar_power_cmd, battery_power_cmd] ) # To charge battery -def test_HybridSupervisoryControllerBaseline_subsets(): +def test_HybridSupervisoryControllerBaseline_subsets( + test_hercules_dict, test_interface_hercules_hybrid_ad +): """ Tests that the HybridSupervisoryControllerBaseline can be run with only some of the wind, solar, and battery controllers. """ - test_interface = HerculesHybridADInterface(test_hercules_dict) + test_interface = test_interface_hercules_hybrid_ad # Establish lower controllers - wind_controller = WindFarmPowerTrackingController(test_interface, test_hercules_dict) - solar_controller = SolarPassthroughController(test_interface, test_hercules_dict) - battery_controller = BatteryPassthroughController(test_interface, test_hercules_dict) + wind_controller = WindFarmPowerTrackingController( + test_interface_hercules_hybrid_ad, test_hercules_dict + ) + solar_controller = SolarPassthroughController( + test_interface_hercules_hybrid_ad, test_hercules_dict + ) + battery_controller = BatteryPassthroughController( + test_interface_hercules_hybrid_ad, test_hercules_dict + ) ## First, try with wind and solar only test_controller = HybridSupervisoryControllerBaseline( - interface=test_interface, + interface=test_interface_hercules_hybrid_ad, input_dict=test_hercules_dict, wind_controller=wind_controller, solar_controller=solar_controller, @@ -419,25 +380,27 @@ def test_HybridSupervisoryControllerBaseline_subsets(): [wind_power_cmd, solar_power_cmd, battery_power_cmd] ) -def test_BatteryPassthroughController(): - test_interface = HerculesHybridADInterface(test_hercules_dict) - test_controller = BatteryPassthroughController(test_interface, test_hercules_dict) +def test_BatteryPassthroughController(test_hercules_dict, test_interface_hercules_hybrid_ad): + test_controller = BatteryPassthroughController( + test_interface_hercules_hybrid_ad, test_hercules_dict + ) power_ref = 1000 measurements_dict = {"power_reference": power_ref} controls_dict = test_controller.compute_controls(measurements_dict) assert controls_dict["power_setpoint"] == power_ref -def test_SolarPassthroughController(): - test_interface = HerculesHybridADInterface(test_hercules_dict) - test_controller = SolarPassthroughController(test_interface, test_hercules_dict) +def test_SolarPassthroughController(test_hercules_dict, test_interface_hercules_hybrid_ad): + test_controller = SolarPassthroughController( + test_interface_hercules_hybrid_ad, test_hercules_dict + ) power_ref = 1000 measurements_dict = {"power_reference": power_ref} controls_dict = test_controller.compute_controls(measurements_dict) assert controls_dict["power_setpoint"] == power_ref -def test_BatteryController(): +def test_BatteryController(test_hercules_dict): test_interface = HerculesBatteryInterface(test_hercules_dict) test_controller = BatteryController(test_interface, test_hercules_dict, {"k_batt":0.1}) @@ -539,17 +502,17 @@ def test_BatteryController(): assert out_0 > out_1 -def test_HydrogenPlantController(): +def test_HydrogenPlantController(test_hercules_dict, test_interface_hercules_hybrid_ad): """ Tests that the HydrogenPlantController outputs a reasonable signal """ - test_interface = HerculesHybridADInterface(test_hercules_dict) - ## Test with only wind providing generation - wind_controller = WindFarmPowerTrackingController(test_interface, test_hercules_dict) + wind_controller = WindFarmPowerTrackingController( + test_interface_hercules_hybrid_ad, test_hercules_dict + ) test_controller = HydrogenPlantController( - interface=test_interface, + interface=test_interface_hercules_hybrid_ad, input_dict=test_hercules_dict, generator_controller=wind_controller, ) @@ -581,15 +544,19 @@ def test_HydrogenPlantController(): # Test with a full wind/solar/battery plant hybrid_controller = HybridSupervisoryControllerBaseline( - interface=test_interface, + interface=test_interface_hercules_hybrid_ad, input_dict=test_hercules_dict, wind_controller=wind_controller, - solar_controller=SolarPassthroughController(test_interface, test_hercules_dict), - battery_controller=BatteryPassthroughController(test_interface, test_hercules_dict) + solar_controller=SolarPassthroughController( + test_interface_hercules_hybrid_ad, test_hercules_dict + ), + battery_controller=BatteryPassthroughController( + test_interface_hercules_hybrid_ad, test_hercules_dict + ) ) test_controller = HydrogenPlantController( - interface=test_interface, + interface=test_interface_hercules_hybrid_ad, input_dict=test_hercules_dict, generator_controller=hybrid_controller, ) @@ -622,7 +589,7 @@ def test_HydrogenPlantController(): # Test an error is raised if controller_parameters is passed while also specified on input_dict with pytest.raises(KeyError): HydrogenPlantController( - interface=test_interface, + interface=test_interface_hercules_hybrid_ad, input_dict=test_hercules_dict, generator_controller=hybrid_controller, controller_parameters=external_controller_parameters @@ -633,7 +600,7 @@ def test_HydrogenPlantController(): del test_hercules_dict["controller"]["nominal_plant_power_kW"] with pytest.raises(TypeError): HydrogenPlantController( - interface=test_interface, + interface=test_interface_hercules_hybrid_ad, input_dict=test_hercules_dict, generator_controller=hybrid_controller, ) @@ -643,19 +610,20 @@ def test_HydrogenPlantController(): del test_hercules_dict["controller"]["hydrogen_controller_gain"] test_controller = HydrogenPlantController( - interface=test_interface, + interface=test_interface_hercules_hybrid_ad, input_dict=test_hercules_dict, generator_controller=hybrid_controller, controller_parameters=external_controller_parameters ) -def test_YawSetpointPassthroughController(): +def test_YawSetpointPassthroughController(test_hercules_dict, test_interface_hercules_ad): """ Tests that the YawSetpointPassthroughController simply passes through the yaw setpoints from the interface. """ - test_interface = HerculesADInterface(test_hercules_dict) - test_controller = YawSetpointPassthroughController(test_interface, test_hercules_dict) + test_controller = YawSetpointPassthroughController( + test_interface_hercules_ad, test_hercules_dict + ) # Check that the controller can be stepped test_hercules_dict["time"] = 20 diff --git a/tests/estimator_base_test.py b/tests/estimator_base_test.py index 3cd6e520..47da3f59 100644 --- a/tests/estimator_base_test.py +++ b/tests/estimator_base_test.py @@ -1,24 +1,5 @@ import pytest from whoc.estimators.estimator_base import EstimatorBase -from whoc.interfaces.interface_base import InterfaceBase - - -class StandinInterface(InterfaceBase): - """ - Empty class to test controllers. - """ - - def __init__(self): - super().__init__() - - def get_measurements(self): - pass - - def check_controls(self): - pass - - def send_controls(self): - pass class InheritanceTestClassBad(EstimatorBase): @@ -42,26 +23,22 @@ def compute_estimates(self): pass -def test_EstimatorBase_methods(): +def test_EstimatorBase_methods(test_interface_standin): """ Check that the base interface class establishes the correct methods. """ - test_interface = StandinInterface() - - estimator_base = InheritanceTestClassGood(test_interface) + estimator_base = InheritanceTestClassGood(test_interface_standin) assert hasattr(estimator_base, "_receive_measurements") # assert hasattr(estimator_base, "_send_estimates") # Not yet sure we want this. assert hasattr(estimator_base, "step") assert hasattr(estimator_base, "compute_estimates") -def test_inherited_methods(): +def test_inherited_methods(test_interface_standin): """ Check that a subclass of InterfaceBase inherits methods correctly. """ - test_interface = StandinInterface() - with pytest.raises(TypeError): - _ = InheritanceTestClassBad(test_interface) + _ = InheritanceTestClassBad(test_interface_standin) - _ = InheritanceTestClassGood(test_interface) + _ = InheritanceTestClassGood(test_interface_standin) diff --git a/tests/estimator_library_test.py b/tests/estimator_library_test.py index 0fe737c1..cd67eff1 100644 --- a/tests/estimator_library_test.py +++ b/tests/estimator_library_test.py @@ -1,79 +1,23 @@ import numpy as np -import pytest - from whoc.estimators import WindDirectionPassthroughEstimator -from whoc.interfaces import HerculesADInterface -from whoc.interfaces.interface_base import InterfaceBase - -@pytest.fixture -def test_hercules_dict(): - return { - "dt": 1, - "time": 0, - "controller": { - "num_turbines": 2, - "initial_conditions": {"yaw": [270.0, 270.0]}, - "nominal_plant_power_kW": 10000, - "nominal_hydrogen_rate_kgps": 0.1, - "hydrogen_controller_gain": 1.0, - }, - "hercules_comms": { - "amr_wind": { - "test_farm": { - "turbine_wind_directions": [271.0, 272.5], - "turbine_powers": [4000.0, 4001.0], - "wind_speed": 10.0, - } - } - }, - "py_sims": { - "test_battery": { - "outputs": {"power": 10.0, "soc": 0.3}, - "charge_rate":20, - "discharge_rate":20 - }, - "test_solar": {"outputs": {"power_mw": 1.0, "dni": 1000.0, "aoi": 30.0}}, - "test_hydrogen": {"outputs": {"H2_mfr": 0.03}}, - "inputs": {}, - }, - "external_signals": {"wind_power_reference": 1000.0, "plant_power_reference": 1000.0, - "hydrogen_reference": 0.02}, - } -class StandinInterface(InterfaceBase): - """ - Empty class to test controllers. - """ - - def __init__(self): - super().__init__() - - def get_measurements(self): - pass - - def check_controls(self): - pass - - def send_controls(self): - pass - -def test_estimator_instantiation(): +def test_estimator_instantiation(test_interface_standin): """ Tests whether all controllers can be imported correctly and that they each implement the required methods specified by ControllerBase. """ - test_interface = StandinInterface() + _ = WindDirectionPassthroughEstimator(interface=test_interface_standin) - _ = WindDirectionPassthroughEstimator(interface=test_interface) - -def test_YawSetpointPassthroughController(test_hercules_dict): +def test_YawSetpointPassthroughController(test_interface_hercules_ad, test_hercules_dict): """ Tests that the YawSetpointPassthroughController simply passes through the yaw setpoints from the interface. """ - test_interface = HerculesADInterface(test_hercules_dict) - test_estimator = WindDirectionPassthroughEstimator(test_interface, test_hercules_dict) + test_estimator = WindDirectionPassthroughEstimator( + test_interface_hercules_ad, + test_hercules_dict + ) # Check that the controller can be stepped (simply returns inputs) test_hercules_dict["time"] = 20 diff --git a/tests/interface_library_test.py b/tests/interface_library_test.py index f7fbe40a..c1b3a830 100644 --- a/tests/interface_library_test.py +++ b/tests/interface_library_test.py @@ -5,37 +5,8 @@ HerculesHybridADInterface, ) -test_hercules_dict = { - "dt": 1, - "time": 0, - "controller": {"num_turbines": 2, "wind_capacity_MW": 10}, - "hercules_comms": { - "amr_wind": { - "test_farm": { - "turbine_wind_directions": [271.0, 272.5], - "turbine_powers": [4000.0, 4001.0], - "wind_speed": 10.0, - } - } - }, - "py_sims": { - "test_battery": {"outputs": {"power": 10.0, "soc": 0.3}, "charge_rate":20}, - "test_solar": {"outputs": {"power_mw": 1.0, "dni": 1000.0, "aoi": 30.0}}, - "test_hydrogen": {"outputs": {"H2_mfr": 0.03} }, - "inputs": {}, - }, - "external_signals": { - "wind_power_reference": 1000.0, - "plant_power_reference": 1000.0, - "forecast_ws_mean_0": 8.0, - "forecast_ws_mean_1": 8.1, - "ws_median_0": 8.1, - "hydrogen_reference": 0.02, - }, -} - -def test_interface_instantiation(): +def test_interface_instantiation(test_hercules_dict): """ Tests whether all interfaces can be imported correctly and that they each implement the required methods specified by InterfaceBase. @@ -47,7 +18,7 @@ def test_interface_instantiation(): # _ = ROSCO_ZMQInterface() -def test_HerculesADInterface(): +def test_HerculesADInterface(test_hercules_dict): interface = HerculesADInterface(hercules_dict=test_hercules_dict) # Test get_measurements() @@ -117,7 +88,7 @@ def test_HerculesADInterface(): test_hercules_dict["external_signals"]["wind_power_reference"] = 1000.0 test_hercules_dict["external_signals"]["plant_power_reference"] = 1000.0 -def test_HerculesHybridADInterface(): +def test_HerculesHybridADInterface(test_hercules_dict): interface = HerculesHybridADInterface(hercules_dict=test_hercules_dict) # Test get_measurements() @@ -202,7 +173,7 @@ def test_HerculesHybridADInterface(): with pytest.raises(TypeError): # Bad kwarg interface.send_controls(test_hercules_dict, **bad_controls_dict) -def test_HerculesBatteryInterface(): +def test_HerculesBatteryInterface(test_hercules_dict): interface = HerculesBatteryInterface(hercules_dict=test_hercules_dict) diff --git a/tests/wake_steering_design_test.py b/tests/wake_steering_design_test.py index 0f238d2a..8f66e232 100644 --- a/tests/wake_steering_design_test.py +++ b/tests/wake_steering_design_test.py @@ -19,6 +19,7 @@ YAML_INPUT = TEST_DATA / "floris_input.yaml" def generic_df_opt( + floris_dictionary, wd_resolution=4.0, wd_min=220.0, wd_max=310.0, @@ -34,7 +35,7 @@ def generic_df_opt( kwargs_UncertainFlorisModel = {}, ): - fmodel_test = FlorisModel(YAML_INPUT) + fmodel_test = FlorisModel(floris_dictionary) if wd_std is None: return build_simple_wake_steering_lookup_table( @@ -67,7 +68,7 @@ def generic_df_opt( kwargs_UncertainFlorisModel=kwargs_UncertainFlorisModel, ) -def test_build_simple_wake_steering_lookup_table(): +def test_build_simple_wake_steering_lookup_table(floris_dict): # Start with the simple case wd_resolution = 4.0 @@ -82,6 +83,7 @@ def test_build_simple_wake_steering_lookup_table(): minimum_yaw_angle = -20 maximum_yaw_angle = 20 df_opt = generic_df_opt( + floris_dict, wd_resolution=wd_resolution, wd_min=wd_min, wd_max=wd_max, @@ -94,7 +96,7 @@ def test_build_simple_wake_steering_lookup_table(): ) - df_opt = generic_df_opt() + df_opt = generic_df_opt(floris_dict) opt_yaw_angles = np.vstack(df_opt["yaw_angles_opt"]) @@ -113,6 +115,7 @@ def test_build_simple_wake_steering_lookup_table(): wd_max = 360.0 minimum_yaw_angle = -5 # Positive numbers DO NOT WORK. FLORIS bug? df_opt = generic_df_opt( + floris_dict, wd_resolution=wd_resolution, wd_min=wd_min, wd_max=wd_max, @@ -142,6 +145,7 @@ def test_build_simple_wake_steering_lookup_table(): wd_min = 2.0 wd_max = 360.0 # Shouldn't appear in output; max should be 358.0 df_opt = generic_df_opt( + floris_dict, wd_resolution=wd_resolution, wd_min=wd_min, wd_max=wd_max, @@ -149,12 +153,12 @@ def test_build_simple_wake_steering_lookup_table(): assert df_opt.wind_direction.min() == wd_min assert df_opt.wind_direction.max() == 358.0 -def test_build_uncertain_wake_steering_lookup_table(): +def test_build_uncertain_wake_steering_lookup_table(floris_dict): max_yaw_angle = 35 # To force split between basic and uncertain - df_opt_simple = generic_df_opt(wd_std=None, maximum_yaw_angle=max_yaw_angle) - df_opt_uncertain = generic_df_opt(wd_std=3.0, maximum_yaw_angle=max_yaw_angle) + df_opt_simple = generic_df_opt(floris_dict, wd_std=None, maximum_yaw_angle=max_yaw_angle) + df_opt_uncertain = generic_df_opt(floris_dict, wd_std=3.0, maximum_yaw_angle=max_yaw_angle) max_offset_simple = df_opt_simple.yaw_angles_opt.apply(lambda x: np.max(x)).max() max_offset_uncertain = df_opt_uncertain.yaw_angles_opt.apply(lambda x: np.max(x)).max() @@ -162,19 +166,21 @@ def test_build_uncertain_wake_steering_lookup_table(): # Check that kwargs are passed correctly (results not identical) df_opt_uncertain_fixed = generic_df_opt( + floris_dict, wd_std=3.0, maximum_yaw_angle=max_yaw_angle, kwargs_UncertainFlorisModel={"fix_yaw_to_nominal_direction": True} ) assert not np.allclose(df_opt_uncertain.farm_power_opt, df_opt_uncertain_fixed.farm_power_opt) -def test_apply_static_rate_limits(): +def test_apply_static_rate_limits(floris_dict): eps = 1e-4 wd_resolution = 4 ws_resolution = 0.5 ti_resolution = 0.01 df_opt = generic_df_opt( + floris_dict, wd_resolution=wd_resolution, ws_resolution=ws_resolution, ti_resolution=ti_resolution @@ -212,12 +218,12 @@ def test_apply_static_rate_limits(): assert not (np.abs(np.diff(offsets_unlimited, axis=1)) <= ws_rate_limit*ws_resolution).all() assert not (np.abs(np.diff(offsets_unlimited, axis=2)) <= ti_rate_limit*ti_resolution).all() -def test_apply_wind_speed_ramps(): +def test_apply_wind_speed_ramps(floris_dict): ws_specified = 8.0 ws_wake_steering_cut_out = 13.0 ws_wake_steering_fully_engaged_high = 10.0 - df_opt_single_ws = generic_df_opt(ws_min=ws_specified, ws_max=ws_specified) + df_opt_single_ws = generic_df_opt(floris_dict, ws_min=ws_specified, ws_max=ws_specified) df_opt_ramps = apply_wind_speed_ramps( df_opt_single_ws, @@ -252,9 +258,9 @@ def test_apply_wind_speed_ramps(): )/2 ) -def test_wake_steering_interpolant(): +def test_wake_steering_interpolant(floris_dict): - df_opt = generic_df_opt() + df_opt = generic_df_opt(floris_dict) yaw_interpolant = get_yaw_angles_interpolant(df_opt) @@ -284,8 +290,8 @@ def test_wake_steering_interpolant(): with pytest.raises(ValueError): _ = yaw_interpolant(200.0, 8.0, 0.06) # min specified wd is 220 - # Check wrapping works - df_0_270 = generic_df_opt(wd_min=0.0, wd_max=270.0, wd_resolution=10.0) # Includes 0 degree WD + # Check wrapping works (includes 0 degree wind direction) + df_0_270 = generic_df_opt(floris_dict, wd_min=0.0, wd_max=270.0, wd_resolution=10.0) yaw_interpolant = get_yaw_angles_interpolant(df_0_270) _ = yaw_interpolant(0.0, 8.0, 0.06) _ = yaw_interpolant(355.0, 8.0, 0.06) @@ -295,9 +301,9 @@ def test_wake_steering_interpolant(): with pytest.raises(ValueError): _ = yaw_interpolant(361.0, 8.0, 0.06) -def test_hysteresis_zones(): +def test_hysteresis_zones(floris_dict): - df_opt = generic_df_opt() + df_opt = generic_df_opt(floris_dict) min_zone_width = 4.0 hysteresis_dict_base = {"T000": [(270-min_zone_width/2, 270+min_zone_width/2)]} @@ -307,12 +313,12 @@ def test_hysteresis_zones(): assert hysteresis_dict_test == hysteresis_dict_base # Check angle wrapping works (runs through) - df_opt = generic_df_opt(wd_min=0.0, wd_max=360.0) + df_opt = generic_df_opt(floris_dict, wd_min=0.0, wd_max=360.0) hysteresis_dict_test = compute_hysteresis_zones(df_opt, min_zone_width=min_zone_width) assert hysteresis_dict_test["T000"] == hysteresis_dict_base["T000"] # Limited wind directions that span 360/0 \ - df_opt_2 = generic_df_opt() + df_opt_2 = generic_df_opt(floris_dict) df_opt_2.wind_direction = (df_opt_2.wind_direction + 90.0) % 360.0 df_opt_2 = df_opt_2.sort_values(by=["wind_direction", "wind_speed", "turbulence_intensity"]) hysteresis_dict_test = compute_hysteresis_zones(df_opt_2, min_zone_width=min_zone_width) @@ -320,21 +326,21 @@ def test_hysteresis_zones(): == np.array(hysteresis_dict_base["T000"][0])).all() # Check 0 low end, less than 360 upper end - df_opt = generic_df_opt(wd_min=0.0, wd_max=300.0) + df_opt = generic_df_opt(floris_dict, wd_min=0.0, wd_max=300.0) hysteresis_dict_test = compute_hysteresis_zones(df_opt, min_zone_width=min_zone_width) assert hysteresis_dict_test["T000"] == hysteresis_dict_base["T000"] # Check nonzero low end, 360 upper end - df_opt = generic_df_opt(wd_min=200.0, wd_max=360.0) + df_opt = generic_df_opt(floris_dict, wd_min=200.0, wd_max=360.0) hysteresis_dict_test = compute_hysteresis_zones(df_opt, min_zone_width=min_zone_width) assert hysteresis_dict_test["T000"] == hysteresis_dict_base["T000"] # Close to zero low end, 360 upper end - df_opt = generic_df_opt(wd_min=2.0, wd_max=360.0) + df_opt = generic_df_opt(floris_dict, wd_min=2.0, wd_max=360.0) _ = compute_hysteresis_zones(df_opt) # Check grouping of regions by reducing yaw rate threshold - df_opt = generic_df_opt() + df_opt = generic_df_opt(floris_dict) hysteresis_dict_test = compute_hysteresis_zones( df_opt, min_zone_width=3*min_zone_width, # Force regions to be grouped @@ -443,10 +449,10 @@ def test_create_uniform_wind_rose(): frequencies = wind_rose.unpack_freq() assert (frequencies == frequencies[0]).all() -def test_check_df_opt_ordering(): +def test_check_df_opt_ordering(floris_dict): # Pass tests - df_opt = generic_df_opt() + df_opt = generic_df_opt(floris_dict) check_df_opt_ordering(df_opt) # Remove a row so that not all data is present From c3e02d943e420d61032b0b6b5631ebea2988a1c9 Mon Sep 17 00:00:00 2001 From: misi9170 Date: Thu, 4 Dec 2025 19:57:48 -0700 Subject: [PATCH 7/8] Fix line length issues --- tests/controller_library_test.py | 61 +++++++++++++++++----------- tests/hercules_interface_test.py | 1 + tests/hercules_v1_interfaces_test.py | 12 ++++-- tests/test_data.py | 53 ++++++++++++++++++++++++ 4 files changed, 100 insertions(+), 27 deletions(-) create mode 100755 tests/test_data.py diff --git a/tests/controller_library_test.py b/tests/controller_library_test.py index 92dd36da..a9ea5132 100644 --- a/tests/controller_library_test.py +++ b/tests/controller_library_test.py @@ -40,7 +40,10 @@ def test_controller_instantiation(test_interface_standin, test_hercules_v1_dict) input_dict=test_hercules_v1_dict, wind_controller=1, # Override error raised for empty controllers ) - _ = SolarPassthroughController(interface=test_interface_standin, input_dict=test_hercules_v1_dict) + _ = SolarPassthroughController( + interface=test_interface_standin, + input_dict=test_hercules_v1_dict + ) _ = BatteryPassthroughController( interface=test_interface_standin, input_dict=test_hercules_v1_dict ) @@ -58,9 +61,9 @@ def test_LookupBasedWakeSteeringController(test_hercules_v1_dict, test_interface # Check that the controller can be stepped test_hercules_v1_dict["time"] = 20 - test_hercules_v1_dict_out = test_controller.step(input_dict=test_hercules_v1_dict) + test_dict_out = test_controller.step(input_dict=test_hercules_v1_dict) test_angles = np.array( - test_hercules_v1_dict_out["hercules_comms"]["amr_wind"]["test_farm"]["turbine_yaw_angles"] + test_dict_out["hercules_comms"]["amr_wind"]["test_farm"]["turbine_yaw_angles"] ) wind_directions = np.array( test_hercules_v1_dict["hercules_comms"]["amr_wind"]["test_farm"]["turbine_wind_directions"] @@ -83,9 +86,9 @@ def test_LookupBasedWakeSteeringController(test_hercules_v1_dict, test_interface ) test_hercules_v1_dict["time"] = 20 - test_hercules_v1_dict_out = test_controller.step(input_dict=test_hercules_v1_dict) + test_dict_out = test_controller.step(input_dict=test_hercules_v1_dict) test_angles = np.array( - test_hercules_v1_dict_out["hercules_comms"]["amr_wind"]["test_farm"]["turbine_yaw_angles"] + test_dict_out["hercules_comms"]["amr_wind"]["test_farm"]["turbine_yaw_angles"] ) wind_directions = np.array( test_hercules_v1_dict["hercules_comms"]["amr_wind"]["test_farm"]["turbine_wind_directions"] @@ -101,9 +104,9 @@ def test_WindFarmPowerDistributingController(test_hercules_v1_dict, test_interfa # Default behavior when no power reference is given test_hercules_v1_dict["time"] = 20 test_hercules_v1_dict["external_signals"] = {} - test_hercules_v1_dict_out = test_controller.step(input_dict=test_hercules_v1_dict) + test_dict_out = test_controller.step(input_dict=test_hercules_v1_dict) test_power_setpoints = np.array( - test_hercules_v1_dict_out["hercules_comms"]["amr_wind"]["test_farm"]["turbine_power_setpoints"] + test_dict_out["hercules_comms"]["amr_wind"]["test_farm"]["turbine_power_setpoints"] ) assert np.allclose( test_power_setpoints, @@ -112,9 +115,9 @@ def test_WindFarmPowerDistributingController(test_hercules_v1_dict, test_interfa # Test with power reference test_hercules_v1_dict["external_signals"]["wind_power_reference"] = 1000 - test_hercules_v1_dict_out = test_controller.step(input_dict=test_hercules_v1_dict) + test_dict_out = test_controller.step(input_dict=test_hercules_v1_dict) test_power_setpoints = np.array( - test_hercules_v1_dict_out["hercules_comms"]["amr_wind"]["test_farm"]["turbine_power_setpoints"] + test_dict_out["hercules_comms"]["amr_wind"]["test_farm"]["turbine_power_setpoints"] ) assert np.allclose(test_power_setpoints, 500) @@ -127,17 +130,17 @@ def test_WindFarmPowerTrackingController(test_hercules_v1_dict, test_interface_h # Test no change to power setpoints if producing desired power test_hercules_v1_dict["external_signals"]["wind_power_reference"] = 1000 test_hercules_v1_dict["hercules_comms"]["amr_wind"]["test_farm"]["turbine_powers"] = [500, 500] - test_hercules_v1_dict_out = test_controller.step(input_dict=test_hercules_v1_dict) + test_dict_out = test_controller.step(input_dict=test_hercules_v1_dict) test_power_setpoints = np.array( - test_hercules_v1_dict_out["hercules_comms"]["amr_wind"]["test_farm"]["turbine_power_setpoints"] + test_dict_out["hercules_comms"]["amr_wind"]["test_farm"]["turbine_power_setpoints"] ) assert np.allclose(test_power_setpoints, 500) # Test if power exceeds farm reference, power setpoints are reduced test_hercules_v1_dict["hercules_comms"]["amr_wind"]["test_farm"]["turbine_powers"] = [600, 600] - test_hercules_v1_dict_out = test_controller.step(input_dict=test_hercules_v1_dict) + test_dict_out = test_controller.step(input_dict=test_hercules_v1_dict) test_power_setpoints = np.array( - test_hercules_v1_dict_out["hercules_comms"]["amr_wind"]["test_farm"]["turbine_power_setpoints"] + test_dict_out["hercules_comms"]["amr_wind"]["test_farm"]["turbine_power_setpoints"] ) assert ( test_power_setpoints @@ -146,9 +149,9 @@ def test_WindFarmPowerTrackingController(test_hercules_v1_dict, test_interface_h # Test if power is less than farm reference, power setpoints are increased test_hercules_v1_dict["hercules_comms"]["amr_wind"]["test_farm"]["turbine_powers"] = [550, 400] - test_hercules_v1_dict_out = test_controller.step(input_dict=test_hercules_v1_dict) + test_dict_out = test_controller.step(input_dict=test_hercules_v1_dict) test_power_setpoints = np.array( - test_hercules_v1_dict_out["hercules_comms"]["amr_wind"]["test_farm"]["turbine_power_setpoints"] + test_dict_out["hercules_comms"]["amr_wind"]["test_farm"]["turbine_power_setpoints"] ) assert ( test_power_setpoints @@ -162,13 +165,15 @@ def test_WindFarmPowerTrackingController(test_hercules_v1_dict, test_interface_h proportional_gain=2 ) test_hercules_v1_dict["hercules_comms"]["amr_wind"]["test_farm"]["turbine_powers"] = [600, 600] - test_hercules_v1_dict_out = test_controller.step(input_dict=test_hercules_v1_dict) + test_dict_out = test_controller.step(input_dict=test_hercules_v1_dict) test_power_setpoints_a = np.array( - test_hercules_v1_dict_out["hercules_comms"]["amr_wind"]["test_farm"]["turbine_power_setpoints"] + test_dict_out["hercules_comms"]["amr_wind"]["test_farm"]["turbine_power_setpoints"] ) assert (test_power_setpoints_a < test_power_setpoints).all() -def test_HybridSupervisoryControllerBaseline(test_hercules_v1_dict, test_interface_hercules_hybrid_ad): +def test_HybridSupervisoryControllerBaseline( + test_hercules_v1_dict, test_interface_hercules_hybrid_ad + ): # Establish lower controllers wind_controller = WindFarmPowerTrackingController( @@ -195,7 +200,9 @@ def test_HybridSupervisoryControllerBaseline(test_hercules_v1_dict, test_interfa # Simply test the supervisory_control method, for the time being test_hercules_v1_dict["external_signals"]["plant_power_reference"] = power_ref - test_hercules_v1_dict["hercules_comms"]["amr_wind"]["test_farm"]["turbine_powers"] = wind_current + test_hercules_v1_dict["hercules_comms"]["amr_wind"]["test_farm"]["turbine_powers"] = ( + wind_current + ) test_hercules_v1_dict["py_sims"]["test_solar"]["outputs"]["power_mw"] = solar_current / 1e3 test_controller.prev_solar_power = solar_current # To override filtering test_controller.prev_wind_power = sum(wind_current) # To override filtering @@ -251,7 +258,9 @@ def test_HybridSupervisoryControllerBaseline_subsets( # Simply test the supervisory_control method, for the time being test_hercules_v1_dict["external_signals"]["plant_power_reference"] = power_ref - test_hercules_v1_dict["hercules_comms"]["amr_wind"]["test_farm"]["turbine_powers"] = wind_current + test_hercules_v1_dict["hercules_comms"]["amr_wind"]["test_farm"]["turbine_powers"] = ( + wind_current + ) test_hercules_v1_dict["py_sims"]["test_solar"]["outputs"]["power_mw"] = solar_current / 1e3 test_controller.prev_solar_power = solar_current # To override filtering test_controller.prev_wind_power = sum(wind_current) # To override filtering @@ -583,7 +592,9 @@ def test_HydrogenPlantController(test_hercules_v1_dict, test_interface_hercules_ # Simply test the supervisory_control method, for the time being test_hercules_v1_dict["external_signals"]["hydrogen_reference"] = hyrogen_ref - test_hercules_v1_dict["hercules_comms"]["amr_wind"]["test_farm"]["turbine_powers"] = wind_current + test_hercules_v1_dict["hercules_comms"]["amr_wind"]["test_farm"]["turbine_powers"] = ( + wind_current + ) test_hercules_v1_dict["py_sims"]["test_battery"]["outputs"]["power"] = 0.0 test_hercules_v1_dict["py_sims"]["test_solar"]["outputs"]["power_mw"] = 0.0 test_controller.filtered_power_prev = sum(wind_current) # To override filtering @@ -629,7 +640,9 @@ def test_HydrogenPlantController(test_hercules_v1_dict, test_interface_hercules_ solar_current = 1000 battery_current = 500 total_current_power = sum(wind_current) + solar_current - battery_current - test_hercules_v1_dict["hercules_comms"]["amr_wind"]["test_farm"]["turbine_powers"] = wind_current + test_hercules_v1_dict["hercules_comms"]["amr_wind"]["test_farm"]["turbine_powers"] = ( + wind_current + ) test_hercules_v1_dict["py_sims"]["test_battery"]["outputs"]["power"] = battery_current test_hercules_v1_dict["py_sims"]["test_solar"]["outputs"]["power_mw"] = solar_current / 1e3 test_controller.filtered_power_prev = total_current_power # To override filtering @@ -691,9 +704,9 @@ def test_YawSetpointPassthroughController(test_hercules_v1_dict, test_interface_ # Check that the controller can be stepped test_hercules_v1_dict["time"] = 20 - test_hercules_v1_dict_out = test_controller.step(input_dict=test_hercules_v1_dict) + test_dict_out = test_controller.step(input_dict=test_hercules_v1_dict) assert np.allclose( - test_hercules_v1_dict_out["hercules_comms"]["amr_wind"]["test_farm"]["turbine_yaw_angles"], + test_dict_out["hercules_comms"]["amr_wind"]["test_farm"]["turbine_yaw_angles"], test_hercules_v1_dict["hercules_comms"]["amr_wind"]["test_farm"]["turbine_wind_directions"] ) diff --git a/tests/hercules_interface_test.py b/tests/hercules_interface_test.py index 1f898108..c2889247 100644 --- a/tests/hercules_interface_test.py +++ b/tests/hercules_interface_test.py @@ -1,6 +1,7 @@ import pytest from hycon.interfaces import HerculesInterface + def test_interface_instantiation(test_hercules_dict): """ Tests whether all interfaces can be imported correctly and that they diff --git a/tests/hercules_v1_interfaces_test.py b/tests/hercules_v1_interfaces_test.py index 95f20bf7..17529013 100644 --- a/tests/hercules_v1_interfaces_test.py +++ b/tests/hercules_v1_interfaces_test.py @@ -26,7 +26,9 @@ def test_HerculesADInterface(test_hercules_v1_dict): assert measurements["time"] == test_hercules_v1_dict["time"] assert ( measurements["wind_farm"]["wind_directions"] - == test_hercules_v1_dict["hercules_comms"]["amr_wind"]["test_farm"]["turbine_wind_directions"] + == test_hercules_v1_dict["hercules_comms"]["amr_wind"]["test_farm"][ + "turbine_wind_directions" + ] ) assert ( measurements["wind_farm"]["turbine_powers"] @@ -80,9 +82,13 @@ def test_HerculesADInterface(test_hercules_v1_dict): # wind_power_reference takes precedence test_hercules_v1_dict["external_signals"]["wind_power_reference"] = 500.0 test_hercules_v1_dict["external_signals"]["plant_power_reference"] = 400.0 - assert interface.get_measurements(test_hercules_v1_dict)["wind_farm"]["power_reference"] == 500.0 + assert ( + interface.get_measurements(test_hercules_v1_dict)["wind_farm"]["power_reference"] == 500.0 + ) del test_hercules_v1_dict["external_signals"]["wind_power_reference"] - assert interface.get_measurements(test_hercules_v1_dict)["wind_farm"]["power_reference"] == 400.0 + assert ( + interface.get_measurements(test_hercules_v1_dict)["wind_farm"]["power_reference"] == 400.0 + ) # Reinstate original values for future tests test_hercules_v1_dict["external_signals"]["wind_power_reference"] = 1000.0 test_hercules_v1_dict["external_signals"]["plant_power_reference"] = 1000.0 diff --git a/tests/test_data.py b/tests/test_data.py new file mode 100755 index 00000000..48a91ae9 --- /dev/null +++ b/tests/test_data.py @@ -0,0 +1,53 @@ +FLORIS_DICT = { + "logging": { + "console": {"enable": True, "level": "WARNING"}, + "file": {"enable": False, "level": "WARNING"}, + }, + "solver": {"type": "turbine_grid", "turbine_grid_points": 3}, + "wake": { + "model_strings": { + "combination_model": "sosfs", + "deflection_model": "gauss", + "turbulence_model": "crespo_hernandez", + "velocity_model": "gauss", + }, + "enable_secondary_steering": True, + "enable_yaw_added_recovery": True, + "enable_transverse_velocities": True, + "enable_active_wake_mixing": False, + "wake_deflection_parameters": { + "gauss": { + "ad": 0.0, + "alpha": 0.58, + "bd": 0.0, + "beta": 0.077, + "dm": 1.0, + "ka": 0.38, + "kb": 0.004, + }, + }, + "wake_turbulence_parameters": { + "crespo_hernandez": {"initial": 0.1, "constant": 0.5, "ai": 0.8, "downstream": -0.32} + }, + "wake_velocity_parameters": { + "gauss": {"alpha": 0.58, "beta": 0.077, "ka": 0.38, "kb": 0.004}, + }, + }, + "farm": { + "layout_x": [0.0], + "layout_y": [0.0], + "turbine_type": ["nrel_5MW"], + }, + "flow_field": { + "wind_speeds": [8.0], + "wind_directions": [270.0], + "turbulence_intensities": [0.06], + "wind_veer": 0.0, + "wind_shear": 0.12, + "air_density": 1.225, + "reference_wind_height": 90.0, + }, + "name": "GCH_for_FlorisStandin", + "description": "FLORIS Gauss Curl Hybrid model standing in for AMR-Wind", + "floris_version": "v4.x", +} \ No newline at end of file From f42b83d8a6d2b5ae5a017bf53aa5733c7fc9fe3f Mon Sep 17 00:00:00 2001 From: misi9170 Date: Thu, 4 Dec 2025 20:00:42 -0700 Subject: [PATCH 8/8] Remove accidentally committed file --- tests/test_data.py | 53 ---------------------------------------------- 1 file changed, 53 deletions(-) delete mode 100755 tests/test_data.py diff --git a/tests/test_data.py b/tests/test_data.py deleted file mode 100755 index 48a91ae9..00000000 --- a/tests/test_data.py +++ /dev/null @@ -1,53 +0,0 @@ -FLORIS_DICT = { - "logging": { - "console": {"enable": True, "level": "WARNING"}, - "file": {"enable": False, "level": "WARNING"}, - }, - "solver": {"type": "turbine_grid", "turbine_grid_points": 3}, - "wake": { - "model_strings": { - "combination_model": "sosfs", - "deflection_model": "gauss", - "turbulence_model": "crespo_hernandez", - "velocity_model": "gauss", - }, - "enable_secondary_steering": True, - "enable_yaw_added_recovery": True, - "enable_transverse_velocities": True, - "enable_active_wake_mixing": False, - "wake_deflection_parameters": { - "gauss": { - "ad": 0.0, - "alpha": 0.58, - "bd": 0.0, - "beta": 0.077, - "dm": 1.0, - "ka": 0.38, - "kb": 0.004, - }, - }, - "wake_turbulence_parameters": { - "crespo_hernandez": {"initial": 0.1, "constant": 0.5, "ai": 0.8, "downstream": -0.32} - }, - "wake_velocity_parameters": { - "gauss": {"alpha": 0.58, "beta": 0.077, "ka": 0.38, "kb": 0.004}, - }, - }, - "farm": { - "layout_x": [0.0], - "layout_y": [0.0], - "turbine_type": ["nrel_5MW"], - }, - "flow_field": { - "wind_speeds": [8.0], - "wind_directions": [270.0], - "turbulence_intensities": [0.06], - "wind_veer": 0.0, - "wind_shear": 0.12, - "air_density": 1.225, - "reference_wind_height": 90.0, - }, - "name": "GCH_for_FlorisStandin", - "description": "FLORIS Gauss Curl Hybrid model standing in for AMR-Wind", - "floris_version": "v4.x", -} \ No newline at end of file