ROSCO-MATLAB Implementation

Hi Shubham,

It looks like the descriptors in that DISCON.IN file are out of date - sorry about that. VS_ControlMode = 3 corresponds to TSR tracking control below rated, and constant power above rated.

The most recent descriptions can be found here:
rosco-toolbox.readthedocs.io/en … on-in-file

The details of this are discussed in the WES pre-print about ROSCO here:
wes.copernicus.org/preprints/wes-2021-19/

Cheers,
Nikhar

Hello Nikhar,
Thanks for the prompt reply. I really appreciate all the help.

Cheers,
Shubham

Hello Nikhar,
I have attached the code for the PIController function.

[code]
REAL FUNCTION PIController(error, kp, ki, minValue, maxValue, DT, I0, reset, inst)
! PI controller, with output saturation

    IMPLICIT NONE
    ! Allocate Inputs
    REAL(8), INTENT(IN)         :: error
    REAL(8), INTENT(IN)         :: kp
    REAL(8), INTENT(IN)         :: ki
    REAL(8), INTENT(IN)         :: minValue
    REAL(8), INTENT(IN)         :: maxValue
    REAL(8), INTENT(IN)         :: DT
    INTEGER(4), INTENT(INOUT)   :: inst
    REAL(8), INTENT(IN)         :: I0
    LOGICAL, INTENT(IN)         :: reset     
    ! Allocate local variables
    INTEGER(4)                      :: i                                            ! Counter for making arrays
    REAL(8)                         :: PTerm                                        ! Proportional term
    REAL(8), DIMENSION(99), SAVE    :: ITerm = (/ (real(9999.9), i = 1,99) /)       ! Integral term, current.
    REAL(8), DIMENSION(99), SAVE    :: ITermLast = (/ (real(9999.9), i = 1,99) /)   ! Integral term, the last time this controller was called. Supports 99 separate instances.
    INTEGER(4), DIMENSION(99), SAVE :: FirstCall = (/ (1, i=1,99) /)                ! First call of this function?
    
    ! Initialize persistent variables/arrays, and set initial condition for integrator term
    IF ((FirstCall(inst) == 1) .OR. reset) THEN
        ITerm(inst) = I0
        ITermLast(inst) = I0
        
        FirstCall(inst) = 0
        PIController = I0
    ELSE
        PTerm = kp*error
        ITerm(inst) = ITerm(inst) + DT*ki*error
        ITerm(inst) = saturate(ITerm(inst), minValue, maxValue)
        PIController = saturate(PTerm + ITerm(inst), minValue, maxValue)
    
        ITermLast(inst) = ITerm(inst)
    END IF
    inst = inst + 1
    
END FUNCTION PIController[/code]

As per my understanding, as all the values of the FirstCall array are initialized as 1, the control will never transfer to the second part of the loop (the else part). Is this right, or I am missing some point?

Thanks,
Shubham

Hi Shubham,

FirstCall(inst) is set to zero in the first part of the for loop. Because of this, all subsequent calls of the function will go to the second part of the loop unless reset=True.

Nikhar

Hello Nikhar,
Correct me if I am wrong. Here inst is used to access the value of a particular cell of the FirstCall array. Since towards the end of the subroutine, the inst variable is modified as inst = inst+1, so on every to call to the subroutine the inst value will be different. So effectively, everytime we will be checking a different location of the FirstCall array with 1.

Thanks,
Shubham

Hi Shubham,
Each time the controller is called, the SetParameters subroutine is subsequently called. This happens here:
github.com/NREL/ROSCO/blob/e53f … ON.F90#L64

Then, in the SetParameters subroutine, the filter and controller instances are re-set to 1:
github.com/NREL/ROSCO/blob/e53f … s.f90#L512

So, basically, each time the DISCON routine is called, the instance variable is reset to one. This effectively “counts” the order at which the filters, pi-controllers, etc. are called. So, in the PIController function, the FirstCall gets filled as many times as the PIController function is called, with each entry corresponding to the order in which it is called. Hopefully this makes sense, it is a little confusing.

This behavior exists because of the way the memory needs to be stored for FORTRAN implementation (and could probably be done in a cleaner way). For your MATLAB implementation, though, I don’t think you should do this, as function calls and full routines do not allocate memory in MATLAB in the way that FORTRAN does.

Cheers,
Nikhar

Hi Nikhar,
How to generate the performance file (Cp_Ct_Cq.txt) for the NREL 5MW turbine with the Monopile foundation? Can you please share the standard file, if you have already generated it?

Thanks,
Shubham

Hi Shubham,
The Cp_Ct_Cq.txt file can be generated using CCBlade, as is shown in example_03.py in the ROSCO toolbox:
github.com/NREL/ROSCO_toolbox/b … mple_03.py

For the NREL5MW rotor, the file can be found in the ROSCO toolbox here:
github.com/NREL/ROSCO_toolbox/b … REL5MW.txt

It is no different for the NREL 5MW turbine on the Monopile foundation, as the rotor does not change between the two turbines.

