Reading the controller DLL

Dear all,
I hope I am placing the following question in the right place.
I would like to understand the module BladedDLLInterface.f90 because I want to write a similar code to use bladed controllers. In particular, I am trying to use the ROSCO controller in an in-house software.
My approach is the following:
First, I created an additional LIB file from the DLL. I have specified its path in Visual Studio as “Additional Dependency”. The additional text-file with controller parameters is also located there. It’s name is set in the input variable accINFILE .
To access the DISCON controller function, I used the following code:

subroutine DISCON(this, avrSWAP, aviFAIL, accINFILE, avcOUTNAME, avcMSG) 
    use, intrinsic :: iso_c_binding, only : c_float, c_int, c_char, c_null_char
    implicit none 
    !DEC$ ATTRIBUTES DLLEXPORT :: DISCON
    class(model_controller), intent(inout)           :: this
    REAL(C_FLOAT),  dimension(117),   INTENT(INOUT)  :: avrSWAP     
    INTEGER(C_INT),                   INTENT(INOUT)  :: aviFAIL    
    CHARACTER(KIND=C_CHAR, len = 28), INTENT(IN)     :: accINFILE  
    CHARACTER(KIND=C_CHAR, len = 20), INTENT(IN)     :: avcOUTNAME  
    CHARACTER(KIND=C_CHAR, len = 49), INTENT(INOUT)  :: avcMSG 
 end subroutine DISCON .

Then I call the controller like an ordinary subroutine

DISCON(this, avrSWAP, aviFAIL, accINFILE, avcOUTNAME, avcMSG).

However, the controller code does not seem to work. I would be very happy if it could be explained to me whether I have forgotten important (ROSCO specific) steps or reading a DLL basically does not work like this. Do you also create a LIB file or can you only access the controller functions with the DLL? I would be very grateful for an answer.
Best regards,
David

Hi David,

Several months back, within ROSCO, I made something similar for calling external libraries.

Here is a working example, and here are the parts of BladedInterface.f90 that I added to ROSCO.

Some things that tripped me up (that I can remember):

  • different system architectures require different library loading functions
  • these lengths need to be exactly correct, otherwise there were memory leaks and seg faults

Here is another tool that might be of interest; it calls a dynamic library from python.

To actually respond to your post/questions: I didn’t have to specify any dependencies, though I use cmake to compile ROSCO. You definitely need to load the library somewhere. I don’t create a lib file called DISCON, that dynamic library should already exist somewhere.

I borrowed heavily (and happily) from the OpenFAST source, so they might be able to provide more precise answers than I can.

Best, Dan

Hi David,

When you want to link a library (DLL) to a code, there are two ways that it can be done:

  1. At build time, you add a .lib file to the dependencies so the code links with the executable. This typically means that the library is loaded immediately when the executable begins, and it must always be named the same thing.
  2. You DON’T link with a library file when building the executable, but you add an abstract interface in the code to tell it what subroutine interfaces will look like in a library you call. You can dynamically load the library, specifying a different name for the DLL, names of its subroutines (as long as they follow the types/sizes specified in the abstract interface). The routines to load the library are system dependent, so if you are going to build on different systems, you have to take that into account (see OpenFAST’s NWTC_Library Sys*.f90 routines for loading and freeing libraries). This option requires a little more complicated code, but is much more flexible.

The Bladed DLL controller interface in OpenFAST uses option #2, so you don’t specify dependencies, but it seems like you are trying to do option #1.

I have never attempted using a class passed through the DISCON-type interface. I’d be surprised if class is supported in the c bindings that we need to use in OpenFAST to make it possible to allow DLLs written in a language other than Fortran. (You can work around that a bit by pass pointers to objects instead.) But, if you want to link your DLL using option #1, I think that should work.

Anyway, I guess my answer really depends on the way you want to set up your DLL with your in-house software.

