Source code for nrel_5mw_controller.pitch_controller
"""Implementation of the pitch controller for the NREL 5MW wind turbine
controller.
"""
import numpy as np
import yaml
from .util import saturate
[docs]class PitchController:
"""Time-stepping pitch controller for the NREL 5MW wind turbine.
It expects the following parameters:
* ``proportional gain``: Proportional gain of the PI controller
* ``integral gain``: Integral gain of the PI controller
* ``pitch schedule doubled angle``: This is the angle at which the pitch
controller gain is halved.
* ``pitch angle min``: Minimum pitch angle limit
* ``pitch angle max``: Maximum pitch angle limit
* ``pitch rate limit``: Maximum pitch rate limit (up or down)
* ``rated speed``: The generator speed setpoint for the controller
* ``speed filter corner freq``: The frequency of the generator speed filter.
"""
def __init__(self, timestep, params):
self.params = params
self.timestep = timestep
self.reset()
[docs] def reset(self):
"""Reset the controller state."""
# Values from the previous timestep
self.last_time = None
self.speed_error_int = None
self.pitch_demand = None
self.speed_filtered = None
[docs] def get_scheduled_gain(self, pitch):
"""Calculate the gain schedule factor."""
GK = 1.0 / (1.0 + pitch / self.params['pitch schedule doubled angle'])
return GK
[docs] def initialise(self, time, measured_speed, measured_pitch):
"""Initialise the controller.
Args:
time (float): current timestamp
measured_speed (float): current measured generator speed
measured_pitch (float): current measured pitch angle
"""
self.last_time = time - self.timestep
self.speed_filtered = measured_speed
self.pitch_demand = measured_pitch
# Initialise integral speed error. This will ensure that the
# pitch angle is unchanged if the initial speed_error is zero
GK = self.get_scheduled_gain(measured_pitch)
self.speed_error_int = (measured_pitch /
(GK * self.params['integral gain']))
[docs] def get_pitch_demand(self, speed_error, speed_error_int, GK):
# Compute the pitch commands associated with the proportional
# and integral gains:
demand_p = GK * self.params['proportional gain'] * speed_error
demand_i = GK * self.params['integral gain'] * speed_error_int
# Superimpose the individual commands to get the total pitch command;
# saturate the overall command using the pitch angle limits:
demand = saturate(demand_p + demand_i,
self.params['pitch angle min'],
self.params['pitch angle max'])
return demand
[docs] def step(self, time, measured_speed, measured_pitch):
"""Step the controller forwards to the next timestep.
This is the main method of the controller class.
Args:
time (float): the current timestamp
measured_speed (float): current measured generator speed
measured_pitch (float): current measured pitch angle
"""
# First run?
if self.last_time is None:
self.initialise(time, measured_speed, measured_pitch)
# Check if enough time has elapsed
elapsed_time = time - self.last_time
if elapsed_time < self.timestep:
return
# Update filtered speed
alpha = np.exp(-elapsed_time * self.params['speed filter corner freq'])
self.speed_filtered = ((1 - alpha) * measured_speed +
alpha * self.speed_filtered)
# Compute the current speed error and its integral
# w.r.t. time; saturate the integral term using the pitch
# angle limits:
GK = self.get_scheduled_gain(self.pitch_demand)
speed_error = self.speed_filtered - self.params['rated speed']
self.speed_error_int += (speed_error * elapsed_time)
self.speed_error_int = saturate(
self.speed_error_int,
self.params['pitch angle min'] / (GK*self.params['integral gain']),
self.params['pitch angle max'] / (GK*self.params['integral gain']))
# Saturate the overall commanded pitch using the pitch rate limit:
demand = self.get_pitch_demand(speed_error, self.speed_error_int, GK)
pitch_rate = saturate((demand - measured_pitch) / elapsed_time,
-self.params['pitch rate limit'],
+self.params['pitch rate limit'])
self.pitch_demand = measured_pitch + pitch_rate * elapsed_time
self.last_time = time
[docs] @classmethod
def from_yaml(cls, filename):
"""Read controller params from 'controller' section of YAML file"""
with open(filename) as f:
config = yaml.safe_load(f)
c = config['controller']
return cls(c['timestep'], c['pitch controller'])