• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

WISDEM / WEIS / 12283078613

11 Dec 2024 06:59PM UTC coverage: 78.249% (-0.6%) from 78.802%
12283078613

Pull #308

github

dzalkind
Merge remote-tracking branch 'origin/DLC_RefactorCaseInputs' into DLC_RefactorCaseInputs
Pull Request #308: DLC Generation - Refactor and New Cases

446 of 665 new or added lines in 9 files covered. (67.07%)

3 existing lines in 3 files now uncovered.

21416 of 27369 relevant lines covered (78.25%)

0.78 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

83.09
/weis/aeroelasticse/openmdao_openfast.py
1
import numpy as np
1✔
2
import pandas as pd
1✔
3
import os
1✔
4
import shutil
1✔
5
import sys
1✔
6
import copy
1✔
7
import glob
1✔
8
import logging
1✔
9
import pickle
1✔
10
from pathlib import Path
1✔
11
from scipy.interpolate                      import PchipInterpolator
1✔
12
from openmdao.api                           import ExplicitComponent
1✔
13
from openmdao.utils.mpi import MPI
1✔
14
from wisdem.commonse import NFREQ
1✔
15
from wisdem.commonse.cylinder_member import get_nfull
1✔
16
import wisdem.commonse.utilities              as util
1✔
17
from wisdem.rotorse.rotor_power             import eval_unsteady
1✔
18
from weis.aeroelasticse.FAST_writer         import InputWriter_OpenFAST
1✔
19
from weis.aeroelasticse.FAST_reader         import InputReader_OpenFAST
1✔
20
import weis.aeroelasticse.runFAST_pywrapper as fastwrap
1✔
21
from weis.aeroelasticse.FAST_post         import FAST_IO_timeseries
1✔
22
from wisdem.floatingse.floating_frame import NULL, NNODES_MAX, NELEM_MAX
1✔
23
from weis.dlc_driver.dlc_generator    import DLCGenerator
1✔
24
from weis.aeroelasticse.CaseGen_General import CaseGen_General
1✔
25
from functools import partial
1✔
26
from pCrunch import PowerProduction
1✔
27
from weis.aeroelasticse.LinearFAST import LinearFAST
1✔
28
from weis.control.LinearModel import LinearTurbineModel, LinearControlModel
1✔
29
from weis.aeroelasticse import FileTools
1✔
30
from weis.aeroelasticse.turbsim_file   import TurbSimFile
1✔
31
from weis.aeroelasticse.turbsim_util import generate_wind_files
1✔
32
from weis.aeroelasticse.utils import OLAFParams
1✔
33
from rosco.toolbox import control_interface as ROSCO_ci
1✔
34
from pCrunch.io import OpenFASTOutput
1✔
35
from pCrunch import LoadsAnalysis, PowerProduction, FatigueParams
1✔
36
from weis.control.dtqp_wrapper          import dtqp_wrapper
1✔
37
from weis.aeroelasticse.StC_defaults        import default_StC_vt
1✔
38
from weis.aeroelasticse.CaseGen_General import case_naming
1✔
39
from wisdem.inputs import load_yaml, write_yaml
1✔
40

41
logger = logging.getLogger("wisdem/weis")
1✔
42

43
weis_dir = os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
1✔
44

45
def make_coarse_grid(s_grid, diam):
1✔
46

47
    s_coarse = [s_grid[0]]
1✔
48
    slope = np.diff(diam) / np.diff(s_grid)
1✔
49
    for k in range(slope.size-1):
1✔
50
        if np.abs(slope[k]-slope[k+1]) > 1e-2:
1✔
51
            s_coarse.append(s_grid[k+1])
×
52
    s_coarse.append(s_grid[-1])
1✔
53
    return np.array(s_coarse)
1✔
54

55
    
56
class FASTLoadCases(ExplicitComponent):
1✔
57
    def initialize(self):
1✔
58
        self.options.declare('modeling_options')
1✔
59
        self.options.declare('opt_options')
1✔
60

61
    def setup(self):
1✔
62
        modopt = self.options['modeling_options']
1✔
63
        rotorse_options  = modopt['WISDEM']['RotorSE']
1✔
64
        mat_init_options = modopt['materials']
1✔
65

66
        self.n_blades      = modopt['assembly']['number_of_blades']
1✔
67
        self.n_span        = n_span    = rotorse_options['n_span']
1✔
68
        self.n_pc          = n_pc      = rotorse_options['n_pc']
1✔
69

70
        # Environmental Conditions needed regardless of where model comes from
71
        self.add_input('V_cutin',     val=0.0, units='m/s',      desc='Minimum wind speed where turbine operates (cut-in)')
1✔
72
        self.add_input('V_cutout',    val=0.0, units='m/s',      desc='Maximum wind speed where turbine operates (cut-out)')
1✔
73
        self.add_input('Vrated',      val=0.0, units='m/s',      desc='rated wind speed')
1✔
74
        self.add_input('hub_height',                val=0.0, units='m', desc='hub height')
1✔
75
        self.add_discrete_input('turbulence_class', val='A', desc='IEC turbulence class')
1✔
76
        self.add_discrete_input('turbine_class',    val='I', desc='IEC turbine class')
1✔
77
        self.add_input('Rtip',              val=0.0, units='m', desc='dimensional radius of tip')
1✔
78
        self.add_input('shearExp',    val=0.0,                   desc='shear exponent')
1✔
79

80
        if not self.options['modeling_options']['Level3']['from_openfast']:
1✔
81
            self.n_pitch       = n_pitch   = rotorse_options['n_pitch_perf_surfaces']
1✔
82
            self.n_tsr         = n_tsr     = rotorse_options['n_tsr_perf_surfaces']
1✔
83
            self.n_U           = n_U       = rotorse_options['n_U_perf_surfaces']
1✔
84
            self.n_mat         = n_mat    = mat_init_options['n_mat']
1✔
85
            self.n_layers      = n_layers = rotorse_options['n_layers']
1✔
86

87
            self.n_xy          = n_xy      = rotorse_options['n_xy'] # Number of coordinate points to describe the airfoil geometry
1✔
88
            self.n_aoa         = n_aoa     = rotorse_options['n_aoa']# Number of angle of attacks
1✔
89
            self.n_Re          = n_Re      = rotorse_options['n_Re'] # Number of Reynolds, so far hard set at 1
1✔
90
            self.n_tab         = n_tab     = rotorse_options['n_tab']# Number of tabulated data. For distributed aerodynamic control this could be > 1
1✔
91
            
92
            self.te_ss_var       = rotorse_options['te_ss']
1✔
93
            self.te_ps_var       = rotorse_options['te_ps']
1✔
94
            self.spar_cap_ss_var = rotorse_options['spar_cap_ss']
1✔
95
            self.spar_cap_ps_var = rotorse_options['spar_cap_ps']
1✔
96

97
            n_freq_blade = int(rotorse_options['n_freq']/2)
1✔
98
            n_pc         = int(rotorse_options['n_pc'])
1✔
99

100
            self.n_xy          = n_xy      = rotorse_options['n_xy'] # Number of coordinate points to describe the airfoil geometry
1✔
101
            self.n_aoa         = n_aoa     = rotorse_options['n_aoa']# Number of angle of attacks
1✔
102
            self.n_Re          = n_Re      = rotorse_options['n_Re'] # Number of Reynolds, so far hard set at 1
1✔
103
            self.n_tab         = n_tab     = rotorse_options['n_tab']# Number of tabulated data. For distributed aerodynamic control this could be > 1
1✔
104

105
            self.te_ss_var       = rotorse_options['te_ss']
1✔
106
            self.te_ps_var       = rotorse_options['te_ps']
1✔
107
            self.spar_cap_ss_var = rotorse_options['spar_cap_ss']
1✔
108
            self.spar_cap_ps_var = rotorse_options['spar_cap_ps']
1✔
109

110
            # ElastoDyn Inputs
111
            # Assuming the blade modal damping to be unchanged. Cannot directly solve from the Rayleigh Damping without making assumptions. J.Jonkman recommends 2-3% https://wind.nrel.gov/forum/wind/viewtopic.php?t=522
