module m_alm_controller !! Variable-speed generator-torque controller and PI collective blade-pitch !! controller of the NREL offshore 5 MW baseline turbine, as written by !! J. Jonkman (NREL/NWTC) for the IEA Annex XXIII OC3 studies (DISCON). !! !! Ported from Incompact3d's `acl_controller.f90`. The torque law has five !! regions (1, 1 1/2, 2, 2 1/2 and 3) on the low-pass filtered generator !! speed; the pitch controller is a gain-scheduled PI on the speed error, !! with pitch-angle and pitch-rate limits. The pitch gains and limits are !! those of the NREL 5 MW turbine; the torque parameters come from the !! turbine input file. use m_common, only: dp use m_logger, only: abort_run, log_info implicit none private public :: controller_t, init_controller, operate_controller integer, parameter, public :: max_ctrl_blades = 3 type :: controller_t ! Input parameters real(dp) :: corner_freq = 0._dp !! speed filter corner frequency [rad/s] real(dp) :: pc_dt = 0._dp !! pitch controller interval [s] real(dp) :: pc_ki = 0._dp !! integral gain at rated pitch [-] real(dp) :: pc_kk = 0._dp !! pitch where dP/dpitch doubles [rad] real(dp) :: pc_kp = 0._dp !! proportional gain at rated pitch [s] real(dp) :: pc_max_pit = 0._dp !! maximum pitch [rad] real(dp) :: pc_max_rat = 0._dp !! maximum pitch rate [rad/s] real(dp) :: pc_min_pit = 0._dp !! minimum pitch [rad] real(dp) :: pc_ref_spd = 0._dp !! reference generator speed [rad/s] real(dp) :: vs_ct_in_sp = 0._dp !! cut-in generator speed [rad/s] real(dp) :: vs_dt = 0._dp !! torque controller interval [s] real(dp) :: vs_max_rat = 0._dp !! maximum torque rate [N m/s] real(dp) :: vs_max_tq = 0._dp !! maximum generator torque [N m] real(dp) :: vs_rgn2_k = 0._dp !! region 2 torque constant real(dp) :: vs_rgn2_sp = 0._dp !! region 1 1/2 to 2 speed [rad/s] real(dp) :: vs_rgn3_mp = 0._dp !! pitch above which region 3 applies real(dp) :: vs_rt_gn_sp = 0._dp !! rated generator speed [rad/s] real(dp) :: vs_rt_pwr = 0._dp !! rated power [W] real(dp) :: vs_sl_pc = 0._dp !! region 2 1/2 slip [%] real(dp) :: gear_box_ratio = 0._dp real(dp) :: i_generator = 0._dp !! generator moment of inertia ! State real(dp) :: bl_pitch(max_ctrl_blades) = 0._dp real(dp) :: gen_speed = 0._dp real(dp) :: gen_speed_f = 0._dp real(dp) :: gen_trq = 0._dp real(dp) :: gk = 0._dp real(dp) :: int_spd_err = 0._dp real(dp) :: last_gen_trq = 0._dp real(dp) :: last_time = 0._dp real(dp) :: last_time_pc = 0._dp real(dp) :: last_time_vs = 0._dp real(dp) :: pit_com(max_ctrl_blades) = 0._dp real(dp) :: vs_slope15 = 0._dp real(dp) :: vs_slope25 = 0._dp real(dp) :: vs_sy_sp = 0._dp real(dp) :: vs_tr_gn_sp = 0._dp integer :: status = 0 !! 0 on the first call, then > 0 end type controller_t contains subroutine init_controller(ctrl, generator_inertia, gb_ratio, & rated_gen_speed, rated_limit_gen_torque, & cut_in_gen_speed, region2_start_gen_speed, & kgen, rated_power, maximum_torque) implicit none type(controller_t), intent(inout) :: ctrl real(dp), intent(in) :: generator_inertia, gb_ratio, rated_gen_speed, & rated_limit_gen_torque, cut_in_gen_speed, & region2_start_gen_speed, kgen, rated_power, & maximum_torque ctrl%gear_box_ratio = gb_ratio ctrl%i_generator = generator_inertia ctrl%vs_ct_in_sp = cut_in_gen_speed ctrl%vs_rt_gn_sp = rated_gen_speed ctrl%vs_max_rat = rated_limit_gen_torque ctrl%vs_rgn2_k = kgen ctrl%vs_rgn2_sp = region2_start_gen_speed ctrl%vs_rt_pwr = rated_power ctrl%vs_max_tq = maximum_torque ctrl%corner_freq = 1.570796_dp ctrl%pc_dt = 0.00125_dp ctrl%pc_ki = 0.008068634_dp ctrl%pc_kk = 0.1099965_dp ctrl%pc_kp = 0.01882681_dp ctrl%pc_max_pit = 1.570796_dp ctrl%pc_max_rat = 0.1396263_dp ctrl%pc_min_pit = 0._dp ctrl%pc_ref_spd = 122.9096_dp ctrl%vs_dt = 0.00125_dp ctrl%vs_rgn3_mp = 0.01745329_dp ctrl%vs_sl_pc = 10._dp ! Derived torque-control parameters ctrl%vs_sy_sp = ctrl%vs_rt_gn_sp/(1._dp + 0.01_dp*ctrl%vs_sl_pc) ctrl%vs_slope15 = (ctrl%vs_rgn2_k*ctrl%vs_rgn2_sp*ctrl%vs_rgn2_sp) & /(ctrl%vs_rgn2_sp - ctrl%vs_ct_in_sp) ctrl%vs_slope25 = (ctrl%vs_rt_pwr/ctrl%vs_rt_gn_sp) & /(ctrl%vs_rt_gn_sp - ctrl%vs_sy_sp) if (ctrl%vs_rgn2_k == 0._dp) then ! Flat region 2 torque ctrl%vs_tr_gn_sp = ctrl%vs_sy_sp else ! Region 2 torque quadratic in speed ctrl%vs_tr_gn_sp = (ctrl%vs_slope25 & - sqrt(ctrl%vs_slope25*(ctrl%vs_slope25 & - 4._dp*ctrl%vs_rgn2_k & *ctrl%vs_sy_sp))) & /(2._dp*ctrl%vs_rgn2_k) end if call check(ctrl%vs_ct_in_sp >= 0._dp, 'VS_CtInSp must not be negative.') call check(ctrl%vs_rgn2_sp > ctrl%vs_ct_in_sp, & 'VS_Rgn2Sp must be greater than VS_CtInSp.') call check(ctrl%vs_tr_gn_sp >= ctrl%vs_rgn2_sp, & 'VS_TrGnSp must not be less than VS_Rgn2Sp.') call check(ctrl%vs_max_rat > 0._dp, 'VS_MaxRat must be greater than zero.') call check(ctrl%vs_rt_pwr >= 0._dp, 'VS_RtPwr must not be negative.') call check(ctrl%vs_rgn2_k >= 0._dp, 'VS_Rgn2K must not be negative.') call check(ctrl%vs_rgn2_k*ctrl%vs_rt_gn_sp*ctrl%vs_rt_gn_sp & <= ctrl%vs_rt_pwr/ctrl%vs_rt_gn_sp, & 'VS_Rgn2K*VS_RtGnSp^2 must not be greater than & &VS_RtPwr/VS_RtGnSp.') call check(ctrl%vs_max_tq >= ctrl%vs_rt_pwr/ctrl%vs_rt_gn_sp, & 'VS_RtPwr/VS_RtGnSp must not be greater than VS_MaxTq.') ctrl%status = 0 call log_info('ALM: NREL 5MW torque and pitch controller enabled') contains subroutine check(ok, message) logical, intent(in) :: ok character(len=*), intent(in) :: message if (.not. ok) call abort_run('ALM controller: '//message, & collective=.true.) end subroutine check end subroutine init_controller subroutine operate_controller(ctrl, time, n_blades, rot_speed) !! Advance the controller to `time` given the rotor speed [rad/s]. The !! commanded generator torque is left in gen_trq and the commanded blade !! pitch angles in pit_com. implicit none type(controller_t), intent(inout) :: ctrl real(dp), intent(in) :: time, rot_speed integer, intent(in) :: n_blades ! Slightly above one, so a controller interval equal to the time step ! fires every step despite rounding real(dp), parameter :: one_plus_eps = 1._dp + epsilon(1._dp) real(dp) :: alpha, elap_time, trq_rate, spd_err, pit_com_p, pit_com_i, & pit_com_t, pit_rate integer :: k ctrl%gen_speed = rot_speed*ctrl%gear_box_ratio if (ctrl%status == 0) then ! First call: start the filter and integrator so the pitch stays ! unchanged for a zero speed error, and fire both controllers below. ! As in DISCON (iStatus >= 0); Incompact3d skips control on this call ctrl%gen_speed_f = ctrl%gen_speed ctrl%pit_com = ctrl%bl_pitch ctrl%gk = 1._dp/(1._dp + ctrl%pit_com(1)/ctrl%pc_kk) ctrl%int_spd_err = ctrl%pit_com(1)/(ctrl%gk*ctrl%pc_ki) ctrl%last_time = time ctrl%last_time_pc = time - ctrl%pc_dt ctrl%last_time_vs = time - ctrl%vs_dt end if ! Single-pole low-pass filter on the generator speed alpha = exp((ctrl%last_time - time)*ctrl%corner_freq) ctrl%gen_speed_f = (1._dp - alpha)*ctrl%gen_speed & + alpha*ctrl%gen_speed_f ! Variable-speed torque control elap_time = time - ctrl%last_time_vs if (time*one_plus_eps - ctrl%last_time_vs >= ctrl%vs_dt) then if (ctrl%gen_speed_f >= ctrl%vs_rt_gn_sp & .or. ctrl%pit_com(1) >= ctrl%vs_rgn3_mp) then ! Region 3: constant power ctrl%gen_trq = ctrl%vs_rt_pwr/ctrl%gen_speed_f else if (ctrl%gen_speed_f <= ctrl%vs_ct_in_sp) then ! Region 1: no torque ctrl%gen_trq = 0._dp else if (ctrl%gen_speed_f < ctrl%vs_rgn2_sp) then ! Region 1 1/2: linear ramp from zero to optimal ctrl%gen_trq = ctrl%vs_slope15*(ctrl%gen_speed_f - ctrl%vs_ct_in_sp) else if (ctrl%gen_speed_f < ctrl%vs_tr_gn_sp) then ! Region 2: optimal torque, quadratic in speed ctrl%gen_trq = ctrl%vs_rgn2_k*ctrl%gen_speed_f*ctrl%gen_speed_f else ! Region 2 1/2: induction-generator transition ctrl%gen_trq = ctrl%vs_slope25*(ctrl%gen_speed_f - ctrl%vs_sy_sp) end if ! Saturate with the torque and torque-rate limits ctrl%gen_trq = min(ctrl%gen_trq, ctrl%vs_max_tq) ! Start the rate limit from the first command, not from zero torque if (ctrl%status == 0) ctrl%last_gen_trq = ctrl%gen_trq trq_rate = (ctrl%gen_trq - ctrl%last_gen_trq)/elap_time trq_rate = min(max(trq_rate, -ctrl%vs_max_rat), ctrl%vs_max_rat) ctrl%gen_trq = ctrl%last_gen_trq + trq_rate*elap_time ctrl%last_time_vs = time ctrl%last_gen_trq = ctrl%gen_trq end if ! Pitch control elap_time = time - ctrl%last_time_pc if (time*one_plus_eps - ctrl%last_time_pc >= ctrl%pc_dt) then ! Gain scheduling on the previously commanded pitch of blade 1 ctrl%gk = 1._dp/(1._dp + ctrl%pit_com(1)/ctrl%pc_kk) ! Speed error and its integral, saturated with the pitch limits spd_err = ctrl%gen_speed_f - ctrl%pc_ref_spd ctrl%int_spd_err = ctrl%int_spd_err + spd_err*elap_time ctrl%int_spd_err = min(max(ctrl%int_spd_err, & ctrl%pc_min_pit/(ctrl%gk*ctrl%pc_ki)), & ctrl%pc_max_pit/(ctrl%gk*ctrl%pc_ki)) pit_com_p = ctrl%gk*ctrl%pc_kp*spd_err pit_com_i = ctrl%gk*ctrl%pc_ki*ctrl%int_spd_err pit_com_t = min(max(pit_com_p + pit_com_i, ctrl%pc_min_pit), & ctrl%pc_max_pit) ! Saturate each blade's command with the pitch-rate limit do k = 1, min(n_blades, max_ctrl_blades) pit_rate = (pit_com_t - ctrl%bl_pitch(k))/elap_time pit_rate = min(max(pit_rate, -ctrl%pc_max_rat), ctrl%pc_max_rat) ctrl%pit_com(k) = ctrl%bl_pitch(k) + pit_rate*elap_time ctrl%bl_pitch(k) = ctrl%pit_com(k) end do ctrl%last_time_pc = time end if ctrl%last_time = time end subroutine operate_controller end module m_alm_controller