# spawnwind
# Copyright (C) 2018-2019, Simmovation Ltd.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
"""Implementation of :class:`AeroelasticSimulationSpawner` for FAST
"""
import os
from os import path
import copy
from ..spawners import AeroelasticSimulationSpawner
from .tasks import FastSimulationTask
# pylint: disable=too-many-public-methods
[docs]class FastSimulationSpawner(AeroelasticSimulationSpawner):
"""Spawns FAST simulation tasks with wind generation dependency if necessary"""
def __init__(self, fast_input, wind_spawner, prereq_outdir):
"""Initialises :class:`FastSimulationSpawner`
:param fast_input: The FAST input
:type fast_input: :class:`FastInput`
:param wind_spawner: The TurbSim/wind calculation spawner
:type wind_spawner: :class:`TurbsimSpawner`
:param prereq_outdir: The output directory for prerequisites
:type prereq_outdir: path-like
"""
self._input = fast_input
self._wind_spawner = wind_spawner
self._prereq_outdir = prereq_outdir
# non-arguments:
self._wind_input = fast_input.get_wind_input(wind_spawner)
self._aero_input = fast_input.get_aero_input(self._wind_input)
self._elastodyn_input = self._input.get_elastodyn_input()
self._blade_range = list(range(1, self.get_number_of_blades()+1))
self._servodyn_input = self._input.get_servodyn_input(self._blade_range)
# intermediate parameters
self._pitch_manoeuvre_rate = None
self._yaw_manoeuvre_rate = None
# pylint: disable=arguments-differ
[docs] def spawn(self, path_, metadata):
"""Spawn a simulation task
:param path_: The output path for the task
:type path_: str
:param metadata: Metadata to add to the task
:type metadata: dict
:returns: The simulation task
:rtype: :class:`SimulationTask`
"""
if not path.isabs(path_):
raise ValueError('Must provide an absolute path')
if not path.isdir(path_):
os.makedirs(path_)
wind_tasks = self._wind_input.get_wind_gen_tasks(self._prereq_outdir, metadata)
self._write_linked_module_input(self._wind_input, path_)
self._write_linked_module_input(self._aero_input, path_)
self._write_linked_module_input(self._elastodyn_input, path_)
self._write_linked_module_input(self._servodyn_input, path_)
sim_input_file = path.join(path_, 'fast.input')
self._input.to_file(sim_input_file)
sim_task = FastSimulationTask(
'run ' + path_,
_input_file_path=sim_input_file,
_dependencies=wind_tasks,
_metadata=metadata
)
return sim_task
def _write_linked_module_input(self, module, path_):
if hasattr(module, 'key'):
self._input[module.key] = module.to_file(path.join(path_, module.key + '.input'))
[docs] def branch(self):
"""Create a copy of this spawner
:returns: A copy of this spawner with all values equal
:rtype: :class:`FastSimulationSpawner`
"""
branched_spawner = copy.deepcopy(self)
# pylint: disable=protected-access
branched_spawner._input = copy.deepcopy(self._input)
branched_spawner._wind_spawner = self._wind_spawner.branch()
branched_spawner._wind_input.wind_gen_spawner = branched_spawner._wind_spawner
return branched_spawner
# Simulation options
# pylint: disable=missing-docstring
def get_output_start_time(self):
return float(self._input['TStart'])
# pylint: disable=missing-docstring
def set_output_start_time(self, time):
delta = time - self.get_output_start_time()
self._input['TStart'] = time
self._input['TMax'] = float(self._input['TMax']) + delta # Adjust max time so that simulation time is constant
self._wind_spawner.duration = float(self._input['TMax'])
# pylint: disable=missing-docstring
def get_simulation_time(self):
return float(self._input['TMax']) - self.get_output_start_time()
# pylint: disable=missing-docstring
def set_simulation_time(self, time):
total_time = float(time) + self.get_output_start_time()
self._input['TMax'] = float(time) + self.get_output_start_time()
self._wind_spawner.analysis_time = time
self._wind_spawner.duration = total_time
# Initial Conditions
# pylint: disable=missing-docstring
def get_initial_rotor_speed(self):
return float(self._elastodyn_input['RotSpeed'])
# pylint: disable=missing-docstring
def set_initial_rotor_speed(self, rotor_speed):
self._elastodyn_input['RotSpeed'] = rotor_speed
# pylint: disable=missing-docstring
def get_initial_azimuth(self):
return float(self._elastodyn_input['Azimuth'])
# pylint: disable=missing-docstring
def set_initial_azimuth(self, azimuth):
self._elastodyn_input['Azimuth'] = azimuth
# pylint: disable=missing-docstring
def get_initial_yaw(self):
return float(self._elastodyn_input['NacYaw']) # 'YawNeut' could be another possibility here
# pylint: disable=missing-docstring
def set_initial_yaw(self, angle):
self._elastodyn_input['NacYaw'] = angle
# pylint: disable=missing-docstring
def get_initial_pitch(self):
raise NotImplementedError()
# pylint: disable=missing-docstring
def set_initial_pitch(self, angle):
for bld_num in self._blade_range:
self.set_blade_initial_pitch(bld_num, angle)
# pylint: disable=missing-docstring
def get_blade_initial_pitch(self, index):
return float(self._elastodyn_input.get_on_blade('BlPitch', index))
# pylint: disable=missing-docstring
def set_blade_initial_pitch(self, index, angle):
self._elastodyn_input.set_on_blade('BlPitch', index, angle)
self._servodyn_input.reconcile_pitch_manoeuvre(index, angle)
# Supervisory operation
# pylint: disable=missing-docstring
def get_operation_mode(self):
raise NotImplementedError('Incapable of determining operation mode') # this is a tricky one!
# pylint: disable=missing-docstring
def set_operation_mode(self, mode):
if mode not in ['normal', 'idling', 'parked']:
raise ValueError('mode \'' + mode + '\' unrecognised')
# Generator
large_time = self._make_large_time()
if mode == 'normal':
self._servodyn_input['GenTiStr'] = True
self._servodyn_input['TimGenOn'] = 0.0 # time to turn generator on
self._servodyn_input['TimGenOf'] = large_time # never turn generator off
self._free_pitch()
else:
self._servodyn_input['GenTiStr'] = True
self._servodyn_input['TimGenOn'] = large_time # never turn generator on
self._servodyn_input['TimGenOf'] = 0.0 # time to turn generator off
self._fix_pitch()
# rotor freedom
if mode in ['normal', 'idling']:
self._elastodyn_input['GenDOF'] = True
else:
self._elastodyn_input['GenDOF'] = False
if mode in ['idling', 'parked']:
self._servodyn_input['TPCOn'] = large_time # fix pitch by turning pitch control off
self.initial_rotor_speed = 0.0
self.wake_model_on = False # Induction factors should be near zero and the model breaks down
else:
self._servodyn_input['TPCOn'] = 0.0
# pylint: disable=missing-docstring
def get_pitch_manoeuvre_time(self):
raise NotImplementedError('Incapable of determining pitch manoeuvre time for all blades at once')
# pylint: disable=missing-docstring
def set_pitch_manoeuvre_time(self, time):
for bld_num in self._blade_range:
self.set_blade_pitch_manoeuvre_time(bld_num, time)
# pylint: disable=missing-docstring
def get_blade_pitch_manoeuvre_time(self, index):
return self._servodyn_input.get_blade_pitch_manoeuvre_time(index)
# pylint: disable=missing-docstring
def set_blade_pitch_manoeuvre_time(self, index, time):
self._servodyn_input.set_blade_pitch_manoeuvre_time(index, time)
self._servodyn_input.reconcile_pitch_manoeuvre(index, self.get_blade_initial_pitch(index))
# pylint: disable=missing-docstring
def get_pitch_manoeuvre_rate(self):
return self._servodyn_input.pitch_manoeuvre_rate
# pylint: disable=missing-docstring
def set_pitch_manoeuvre_rate(self, pitch_rate):
self._servodyn_input.pitch_manoeuvre_rate = pitch_rate
for bld_num in self._blade_range:
self._servodyn_input.reconcile_pitch_manoeuvre(bld_num, self.get_blade_initial_pitch(bld_num))
# pylint: disable=missing-docstring
def get_final_pitch(self):
return self._servodyn_input.final_pitch
# pylint: disable=missing-docstring
def set_final_pitch(self, angle):
self._servodyn_input.final_pitch = angle
for bld_num in self._blade_range:
self._servodyn_input.reconcile_pitch_manoeuvre(bld_num, self.get_blade_initial_pitch(bld_num))
# pylint: disable=missing-docstring
def get_blade_final_pitch(self, index):
return self._servodyn_input.get_blade_final_pitch(index)
# pylint: disable=missing-docstring
def set_blade_final_pitch(self, index, angle):
self._servodyn_input.set_blade_final_pitch(index, angle)
self._servodyn_input.reconcile_pitch_manoeuvre(index, self.get_blade_initial_pitch(index))
# pylint: disable=missing-docstring
def get_pitch_control_start_time(self):
return self._servodyn_input.pitch_control_start_time
# pylint: disable=missing-docstring
def set_pitch_control_start_time(self, time):
self._servodyn_input.pitch_control_start_time = time
# pylint: disable=missing-docstring
def get_yaw_manoeuvre_time(self):
return self._servodyn_input.yaw_manoeuvre_time
# pylint: disable=missing-docstring
def set_yaw_manoeuvre_time(self, time):
self._servodyn_input.yaw_manoeuvre_time = time
# pylint: disable=missing-docstring
def get_yaw_manoeuvre_rate(self):
return self._servodyn_input.yaw_manoeuvre_rate
# pylint: disable=missing-docstring
def set_yaw_manoeuvre_rate(self, rate):
self._servodyn_input.yaw_manoeuvre_rate = rate
self._servodyn_input.reconcile_yaw_manoeuvre(self.initial_yaw)
# pylint: disable=missing-docstring
def get_final_yaw(self):
return self._servodyn_input.final_yaw
# pylint: disable=missing-docstring
def set_final_yaw(self, angle):
self._servodyn_input.final_yaw = angle
self._servodyn_input.reconcile_yaw_manoeuvre(self.initial_yaw)
# Turbine faults
# pylint: disable=missing-docstring
def get_grid_loss_time(self):
return float(self._servodyn_input['TimGenOf'])
# pylint: disable=missing-docstring
def set_grid_loss_time(self, time):
self._servodyn_input['GenTiStp'] = True
self._servodyn_input['TimGenOf'] = time
# Properties deferred to wind input:
# pylint: disable=missing-docstring
def get_wind_type(self):
return self._wind_input.wind_type
# pylint: disable=missing-docstring
def set_wind_type(self, wind_type):
self._wind_input.wind_type = wind_type
# pylint: disable=missing-docstring
def get_wind_speed(self):
return self._wind_input.wind_speed
# pylint: disable=missing-docstring
def set_wind_speed(self, speed):
self._wind_input.wind_speed = speed
# pylint: disable=missing-docstring
def get_turbulence_intensity(self):
return self._wind_input.turbulence_intensity
# pylint: disable=missing-docstring
def set_turbulence_intensity(self, turbulence_intensity):
self._wind_input.turbulence_intensity = turbulence_intensity
# pylint: disable=missing-docstring
def get_turbulence_seed(self):
return self._wind_input.turbulence_seed
# pylint: disable=missing-docstring
def set_turbulence_seed(self, seed):
self._wind_input.turbulence_seed = seed
# pylint: disable=missing-docstring
def get_wind_shear(self):
return self._wind_input.wind_shear
# pylint: disable=missing-docstring
def set_wind_shear(self, exponent):
self._wind_input.wind_shear = exponent
# pylint: disable=missing-docstring
def get_upflow(self):
return self._wind_input.upflow
# pylint: disable=missing-docstring
def set_upflow(self, angle):
self._wind_input.upflow = angle
# pylint: disable=missing-docstring
def get_wind_file(self):
return self._wind_input.wind_file
# pylint: disable=missing-docstring
def set_wind_file(self, file):
self._wind_input.wind_file = os.path.abspath(file)
# pylint: disable=missing-docstring
def get_wake_model_on(self):
return self._aero_input.wake_model_on
# pylint: disable=missing-docstring
def set_wake_model_on(self, on):
self._aero_input.wake_model_on = on
# pylint: disable=missing-docstring
def get_dynamic_stall_on(self):
return self._aero_input.dynamic_stall_on
# pylint: disable=missing-docstring
def set_dynamic_stall_on(self, on):
self._aero_input.dynamic_stall_on = on
# Properties of turbine, for which setting is not supported
def get_number_of_blades(self):
return int(self._elastodyn_input['NumBl'])
# non-properties
def _fix_pitch(self, pitch_angle=None):
if pitch_angle is not None:
self.initial_pitch = pitch_angle
self._servodyn_input.pitch_manoeuvre_rate = 0.0
for bld_num in self._blade_range:
self._servodyn_input.set_blade_final_pitch(bld_num, self.get_blade_initial_pitch(bld_num))
self._servodyn_input.reconcile_pitch_manoeuvre(bld_num, self.get_blade_initial_pitch(bld_num))
def _free_pitch(self):
large_time = self._make_large_time()
for bld_num in self._blade_range:
self._servodyn_input.set_blade_pitch_manoeuvre_time(bld_num, large_time)
def _make_large_time(self):
return max(9999.9, float(self._input['TMax']) + 1.0)