Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions environment.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ dependencies:
- openfast-io==4.2
- pyopenfast==4.2
- openraft>=2.0.3
- openmdao
- openmdao~=3.44.0
- osqp
- pcrunch>=2.1.5
- pip
Expand All @@ -28,7 +28,7 @@ dependencies:
- pyvista
- rosco>=2.10.1
- trimesh
- wisdem>=4.2.5
- wisdem~=4.2.5 # tight coupling to WISDEM's OM output names; unpin when actively developing
- pip:
- control
- dash-vtk
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,14 +77,6 @@ constraints:
flag: True
buoyancy:
flag: False
stress:
flag: False
global_buckling:
flag: False
shell_buckling:
flag: False
mooring_heel:
flag: False
freeboard_margin: # keep freeboard from being submerged below water during survival_heel, largest wave
flag: True
draft_margin: # keep draft from raising above water line during survival_heel, largest wave
Expand All @@ -95,9 +87,6 @@ constraints:
Max_PtfmPitch:
flag: True
max: 6.0
Std_PtfmPitch:
flag: False
max: 1.25 # Same as IEA-15MW with same DLCs
nacelle_acceleration:
flag: True
max: 2.0
Expand Down
83 changes: 83 additions & 0 deletions weis/control/test/test_tune_rosco.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
"""
Unit tests for weis.control.tune_rosco.resolve_tsr_operational

Regression test for a bug where setting 'TSR_operational' in rosco_tuning_inputs
(passed through to modeling_options['ROSCO']) had no effect on the TSR used to
tune ROSCO / write VS_TSRopt to DISCON.IN, because TuneROSCO.compute() only ever
read the WISDEM-provided tsr_operational input.
"""

import unittest

import numpy as np
from rosco.toolbox.turbine import RotorPerformance

from weis.control.tune_rosco import resolve_tsr_operational


def make_cp_surface():
# Small synthetic Cp(TSR, pitch) surface with an unambiguous maximum
# at TSR = 7.0, pitch = 0 rad.
pitch_initial_rad = np.deg2rad(np.array([0.0, 5.0, 10.0]))
tsr_initial = np.array([5.0, 6.0, 7.0, 8.0, 9.0])
performance_table = np.array([
[0.30, 0.20, 0.10],
[0.40, 0.30, 0.20],
[0.48, 0.35, 0.22], # max at TSR=7.0, pitch=0
[0.42, 0.32, 0.21],
[0.33, 0.25, 0.15],
])
return RotorPerformance(performance_table, pitch_initial_rad, tsr_initial)


class TestResolveTSROperational(unittest.TestCase):
def setUp(self):
self.Cp = make_cp_surface()
self.rated_rotor_speed = 1.0 # rad/s
self.rotor_radius = 50.0 # m
self.tsr_operational_default = 6.0 # e.g. WISDEM's control.rated_TSR

def test_auto_compute_from_cp_surface(self):
# TSR_operational = 0 should auto-compute from the Cp surface (Cp.TSR_opt)
rosco_init_options = {'TSR_operational': 0}
tsr, v_rated = resolve_tsr_operational(
rosco_init_options,
self.tsr_operational_default,
self.rated_rotor_speed,
self.rotor_radius,
self.Cp,
)
self.assertEqual(tsr, self.Cp.TSR_opt)
self.assertEqual(tsr, 7.0)
self.assertAlmostEqual(v_rated, self.rated_rotor_speed * self.rotor_radius / tsr)

def test_explicit_override(self):
# A positive TSR_operational should be used directly, overriding the WISDEM default
rosco_init_options = {'TSR_operational': 8.5}
tsr, v_rated = resolve_tsr_operational(
rosco_init_options,
self.tsr_operational_default,
self.rated_rotor_speed,
self.rotor_radius,
self.Cp,
)
self.assertEqual(tsr, 8.5)
self.assertAlmostEqual(v_rated, self.rated_rotor_speed * self.rotor_radius / 8.5)

def test_default_unchanged_when_key_absent(self):
# If 'TSR_operational' is not in rosco_init_options, the WISDEM-provided
# default should pass through unchanged (pre-existing behavior).
rosco_init_options = {}
tsr, v_rated = resolve_tsr_operational(
rosco_init_options,
self.tsr_operational_default,
self.rated_rotor_speed,
self.rotor_radius,
self.Cp,
)
self.assertEqual(tsr, self.tsr_operational_default)
self.assertAlmostEqual(v_rated, self.rated_rotor_speed * self.rotor_radius / self.tsr_operational_default)


