Skip to content

Commit

Permalink
Establish tests and clarify naming (NREL#24)
Browse files Browse the repository at this point in the history
* Testing interface_base.

* Remove repeated class definition.

* renaming send_setpoints and compute_setpoints to send_controls and compute_controls, respectively, for generality.

* Move controller_base into controllers/ directory.

* Beginning to build out interface library tests.

* Updating to check_controls.

* setpoints -> controls for dicts.

* Test imports, methods implemented.

* Tests for HerculesADYawInterface.

* Explicit dictionary input.

* Simplified tests for HerculesWindBatteryInterface

* Ruff.

* Inheritance tests.

* Beginnings of controller library test.

* Improved developer installation instructions.

* Add note for future test for HerculesWindBatteryController.

* Ruff.

* Add zmq to requirements.

* Reverting some changes from setpoints to controls in the wind battery controlller/interface for current compatibility with Hercules.
  • Loading branch information
misi9170 authored Dec 21, 2023
1 parent b481b1a commit 3062ac5
Show file tree
Hide file tree
Showing 25 changed files with 459 additions and 138 deletions.
8 changes: 4 additions & 4 deletions docs/controllers.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
The `whoc.controllers` module contains a library of wind and hybrid power plant
controllers. Each controller must inherit from `ControllerBase` (see
controller_base.py) and implement a
mandatory `compute_setpoints()` method, which contains the relevant control
algorithm and writes final control signals to the `setpoints_dict` attribute
as key-value pairs. `compute_setpoints()` is, in turn, called in the `step()`
mandatory `compute_controls()` method, which contains the relevant control
algorithm and writes final control signals to the `controls_dict` attribute
as key-value pairs. `compute_controls()` is, in turn, called in the `step()`
method of `ControllerBase`.

## Available controllers
Expand All @@ -15,4 +15,4 @@ For yaw controller of actuator disk-type turbines (as a stand-in, will be
updated).

### WakeSteeringROSCOStandin
May be combined into a universal simple wake steering controller.
May be combined into a universal simple wake steeringcontroller.
Binary file modified docs/graphics/main_attribution_inheritance.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/graphics/second-level_attribution_inheritance.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion docs/install_instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ already activated your conda environment) according to:

```
git clone https://github.com/your-github-id/wind-hybrid-open-controller
pip install -e wind-hybrid-open-controller/
pip install -e "wind-hybrid-open-controller/[develop]"
```
To contribute back to the base repository
https://github.com/NREL/wind-hybrid-open-controller, please do the following:
Expand Down
8 changes: 4 additions & 4 deletions docs/interfaces.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,13 @@ which can be found n interface_base.py, and should implement three methods:
organize into a dictionary that the calling controller can utilize. Optionally,
receives a large dictionary (for example, the Hercules `main_dict`), from which
useable measurements can be extracted/repackaged for easy use in the controller.
- `check_setpoints()`: Check that the keys in `setpoints_dict` are viable for
- `check_controls()`: Check that the keys in `controls_dict` are viable for
the receiving plant.
- `send_setpoints()`: Send setpoints to the simulation assets. Setpoints are
created as specific keyword arguements, which match those setpoints generated
- `send_controls()`: Send controls to the simulation assets. Controls are
created as specific keyword arguements, which match those controls generated
by the calling controller. Optionally, receives a large dictionary
(for example, the Hercules `main_dict`), which can be written to and returned
with setpoints as needed.
with controls as needed.

These methods will all be called in the `step()` method of `ControllerBase`.

Expand Down
4 changes: 2 additions & 2 deletions docs/intro.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ the controller objects that it contains.
controllers should inherit from. Inheritance is shown using arrows in the
diagram above. The key method of `ControllerBase` is the `step()` method,
which progresses the controller by receiving plant measurements; computing
control setpoints (which must be implemented in the children of
`ControllerBase`); and sending the setpoints back to the plant. Children of
controls (which must be implemented in the children of
`ControllerBase`); and sending the controls back to the plant. Children of
`ControllerBase` should inherit `step()` rather than overloading it.
Additionally, on instantiation, `ControllerBase` expects to receive an
instantiated `interface` object (discussed next). For information can be
Expand Down
2 changes: 1 addition & 1 deletion run_whoc.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ def run_zmq():
yaw_setpoint = 20.0

# Send new setpoints back to ROSCO
s.send_setpoints(nacelleHeading=yaw_setpoint)
s.send_controls(nacelleHeading=yaw_setpoint)

if measurements["iStatus"] == -1:
connect_zmq = False
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
# "dash-daq==0.5.0",
# "scikit-image",
# ZMQ stuff
# "zmq",
"zmq",
# NETCDF
# "netCDF4",
# YAML
Expand Down
70 changes: 64 additions & 6 deletions tests/controller_base_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,70 @@

# See https://nrel.github.io/wind-hybrid-open-controller for documentation

import unittest
import pytest
from whoc.controllers.controller_base import ControllerBase
from whoc.interfaces.interface_base import InterfaceBase

