diff --git a/environment.yml b/environment.yml index fd83ecf7c..e9f0d243e 100644 --- a/environment.yml +++ b/environment.yml @@ -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 @@ -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 diff --git a/examples/04_frequency_domain_analysis_design/iea22_raft_opt_analysis.yaml b/examples/04_frequency_domain_analysis_design/iea22_raft_opt_analysis.yaml index c3c3d8a7d..2880dd234 100644 --- a/examples/04_frequency_domain_analysis_design/iea22_raft_opt_analysis.yaml +++ b/examples/04_frequency_domain_analysis_design/iea22_raft_opt_analysis.yaml @@ -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 @@ -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 diff --git a/weis/control/test/test_tune_rosco.py b/weis/control/test/test_tune_rosco.py new file mode 100644 index 000000000..aa08d3208 --- /dev/null +++ b/weis/control/test/test_tune_rosco.py @@ -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() diff --git a/weis/control/tune_rosco.py b/weis/control/tune_rosco.py index afb157731..83510b004 100644 --- a/weis/control/tune_rosco.py +++ b/weis/control/tune_rosco.py @@ -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') @@ -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 @@ -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