Hello together,
thank you very much for the helpful answers.
I have loaded the DLL in the way I think is the 2nd suggested solution. As a result Visual Studio tells me that libdiscon.dll was loaded.
However, the entire programme aborts when the DISCON function is called. I suspect that it must be because the input values are wrong. In the code snippet below you can see the code that calls the routine.
I have been provided with an optimised IN file (DISCON_DATA.IN), which I have placed in the working folder where the EXE is also created.
module class_controller


    implicit none
    
    type :: model_controller
        logical   :: boolean_abort = .FALSE. 
        contains
        procedure :: use_controller
      
    end type model_controller
    
        
    contains
    
    subroutine use_controller(this, swap)
      use, intrinsic :: iso_c_binding, only : c_float, c_int, c_char, c_null_char
      use DISCON_m, only : SetupDISCON, DISCON
      implicit none
     
      class(model_controller), intent(inout)        :: this
      real(kind = 8), dimension(78), intent(inout)  :: swap
      real(c_float), dimension(78)                  :: ctrl_avrSWAP(78)                    
      integer(c_int)                                :: ctrl_aviFAIL                    
      character(kind=c_char,len=13)                 :: ctrl_accINFILE
      character(kind=c_char,len=51)                 :: ctrl_avcOUTNAME           
      character(kind=c_char,len=49)                 :: ctrl_avcMSG    
    
    
      call SetupDISCON(this%boolean_abort) 
      if (this%boolean_abort .eqv. .TRUE.) then
        return
      end if
      
      ctrl_accINFILE   = c_char_"DISCON_DATA.IN" // c_null_char
      ctrl_avcOUTNAME  = c_char_"OUTNAME.txt" // c_null_char
      !ctrl_avcMSG      = c_null_char
      !ctrl_aviFAIL     = 0_c_int

      ctrl_avrSWAP     = swap               ! converting input to C++ float values
      ctrl_avrSWAP(50) = 13.0_c_float       ! length of the path "ctrl_accINFILE" / No. of characters in the INFILE argument, -1 because it starts counting at 0 ?
      ctrl_avrSWAP(51) = 10.0_c_float       ! No. of characters in the OUTNAME argument
     
      ! Calling the actual controller function from the DLL
      call DISCON(ctrl_avrSWAP, ctrl_aviFAIL, ctrl_accINFILE, ctrl_avcOUTNAME, ctrl_avcMSG )
      
      swap = ctrl_avrSWAP ! Enter the new values in the array
    
    end subroutine use_controller
    
end module class_controller

My specific questions at this point are:

  • Are there any other external files that the ROSCO controller needs?
  • Can I assume an array size for the variable ctrl_swap or must it necessarily be passed in an allocatable way?
  • Possibly the problem is that it is the first time step, so values in the swap array are missing (=0) because they could not yet be calculated. Which values must necessarily be included in the first call of DISCON so that the controller begins to work? (For now, it would be enough for me to control only the pitch).

Best regards,
David

I don’t know what’s inside your DISCON routine (and have no experience with ROSCO), so it’s hard to answer conclusively.

However, I can say that the “normal” Bladed DLL DISCON routine needs an already allocated avrSWAP array. For the Bladed v3.6 interface, it needed to be at least 85 elements long. Later versions of Bladed specify 165 elements. However, that size is really just determined by how your calling routine and DISCON use that data. Having an array too big is better than too small.

It might be helpful to read the Bladed documentation on the DISCON interface, or read the Fill_avrSWAP routine in OpenFAST source code: openfast/BladedInterface.f90 at main · OpenFAST/openfast · GitHub to see what needs to be set on the first call to the DLL (but that will really depend on how the DISCON routine in the DLL is actually written). I would definitely set anything that is a parameter (almost anything that doesn’t start with u%)

If you add print statements inside the DLL, you might be able to tell where the program is actually failing… My first guess is that you may have to send larger array sizes.

Hello everyone,
thanks for the comments.
In the subroutine Fill_avrSWAP, if I understand it correctly, parameters are read from the .IN-file and inserted into the avr_SWAP-array.
I have assumed so far that the ROSCO controller itself reads the constant parameters from this file (in the routine ReadControlParameterFileSub). Otherwise, I would not understand why the IN file would exist as an input in DISCON at all.
Until now I have passed the controller only the values that are read in the ROSCO routine ReadAvrSWAP with every call.
So my question is: Do I actually have to read the file again “manually” and save the values in avr_SWAP?
Best,
David