if __name__ == "__main__":
unittest.main()
57 changes: 55 additions & 2 deletions weis/control/tune_rosco.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,49 @@
logger = logging.getLogger("wisdem/weis")


def resolve_tsr_operational(rosco_init_options, tsr_operational_default, rated_rotor_speed, rotor_radius, Cp):
'''
Resolve the Region 2 TSR_operational (and dependent rated wind speed) used to tune ROSCO.

If 'TSR_operational' is present in rosco_init_options (i.e. set via rosco_tuning_inputs),
it takes precedence over tsr_operational_default (WISDEM's control.rated_TSR / optimal_tsr):
- 0 triggers auto-computation from the Cp surface (Cp.TSR_opt), matching standalone
ROSCO's convention in rosco.toolbox.turbine.Turbine.
- A positive value is used directly as an override.
If the key is absent, tsr_operational_default is returned unchanged.

Parameters
----------
rosco_init_options : dict
modeling_options['ROSCO'], may optionally contain 'TSR_operational'
tsr_operational_default : float
Default operational TSR, usually from WISDEM's control.rated_TSR
rated_rotor_speed : float
Rated rotor speed, rad/s
rotor_radius : float
Rotor radius, m
Cp : rosco.toolbox.turbine.RotorPerformance
Power coefficient surface, used for auto-computation of TSR_operational

Returns
-------
tsr_operational : float
v_rated : float
'''
tsr_operational = tsr_operational_default
if 'TSR_operational' in rosco_init_options:
tsr_override = float(rosco_init_options['TSR_operational'])
if tsr_override == 0:
tsr_operational = Cp.TSR_opt
logger.info(f"TSR_operational auto-computed from Cp surface: {tsr_operational:.4f}")
elif tsr_override > 0:
tsr_operational = tsr_override
logger.info(f"TSR_operational overridden by ROSCO tuning input: {tsr_override:.4f}")

v_rated = rated_rotor_speed * rotor_radius / tsr_operational
return tsr_operational, v_rated


class ServoSE_ROSCO(Group):
def initialize(self):
self.options.declare('modeling_options')
Expand Down Expand Up @@ -288,12 +331,12 @@ def compute(self,inputs,outputs, discrete_inputs, discrete_outputs):
WISDEM_turbine.rated_power = float(inputs['rated_power'][0])
WISDEM_turbine.rated_torque = float(inputs['rated_torque'][0]) / WISDEM_turbine.Ng * float(inputs['gearbox_efficiency'][0])
WISDEM_turbine.max_torque = WISDEM_turbine.rated_torque * 1.1 # TODO: make this an input if studying constant power
WISDEM_turbine.v_rated = float(inputs['rated_rotor_speed'][0])*float(inputs['R'][0]) / float(inputs['tsr_operational'][0])
WISDEM_turbine.v_min = float(inputs['v_min'][0])
WISDEM_turbine.v_max = float(inputs['v_max'][0])
WISDEM_turbine.max_pitch_rate = float(inputs['max_pitch_rate'][0])
WISDEM_turbine.min_pitch_rate = -float(inputs['max_pitch_rate'][0])
WISDEM_turbine.TSR_operational = float(inputs['tsr_operational'][0])
# TSR_operational and v_rated are finalized below, once the Cp surface is available
# (see resolve_tsr_operational), since ROSCO tuning inputs may override the WISDEM value.
WISDEM_turbine.TowerHt = float(inputs['TowerHt'][0])
WISDEM_turbine.bld_edgewise_freq = float(inputs['edge_freq'][0]) * 2 * np.pi

Expand All @@ -320,6 +363,16 @@ def compute(self,inputs,outputs, discrete_inputs, discrete_outputs):
WISDEM_turbine.Ct = RotorPerformance(self.Ct_table,self.pitch_vector,self.tsr_vector)
WISDEM_turbine.Cq = RotorPerformance(self.Cq_table,self.pitch_vector,self.tsr_vector)

# Resolve TSR_operational (and dependent v_rated), allowing ROSCO tuning inputs
# (rosco_init_options['TSR_operational']) to override the WISDEM value.
WISDEM_turbine.TSR_operational, WISDEM_turbine.v_rated = resolve_tsr_operational(
rosco_init_options,
float(inputs['tsr_operational'][0]),
WISDEM_turbine.rated_rotor_speed,
WISDEM_turbine.rotor_radius,
WISDEM_turbine.Cp,
)

# Load blade info to pass to flap controller tuning process
if rosco_init_options['Flp_Mode'] >= 1:
# Create airfoils
Expand Down
Loading