Source code for nrel_5mw_controller.torque_controller

"""Implementation of the NREL 5MW wind turbine torque controller.

"""

import numpy as np
import yaml

from .util import saturate


[docs]class TorqueController: """Time-stepping torque controller for the NREL 5MW wind turbine. It expects the following parameters: * ``rated speed``: The generator speed setpoint for the controller * ``rated power``: The generator power setpoint for the controller, for constant power mode * ``slip percent``: Generator slip rate, to calculate the synchronous speed * ``opt constant``: the k coefficient for the optimal speed control region * ``speed filter corner freq``: The frequency of the generator speed filter. * ``cut in speed``: cut in generator speed * ``opt min speed``: minimum generator speed for optimal control (linear ramp between cut in speed and this speed) * ``torque max``: maximum generator torque * ``torque rate limit``: Maximum torque rate limit (up or down) Optional parameters: * ``constant torque``: control for this constant torque above rated, instead of constant power. """ def __init__(self, timestep, params): self.params = params self.timestep = timestep # Calculate maximum optimum-torque speed to achieve slope Qrated = self.params['rated power'] / params['rated speed'] sync_speed = params['rated speed'] / (1 + params['slip percent']/100) slope25 = Qrated / (self.params['rated speed'] - sync_speed) kopt = params['opt constant'] params['opt max speed'] = ( (slope25 - np.sqrt(slope25*(slope25 - 4*kopt*sync_speed))) / (2 * kopt)) # For Hywind: optionally use constant torque instead of constant power self.constant_torque = params.get('constant torque', None) assert self.constant_torque is None or self.constant_torque > 0 # Check values assert params['speed filter corner freq'] > 0 assert timestep > 0 assert params['slip percent'] > 0 assert params['opt constant'] > 0 assert params['torque rate limit'] > 0 assert (0 < params['cut in speed'] < params['opt min speed'] < params['opt max speed'] < params['rated speed']) assert (0 < (kopt * params['rated speed']**2) < Qrated < params['torque max']) self.reset()
[docs] def reset(self): """Reset the controller state.""" # Values from the previous timestep self.last_time = None self.torque_demand = None self.speed_filtered = None
def _optQ(self, speed): return self.params['opt constant'] * speed**2
[docs] def get_torque(self, spd, const_power): Vin = self.params['cut in speed'] Vo1 = self.params['opt min speed'] Vo2 = self.params['opt max speed'] Vrated = self.params['rated speed'] Qrated = self.params['rated power'] / Vrated if spd >= Vrated or const_power: # Region 3 - constant power if spd <= 0: # Needed for harmonic linearisation torque = self.params['torque max'] elif self.constant_torque is not None: torque = self.constant_torque else: torque = self.params['rated power'] / spd elif spd < Vo1: # Region 1 to 1.5 - linear ramp from cut-in to optimal region torque = np.interp(spd, [Vin, Vo1], [0, self._optQ(Vo1)]) elif spd < Vo2: # Region 2 - optimal control torque = self._optQ(spd) else: # Region 2.5 - linear ramp torque = np.interp(spd, [Vo2, Vrated], [self._optQ(Vo2), Qrated]) # Limit to maximum torque torque = saturate(torque, 0, self.params['torque max']) return torque
[docs] def initialise(self, time, measured_speed): """Initialise the controller. Args: time (float): current timestamp measured_speed (float): current measured generator speed """ self.last_time = time - self.timestep self.speed_filtered = measured_speed
[docs] def step(self, time, measured_speed, force_constant_power): """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 force_constant_power (bool): force constant power mode? """ # First run? if self.last_time is None: self.initialise(time, measured_speed) # 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) # Choose the desired torque & limit torque = self.get_torque(self.speed_filtered, force_constant_power) # Saturate the commanded torque using the rate limit if self.torque_demand is not None: rate = saturate((torque - self.torque_demand) / elapsed_time, -self.params['torque rate limit'], +self.params['torque rate limit']) torque = self.torque_demand + rate * elapsed_time self.torque_demand = torque 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['torque controller'])