import numpy as np

class StandinInterface(InterfaceBase):
"""
Empty class to test controllers.
"""

class TestControllerBase(unittest.TestCase):
def test_1(self):
x = np.array([1])
assert x == x
def __init__(self):
super().__init__()

def get_measurements(self):
pass

def check_controls(self):
pass

def send_controls(self):
pass


class InheritanceTestClassBad(ControllerBase):
"""
Class that is missing necessary methods.
"""

def __init__(self, interface):
super().__init__(interface)


class InheritanceTestClassGood(ControllerBase):
"""
Class that is missing necessary methods.
"""

def __init__(self, interface):
super().__init__(interface)

def compute_controls(self):
pass


def test_ControllerBase_methods():
"""
Check that the base interface class establishes the correct methods.
"""
test_interface = StandinInterface()

controller_base = InheritanceTestClassGood(test_interface)
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():
"""
Check that a subclass of InterfaceBase inherits methods correctly.
"""
test_interface = StandinInterface()

with pytest.raises(TypeError):
_ = InheritanceTestClassBad(test_interface)

_ = InheritanceTestClassGood(test_interface)
88 changes: 88 additions & 0 deletions tests/controller_library_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# Copyright 2021 NREL

# Licensed under the Apache License, Version 2.0 (the "License"); you may not
# use this file except in compliance with the License. You may obtain a copy of
# the License at http://www.apache.org/licenses/LICENSE-2.0

# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations under
# the License.

# See https://nrel.github.io/wind-hybrid-open-controller for documentation

from whoc.controllers import (
HerculesWindBatteryController,
WakeSteeringADStandin,
)
from whoc.interfaces import HerculesADYawInterface
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]}},
"hercules_comms": {
"amr_wind": {
"test_farm": {
"turbine_wind_directions": [271.0, 272.5],
"turbine_powers": [4000.0, 4001.0],
}
}
},
"py_sims": {"test_battery": {"outputs": 10.0}},
}


def test_controller_instantiation():
"""
Tests whether all controllers can be imported correctly and that they
each implement the required methods specified by ControllerBase.
"""
test_interface = StandinInterface()

_ = WakeSteeringADStandin(interface=test_interface, input_dict=test_hercules_dict)
_ = HerculesWindBatteryController(interface=test_interface, input_dict=test_hercules_dict)


def test_WakeSteeringADStandin():
test_interface = HerculesADYawInterface(test_hercules_dict)
test_controller = WakeSteeringADStandin(interface=test_interface, input_dict=test_hercules_dict)

# Check that the controller can be stepped
test_hercules_dict_out = test_controller.step(hercules_dict=test_hercules_dict)
assert test_hercules_dict_out["hercules_comms"]["amr_wind"]["test_farm"][
"turbine_yaw_angles"
] == [270.0, 270.0]

test_hercules_dict["time"] = 20
test_hercules_dict_out = test_controller.step(hercules_dict=test_hercules_dict)
assert (
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"]
)


def test_HerculesWindBatteryController():
# TODO: write this test, possibly clean up HerculesWindBatteryController class
pass
23 changes: 0 additions & 23 deletions tests/controllers_test.py

This file was deleted.

77 changes: 77 additions & 0 deletions tests/interface_base_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# Copyright 2021 NREL

# Licensed under the Apache License, Version 2.0 (the "License"); you may not
# use this file except in compliance with the License. You may obtain a copy of
# the License at http://www.apache.org/licenses/LICENSE-2.0

# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations under
# the License.

# See https://nrel.github.io/wind-hybrid-open-controller for documentation

# import inspect
import pytest
from whoc.interfaces.interface_base import InterfaceBase

# import whoc.interfaces


class InheritanceTestClassBad(InterfaceBase):
"""
Class that is missing necessary methods.
"""

def __init__(self):
super().__init__()


class InheritanceTestClassGood(InterfaceBase):
"""
Class that is missing necessary methods.
"""

def __init__(self):
super().__init__()

def get_measurements(self):
pass

def check_controls(self):
pass

def send_controls(self):
pass


def test_InterfaceBase_methods():
"""
Check that the base interface class establishes the correct methods.
"""
interface_base = InheritanceTestClassGood()
assert hasattr(interface_base, "get_measurements")
assert hasattr(interface_base, "check_controls")
assert hasattr(interface_base, "send_controls")


def test_inherited_methods():
"""
Check that a subclass of InterfaceBase inherits methods correctly.
"""

with pytest.raises(TypeError):
_ = InheritanceTestClassBad()

_ = InheritanceTestClassGood()


def test_all_interfaces_implement_methods():
# In future, I'd like to dynamically instantiate classes, but the different
# inputs that they require on __init__ is currently a roadblock, so I'll just
# explicitly instantiate each interface class for the time being.

# class_dict = dict(inspect.getmembers(whoc.interfaces, inspect.isclass))

pass
Loading

0 comments on commit 3062ac5

Please sign in to comment.