Hi David,

The Fill_avrSWAP subroutine mostly sends turbine information to ROSCO. The values that ROSCO reads are here, but more are filled in OpenFAST.

I think in some controllers using the Bladed interface, you can send it constant controller parameters. In ROSCO, it uses only the file path of the DISCON (avrSWAP(50)) and to read in the ROSCO controller parameters here.

You shouldn’t need to load the parameters of the DISCON twice, but filling entries 49-51 is important for the controller to know where to look for the parameters and write outputs.

I hope this helps.

Best, Dan

Hi Dan,

thanks for the quick reply.
I filled the entries 49-51. If these do not match the IN file, I correctly get an error message.
Do I understand you correctly that there are controllers that need to be given the parameters via openFast and others, like ROSCO, that read them in themselves?

Let’s consider the case of the minimum pitch angle. This constant parameter is contained in the IN file. So ROSCO will read it itself from the file and I don’t have to give it to it again in avrSWAP?
To check this, I have changed this parameter from 0° to 10° in the file. However, the blades still pitch up to 0°.

Best, David

Hi David,

Changing the minimum pitch in ROSCO should have an effect if the ROSCO controller is being used. The parameter given in ServoDyn will be ignored in that case.

Best, Dan

Hello everyone,
because the results of my simulations do not match with the power curves in the report of the NREL 15 MW turbine, I am still looking for wrong input values or outputs that are not used correctly.
I use the following files:
libdiscon.dll: IEA-15-240-RWT/OpenFAST/IEA-15-240-RWT/ServoData at master · IEAWindTask37/IEA-15-240-RWT · GitHub
Cp_Ct_Cq.IEA15MW.txt : IEA-15-240-RWT/OpenFAST/IEA-15-240-RWT at master · IEAWindTask37/IEA-15-240-RWT · GitHub
and the DISCON.IN-file and : IEA-15-240-RWT/OpenFAST/IEA-15-240-RWT-Monopile at master · IEAWindTask37/IEA-15-240-RWT · GitHub

In particular, I am unsure about the following inputs and outputs:

  • avr_SWAP(11) = avr_SWAP(45) ! 'current demanded pitch angle' = 'Demanded pitch angle (Collective Pitch)' ?
  • What is meant in avr_SWAP(13) 'Demanded power'? Maybe demanded torque * current rotor speed?
  • avr_SWAP(22) = avr_SWAP(47) ! Demanded generator torque' = 'Demanded generator torque' ?

Something that may give a clue to the problem is the Wind Speed Estimator. Every 10 seconds the ROSCO controller outputs some values, including this wind speed estimator. Although I have carried out a simulation with a constant wind speed of 12m/s as an example, the controller determines 5m/s here. If I set ‘WE_Mode’ in the DISCON.IN file from 2 to 1, I do not get the correct values overall, but I do get the correct wind speed.

Best regards,
David

Hi David,

I can’t reply all your questions but regarding the avr_SWAP array values & meanings in general, there are some explanations in Bladed User Manual, if you can find it online.

Best regards,

Hi David,

It’s difficult to debug your issue without looking at more signals. You could try disabling the controller and setting a constant pitch angle to ensure the controller is the issue.

You could also try setting WE_Mode to 0, and ROSCO will use the hub height wind speed as the estimate; this would only mask what might be the more significant issue.

You can also set LoggingLevel to 3 in ROSCO, and it will output the avrSWAP being sent to and from OpenFAST vs. your model, which might help highlight any differences. In the .dbg file outputted by ROSCO are WE_b and WE_t, among other signals, which show the inputs to the wind speed estimator. In the past, the units of those signals have caused problems.

I hope this helps. I don’t know the answers about the avrSWAP beyond what has already been posted.

Best, Dan