Skip to content

Commit

Permalink
Modified ZeroMQ interface to include pitch offsets (NREL#261)
Browse files Browse the repository at this point in the history
* Modified ZeroMQ interface to include pitch setpoints

* Fixed zeromq time limit issue

* Fixed simulation restart bug when using zeromq

* Light formatting changes

* Remove specific ZeroMQ data structure, add to LocalVars, rename

* Full remove ZMQ_Variables

* Tidy NREL5MW tuning yaml and control_interface

* Raise exception in control_interface if DLL returns an error

* Fix name of debug file

* Combine ZMQ examples, check outputs

* Check ZMQ update period based on simulation update period

* Initialize ZMQ local vars

* Make tuning yaml, zmq example consistent with others

* Update DISCONs

* Let DISCON in control_interface be initialized even if there's an error

* Remove extra zmq example

---------

Co-authored-by: dzalkind <[email protected]>
  • Loading branch information
mvanv and dzalkind authored Nov 6, 2023
1 parent a610e55 commit c75738e
Show file tree
Hide file tree
Showing 20 changed files with 151 additions and 91 deletions.
59 changes: 51 additions & 8 deletions Examples/17_zeromq_interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,25 +18,44 @@
from ROSCO_toolbox import sim as ROSCO_sim
from ROSCO_toolbox import turbine as ROSCO_turbine
from ROSCO_toolbox import controller as ROSCO_controller
from ROSCO_toolbox.ofTools.fast_io import output_processing
import numpy as np
import multiprocessing as mp

TIME_CHECK = 30
DESIRED_YAW_OFFSET = 20
DESIRED_PITCH_OFFSET = np.deg2rad(2) * np.sin(0.1 * TIME_CHECK) + np.deg2rad(2)

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

# Decide new control input based on measurements

# Yaw set point
current_time = measurements['Time']
if current_time <= 10.0:
yaw_setpoint = 0.0
else:
yaw_setpoint = 20.0
yaw_setpoint = DESIRED_YAW_OFFSET

# Pitch offset
if current_time >= 10.0:
col_pitch_command = np.deg2rad(2) * np.sin(0.1 * current_time) + np.deg2rad(2) # Implement dynamic induction control
else:
col_pitch_command = 0.0

# Send new commands back to ROSCO
pitch_command = 3 * [0]
pitch_command[0] = col_pitch_command
pitch_command[1] = col_pitch_command
pitch_command[2] = col_pitch_command

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

if measurements['iStatus'] == -1:
connect_zmq = False
Expand All @@ -56,6 +75,7 @@ def sim_rosco():
# Enable ZeroMQ & yaw control
controller_params['Y_ControlMode'] = 1
controller_params['ZMQ_Mode'] = 1
controller_params['ZMQ_UpdatePeriod'] = 0.025

# Specify controller dynamic library path and name
this_dir = os.path.dirname(os.path.abspath(__file__))
Expand All @@ -76,19 +96,22 @@ def sim_rosco():

# 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'])
tune_dir, path_params['rotor_performance_filename'])
turbine.load_from_fast(
path_params['FAST_InputFile'],
os.path.join(tune_dir, path_params['FAST_directory']),
rot_source='txt', txt_filename=cp_filename
)

# Tune controller
controller_params['LoggingLevel'] = 2
controller = ROSCO_controller.Controller(controller_params)
controller.tune_controller(turbine)

# Write parameter input file
param_filename = os.path.join(this_dir, 'DISCON_zmq.IN')
sim_dir = os.path.join(example_out_dir,'17_ZeroMQ')
os.makedirs(sim_dir,exist_ok=True)
param_filename = os.path.join(sim_dir, 'DISCON_zmq.IN')
write_DISCON(
turbine, controller,
param_file=param_filename,
Expand All @@ -97,7 +120,11 @@ def sim_rosco():


# Load controller library
controller_int = ROSCO_ci.ControllerInterface(lib_name, param_filename=param_filename, sim_name='sim-zmq')
controller_int = ROSCO_ci.ControllerInterface(
lib_name,
param_filename=param_filename,
sim_name=os.path.join(sim_dir,'sim-zmq')
)

# Load the simulator
sim = ROSCO_sim.Sim(turbine, controller_int)
Expand All @@ -121,8 +148,24 @@ def sim_rosco():
if False:
plt.show()
else:
plt.savefig(os.path.join(example_out_dir, '16_NREL5MW_zmqYaw.png'))
plt.savefig(os.path.join(example_out_dir, '17_NREL5MW_ZMQ.png'))

# Check that info is passed to ROSCO
op = output_processing.output_processing()
local_vars = op.load_fast_out([os.path.join(sim_dir,'sim-zmq.RO.dbg2')], tmin=0)
fig, axs = plt.subplots(2,1)
axs[0].plot(local_vars[0]['Time'],local_vars[0]['ZMQ_YawOffset'])
axs[1].plot(local_vars[0]['Time'],local_vars[0]['ZMQ_PitOffset'])

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

# Spot check input at time = 30 sec.
ind_30 = local_vars[0]['Time'] == TIME_CHECK
np.testing.assert_almost_equal(local_vars[0]['ZMQ_YawOffset'][ind_30], DESIRED_YAW_OFFSET)
np.testing.assert_almost_equal(local_vars[0]['ZMQ_PitOffset'][ind_30], DESIRED_PITCH_OFFSET, decimal=3)

if __name__ == "__main__":
p1 = mp.Process(target=run_zmq)
Expand Down
16 changes: 8 additions & 8 deletions ROSCO/rosco_registry/rosco_types.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1264,6 +1264,13 @@ LocalVariables:
<<: *complex
description: Complex angle for each blade, sum of modes?
size: 3
ZMQ_YawOffset:
<<: *real
description: Yaw offsety command, [rad]
ZMQ_PitOffset:
<<: *real
size: 3
description: Pitch command offset provided by ZeroMQ
WE:
<<: *derived_type
id: WE
Expand Down Expand Up @@ -1443,14 +1450,7 @@ ExtDLL_Type:
size: 3
length: 1024
description: The name of the procedure in the DLL that will be called.

ZMQ_Variables:
ZMQ_Flag:
<<: *logical
description: Flag if we're using zeroMQ at all (0-False, 1-True)
Yaw_Offset:
<<: *real
description: Yaw offsety command, [rad]


ExtControlType:
avrSWAP:
Expand Down
5 changes: 2 additions & 3 deletions ROSCO/rosco_registry/write_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,13 +118,12 @@ def write_roscoio(yfile):
# ------------------------------------------------
# ------------ ReadRestartFile ------------------
# ------------------------------------------------
file.write('SUBROUTINE ReadRestartFile(avrSWAP, LocalVar, CntrPar, objInst, PerfData, RootName, size_avcOUTNAME, zmqVar, ErrVar)\n')
file.write('SUBROUTINE ReadRestartFile(avrSWAP, LocalVar, CntrPar, objInst, PerfData, RootName, size_avcOUTNAME, ErrVar)\n')
file.write(" TYPE(LocalVariables), INTENT(INOUT) :: LocalVar\n")
file.write(" TYPE(ControlParameters), INTENT(INOUT) :: CntrPar\n")
file.write(" TYPE(ObjectInstances), INTENT(INOUT) :: objInst\n")
file.write(" TYPE(PerformanceData), INTENT(INOUT) :: PerfData\n")
file.write(" TYPE(ErrorVariables), INTENT(INOUT) :: ErrVar\n")
file.write(" TYPE(ZMQ_Variables), INTENT(INOUT) :: zmqVar\n")
file.write(" REAL(ReKi), INTENT(IN) :: avrSWAP(*)\n")
file.write(" INTEGER(IntKi), INTENT(IN) :: size_avcOUTNAME\n")
file.write(" CHARACTER(size_avcOUTNAME-1), INTENT(IN) :: RootName \n")
Expand Down Expand Up @@ -163,7 +162,7 @@ def write_roscoio(yfile):
file.write(' Close ( Un )\n')
file.write(' ENDIF\n')
file.write(' ! Read Parameter files\n')
file.write(' CALL ReadControlParameterFileSub(CntrPar, LocalVar, zmqVar, LocalVar%ACC_INFILE, LocalVar%ACC_INFILE_SIZE, RootName, ErrVar)\n')
file.write(' CALL ReadControlParameterFileSub(CntrPar, LocalVar, LocalVar%ACC_INFILE, LocalVar%ACC_INFILE_SIZE, RootName, ErrVar)\n')
file.write(' IF (CntrPar%WE_Mode > 0) THEN\n')
file.write(' CALL READCpFile(CntrPar, PerfData, ErrVar)\n')
file.write(' ENDIF\n')
Expand Down
10 changes: 6 additions & 4 deletions ROSCO/src/Controllers.f90
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,9 @@ SUBROUTINE PitchControl(avrSWAP, CntrPar, LocalVar, objInst, DebugVar, ErrVar)
IF (CntrPar%IPC_SatMode == 1) THEN
LocalVar%PitCom(K) = saturate(LocalVar%PitCom(K), LocalVar%PC_MinPit, CntrPar%PC_MaxPit)
END IF

! Add ZeroMQ pitch commands
LocalVar%PitCom(K) = LocalVar%PitCom(K) + LocalVar%ZMQ_PitOffset(K)

! Rate limit
LocalVar%PitCom(K) = ratelimit(LocalVar%PitCom(K), CntrPar%PC_MinRat, CntrPar%PC_MaxRat, LocalVar%DT, LocalVar%restart, LocalVar%rlP,objInst%instRL,LocalVar%BlPitch(K)) ! Saturate the overall command of blade K using the pitch rate limit
Expand Down Expand Up @@ -299,14 +302,14 @@ SUBROUTINE VariableSpeedControl(avrSWAP, CntrPar, LocalVar, objInst, ErrVar)

END SUBROUTINE VariableSpeedControl
!-------------------------------------------------------------------------------------------------------------------------------
SUBROUTINE YawRateControl(avrSWAP, CntrPar, LocalVar, objInst, zmqVar, DebugVar, ErrVar)
SUBROUTINE YawRateControl(avrSWAP, CntrPar, LocalVar, objInst, DebugVar, ErrVar)
! Yaw rate controller
! Y_ControlMode = 0, No yaw control
! Y_ControlMode = 1, Yaw rate control using yaw drive

! TODO: Lots of R2D->D2R, this should be cleaned up.
! TODO: The constant offset implementation is sort of circular here as a setpoint is already being defined in SetVariablesSetpoints. This could also use cleanup
USE ROSCO_Types, ONLY : ControlParameters, LocalVariables, ObjectInstances, DebugVariables, ErrorVariables, ZMQ_Variables
USE ROSCO_Types, ONLY : ControlParameters, LocalVariables, ObjectInstances, DebugVariables, ErrorVariables

REAL(ReKi), INTENT(INOUT) :: avrSWAP(*) ! The swap array, used to pass data to, and receive data from, the DLL controller.

Expand All @@ -315,7 +318,6 @@ SUBROUTINE YawRateControl(avrSWAP, CntrPar, LocalVar, objInst, zmqVar, DebugVar,
TYPE(ObjectInstances), INTENT(INOUT) :: objInst
TYPE(DebugVariables), INTENT(INOUT) :: DebugVar
TYPE(ErrorVariables), INTENT(INOUT) :: ErrVar
TYPE(ZMQ_Variables), INTENT(INOUT) :: zmqVar

! Allocate Variables
REAL(DbKi), SAVE :: NacVaneOffset ! For offset control
Expand Down Expand Up @@ -344,7 +346,7 @@ SUBROUTINE YawRateControl(avrSWAP, CntrPar, LocalVar, objInst, zmqVar, DebugVar,

! Compute/apply offset
IF (CntrPar%ZMQ_Mode == 1) THEN
NacVaneOffset = zmqVar%Yaw_Offset
NacVaneOffset = LocalVar%ZMQ_YawOffset
ELSE
NacVaneOffset = CntrPar%Y_MErrSet ! (deg) # Offset from setpoint
ENDIF
Expand Down
17 changes: 8 additions & 9 deletions ROSCO/src/DISCON.F90
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ SUBROUTINE DISCON(avrSWAP, aviFAIL, accINFILE, avcOUTNAME, avcMSG) BIND (C, NAME
CHARACTER(KIND=C_CHAR), INTENT(IN ) :: accINFILE(NINT(avrSWAP(50))) ! The name of the parameter input file
CHARACTER(KIND=C_CHAR), INTENT(IN ) :: avcOUTNAME(NINT(avrSWAP(51))) ! OUTNAME (Simulation RootName)
CHARACTER(KIND=C_CHAR), INTENT(INOUT) :: avcMSG(NINT(avrSWAP(49))) ! MESSAGE (Message from DLL to simulation code [ErrMsg]) The message which will be displayed by the calling program if aviFAIL <> 0.
CHARACTER(SIZE(avcOUTNAME)-1) :: RootName ! a Fortran version of the input C string (not considered an array here) [subtract 1 for the C null-character]
CHARACTER(SIZE(avcOUTNAME)) :: RootName ! a Fortran version of the input C string (not considered an array here) [subtract 1 for the C null-character]
CHARACTER(SIZE(avcMSG)-1) :: ErrMsg ! a Fortran version of the C string argument (not considered an array here) [subtract 1 for the C null-character]

TYPE(ControlParameters), SAVE :: CntrPar
Expand All @@ -58,7 +58,6 @@ SUBROUTINE DISCON(avrSWAP, aviFAIL, accINFILE, avcOUTNAME, avcMSG) BIND (C, NAME
TYPE(PerformanceData), SAVE :: PerfData
TYPE(DebugVariables), SAVE :: DebugVar
TYPE(ErrorVariables), SAVE :: ErrVar
TYPE(ZMQ_Variables), SAVE :: zmqVar
TYPE(ExtControlType), SAVE :: ExtDLL


Expand All @@ -72,7 +71,7 @@ SUBROUTINE DISCON(avrSWAP, aviFAIL, accINFILE, avcOUTNAME, avcMSG) BIND (C, NAME

! Check for restart
IF ( (NINT(avrSWAP(1)) == -9) .AND. (aviFAIL >= 0)) THEN ! Read restart files
CALL ReadRestartFile(avrSWAP, LocalVar, CntrPar, objInst, PerfData, RootName, SIZE(avcOUTNAME), zmqVar, ErrVar)
CALL ReadRestartFile(avrSWAP, LocalVar, CntrPar, objInst, PerfData, RootName, SIZE(avcOUTNAME), ErrVar)
IF ( CntrPar%LoggingLevel > 0 ) THEN
CALL Debug(LocalVar, CntrPar, DebugVar, ErrVar, avrSWAP, RootName, SIZE(avcOUTNAME))
END IF
Expand All @@ -82,7 +81,7 @@ SUBROUTINE DISCON(avrSWAP, aviFAIL, accINFILE, avcOUTNAME, avcMSG) BIND (C, NAME
CALL ReadAvrSWAP(avrSWAP, LocalVar, CntrPar)

! Set Control Parameters
CALL SetParameters(avrSWAP, accINFILE, SIZE(avcMSG), CntrPar, LocalVar, objInst, PerfData, zmqVar, RootName, ErrVar)
CALL SetParameters(avrSWAP, accINFILE, SIZE(avcMSG), CntrPar, LocalVar, objInst, PerfData, RootName, ErrVar)

! Call external controller, if desired
IF (CntrPar%Ext_Mode > 0 .AND. ErrVar%aviFAIL >= 0) THEN
Expand All @@ -97,8 +96,8 @@ SUBROUTINE DISCON(avrSWAP, aviFAIL, accINFILE, avcOUTNAME, avcMSG) BIND (C, NAME
IF ((LocalVar%iStatus == -8) .AND. (ErrVar%aviFAIL >= 0)) THEN ! Write restart files
CALL WriteRestartFile(LocalVar, CntrPar, ErrVar, objInst, RootName, SIZE(avcOUTNAME))
ENDIF
IF (zmqVar%ZMQ_Flag) THEN
CALL UpdateZeroMQ(LocalVar, CntrPar, zmqVar, ErrVar)
IF (CntrPar%ZMQ_Mode > 0) THEN
CALL UpdateZeroMQ(LocalVar, CntrPar, ErrVar)
ENDIF

CALL WindSpeedEstimator(LocalVar, CntrPar, objInst, PerfData, DebugVar, ErrVar)
Expand All @@ -109,7 +108,7 @@ SUBROUTINE DISCON(avrSWAP, aviFAIL, accINFILE, avcOUTNAME, avcMSG) BIND (C, NAME
CALL PitchControl(avrSWAP, CntrPar, LocalVar, objInst, DebugVar, ErrVar)

IF (CntrPar%Y_ControlMode > 0) THEN
CALL YawRateControl(avrSWAP, CntrPar, LocalVar, objInst, zmqVar, DebugVar, ErrVar)
CALL YawRateControl(avrSWAP, CntrPar, LocalVar, objInst, DebugVar, ErrVar)
END IF

IF (CntrPar%Flp_Mode > 0) THEN
Expand All @@ -129,8 +128,8 @@ SUBROUTINE DISCON(avrSWAP, aviFAIL, accINFILE, avcOUTNAME, avcMSG) BIND (C, NAME
IF ( CntrPar%LoggingLevel > 0 ) THEN
CALL Debug(LocalVar, CntrPar, DebugVar, ErrVar, avrSWAP, RootName, SIZE(avcOUTNAME))
END IF
ELSEIF ((LocalVar%iStatus == -1) .AND. (zmqVar%ZMQ_Flag)) THEN
CALL UpdateZeroMQ(LocalVar, CntrPar, zmqVar, ErrVar)
ELSEIF ((LocalVar%iStatus == -1) .AND. (CntrPar%ZMQ_Mode > 0)) THEN
CALL UpdateZeroMQ(LocalVar, CntrPar, ErrVar)
END IF


Expand Down
2 changes: 1 addition & 1 deletion ROSCO/src/ROSCO_Helpers.f90
Original file line number Diff line number Diff line change
Expand Up @@ -943,7 +943,7 @@ SUBROUTINE ParseDbAry_Opt ( FileLines, ParamName, Ary, AryLen, FileName, ErrVar,
ENDIF

Ary = 0 ! Default of allocatable arrays is 0 for now
PRINT *, "ROSCO Warning: Did not find correct size"//TRIM( ParamName )//" in input file. Using default value of [", Ary, "]"
PRINT *, "ROSCO Warning: Did not find correct size "//TRIM( ParamName )//" in input file. Using default value of [", Ary, "]"
ENDIF

IF (FoundLine) THEN
Expand Down
Loading

0 comments on commit c75738e

Please sign in to comment.