112
            self.add_input('r',                     val=np.zeros(n_span), units='m', desc='radial positions. r[0] should be the hub location \
1✔
113
                while r[-1] should be the blade tip. Any number \
114
                of locations can be specified between these in ascending order.')
115
            self.add_input('le_location',           val=np.zeros(n_span), desc='Leading-edge positions from a reference blade axis (usually blade pitch axis). Locations are normalized by the local chord length. Positive in -x direction for airfoil-aligned coordinate system')
1✔
116
            self.add_input('beam:rhoA',             val=np.zeros(n_span), units='kg/m', desc='mass per unit length')
1✔
117
            self.add_input('beam:EIyy',             val=np.zeros(n_span), units='N*m**2', desc='flatwise stiffness (bending about y-direction of airfoil aligned coordinate system)')
1✔
118
            self.add_input('beam:EIxx',             val=np.zeros(n_span), units='N*m**2', desc='edgewise stiffness (bending about :ref:`x-direction of airfoil aligned coordinate system <blade_airfoil_coord>`)')
1✔
119
            self.add_input('x_tc',                  val=np.zeros(n_span), units='m',      desc='x-distance to the neutral axis (torsion center)')
1✔
120
            self.add_input('y_tc',                  val=np.zeros(n_span), units='m',      desc='y-distance to the neutral axis (torsion center)')
1✔
121
            self.add_input('flap_mode_shapes',      val=np.zeros((n_freq_blade,5)), desc='6-degree polynomial coefficients of mode shapes in the flap direction (x^2..x^6, no linear or constant term)')
1✔
122
            self.add_input('edge_mode_shapes',      val=np.zeros((n_freq_blade,5)), desc='6-degree polynomial coefficients of mode shapes in the edge direction (x^2..x^6, no linear or constant term)')
1✔
123
            self.add_input('gearbox_efficiency',    val=1.0,               desc='Gearbox efficiency')
1✔
124
            self.add_input('gearbox_ratio',         val=1.0,               desc='Gearbox ratio')
1✔
125
            self.add_input('platform_displacement', val=1.0,               desc='Volumetric platform displacement', units='m**3')
1✔
126

127
            # ServoDyn Inputs
128
            self.add_input('generator_efficiency',   val=1.0,              desc='Generator efficiency')
1✔
129
            self.add_input('max_pitch_rate',         val=0.0,        units='deg/s',          desc='Maximum allowed blade pitch rate')
1✔
130

131
            # StC or TMD inputs; structural control and tuned mass dampers
132

133
            # tower properties
134
            n_height_tow = modopt['WISDEM']['TowerSE']['n_height']
1✔
135
            n_full_tow   = get_nfull(n_height_tow, nref=modopt['WISDEM']['TowerSE']['n_refine'])
1✔
136
            n_freq_tower = int(NFREQ/2)
1✔
137
            self.add_input('fore_aft_modes',   val=np.zeros((n_freq_tower,5)),               desc='6-degree polynomial coefficients of mode shapes in the flap direction (x^2..x^6, no linear or constant term)')
1✔
138
            self.add_input('side_side_modes',  val=np.zeros((n_freq_tower,5)),               desc='6-degree polynomial coefficients of mode shapes in the edge direction (x^2..x^6, no linear or constant term)')
1✔
139
            self.add_input('mass_den',         val=np.zeros(n_height_tow-1),         units='kg/m',   desc='sectional mass per unit length')
1✔
140
            self.add_input('foreaft_stff',     val=np.zeros(n_height_tow-1),         units='N*m**2', desc='sectional fore-aft bending stiffness per unit length about the Y_E elastic axis')
1✔
141
            self.add_input('sideside_stff',    val=np.zeros(n_height_tow-1),         units='N*m**2', desc='sectional side-side bending stiffness per unit length about the Y_E elastic axis')
1✔
142
            self.add_input('tor_stff',    val=np.zeros(n_height_tow-1),         units='N*m**2', desc='torsional stiffness per unit length about the Y_E elastic axis')
1✔
143
            self.add_input('tor_freq',    val=0.0,         units='Hz', desc='First tower torsional frequency')
1✔
144
            self.add_input('tower_outer_diameter', val=np.zeros(n_height_tow),   units='m',      desc='cylinder diameter at corresponding locations')
1✔
145
            self.add_input('tower_z', val=np.zeros(n_height_tow),   units='m',      desc='z-coordinates of tower and monopile used in TowerSE')
1✔
146
            self.add_input('tower_z_full', val=np.zeros(n_full_tow),   units='m',      desc='z-coordinates of tower and monopile used in TowerSE')
1✔
147
            self.add_input('tower_height',              val=0.0, units='m', desc='tower height from the tower base')
1✔
148
            self.add_input('tower_base_height',         val=0.0, units='m', desc='tower base height from the ground or mean sea level')
1✔
149
            self.add_input('tower_cd',         val=np.zeros(n_height_tow),                   desc='drag coefficients along tower height at corresponding locations')
1✔
150
            self.add_input("tower_I_base", np.zeros(6), units="kg*m**2", desc="tower moments of inertia at the tower base")
1✔
151

152
            # These next ones are needed for SubDyn
153
            n_height_mon = n_full_mon = 0
1✔
154
            if modopt['flags']['offshore']:
1✔
155
                self.add_input('transition_piece_mass', val=0.0, units='kg')
1✔
156
                self.add_input('transition_piece_I', val=np.zeros(3), units='kg*m**2')
1✔
157
                
158
                if modopt['flags']['monopile']:
1✔
159
                    n_height_mon = modopt['WISDEM']['FixedBottomSE']['n_height']
1✔
160
                    n_full_mon   = get_nfull(n_height_mon, nref=modopt['WISDEM']['FixedBottomSE']['n_refine'])
1✔
161
                    self.add_input('monopile_z', val=np.zeros(n_height_mon),   units='m',      desc='z-coordinates of tower and monopile used in TowerSE')
1✔
162
                    self.add_input('monopile_z_full', val=np.zeros(n_full_mon),   units='m',      desc='z-coordinates of tower and monopile used in TowerSE')
1✔
163
                    self.add_input('monopile_outer_diameter', val=np.zeros(n_height_mon),   units='m',      desc='cylinder diameter at corresponding locations')
1✔
164
                    self.add_input('monopile_wall_thickness', val=np.zeros(n_height_mon-1), units='m')
1✔
165
                    self.add_input('monopile_E', val=np.zeros(n_height_mon-1), units='Pa')
1✔
166
                    self.add_input('monopile_G', val=np.zeros(n_height_mon-1), units='Pa')
1✔
167
                    self.add_input('monopile_rho', val=np.zeros(n_height_mon-1), units='kg/m**3')
1✔
168
                    self.add_input('gravity_foundation_mass', val=0.0, units='kg')
1✔
169
                    self.add_input('gravity_foundation_I', val=np.zeros(3), units='kg*m**2')
1✔
170
            monlen = max(0, n_height_mon-1)
1✔
171
            monlen_full = max(0, n_full_mon-1)
1✔
172

173
            # DriveSE quantities
174
            self.add_input('hub_system_cm',   val=np.zeros(3),             units='m',  desc='center of mass of the hub relative to tower to in yaw-aligned c.s.')
1✔
175
            self.add_input('hub_system_I',    val=np.zeros(6),             units='kg*m**2', desc='mass moments of Inertia of hub [Ixx, Iyy, Izz, Ixy, Ixz, Iyz] around its center of mass in yaw-aligned c.s.')
1✔
176
            self.add_input('hub_system_mass', val=0.0,                     units='kg', desc='mass of hub system')
1✔
177
            self.add_input('above_yaw_mass',  val=0.0, units='kg', desc='Mass of the nacelle above the yaw system')
1✔
178
            self.add_input('yaw_mass',        val=0.0, units='kg', desc='Mass of yaw system')
1✔
179
            self.add_input('rna_I_TT',       val=np.zeros(6), units='kg*m**2', desc=' moments of Inertia for the rna [Ixx, Iyy, Izz, Ixy, Ixz, Iyz] about the tower top')
1✔
180
            self.add_input('nacelle_cm',      val=np.zeros(3), units='m', desc='Center of mass of the component in [x,y,z] for an arbitrary coordinate system')
1✔
181
            self.add_input('nacelle_I_TT',       val=np.zeros(6), units='kg*m**2', desc=' moments of Inertia for the nacelle [Ixx, Iyy, Izz, Ixy, Ixz, Iyz] about the tower top')
1✔
182
            self.add_input('distance_tt_hub', val=0.0,         units='m',   desc='Vertical distance from tower top plane to hub flange')
1✔
183
            self.add_input('twr2shaft',       val=0.0,         units='m',   desc='Vertical distance from tower top plane to shaft start')
1✔
184
            self.add_input('GenIner',         val=0.0,         units='kg*m**2',   desc='Moments of inertia for the generator about high speed shaft')
1✔
185
            self.add_input('drivetrain_spring_constant',         val=0.0,         units='N*m/rad',   desc='Moments of inertia for the generator about high speed shaft')
1✔
186
            self.add_input("drivetrain_damping_coefficient", 0.0, units="N*m*s/rad", desc='Equivalent damping coefficient for the drivetrain system')
1✔
187

188
            # AeroDyn Inputs
189
            self.add_input('ref_axis_blade',    val=np.zeros((n_span,3)),units='m',   desc='2D array of the coordinates (x,y,z) of the blade reference axis, defined along blade span. The coordinate system is the one of BeamDyn: it is placed at blade root with x pointing the suction side of the blade, y pointing the trailing edge and z along the blade span. A standard configuration will have negative x values (prebend), if swept positive y values, and positive z values.')
1✔
190
            self.add_input('chord',             val=np.zeros(n_span), units='m', desc='chord at airfoil locations')
1✔
191
            self.add_input('theta',             val=np.zeros(n_span), units='deg', desc='twist at airfoil locations')
1✔
192
            self.add_input('rthick',            val=np.zeros(n_span), desc='relative thickness of airfoil distribution')
1✔
193
            self.add_input('ac',                val=np.zeros(n_span), desc='aerodynamic center of airfoil distribution')
1✔
194
            self.add_input('pitch_axis',        val=np.zeros(n_span), desc='1D array of the chordwise position of the pitch axis (0-LE, 1-TE), defined along blade span.')
1✔
195
            self.add_input('Rhub',              val=0.0, units='m', desc='dimensional radius of hub')
1✔
196
            self.add_input('airfoils_cl',       val=np.zeros((n_span, n_aoa, n_Re, n_tab)), desc='lift coefficients, spanwise')
1✔
197
            self.add_input('airfoils_cd',       val=np.zeros((n_span, n_aoa, n_Re, n_tab)), desc='drag coefficients, spanwise')
1✔
198
            self.add_input('airfoils_cm',       val=np.zeros((n_span, n_aoa, n_Re, n_tab)), desc='moment coefficients, spanwise')
1✔
199
            self.add_input('airfoils_aoa',      val=np.zeros((n_aoa)), units='deg', desc='angle of attack grid for polars')
1✔
200
            self.add_input('airfoils_Re',       val=np.zeros((n_Re)), desc='Reynolds numbers of polars')
1✔
201
            self.add_input('airfoils_Ctrl',     val=np.zeros((n_span, n_Re, n_tab)), units='deg',desc='Airfoil control paremeter (i.e. flap angle)')
1✔
202

203
            # Airfoil coordinates
204
            self.add_input('coord_xy_interp',   val=np.zeros((n_span, n_xy, 2)),              desc='3D array of the non-dimensional x and y airfoil coordinates of the airfoils interpolated along span for n_span stations. The leading edge is place at x=0 and y=0.')
1✔
205

206
            # Floating platform inputs
207
            self.add_input("transition_node", np.zeros(3), units="m")
1✔
208
            self.add_input("platform_nodes", NULL * np.ones((NNODES_MAX, 3)), units="m")
1✔
209
            self.add_input("platform_elem_n1", NULL * np.ones(NELEM_MAX, dtype=np.int_))
1✔
210
            self.add_input("platform_elem_n2", NULL * np.ones(NELEM_MAX, dtype=np.int_))
1✔
211
            self.add_input("platform_elem_D", NULL * np.ones(NELEM_MAX), units="m")
1✔
212
            self.add_input("platform_elem_t", NULL * np.ones(NELEM_MAX), units="m")
1✔
213
            self.add_input("platform_elem_rho", NULL * np.ones(NELEM_MAX), units="kg/m**3")
1✔
214
            self.add_input("platform_elem_E", NULL * np.ones(NELEM_MAX), units="Pa")
1✔
215
            self.add_input("platform_elem_G", NULL * np.ones(NELEM_MAX), units="Pa")
1✔
216
            self.add_input("platform_total_center_of_mass", np.zeros(3), units="m")
1✔
217
            self.add_input("platform_mass", 0.0, units="kg")
1✔
218
            self.add_input("platform_I_total", np.zeros(6), units="kg*m**2")
1✔
219

220
            if modopt['flags']["floating"]:
1✔
221
                n_member = modopt["floating"]["members"]["n_members"]
1✔
222
                for k in range(n_member):
1✔
223
                    n_height_mem = modopt["floating"]["members"]["n_height"][k]
1✔
224
                    self.add_input(f"member{k}:joint1", np.zeros(3), units="m")
1✔
225
                    self.add_input(f"member{k}:joint2", np.zeros(3), units="m")
1✔
226
                    self.add_input(f"member{k}:s", np.zeros(n_height_mem))
1✔
227
                    self.add_input(f"member{k}:s_ghost1", 0.0)
1✔
228
                    self.add_input(f"member{k}:s_ghost2", 0.0)
1✔
229
                    self.add_input(f"member{k}:outer_diameter", np.zeros(n_height_mem), units="m")
1✔
230
                    self.add_input(f"member{k}:wall_thickness", np.zeros(n_height_mem-1), units="m")
1✔
231

232
            # Turbine level inputs
233
            self.add_discrete_input('rotor_orientation',val='upwind', desc='Rotor orientation, either upwind or downwind.')
1✔
234
            self.add_input('control_ratedPower',        val=0.,  units='W',    desc='machine power rating')
1✔
235
            self.add_input('control_maxOmega',          val=0.0, units='rpm',  desc='maximum allowed rotor rotation speed')
1✔
236
            self.add_input('control_maxTS',             val=0.0, units='m/s',  desc='maximum allowed blade tip speed')
1✔
237
            self.add_input('cone',             val=0.0, units='deg',   desc='Cone angle of the rotor. It defines the angle between the rotor plane and the blade pitch axis. A standard machine has positive values.')
1✔
238
            self.add_input('tilt',             val=0.0, units='deg',   desc='Nacelle uptilt angle. A standard machine has positive values.')
1✔
239
            self.add_input('overhang',         val=0.0, units='m',     desc='Horizontal distance from tower top to hub center.')
1✔
240

241
            # Initial conditions
242
            self.add_input('U', val=np.zeros(n_pc), units='m/s', desc='wind speeds')
1✔
243
            self.add_input('Omega', val=np.zeros(n_pc), units='rpm', desc='rotation speeds to run')
1✔
244
            self.add_input('pitch', val=np.zeros(n_pc), units='deg', desc='pitch angles to run')
1✔
245
            self.add_input("Ct_aero", val=np.zeros(n_pc), desc="rotor aerodynamic thrust coefficient")
1✔
246

247
            # Cp-Ct-Cq surfaces
248
            self.add_input('Cp_aero_table', val=np.zeros((n_tsr, n_pitch, n_U)), desc='Table of aero power coefficient')
1✔
249
            self.add_input('Ct_aero_table', val=np.zeros((n_tsr, n_pitch, n_U)), desc='Table of aero thrust coefficient')
1✔
250
            self.add_input('Cq_aero_table', val=np.zeros((n_tsr, n_pitch, n_U)), desc='Table of aero torque coefficient')
1✔
251
            self.add_input('pitch_vector',  val=np.zeros(n_pitch), units='deg',  desc='Pitch vector used')
1✔
252
            self.add_input('tsr_vector',    val=np.zeros(n_tsr),                 desc='TSR vector used')
1✔
253
            self.add_input('U_vector',      val=np.zeros(n_U),     units='m/s',  desc='Wind speed vector used')
1✔
254

255
            # Environmental conditions
256
            self.add_input('V_R25',       val=0.0, units='m/s',      desc='region 2.5 transition wind speed')
1✔
257
            self.add_input('Vgust',       val=0.0, units='m/s',      desc='gust wind speed')
1✔
258
            self.add_input('V_extreme1',  val=0.0, units='m/s',      desc='IEC extreme wind speed at hub height for a 1-year retunr period')
1✔
259
            self.add_input('V_extreme50', val=0.0, units='m/s',      desc='IEC extreme wind speed at hub height for a 50-year retunr period')
1✔
260
            self.add_input('V_mean_iec',  val=0.0, units='m/s',      desc='IEC mean wind for turbulence class')
1✔
261
            
262
            self.add_input('rho',         val=0.0, units='kg/m**3',  desc='density of air')
1✔
263
            self.add_input('mu',          val=0.0, units='kg/(m*s)', desc='dynamic viscosity of air')
1✔
264
            self.add_input('speed_sound_air',  val=340.,    units='m/s',        desc='Speed of sound in air.')
1✔
265
            self.add_input(
1✔
266
                    "water_depth", val=0.0, units="m", desc="Water depth for analysis.  Values > 0 mean offshore"
267
                )
268
            self.add_input('rho_water',   val=0.0, units='kg/m**3',  desc='density of water')
1✔
269
            self.add_input('mu_water',    val=0.0, units='kg/(m*s)', desc='dynamic viscosity of water')
1✔
270
            self.add_input('beta_wave',    val=0.0, units='deg', desc='Incident wave propagation heading direction')
1✔
271
            self.add_input('Hsig_wave',    val=0.0, units='m', desc='Significant wave height of incident waves')
1✔
272
            self.add_input('Tsig_wave',    val=0.0, units='s', desc='Peak-spectral period of incident waves')
1✔
273

274
            # Blade composite material properties (used for fatigue analysis)
275
            self.add_input('gamma_f',      val=1.35,                             desc='safety factor on loads')
1✔
276
            self.add_input('gamma_m',      val=1.1,                              desc='safety factor on materials')
1✔
277
            self.add_input('E',            val=np.zeros([n_mat, 3]), units='Pa', desc='2D array of the Youngs moduli of the materials. Each row represents a material, the three columns represent E11, E22 and E33.')
1✔
278
            self.add_input('Xt',           val=np.zeros([n_mat, 3]), units='Pa', desc='2D array of the Ultimate Tensile Strength (UTS) of the materials. Each row represents a material, the three columns represent Xt12, Xt13 and Xt23.')
1✔
279
            self.add_input('Xc',           val=np.zeros([n_mat, 3]), units='Pa', desc='2D array of the Ultimate Compressive Strength (UCS) of the materials. Each row represents a material, the three columns represent Xc12, Xc13 and Xc23.')
1✔
280
            self.add_input('m',            val=np.zeros([n_mat]),                desc='2D array of the S-N fatigue slope exponent for the materials')
1✔
281

282
            # Blade composit layup info (used for fatigue analysis)
283
            self.add_input('sc_ss_mats',   val=np.zeros((n_span, n_mat)),        desc="spar cap, suction side,  boolean of materials in each composite layer spanwise, passed as floats for differentiablity, used for Fatigue Analysis")
1✔
284
            self.add_input('sc_ps_mats',   val=np.zeros((n_span, n_mat)),        desc="spar cap, pressure side, boolean of materials in each composite layer spanwise, passed as floats for differentiablity, used for Fatigue Analysis")
1✔
285
            self.add_input('te_ss_mats',   val=np.zeros((n_span, n_mat)),        desc="trailing edge reinforcement, suction side,  boolean of materials in each composite layer spanwise, passed as floats for differentiablity, used for Fatigue Analysis")
1✔
286
            self.add_input('te_ps_mats',   val=np.zeros((n_span, n_mat)),        desc="trailing edge reinforcement, pressure side, boolean of materials in each composite layer spanwise, passed as floats for differentiablity, used for Fatigue Analysis")
1✔
287
            self.add_discrete_input('definition_layer', val=np.zeros(n_layers),  desc='1D array of flags identifying how layers are specified in the yaml. 1) all around (skin, paint, ) 2) offset+rotation twist+width (spar caps) 3) offset+user defined rotation+width 4) midpoint TE+width (TE reinf) 5) midpoint LE+width (LE reinf) 6) layer position fixed to other layer (core fillers) 7) start and width 8) end and width 9) start and end nd 10) web layer')
1✔
288
            # self.add_discrete_input('layer_name',       val=n_layers * [''],     desc='1D array of the names of the layers modeled in the blade structure.')
289
            # self.add_discrete_input('layer_web',        val=n_layers * [''],     desc='1D array of the names of the webs the layer is associated to. If the layer is on the outer profile this entry can simply stay empty.')
290
            # self.add_discrete_input('layer_mat',        val=n_layers * [''],     desc='1D array of the names of the materials of each layer modeled in the blade structure.')
291
            self.layer_name = rotorse_options['layer_name']
1✔
292

293
            # MoorDyn inputs
294
            mooropt = modopt["mooring"]
1✔
295
            if self.options["modeling_options"]["flags"]["mooring"]:
1✔
296
                n_nodes = mooropt["n_nodes"]
1✔
297
                n_lines = mooropt["n_lines"]
1✔
298
                self.add_input("line_diameter", val=np.zeros(n_lines), units="m")
1✔
299
                self.add_input("line_mass_density", val=np.zeros(n_lines), units="kg/m")
1✔
300
                self.add_input("line_stiffness", val=np.zeros(n_lines), units="N")
1✔
301
                self.add_input("line_transverse_added_mass", val=np.zeros(n_lines), units="kg/m")
1✔
302
                self.add_input("line_tangential_added_mass", val=np.zeros(n_lines), units="kg/m")
1✔
303
                self.add_input("line_transverse_drag", val=np.zeros(n_lines))
1✔
304
                self.add_input("line_tangential_drag", val=np.zeros(n_lines))
1✔
305
                self.add_input("nodes_location_full", val=np.zeros((n_nodes, 3)), units="m")
1✔
306
                self.add_input("nodes_mass", val=np.zeros(n_nodes), units="kg")
1✔
307
                self.add_input("nodes_volume", val=np.zeros(n_nodes), units="m**3")
1✔
308
                self.add_input("nodes_added_mass", val=np.zeros(n_nodes))
1✔
309
                self.add_input("nodes_drag_area", val=np.zeros(n_nodes), units="m**2")
1✔
310
                self.add_input("unstretched_length", val=np.zeros(n_lines), units="m")
1✔
311
                self.add_discrete_input("node_names", val=[""] * n_nodes)
1✔
312

313
            # Inputs required for fatigue processing
314
            self.add_input('lifetime', val=25.0, units='yr', desc='Turbine design lifetime')
1✔
315
            self.add_input('blade_sparU_wohlerexp',   val=1.0,   desc='Blade root Wohler exponent, m, in S/N curve S=A*N^-(1/m)')
1✔
316
            self.add_input('blade_sparU_wohlerA',   val=1.0, units="Pa",   desc='Blade root parameter, A, in S/N curve S=A*N^-(1/m)')
1✔
317
            self.add_input('blade_sparU_ultstress',   val=1.0, units="Pa",   desc='Blade root ultimate stress for material')
1✔
318
            self.add_input('blade_sparL_wohlerexp',   val=1.0,   desc='Blade root Wohler exponent, m, in S/N curve S=A*N^-(1/m)')
1✔
319
            self.add_input('blade_sparL_wohlerA',   val=1.0, units="Pa",   desc='Blade root parameter, A, in S/N curve S=A*N^-(1/m)')
1✔
320
            self.add_input('blade_sparL_ultstress',   val=1.0, units="Pa",   desc='Blade root ultimate stress for material')
1✔
321
            self.add_input('blade_teU_wohlerexp',   val=1.0,   desc='Blade root Wohler exponent, m, in S/N curve S=A*N^-(1/m)')
1✔
322
            self.add_input('blade_teU_wohlerA',   val=1.0, units="Pa",   desc='Blade root parameter, A, in S/N curve S=A*N^-(1/m)')
1✔
323
            self.add_input('blade_teU_ultstress',   val=1.0, units="Pa",   desc='Blade root ultimate stress for material')
1✔
324
            self.add_input('blade_teL_wohlerexp',   val=1.0,   desc='Blade root Wohler exponent, m, in S/N curve S=A*N^-(1/m)')
1✔
325
            self.add_input('blade_teL_wohlerA',   val=1.0, units="Pa",   desc='Blade root parameter, A, in S/N curve S=A*N^-(1/m)')
1✔
326
            self.add_input('blade_teL_ultstress',   val=1.0, units="Pa",   desc='Blade root ultimate stress for material')
1✔
327
            self.add_input('blade_root_sparU_load2stress',   val=np.ones(6), units="m**2",  desc='Blade root upper spar cap coefficient between axial load and stress S=C^T [Fx-z;Mx-z]')
1✔
328
            self.add_input('blade_root_sparL_load2stress',   val=np.ones(6), units="m**2",  desc='Blade root lower spar cap coefficient between axial load and stress S=C^T [Fx-z;Mx-z]')
1✔
329
            self.add_input('blade_maxc_teU_load2stress',   val=np.ones(6), units="m**2",  desc='Blade max chord upper trailing edge coefficient between axial load and stress S=C^T [Fx-z;Mx-z]')
1✔
330
            self.add_input('blade_maxc_teL_load2stress',   val=np.ones(6), units="m**2",  desc='Blade max chord lower trailing edge coefficient between axial load and stress S=C^T [Fx-z;Mx-z]')
1✔
331
            self.add_input('lss_wohlerexp',   val=1.0,   desc='Low speed shaft Wohler exponent, m, in S/N curve S=A*N^-(1/m)')
1✔
332
            self.add_input('lss_wohlerA',     val=1.0,   desc='Low speed shaft parameter, A, in S/N curve S=A*N^-(1/m)')
1✔
333
            self.add_input('lss_ultstress',   val=1.0, units="Pa",   desc='Low speed shaft Ultimate stress for material')
1✔
334
            self.add_input('lss_axial_load2stress',   val=np.ones(6), units="m**2",  desc='Low speed shaft coefficient between axial load and stress S=C^T [Fx-z;Mx-z]')
1✔
335
            self.add_input('lss_shear_load2stress',   val=np.ones(6), units="m**2",  desc='Low speed shaft coefficient between shear load and stress S=C^T [Fx-z;Mx-z]')
1✔
336
            self.add_input('tower_wohlerexp',   val=np.ones(n_height_tow-1),   desc='Tower Wohler exponent, m, in S/N curve S=A*N^-(1/m)')
1✔
337
            self.add_input('tower_wohlerA',     val=np.ones(n_height_tow-1),   desc='Tower parameter, A, in S/N curve S=A*N^-(1/m)')
1✔
338
            self.add_input('tower_ultstress',   val=np.ones(n_height_tow-1), units="Pa",   desc='Tower ultimate stress for material')
1✔
339
            self.add_input('tower_axial_load2stress',   val=np.ones([n_height_tow-1,6]), units="m**2",  desc='Tower coefficient between axial load and stress S=C^T [Fx-z;Mx-z]')
1✔
340
            self.add_input('tower_shear_load2stress',   val=np.ones([n_height_tow-1,6]), units="m**2",  desc='Tower coefficient between shear load and stress S=C^T [Fx-z;Mx-z]')
1✔
341
            self.add_input('monopile_wohlerexp',   val=np.ones(monlen),   desc='Tower Wohler exponent, m, in S/N curve S=A*N^-(1/m)')
1✔
342
            self.add_input('monopile_wohlerA',     val=np.ones(monlen),   desc='Tower parameter, A, in S/N curve S=A*N^-(1/m)')
1✔
343
            self.add_input('monopile_ultstress',   val=np.ones(monlen), units="Pa",   desc='Tower ultimate stress for material')
1✔
344
            self.add_input('monopile_axial_load2stress',   val=np.ones([monlen,6]), units="m**2",  desc='Tower coefficient between axial load and stress S=C^T [Fx-z;Mx-z]')
1✔
345
            self.add_input('monopile_shear_load2stress',   val=np.ones([monlen,6]), units="m**2",  desc='Tower coefficient between shear load and stress S=C^T [Fx-z;Mx-z]')
1✔
346
        
347

348
        # TMD params
349
        if self.options['modeling_options']['flags']['TMDs']:
1✔
350
            n_TMDs = self.options['modeling_options']['TMDs']['n_TMDs']
1✔
351
            self.add_input('TMD_mass',         val=np.zeros(n_TMDs), units='kg',         desc='TMD Mass')
1✔
352
            self.add_input('TMD_stiffness',    val=np.zeros(n_TMDs), units='N/m',        desc='TMD Stiffnes')
1✔
353
            self.add_input('TMD_damping',      val=np.zeros(n_TMDs), units='N/(m/s)',    desc='TMD Damping')
1✔
354

355
        # DLC options
356
        n_ws_aep = np.max([1,modopt['DLC_driver']['n_ws_aep']])
1✔
357

358
        # OpenFAST options
359
        OFmgmt = modopt['General']['openfast_configuration']
1✔
360
        self.model_only = OFmgmt['model_only']
1✔
361
        FAST_directory_base = OFmgmt['OF_run_dir']
1✔
362
        # If the path is relative, make it an absolute path to modeling options file
363
        if not os.path.isabs(FAST_directory_base):
1✔
364
            FAST_directory_base = os.path.join(os.path.dirname(modopt['fname_input_modeling']), FAST_directory_base)
×
365
        # Flag to clear OpenFAST run folder. Use it only if disk space is an issue
366
        self.clean_FAST_directory = False
1✔
367
        self.FAST_InputFile = OFmgmt['OF_run_fst']
1✔
368
        # File naming changes whether in MPI or not
369
        if MPI:
1✔
370
            rank    = MPI.COMM_WORLD.Get_rank()
×
371
            self.FAST_runDirectory = os.path.join(FAST_directory_base,'rank_%000d'%int(rank))
×
372
            self.FAST_namingOut = self.FAST_InputFile+'_%000d'%int(rank)
×
373
        else:
374
            self.FAST_runDirectory = FAST_directory_base
1✔
375
            self.FAST_namingOut = self.FAST_InputFile
1✔
376
        self.wind_directory = os.path.join(self.FAST_runDirectory, 'wind')
1✔
377
        if not os.path.exists(self.FAST_runDirectory):
1✔
378
            os.makedirs(self.FAST_runDirectory, exist_ok=True)
1✔
379
        if not os.path.exists(self.wind_directory):
1✔
380
            os.makedirs(self.wind_directory, exist_ok=True)
1✔
381
        # Number of cores used outside of MPI. If larger than 1, the multiprocessing module is called
382
        self.cores = OFmgmt['cores']
1✔
383
        self.case = {}
1✔
384
        self.channels = {}
1✔
385
        self.mpi_run = False
1✔
386
        if 'mpi_run' in OFmgmt.keys():
1✔
387
            self.mpi_run         = OFmgmt['mpi_run']
1✔
388
            if self.mpi_run:
1✔
389
                self.mpi_comm_map_down   = OFmgmt['mpi_comm_map_down']
×
390

391
        # User-defined FAST library/executable
392
        if OFmgmt['FAST_exe'] != 'none':
1✔
393
            if os.path.isabs(OFmgmt['FAST_exe']):
×
394
                self.FAST_exe_user = OFmgmt['FAST_exe']
×
395
            else:
396
                self.FAST_exe_user = os.path.join(os.path.dirname(self.options['modeling_options']['fname_input_modeling']),
×
397
                                             OFmgmt['FAST_exe'])
398
        else:
399
            self.FAST_exe_user = None
1✔
400

401
        if OFmgmt['FAST_lib'] != 'none':
1✔
402
            if os.path.isabs(OFmgmt['FAST_lib']):
×
403
                self.FAST_lib_user = OFmgmt['FAST_lib']
×
404
            else:
405
                self.FAST_lib_user = os.path.join(os.path.dirname(self.options['modeling_options']['fname_input_modeling']),
×
406
                                             OFmgmt['FAST_lib'])
407
        else:
408
            self.FAST_lib_user = None
1✔
409

410
        if OFmgmt['turbsim_exe'] != 'none':
1✔
411
            if os.path.isabs(OFmgmt['turbsim_exe']):
×
412
                self.turbsim_exe = OFmgmt['turbsim_exe']
×
413
            else:
414
                self.turbsim_exe = os.path.join(os.path.dirname(self.options['modeling_options']['fname_input_modeling']),
×
415
                                             OFmgmt['turbsim_exe'])
416
        else:
417
            self.turbsim_exe = shutil.which('turbsim')
1✔
418
        
419

420
        # Rotor power outputs
421
        self.add_output('V_out', val=np.zeros(n_ws_aep), units='m/s', desc='wind speed vector from the OF simulations')
1✔
422
        self.add_output('P_out', val=np.zeros(n_ws_aep), units='W', desc='rotor electrical power')
1✔
423
        self.add_output('Cp_out', val=np.zeros(n_ws_aep), desc='rotor aero power coefficient')
1✔
424
        self.add_output('Ct_out', val=np.zeros(n_ws_aep), desc='rotor aero thrust coefficient')
1✔
425
        self.add_output('Omega_out', val=np.zeros(n_ws_aep), units='rpm', desc='rotation speeds to run')
1✔
426
        self.add_output('pitch_out', val=np.zeros(n_ws_aep), units='deg', desc='pitch angles to run')
1✔
427
        self.add_output('AEP', val=0.0, units='kW*h', desc='annual energy production reconstructed from the openfast simulations')
1✔
428

429
        self.add_output('My_std',      val=0.0,            units='N*m',  desc='standard deviation of blade root flap bending moment in out-of-plane direction')
1✔
430
        self.add_output('flp1_std',    val=0.0,            units='deg',  desc='standard deviation of trailing-edge flap angle')
1✔
431

432
        self.add_output('rated_V',     val=0.0,            units='m/s',  desc='rated wind speed')
1✔
433
        self.add_output('rated_Omega', val=0.0,            units='rpm',  desc='rotor rotation speed at rated')
1✔
434
        self.add_output('rated_pitch', val=0.0,            units='deg',  desc='pitch setting at rated')
1✔
435
        self.add_output('rated_T',     val=0.0,            units='N',    desc='rotor aerodynamic thrust at rated')
1✔
436
        self.add_output('rated_Q',     val=0.0,            units='N*m',  desc='rotor aerodynamic torque at rated')
1✔
437

438
        self.add_output('loads_r',      val=np.zeros(n_span), units='m', desc='radial positions along blade going toward tip')
1✔
439
        self.add_output('loads_Px',     val=np.zeros(n_span), units='N/m', desc='distributed loads in blade-aligned x-direction')
1✔
440
        self.add_output('loads_Py',     val=np.zeros(n_span), units='N/m', desc='distributed loads in blade-aligned y-direction')
1✔
441
        self.add_output('loads_Pz',     val=np.zeros(n_span), units='N/m', desc='distributed loads in blade-aligned z-direction')
1✔
442
        self.add_output('loads_Omega',  val=0.0, units='rpm', desc='rotor rotation speed')
1✔
443
        self.add_output('loads_pitch',  val=0.0, units='deg', desc='pitch angle')
1✔
444
        self.add_output('loads_azimuth', val=0.0, units='deg', desc='azimuthal angle')
1✔
445

446
        # Control outputs
447
        self.add_output('rotor_overspeed',  val=0.0, desc='Maximum percent overspeed of the rotor during all OpenFAST simulations')  # is this over a set of sims?
1✔
448
        self.add_output('max_nac_accel',    val=0.0, units='m/s**2', desc='Maximum nacelle acceleration magnitude all OpenFAST simulations')  # is this over a set of sims?
1✔
449
        self.add_output('avg_pitch_travel',    val=0.0, units='deg/s', desc='Average pitch travel')  # is this over a set of sims?
1✔
450
        self.add_output('pitch_duty_cycle',    val=0.0, units='deg/s', desc='Number of pitch direction changes')  # is this over a set of sims?
1✔
451
        self.add_output('max_pitch_rate_sim',    val=0.0, units='deg/s', desc='Maximum pitch command rate over all simulations')  # is this over a set of sims?
1✔
452

453
        # Blade outputs
454
        self.add_output('max_TipDxc', val=0.0, units='m', desc='Maximum of channel TipDxc, i.e. out of plane tip deflection. For upwind rotors, the max value is tower the tower')
1✔
455
        self.add_output('max_RootMyb', val=0.0, units='kN*m', desc='Maximum of the signals RootMyb1, RootMyb2, ... across all n blades representing the maximum blade root flapwise moment')
1✔
456
        self.add_output('max_RootMyc', val=0.0, units='kN*m', desc='Maximum of the signals RootMyb1, RootMyb2, ... across all n blades representing the maximum blade root out of plane moment')
1✔
457
        self.add_output('max_RootMzb', val=0.0, units='kN*m', desc='Maximum of the signals RootMzb1, RootMzb2, ... across all n blades representing the maximum blade root torsional moment')
1✔
458
        self.add_output('DEL_RootMyb', val=0.0, units='kN*m', desc='damage equivalent load of blade root flap bending moment in out-of-plane direction')
1✔
459
        self.add_output('max_aoa', val=np.zeros(n_span), units='deg', desc='maxima of the angles of attack distributed along blade span')
1✔
460
        self.add_output('std_aoa', val=np.zeros(n_span), units='deg', desc='standard deviation of the angles of attack distributed along blade span')
1✔
461
        self.add_output('mean_aoa', val=np.zeros(n_span), units='deg', desc='mean of the angles of attack distributed along blade span')
1✔
462
        # Blade loads corresponding to maximum blade tip deflection
463
        self.add_output('blade_maxTD_Mx', val=np.zeros(n_span), units='kN*m', desc='distributed moment around blade-aligned x-axis corresponding to maximum blade tip deflection')
1✔
464
        self.add_output('blade_maxTD_My', val=np.zeros(n_span), units='kN*m', desc='distributed moment around blade-aligned y-axis corresponding to maximum blade tip deflection')
1✔
465
        self.add_output('blade_maxTD_Fz', val=np.zeros(n_span), units='kN', desc='distributed force in blade-aligned z-direction corresponding to maximum blade tip deflection')
1✔
466

467
        # Hub outputs
468
        self.add_output('hub_Fxyz', val=np.zeros(3), units='kN', desc = 'Maximum hub forces in the non rotating frame')
1✔
469
        self.add_output('hub_Mxyz', val=np.zeros(3), units='kN*m', desc = 'Maximum hub moments in the non rotating frame')
1✔
470
        self.add_output('hub_Fxyz_aero', val=np.zeros(3), units='N', desc = 'Aero-only maximum hub forces in the non rotating frame')
1✔
471
        self.add_output('hub_Mxyz_aero', val=np.zeros(3), units='N*m', desc = 'Aero-only maximum hub moments in the non rotating frame')
1✔
472

473
        self.add_output('max_TwrBsMyt',val=0.0, units='kN*m', desc='maximum of tower base bending moment in fore-aft direction')
1✔
474
        self.add_output('max_TwrBsMyt_ratio',val=0.0,  desc='ratio of maximum of tower base bending moment in fore-aft direction to maximum allowable bending moment')
1✔
475
        self.add_output('DEL_TwrBsMyt',val=0.0, units='kN*m', desc='damage equivalent load of tower base bending moment in fore-aft direction')
1✔
476
        self.add_output('DEL_TwrBsMyt_ratio',val=0.0, desc='ratio of damage equivalent load of tower base bending moment in fore-aft direction to maximum allowable bending moment')
1✔
477
        
478
        # Tower outputs
479
        if not self.options['modeling_options']['Level3']['from_openfast']:
1✔
480
            self.add_output('tower_maxMy_Fx', val=np.zeros(n_full_tow-1), units='kN', desc='distributed force in tower-aligned x-direction corresponding to maximum fore-aft moment at tower base')
1✔
481
            self.add_output('tower_maxMy_Fy', val=np.zeros(n_full_tow-1), units='kN', desc='distributed force in tower-aligned y-direction corresponding to maximum fore-aft moment at tower base')
1✔
482
            self.add_output('tower_maxMy_Fz', val=np.zeros(n_full_tow-1), units='kN', desc='distributed force in tower-aligned z-direction corresponding to maximum fore-aft moment at tower base')
1✔
483
            self.add_output('tower_maxMy_Mx', val=np.zeros(n_full_tow-1), units='kN*m', desc='distributed moment around tower-aligned x-axis corresponding to maximum fore-aft moment at tower base')
1✔
484
            self.add_output('tower_maxMy_My', val=np.zeros(n_full_tow-1), units='kN*m', desc='distributed moment around tower-aligned x-axis corresponding to maximum fore-aft moment at tower base')
1✔
485
            self.add_output('tower_maxMy_Mz', val=np.zeros(n_full_tow-1), units='kN*m', desc='distributed moment around tower-aligned x-axis corresponding to maximum fore-aft moment at tower base')
1✔
486

487
            # Monopile outputs
488
            self.add_output('max_M1N1MKye',val=0.0, units='kN*m', desc='maximum of My moment of member 1 at node 1 (base of the monopile)')
1✔
489
            self.add_output('monopile_maxMy_Fx', val=np.zeros(monlen_full), units='kN', desc='distributed force in monopile-aligned x-direction corresponding to max_M1N1MKye')
1✔
490
            self.add_output('monopile_maxMy_Fy', val=np.zeros(monlen_full), units='kN', desc='distributed force in monopile-aligned y-direction corresponding to max_M1N1MKye')
1✔
491
            self.add_output('monopile_maxMy_Fz', val=np.zeros(monlen_full), units='kN', desc='distributed force in monopile-aligned z-direction corresponding to max_M1N1MKye')
1✔
492
            self.add_output('monopile_maxMy_Mx', val=np.zeros(monlen_full), units='kN*m', desc='distributed moment around tower-aligned x-axis corresponding to max_M1N1MKye')
1✔
493
            self.add_output('monopile_maxMy_My', val=np.zeros(monlen_full), units='kN*m', desc='distributed moment around tower-aligned x-axis corresponding to max_M1N1MKye')
1✔
494
            self.add_output('monopile_maxMy_Mz', val=np.zeros(monlen_full), units='kN*m', desc='distributed moment around tower-aligned x-axis corresponding to max_M1N1MKye')
1✔
495

496
        # Floating outputs
497
        self.add_output('Max_PtfmPitch', val=0.0, desc='Maximum platform pitch angle over a set of OpenFAST simulations')
1✔
498
        self.add_output('Std_PtfmPitch', val=0.0, units='deg', desc='standard deviation of platform pitch angle')
1✔
499
        self.add_output('Max_Offset', val=0.0, units='m', desc='Maximum distance in surge/sway direction')
1✔
500

501
        # Fatigue output
502
        self.add_output('damage_blade_root_sparU', val=0.0, desc="Miner's rule cumulative damage to upper spar cap at blade root")
1✔
503
        self.add_output('damage_blade_root_sparL', val=0.0, desc="Miner's rule cumulative damage to lower spar cap at blade root")
1✔
504
        self.add_output('damage_blade_maxc_teU', val=0.0, desc="Miner's rule cumulative damage to upper trailing edge at blade max chord")
1✔
505
        self.add_output('damage_blade_maxc_teL', val=0.0, desc="Miner's rule cumulative damage to lower trailing edge at blade max chord")
1✔
506
        self.add_output('damage_lss', val=0.0, desc="Miner's rule cumulative damage to low speed shaft at hub attachment")
1✔
507
        self.add_output('damage_tower_base', val=0.0, desc="Miner's rule cumulative damage at tower base")
1✔
508
        self.add_output('damage_monopile_base', val=0.0, desc="Miner's rule cumulative damage at monopile base")
1✔
509

510
        # Simulation output
511
        self.add_output('openfast_failed', val=0.0, desc="Numerical value for whether any openfast runs failed. 0 if false, 2 if true")
1✔
512
        
513
        # Open loop to closed loop error
514
        if self.options['modeling_options']['OL2CL']['flag']:
1✔
515
            self.add_output('OL2CL_pitch', val=0.0, desc="Open loop to closed loop avarege error")
×
516

517
        self.add_discrete_output('fst_vt_out', val={})
1✔
518
        self.add_discrete_output('ts_out_dir', val={})
1✔
519

520
        # Iteration counter for openfast calls. Initialize at -1 so 0 after first call
521
        self.of_inumber = -1
1✔
522
        self.sim_idx = -1
1✔
523

524
        if modopt['Level2']['flag']:
1✔
525
            if MPI:
×
526
                rank = MPI.COMM_WORLD.Get_rank()
×
527
                lin_pkl_dir = os.path.join(self.options['opt_options']['general']['folder_output'], 'lin', 'rank_{}'.format(rank))
×
528
                if not os.path.exists(lin_pkl_dir):
×
529
                    os.makedirs(lin_pkl_dir, exist_ok=True)
×
530
                self.lin_pkl_file_name = os.path.join(lin_pkl_dir, 'ABCD_matrices.pkl')
×
531
            else:
532
                lin_pkl_dir = os.path.join(self.options['opt_options']['general']['folder_output'], 'lin')
×
533
                self.lin_pkl_file_name = os.path.join(lin_pkl_dir, 'ABCD_matrices.pkl')
×
534

535
            self.ABCD_list = []
×
536
            path = '.'.join(self.lin_pkl_file_name.split('.')[:-1])
×
537
            os.makedirs(path, exist_ok=True)
×
538

539
            with open(self.lin_pkl_file_name, 'wb') as handle:
×
540
                pickle.dump(self.ABCD_list, handle)
×
541
            
542
            self.lin_idx = 0
×
543

544
    def compute(self, inputs, outputs, discrete_inputs, discrete_outputs):
1✔
545
        modopt = self.options['modeling_options']
1✔
546
        sys.stdout.flush()
1✔
547

548
        if modopt['Level2']['flag']:
1✔
549
            self.sim_idx += 1
×
550
            ABCD = {
×
551
                'sim_idx' : self.sim_idx,
552
                'A' : None,
553
                'B' : None,
554
                'C' : None,
555
                'D' : None,
556
                'x_ops':None,
557
                'u_ops':None,
558
                'y_ops':None,
559
                'u_h':None,
560
                'omega_rpm' : None,
561
                'DescCntrlInpt' : None,
562
                'DescStates' : None,
563
                'DescOutput' : None,
564
                'StateDerivOrder' : None,
565
                'ind_fast_inps' : None,
566
                'ind_fast_outs' : None,
567
                'AEP':None
568
                }
569
            with open(self.lin_pkl_file_name, 'rb') as handle:
×
570
                ABCD_list = pickle.load(handle)
×
571

572
            ABCD_list.append(ABCD)
×
573
            self.ABCD_list = ABCD_list
×
574

575
            with open(self.lin_pkl_file_name, 'wb') as handle:
×
576
                pickle.dump(ABCD_list, handle)
×
577

578
        fst_vt = self.init_FAST_model()
1✔
579

580
        if not modopt['Level3']['from_openfast']:
1✔
581
            fst_vt = self.update_FAST_model(fst_vt, inputs, discrete_inputs)
1✔
582
        else:
583
            fast_reader = InputReader_OpenFAST()
1✔
584
            fast_reader.FAST_InputFile  = modopt['Level3']['openfast_file']   # FAST input file (ext=.fst)
1✔
585
            fast_reader.FAST_directory  = modopt['Level3']['openfast_dir']   # Path to fst directory files
1✔
586
            fast_reader.path2dll            = modopt['General']['openfast_configuration']['path2dll']   # Path to dll file
1✔
587
            fast_reader.execute()
1✔
588
            fst_vt = fast_reader.fst_vt
1✔
589
            # Re-load modeling options without defaults to learn only what needs to change, has already been validated when first loaded
590
            modopts_no_defaults = load_yaml(self.options['modeling_options']['fname_input_modeling'])
1✔
591
            fst_vt = self.load_FAST_model_opts(fst_vt,modopts_no_defaults)
1✔
592

593
            # Fix TwrTI: WEIS modeling options have it as a single value...
594
            if not isinstance(fst_vt['AeroDyn15']['TwrTI'],list):
1✔
595
                fst_vt['AeroDyn15']['TwrTI'] = [fst_vt['AeroDyn15']['TwrTI']] * len(fst_vt['AeroDyn15']['TwrElev'])
×
596
            if not isinstance(fst_vt['AeroDyn15']['TwrCb'],list):
1✔
597
                fst_vt['AeroDyn15']['TwrCb'] = [fst_vt['AeroDyn15']['TwrCb']] * len(fst_vt['AeroDyn15']['TwrElev'])
×
598

599
            # Fix AddF0: Should be a n x 1 array (list of lists):
600
            if fst_vt['HydroDyn']:
1✔
601
                fst_vt['HydroDyn']['AddF0'] = [[F0] for F0 in fst_vt['HydroDyn']['AddF0']]
1✔
602

603
            if modopt['ROSCO']['flag']:
1✔
604
                fst_vt['DISCON_in'] = modopt['General']['openfast_configuration']['fst_vt']['DISCON_in']
1✔
605
                
606
                
607
        if self.model_only == True:
1✔
608
            # Write input OF files, but do not run OF
609
            fst_vt['Fst']['TMax'] = 10.
1✔
610
            fst_vt['Fst']['TStart'] = 0.
1✔
611
            self.write_FAST(fst_vt, discrete_outputs)
1✔
612
        else:
613
            # Write OF model and run
614
            summary_stats, extreme_table, DELs, Damage, case_list, case_name, chan_time, dlc_generator  = self.run_FAST(inputs, discrete_inputs, fst_vt)
1✔
615

616
            # Set up linear turbine model
617
            if modopt['Level2']['flag']:
1✔
618
                try: 
×
619
                    LinearTurbine = LinearTurbineModel(
×
620
                    self.FAST_runDirectory,
621
                    self.lin_case_name,
622
                    nlin=modopt['Level2']['linearization']['NLinTimes'],
623
                    reduceControls=True
624
                    )
625
                except FileNotFoundError as e:
×
626
                    logger.warning('FileNotFoundError: {} {}'.format(e.strerror, e.filename))
×
627
                    return
×
628

629
                # Save linearizations
630
                logger.warning('Saving ABCD matrices!')
×
631
                ABCD = {
×
632
                    'sim_idx' : self.sim_idx,
633
                    'A' : LinearTurbine.A_ops,
634
                    'B' : LinearTurbine.B_ops,
635
                    'C' : LinearTurbine.C_ops,
636
                    'D' : LinearTurbine.D_ops,
637
                    'x_ops':LinearTurbine.x_ops,
638
                    'u_ops':LinearTurbine.u_ops,
639
                    'y_ops':LinearTurbine.y_ops,
640
                    'u_h':LinearTurbine.u_h,
641
                    'omega_rpm' : LinearTurbine.omega_rpm,
642
                    'DescCntrlInpt' : LinearTurbine.DescCntrlInpt,
643
                    'DescStates' : LinearTurbine.DescStates,
644
                    'DescOutput' : LinearTurbine.DescOutput,
645
                    'StateDerivOrder' : LinearTurbine.StateDerivOrder,
646
                    'ind_fast_inps' : LinearTurbine.ind_fast_inps,
647
                    'ind_fast_outs' : LinearTurbine.ind_fast_outs,
648
                    }
649
                with open(self.lin_pkl_file_name, 'rb') as handle:
×
650
                    ABCD_list = pickle.load(handle)
×
651

652
                ABCD_list[self.sim_idx] = ABCD
×
653

654
                with open(self.lin_pkl_file_name, 'wb') as handle:
×
655
                    pickle.dump(ABCD_list, handle)
×
656
                    
657
                lin_files = glob.glob(os.path.join(self.FAST_runDirectory, '*.lin'))
×
658
                
659
                dest = os.path.join(self.FAST_runDirectory, f'copied_lin_files_{self.lin_idx}')
×
660
                Path(dest).mkdir(parents=True, exist_ok=True)
×
661
                for file in lin_files:
×
662
                    shutil.copy2(file, dest)
×
663
                self.lin_idx += 1
×
664

665
                # Shorten output names from linearization output to one like level3 openfast output
666
                # This depends on how openfast sets up the linearization output names and may break if that is changed
667
                OutList     = [out_name.split()[1][:-1] for out_name in LinearTurbine.DescOutput]
×
668
                OutOps      = {}
×
669
                for i_out, out in enumerate(OutList):
×
670
                    OutOps[out] = LinearTurbine.y_ops[i_out,:]
×
671

672
                # save to yaml, might want in analysis outputs
673
                FileTools.save_yaml(
×
674
                    self.FAST_runDirectory,
675
                    'OutOps.yaml',OutOps)
676

677
                # Set up Level 2 disturbance (simulation or DTQP)
678
                if modopt['Level2']['simulation']['flag'] or modopt['Level2']['DTQP']['flag']:
×
679
                    # Extract disturbance(s)
680
                    level2_disturbance = []
×
681
                    for case in case_list:
×
682
                        ts_file     = TurbSimFile(case[('InflowWind','FileName_BTS')])
×
683
                        ts_file.compute_rot_avg(fst_vt['ElastoDyn']['TipRad'])
×
684
                        u_h         = ts_file['rot_avg'][0,:]
×
685
                        tt          = ts_file['t']
×
686
                        level2_disturbance.append({'Time':tt, 'Wind': u_h})
×
687

688
                # Run linear simulation:
689

690
                # Get case list, wind inputs should have already been generated
691
                if modopt['Level2']['simulation']['flag']:
×
692
            
693
                    if modopt['Level2']['DTQP']['flag']:
×
694
                        raise Exception('Only DTQP or simulation flag can be set to true in Level2 modeling options')
×
695

696
                    # This is going to use the last discon_in file of the linearization set as the simulation file
697
                    # Currently fine because openfast is executed (or not executed if overwrite=False) after the file writing
698
                    if 'DLL_InFile' in self.fst_vt['ServoDyn']:     # if using file inputs
×
699
                        discon_in_file = os.path.join(self.FAST_runDirectory, self.fst_vt['ServoDyn']['DLL_InFile'])
×
700
                    else:       # if using fst_vt inputs from openfast_openmdao
701
                        discon_in_file = os.path.join(self.FAST_runDirectory, self.lin_case_name[0] + '_DISCON.IN')
×
702

703
                    lib_name = modopt['General']['openfast_configuration']['path2dll']
×
704

705
                    ss = {}
×
706
                    et = {}
×
707
                    dl = {}
×
708
                    dam = {}
×
709
                    ct = []
×
710
                    for i_dist, dist in enumerate(level2_disturbance):
×
711
                        sim_name = 'l2_sim_{}'.format(i_dist)
×
712
                        controller_int = ROSCO_ci.ControllerInterface(
×
713
                            lib_name,
714
                            param_filename=discon_in_file,
715
                            DT=1/80,        # modelling input?
716
                            sim_name = os.path.join(self.FAST_runDirectory,sim_name)
717
                            )
718

719
                        l2_out, _, P_op = LinearTurbine.solve(dist,Plot=False,controller=controller_int)
×
720

721
                        output = OpenFASTOutput.from_dict(l2_out, sim_name, magnitude_channels=self.magnitude_channels)
×
722

723
                        _name, _ss, _et, _dl, _dam = self.la._process_output(output)
×
724
                        ss[_name] = _ss
×
725
                        et[_name] = _et
×
726
                        dl[_name] = _dl
×
727
                        dam[_name] = _dam
×
728
                        ct.append(l2_out)
×
729

730
                        output.df.to_pickle(os.path.join(self.FAST_runDirectory,sim_name+'.p'))
×
731

732
                        summary_stats, extreme_table, DELs, Damage = self.la.post_process(ss, et, dl, dam)
×
733
                        
734
                        # Overwrite timeseries with simulated data instead of saved linearization timeseries
735
                        chan_time = ct
×
736

737
                elif modopt['Level2']['DTQP']['flag']:
×
738

739
                    summary_stats, extreme_table, DELs, Damage = dtqp_wrapper(
×
740
                        LinearTurbine, 
741
                        level2_disturbance, 
742
                        self.options['opt_options'], 
743
                        self.options['modeling_options'], 
744
                        self.fst_vt, 
745
                        self.la, 
746
                        self.magnitude_channels, 
747
                        self.FAST_runDirectory
748
                    )
749

750
                    # TODO: pull chan_time out of here
751

752
            # Post process regardless of level
753
            self.post_process(summary_stats, extreme_table, DELs, Damage, case_list, dlc_generator, chan_time, inputs, discrete_inputs, outputs, discrete_outputs)
1✔
754
            
755
            # Save AEP value to linear pickle file
756
            if modopt['Level2']['flag']:
1✔
757
                with open(self.lin_pkl_file_name, 'rb') as handle:
×
758
                        ABCD_list = pickle.load(handle)
×
759

760
                ABCD_list[self.sim_idx]['AEP'] = outputs['AEP']
×
761

762
                with open(self.lin_pkl_file_name, 'wb') as handle:
×
763
                    pickle.dump(ABCD_list, handle)
×
764
        
765
        # delete run directory. not recommended for most cases, use for large parallelization problems where disk storage will otherwise fill up
766
        if self.clean_FAST_directory:
1✔
767
            try:
×
768
                shutil.rmtree(self.FAST_runDirectory)
×
769
            except:
×
770
                logger.warning('Failed to delete directory: %s'%self.FAST_runDirectory)
×
771

772
    def init_FAST_model(self):
1✔
773

774
        modopt = self.options['modeling_options']
1✔
775
        fst_vt = modopt['General']['openfast_configuration']['fst_vt']
1✔
776

777
        # Main .fst file`
778
        fst_vt['Fst']               = {}
1✔
779
        fst_vt['ElastoDyn']         = {}
1✔
780
        fst_vt['ElastoDynBlade']    = {}
1✔
781
        fst_vt['ElastoDynTower']    = {}
1✔
782
        fst_vt['AeroDyn15']         = {}
1✔
783
        fst_vt['AeroDynBlade']      = {}
1✔
784
        fst_vt['ServoDyn']          = {}
1✔
785
        fst_vt['InflowWind']        = {}
1✔
786
        fst_vt['SubDyn']            = {}
1✔
787
        fst_vt['HydroDyn']          = {}
1✔
788
        fst_vt['MoorDyn']           = {}
1✔
789
        fst_vt['MAP']               = {}
1✔
790
        
791
        # List of structural controllers
792
        fst_vt['TStC'] = {}; fst_vt['TStC'] = []
1✔
793
        fst_vt['SStC'] = {}; fst_vt['SStC'] = []
1✔
794
        fst_vt['NStC'] = {}; fst_vt['NStC'] = []
1✔
795
        fst_vt['BStC'] = {}; fst_vt['BStC'] = []
1✔
796

797
        fst_vt = self.load_FAST_model_opts(fst_vt)
1✔
798

799
        return fst_vt
1✔
800

801
    def load_FAST_model_opts(self,fst_vt,modeling_options={}):
1✔
802
        # Can provide own modeling options, used when we don't want to use default OpenFAST options
803
        if not modeling_options:
1✔
804
            modeling_options = self.options['modeling_options']
1✔
805

806
        if 'simulation' in modeling_options['Level3']:
1✔
807
            for key in modeling_options['Level3']['simulation']:
1✔
808
                fst_vt['Fst'][key] = modeling_options['Level3']['simulation'][key]
1✔
809

810
        if 'ElastoDyn' in modeling_options['Level3']:
1✔
811
            for key in modeling_options['Level3']['ElastoDyn']:
1✔
812
                fst_vt['ElastoDyn'][key] = modeling_options['Level3']['ElastoDyn'][key]
1✔
813
        
814
        if 'ElastoDynBlade' in modeling_options['Level3']:
1✔
815
            for key in modeling_options['Level3']['ElastoDynBlade']:
1✔
816
                fst_vt['ElastoDynBlade'][key] = modeling_options['Level3']['ElastoDynBlade'][key]
1✔
817

818
        if 'ElastoDynTower' in modeling_options['Level3']:   
1✔
819
            for key in modeling_options['Level3']['ElastoDynTower']:
1✔
820
                fst_vt['ElastoDynTower'][key] = modeling_options['Level3']['ElastoDynTower'][key]
1✔
821

822
        if 'AeroDyn' in modeling_options['Level3']:    
1✔
823
            for key in modeling_options['Level3']['AeroDyn']:
1✔
824
                fst_vt['AeroDyn15'][key] = copy.copy(modeling_options['Level3']['AeroDyn'][key])
1✔
825

826
        if 'InflowWind' in modeling_options['Level3']:    
1✔
827
            for key in modeling_options['Level3']['InflowWind']:
1✔
828
                fst_vt['InflowWind'][key] = modeling_options['Level3']['InflowWind'][key]
1✔
829
            
830
        if 'ServoDyn' in modeling_options['Level3']:    
1✔
831
            for key in modeling_options['Level3']['ServoDyn']:
1✔
832
                fst_vt['ServoDyn'][key] = modeling_options['Level3']['ServoDyn'][key]
1✔
833

834
        if 'SubDyn' in modeling_options['Level3']:    
1✔
835
            for key in modeling_options['Level3']['SubDyn']:
1✔
836
                fst_vt['SubDyn'][key] = modeling_options['Level3']['SubDyn'][key]
1✔
837

838
        if 'HydroDyn' in modeling_options['Level3']:    
1✔
839
            for key in modeling_options['Level3']['HydroDyn']:
1✔
840
                fst_vt['HydroDyn'][key] = modeling_options['Level3']['HydroDyn'][key]
1✔
841

842
        if 'MoorDyn' in modeling_options['Level3']:    
1✔
843
            for key in modeling_options['Level3']['MoorDyn']:
1✔
844
                fst_vt['MoorDyn'][key] = modeling_options['Level3']['MoorDyn'][key]
1✔
845
        
846
        if 'outlist' in modeling_options['Level3']:
1✔
847
            for key1 in modeling_options['Level3']['outlist']:
1✔
848
                    for key2 in modeling_options['Level3']['outlist'][key1]:
1✔
849
                        fst_vt['outlist'][key1][key2] = modeling_options['Level3']['outlist'][key1][key2]
×
850
        
851
        if 'path2dll' in modeling_options['General']['openfast_configuration']:
1✔
852
            fst_vt['ServoDyn']['DLL_FileName'] = modeling_options['General']['openfast_configuration']['path2dll']
1✔
853

854
        if fst_vt['AeroDyn15']['IndToler'] == 0.:
1✔
855
            fst_vt['AeroDyn15']['IndToler'] = 'Default'
1✔
856
        if fst_vt['AeroDyn15']['DTAero'] == 0.:
1✔
857
            fst_vt['AeroDyn15']['DTAero'] = 'Default'
1✔
858
        if 'OLAF' in fst_vt['AeroDyn15']:
1✔
859
            if fst_vt['AeroDyn15']['OLAF']['DTfvw'] == 0.:
1✔
860
                fst_vt['AeroDyn15']['OLAF']['DTfvw'] = 'Default'
1✔
861
        else:
862
            fst_vt['AeroDyn15']['OLAF'] = {}
1✔
863
        if fst_vt['ElastoDyn']['DT'] == 0.:
1✔
864
            fst_vt['ElastoDyn']['DT'] = 'Default'
1✔
865
        if fst_vt['Fst']['DT_Out'] == 0.:
1✔
866
            fst_vt['Fst']['DT_Out'] = 'Default'
1✔
867

868
        return fst_vt
1✔
869

870
    def update_FAST_model(self, fst_vt, inputs, discrete_inputs):
1✔
871

872
        modopt = self.options['modeling_options']
1✔
873

874
        # Update fst_vt nested dictionary with data coming from WISDEM
875

876
        # Comp flags in main input
877
        if modopt['flags']['offshore']:
1✔
878
            fst_vt['Fst']['CompHydro'] = 1  # Use HydroDyn if not set in modeling inputs 
1✔
879

880
        # If there's mooring and  CompMooring not set in modeling inputs
881
        if modopt["flags"]["mooring"] and not fst_vt['Fst']['CompMooring']:
1✔
882
            fst_vt['Fst']['CompMooring'] = 3  # Use MoorDyn             
×
883

884
        # Update ElastoDyn
885
        fst_vt['ElastoDyn']['NumBl']  = self.n_blades
1✔
886
        fst_vt['ElastoDyn']['TipRad'] = inputs['Rtip'][0]
1✔
887
        fst_vt['ElastoDyn']['HubRad'] = inputs['Rhub'][0]
1✔
888
        if discrete_inputs['rotor_orientation'] == 'upwind':
1✔
889
            k = -1.
1✔
890
        else:
891
            k = 1
×
892
        fst_vt['ElastoDyn']['PreCone(1)'] = k*inputs['cone'][0]
1✔
893
        fst_vt['ElastoDyn']['PreCone(2)'] = k*inputs['cone'][0]
1✔
894
        fst_vt['ElastoDyn']['PreCone(3)'] = k*inputs['cone'][0]
1✔
895
        fst_vt['ElastoDyn']['ShftTilt']   = k*inputs['tilt'][0]
1✔
896
        fst_vt['ElastoDyn']['OverHang']   = k*inputs['overhang'][0] / np.cos(np.deg2rad(inputs['tilt'][0])) # OpenFAST defines the overhang tilted (!)
1✔
897
        fst_vt['ElastoDyn']['GBoxEff']    = inputs['gearbox_efficiency'][0] * 100.
1✔
898
        fst_vt['ElastoDyn']['GBRatio']    = inputs['gearbox_ratio'][0]
1✔
899

900
        # Update ServoDyn
901
        fst_vt['ServoDyn']['GenEff']       = float(inputs['generator_efficiency']/inputs['gearbox_efficiency']) * 100.
1✔
902
        fst_vt['ServoDyn']['PitManRat(1)'] = float(inputs['max_pitch_rate'])
1✔
903
        fst_vt['ServoDyn']['PitManRat(2)'] = float(inputs['max_pitch_rate'])
1✔
904
        fst_vt['ServoDyn']['PitManRat(3)'] = float(inputs['max_pitch_rate'])
1✔
905
        
906

907
        # Update ServoDyn
908
        fst_vt['ServoDyn']['GenEff']       = float(inputs['generator_efficiency']/inputs['gearbox_efficiency']) * 100.
1✔
909
        fst_vt['ServoDyn']['PitManRat(1)'] = float(inputs['max_pitch_rate'])
1✔
910
        fst_vt['ServoDyn']['PitManRat(2)'] = float(inputs['max_pitch_rate'])
1✔
911
        fst_vt['ServoDyn']['PitManRat(3)'] = float(inputs['max_pitch_rate'])
1✔
912
        
913

914
        # Masses and inertias from DriveSE
915
        fst_vt['ElastoDyn']['HubMass']   = inputs['hub_system_mass'][0]
1✔
916
        fst_vt['ElastoDyn']['HubIner']   = inputs['hub_system_I'][0]
1✔
917
        fst_vt['ElastoDyn']['HubCM']     = inputs['hub_system_cm'][0] # k*inputs['overhang'][0] - inputs['hub_system_cm'][0], but we need to solve the circular dependency in DriveSE first
1✔
918
        fst_vt['ElastoDyn']['NacMass']   = inputs['above_yaw_mass'][0]
1✔
919
        fst_vt['ElastoDyn']['YawBrMass'] = inputs['yaw_mass'][0]
1✔
920
        # Advice from R. Bergua, add 1/3 the tower yaw inertia here because it is activated as a lumped modal mass
921
        fst_vt['ElastoDyn']['NacYIner']  = inputs['nacelle_I_TT'][2] + inputs['tower_I_base'][2]/3.0
1✔
922
        fst_vt['ElastoDyn']['NacCMxn']   = -k*inputs['nacelle_cm'][0]
1✔
923
        fst_vt['ElastoDyn']['NacCMyn']   = inputs['nacelle_cm'][1]
1✔
924
        fst_vt['ElastoDyn']['NacCMzn']   = inputs['nacelle_cm'][2]
1✔
925
        tower_top_height = float(inputs['hub_height']) - float(inputs['distance_tt_hub']) # Height of tower above ground level [onshore] or MSL [offshore] (meters)
1✔
926
        # The Twr2Shft is just the difference between hub height, tower top height, and sin(tilt)*overhang
927
        fst_vt['ElastoDyn']['Twr2Shft']  = float(inputs['hub_height']) - tower_top_height - abs(fst_vt['ElastoDyn']['OverHang'])*np.sin(np.deg2rad(inputs['tilt'][0]))
1✔
928
        fst_vt['ElastoDyn']['GenIner']   = float(inputs['GenIner'])
1✔
929

930
        # Mass and inertia inputs
931
        fst_vt['ElastoDyn']['TipMass(1)'] = 0.
1✔
932
        fst_vt['ElastoDyn']['TipMass(2)'] = 0.
1✔
933
        fst_vt['ElastoDyn']['TipMass(3)'] = 0.
1✔
934

935
        tower_base_height = max(float(inputs['tower_base_height']), float(inputs["platform_total_center_of_mass"][2]))
1✔
936
        fst_vt['ElastoDyn']['TowerBsHt'] = tower_base_height # Height of tower base above ground level [onshore] or MSL [offshore] (meters)
1✔
937
        fst_vt['ElastoDyn']['TowerHt']   = tower_top_height
1✔
938

939
        # TODO: There is some confusion on PtfmRefzt
940
        # DZ: based on the openfast r-tests:
941
        #   if this is floating, the z ref. point is 0.  Is this the reference that platform_total_center_of_mass is relative to?
942
        #   if fixed bottom, it's the tower base height.
943
        if modopt['flags']['floating']:
1✔
944
            fst_vt['ElastoDyn']['PtfmMass'] = float(inputs["platform_mass"])
1✔
945
            fst_vt['ElastoDyn']['PtfmRIner'] = float(inputs["platform_I_total"][0])
1✔
946
            fst_vt['ElastoDyn']['PtfmPIner'] = float(inputs["platform_I_total"][1])
1✔
947
            fst_vt['ElastoDyn']['PtfmYIner'] = float(inputs["platform_I_total"][2])
1✔
948
            fst_vt['ElastoDyn']['PtfmCMxt'] = float(inputs["platform_total_center_of_mass"][0])
1✔
949
            fst_vt['ElastoDyn']['PtfmCMyt'] = float(inputs["platform_total_center_of_mass"][1])
1✔
950
            fst_vt['ElastoDyn']['PtfmCMzt'] = float(inputs["platform_total_center_of_mass"][2])
1✔
951
            fst_vt['ElastoDyn']['PtfmRefzt'] = 0. # Vertical distance from the ground level [onshore] or MSL [offshore] to the platform reference point (meters)
1✔
952

953
        else:
954
            # Ptfm* can capture the transition piece for fixed-bottom, but we are doing that in subdyn, so only worry about getting height right
955
            fst_vt['ElastoDyn']['PtfmMass'] = 0.
1✔
956
            fst_vt['ElastoDyn']['PtfmRIner'] = 0.
1✔
957
            fst_vt['ElastoDyn']['PtfmPIner'] = 0.
1✔
958
            # Advice from R. Bergua- Use a dummy quantity (at least 1e4) here when have fixed-bottom support
959
            fst_vt['ElastoDyn']['PtfmYIner'] = 1e8 if modopt['flags']['offshore'] else 0.0
1✔
960
            fst_vt['ElastoDyn']['PtfmCMxt'] = 0.
1✔
961
            fst_vt['ElastoDyn']['PtfmCMyt'] = 0.
1✔
962
            fst_vt['ElastoDyn']['PtfmCMzt'] = float(inputs['tower_base_height'])
1✔
963
            fst_vt['ElastoDyn']['PtfmRefzt'] = tower_base_height # Vertical distance from the ground level [onshore] or MSL [offshore] to the platform reference point (meters)
1✔
964

965
        # Drivetrain inputs
966
        fst_vt['ElastoDyn']['DTTorSpr'] = float(inputs['drivetrain_spring_constant'])
1✔
967
        fst_vt['ElastoDyn']['DTTorDmp'] = float(inputs['drivetrain_damping_coefficient'])
1✔
968

969
        # Update Inflowwind
970
        fst_vt['InflowWind']['RefHt'] = float(inputs['hub_height'])
1✔
971
        fst_vt['InflowWind']['RefHt_Uni'] = float(inputs['hub_height'])
1✔
972
        fst_vt['InflowWind']['PLexp'] = float(inputs['shearExp'])
1✔
973
        if fst_vt['InflowWind']['NWindVel'] == 1:
1✔
974
            fst_vt['InflowWind']['WindVxiList'] = 0.
1✔
975
            fst_vt['InflowWind']['WindVyiList'] = 0.
1✔
976
            fst_vt['InflowWind']['WindVziList'] = float(inputs['hub_height'])
1✔
977
        else:
978
            raise Exception('The code only supports InflowWind NWindVel == 1')
×
979

980
        # Update AeroDyn Tower Input File starting one station above ground to avoid error because the wind grid hits the ground
981
        twr_elev  = inputs['tower_z']
1✔
982
        twr_d     = inputs['tower_outer_diameter']
1✔
983
        twr_index = np.argmin(abs(twr_elev - np.maximum(1.0, tower_base_height)))
1✔
984
        cd_index  = 0
1✔
985
        if twr_elev[twr_index] <= 1.:
1✔
986
            twr_index += 1
1✔
987
            cd_index  += 1
1✔
988
        fst_vt['AeroDyn15']['NumTwrNds'] = len(twr_elev[twr_index:])
1✔
989
        fst_vt['AeroDyn15']['TwrElev']   = twr_elev[twr_index:]
1✔
990
        fst_vt['AeroDyn15']['TwrDiam']   = twr_d[twr_index:]
1✔
991
        fst_vt['AeroDyn15']['TwrCd']     = inputs['tower_cd'][cd_index:]
1✔
992
        fst_vt['AeroDyn15']['TwrTI']     = np.ones(len(twr_elev[twr_index:])) * fst_vt['AeroDyn15']['TwrTI']
1✔
993
        fst_vt['AeroDyn15']['TwrCb']     = np.ones(len(twr_elev[twr_index:])) * fst_vt['AeroDyn15']['TwrCb']
1✔
994

995
        z_tow = twr_elev
1✔
996
        z_sec, _ = util.nodal2sectional(z_tow)
1✔
997
        sec_loc = (z_sec - z_sec[0]) / (z_sec[-1] - z_sec[0])
1✔
998
        fst_vt['ElastoDynTower']['NTwInpSt'] = len(sec_loc)
1✔
999
        fst_vt['ElastoDynTower']['HtFract']  = sec_loc
1✔
1000
        fst_vt['ElastoDynTower']['TMassDen'] = inputs['mass_den']
1✔
1001
        fst_vt['ElastoDynTower']['TwFAStif'] = inputs['foreaft_stff']
1✔
1002
        fst_vt['ElastoDynTower']['TwSSStif'] = inputs['sideside_stff']
1✔
1003
        fst_vt['ElastoDynTower']['TwFAM1Sh'] = inputs['fore_aft_modes'][0, :]  / sum(inputs['fore_aft_modes'][0, :])
1✔
1004
        fst_vt['ElastoDynTower']['TwFAM2Sh'] = inputs['fore_aft_modes'][1, :]  / sum(inputs['fore_aft_modes'][1, :])
1✔
1005
        fst_vt['ElastoDynTower']['TwSSM1Sh'] = inputs['side_side_modes'][0, :] / sum(inputs['side_side_modes'][0, :])
1✔
1006
        fst_vt['ElastoDynTower']['TwSSM2Sh'] = inputs['side_side_modes'][1, :] / sum(inputs['side_side_modes'][1, :])
1✔
1007
        
1008
        # Calculate yaw stiffness of tower (springs in series) and use in servodyn as yaw spring constant
1009
        k_tow_tor = inputs['tor_stff'] / np.diff(inputs['tower_z'])
1✔
1010
        k_tow_tor = 1.0/np.sum(1.0/k_tow_tor)
1✔
1011
        # R. Bergua's suggestion to set the stiffness to the tower torsional stiffness and the
1012
        # damping to the frequency of the first tower torsional mode- easier than getting the yaw inertia right
1013
        damp_ratio = 0.01
1✔
1014
        f_torsion = float(inputs['tor_freq'])
1✔
1015
        fst_vt['ServoDyn']['YawSpr'] = k_tow_tor
1✔
1016
        if f_torsion > 0.0:
1✔
1017
            fst_vt['ServoDyn']['YawDamp'] = damp_ratio * k_tow_tor / np.pi / f_torsion
1✔
1018
        else:
1019
            fst_vt['ServoDyn']['YawDamp'] = 2 * damp_ratio * np.sqrt(k_tow_tor * inputs['rna_I_TT'][2])
1✔
1020

1021
        # Update ElastoDyn Blade Input File
1022
        fst_vt['ElastoDynBlade']['NBlInpSt']   = len(inputs['r'])
1✔
1023
        fst_vt['ElastoDynBlade']['BlFract']    = (inputs['r']-inputs['Rhub'])/(inputs['Rtip']-inputs['Rhub'])
1✔
1024
        fst_vt['ElastoDynBlade']['BlFract'][0] = 0.
1✔
1025
        fst_vt['ElastoDynBlade']['BlFract'][-1]= 1.
1✔
1026
        fst_vt['ElastoDynBlade']['PitchAxis']  = inputs['le_location']
1✔
1027
        fst_vt['ElastoDynBlade']['StrcTwst']   = inputs['theta'] # to do: structural twist is not nessessarily (nor likely to be) the same as aero twist
1✔
1028
        fst_vt['ElastoDynBlade']['BMassDen']   = inputs['beam:rhoA']
1✔
1029
        fst_vt['ElastoDynBlade']['FlpStff']    = inputs['beam:EIyy']
1✔
1030
        fst_vt['ElastoDynBlade']['EdgStff']    = inputs['beam:EIxx']
1✔
1031
        fst_vt['ElastoDynBlade']['BldFl1Sh']   = np.zeros(5)
1✔
1032
        fst_vt['ElastoDynBlade']['BldFl2Sh']   = np.zeros(5)
1✔
1033
        fst_vt['ElastoDynBlade']['BldEdgSh']   = np.zeros(5)
1✔
1034
        for i in range(5):
1✔
1035
            fst_vt['ElastoDynBlade']['BldFl1Sh'][i] = inputs['flap_mode_shapes'][0,i] / sum(inputs['flap_mode_shapes'][0,:])
1✔
1036
            fst_vt['ElastoDynBlade']['BldFl2Sh'][i] = inputs['flap_mode_shapes'][1,i] / sum(inputs['flap_mode_shapes'][1,:])
1✔
1037
            fst_vt['ElastoDynBlade']['BldEdgSh'][i] = inputs['edge_mode_shapes'][0,i] / sum(inputs['edge_mode_shapes'][0,:])
1✔
1038

1039
        # Update AeroDyn15
1040
        fst_vt['AeroDyn15']['AirDens']   = float(inputs['rho'])
1✔
1041
        fst_vt['AeroDyn15']['KinVisc']   = inputs['mu'][0] / inputs['rho'][0]
1✔
1042
        fst_vt['AeroDyn15']['SpdSound']  = float(inputs['speed_sound_air'])
1✔
1043

1044
        # Update AeroDyn15 Blade Input File
1045
        r = (inputs['r']-inputs['Rhub'])
1✔
1046
        r[0]  = 0.
1✔
1047
        r[-1] = inputs['Rtip']-inputs['Rhub']
1✔
1048
        fst_vt['AeroDynBlade']['NumBlNds'] = self.n_span
1✔
1049
        fst_vt['AeroDynBlade']['BlSpn']    = r
1✔
1050
        BlCrvAC, BlSwpAC = self.get_ac_axis(inputs)
1✔
1051
        fst_vt['AeroDynBlade']['BlCrvAC']  = BlCrvAC
1✔
1052
        fst_vt['AeroDynBlade']['BlSwpAC']  = BlSwpAC
1✔
1053
        fst_vt['AeroDynBlade']['BlCrvAng'] = np.degrees(np.arcsin(np.gradient(BlCrvAC)/np.gradient(r)))
1✔
1054
        fst_vt['AeroDynBlade']['BlTwist']  = inputs['theta']
1✔
1055
        fst_vt['AeroDynBlade']['BlChord']  = inputs['chord']
1✔
1056
        fst_vt['AeroDynBlade']['BlAFID']   = np.asarray(range(1,self.n_span+1))
1✔
1057

1058
        # Update AeroDyn15 Airfoile Input Files
1059
        # airfoils = inputs['airfoils']
1060
        fst_vt['AeroDyn15']['NumAFfiles'] = self.n_span
1✔
1061
        # fst_vt['AeroDyn15']['af_data'] = [{}]*len(airfoils)
1062
        fst_vt['AeroDyn15']['af_data'] = []
1✔
1063

1064
        # Set the AD15 flag AFTabMod, deciding whether we use more Re per airfoil or user-defined tables (used for example in distributed aerodynamic control)
1065
        if fst_vt['AeroDyn15']['AFTabMod'] == 1:
1✔
1066
            # If AFTabMod is the default coming form the schema, check the value from WISDEM, which might be set to 2 if more Re per airfoil are defined in the geometry yaml
1067
            fst_vt['AeroDyn15']['AFTabMod'] = modopt["WISDEM"]["RotorSE"]["AFTabMod"]
1✔
1068
        if self.n_tab > 1 and fst_vt['AeroDyn15']['AFTabMod'] == 1:
1✔
1069
            fst_vt['AeroDyn15']['AFTabMod'] = 3
×
1070
        elif self.n_tab > 1 and fst_vt['AeroDyn15']['AFTabMod'] == 2:
1✔
1071
            raise Exception('OpenFAST does not support both multiple Re and multiple user defined tabs. Please remove DAC devices or Re polars')
×
1072

1073
        for i in range(self.n_span): # No of blade radial stations
1✔
1074

1075
            fst_vt['AeroDyn15']['af_data'].append([])
1✔
1076

1077
            if fst_vt['AeroDyn15']['AFTabMod'] == 1:
1✔
1078
                loop_index = 1
1✔
1079
            elif fst_vt['AeroDyn15']['AFTabMod'] == 2:
×
1080
                loop_index = self.n_Re
×
1081
            else:
1082
                loop_index = self.n_tab
×
1083

1084
            for j in range(loop_index): # Number of tabs or Re
1✔
1085
                if fst_vt['AeroDyn15']['AFTabMod'] == 1:
1✔
1086
                    unsteady = eval_unsteady(inputs['airfoils_aoa'], inputs['airfoils_cl'][i,:,0,0], inputs['airfoils_cd'][i,:,0,0], inputs['airfoils_cm'][i,:,0,0])
1✔
1087
                elif fst_vt['AeroDyn15']['AFTabMod'] == 2:
×
1088
                    unsteady = eval_unsteady(inputs['airfoils_aoa'], inputs['airfoils_cl'][i,:,j,0], inputs['airfoils_cd'][i,:,j,0], inputs['airfoils_cm'][i,:,j,0])
×
1089
                else:
1090
                    unsteady = eval_unsteady(inputs['airfoils_aoa'], inputs['airfoils_cl'][i,:,0,j], inputs['airfoils_cd'][i,:,0,j], inputs['airfoils_cm'][i,:,0,j])
×
1091

1092
                fst_vt['AeroDyn15']['af_data'][i].append({})
1✔
1093

1094

1095
                fst_vt['AeroDyn15']['af_data'][i][j]['InterpOrd'] = "DEFAULT"
1✔
1096
                fst_vt['AeroDyn15']['af_data'][i][j]['NonDimArea']= 1
1✔
1097
                if modopt['General']['openfast_configuration']['generate_af_coords']:
1✔
1098
                    fst_vt['AeroDyn15']['af_data'][i][j]['NumCoords'] = '@"AF{:02d}_Coords.txt"'.format(i)
×
1099
                else:
1100
                    fst_vt['AeroDyn15']['af_data'][i][j]['NumCoords'] = '0'
1✔
1101

1102
                fst_vt['AeroDyn15']['af_data'][i][j]['NumTabs']   = loop_index
1✔
1103
                if fst_vt['AeroDyn15']['AFTabMod'] == 3:
1✔
1104
                    fst_vt['AeroDyn15']['af_data'][i][j]['Ctrl'] = inputs['airfoils_Ctrl'][i,0,j]  # unsteady['Ctrl'] # added to unsteady function for variable flap controls at airfoils
×
1105
                    fst_vt['AeroDyn15']['af_data'][i][j]['Re']   = inputs['airfoils_Re'][0] # If AFTabMod==3 the Re is neglected, but it still must be the same across tables
×
1106
                else:
1107
                    fst_vt['AeroDyn15']['af_data'][i][j]['Re']   = inputs['airfoils_Re'][j]
1✔
1108
                    fst_vt['AeroDyn15']['af_data'][i][j]['Ctrl'] = 0.
1✔
1109
                fst_vt['AeroDyn15']['af_data'][i][j]['InclUAdata']= "True"
1✔
1110
                fst_vt['AeroDyn15']['af_data'][i][j]['alpha0']    = unsteady['alpha0']
1✔
1111
                fst_vt['AeroDyn15']['af_data'][i][j]['alpha1']    = max(unsteady['alpha0'], unsteady['alpha1'])
1✔
1112
                fst_vt['AeroDyn15']['af_data'][i][j]['alpha2']    = min(unsteady['alpha0'], unsteady['alpha2'])
1✔
1113
                fst_vt['AeroDyn15']['af_data'][i][j]['eta_e']     = unsteady['eta_e']
1✔
1114
                fst_vt['AeroDyn15']['af_data'][i][j]['C_nalpha']  = unsteady['C_nalpha']
1✔
1115
                fst_vt['AeroDyn15']['af_data'][i][j]['T_f0']      = unsteady['T_f0']
1✔
1116
                fst_vt['AeroDyn15']['af_data'][i][j]['T_V0']      = unsteady['T_V0']
1✔
1117
                fst_vt['AeroDyn15']['af_data'][i][j]['T_p']       = unsteady['T_p']
1✔
1118
                fst_vt['AeroDyn15']['af_data'][i][j]['T_VL']      = unsteady['T_VL']
1✔
1119
                fst_vt['AeroDyn15']['af_data'][i][j]['b1']        = unsteady['b1']
1✔
1120
                fst_vt['AeroDyn15']['af_data'][i][j]['b2']        = unsteady['b2']
1✔
1121
                fst_vt['AeroDyn15']['af_data'][i][j]['b5']        = unsteady['b5']
1✔
1122
                fst_vt['AeroDyn15']['af_data'][i][j]['A1']        = unsteady['A1']
1✔
1123
                fst_vt['AeroDyn15']['af_data'][i][j]['A2']        = unsteady['A2']
1✔
1124
                fst_vt['AeroDyn15']['af_data'][i][j]['A5']        = unsteady['A5']
1✔
1125
                fst_vt['AeroDyn15']['af_data'][i][j]['S1']        = unsteady['S1']
1✔
1126
                fst_vt['AeroDyn15']['af_data'][i][j]['S2']        = unsteady['S2']
1✔
1127
                fst_vt['AeroDyn15']['af_data'][i][j]['S3']        = unsteady['S3']
1✔
1128
                fst_vt['AeroDyn15']['af_data'][i][j]['S4']        = unsteady['S4']
1✔
1129
                fst_vt['AeroDyn15']['af_data'][i][j]['Cn1']       = unsteady['Cn1']
1✔
1130
                fst_vt['AeroDyn15']['af_data'][i][j]['Cn2']       = unsteady['Cn2']
1✔
1131
                fst_vt['AeroDyn15']['af_data'][i][j]['St_sh']     = unsteady['St_sh']
1✔
1132
                fst_vt['AeroDyn15']['af_data'][i][j]['Cd0']       = unsteady['Cd0']
1✔
1133
                fst_vt['AeroDyn15']['af_data'][i][j]['Cm0']       = unsteady['Cm0']
1✔
1134
                fst_vt['AeroDyn15']['af_data'][i][j]['k0']        = unsteady['k0']
1✔
1135
                fst_vt['AeroDyn15']['af_data'][i][j]['k1']        = unsteady['k1']
1✔
1136
                fst_vt['AeroDyn15']['af_data'][i][j]['k2']        = unsteady['k2']
1✔
1137
                fst_vt['AeroDyn15']['af_data'][i][j]['k3']        = unsteady['k3']
1✔
1138
                fst_vt['AeroDyn15']['af_data'][i][j]['k1_hat']    = unsteady['k1_hat']
1✔
1139
                fst_vt['AeroDyn15']['af_data'][i][j]['x_cp_bar']  = unsteady['x_cp_bar']
1✔
1140
                fst_vt['AeroDyn15']['af_data'][i][j]['UACutout']  = unsteady['UACutout']
1✔
1141
                fst_vt['AeroDyn15']['af_data'][i][j]['filtCutOff']= unsteady['filtCutOff']
1✔
1142
                fst_vt['AeroDyn15']['af_data'][i][j]['NumAlf']    = len(unsteady['Alpha'])
1✔
1143
                fst_vt['AeroDyn15']['af_data'][i][j]['Alpha']     = np.array(unsteady['Alpha'])
1✔
1144
                fst_vt['AeroDyn15']['af_data'][i][j]['Cl']        = np.array(unsteady['Cl'])
1✔
1145
                fst_vt['AeroDyn15']['af_data'][i][j]['Cd']        = np.array(unsteady['Cd'])
1✔
1146
                fst_vt['AeroDyn15']['af_data'][i][j]['Cm']        = np.array(unsteady['Cm'])
1✔
1147
                fst_vt['AeroDyn15']['af_data'][i][j]['Cpmin']     = np.zeros_like(unsteady['Cm'])
1✔
1148

1149
        fst_vt['AeroDyn15']['af_coord'] = []
1✔
1150
        fst_vt['AeroDyn15']['rthick']   = np.zeros(self.n_span)
1✔
1151
        fst_vt['AeroDyn15']['ac']   = np.zeros(self.n_span)
1✔
1152
        for i in range(self.n_span):
1✔
1153
            fst_vt['AeroDyn15']['af_coord'].append({})
1✔
1154
            fst_vt['AeroDyn15']['af_coord'][i]['x']  = inputs['coord_xy_interp'][i,:,0]
1✔
1155
            fst_vt['AeroDyn15']['af_coord'][i]['y']  = inputs['coord_xy_interp'][i,:,1]
1✔
1156
            fst_vt['AeroDyn15']['rthick'][i]         = inputs['rthick'][i]
1✔
1157
            fst_vt['AeroDyn15']['ac'][i]             = inputs['ac'][i]
1✔
1158

1159
        # # AeroDyn blade spanwise output positions
1160
        r_out_target  = [0.1, 0.20, 0.30, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9]
1✔
1161
        r = r/r[-1]
1✔
1162
        idx_out       = [np.argmin(abs(r-ri)) for ri in r_out_target]
1✔
1163
        self.R_out_AD = [fst_vt['AeroDynBlade']['BlSpn'][i] for i in idx_out]
1✔
1164
        if len(self.R_out_AD) != len(np.unique(self.R_out_AD)):
1✔
1165
            raise Exception('ERROR: the spanwise resolution is too coarse and does not support 9 channels along blade span. Please increase it in the modeling_options.yaml.')
×
1166
        fst_vt['AeroDyn15']['BlOutNd']  = [str(idx+1) for idx in idx_out]
1✔
1167
        fst_vt['AeroDyn15']['NBlOuts']  = len(idx_out)
1✔
1168

1169
        # ElastoDyn blade spanwise output positions
1170
        nBldNodes     = fst_vt['ElastoDyn']['BldNodes']
1✔
1171
        bld_fract     = np.arange(1./nBldNodes/2., 1, 1./nBldNodes)
1✔
1172
        idx_out       = [np.argmin(abs(bld_fract-ri)) for ri in r_out_target]
1✔
1173
        r_nodes       = bld_fract*(fst_vt['ElastoDyn']['TipRad']-fst_vt['ElastoDyn']['HubRad']) + fst_vt['ElastoDyn']['HubRad']
1✔
1174
        self.R_out_ED_bl = np.hstack((fst_vt['ElastoDyn']['HubRad'], [r_nodes[i] for i in idx_out]))
1✔
1175
        if len(self.R_out_ED_bl) != len(np.unique(self.R_out_ED_bl)):
1✔
1176
            raise Exception('ERROR: the spanwise resolution is too coarse and does not support 9 channels along blade span. Please increase it in the modeling_options.yaml.')
×
1177
        fst_vt['ElastoDyn']['BldGagNd'] = [idx+1 for idx in idx_out]
1✔
1178
        fst_vt['ElastoDyn']['NBlGages'] = len(idx_out)
1✔
1179

1180
        # ElastoDyn tower output positions along height
1181
        fst_vt['ElastoDyn']['NTwGages'] = 9
1✔
1182
        nTwrNodes = fst_vt['ElastoDyn']['TwrNodes']
1✔
1183
        twr_fract = np.arange(1./nTwrNodes/2., 1, 1./nTwrNodes)
1✔
1184
        idx_out = [np.argmin(abs(twr_fract-ri)) for ri in r_out_target]
1✔
1185
        fst_vt['ElastoDyn']['TwrGagNd'] = [idx+1 for idx in idx_out]
1✔
1186
        fst_vt['AeroDyn15']['NTwOuts'] = 0
1✔
1187
        self.Z_out_ED_twr = np.hstack((0., [twr_fract[i] for i in idx_out], 1.))
1✔
1188
        if len(np.unique(self.Z_out_ED_twr)) < len(self.Z_out_ED_twr):
1✔
1189
            raise Exception('The minimum number of tower nodes for WEIS to compute forces along the tower height is 11.')
×
1190

1191
        # SubDyn inputs- monopile and floating
1192
        if modopt['flags']['monopile']:
1✔
1193
            mono_d = inputs['monopile_outer_diameter']
1✔
1194
            mono_t = inputs['monopile_wall_thickness']
1✔
1195
            mono_elev = inputs['monopile_z']
1✔
1196
            n_joints = len(mono_d[1:]) # Omit submerged pile
1✔
1197
            n_members = n_joints - 1
1✔
1198
            itrans = n_joints - 1
1✔
1199
            fst_vt['SubDyn']['JointXss'] = np.zeros( n_joints )
1✔
1200
            fst_vt['SubDyn']['JointYss'] = np.zeros( n_joints )
1✔
1201
            fst_vt['SubDyn']['JointZss'] = mono_elev[1:]
1✔
1202
            fst_vt['SubDyn']['NReact'] = 1
1✔
1203
            fst_vt['SubDyn']['RJointID'] = [1]
1✔
1204
            fst_vt['SubDyn']['RctTDXss'] = fst_vt['SubDyn']['RctTDYss'] = fst_vt['SubDyn']['RctTDZss'] = [1]
1✔
1205
            fst_vt['SubDyn']['RctRDXss'] = fst_vt['SubDyn']['RctRDYss'] = fst_vt['SubDyn']['RctRDZss'] = [1]
1✔
1206
            fst_vt['SubDyn']['NInterf'] = 1
1✔
1207
            fst_vt['SubDyn']['IJointID'] = [n_joints]
1✔
1208
            fst_vt['SubDyn']['MJointID1'] = np.arange( n_members, dtype=np.int_ ) + 1
1✔
1209
            fst_vt['SubDyn']['MJointID2'] = np.arange( n_members, dtype=np.int_ ) + 2
1✔
1210
            fst_vt['SubDyn']['YoungE1'] = inputs['monopile_E'][1:]
1✔
1211
            fst_vt['SubDyn']['ShearG1'] = inputs['monopile_G'][1:]
1✔
1212
            fst_vt['SubDyn']['MatDens1'] = inputs['monopile_rho'][1:]
1✔
1213
            fst_vt['SubDyn']['XsecD'] = util.nodal2sectional(mono_d[1:])[0] # Don't need deriv
1✔
1214
            fst_vt['SubDyn']['XsecT'] = mono_t[1:]
1✔
1215
            
1216
            # Find the members where the 9 channels of SubDyn should be placed
1217
            grid_joints_monopile = (fst_vt['SubDyn']['JointZss'] - fst_vt['SubDyn']['JointZss'][0]) / (fst_vt['SubDyn']['JointZss'][-1] - fst_vt['SubDyn']['JointZss'][0])
1✔
1218
            n_channels = 9
1✔
1219
            grid_target = np.linspace(0., 0.999999999, n_channels)
1✔
1220
            idx_out = [np.where(grid_i >= grid_joints_monopile)[0][-1] for grid_i in grid_target]
1✔
1221
            idx_out = np.unique(idx_out)
1✔
1222
            fst_vt['SubDyn']['NMOutputs'] = len(idx_out)
1✔
1223
            fst_vt['SubDyn']['MemberID_out'] = [idx+1 for idx in idx_out]
1✔
1224
            fst_vt['SubDyn']['NOutCnt'] = np.ones_like(fst_vt['SubDyn']['MemberID_out'])
1✔
1225
            fst_vt['SubDyn']['NodeCnt'] = np.ones_like(fst_vt['SubDyn']['MemberID_out'])
1✔
1226
            fst_vt['SubDyn']['NodeCnt'][-1] = 2
1✔
1227
            self.Z_out_SD_mpl = [grid_joints_monopile[i] for i in idx_out]
1✔
1228

1229
        elif modopt['flags']['floating']:
1✔
1230
            joints_xyz = inputs["platform_nodes"]
1✔
1231
            n_joints = np.where(joints_xyz[:, 0] == NULL)[0][0]
1✔
1232
            joints_xyz = joints_xyz[:n_joints, :]
1✔
1233
            itrans = util.closest_node(joints_xyz, inputs["transition_node"])
1✔
1234

1235
            N1 = np.int_(inputs["platform_elem_n1"])
1✔
1236
            n_members = np.where(N1 == NULL)[0][0]
1✔
1237
            N1 = N1[:n_members]
1✔
1238
            N2 = np.int_(inputs["platform_elem_n2"][:n_members])
1✔
1239

1240
            fst_vt['SubDyn']['JointXss'] = joints_xyz[:,0]
1✔
1241
            fst_vt['SubDyn']['JointYss'] = joints_xyz[:,1]
1✔
1242
            fst_vt['SubDyn']['JointZss'] = joints_xyz[:,2]
1✔
1243
            fst_vt['SubDyn']['NReact'] = 0
1✔
1244
            fst_vt['SubDyn']['RJointID'] = []
1✔
1245
            fst_vt['SubDyn']['RctTDXss'] = fst_vt['SubDyn']['RctTDYss'] = fst_vt['SubDyn']['RctTDZss'] = []
1✔
1246
            fst_vt['SubDyn']['RctRDXss'] = fst_vt['SubDyn']['RctRDYss'] = fst_vt['SubDyn']['RctRDZss'] = []
1✔
1247
            if modopt['floating']['transition_joint'] is None:
1✔
1248
                fst_vt['SubDyn']['NInterf'] = 0
×
1249
                fst_vt['SubDyn']['IJointID'] = []
×
1250
            else:
1251
                fst_vt['SubDyn']['NInterf'] = 1
1✔
1252
                fst_vt['SubDyn']['IJointID'] = [itrans+1]
1✔
1253
            fst_vt['SubDyn']['MJointID1'] = N1+1
1✔
1254
            fst_vt['SubDyn']['MJointID2'] = N2+1
1✔
1255

1256
            fst_vt['SubDyn']['YoungE1'] = inputs["platform_elem_E"][:n_members]
1✔
1257
            fst_vt['SubDyn']['ShearG1'] = inputs["platform_elem_G"][:n_members]
1✔
1258
            fst_vt['SubDyn']['MatDens1'] = inputs["platform_elem_rho"][:n_members]
1✔
1259
            fst_vt['SubDyn']['XsecD'] = inputs["platform_elem_D"][:n_members]
1✔
1260
            fst_vt['SubDyn']['XsecT'] = inputs["platform_elem_t"][:n_members]
1✔
1261

1262
        # SubDyn inputs- offshore generic
1263
        if modopt['flags']['offshore']:
1✔
1264
            mgrav = 0.0 if not modopt['flags']['monopile'] else float(inputs['gravity_foundation_mass'])
1✔
1265
            if fst_vt['SubDyn']['SDdeltaT']<=-999.0: fst_vt['SubDyn']['SDdeltaT'] = "DEFAULT"
1✔
1266
            fst_vt['SubDyn']['GuyanDamp'] = np.vstack( tuple([fst_vt['SubDyn']['GuyanDamp'+str(m+1)] for m in range(6)]) )
1✔
1267
            fst_vt['SubDyn']['Rct_SoilFile'] = [""]*fst_vt['SubDyn']['NReact']
1✔
1268
            fst_vt['SubDyn']['NJoints'] = n_joints
1✔
1269
            fst_vt['SubDyn']['JointID'] = np.arange( n_joints, dtype=np.int_) + 1
1✔
1270
            fst_vt['SubDyn']['JointType'] = np.ones( n_joints, dtype=np.int_)
1✔
1271
            fst_vt['SubDyn']['JointDirX'] = fst_vt['SubDyn']['JointDirY'] = fst_vt['SubDyn']['JointDirZ'] = np.zeros( n_joints )
1✔
1272
            fst_vt['SubDyn']['JointStiff'] = np.zeros( n_joints )
1✔
1273
            fst_vt['SubDyn']['ItfTDXss'] = fst_vt['SubDyn']['ItfTDYss'] = fst_vt['SubDyn']['ItfTDZss'] = [1]
1✔
1274
            fst_vt['SubDyn']['ItfRDXss'] = fst_vt['SubDyn']['ItfRDYss'] = fst_vt['SubDyn']['ItfRDZss'] = [1]
1✔
1275
            fst_vt['SubDyn']['NMembers'] = n_members
1✔
1276
            fst_vt['SubDyn']['MemberID'] = np.arange( n_members, dtype=np.int_ ) + 1
1✔
1277
            fst_vt['SubDyn']['MPropSetID1'] = fst_vt['SubDyn']['MPropSetID2'] = np.arange( n_members, dtype=np.int_ ) + 1
1✔
1278
            fst_vt['SubDyn']['MType'] = np.ones( n_members, dtype=np.int_ )
1✔
1279
            fst_vt['SubDyn']['NPropSets'] = n_members
1✔
1280
            fst_vt['SubDyn']['PropSetID1'] = np.arange( n_members, dtype=np.int_ ) + 1
1✔
1281
            fst_vt['SubDyn']['NCablePropSets'] = 0
1✔
1282
            fst_vt['SubDyn']['NRigidPropSets'] = 0
1✔
1283
            fst_vt['SubDyn']['NCOSMs'] = 0
1✔
1284
            fst_vt['SubDyn']['NXPropSets'] = 0
1✔
1285
            fst_vt['SubDyn']['NCmass'] = 2 if mgrav > 0.0 else 1
1✔
1286
            fst_vt['SubDyn']['CMJointID'] = [itrans+1]
1✔
1287
            fst_vt['SubDyn']['JMass'] = [float(inputs['transition_piece_mass'])]
1✔
1288
            fst_vt['SubDyn']['JMXX'] = [inputs['transition_piece_I'][0]]
1✔
1289
            fst_vt['SubDyn']['JMYY'] = [inputs['transition_piece_I'][1]]
1✔
1290
            fst_vt['SubDyn']['JMZZ'] = [inputs['transition_piece_I'][2]]
1✔
1291
            fst_vt['SubDyn']['JMXY'] = fst_vt['SubDyn']['JMXZ'] = fst_vt['SubDyn']['JMYZ'] = [0.0]
1✔
1292
            fst_vt['SubDyn']['MCGX'] = fst_vt['SubDyn']['MCGY'] = fst_vt['SubDyn']['MCGZ'] = [0.0]
1✔
1293
            if mgrav > 0.0:
1✔
1294
                fst_vt['SubDyn']['CMJointID'] += [1]
×
1295
                fst_vt['SubDyn']['JMass'] += [mgrav]
×
1296
                fst_vt['SubDyn']['JMXX'] += [inputs['gravity_foundation_I'][0]]
×
1297
                fst_vt['SubDyn']['JMYY'] += [inputs['gravity_foundation_I'][1]]
×
1298
                fst_vt['SubDyn']['JMZZ'] += [inputs['gravity_foundation_I'][2]]
×
1299
                fst_vt['SubDyn']['JMXY'] += [0.0]
×
1300
                fst_vt['SubDyn']['JMXZ'] += [0.0]
×
1301
                fst_vt['SubDyn']['JMYZ'] += [0.0]
×
1302
                fst_vt['SubDyn']['MCGX'] += [0.0]
×
1303
                fst_vt['SubDyn']['MCGY'] += [0.0]
×
1304
                fst_vt['SubDyn']['MCGZ'] += [0.0]
×
1305

1306

1307
        # HydroDyn inputs
1308
        if modopt['flags']['monopile']:
1✔
1309
            z_coarse = make_coarse_grid(mono_elev[1:], mono_d[1:])
1✔
1310
            # Don't want any nodes near zero for annoying hydrodyn errors
1311
            idx0 = np.intersect1d(np.where(z_coarse>-0.5), np.where(z_coarse<0.5))
1✔
1312
            z_coarse = np.delete(z_coarse, idx0) 
1✔
1313
            n_joints = len(z_coarse)
1✔
1314
            n_members = n_joints - 1
1✔
1315
            joints_xyz = np.c_[np.zeros((n_joints,2)), z_coarse]
1✔
1316
            d_coarse = np.interp(z_coarse, mono_elev[1:], mono_d[1:])
1✔
1317
            t_coarse = util.sectional_interp(z_coarse, mono_elev[1:], mono_t[1:])
1✔
1318
            N1 = np.arange( n_members, dtype=np.int_ ) + 1
1✔
1319
            N2 = np.arange( n_members, dtype=np.int_ ) + 2
1✔
1320
            
1321
        elif modopt['flags']['floating']:
1✔
1322
            joints_xyz = np.empty((0, 3))
1✔
1323
            N1 = np.array([], dtype=np.int_)
1✔
1324
            N2 = np.array([], dtype=np.int_)
1✔
1325
            d_coarse = np.array([])
1✔
1326
            t_coarse = np.array([])
1✔
1327
            
1328
            # Look over members and grab all nodes and internal connections
1329
            n_member = modopt["floating"]["members"]["n_members"]
1✔
1330
            for k in range(n_member):
1✔
1331
                s_grid = inputs[f"member{k}:s"]
1✔
1332
                idiam = inputs[f"member{k}:outer_diameter"]
1✔
1333
                s_coarse = make_coarse_grid(s_grid, idiam)
1✔
1334
                s_coarse = np.unique( np.minimum( np.maximum(s_coarse, inputs[f"member{k}:s_ghost1"]), inputs[f"member{k}:s_ghost2"]) )
1✔
1335
                id_coarse = np.interp(s_coarse, s_grid, idiam)
1✔
1336
                it_coarse = util.sectional_interp(s_coarse, s_grid, inputs[f"member{k}:wall_thickness"])
1✔
1337
                xyz0 = inputs[f"member{k}:joint1"]
1✔
1338
                xyz1 = inputs[f"member{k}:joint2"]
1✔
1339
                dxyz = xyz1 - xyz0
1✔
1340
                inode_xyz = np.outer(s_coarse, dxyz) + xyz0[np.newaxis, :]
1✔
1341
                inode_range = np.arange(inode_xyz.shape[0] - 1)
1✔
1342

1343
                nk = joints_xyz.shape[0]
1✔
1344
                N1 = np.append(N1, nk + inode_range + 1)
1✔
1345
                N2 = np.append(N2, nk + inode_range + 2)
1✔
1346
                d_coarse = np.append(d_coarse, id_coarse)  
1✔
1347
                t_coarse = np.append(t_coarse, it_coarse)  
1✔
1348
                joints_xyz = np.append(joints_xyz, inode_xyz, axis=0)
1✔
1349
                
1350
        if modopt['flags']['offshore']:
1✔
1351
            fst_vt['HydroDyn']['WtrDens'] = float(inputs['rho_water'])
1✔
1352
            fst_vt['HydroDyn']['WtrDpth'] = float(inputs['water_depth'])
1✔
1353
            fst_vt['HydroDyn']['MSL2SWL'] = 0.0
1✔
1354
            fst_vt['HydroDyn']['WaveHs'] = float(inputs['Hsig_wave'])
1✔
1355
            fst_vt['HydroDyn']['WaveTp'] = float(inputs['Tsig_wave'])
1✔
1356
            if fst_vt['HydroDyn']['WavePkShp']<=-999.0: fst_vt['HydroDyn']['WavePkShp'] = "DEFAULT"
1✔
1357
            fst_vt['HydroDyn']['WaveDir'] = float(inputs['beta_wave'])
1✔
1358
            fst_vt['HydroDyn']['WaveDirRange'] = fst_vt['HydroDyn']['WaveDirRange'] / np.rad2deg(1)
1✔
1359
            fst_vt['HydroDyn']['WaveElevxi'] = [str(m) for m in fst_vt['HydroDyn']['WaveElevxi']]
1✔
1360
            fst_vt['HydroDyn']['WaveElevyi'] = [str(m) for m in fst_vt['HydroDyn']['WaveElevyi']]
1✔
1361
            fst_vt['HydroDyn']['CurrSSDir'] = "DEFAULT" if fst_vt['HydroDyn']['CurrSSDir']<=-999.0 else np.rad2deg(fst_vt['HydroDyn']['CurrSSDir'])
1✔
1362
            fst_vt['HydroDyn']['AddF0'] = np.array( fst_vt['HydroDyn']['AddF0'] ).reshape(-1,1)
1✔
1363
            fst_vt['HydroDyn']['AddCLin'] = np.vstack( tuple([fst_vt['HydroDyn']['AddCLin'+str(m+1)] for m in range(6)]) )
1✔
1364
            fst_vt['HydroDyn']['AddBLin'] = np.vstack( tuple([fst_vt['HydroDyn']['AddBLin'+str(m+1)] for m in range(6)]) )
1✔
1365
            BQuad = np.vstack( tuple([fst_vt['HydroDyn']['AddBQuad'+str(m+1)] for m in range(6)]) )
1✔
1366
            if np.any(BQuad):
1✔
1367
                logger.warning('WARNING: You are adding in additional drag terms that may double count strip theory estimated viscous drag terms.  Please zero out the BQuad entries or use modeling options SimplCd/a/p and/or potential_model_override and/or potential_bem_members to suppress strip theory for the members')
1✔
1368
            fst_vt['HydroDyn']['AddBQuad'] = BQuad
1✔
1369
            fst_vt['HydroDyn']['NAxCoef'] = 1
1✔
1370
            fst_vt['HydroDyn']['AxCoefID'] = 1 + np.arange( fst_vt['HydroDyn']['NAxCoef'], dtype=np.int_)
1✔
1371
            fst_vt['HydroDyn']['AxCd'] = np.zeros( fst_vt['HydroDyn']['NAxCoef'] )
1✔
1372
            fst_vt['HydroDyn']['AxCa'] = np.zeros( fst_vt['HydroDyn']['NAxCoef'] )
1✔
1373
            fst_vt['HydroDyn']['AxCp'] = np.ones( fst_vt['HydroDyn']['NAxCoef'] )
1✔
1374
            # Use coarse member nodes for HydroDyn
1375

1376
            # Simplify members if using potential model only
1377
            if modopt["Level1"]["potential_model_override"] == 2:
1✔
1378
                joints_xyz = np.array([[0,0,0],[0,0,-1]])
1✔
1379
                N1 = np.array([N1[0]])
1✔
1380
                N2 = np.array([N2[0]])
1✔
1381
                
1382
            # Tweak z-position
1383
            idx = np.where(joints_xyz[:,2]==-fst_vt['HydroDyn']['WtrDpth'])[0]
1✔
1384
            if len(idx) > 0:
1✔
1385
                joints_xyz[idx,2] += 1e-2
1✔
1386
            # Store data
1387
            n_joints = joints_xyz.shape[0]
1✔
1388
            n_members = N1.shape[0]
1✔
1389
            ijoints = np.arange( n_joints, dtype=np.int_ ) + 1
1✔
1390
            imembers = np.arange( n_members, dtype=np.int_ ) + 1
1✔
1391
            fst_vt['HydroDyn']['NJoints'] = n_joints
1✔
1392
            fst_vt['HydroDyn']['JointID'] = ijoints
1✔
1393
            fst_vt['HydroDyn']['Jointxi'] = joints_xyz[:,0]
1✔
1394
            fst_vt['HydroDyn']['Jointyi'] = joints_xyz[:,1]
1✔
1395
            fst_vt['HydroDyn']['Jointzi'] = joints_xyz[:,2]
1✔
1396
            fst_vt['HydroDyn']['NPropSets'] = n_joints      # each joint has a cross section
1✔
1397
            fst_vt['HydroDyn']['PropSetID'] = ijoints
1✔
1398
            fst_vt['HydroDyn']['PropD'] = d_coarse
1✔
1399
            fst_vt['HydroDyn']['PropThck'] = t_coarse
1✔
1400
            fst_vt['HydroDyn']['NMembers'] = n_members
1✔
1401
            fst_vt['HydroDyn']['MemberID'] = imembers
1✔
1402
            fst_vt['HydroDyn']['MJointID1'] = fst_vt['HydroDyn']['MPropSetID1'] = N1
1✔
1403
            fst_vt['HydroDyn']['MJointID2'] = fst_vt['HydroDyn']['MPropSetID2'] = N2
1✔
1404
            fst_vt['HydroDyn']['MDivSize'] = 0.5*np.ones( fst_vt['HydroDyn']['NMembers'] )
1✔
1405
            fst_vt['HydroDyn']['MCoefMod'] = np.ones( fst_vt['HydroDyn']['NMembers'], dtype=np.int_)
1✔
1406
            fst_vt['HydroDyn']['JointAxID'] = np.ones( fst_vt['HydroDyn']['NJoints'], dtype=np.int_)
1✔
1407
            fst_vt['HydroDyn']['JointOvrlp'] = np.zeros( fst_vt['HydroDyn']['NJoints'], dtype=np.int_)
1✔
1408
            fst_vt['HydroDyn']['NCoefDpth'] = 0
1✔
1409
            fst_vt['HydroDyn']['NCoefMembers'] = 0
1✔
1410
            fst_vt['HydroDyn']['NFillGroups'] = 0
1✔
1411
            fst_vt['HydroDyn']['NMGDepths'] = 0
1✔
1412

1413
            if modopt["Level1"]["potential_model_override"] == 1:
1✔
1414
                # Strip theory only, no BEM
1415
                fst_vt['HydroDyn']['PropPot'] = [False] * fst_vt['HydroDyn']['NMembers']
×
1416
            elif modopt["Level1"]["potential_model_override"] == 2:
1✔
1417
                # BEM only, no strip theory
1418
                fst_vt['HydroDyn']['SimplCd'] = fst_vt['HydroDyn']['SimplCdMG'] = 0.0
1✔
1419
                fst_vt['HydroDyn']['SimplCa'] = fst_vt['HydroDyn']['SimplCaMG'] = 0.0
1✔
1420
                fst_vt['HydroDyn']['SimplCp'] = fst_vt['HydroDyn']['SimplCpMG'] = 0.0
1✔
1421
                fst_vt['HydroDyn']['SimplAxCd'] = fst_vt['HydroDyn']['SimplAxCdMG'] = 0.0
1✔
1422
                fst_vt['HydroDyn']['SimplAxCa'] = fst_vt['HydroDyn']['SimplAxCaMG'] = 0.0
1✔
1423
                fst_vt['HydroDyn']['SimplAxCp'] = fst_vt['HydroDyn']['SimplAxCpMG'] = 0.0
1✔
1424
                fst_vt['HydroDyn']['PropPot'] = [True] * fst_vt['HydroDyn']['NMembers']
1✔
1425
            else:
1426
                PropPotBool = [False] * fst_vt['HydroDyn']['NMembers']
1✔
1427
                for k in range(fst_vt['HydroDyn']['NMembers']):
1✔
1428
                    # Potential modeling of fixed substructres not supported
1429
                    if modopt['flags']['floating']:
1✔
1430
                        idx = modopt['floating']['members']['platform_elem_memid'][k]
×
1431
                        PropPotBool[k] = modopt["Level1"]["model_potential"][idx]    
×
1432
                fst_vt['HydroDyn']['PropPot'] = PropPotBool
1✔
1433

1434
            if fst_vt['HydroDyn']['NBody'] > 1:
1✔
1435
                raise Exception('Multiple HydroDyn bodies (NBody > 1) is currently not supported in WEIS')
×
1436

1437
            # Offset of body reference point
1438
            fst_vt['HydroDyn']['PtfmRefxt']     = 0
1✔
1439
            fst_vt['HydroDyn']['PtfmRefyt']     = 0
1✔
1440
            fst_vt['HydroDyn']['PtfmRefzt']     = 0
1✔
1441
            fst_vt['HydroDyn']['PtfmRefztRot']  = 0
1✔
1442

1443
            # If we're using the potential model, need these settings that aren't default
1444
            if fst_vt['HydroDyn']['PotMod'] == 1:
1✔
1445
                fst_vt['HydroDyn']['ExctnMod'] = 1
1✔
1446
                fst_vt['HydroDyn']['RdtnMod'] = 1
1✔
1447
                fst_vt['HydroDyn']['RdtnDT'] = "DEFAULT"
1✔
1448

1449
            if fst_vt['HydroDyn']['PotMod'] == 1 and modopt['Level2']['flag'] and modopt['Level1']['runPyHAMS']:
1✔
1450
                fst_vt['HydroDyn']['ExctnMod'] = 1
×
1451
                fst_vt['HydroDyn']['RdtnMod'] = 1
×
1452
                fst_vt['HydroDyn']['RdtnDT'] = "DEFAULT"
×
1453

1454
                from weis.ss_fitting.SS_FitTools import SSFit_Excitation, FDI_Fitting
×
1455
                logger.warning('Writing .ss and .ssexctn models to: {}'.format(fst_vt['HydroDyn']['PotFile']))
×
1456
                exctn_fit = SSFit_Excitation(HydroFile=fst_vt['HydroDyn']['PotFile'])
×
1457
                rad_fit = FDI_Fitting(HydroFile=fst_vt['HydroDyn']['PotFile'])
×
1458
                exctn_fit.writeMats()
×
1459
                rad_fit.fit()
×
1460
                rad_fit.outputMats()
×
1461
                if True:
×
1462
                    fig_list = rad_fit.visualizeFits()
×
1463
                    
1464
                    os.makedirs(os.path.join(os.path.dirname(fst_vt['HydroDyn']['PotFile']),'rad_fit'), exist_ok=True)
×
1465

1466
                    for i_fig, fig in enumerate(fig_list):
×
1467
                        fig.savefig(os.path.join(os.path.dirname(fst_vt['HydroDyn']['PotFile']),'rad_fit',f'rad_fit_{i_fig}.png'))
×
1468

1469
            # scale PtfmVol0 based on platform mass, temporary solution to buoyancy issue where spar's heave is very sensitive to platform mass
1470
            if fst_vt['HydroDyn']['PtfmMass_Init']:
1✔
1471
                fst_vt['HydroDyn']['PtfmVol0'] = float(inputs['platform_displacement']) * (1 + ((fst_vt['ElastoDyn']['PtfmMass'] / fst_vt['HydroDyn']['PtfmMass_Init']) - 1) * .9 )  #* 1.04 # 8029.21
×
1472
            else:
1473
                fst_vt['HydroDyn']['PtfmVol0'] = float(inputs['platform_displacement'])
1✔
1474

1475

1476
        # Moordyn inputs
1477
        if modopt["flags"]["mooring"]:
1✔
1478
            mooropt = modopt["mooring"]
1✔
1479
            
1480
            # Line types
1481
            # Creating a line type for each line, regardless of whether it is unique or not
1482
            n_lines = mooropt["n_lines"]
1✔
1483
            line_names = ['line'+str(m) for m in range(n_lines)]
1✔
1484
            fst_vt['MoorDyn']['NTypes'] = n_lines
1✔
1485
            fst_vt['MoorDyn']['Name'] = fst_vt['MAP']['LineType'] = line_names
1✔
1486
            fst_vt['MoorDyn']['Diam'] = fst_vt['MAP']['Diam'] = inputs["line_diameter"]
1✔
1487
            fst_vt['MoorDyn']['MassDen'] = fst_vt['MAP']['MassDenInAir'] = inputs["line_mass_density"]
1✔
1488
            fst_vt['MoorDyn']['EA'] = inputs["line_stiffness"]
1✔
1489
            fst_vt['MoorDyn']['EI'] = np.zeros(n_lines)     # MoorPy does not have EI, yet
1✔
1490
            fst_vt['MoorDyn']['BA_zeta'] = -1*np.ones(n_lines, dtype=np.int64)
1✔
1491
            fst_vt['MoorDyn']['Ca'] = inputs["line_transverse_added_mass"]
1✔
1492
            fst_vt['MoorDyn']['CaAx'] = inputs["line_tangential_added_mass"]
1✔
1493
            fst_vt['MoorDyn']['Cd'] = inputs["line_transverse_drag"]
1✔
1494
            fst_vt['MoorDyn']['CdAx'] = inputs["line_tangential_drag"]
1✔
1495

1496
            # Connection properties - Points
1497
            n_nodes = mooropt["n_nodes"]
1✔
1498
            fst_vt['MoorDyn']['NConnects'] = n_nodes
1✔
1499
            fst_vt['MoorDyn']['Point_ID'] = np.arange(n_nodes)+1
1✔
1500
            fst_vt['MoorDyn']['Attachment'] = mooropt["node_type"][:]
1✔
1501
            fst_vt['MoorDyn']['X'] = inputs['nodes_location_full'][:,0]
1✔
1502
            fst_vt['MoorDyn']['Y'] = inputs['nodes_location_full'][:,1]
1✔
1503
            fst_vt['MoorDyn']['Z'] = inputs['nodes_location_full'][:,2]
1✔
1504
            fst_vt['MoorDyn']['M'] = inputs['nodes_mass']
1✔
1505
            fst_vt['MoorDyn']['V'] = inputs['nodes_volume']
1✔
1506
            fst_vt['MoorDyn']['CdA'] = inputs['nodes_drag_area']
1✔
1507
            fst_vt['MoorDyn']['CA'] = inputs['nodes_added_mass']
1✔
1508

1509
            # Lines
1510
            fst_vt['MoorDyn']['NLines'] = n_lines
1✔
1511
            fst_vt['MoorDyn']['Line_ID'] = np.arange(n_lines)+1
1✔
1512
            fst_vt['MoorDyn']['LineType'] = line_names
1✔
1513
            fst_vt['MoorDyn']['UnstrLen'] = inputs['unstretched_length']
1✔
1514
            fst_vt['MoorDyn']['NumSegs'] = 50*np.ones(n_lines, dtype=np.int64)      # TODO: make this a modeling option
1✔
1515
            fst_vt['MoorDyn']['AttachA'] = np.zeros(n_lines, dtype=np.int64)
1✔
1516
            fst_vt['MoorDyn']['AttachB'] = np.zeros(n_lines, dtype=np.int64)
1✔
1517
            fst_vt['MoorDyn']['Outputs'] = ['-'] * n_lines
1✔
1518
            fst_vt['MoorDyn']['CtrlChan'] = np.zeros(n_lines, dtype=np.int64)
1✔
1519

1520
            for k in range(n_lines):
1✔
1521
                id1 = discrete_inputs['node_names'].index( mooropt["node1"][k] )
1✔
1522
                id2 = discrete_inputs['node_names'].index( mooropt["node2"][k] )
1✔
1523
                if (fst_vt['MoorDyn']['Attachment'][id1].lower() == 'vessel' and
1✔
1524
                    fst_vt['MoorDyn']['Attachment'][id2].lower().find('fix') >= 0):
1525
                    fst_vt['MoorDyn']['AttachB'][k] = id1+1
×
1526
                    fst_vt['MoorDyn']['AttachA'][k] = id2+1
×
1527
                elif (fst_vt['MoorDyn']['Attachment'][id2].lower() == 'vessel' and
1✔
1528
                    fst_vt['MoorDyn']['Attachment'][id1].lower().find('fix') >= 0):
1529
                    fst_vt['MoorDyn']['AttachB'][k] = id2+1
1✔
1530
                    fst_vt['MoorDyn']['AttachA'][k] = id1+1
1✔
1531
                else:
1532
                    logger.warning(discrete_inputs['node_names'])
×
1533
                    logger.warning(mooropt["node1"][k], mooropt["node2"][k])
×
1534
                    logger.warning(fst_vt['MoorDyn']['Attachment'][id1], fst_vt['MoorDyn']['Attachment'][id2])
×
1535
                    raise ValueError('Mooring line seems to be between unknown endpoint types.')
×
1536

1537
            # MoorDyn Control - Optional
1538
            fst_vt['MoorDyn']['ChannelID'] = []
1✔
1539
            
1540
            # MAP - linearization only
1541
            for key in fst_vt['MoorDyn']:
1✔
1542
                fst_vt['MAP'][key] = copy.copy(fst_vt['MoorDyn'][key])
1✔
1543

1544
            for idx, node_type in enumerate(fst_vt['MAP']['Attachment']):
1✔
1545
                if node_type == 'fixed':
1✔
1546
                    fst_vt['MAP']['Attachment'][idx] = 'fix'
1✔
1547

1548
            # TODO: FIXME: these values are hardcoded for the IEA15MW linearization studies
1549
            fst_vt['MAP']['LineType'] = ['main', 'main', 'main']
1✔
1550
            fst_vt['MAP']['CB'] = np.ones(n_lines)
1✔
1551
            fst_vt['MAP']['CIntDamp'] = np.zeros(n_lines)
1✔
1552
            fst_vt['MAP']['Ca'] = np.zeros(n_lines)
1✔
1553
            fst_vt['MAP']['Cdn'] = np.zeros(n_lines)
1✔
1554
            fst_vt['MAP']['Cdt'] = np.zeros(n_lines)
1✔
1555
            fst_vt['MAP']['B'] = np.zeros( n_nodes )
1✔
1556
            fst_vt['MAP']['Option'] = ["outer_tol 1e-5"]
1✔
1557

1558

1559
        # Structural Control
1560
        fst_vt['ServoDyn']['NumBStC']       = 0
1✔
1561
        fst_vt['ServoDyn']['BStCfiles']     = ["unused"]
1✔
1562
        fst_vt['ServoDyn']['NumNStC']       = 0
1✔
1563
        fst_vt['ServoDyn']['NStCfiles']     = ["unused"]
1✔
1564
        fst_vt['ServoDyn']['NumTStC']       = 0 
1✔
1565
        fst_vt['ServoDyn']['TStCfiles']     = []
1✔
1566
        fst_vt['ServoDyn']['NumSStC']       = 0
1✔
1567
        fst_vt['ServoDyn']['SStCfiles']     = []
1✔
1568
        
1569
        if modopt['flags']['TMDs']:
1✔
1570
            for i_TMD in range(modopt['TMDs']['n_TMDs']):
1✔
1571

1572
                StC_i = default_StC_vt()
1✔
1573

1574
                StC_i['StC_DOF_MODE']   = 1
1✔
1575
                StC_i['StC_X_DOF']     = modopt['TMDs']['X_DOF'][i_TMD]
1✔
1576
                StC_i['StC_Y_DOF']     = modopt['TMDs']['Y_DOF'][i_TMD]
1✔
1577
                StC_i['StC_Z_DOF']     = modopt['TMDs']['Z_DOF'][i_TMD]
1✔
1578

1579
                if StC_i['StC_X_DOF'] and StC_i['StC_Y_DOF'] and not StC_i['StC_Z_DOF']:
1✔
1580
                    StC_i['StC_DOF_MODE']   = 2
×
1581
                    StC_i['StC_XY_M']       = inputs['TMD_mass'][i_TMD]
×
1582

1583
                # Compute spring offset for each direction, initializing
1584
                g = modopt['Level3']['simulation']['Gravity']
1✔
1585
                spring_offset = np.zeros(3)
1✔
1586
                
1587
                # Set Mass, Stiffness, Damping only in DOFs enabled
1588
                if StC_i['StC_X_DOF']:
1✔
1589
                    StC_i['StC_X_M'] = inputs['TMD_mass'][i_TMD]
×
1590
                    StC_i['StC_X_K'] = inputs['TMD_stiffness'][i_TMD]
×
1591
                    StC_i['StC_X_C'] = inputs['TMD_damping'][i_TMD]
×
1592
                
1593
                if StC_i['StC_Y_DOF']:
1✔
1594
                    StC_i['StC_Y_M'] = inputs['TMD_mass'][i_TMD]
×
1595
                    StC_i['StC_Y_K'] = inputs['TMD_stiffness'][i_TMD]
×
1596
                    StC_i['StC_Y_C'] = inputs['TMD_damping'][i_TMD]
×
1597

1598
                if StC_i['StC_Z_DOF']:
1✔
1599
                    StC_i['StC_Z_M'] = inputs['TMD_mass'][i_TMD]
1✔
1600
                    StC_i['StC_Z_K'] = inputs['TMD_stiffness'][i_TMD]
1✔
1601
                    StC_i['StC_Z_C'] = inputs['TMD_damping'][i_TMD]
1✔
1602
                    spring_offset[2] = StC_i['StC_Z_M'] * g / StC_i['StC_Z_K']
1✔
1603

1604
                # Set position
1605
                StC_i['StC_P_X']  = modopt['TMDs']['location'][i_TMD][0]
1✔
1606
                StC_i['StC_P_Y']  = modopt['TMDs']['location'][i_TMD][1]
1✔
1607
                StC_i['StC_P_Z']  = modopt['TMDs']['location'][i_TMD][2]
1✔
1608
                
1609
                if modopt['TMDs']['preload_spring'][i_TMD]:
1✔
1610
                    StC_i['StC_Z_PreLd']  = "gravity"
1✔
1611
                    
1612

1613
                if modopt['TMDs']['component'][i_TMD] == 'tower':
1✔
1614
                    fst_vt['ServoDyn']['NumTStC'] += 1
×
1615
                    fst_vt['ServoDyn']['TStCfiles'].append(os.path.join(self.FAST_runDirectory,self.FAST_namingOut + f"_StC_Twr_{i_TMD}.dat"))
×
1616
                    fst_vt['TStC'].append(StC_i)
×
1617

1618
                elif modopt['TMDs']['component'][i_TMD] in modopt['floating']['members']['name']:
1✔
1619
                    fst_vt['ServoDyn']['NumSStC'] += 1
1✔
1620
                    fst_vt['ServoDyn']['SStCfiles'].append(os.path.join(self.FAST_runDirectory,self.FAST_namingOut + f"_StC_Ptfm_{i_TMD}.dat"))
1✔
1621
                    fst_vt['SStC'].append(StC_i)
1✔
1622

1623
            # If no StC file assigned, set to unused
1624
            if not fst_vt['ServoDyn']['TStCfiles']:
1✔
1625
                fst_vt['ServoDyn']['TStCfiles'] = ["unused"]
1✔
1626
            if not fst_vt['ServoDyn']['SStCfiles']:
1✔
1627
                fst_vt['ServoDyn']['SStCfiles'] = ["unused"]
×
1628

1629

1630
        return fst_vt
1✔
1631

1632
    def output_channels(self,fst_vt):
1✔
1633
        modopt = self.options['modeling_options']
1✔
1634

1635
        # Mandatory output channels to include
1636
        # TODO: what else is needed here?
1637
        channels_out  = ["TipDxc1", "TipDyc1", "TipDzc1", "TipDxc2", "TipDyc2", "TipDzc2"]
1✔
1638
        channels_out += ["RootMxc1", "RootMyc1", "RootMzc1", "RootMxc2", "RootMyc2", "RootMzc2"]
1✔
1639
        channels_out += ["TipDxb1", "TipDyb1", "TipDzb1", "TipDxb2", "TipDyb2", "TipDzb2"]
1✔
1640
        channels_out += ["RootMxb1", "RootMyb1", "RootMzb1", "RootMxb2", "RootMyb2", "RootMzb2"]
1✔
1641
        channels_out += ["RootFxc1", "RootFyc1", "RootFzc1", "RootFxc2", "RootFyc2", "RootFzc2"]
1✔
1642
        channels_out += ["RootFxb1", "RootFyb1", "RootFzb1", "RootFxb2", "RootFyb2", "RootFzb2"]
1✔
1643
        channels_out += ["Spn1FLzb1", "Spn2FLzb1", "Spn3FLzb1", "Spn4FLzb1", "Spn5FLzb1", "Spn6FLzb1", "Spn7FLzb1", "Spn8FLzb1", "Spn9FLzb1"]
1✔
1644
        channels_out += ["Spn1MLxb1", "Spn2MLxb1", "Spn3MLxb1", "Spn4MLxb1", "Spn5MLxb1", "Spn6MLxb1", "Spn7MLxb1", "Spn8MLxb1", "Spn9MLxb1"]
1✔
1645
        channels_out += ["Spn1MLyb1", "Spn2MLyb1", "Spn3MLyb1", "Spn4MLyb1", "Spn5MLyb1", "Spn6MLyb1", "Spn7MLyb1", "Spn8MLyb1", "Spn9MLyb1"]
1✔
1646
        channels_out += ["Spn1FLzb2", "Spn2FLzb2", "Spn3FLzb2", "Spn4FLzb2", "Spn5FLzb2", "Spn6FLzb2", "Spn7FLzb2", "Spn8FLzb2", "Spn9FLzb2"]
1✔
1647
        channels_out += ["Spn1MLxb2", "Spn2MLxb2", "Spn3MLxb2", "Spn4MLxb2", "Spn5MLxb2", "Spn6MLxb2", "Spn7MLxb2", "Spn8MLxb2", "Spn9MLxb2"]
1✔
1648
        channels_out += ["Spn1MLyb2", "Spn2MLyb2", "Spn3MLyb2", "Spn4MLyb2", "Spn5MLyb2", "Spn6MLyb2", "Spn7MLyb2", "Spn8MLyb2", "Spn9MLyb2"]
1✔
1649
        channels_out += ["Spn1FLzb3", "Spn2FLzb3", "Spn3FLzb3", "Spn4FLzb3", "Spn5FLzb3", "Spn6FLzb3", "Spn7FLzb3", "Spn8FLzb3", "Spn9FLzb3"]
1✔
1650
        channels_out += ["Spn1MLxb3", "Spn2MLxb3", "Spn3MLxb3", "Spn4MLxb3", "Spn5MLxb3", "Spn6MLxb3", "Spn7MLxb3", "Spn8MLxb3", "Spn9MLxb3"]
1✔
1651
        channels_out += ["Spn1MLyb3", "Spn2MLyb3", "Spn3MLyb3", "Spn4MLyb3", "Spn5MLyb3", "Spn6MLyb3", "Spn7MLyb3", "Spn8MLyb3", "Spn9MLyb3"]
1✔
1652
        channels_out += ["TipClrnc1", "TipClrnc2", "TipClrnc3"]
1✔
1653
        channels_out += ["RtFldCp", "RtFldCt"]
1✔
1654
        channels_out += ["RtFldFxh", "RtFldFyh", "RtFldFzh", "RtFldMxh", "RtFldMyh", "RtFldMzh"]
1✔
1655
        channels_out += ["RotSpeed", "GenSpeed", "NacYaw", "Azimuth"]
1✔
1656
        channels_out += ["GenPwr", "GenTq", "BldPitch1", "BldPitch2", "BldPitch3"]
1✔
1657
        channels_out += ["Wind1VelX", "Wind1VelY", "Wind1VelZ"]
1✔
1658
        channels_out += ["RtVAvgxh", "RtVAvgyh", "RtVAvgzh"]
1✔
1659
        channels_out += ["TwrBsFxt",  "TwrBsFyt", "TwrBsFzt", "TwrBsMxt",  "TwrBsMyt", "TwrBsMzt"]
1✔
1660
        channels_out += ["YawBrFxp", "YawBrFyp", "YawBrFzp", "YawBrMxp", "YawBrMyp", "YawBrMzp"]
1✔
1661
        channels_out += ["TwHt1FLxt", "TwHt2FLxt", "TwHt3FLxt", "TwHt4FLxt", "TwHt5FLxt", "TwHt6FLxt", "TwHt7FLxt", "TwHt8FLxt", "TwHt9FLxt"]
1✔
1662
        channels_out += ["TwHt1FLyt", "TwHt2FLyt", "TwHt3FLyt", "TwHt4FLyt", "TwHt5FLyt", "TwHt6FLyt", "TwHt7FLyt", "TwHt8FLyt", "TwHt9FLyt"]
1✔
1663
        channels_out += ["TwHt1FLzt", "TwHt2FLzt", "TwHt3FLzt", "TwHt4FLzt", "TwHt5FLzt", "TwHt6FLzt", "TwHt7FLzt", "TwHt8FLzt", "TwHt9FLzt"]
1✔
1664
        channels_out += ["TwHt1MLxt", "TwHt2MLxt", "TwHt3MLxt", "TwHt4MLxt", "TwHt5MLxt", "TwHt6MLxt", "TwHt7MLxt", "TwHt8MLxt", "TwHt9MLxt"]
1✔
1665
        channels_out += ["TwHt1MLyt", "TwHt2MLyt", "TwHt3MLyt", "TwHt4MLyt", "TwHt5MLyt", "TwHt6MLyt", "TwHt7MLyt", "TwHt8MLyt", "TwHt9MLyt"]
1✔
1666
        channels_out += ["TwHt1MLzt", "TwHt2MLzt", "TwHt3MLzt", "TwHt4MLzt", "TwHt5MLzt", "TwHt6MLzt", "TwHt7MLzt", "TwHt8MLzt", "TwHt9MLzt"]
1✔
1667
        channels_out += ["RotThrust", "LSShftFxs", "LSShftFys", "LSShftFzs", "LSShftFxa", "LSShftFya", "LSShftFza"]
1✔
1668
        channels_out += ["RotTorq", "LSSTipMxs", "LSSTipMys", "LSSTipMzs", "LSSTipMxa", "LSSTipMya", "LSSTipMza"]
1✔
1669
        channels_out += ["B1N1Alpha", "B1N2Alpha", "B1N3Alpha", "B1N4Alpha", "B1N5Alpha", "B1N6Alpha", "B1N7Alpha", "B1N8Alpha", "B1N9Alpha", "B2N1Alpha", "B2N2Alpha", "B2N3Alpha", "B2N4Alpha", "B2N5Alpha", "B2N6Alpha", "B2N7Alpha", "B2N8Alpha","B2N9Alpha"]
1✔
1670
        channels_out += ["PtfmSurge", "PtfmSway", "PtfmHeave", "PtfmRoll", "PtfmPitch", "PtfmYaw","NcIMURAys"]
1✔
1671
        channels_out += ['NcIMUTAxs','NcIMUTAzs','NcIMUTAzs']
1✔
1672
        if self.n_blades == 3:
1✔
1673
            channels_out += ["TipDxc3", "TipDyc3", "TipDzc3", "RootMxc3", "RootMyc3", "RootMzc3", "TipDxb3", "TipDyb3", "TipDzb3", "RootMxb3",
1✔
1674
                             "RootMyb3", "RootMzb3", "RootFxc3", "RootFyc3", "RootFzc3", "RootFxb3", "RootFyb3", "RootFzb3", "BldPitch3"]
1675
            channels_out += ["B3N1Alpha", "B3N2Alpha", "B3N3Alpha", "B3N4Alpha", "B3N5Alpha", "B3N6Alpha", "B3N7Alpha", "B3N8Alpha", "B3N9Alpha"]
1✔
1676

1677
        # Channels for distributed aerodynamic control
1678
        if self.options['modeling_options']['ROSCO']['Flp_Mode']:   # we're doing flap control
1✔
1679
            channels_out += ['BLFLAP1', 'BLFLAP2', 'BLFLAP3']
×
1680

1681
        # Channels for wave outputs
1682
        if modopt['flags']['offshore']:
1✔
1683
            channels_out += ["Wave1Elev","WavesF1xi","WavesF1zi","WavesM1yi"]
1✔
1684
            channels_out += ["WavesF2xi","WavesF2yi","WavesF2zi","WavesM2xi","WavesM2yi","WavesM2zi"]
1✔
1685

1686
        # Channels for monopile-based structure
1687
        if modopt['flags']['monopile']:
1✔
1688
            if modopt['Level3']['simulation']['CompSub']:
1✔
1689
                k=1
1✔
1690
                for i in range(len(self.Z_out_SD_mpl)):
1✔
1691
                    if k==9:
1✔
1692
                        Node=2
×
1693
                    else:
1694
                        Node=1
1✔
1695
                    channels_out += ["M" + str(k) + "N" + str(Node) + "FKxe"]
1✔
1696
                    channels_out += ["M" + str(k) + "N" + str(Node) + "FKye"]
1✔
1697
                    channels_out += ["M" + str(k) + "N" + str(Node) + "FKze"]
1✔
1698
                    channels_out += ["M" + str(k) + "N" + str(Node) + "MKxe"]
1✔
1699
                    channels_out += ["M" + str(k) + "N" + str(Node) + "MKye"]
1✔
1700
                    channels_out += ["M" + str(k) + "N" + str(Node) + "MKze"]
1✔
1701
                    channels_out += ['ReactFXss', 'ReactFYss', 'ReactFZss', 'ReactMXss', 'ReactMYss', 'ReactMZss']
1✔
1702
                    k+=1
1✔
1703
            else:
1704
                raise Exception('CompSub must be 1 in the modeling options to run SubDyn and compute monopile loads')
×
1705

1706
        # Floating output channels
1707
        if modopt['flags']['floating']:
1✔
1708
            channels_out += ["PtfmPitch", "PtfmRoll", "PtfmYaw", "PtfmSurge", "PtfmSway", "PtfmHeave"]
1✔
1709

1710
        # Structural Control Channels
1711
        if modopt['flags']['TMDs']:
1✔
1712
            for i_SStC in range(len(fst_vt['SStC'])):
1✔
1713
                channels_out += [f'SStC{i_SStC+1}_Fxi',f'SStC{i_SStC+1}_Fyi',f'SStC{i_SStC+1}_Fzi']
1✔
1714
                channels_out += [f'SStC{i_SStC+1}_Mxi',f'SStC{i_SStC+1}_Myi',f'SStC{i_SStC+1}_Mzi']
1✔
1715
                channels_out += [f'SStC{i_SStC+1}_Fxl',f'SStC{i_SStC+1}_Fyl',f'SStC{i_SStC+1}_Fzl']
1✔
1716
                channels_out += [f'SStC{i_SStC+1}_Mxl',f'SStC{i_SStC+1}_Myl',f'SStC{i_SStC+1}_Mzl']
1✔
1717
                channels_out += [f'SStC{i_SStC+1}_XQ',f'SStC{i_SStC+1}_YQ',f'SStC{i_SStC+1}_ZQ']
1✔
1718
                channels_out += [f'SStC{i_SStC+1}_XQD',f'SStC{i_SStC+1}_YQD',f'SStC{i_SStC+1}_ZQD']
1✔
1719

1720
            for i_TStC in range(len(fst_vt['TStC'])):
1✔
1721
                channels_out += [f'TStC{i_TStC+1}_Fxi',f'TStC{i_TStC+1}_Fyi',f'TStC{i_TStC+1}_Fzi']
×
1722
                channels_out += [f'TStC{i_TStC+1}_Mxi',f'TStC{i_TStC+1}_Myi',f'TStC{i_TStC+1}_Mzi']
×
1723
                channels_out += [f'TStC{i_TStC+1}_Fxl',f'TStC{i_TStC+1}_Fyl',f'TStC{i_TStC+1}_Fzl']
×
1724
                channels_out += [f'TStC{i_TStC+1}_Mxl',f'TStC{i_TStC+1}_Myl',f'TStC{i_TStC+1}_Mzl']
×
1725
                channels_out += [f'TStC{i_TStC+1}_XQ',f'TStC{i_TStC+1}_YQ',f'TStC{i_TStC+1}_ZQ']
×
1726
                channels_out += [f'TStC{i_TStC+1}_XQD',f'TStC{i_TStC+1}_YQD',f'TStC{i_TStC+1}_ZQD']
×
1727

1728

1729
        channels = {}
1✔
1730
        for var in channels_out:
1✔
1731
            channels[var] = True
1✔
1732

1733
        return channels
1✔
1734

1735
    def run_FAST(self, inputs, discrete_inputs, fst_vt):
1✔
1736

1737
        modopt = self.options['modeling_options']
1✔
1738
        DLCs = modopt['DLC_driver']['DLCs']
1✔
1739
        # Initialize the DLC generator
1740
        cut_in = float(inputs['V_cutin'])
1✔
1741
        cut_out = float(inputs['V_cutout'])
1✔
1742
        rated = float(inputs['Vrated'])
1✔
1743
        ws_class = discrete_inputs['turbine_class']
1✔
1744
        wt_class = discrete_inputs['turbulence_class']
1✔
1745
        hub_height = float(inputs['hub_height'])
1✔
1746
        rotorD = float(inputs['Rtip'])*2.
1✔
1747
        PLExp = float(inputs['shearExp'])
1✔
1748
        fix_wind_seeds = modopt['DLC_driver']['fix_wind_seeds']
1✔
1749
        fix_wave_seeds = modopt['DLC_driver']['fix_wave_seeds']
1✔
1750
        metocean = modopt['DLC_driver']['metocean_conditions']
1✔
1751
        
1752
        # Set initial rotor speed and pitch if the WT operates in this DLC and available,
1753
        # otherwise set pitch to 90 deg and rotor speed to 0 rpm when not operating
1754
        # set rotor speed to rated and pitch to 15 deg if operating
1755
        if self.options['modeling_options']['Level3']['from_openfast']:
1✔
1756
            modopt_dir = os.path.dirname(self.options['modeling_options']['fname_input_modeling'])
1✔
1757
            reg_traj = os.path.join(modopt_dir,self.options['modeling_options']['Level3']['regulation_trajectory'])
1✔
1758
            if os.path.isfile(reg_traj):
1✔
1759
                data = load_yaml(reg_traj)
1✔
1760
                cases = data['cases']
1✔
1761
                U_interp = [case["configuration"]["wind_speed"] for case in cases]
1✔
1762
                pitch_interp = [case["configuration"]["pitch"] for case in cases]
1✔
1763
                rot_speed_interp = [case["configuration"]["rotor_speed"] for case in cases]
1✔
1764
                Ct_aero_interp = [case["outputs"]["integrated"]["ct"] for case in cases]
1✔
1765
            else:
NEW
1766
                logger.warning("A yaml file with rotor speed, pitch, and Ct is required in modeling options->Level3->regulation_trajectory.",
×
1767
                        " This file does not exist. Check WEIS example 02 for a template file")
NEW
1768
                U_interp = np.arange(cut_in, cut_out)
×
NEW
1769
                pitch_interp = np.ones_like(U_interp) * 5. # fixed initial pitch at 5 deg
×
NEW
1770
                rot_speed_interp = np.ones_like(U_interp) * 5. # fixed initial omega at 5 rpm
×
NEW
1771
                Ct_aero_interp = np.ones_like(U_interp) * 0.7 # fixed initial ct at 0.7
×
1772
        else:
1773
            U_interp = inputs['U']
1✔
1774
            pitch_interp = inputs['pitch']
1✔
1775
            rot_speed_interp = inputs['Omega']
1✔
1776
            Ct_aero_interp = inputs['Ct_aero']
1✔
1777
        
1778
        tau1_const_interp = np.zeros_like(Ct_aero_interp)
1✔
1779
        for i in range(len(Ct_aero_interp)):
1✔
1780
            a = 1. / 2. * (1. - np.sqrt(1. - np.min([Ct_aero_interp[i],1])))    # don't allow Ct_aero > 1
1✔
1781
            tau1_const_interp[i] = 1.1 / (1. - 1.3 * np.min([a, 0.5])) * inputs['Rtip'][0] / U_interp[i]
1✔
1782

1783
        initial_condition_table = {}
1✔
1784
        initial_condition_table['U'] = U_interp
1✔
1785
        initial_condition_table['pitch_initial'] = pitch_interp
1✔
1786
        initial_condition_table['rot_speed_initial'] = rot_speed_interp
1✔
1787
        initial_condition_table['Ct_aero'] = Ct_aero_interp
1✔
1788
        initial_condition_table['tau1_const'] = tau1_const_interp
1✔
1789
        
1790
        
1791
        dlc_generator = DLCGenerator(
1✔
1792
            cut_in, 
1793
            cut_out, 
1794
            rated, 
1795
            ws_class, 
1796
            wt_class, 
1797
            fix_wind_seeds, 
1798
            fix_wave_seeds, 
1799
            metocean, 
1800
            initial_condition_table,
1801
            )
1802
        # Generate cases from user inputs
1803
        for i_DLC in range(len(DLCs)):
1✔
1804
            DLCopt = DLCs[i_DLC]
1✔
1805
            dlc_generator.generate(DLCopt['DLC'], DLCopt)
1✔
1806

1807
        # Initialize parametric inputs
1808
        WindFile_type = np.zeros(dlc_generator.n_cases, dtype=int)
1✔
1809
        WindFile_name = [''] * dlc_generator.n_cases
1✔
1810

1811
        self.TMax = np.zeros(dlc_generator.n_cases)
1✔
1812
        self.TStart = np.zeros(dlc_generator.n_cases)
1✔
1813

1814
        for i_case in range(dlc_generator.n_cases):
1✔
1815
            if dlc_generator.cases[i_case].turbulent_wind:
1✔
1816
                # Assign values common to all DLCs
1817
                # Wind turbulence class
1818
                if dlc_generator.cases[i_case].IECturbc > 0:    # use custom TI for DLC case
1✔
1819
                    dlc_generator.cases[i_case].IECturbc = str(dlc_generator.cases[i_case].IECturbc)
1✔
1820
                    dlc_generator.cases[i_case].IEC_WindType = 'NTM'        # must use NTM for custom TI
1✔
1821
                else:
1822
                    dlc_generator.cases[i_case].IECturbc = wt_class
1✔
1823
                # Reference height for wind speed
1824
                if not dlc_generator.cases[i_case].RefHt:   # default RefHt is 0, use hub_height if not set
1✔
1825
                    dlc_generator.cases[i_case].RefHt = hub_height
1✔
1826
                # Center of wind grid (TurbSim confusingly calls it HubHt)
1827
                if not dlc_generator.cases[i_case].HubHt:   # default HubHt is 0, use hub_height if not set
1✔
1828
                    dlc_generator.cases[i_case].HubHt = hub_height
1✔
1829

1830
                if not dlc_generator.cases[i_case].GridHeight:   # default GridHeight is 0, use hub_height if not set
1✔
1831
                    dlc_generator.cases[i_case].GridHeight =  2. * hub_height - 1.e-3
1✔
1832

1833
                if not dlc_generator.cases[i_case].GridWidth:   # default GridWidth is 0, use hub_height if not set
1✔
1834
                    dlc_generator.cases[i_case].GridWidth =  2. * hub_height - 1.e-3
1✔
1835

1836
                # Power law exponent of wind shear
1837
                if dlc_generator.cases[i_case].PLExp < 0:    # use PLExp based on environment options (shear_exp), otherwise use custom DLC PLExp
1✔
1838
                    dlc_generator.cases[i_case].PLExp = PLExp
1✔
1839
                # Length of wind grids
1840
                dlc_generator.cases[i_case].AnalysisTime = dlc_generator.cases[i_case].total_time
1✔
1841

1842
        # Generate wind files
1843
        if MPI and not self.options['opt_options']['driver']['design_of_experiments']['flag']:
1✔
1844
            # mpi comm management
1845
            comm = MPI.COMM_WORLD
×
1846
            rank = comm.Get_rank()
×
1847
            sub_ranks = self.mpi_comm_map_down[rank]
×
1848
            size = len(sub_ranks)
×
1849

1850
            N_cases = dlc_generator.n_cases # total number of cases
×
1851
            N_loops = int(np.ceil(float(N_cases)/float(size)))  # number of times function calls need to "loop"
×
1852
            # iterate loops
1853
            for i in range(N_loops):
×
1854
                idx_s = i*size
×
1855
                idx_e = min((i+1)*size, N_cases)
×
1856

1857
                for idx, i_case in enumerate(np.arange(idx_s,idx_e)):
×
1858
                    data = [partial(generate_wind_files, dlc_generator, self.FAST_namingOut, self.wind_directory, rotorD, hub_height, self.turbsim_exe), i_case]
×
1859
                    rank_j = sub_ranks[idx]
×
1860
                    comm.send(data, dest=rank_j, tag=0)
×
1861

1862
                for idx, i_case in enumerate(np.arange(idx_s, idx_e)):
×
1863
                    rank_j = sub_ranks[idx]
×
1864
                    WindFile_type[i_case] , WindFile_name[i_case] = comm.recv(source=rank_j, tag=1)
×
1865
        else:
1866
            for i_case in range(dlc_generator.n_cases):
1✔
1867
                WindFile_type[i_case] , WindFile_name[i_case] = generate_wind_files(
1✔
1868
                    dlc_generator, self.FAST_namingOut, self.wind_directory, rotorD, hub_height, self.turbsim_exe, i_case)
1869

1870

1871
        # Apply olaf settings, should be similar to above?
1872
        if dlc_generator.default_options['wake_mod'] == 3:  # OLAF is used 
1✔
NEW
1873
            apply_olaf_parameters(dlc_generator,fst_vt)
×
1874
                    
1875
        # Parameteric inputs
1876
        case_name = []
1✔
1877
        case_list = []
1✔
1878
        for i_case, case_inputs in enumerate(dlc_generator.openfast_case_inputs):
1✔
1879
            # Generate case list for DLC i
1880
            dlc_label = DLCs[i_case]['DLC']
1✔
1881
            case_list_i, case_name_i = CaseGen_General(case_inputs, self.FAST_runDirectory, self.FAST_InputFile, filename_ext=f'_DLC{dlc_label}_{i_case}')
1✔
1882
            # Add DLC to case names
1883
            case_name_i = [f'DLC{dlc_label}_{i_case}_{cni}' for cni in case_name_i]   # TODO: discuss case labeling with stakeholders
1✔
1884
            
1885
            # Extend lists of cases
1886
            case_list.extend(case_list_i)
1✔
1887
            case_name.extend(case_name_i)
1✔
1888

1889
        # Apply wind files to case_list (this info will be in combined case matrix, but not individual DLCs)
1890
        for case_i, wt, wf in zip(case_list,WindFile_type,WindFile_name):
1✔
1891
            case_i[('InflowWind','WindType')] = wt
1✔
1892
            case_i[('InflowWind','Filename_Uni')] = wf
1✔
1893
            case_i[('InflowWind','FileName_BTS')] = wf
1✔
1894

1895
        # Save some case info
1896
        self.TMax = [c.total_time for c in dlc_generator.cases]
1✔
1897
        self.TStart = [c.transient_time for c in dlc_generator.cases]
1✔
1898
        dlc_label = [c.label for c in dlc_generator.cases]
1✔
1899

1900

1901
        # Merge various cases into single case matrix
1902
        case_df = pd.DataFrame(case_list)
1✔
1903
        case_df.index = case_name
1✔
1904
        # Add case name and dlc label to front for readability
1905
        case_df.insert(0,'DLC',dlc_label)
1✔
1906
        case_df.insert(0,'case_name',case_name)
1✔
1907
        text_table = case_df.to_string(index=False)
1✔
1908

1909
        # Write the text table to a yaml, text file
1910
        write_yaml(case_df.to_dict(),os.path.join(self.FAST_runDirectory,'case_matrix_combined.yaml'))
1✔
1911
        with open(os.path.join(self.FAST_runDirectory,'case_matrix_combined.txt'), 'w') as file:
1✔
1912
            file.write(text_table)
1✔
1913
            
1914
            
1915
        channels= self.output_channels(fst_vt)
1✔
1916

1917
        # Delete the extra case_inputs because they don't play nicely with aeroelasticse
1918
        for case in case_list:
1✔
1919
            for key in list(case):
1✔
1920
                if key[0] in ['DLC','TurbSim']:
1✔
1921
                    del case[key]
1✔
1922
        
1923
        
1924
        # FAST wrapper setup
1925
        # JJ->DZ: here is the first point in logic for linearization
1926
        if modopt['Level2']['flag']:
1✔
1927
            linearization_options               = modopt['Level2']['linearization']
×
1928

1929
            # Use openfast binary until library works
1930
            fastBatch                           = LinearFAST(**linearization_options)
×
1931
            fastBatch.FAST_runDirectory         = self.FAST_runDirectory
×
1932
            fastBatch.use_exe                   = True      # linearization not working with library
×
1933
            fastBatch.fst_vt                    = fst_vt
×
1934
            fastBatch.cores                     = self.cores
×
1935

1936
            lin_case_list, lin_case_name        = fastBatch.gen_linear_cases(inputs)
×
1937
            fastBatch.case_list                 = lin_case_list
×
1938
            fastBatch.case_name_list            = lin_case_name
×
1939

1940
            # Save this list of linear cases for making linear model, not the best solution, but it works
1941
            self.lin_case_name                  = lin_case_name
×
1942
        else:
1943
            fastBatch                           = fastwrap.runFAST_pywrapper_batch()
1✔
1944
            fastBatch.FAST_runDirectory         = self.FAST_runDirectory
1✔
1945
            fastBatch.case_list                 = case_list
1✔
1946
            fastBatch.case_name_list            = case_name     
1✔
1947
            fastBatch.use_exe                   = modopt['General']['openfast_configuration']['use_exe']
1✔
1948
        
1949
        fastBatch.channels          = channels
1✔
1950
        fastBatch.FAST_InputFile    = self.FAST_InputFile
1✔
1951
        fastBatch.fst_vt            = fst_vt
1✔
1952
        fastBatch.keep_time         = modopt['General']['openfast_configuration']['keep_time']
1✔
1953
        fastBatch.post              = FAST_IO_timeseries
1✔
1954
        fastBatch.allow_fails       = modopt['General']['openfast_configuration']['allow_fails']
1✔
1955
        fastBatch.fail_value        = modopt['General']['openfast_configuration']['fail_value']
1✔
1956
        fastBatch.write_stdout      = modopt['General']['openfast_configuration']['write_stdout']
1✔
1957
        if self.FAST_exe_user is not None:
1✔
1958
            fastBatch.FAST_exe      = self.FAST_exe_user
×
1959
        if self.FAST_lib_user is not None:
1✔
1960
            fastBatch.FAST_lib      = self.FAST_lib_user
×
1961

1962
        fastBatch.overwrite_outfiles = True  #<--- Debugging only, set to False to prevent OpenFAST from running if the .outb already exists
1✔
1963

1964
        # Initialize fatigue channels and setings
1965
        # TODO: Stress Concentration Factor?
1966
        magnitude_channels = dict( fastwrap.magnitude_channels_default )
1✔
1967
        fatigue_channels =  dict( fastwrap.fatigue_channels_default )
1✔
1968

1969
        # Nacelle accelleration
1970
        magnitude_channels['NcIMUTA'] = ['NcIMUTAxs','NcIMUTAzs','NcIMUTAzs']
1✔
1971

1972
        # Blade fatigue: spar caps at the root (upper & lower?), TE at max chord
1973
        # Convert ultstress and S_intercept values to kPa with 1e-3 factor
1974
        if not modopt['Level3']['from_openfast']:
1✔
1975
            for u in ['U','L']:
1✔
1976
                blade_fatigue_root = FatigueParams(load2stress=1.0,
1✔
1977
                                                lifetime=inputs['lifetime'],
1978
                                                slope=inputs[f'blade_spar{u}_wohlerexp'],
1979
                                                ult_stress=1e-3*inputs[f'blade_spar{u}_ultstress'],
1980
                                                S_intercept=1e-3*inputs[f'blade_spar{u}_wohlerA'])
1981
                blade_fatigue_te = FatigueParams(load2stress=1.0,
1✔
1982
                                                lifetime=inputs['lifetime'],
1983
                                                slope=inputs[f'blade_te{u}_wohlerexp'],
1984
                                                ult_stress=1e-3*inputs[f'blade_te{u}_ultstress'],
1985
                                                S_intercept=1e-3*inputs[f'blade_te{u}_wohlerA'])
1986
                
1987
                for k in range(1,self.n_blades+1):
1✔
1988
                    blade_root_Fz = blade_fatigue_root.copy()
1✔
1989
                    blade_root_Fz.load2stress = inputs[f'blade_root_spar{u}_load2stress'][2]
1✔
1990
                    fatigue_channels[f'RootSpar{u}_Fzb{k}'] = blade_root_Fz
1✔
1991
                    magnitude_channels[f'RootSpar{u}_Fzb{k}'] = [f'RootFzb{k}']
1✔
1992

1993
                    blade_root_Mx = blade_fatigue_root.copy()
1✔
1994
                    blade_root_Mx.load2stress = inputs[f'blade_root_spar{u}_load2stress'][3]
1✔
1995
                    fatigue_channels[f'RootSpar{u}_Mxb{k}'] = blade_root_Mx
1✔
1996
                    magnitude_channels[f'RootSpar{u}_Mxb{k}'] = [f'RootMxb{k}']
1✔
1997

1998
                    blade_root_My = blade_fatigue_root.copy()
1✔
1999
                    blade_root_My.load2stress = inputs[f'blade_root_spar{u}_load2stress'][4]
1✔
2000
                    fatigue_channels[f'RootSpar{u}_Myb{k}'] = blade_root_My
1✔
2001
                    magnitude_channels[f'RootSpar{u}_Myb{k}'] = [f'RootMyb{k}']
1✔
2002

2003
                    blade_maxc_Fz = blade_fatigue_te.copy()
1✔
2004
                    blade_maxc_Fz.load2stress = inputs[f'blade_maxc_te{u}_load2stress'][2]
1✔
2005
                    fatigue_channels[f'Spn2te{u}_FLzb{k}'] = blade_maxc_Fz
1✔
2006
                    magnitude_channels[f'Spn2te{u}_FLzb{k}'] = [f'Spn2FLzb{k}']
1✔
2007

2008
                    blade_maxc_Mx = blade_fatigue_te.copy()
1✔
2009
                    blade_maxc_Mx.load2stress = inputs[f'blade_maxc_te{u}_load2stress'][3]
1✔
2010
                    fatigue_channels[f'Spn2te{u}_MLxb{k}'] = blade_maxc_Mx
1✔
2011
                    magnitude_channels[f'Spn2te{u}_MLxb{k}'] = [f'Spn2MLxb{k}']
1✔
2012

2013
                    blade_maxc_My = blade_fatigue_te.copy()
1✔
2014
                    blade_maxc_My.load2stress = inputs[f'blade_maxc_te{u}_load2stress'][4]
1✔
2015
                    fatigue_channels[f'Spn2te{u}_MLyb{k}'] = blade_maxc_My
1✔
2016
                    magnitude_channels[f'Spn2te{u}_MLyb{k}'] = [f'Spn2MLyb{k}']
1✔
2017

2018
            # Low speed shaft fatigue
2019
            # Convert ultstress and S_intercept values to kPa with 1e-3 factor
2020
            lss_fatigue = FatigueParams(load2stress=1.0,
1✔
2021
                                        lifetime=inputs['lifetime'],
2022
                                        slope=inputs['lss_wohlerexp'],
2023
                                        ult_stress=1e-3*inputs['lss_ultstress'],
2024
                                        S_intercept=1e-3*inputs['lss_wohlerA'])        
2025
            for s in ['Ax','Sh']:
1✔
2026
                sstr = 'axial' if s=='Ax' else 'shear'
1✔
2027
                for ik, k in enumerate(['F','M']):
1✔
2028
                    for ix, x in enumerate(['x','yz']):
1✔
2029
                        idx = 3*ik+ix
1✔
2030
                        lss_fatigue_ii = lss_fatigue.copy()
1✔
2031
                        lss_fatigue_ii.load2stress = inputs[f'lss_{sstr}_load2stress'][idx]
1✔
2032
                        fatigue_channels[f'LSShft{s}{k}{x}a'] = lss_fatigue_ii
1✔
2033
                        if ix==0:
1✔
2034
                            magnitude_channels[f'LSShft{s}{k}{x}a'] = ['RotThrust'] if ik==0 else ['RotTorq']
1✔
2035
                        else:
2036
                            magnitude_channels[f'LSShft{s}{k}{x}a'] = ['LSShftFya', 'LSShftFza'] if ik==0 else ['LSSTipMya', 'LSSTipMza']
1✔
2037

2038
            # Aero-only hub loads
2039
            magnitude_channels["RtFldF"] = ["RtFldFxh", "RtFldFyh", "RtFldFzh"]
1✔
2040
            magnitude_channels["RtFldM"] = ["RtFldMxh", "RtFldMyh", "RtFldMzh"]
1✔
2041

2042
            # Fatigue at the tower base
2043
            # Convert ultstress and S_intercept values to kPa with 1e-3 factor
2044
            tower_fatigue_base = FatigueParams(load2stress=1.0,
1✔
2045
                                               lifetime=inputs['lifetime'],
2046
                                               slope=inputs['tower_wohlerexp'][0],
2047
                                               ult_stress=1e-3*inputs['tower_ultstress'][0],
2048
                                               S_intercept=1e-3*inputs['tower_wohlerA'][0])
2049
            for s in ['Ax','Sh']:
1✔
2050
                sstr = 'axial' if s=='Ax' else 'shear'
1✔
2051
                for ik, k in enumerate(['F','M']):
1✔
2052
                    for ix, x in enumerate(['xy','z']):
1✔
2053
                        idx = 3*ik+2*ix
1✔
2054
                        tower_fatigue_ii = tower_fatigue_base.copy()
1✔
2055
                        tower_fatigue_ii.load2stress = inputs[f'tower_{sstr}_load2stress'][0,idx]
1✔
2056
                        fatigue_channels[f'TwrBs{s}{k}{x}t'] = tower_fatigue_ii
1✔
2057
                        magnitude_channels[f'TwrBs{s}{k}{x}t'] = [f'TwrBs{k}{x}t'] if x=='z' else [f'TwrBs{k}xt', f'TwrBs{k}yt']
1✔
2058

2059
            # Fatigue at monopile base (mudline)
2060
            # No need to convert to kPa here since SubDyn reports in N already
2061
            if modopt['flags']['monopile']:
1✔
2062
                monopile_fatigue_base = FatigueParams(load2stress=1.0,
1✔
2063
                                                      lifetime=inputs['lifetime'],
2064
                                                      slope=inputs['monopile_wohlerexp'][0],
2065
                                                      ult_stress=inputs['monopile_ultstress'][0],
2066
                                                      S_intercept=inputs['monopile_wohlerA'][0])
2067
                for s in ['Ax','Sh']:
1✔
2068
                    sstr = 'axial' if s=='Ax' else 'shear'
1✔
2069
                    for ik, k in enumerate(['F','M']):
1✔
2070
                        for ix, x in enumerate(['xy','z']):
1✔
2071
                            idx = 3*ik+2*ix
1✔
2072
                            monopile_fatigue_ii = monopile_fatigue_base.copy()
1✔
2073
                            monopile_fatigue_ii.load2stress = inputs[f'monopile_{sstr}_load2stress'][0,idx]
1✔
2074
                            fatigue_channels[f'M1N1{s}{k}K{x}e'] = monopile_fatigue_ii
1✔
2075
                            magnitude_channels[f'M1N1{s}{k}K{x}e'] = [f'M1N1{k}K{x}e'] if x=='z' else [f'M1N1{k}Kxe', f'M1N1{k}Kye']
1✔
2076

2077
        # Store settings
2078
        fastBatch.goodman            = modopt['General']['goodman_correction'] # Where does this get placed in schema?
1✔
2079
        fastBatch.fatigue_channels   = fatigue_channels
1✔
2080
        fastBatch.magnitude_channels = magnitude_channels
1✔
2081
        self.la = LoadsAnalysis(
1✔
2082
            outputs=[],
2083
            magnitude_channels=magnitude_channels,
2084
            fatigue_channels=fatigue_channels,
2085
        )
2086
        self.magnitude_channels = magnitude_channels
1✔
2087

2088
        # Run FAST
2089
        if self.mpi_run and not self.options['opt_options']['driver']['design_of_experiments']['flag']:
1✔
2090
            summary_stats, extreme_table, DELs, Damage, chan_time = fastBatch.run_mpi(self.mpi_comm_map_down)
×
2091
        else:
2092
            if self.cores == 1:
1✔
2093
                summary_stats, extreme_table, DELs, Damage, chan_time = fastBatch.run_serial()
1✔
2094
            else:
2095
                summary_stats, extreme_table, DELs, Damage, chan_time = fastBatch.run_multi(self.cores)
×
2096

2097
        self.fst_vt = fst_vt
1✔
2098
        self.of_inumber = self.of_inumber + 1
1✔
2099
        sys.stdout.flush()
1✔
2100

2101
        return summary_stats, extreme_table, DELs, Damage, case_list, case_name, chan_time, dlc_generator
1✔
2102

2103
    def post_process(self, summary_stats, extreme_table, DELs, damage, case_list, dlc_generator, chan_time, inputs, discrete_inputs, outputs, discrete_outputs):
1✔
2104
        modopt = self.options['modeling_options']
1✔
2105

2106
        # Analysis
2107
        if self.options['modeling_options']['flags']['blade'] and bool(self.fst_vt['Fst']['CompAero']):
1✔
2108
            outputs, discrete_outputs = self.get_blade_loading(summary_stats, extreme_table, inputs, discrete_inputs, outputs, discrete_outputs)
1✔
2109
        if self.options['modeling_options']['flags']['tower']:
1✔
2110
            outputs = self.get_tower_loading(summary_stats, extreme_table, inputs, outputs)
1✔
2111
        # SubDyn is only supported in Level3: linearization in OpenFAST will be available in 3.0.0
2112
        if modopt['flags']['monopile'] and modopt['Level3']['flag']:
1✔
2113
            outputs = self.get_monopile_loading(summary_stats, extreme_table, inputs, outputs)
1✔
2114

2115
        # If DLC 1.1 not used, calculate_AEP will just compute average power of simulations
2116
        outputs, discrete_outputs = self.calculate_AEP(summary_stats, case_list, dlc_generator, discrete_inputs, outputs, discrete_outputs)
1✔
2117

2118
        outputs, discrete_outputs = self.get_weighted_DELs(dlc_generator, DELs, damage, discrete_inputs, outputs, discrete_outputs)
1✔
2119
        
2120
        outputs, discrete_outputs = self.get_control_measures(summary_stats, chan_time, inputs, discrete_inputs, outputs, discrete_outputs)
1✔
2121

2122
        if modopt['flags']['floating'] or (modopt['Level3']['from_openfast'] and self.fst_vt['Fst']['CompMooring']>0):
1✔
2123
            outputs, discrete_outputs = self.get_floating_measures(summary_stats, chan_time, inputs, discrete_inputs,outputs, discrete_outputs)
1✔
2124

2125
        # Did any OpenFAST runs fail?
2126
        if modopt['Level3']['flag']:
1✔
2127
            if any(summary_stats['openfast_failed']['mean'] > 0):
1✔
2128
                outputs['openfast_failed'] = 2
×
2129

2130
        # # Did any OpenFAST runs fail?
2131
        # if any(summary_stats['openfast_failed']['mean'] > 0):
2132
        #     outputs['openfast_failed'] = 2
2133

2134
        # Save Data
2135
        if modopt['General']['openfast_configuration']['save_timeseries']:
1✔
2136
            self.save_timeseries(chan_time)
1✔
2137

2138
        if modopt['General']['openfast_configuration']['save_iterations']:
1✔
2139
            self.save_iterations(summary_stats,DELs,discrete_outputs)
1✔
2140

2141
        # Open loop to closed loop error, move this to before save_timeseries when finished
2142
        if modopt['OL2CL']['flag']:
1✔
2143
            outputs = self.get_OL2CL_error(chan_time,outputs)
×
2144

2145
    def get_blade_loading(self, sum_stats, extreme_table, inputs, discrete_inputs, outputs, discrete_outputs):
1✔
2146
        """
2147
        Find the spanwise loading along the blade span.
2148

2149
        Parameters
2150
        ----------
2151
        sum_stats : pd.DataFrame
2152
        extreme_table : dict
2153
        """
2154

2155
        # Determine maximum deflection magnitudes
2156
        if self.n_blades == 2:
1✔
2157
            defl_mag = [max(sum_stats['TipDxc1']['max']), max(sum_stats['TipDxc2']['max'])]
×
2158
        else:
2159
            defl_mag = [max(sum_stats['TipDxc1']['max']), max(sum_stats['TipDxc2']['max']), max(sum_stats['TipDxc3']['max'])]
1✔
2160
        # Get the maximum out of plane blade deflection
2161
        outputs["max_TipDxc"] = np.max(defl_mag)
1✔
2162

2163
        # Return moments around x and y and axial force along blade span at instance of largest flapwise bending moment at each node
2164
        My_chans = ["RootMyb", "Spn1MLyb", "Spn2MLyb", "Spn3MLyb", "Spn4MLyb", "Spn5MLyb", "Spn6MLyb", "Spn7MLyb", "Spn8MLyb", "Spn9MLyb"]
1✔
2165
        Mx_chans = ["RootMxb", "Spn1MLxb", "Spn2MLxb", "Spn3MLxb", "Spn4MLxb", "Spn5MLxb", "Spn6MLxb", "Spn7MLxb", "Spn8MLxb", "Spn9MLxb"]
1✔
2166
        Fz_chans = ["RootFzb", "Spn1FLzb", "Spn2FLzb", "Spn3FLzb", "Spn4FLzb", "Spn5FLzb", "Spn6FLzb", "Spn7FLzb", "Spn8FLzb", "Spn9FLzb"]
1✔
2167
            
2168
        Fz = []
1✔
2169
        Mx = []
1✔
2170
        My = []
1✔
2171
        for My_chan,Mx_chan,Fz_chan in zip(My_chans, Mx_chans, Fz_chans):
1✔
2172
            if self.n_blades == 2:
1✔
2173
                bld_idx_max = np.argmax([max(sum_stats[My_chan+'1']['max']), max(sum_stats[My_chan+'2']['max'])])
×
2174
            else:
2175
                bld_idx_max = np.argmax([max(sum_stats[My_chan+'1']['max']), max(sum_stats[My_chan+'2']['max']), max(sum_stats[My_chan+'3']['max'])])
1✔
2176
            My_max_chan = My_chan + str(bld_idx_max+1)
1✔
2177
            My.append(extreme_table[My_max_chan][np.argmax(sum_stats[My_max_chan]['max'])][My_chan+str(bld_idx_max+1)])
1✔
2178
            Mx.append(extreme_table[My_max_chan][np.argmax(sum_stats[My_max_chan]['max'])][Mx_chan+str(bld_idx_max+1)])
1✔
2179
            Fz.append(extreme_table[My_max_chan][np.argmax(sum_stats[My_max_chan]['max'])][Fz_chan+str(bld_idx_max+1)])
1✔
2180

2181

2182
        if np.any(np.isnan(Fz)):
1✔
2183
            logger.warning('WARNING: nans found in Fz extremes')
×
2184
            Fz[np.isnan(Fz)] = 0.0
×
2185
        if np.any(np.isnan(Mx)):
1✔
2186
            logger.warning('WARNING: nans found in Mx extremes')
×
2187
            Mx[np.isnan(Mx)] = 0.0
×
2188
        if np.any(np.isnan(My)):
1✔
2189
            logger.warning('WARNING: nans found in My extremes')
×
2190
            My[np.isnan(My)] = 0.0
×
2191
        spline_Fz = PchipInterpolator(np.hstack((self.R_out_ED_bl, inputs['Rtip'])), np.hstack((Fz, 0.)))
1✔
2192
        spline_Mx = PchipInterpolator(np.hstack((self.R_out_ED_bl, inputs['Rtip'])), np.hstack((Mx, 0.)))
1✔
2193
        spline_My = PchipInterpolator(np.hstack((self.R_out_ED_bl, inputs['Rtip'])), np.hstack((My, 0.)))
1✔
2194

2195
        r = inputs['r']
1✔
2196
        Fz_out = spline_Fz(r).flatten()
1✔
2197
        Mx_out = spline_Mx(r).flatten()
1✔
2198
        My_out = spline_My(r).flatten()
1✔
2199

2200
        outputs['blade_maxTD_Mx'] = Mx_out
1✔
2201
        outputs['blade_maxTD_My'] = My_out
1✔
2202
        outputs['blade_maxTD_Fz'] = Fz_out
1✔
2203

2204
        # Determine maximum root moment
2205
        if self.n_blades == 2:
1✔
2206
            blade_root_flap_moment = max([max(sum_stats['RootMyb1']['max']), max(sum_stats['RootMyb2']['max'])])
×
2207
            blade_root_oop_moment  = max([max(sum_stats['RootMyc1']['max']), max(sum_stats['RootMyc2']['max'])])
×
2208
            blade_root_tors_moment  = max([max(sum_stats['RootMzb1']['max']), max(sum_stats['RootMzb2']['max'])])
×
2209
        else:
2210
            blade_root_flap_moment = max([max(sum_stats['RootMyb1']['max']), max(sum_stats['RootMyb2']['max']), max(sum_stats['RootMyb3']['max'])])
1✔
2211
            blade_root_oop_moment  = max([max(sum_stats['RootMyc1']['max']), max(sum_stats['RootMyc2']['max']), max(sum_stats['RootMyc3']['max'])])
1✔
2212
            blade_root_tors_moment  = max([max(sum_stats['RootMzb1']['max']), max(sum_stats['RootMzb2']['max']), max(sum_stats['RootMzb3']['max'])])
1✔
2213
        outputs['max_RootMyb'] = blade_root_flap_moment
1✔
2214
        outputs['max_RootMyc'] = blade_root_oop_moment
1✔
2215
        outputs['max_RootMzb'] = blade_root_tors_moment
1✔
2216

2217
        ## Get hub moments and forces in the non-rotating frame
2218
        outputs['hub_Fxyz'] = np.array([extreme_table['LSShftF'][np.argmax(sum_stats['LSShftF']['max'])]['RotThrust'],
1✔
2219
                                    extreme_table['LSShftF'][np.argmax(sum_stats['LSShftF']['max'])]['LSShftFys'],
2220
                                    extreme_table['LSShftF'][np.argmax(sum_stats['LSShftF']['max'])]['LSShftFzs']])*1.e3
2221
        outputs['hub_Mxyz'] = np.array([extreme_table['LSShftM'][np.argmax(sum_stats['LSShftM']['max'])]['RotTorq'],
1✔
2222
                                    extreme_table['LSShftM'][np.argmax(sum_stats['LSShftM']['max'])]['LSSTipMys'],
2223
                                    extreme_table['LSShftM'][np.argmax(sum_stats['LSShftM']['max'])]['LSSTipMzs']])*1.e3
2224
        
2225
        # Aero-only for WISDEM (outputs are in N and N-m)
2226
        outputs['hub_Fxyz_aero'] = np.array([extreme_table['RtFldF'][np.argmax(sum_stats['RtFldF']['max'])]['RtFldFxh'],
1✔
2227
                                    extreme_table['RtFldF'][np.argmax(sum_stats['RtFldF']['max'])]['RtFldFyh'],
2228
                                    extreme_table['RtFldF'][np.argmax(sum_stats['RtFldF']['max'])]['RtFldFzh']])
2229
        outputs['hub_Mxyz_aero'] = np.array([extreme_table['RtFldM'][np.argmax(sum_stats['RtFldM']['max'])]['RtFldMxh'],
1✔
2230
                                    extreme_table['RtFldM'][np.argmax(sum_stats['RtFldM']['max'])]['RtFldMyh'],
2231
                                    extreme_table['RtFldM'][np.argmax(sum_stats['RtFldM']['max'])]['RtFldMzh']])
2232

2233
        ## Post process aerodynamic data
2234
        # Angles of attack - max, std, mean
2235
        blade1_chans_aoa = ["B1N1Alpha", "B1N2Alpha", "B1N3Alpha", "B1N4Alpha", "B1N5Alpha", "B1N6Alpha", "B1N7Alpha", "B1N8Alpha", "B1N9Alpha"]
1✔
2236
        blade2_chans_aoa = ["B2N1Alpha", "B2N2Alpha", "B2N3Alpha", "B2N4Alpha", "B2N5Alpha", "B2N6Alpha", "B2N7Alpha", "B2N8Alpha", "B2N9Alpha"]
1✔
2237
        aoa_max_B1  = [np.max(sum_stats[var]['max'])    for var in blade1_chans_aoa]
1✔
2238
        aoa_mean_B1 = [np.mean(sum_stats[var]['mean'])  for var in blade1_chans_aoa]
1✔
2239
        aoa_std_B1  = [np.mean(sum_stats[var]['std'])   for var in blade1_chans_aoa]
1✔
2240
        aoa_max_B2  = [np.max(sum_stats[var]['max'])    for var in blade2_chans_aoa]
1✔
2241
        aoa_mean_B2 = [np.mean(sum_stats[var]['mean'])  for var in blade2_chans_aoa]
1✔
2242
        aoa_std_B2  = [np.mean(sum_stats[var]['std'])   for var in blade2_chans_aoa]
1✔
2243
        if self.n_blades == 2:
1✔
2244
            spline_aoa_max      = PchipInterpolator(self.R_out_AD, np.max([aoa_max_B1, aoa_max_B2], axis=0))
×
2245
            spline_aoa_std      = PchipInterpolator(self.R_out_AD, np.mean([aoa_std_B1, aoa_std_B2], axis=0))
×
2246
            spline_aoa_mean     = PchipInterpolator(self.R_out_AD, np.mean([aoa_mean_B1, aoa_mean_B2], axis=0))
×
2247
        elif self.n_blades == 3:
1✔
2248
            blade3_chans_aoa    = ["B3N1Alpha", "B3N2Alpha", "B3N3Alpha", "B3N4Alpha", "B3N5Alpha", "B3N6Alpha", "B3N7Alpha", "B3N8Alpha", "B3N9Alpha"]
1✔
2249
            aoa_max_B3          = [np.max(sum_stats[var]['max'])    for var in blade3_chans_aoa]
1✔
2250
            aoa_mean_B3         = [np.mean(sum_stats[var]['mean'])  for var in blade3_chans_aoa]
1✔
2251
            aoa_std_B3          = [np.mean(sum_stats[var]['std'])   for var in blade3_chans_aoa]
1✔
2252
            spline_aoa_max      = PchipInterpolator(self.R_out_AD, np.max([aoa_max_B1, aoa_max_B2, aoa_max_B3], axis=0))
1✔
2253
            spline_aoa_std      = PchipInterpolator(self.R_out_AD, np.mean([aoa_max_B1, aoa_std_B2, aoa_std_B3], axis=0))
1✔
2254
            spline_aoa_mean     = PchipInterpolator(self.R_out_AD, np.mean([aoa_mean_B1, aoa_mean_B2, aoa_mean_B3], axis=0))
1✔
2255
        else:
2256
            raise Exception('The calculations only support 2 or 3 bladed rotors')
×
2257

2258
        outputs['max_aoa']  = spline_aoa_max(r)
1✔
2259
        outputs['std_aoa']  = spline_aoa_std(r)
1✔
2260
        outputs['mean_aoa'] = spline_aoa_mean(r)
1✔
2261

2262
        return outputs, discrete_outputs
1✔
2263

2264
    def get_tower_loading(self, sum_stats, extreme_table, inputs, outputs):
1✔
2265
        """
2266
        Find the loading along the tower height.
2267

2268
        Parameters
2269
        ----------
2270
        sum_stats : pd.DataFrame
2271
        extreme_table : dict
2272
        """
2273

2274
        tower_chans_Fx = ["TwrBsFxt", "TwHt1FLxt", "TwHt2FLxt", "TwHt3FLxt", "TwHt4FLxt", "TwHt5FLxt", "TwHt6FLxt", "TwHt7FLxt", "TwHt8FLxt", "TwHt9FLxt", "YawBrFxp"]
1✔
2275
        tower_chans_Fy = ["TwrBsFyt", "TwHt1FLyt", "TwHt2FLyt", "TwHt3FLyt", "TwHt4FLyt", "TwHt5FLyt", "TwHt6FLyt", "TwHt7FLyt", "TwHt8FLyt", "TwHt9FLyt", "YawBrFyp"]
1✔
2276
        tower_chans_Fz = ["TwrBsFzt", "TwHt1FLzt", "TwHt2FLzt", "TwHt3FLzt", "TwHt4FLzt", "TwHt5FLzt", "TwHt6FLzt", "TwHt7FLzt", "TwHt8FLzt", "TwHt9FLzt", "YawBrFzp"]
1✔
2277
        tower_chans_Mx = ["TwrBsMxt", "TwHt1MLxt", "TwHt2MLxt", "TwHt3MLxt", "TwHt4MLxt", "TwHt5MLxt", "TwHt6MLxt", "TwHt7MLxt", "TwHt8MLxt", "TwHt9MLxt", "YawBrMxp"]
1✔
2278
        tower_chans_My = ["TwrBsMyt", "TwHt1MLyt", "TwHt2MLyt", "TwHt3MLyt", "TwHt4MLyt", "TwHt5MLyt", "TwHt6MLyt", "TwHt7MLyt", "TwHt8MLyt", "TwHt9MLyt", "YawBrMyp"]
1✔
2279
        tower_chans_Mz = ["TwrBsMzt", "TwHt1MLzt", "TwHt2MLzt", "TwHt3MLzt", "TwHt4MLzt", "TwHt5MLzt", "TwHt6MLzt", "TwHt7MLzt", "TwHt8MLzt", "TwHt9MLzt", "YawBrMzp"]
1✔
2280

2281
        fatb_max_chan   = "TwrBsMyt"
1✔
2282

2283
        # Get the maximum fore-aft moment at tower base
2284
        outputs["max_TwrBsMyt"] = np.max(sum_stats[fatb_max_chan]['max'])
1✔
2285
        outputs["max_TwrBsMyt_ratio"] = np.max(sum_stats[fatb_max_chan]['max'])/self.options['opt_options']['constraints']['control']['Max_TwrBsMyt']['max']
1✔
2286
        # Return forces and moments along tower height at instance of largest fore-aft tower base moment
2287
        Fx = [extreme_table[fatb_max_chan][np.argmax(sum_stats[fatb_max_chan]['max'])][var] for var in tower_chans_Fx]
1✔
2288
        Fy = [extreme_table[fatb_max_chan][np.argmax(sum_stats[fatb_max_chan]['max'])][var] for var in tower_chans_Fy]
1✔
2289
        Fz = [extreme_table[fatb_max_chan][np.argmax(sum_stats[fatb_max_chan]['max'])][var] for var in tower_chans_Fz]
1✔
2290
        Mx = [extreme_table[fatb_max_chan][np.argmax(sum_stats[fatb_max_chan]['max'])][var] for var in tower_chans_Mx]
1✔
2291
        My = [extreme_table[fatb_max_chan][np.argmax(sum_stats[fatb_max_chan]['max'])][var] for var in tower_chans_My]
1✔
2292
        Mz = [extreme_table[fatb_max_chan][np.argmax(sum_stats[fatb_max_chan]['max'])][var] for var in tower_chans_Mz]
1✔
2293

2294
        # Spline results on tower basic grid
2295
        spline_Fx      = PchipInterpolator(self.Z_out_ED_twr, Fx)
1✔
2296
        spline_Fy      = PchipInterpolator(self.Z_out_ED_twr, Fy)
1✔
2297
        spline_Fz      = PchipInterpolator(self.Z_out_ED_twr, Fz)
1✔
2298
        spline_Mx      = PchipInterpolator(self.Z_out_ED_twr, Mx)
1✔
2299
        spline_My      = PchipInterpolator(self.Z_out_ED_twr, My)
1✔
2300
        spline_Mz      = PchipInterpolator(self.Z_out_ED_twr, Mz)
1✔
2301

2302
        z_full = inputs['tower_z_full']
1✔
2303
        z_sec, _ = util.nodal2sectional(z_full)
1✔
2304
        z = (z_sec - z_sec[0]) / (z_sec[-1] - z_sec[0])
1✔
2305

2306
        outputs['tower_maxMy_Fx'] = spline_Fx(z)
1✔
2307
        outputs['tower_maxMy_Fy'] = spline_Fy(z)
1✔
2308
        outputs['tower_maxMy_Fz'] = spline_Fz(z)
1✔
2309
        outputs['tower_maxMy_Mx'] = spline_Mx(z)
1✔
2310
        outputs['tower_maxMy_My'] = spline_My(z)
1✔
2311
        outputs['tower_maxMy_Mz'] = spline_Mz(z)
1✔
2312
        
2313
        return outputs
1✔
2314

2315
    def get_monopile_loading(self, sum_stats, extreme_table, inputs, outputs):
1✔
2316
        """
2317
        Find the loading along the monopile length.
2318

2319
        Parameters
2320
        ----------
2321
        sum_stats : pd.DataFrame
2322
        extreme_table : dict
2323
        """
2324

2325
        monopile_chans_Fx = []
1✔
2326
        monopile_chans_Fy = []
1✔
2327
        monopile_chans_Fz = []
1✔
2328
        monopile_chans_Mx = []
1✔
2329
        monopile_chans_My = []
1✔
2330
        monopile_chans_Mz = []
1✔
2331
        k=1
1✔
2332
        for i in range(len(self.Z_out_SD_mpl)):
1✔
2333
            if k==9:
1✔
2334
                Node=2
×
2335
            else:
2336
                Node=1
1✔
2337
            monopile_chans_Fx += ["M" + str(k) + "N" + str(Node) + "FKxe"]
1✔
2338
            monopile_chans_Fy += ["M" + str(k) + "N" + str(Node) + "FKye"]
1✔
2339
            monopile_chans_Fz += ["M" + str(k) + "N" + str(Node) + "FKze"]
1✔
2340
            monopile_chans_Mx += ["M" + str(k) + "N" + str(Node) + "MKxe"]
1✔
2341
            monopile_chans_My += ["M" + str(k) + "N" + str(Node) + "MKye"]
1✔
2342
            monopile_chans_Mz += ["M" + str(k) + "N" + str(Node) + "MKze"]
1✔
2343
            k+=1
1✔
2344

2345
        max_chan   = "M1N1MKye"
1✔
2346

2347
        # # Get the maximum of signal M1N1MKye
2348
        outputs["max_M1N1MKye"] = np.max(sum_stats[max_chan]['max'])
1✔
2349
        # # Return forces and moments along monopile at instance of largest fore-aft tower base moment
2350
        Fx = [extreme_table[max_chan][np.argmax(sum_stats[max_chan]['max'])][var] for var in monopile_chans_Fx]
1✔
2351
        Fy = [extreme_table[max_chan][np.argmax(sum_stats[max_chan]['max'])][var] for var in monopile_chans_Fy]
1✔
2352
        Fz = [extreme_table[max_chan][np.argmax(sum_stats[max_chan]['max'])][var] for var in monopile_chans_Fz]
1✔
2353
        Mx = [extreme_table[max_chan][np.argmax(sum_stats[max_chan]['max'])][var] for var in monopile_chans_Mx]
1✔
2354
        My = [extreme_table[max_chan][np.argmax(sum_stats[max_chan]['max'])][var] for var in monopile_chans_My]
1✔
2355
        Mz = [extreme_table[max_chan][np.argmax(sum_stats[max_chan]['max'])][var] for var in monopile_chans_Mz]
1✔
2356

2357
        # # Spline results on grid of channel locations along the monopile
2358
        spline_Fx      = PchipInterpolator(self.Z_out_SD_mpl, Fx)
1✔
2359
        spline_Fy      = PchipInterpolator(self.Z_out_SD_mpl, Fy)
1✔
2360
        spline_Fz      = PchipInterpolator(self.Z_out_SD_mpl, Fz)
1✔
2361
        spline_Mx      = PchipInterpolator(self.Z_out_SD_mpl, Mx)
1✔
2362
        spline_My      = PchipInterpolator(self.Z_out_SD_mpl, My)
1✔
2363
        spline_Mz      = PchipInterpolator(self.Z_out_SD_mpl, Mz)
1✔
2364

2365
        z_full = inputs['monopile_z_full']
1✔
2366
        z_sec, _ = util.nodal2sectional(z_full)
1✔
2367
        z = (z_sec - z_sec[0]) / (z_sec[-1] - z_sec[0])
1✔
2368

2369
        # SubDyn reports in N, but ElastoDyn and units here report in kN, so scale by 0.001
2370
        outputs['monopile_maxMy_Fx'] = 1e-3*spline_Fx(z)
1✔
2371
        outputs['monopile_maxMy_Fy'] = 1e-3*spline_Fy(z)
1✔
2372
        outputs['monopile_maxMy_Fz'] = 1e-3*spline_Fz(z)
1✔
2373
        outputs['monopile_maxMy_Mx'] = 1e-3*spline_Mx(z)
1✔
2374
        outputs['monopile_maxMy_My'] = 1e-3*spline_My(z)
1✔
2375
        outputs['monopile_maxMy_Mz'] = 1e-3*spline_Mz(z)
1✔
2376

2377
        return outputs
1✔
2378

2379
    def calculate_AEP(self, sum_stats, case_list, dlc_generator, discrete_inputs, outputs, discrete_outputs):
1✔
2380
        """
2381
        Calculates annual energy production of the relevant DLCs in `case_list`.
2382

2383
        Parameters
2384
        ----------
2385
        sum_stats : pd.DataFrame
2386
        case_list : list
2387
        dlc_list : list
2388
        """
2389
        ## Get AEP and power curve
2390

2391
        # determine which dlc will be used for the powercurve calculations, allows using dlc 1.1 if specific power curve calculations were not run
2392

2393
        modopts = self.options['modeling_options']
1✔
2394
        DLCs = [i_dlc['DLC'] for i_dlc in modopts['DLC_driver']['DLCs']]
1✔
2395
        if 'AEP' in DLCs:
1✔
NEW
2396
            DLC_label_for_AEP = 'AEP'
×
2397
        else:
2398
            DLC_label_for_AEP = '1.1'
1✔
2399
            logger.warning('WARNING: DLC 1.1 is being used for AEP calculations.  Use the AEP DLC for more accurate wind modeling with constant TI.')
1✔
2400

2401
        idx_pwrcrv = []
1✔
2402
        U = []
1✔
2403
        for i_case in range(dlc_generator.n_cases):
1✔
2404
            if dlc_generator.cases[i_case].label == DLC_label_for_AEP:
1✔
2405
                idx_pwrcrv = np.append(idx_pwrcrv, i_case)
1✔
2406
                U = np.append(U, dlc_generator.cases[i_case].URef)
1✔
2407

2408
        stats_pwrcrv = sum_stats.iloc[idx_pwrcrv].copy()
1✔
2409

2410
        # Calculate AEP and Performance Data
2411
        if len(U) > 1 and self.fst_vt['Fst']['CompServo'] == 1:
1✔
2412
            pp = PowerProduction(discrete_inputs['turbine_class'])
1✔
2413
            pwr_curve_vars   = ["GenPwr", "RtFldCp", "RtFldCt", "RotSpeed", "BldPitch1"]
1✔
2414
            AEP, perf_data = pp.AEP(stats_pwrcrv, U, pwr_curve_vars)
1✔
2415

2416
            outputs['P_out'] = perf_data['GenPwr']['mean'] * 1.e3
1✔
2417
            outputs['Cp_out'] = perf_data['RtFldCp']['mean']
1✔
2418
            outputs['Ct_out'] = perf_data['RtFldCt']['mean']
1✔
2419
            outputs['Omega_out'] = perf_data['RotSpeed']['mean']
1✔
2420
            outputs['pitch_out'] = perf_data['BldPitch1']['mean']
1✔
2421
            outputs['AEP'] = AEP
1✔
2422
        else:
2423
            # If DLC 1.1 was run
2424
            if len(stats_pwrcrv['RtFldCp']['mean']): 
1✔
2425
                outputs['Cp_out'] = stats_pwrcrv['RtFldCp']['mean']
1✔
2426
                outputs['Ct_out'] = stats_pwrcrv['RtFldCt']['mean']
1✔
2427
                outputs['Omega_out'] = stats_pwrcrv['RotSpeed']['mean']
1✔
2428
                outputs['pitch_out'] = stats_pwrcrv['BldPitch1']['mean']
1✔
2429
                if self.fst_vt['Fst']['CompServo'] == 1:
1✔
2430
                    outputs['AEP'] = stats_pwrcrv['GenPwr']['mean']
1✔
2431
                    outputs['P_out'] = stats_pwrcrv['GenPwr']['mean'].iloc[0] * 1.e3
1✔
2432
                logger.warning('WARNING: OpenFAST is run at a single wind speed. AEP cannot be estimated. Using average power instead.')
1✔
2433
            else:
2434
                outputs['Cp_out'] = sum_stats['RtFldCp']['mean'].mean()
1✔
2435
                outputs['Ct_out'] = sum_stats['RtFldCt']['mean'].mean()
1✔
2436
                outputs['Omega_out'] = sum_stats['RotSpeed']['mean'].mean()
1✔
2437
                outputs['pitch_out'] = sum_stats['BldPitch1']['mean'].mean()
1✔
2438
                if self.fst_vt['Fst']['CompServo'] == 1:
1✔
2439
                    outputs['AEP'] = sum_stats['GenPwr']['mean'].mean()
1✔
2440
                    outputs['P_out'] = sum_stats['GenPwr']['mean'].iloc[0] * 1.e3
1✔
2441
                logger.warning('WARNING: OpenFAST is not run using DLC AEP, 1.1, or 1.2. AEP cannot be estimated. Using average power instead.')
1✔
2442

2443
        if len(U)>0:
1✔
2444
            outputs['V_out'] = np.unique(U)
1✔
2445
        else:
2446
            outputs['V_out'] = dlc_generator.cases[0].URef
1✔
2447

2448
        return outputs, discrete_outputs
1✔
2449

2450
    def get_weighted_DELs(self, dlc_generator, DELs, damage, discrete_inputs, outputs, discrete_outputs):
1✔
2451
        modopt = self.options['modeling_options']
1✔
2452

2453
        # See if we have fatigue DLCs
2454
        U = np.zeros(dlc_generator.n_cases)
1✔
2455
        ifat = []
1✔
2456
        for k in range(dlc_generator.n_cases):
1✔
2457
            U[k] = dlc_generator.cases[k].URef
1✔
2458
            
2459
            if dlc_generator.cases[k].label in ['1.2', '6.4', '7.2']:
1✔
2460
                ifat.append( k )
×
2461

2462
        # If fatigue DLCs are present, then limit analysis to those only
2463
        if len(ifat) > 0:
1✔
2464
            U = U[ifat]
×
2465
            DELs = DELs.iloc[ ifat ]
×
2466
            damage = damage.iloc[ ifat ]
×
2467
        
2468
        # Get wind distribution probabilities, make sure they are normalized
2469
        # This should also take care of averaging across seeds
2470
        pp = PowerProduction(discrete_inputs['turbine_class'])
1✔
2471
        ws_prob = pp.prob_WindDist(U, disttype='pdf')
1✔
2472
        ws_prob /= ws_prob.sum()
1✔
2473

2474
        # Scale all DELs and damage by probability and collapse over the various DLCs (inner dot product)
2475
        # Also work around NaNs
2476
        DELs = DELs.fillna(0.0).multiply(ws_prob, axis=0).sum()
1✔
2477
        damage = damage.fillna(0.0).multiply(ws_prob, axis=0).sum()
1✔
2478
        
2479
        # Standard DELs for blade root and tower base
2480
        outputs['DEL_RootMyb'] = np.max([DELs[f'RootMyb{k+1}'] for k in range(self.n_blades)])
1✔
2481
        outputs['DEL_TwrBsMyt'] = DELs['TwrBsM']
1✔
2482
        outputs['DEL_TwrBsMyt_ratio'] = DELs['TwrBsM']/self.options['opt_options']['constraints']['control']['DEL_TwrBsMyt']['max']
1✔
2483
            
2484
        # Compute total fatigue damage in spar caps at blade root and trailing edge at max chord location
2485
        if not modopt['Level3']['from_openfast']:
1✔
2486
            for k in range(1,self.n_blades+1):
1✔
2487
                for u in ['U','L']:
1✔
2488
                    damage[f'BladeRootSpar{u}_Axial{k}'] = (damage[f'RootSpar{u}_Fzb{k}'] +
1✔
2489
                                                        damage[f'RootSpar{u}_Mxb{k}'] +
2490
                                                        damage[f'RootSpar{u}_Myb{k}'])
2491
                    damage[f'BladeMaxcTE{u}_Axial{k}'] = (damage[f'Spn2te{u}_FLzb{k}'] +
1✔
2492
                                                        damage[f'Spn2te{u}_MLxb{k}'] +
2493
                                                        damage[f'Spn2te{u}_MLyb{k}'])
2494

2495
            # Compute total fatigue damage in low speed shaft, tower base, monopile base
2496
            damage['LSSAxial'] = 0.0
1✔
2497
            damage['LSSShear'] = 0.0
1✔
2498
            damage['TowerBaseAxial'] = 0.0
1✔
2499
            damage['TowerBaseShear'] = 0.0
1✔
2500
            damage['MonopileBaseAxial'] = 0.0
1✔
2501
            damage['MonopileBaseShear'] = 0.0
1✔
2502
            for s in ['Ax','Sh']:
1✔
2503
                sstr = 'Axial' if s=='Ax' else 'Shear'
1✔
2504
                for ik, k in enumerate(['F','M']):
1✔
2505
                    for ix, x in enumerate(['x','yz']):
1✔
2506
                        damage[f'LSS{sstr}'] += damage[f'LSShft{s}{k}{x}a']
1✔
2507
                    for ix, x in enumerate(['xy','z']):
1✔
2508
                        damage[f'TowerBase{sstr}'] += damage[f'TwrBs{s}{k}{x}t']
1✔
2509
                        if modopt['flags']['monopile'] and modopt['Level3']['flag']:
1✔
2510
                            damage[f'MonopileBase{sstr}'] += damage[f'M1N1{s}{k}K{x}e']
1✔
2511

2512
            # Assemble damages
2513
            outputs['damage_blade_root_sparU'] = np.max([damage[f'BladeRootSparU_Axial{k+1}'] for k in range(self.n_blades)])
1✔
2514
            outputs['damage_blade_root_sparL'] = np.max([damage[f'BladeRootSparL_Axial{k+1}'] for k in range(self.n_blades)])
1✔
2515
            outputs['damage_blade_maxc_teU'] = np.max([damage[f'BladeMaxcTEU_Axial{k+1}'] for k in range(self.n_blades)])
1✔
2516
            outputs['damage_blade_maxc_teL'] = np.max([damage[f'BladeMaxcTEL_Axial{k+1}'] for k in range(self.n_blades)])
1✔
2517
            outputs['damage_lss'] = np.sqrt( damage['LSSAxial']**2 + damage['LSSShear']**2 )
1✔
2518
            outputs['damage_tower_base'] = np.sqrt( damage['TowerBaseAxial']**2 + damage['TowerBaseShear']**2 )
1✔
2519
            outputs['damage_monopile_base'] = np.sqrt( damage['MonopileBaseAxial']**2 + damage['MonopileBaseShear']**2 )
1✔
2520

2521
            # Log damages
2522
            if self.options['opt_options']['constraints']['damage']['tower_base']['log']:
1✔
2523
                outputs['damage_tower_base'] = np.log(outputs['damage_tower_base'])
1✔
2524

2525
        return outputs, discrete_outputs
1✔
2526

2527
    def get_control_measures(self, sum_stats, chan_time, inputs, discrete_inputs, outputs, discrete_outputs):
1✔
2528
        '''
2529
        calculate control measures:
2530
            - rotor_overspeed
2531

2532
        given:
2533
            - sum_stats : pd.DataFrame
2534
        '''
2535

2536
        # rotor overspeed
2537
        outputs['rotor_overspeed'] = ( np.max(sum_stats['GenSpeed']['max']) * np.pi/30. / self.fst_vt['DISCON_in']['PC_RefSpd'] ) - 1.0
1✔
2538

2539
        # nacelle accelleration
2540
        outputs['max_nac_accel'] = sum_stats['NcIMUTA']['max'].max()
1✔
2541

2542
        # Max pitch rate
2543
        max_pitch_rates = np.r_[sum_stats['dBldPitch1']['max'],sum_stats['dBldPitch2']['max'],sum_stats['dBldPitch3']['max']]
1✔
2544
        outputs['max_pitch_rate_sim'] = max(max_pitch_rates)  / np.rad2deg(self.fst_vt['DISCON_in']['PC_MaxRat'])        # normalize by ROSCO pitch rate
1✔
2545

2546
        # pitch travel and duty cycle
2547
        if self.options['modeling_options']['General']['openfast_configuration']['keep_time']:
1✔
2548
            tot_time = 0
1✔
2549
            tot_travel = 0
1✔
2550
            num_dir_changes = 0
1✔
2551
            max_pitch_rate = [0,0,0]
1✔
2552
            for i_ts, ts in enumerate(chan_time):
1✔
2553
                t_span = self.TMax[i_ts] - self.TStart[i_ts]
1✔
2554
                for i_blade in range(self.fst_vt['ElastoDyn']['NumBl']):
1✔
2555
                    ts[f'dBldPitch{i_blade+1}'] = np.r_[0,np.diff(ts['BldPitch1'])] / self.fst_vt['Fst']['DT']
1✔
2556

2557
                    time_ind = ts['Time'] >= self.TStart[i_ts]
1✔
2558

2559
                    # total time
2560
                    tot_time += t_span
1✔
2561

2562
                    # total pitch travel (\int |\dot{\frac{d\theta}{dt}| dt)
2563
                    tot_travel += np.trapz(np.abs(ts[f'dBldPitch{i_blade+1}'])[time_ind], x=ts['Time'][time_ind])
1✔
2564

2565
                    # number of direction changes on each blade
2566
                    num_dir_changes += np.sum(np.abs(np.diff(np.sign(ts[f'dBldPitch{i_blade+1}'][time_ind])))) / 2
1✔
2567

2568
                    # max operational pitch rate
2569
                    max_pitch_rate[i_blade] = max(np.max(np.abs(ts[f'dBldPitch{i_blade+1}'])),max_pitch_rate[i_blade])
1✔
2570

2571
            # Normalize by number of blades, total time
2572
            avg_travel_per_sec = tot_travel / self.fst_vt['ElastoDyn']['NumBl'] / tot_time
1✔
2573
            outputs['avg_pitch_travel'] = avg_travel_per_sec
1✔
2574

2575
            dir_change_per_sec = num_dir_changes / self.fst_vt['ElastoDyn']['NumBl'] / tot_time
1✔
2576
            outputs['pitch_duty_cycle'] = dir_change_per_sec
1✔
2577
            # TODO: figure out aggregated calculated channels
2578

2579
        else:
2580
            logger.warning('openmdao_openfast warning: avg_pitch_travel, and pitch_duty_cycle require keep_time = True')
×
2581

2582

2583

2584
        return outputs, discrete_outputs
1✔
2585

2586
    def get_floating_measures(self,sum_stats, chan_time, inputs, discrete_inputs, outputs, discrete_outputs):
1✔
2587
        '''
2588
        calculate floating measures:
2589
            - Std_PtfmPitch (max over all dlcs if constraint, mean otheriwse)
2590
            - Max_PtfmPitch
2591

2592
        given:
2593
            - sum_stats : pd.DataFrame
2594
        '''
2595

2596
        if self.options['opt_options']['constraints']['control']['Std_PtfmPitch']['flag']:
1✔
2597
            outputs['Std_PtfmPitch'] = np.max(sum_stats['PtfmPitch']['std'])
×
2598
        else:
2599
            # Let's just average the standard deviation of PtfmPitch for now
2600
            # TODO: weight based on WS distribution, or something else
2601
            outputs['Std_PtfmPitch'] = np.mean(sum_stats['PtfmPitch']['std'])
1✔
2602

2603
        outputs['Max_PtfmPitch']  = np.max(sum_stats['PtfmPitch']['max'])
1✔
2604

2605
        # Max platform offset        
2606
        outputs['Max_Offset'] = sum_stats['PtfmOffset']['max'].max()
1✔
2607

2608
        return outputs, discrete_outputs
1✔
2609

2610
    def get_OL2CL_error(self,chan_time,outputs):
1✔
2611
        ol_case_names = [os.path.join(
×
2612
            weis_dir,
2613
            self.options['modeling_options']['OL2CL']['trajectory_dir'],
2614
            case_name + '.p'
2615
        ) for case_name in case_naming(self.options['modeling_options']['DLC_driver']['n_cases'],'oloc')]
2616

2617
        rms_pitch_error = np.full(len(chan_time),fill_value=1000.)
×
2618
        for i_ts, timeseries in enumerate(chan_time):
×
2619
            # Get closed loop timeseries
2620
            cl_output = OpenFASTOutput.from_dict(timeseries, self.FAST_namingOut)
×
2621
            cl_ts = cl_output.df
×
2622

2623
            # Get open loop timeseries
2624
            ol_ts = pd.read_pickle(ol_case_names[i_ts])
×
2625

2626
            # resample OL timeseries to match closed loop timeseries
2627
            ol_resample = np.interp(cl_ts['Time'],ol_ts['Time'],ol_ts['BldPitch1'])
×
2628

2629
            # difference between open loop and closed loop (deg.)
2630
            pitch_error = cl_ts['BldPitch1'] - ol_resample
×
2631

2632
            rms_pitch_error[i_ts] = np.sqrt(np.mean(pitch_error**2))
×
2633

2634
            if self.options['modeling_options']['OL2CL']['save_error']:
×
2635
                save_dir = os.path.join(self.FAST_runDirectory,'iteration_'+str(self.of_inumber),'timeseries')
×
2636
                pitch_error.to_pickle(os.path.join(save_dir,'pitch_error_'+ str(i_ts) + '.p'))
×
2637

2638
        # Average over DLCs and return, TODO: weight in future?  only works for a few wind speeds currently
2639
        outputs['OL2CL_pitch'] = np.mean(rms_pitch_error)
×
2640
        return outputs
×
2641

2642

2643
    def get_ac_axis(self, inputs):
1✔
2644
        
2645
        # Get the absolute offset between pitch axis (rotation center) and aerodynamic center
2646
        ch_offset = inputs['chord'] * (inputs['ac'] - inputs['le_location'])
1✔
2647
        # Rotate it by the twist using the AD15 coordinate system
2648
        x , y = util.rotate(0., 0., 0., ch_offset, -np.deg2rad(inputs['theta']))
1✔
2649
        # Apply offset to determine the AC axis
2650
        BlCrvAC = inputs['ref_axis_blade'][:,0] + x
1✔
2651
        BlSwpAC = inputs['ref_axis_blade'][:,1] + y
1✔
2652
        
2653
        return BlCrvAC, BlSwpAC
1✔
2654
    
2655

2656
    def write_FAST(self, fst_vt, discrete_outputs):
1✔
2657
        writer                   = InputWriter_OpenFAST()
1✔
2658
        writer.fst_vt            = fst_vt
1✔
2659
        writer.FAST_runDirectory = self.FAST_runDirectory
1✔
2660
        writer.FAST_namingOut    = self.FAST_namingOut
1✔
2661
        writer.execute()
1✔
2662

2663
    def writeCpsurfaces(self, inputs):
1✔
2664

2665
        modopt = self.options['modeling_options']
×
2666
        FASTpref  = modopt['openfast']['FASTpref']
×
2667
        file_name = os.path.join(FASTpref['file_management']['FAST_runDirectory'], FASTpref['file_management']['FAST_namingOut'] + '_Cp_Ct_Cq.dat')
×
2668

2669
        # Write Cp-Ct-Cq-TSR tables file
2670
        n_pitch = len(inputs['pitch_vector'])
×
2671
        n_tsr   = len(inputs['tsr_vector'])
×
2672
        n_U     = len(inputs['U_vector'])
×
2673

2674
        file = open(file_name,'w')
×
2675
        file.write('# ------- Rotor performance tables ------- \n')
×
2676
        file.write('# ------------ Written using AeroElasticSE with data from CCBlade ------------\n')
×
2677
        file.write('\n')
×
2678
        file.write('# Pitch angle vector - x axis (matrix columns) (deg)\n')
×
2679
        for i in range(n_pitch):
×
2680
            file.write('%.2f   ' % inputs['pitch_vector'][i])
×
2681
        file.write('\n# TSR vector - y axis (matrix rows) (-)\n')
×
2682
        for i in range(n_tsr):
×
2683
            file.write('%.2f   ' % inputs['tsr_vector'][i])
×
2684
        file.write('\n# Wind speed vector - z axis (m/s)\n')
×
2685
        for i in range(n_U):
×
2686
            file.write('%.2f   ' % inputs['U_vector'][i])
×
2687
        file.write('\n')
×
2688

2689
        file.write('\n# Power coefficient\n\n')
×
2690

2691
        for i in range(n_U):
×
2692
            for j in range(n_tsr):
×
2693
                for k in range(n_pitch):
×
2694
                    file.write('%.5f   ' % inputs['Cp_aero_table'][j,k,i])
×
2695
                file.write('\n')
×
2696
            file.write('\n')
×
2697

2698
        file.write('\n#  Thrust coefficient\n\n')
×
2699
        for i in range(n_U):
×
2700
            for j in range(n_tsr):
×
2701
                for k in range(n_pitch):
×
2702
                    file.write('%.5f   ' % inputs['Ct_aero_table'][j,k,i])
×
2703
                file.write('\n')
×
2704
            file.write('\n')
×
2705

2706
        file.write('\n# Torque coefficient\n\n')
×
2707
        for i in range(n_U):
×
2708
            for j in range(n_tsr):
×
2709
                for k in range(n_pitch):
×
2710
                    file.write('%.5f   ' % inputs['Cq_aero_table'][j,k,i])
×
2711
                file.write('\n')
×
2712
            file.write('\n')
×
2713

2714
        file.close()
×
2715

2716

2717
        return file_name
×
2718

2719
    def save_timeseries(self,chan_time):
1✔
2720
        '''
2721
        Save ALL the timeseries: each iteration and openfast run thereof
2722
        TODO: move this deeper into runFAST so we can clear chan_time
2723
        '''
2724

2725
        # Make iteration directory
2726
        save_dir = os.path.join(self.FAST_runDirectory,'iteration_'+str(self.of_inumber),'timeseries')
1✔
2727
        os.makedirs(save_dir, exist_ok=True)
1✔
2728

2729
        # Save each timeseries as a pickled dataframe
2730
        for i_ts, timeseries in enumerate(chan_time):
1✔
2731
            output = OpenFASTOutput.from_dict(timeseries, self.FAST_namingOut)
1✔
2732
            output.df.to_pickle(os.path.join(save_dir,self.FAST_namingOut + '_' + str(i_ts) + '.p'))
1✔
2733

2734
    def save_iterations(self,summ_stats,DELs,discrete_outputs):
1✔
2735
        '''
2736
        Save summary stats, DELs of each iteration
2737
        '''
2738

2739
        # Make iteration directory
2740
        save_dir = os.path.join(self.FAST_runDirectory,'iteration_'+str(self.of_inumber))
1✔
2741
        os.makedirs(save_dir, exist_ok=True)
1✔
2742

2743
        # Save dataframes as pickles
2744
        summ_stats.to_pickle(os.path.join(save_dir,'summary_stats.p'))
1✔
2745
        DELs.to_pickle(os.path.join(save_dir,'DELs.p'))
1✔
2746

2747
        # Save fst_vt as pickle
2748
        with open(os.path.join(save_dir,'fst_vt.p'), 'wb') as f:
1✔
2749
            pickle.dump(self.fst_vt,f)
1✔
2750

2751
        discrete_outputs['ts_out_dir'] = save_dir
1✔
2752

2753
def apply_olaf_parameters(dlc_generator,fst_vt):
1✔
2754
    '''
2755
    Apply OLAF parameters using wind speed, rotor speed, and rotor radius
2756
    Parameters are applied for each case, if WakeMod = 3
2757

2758
    This method requires that the case inputs have been generated for each case in dlc_generator
2759

2760
    OLAF parameters will be appended to proper openfast_case_input for each dlc
2761
    '''
2762

2763
    # Loop over cases
NEW
2764
    for case_input in dlc_generator.openfast_case_inputs:
×
2765

NEW
2766
        min_TMax = min(case_input[('Fst', 'TMax')]['vals'])
×
2767

2768
        # find group with wind_speed
NEW
2769
        wind_group = case_input[('InflowWind', 'HWindSpeed')]['group']
×
2770

NEW
2771
        wind_speeds = case_input[('InflowWind', 'HWindSpeed')]['vals']
×
NEW
2772
        rotor_speeds = case_input[('ElastoDyn', 'RotSpeed')]['vals']
×
2773

NEW
2774
        dt_fvw = len(wind_speeds) * [None]
×
NEW
2775
        tMin = len(wind_speeds) * [None]
×
NEW
2776
        nNWPanels = len(wind_speeds) * [None]
×
NEW
2777
        nNWPanelsFree = len(wind_speeds) * [None]
×
NEW
2778
        nFWPanels = len(wind_speeds) * [None]
×
NEW
2779
        nFWPanelsFree = len(wind_speeds) * [None]
×
2780

NEW
2781
        for i_case in range(len(wind_speeds)):
×
NEW
2782
            dt_fvw[i_case], tMin[i_case], nNWPanels[i_case], nNWPanelsFree[i_case], nFWPanels[i_case], nFWPanelsFree[i_case] = OLAFParams(rotor_speeds[i_case], wind_speeds[i_case], fst_vt['ElastoDyn']['TipRad'])
×
2783

2784
            # Check that runs are long enough
NEW
2785
            if tMin[i_case] > min_TMax:
×
NEW
2786
                logger.warning("OLAF runs are too short in time, the wake is not at convergence")
×
2787

2788
            # TODO: skipping timestep setting because they're big timesteps
2789
            # # Set timestep
2790
            # if fst_vt['Fst']['CompElast'] == 1:
2791
            #     DT[i_case] = dt_fvw[i_case]
2792
            
NEW
2793
            case_input[("AeroDyn15","OLAF","DTfvw")] = {'vals': dt_fvw, 'group': wind_group}
×
NEW
2794
            case_input[("AeroDyn15","OLAF","nNWPanels")] = {'vals': nNWPanels, 'group': wind_group}
×
NEW
2795
            case_input[("AeroDyn15","OLAF","nNWPanelsFree")] = {'vals': nNWPanelsFree, 'group': wind_group}
×
NEW
2796
            case_input[("AeroDyn15","OLAF","nFWPanels")] = {'vals': nFWPanels, 'group': wind_group}
×
NEW
2797
            case_input[("AeroDyn15","OLAF","nFWPanelsFree")] = {'vals': nFWPanelsFree, 'group': wind_group}
×
2798

STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc