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

F/zmq #145

Merged
merged 64 commits into from
Jun 28, 2022
Merged

F/zmq #145

Show file tree
Hide file tree
Changes from 38 commits
Commits
Show all changes
64 commits
Select commit Hold shift + click to select a range
c86d2cc
cleanup api change table
nikhar-abbas Mar 22, 2022
356b225
Merge remote-tracking branch 'upstream/develop' into develop
nikhar-abbas Apr 14, 2022
e1014b7
Merge remote-tracking branch 'upstream/develop' into develop
nikhar-abbas Apr 28, 2022
c506615
Merge remote-tracking branch 'upstream/develop' into develop
nikhar-abbas Jun 9, 2022
1fd87b7
zeromq client and f90 files - initia lcommit
nikhar-abbas Jun 9, 2022
7eb9249
add zmq install reqs
nikhar-abbas Jun 9, 2022
2ca2859
remove yaw-by-ipc
nikhar-abbas Jun 9, 2022
983286c
typo fix
nikhar-abbas Jun 9, 2022
faab3d3
add zeroMQ interface to registry and source - enable zeroMQ for yaw c…
nikhar-abbas Jun 9, 2022
03d87b0
cleanup to compile, move ZMQ_Mode to flags, write DISCONS
nikhar-abbas Jun 9, 2022
f98735b
Merge remote-tracking branch 'upstream/develop' into f/zmq
nikhar-abbas Jun 9, 2022
281c1b4
only set ZMQ_Client if flag is enabled
nikhar-abbas Jun 9, 2022
d6e7309
fix bug in discons
nikhar-abbas Jun 9, 2022
4e3a985
minor updates and add sim with wind direction
nikhar-abbas Jun 9, 2022
65c35da
change to dict inputs for control interface
nikhar-abbas Jun 9, 2022
cd5a0f6
add zmq example
nikhar-abbas Jun 9, 2022
1709d30
Revert "remove yaw-by-ipc"
nikhar-abbas Jun 10, 2022
87a5967
Enable Yaw-by-ipc again
nikhar-abbas Jun 10, 2022
2c85969
update and execute zeromq example
nikhar-abbas Jun 10, 2022
019eed4
add zmq to dependencies
nikhar-abbas Jun 10, 2022
0ea8ec2
sudo for apt-get
nikhar-abbas Jun 10, 2022
0986e6d
libzmq3-dev typo
nikhar-abbas Jun 10, 2022
683f744
setup cmake
nikhar-abbas Jun 10, 2022
576cd72
rename example 16 to 17
nikhar-abbas Jun 13, 2022
b7974cd
Merge remote-tracking branch 'upstream/develop' into f/zmq
nikhar-abbas Jun 13, 2022
9e3aef5
read empty line
nikhar-abbas Jun 13, 2022
6caf12f
update types, regen discons
nikhar-abbas Jun 13, 2022
07f902a
fix build path
nikhar-abbas Jun 13, 2022
8e9f1b4
remove non-ubuntu examples, type in cmake command
nikhar-abbas Jun 13, 2022
39cd7e6
Set ROSCO=True
nikhar-abbas Jun 13, 2022
65ca641
typo fix and update DISCONs
nikhar-abbas Jun 13, 2022
985e736
document API changes
nikhar-abbas Jun 13, 2022
be1e96c
newlines and in-text code
nikhar-abbas Jun 13, 2022
1325cf0
updated removed inputs, fix intext code syntax
nikhar-abbas Jun 13, 2022
481baa5
run example 17
nikhar-abbas Jun 13, 2022
7812cb2
Add description to example 17
nikhar-abbas Jun 13, 2022
84287c8
zeromq -> pyzmq, cleanup
nikhar-abbas Jun 13, 2022
3be9635
better table for new/modified/removed
nikhar-abbas Jun 14, 2022
ea6056c
fix real(8)
nikhar-abbas Jun 21, 2022
e9b9c3c
cleanup zmqVar conventions and uses. Call zmq using a time period
nikhar-abbas Jun 21, 2022
b773468
Update DISCONS
nikhar-abbas Jun 21, 2022
81d611f
run all examples
nikhar-abbas Jun 21, 2022
e25f2ae
Fix Y_uSwitch description
nikhar-abbas Jun 21, 2022
e092f57
update comm address example
nikhar-abbas Jun 21, 2022
c80be0e
execute with runpy instead of importlib
nikhar-abbas Jun 22, 2022
de5d309
move zmq interface classes to control_interface
nikhar-abbas Jun 22, 2022
00f19c1
Properly shutdown ZMQ interface
nikhar-abbas Jun 22, 2022
47b5b0d
add IEA15MW_ExtInterface.yaml
nikhar-abbas Jun 22, 2022
9063107
Incorporate runFAST stuff from WEIS, clean up
dzalkind Jun 24, 2022
1a0e98e
Remove specific rosco_dll
dzalkind Jun 24, 2022
28c7a32
Publish artifacts from examples
nikhar-abbas Jun 27, 2022
fd3d34b
Clean up examples following runFAST business
dzalkind Jun 27, 2022
0ad4f73
Merge remote-tracking branch 'nja/f/zmq' into f/zmq
dzalkind Jun 27, 2022
80b5460
move archive artifacts
nikhar-abbas Jun 27, 2022
55eaba4
Fix build dir in example 17
dzalkind Jun 27, 2022
94bcb6b
Switch install/develop in pytools CI
dzalkind Jun 27, 2022
dcac9af
archive even if exampless fail
nikhar-abbas Jun 27, 2022
006666e
Merge branch 'f/zmq' of https://github.com/nikhar-abbas/ROSCO into f/zmq
nikhar-abbas Jun 27, 2022
f4802df
add zmq build instructions
nikhar-abbas Jun 27, 2022
d527606
cleanup and rename sim
nikhar-abbas Jun 27, 2022
6156474
Remove BITS_IN_ADDR stuff
dzalkind Jun 27, 2022
0db4e6c
Pass case_inputs and rosco_dll to runFAST object
dzalkind Jun 6, 2022
bf8601c
Pass correct DLL_FileName for external control
dzalkind Jun 27, 2022
b165640
Tidy example 15 commenting
dzalkind Jun 27, 2022
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
29 changes: 19 additions & 10 deletions .github/workflows/CI_rosco-pytools.yml
Original file line number Diff line number Diff line change
Expand Up @@ -92,29 +92,39 @@ jobs:
python-version: 3.8
environment-file: environment.yml

# setup cmake
- name: Setup Workspace
run: cmake -E make_directory ${{runner.workspace}}/ROSCO/ROSCO/build

# Install dependencies of ROSCO toolbox
- name: Add dependencies ubuntu specific
if: false == contains( matrix.os, 'windows')
run: |
conda install -y wisdem
- name: Add dependencies windows specific
if: true == contains( matrix.os, 'windows')
run: |
conda install -y m2w64-toolchain libpython
conda install -y wisdem

- name: Add pyFAST dependency
if: false == contains( matrix.os, 'windows')
run: |
git clone http://github.com/OpenFAST/python-toolbox
cd python-toolbox
python -m pip install -e .

# Install ZeroMQ
- name: Install ZeroMQ
run: sudo apt-get install libzmq3-dev

# Install ROSCO toolbox
- name: Install ROSCO toolbox
run: |
python setup.py install --compile-rosco
python setup.py install

- name: Configure and Build ROSCO - unix
working-directory: ${{runner.workspace}}/ROSCO/ROSCO/build
run: |
cmake \
-DCMAKE_INSTALL_PREFIX:PATH=${{runner.workspace}}/ROSCO/ROSCO/install \
-DZMQ_CLIENT=ON \
-DCMAKE_Fortran_COMPILER:STRING=${{env.FORTRAN_COMPILER}} \
..
cmake --build . --target install

# Install OpenFAST
- name: Install OpenFAST
Expand Down Expand Up @@ -156,8 +166,7 @@ jobs:


# Install dependencies of ROSCO toolbox
- name: Add dependencies ubuntu specific
if: false == contains( matrix.os, 'windows')
- name: Add dependencies
run: |
conda install -y wisdem

Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ examples/cp_ct_cq_lut.p