Cheers,
Nikhar

Thanks for the information, Nikhar. This is exactly what I was looking for.

Hello Nikhar,
I have implemented the ROSCO controller in MATLAB, but I am facing an issue. The StateMachine routine uses the variables LocalVar.GenArTq and LocalVar.GenBrTq to decide the region of controller operation.

[code] ! — Pitch controller state machine —
IF (CntrPar%PC_ControlMode == 1) THEN
LocalVar%PC_State = 1
ELSE
LocalVar%PC_State = 0
END IF

        ! --- Torque control state machine ---
        IF (LocalVar%PC_PitComT >= LocalVar%VS_Rgn3Pitch) THEN       

            IF (CntrPar%VS_ControlMode == 1) THEN                   ! Region 3
                LocalVar%VS_State = 5 ! Constant power tracking
            ELSE 
                LocalVar%VS_State = 4 ! Constant torque tracking
            END IF
        ELSE
            IF (LocalVar%GenArTq >= CntrPar%VS_MaxOMTq*1.01) THEN       ! Region 2 1/2 - active PI torque control
                LocalVar%VS_State = 3                 
            ELSEIF ((LocalVar%GenSpeedF < CntrPar%VS_RefSpd) .AND. &
                    (LocalVar%GenBrTq >= CntrPar%VS_MinOMTq)) THEN       ! Region 2 - optimal torque is proportional to the square of the generator speed
                LocalVar%VS_State = 2
            ELSEIF (LocalVar%GenBrTq < CntrPar%VS_MinOMTq) THEN   ! Region 1 1/2
            
                LocalVar%VS_State = 1
            ELSE                                                        ! Error state, Debug
                LocalVar%VS_State = 0
            END IF
        END IF

[/code]

However, these two variables are not calculated for CntrPar.VS_ControlMode = 2 and 3.

[code] ! Optimal Tip-Speed-Ratio tracking controller
IF ((CntrPar%VS_ControlMode == 2) .OR. (CntrPar%VS_ControlMode == 3)) THEN
! Constant Power, update VS_MaxTq
IF (CntrPar%VS_ControlMode == 3) THEN
LocalVar%VS_MaxTq = min((CntrPar%VS_RtPwr/(CntrPar%VS_GenEff/100.0))/LocalVar%GenSpeedF, CntrPar%VS_MaxTq)
END IF

        ! PI controller
        LocalVar%GenTq = PIController(LocalVar%VS_SpdErr, CntrPar%VS_KP(1), CntrPar%VS_KI(1), CntrPar%VS_MinTq, LocalVar%VS_MaxTq, LocalVar%DT, LocalVar%VS_LastGenTrq, .FALSE., objInst%instPI)
        LocalVar%GenTq = saturate(LocalVar%GenTq, CntrPar%VS_MinTq, LocalVar%VS_MaxTq)
    
    ! K*Omega^2 control law with PI torque control in transition regions
    ELSE
        ! Update PI loops for region 1.5 and 2.5 PI control
        LocalVar%GenArTq = PIController(LocalVar%VS_SpdErrAr, CntrPar%VS_KP(1), CntrPar%VS_KI(1), CntrPar%VS_MaxOMTq, CntrPar%VS_ArSatTq, LocalVar%DT, CntrPar%VS_MaxOMTq, .FALSE., objInst%instPI)
        LocalVar%GenBrTq = PIController(LocalVar%VS_SpdErrBr, CntrPar%VS_KP(1), CntrPar%VS_KI(1), CntrPar%VS_MinTq, CntrPar%VS_MinOMTq, LocalVar%DT, CntrPar%VS_MinOMTq, .FALSE., objInst%instPI)
        
        ! The action
        IF (LocalVar%VS_State == 1) THEN ! Region 1.5
            LocalVar%GenTq = LocalVar%GenBrTq
        ELSEIF (LocalVar%VS_State == 2) THEN ! Region 2
            LocalVar%GenTq = CntrPar%VS_Rgn2K*LocalVar%GenSpeedF*LocalVar%GenSpeedF
        ELSEIF (LocalVar%VS_State == 3) THEN ! Region 2.5
            LocalVar%GenTq = LocalVar%GenArTq
        ELSEIF (LocalVar%VS_State == 4) THEN ! Region 3, constant torque
            LocalVar%GenTq = CntrPar%VS_RtTq
        ELSEIF (LocalVar%VS_State == 5) THEN ! Region 3, constant power
            LocalVar%GenTq = (CntrPar%VS_RtPwr/(CntrPar%VS_GenEff/100.0))/LocalVar%GenSpeedF
        END IF
        
        ! Saturate
        LocalVar%GenTq = saturate(LocalVar%GenTq, CntrPar%VS_MinTq, CntrPar%VS_MaxTq)
    ENDIF

[/code]
For this case, do I have to initialize them and assign a constant value? I need your suggestions on this.

Thanks,
Shubham

Hi Shubham,
Because the variables LocalVar%GenArTq and LocalVar%GenBrTq are not used when the TSR tracking torque controller are used, I would assume that these are unnecessary to initialize at all if VS_Mode=2 or 3. This isgbecause MATLAB does not necessitate the detailed memory allocation that FORTRAN does.

Cheers,
Nikhar

Hello Nikhar,
I agree that the variables LocalVar%GenArTq and LocalVar%GenBrTq are not used with the TSR tracking torque controller. However, these variables are always required for determining the region of operation of the torque controller, LocalVar%VS_State, for all values of CntrPar%VS_ControlMode.

Thanks,
Shubham

Hi Shubham,
Based on the if/else statement here: github.com/NREL/ROSCO/blob/fe71 … s.f90#L167, the state machine is only used if VS_ControlModes is 0 or 1.

I think I was a bit confused by your initial question, though. Correct me if I am still wrong here, but the problem is that the state machine is called during each call to the controller, the LocalVar%GenArTq and LocalVar%GenBrTq terms need to be assigned otherwise errors will happen, even when the state machine isn’t used by the torque controller.

So, that said, I think the path of least resistance is probably to just initialize LocalVar%GenArTq and LocalVar%GenBrTq to some constant values. You could also put in an if statement to only call the state machine if it is needed, but this would certainly be a discrepancy between the MATLAB and FORTRAN code.

Cheers,
Nikhar

Hello Nikhar,
I will try to elaborate on the problem further.

  1. The StateMachine routine will be called for every call of the controller, in the order defined in the DISCON file ([url]ROSCO/DISCON.F90 at fe71ce42c58905fb3b4788c0ed05b59917c87671 · NREL/ROSCO · GitHub), to determine the region of operation of the controller.

  2. The StateMachine uses the variables LocalVar%GenArTq and LocalVar%GenBrTq along with other variables to determine the region of operation (LocalVar%VS_State), for all values of the CntrPar%VS_ControlMode ([url]ROSCO/ControllerBlocks.f90 at fe71ce42c58905fb3b4788c0ed05b59917c87671 · NREL/ROSCO · GitHub)

  3. Since LocalVar%GenArTq and LocalVar%GenBrTq are not calculated for CntrPar%VS_ControlMode = 2 and 3 ([url]ROSCO/Controllers.f90 at fe71ce42c58905fb3b4788c0ed05b59917c87671 · NREL/ROSCO · GitHub), the call to the StateMachine in this case results in error.

Please correct me if I am wrong.

Thanks,
Shubham

Hi Shubham,

Yes, you are correct and this all makes sense.

When VS_ControlMode = 2 and 3, even though the state machine is called, the controller state is not actually used for any purpose.

In the FORTRAN implementation of ROSCO, the variables LocalVar%GenArTq and LocalVar%GenBrTq are initialized but never end up being modified, so whenever the state machine is called, they just stay at 0.0.

So, I think there are two options for implementation in MATLAB:

  1. Set GenArTq and GenBrTq to some arbitrary constant when VS_ControlMode = 2 and 3
  2. Skip the state machine all together when VS_ControlMode = 2 and 3

Hopefully this makes sense.

Nikhar

Hello Nikhar,

I have a question about the compiling the ROSCO or the generic 5MW controller. Per your post above, the compiled library consists of the .dll for windows, and .so files for linux. Can these .so files be read on any version of the gcc on a linux machine? If so, how is this accomplished?

Thanks in advance.

Best,
Shyam

Hi Shyam,
The gcc compilers can be used to generate .so files on a linux system per the most recent ROSCO compiling instructions:
rosco-toolbox.readthedocs.io/en … controller

GCC is not needed to simply read the compiled library, though - the library should just be pointed to through your wind turbine simulation software (e.g. in OpenFAST’s ServoDyn input file). As long as the dynamic library was compiled on a machine with the same architecture (e.g. 64-bit linux), it should work.

Hopefully this helps,
Nikhar

Hello Nikhar,

Thank you for your response. Sorry I didn’t make my question very clear. I meant to ask if the .so file compiled with one version of a gcc can be used in a simulation on another machine that has a different version of the gcc? I have been working with the “libDISCON-5MW_Wind_Controller.so” and it works with a particular gcc (v 4.8.5). I was to trying to understand if there are some settings during the compiling process that would be make this .so file compatible with any gcc version as I am having issues with a simulation that is calling a similar .so file that was compiled with a newer gcc.

Thanks again for your help.

Best,
Shyam

Hi Shyam,
From my understanding, as long as the computer architecture is the same, the compiled libraries should work. I am certainly not an expert with all of the nuances of various compilers and computer architectures though, so I could be missing something here. Perhaps somebody else on the forum might have a more thorough response.

Cheers,
Nikhar

Hi Shubham,

I have a very similar issue with implementing ROSCO in MATLAB. Just wondering did you ever end contributing towards the repository with the working code? If so would you mind sharing the link please as this would be a big help for my project.

Kind regards,
Gerard