# Files Generated in Examples
Examples/DISCON.IN
Examples/DISCON_zmq.IN
Examples/*.p

# Exclude testing results
Expand Down
312 changes: 312 additions & 0 deletions Examples/example_17.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,312 @@
'''
----------- Example_17 --------------
Run ROSCO using the ROSCO toolbox control interface and execute communication with ZeroMQ
-------------------------------------

A demonstrator for ZeroMQ communication. Instead of using ROSCO with with control interface,
one could call ROSCO from OpenFAST, and communicate with ZeroMQ through that.
'''


import platform
import os
import matplotlib.pyplot as plt
from ROSCO_toolbox.inputs.validation import load_rosco_yaml
from ROSCO_toolbox.utilities import write_DISCON
from ROSCO_toolbox import control_interface as ROSCO_ci
from ROSCO_toolbox import sim as ROSCO_sim
from ROSCO_toolbox import turbine as ROSCO_turbine
from ROSCO_toolbox import controller as ROSCO_controller
import numpy as np
import zmq
import multiprocessing as mp

class farm_zmq_server():
def __init__(self, network_addresses=["tcp://*:5555", "tcp://*:5556"],
identifiers=None, timeout=600.0, verbose=False):
"""Python implementation for communicating with multiple instances
of the ROSCO ZeroMQ interface. This is useful for SOWFA and FAST.Farm
simulations in which multiple turbines are running in real time.
Args:
network_addresses (str, optional): List with the network addresses
used to communicate with the desired instances of ROSCO.
identifiers (iteratible, optional): List of strings denoting the
turbine identification string, e.g., ["WTG-01", "WTG-02"].
If left unspecified, will simple name the turbines "0" to
nturbs - 1.
timeout (float, optional): Seconds to wait for a message from
the ZeroMQ server before timing out. Defaults to 600.0.
verbose (bool, optional): Print to console. Defaults to False.
"""
self.network_addresses = network_addresses
self.verbose = verbose
self.nturbs = len(self.network_addresses)

if identifiers is None:
identifiers = ["%d" % i for i in range(self.nturbs)]

# Initialize ZeroMQ servers
self.zmq_servers = [None for _ in range(self.nturbs)]
for ti, address in enumerate(self.network_addresses):
self.zmq_servers[ti] = turbine_zmq_server(
network_address=address,
identifier=identifiers[ti],
timeout=timeout,
verbose=verbose)

def get_measurements(self):
measurements = [None for _ in range(self.nturbs)]
for ti in range(self.nturbs):
measurements[ti] = self.zmq_servers[ti].get_measurements()
return measurements

def send_setpoints(self, genTorques=None, nacelleHeadings=None,
bladePitchAngles=None):

# Default choices if unspecified
if genTorques is None:
genTorques = [0.0] * self.nturbs
if nacelleHeadings is None:
nacelleHeadings = [0.0] * self.nturbs
if bladePitchAngles is None:
bladePitchAngles = [[0.0, 0.0, 0.0]] * self.nturbs

# Send setpoints
for ti in range(self.nturbs):
self.zmq_servers[ti].send_setpoints(
genTorque=genTorques[ti],
nacelleHeading=nacelleHeadings[ti],
bladePitch=bladePitchAngles[ti]
)

class turbine_zmq_server():
def __init__(self, network_address="tcp://*:5555", identifier="0",
timeout=600.0, verbose=False):
"""Python implementation of the ZeroMQ server side for the ROSCO
ZeroMQ wind farm control interface. This class makes it easy for
users to receive measurements from ROSCO and then send back control
setpoints (generator torque, nacelle heading and/or blade pitch
angles).
Args:
network_address (str, optional): The network address to
communicate over with the desired instance of ROSCO. Note that,
if running a wind farm simulation in SOWFA or FAST.Farm, there
are multiple instances of ROSCO and each of these instances
needs to communicate over a unique port. Also, for each of those
instances, you will need an instance of zmq_server. This variable
Defaults to "tcp://*:5555".
identifier (str, optional): Turbine identifier. Defaults to "0".
timeout (float, optional): Seconds to wait for a message from
the ZeroMQ server before timing out. Defaults to 600.0.
verbose (bool, optional): Print to console. Defaults to False.
"""
self.network_address = network_address
self.identifier = identifier
self.timeout = timeout
self.verbose = verbose
self._connect()

def _connect(self):
address = self.network_address

# Connect socket
context = zmq.Context()
self.socket = context.socket(zmq.REP)
self.socket.setsockopt(zmq.LINGER, 0)
self.socket.bind(address)

if self.verbose:
print("[%s] Successfully established connection with %s" % (self.identifier, address))

def _disconnect(self):
self.socket.close()
context = zmq.Context()
context.term()

def get_measurements(self):
if self.verbose:
print("[%s] Waiting to receive measurements from ROSCO..." % (self.identifier))

# Initialize a poller for timeouts
poller = zmq.Poller()
poller.register(self.socket, zmq.POLLIN)
timeout_ms = int(self.timeout * 1000)
if poller.poll(timeout_ms):
# Receive measurements over network protocol
message_in = self.socket.recv_string()
else:
raise IOError("[%s] Connection to '%s' timed out."
% (self.identifier, self.network_address))

# Convert to individual strings and then to floats
measurements = message_in
# measurements = bytes.decode(message_in)
measurements = measurements.replace('\x00', '').split(',')
measurements = [float(m) for m in measurements]

# Convert to a measurement dict
measurements = dict({
'iStatus': measurements[0],
'Time': measurements[1],
'VS_MechGenPwr': measurements[2],
'VS_GenPwr': measurements[3],
'GenSpeed': measurements[4],
'RotSpeed': measurements[5],
'GenTqMeas': measurements[6],
'NacelleHeading': measurements[7],
'NacelleVane': measurements[8],
'HorWindV': measurements[9],
'rootMOOP1': measurements[10],
'rootMOOP2': measurements[11],
'rootMOOP3': measurements[12],
'FA_Acc': measurements[13],
'NacIMU_FA_Acc': measurements[14],
'Azimuth': measurements[15],
})

if self.verbose:
print('[%s] Measurements received:' % self.identifier, measurements)

return measurements

def send_setpoints(self, genTorque=0.0, nacelleHeading=0.0,
bladePitch=[0.0, 0.0, 0.0]):
# Create a message with setpoints to send to ROSCO
message_out = b"%016.5f, %016.5f, %016.5f, %016.5f, %016.5f" % (
genTorque, nacelleHeading, bladePitch[0], bladePitch[1],
bladePitch[2])

# Send reply back to client
if self.verbose:
print("[%s] Sending setpoint string to ROSCO: %s." % (self.identifier, message_out))

# Send control setpoints over network protocol
self.socket.send(message_out)

if self.verbose:
print("[%s] Setpoints sent successfully." % self.identifier)



def run_zmq():
s = turbine_zmq_server(network_address="tcp://*:5555", timeout=10.0, verbose=True)
while True:
# Get latest measurements from ROSCO
measurements = s.get_measurements()

# Decide new control input based on measurements
current_time = measurements['Time']
if current_time <= 10.0:
yaw_setpoint = 0.0
else:
yaw_setpoint = 20.0

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

s.disconnect()


def sim_rosco():
# Load yaml file
this_dir = os.path.dirname(os.path.abspath(__file__))
tune_dir = os.path.join(this_dir, '../Tune_Cases')
parameter_filename = os.path.join(tune_dir, 'NREL5MW.yaml')
inps = load_rosco_yaml(parameter_filename)
path_params = inps['path_params']
turbine_params = inps['turbine_params']
controller_params = inps['controller_params']

# Enable ZeroMQ & yaw control
controller_params['Y_ControlMode'] = 1
controller_params['ZMQ_Mode'] = 1

# Specify controller dynamic library path and name
this_dir = os.path.dirname(os.path.abspath(__file__))
example_out_dir = os.path.join(this_dir, 'examples_out')
if not os.path.isdir(example_out_dir):
os.makedirs(example_out_dir)

if platform.system() == 'Windows':
lib_name = os.path.join(this_dir, '../ROSCO/build/libdiscon.dll')
elif platform.system() == 'Darwin':
lib_name = os.path.join(this_dir, '../ROSCO/build/libdiscon.dylib')
else:
lib_name = os.path.join(this_dir, '../ROSCO/build/libdiscon.so')

# # Load turbine model from saved pickle
turbine = ROSCO_turbine.Turbine
turbine = turbine.load(os.path.join(example_out_dir, '01_NREL5MW_saved.p'))

# Load turbine data from OpenFAST and rotor performance text file
cp_filename = os.path.join(
tune_dir, path_params['FAST_directory'], path_params['rotor_performance_filename'])
turbine.load_from_fast(
path_params['FAST_InputFile'],
os.path.join(tune_dir, path_params['FAST_directory']),
dev_branch=True,
rot_source='txt', txt_filename=cp_filename
)

# Tune controller
controller = ROSCO_controller.Controller(controller_params)
controller.tune_controller(turbine)

# Write parameter input file
param_filename = os.path.join(this_dir, 'DISCON_zmq.IN')
write_DISCON(
turbine, controller,
param_file=param_filename,
txt_filename=cp_filename
)


# Load controller library
controller_int = ROSCO_ci.ControllerInterface(
lib_name, param_filename=param_filename, sim_name='sim1')

# Load the simulator
sim = ROSCO_sim.Sim(turbine, controller_int)

# Define a wind speed history
dt = 0.025
tlen = 100 # length of time to simulate (s)
ws0 = 7 # initial wind speed (m/s)
t = np.arange(0, tlen, dt)
ws = np.ones_like(t) * ws0
# add steps at every 100s
for i in range(len(t)):
ws[i] = ws[i] + t[i]//100

# Define wind directions as zeros
wd = np.zeros_like(t)

# Run simulator and plot results
sim.sim_ws_wd_series(t, ws, wd, rotor_rpm_init=4, make_plots=True)

# # Load controller library again to see if we deallocated properly
# controller_int = ROSCO_ci.ControllerInterface(
# lib_name, param_filename=param_filename, sim_name='sim_2')

# # Run simulator again and plot results
# sim_2 = ROSCO_sim.Sim(turbine, controller_int)
# sim_2.sim_ws_series(t, ws, rotor_rpm_init=4)

# # Check if simulations are equal
# np.testing.assert_almost_equal(sim_1.gen_speed, sim_2.gen_speed)

if False:
plt.show()
else:
plt.savefig(os.path.join(example_out_dir, '16_NREL5MW_zmqYaw.png'))


if __name__ == "__main__":
# sim_rosco()

p1 = mp.Process(target=run_zmq)
p1.start()
p2 = mp.Process(target=sim_rosco)
p2.start()
p1.join()
p2.join()
Loading