From f28ea639c2c51d671483065ff84fee211adb402e Mon Sep 17 00:00:00 2001 From: Regis Thedin Date: Wed, 24 Aug 2022 16:04:52 -0600 Subject: [PATCH 001/124] Add low- and high-res boxes domain specifications --- pyFAST/fastfarm/TurbSimCaseCreation.py | 125 +++++++++++++++++-------- 1 file changed, 87 insertions(+), 38 deletions(-) diff --git a/pyFAST/fastfarm/TurbSimCaseCreation.py b/pyFAST/fastfarm/TurbSimCaseCreation.py index 71c1b9f..88f517e 100644 --- a/pyFAST/fastfarm/TurbSimCaseCreation.py +++ b/pyFAST/fastfarm/TurbSimCaseCreation.py @@ -13,38 +13,46 @@ class TSCaseCreation: - def __init__(self, D, HubHt, Vhub, TI, PLexp, x, y, z=None, zbot=1.0, cmax=5.0, fmax=5.0, Cmeander=1.9): + def __init__(self, D, HubHt, Vhub, TI, PLexp, x, y, z=None, zbot=1.0, cmax=5.0, fmax=5.0, + Cmeander=1.9, boxType='highres', high_ext=1.2): """ Instantiate the object. Parameters ---------- - D : float, - rotor diameter (m) - HubHt : float, - turbine hub height (m) - Vhub : float, - mean wind speed at hub height (m/s) - TI : float, + D : float, + rotor diameter (m) + HubHt : float, + turbine hub height (m) + Vhub : float, + mean wind speed at hub height (m/s) + TI : float, turbulence intensity at hub height - PLexp : float, - power law exponent for shear (-) - x, y, z : float, - x-, y-, and z-location of turbine, respectively - if z is None, z is set to 0 - cmax : float, - maximum blade chord (m). If not specified, set to NREL 5MW value. - fmax : float, - maximum excitation frequency (Hz). If not specified set to NREL 5MW tower value. + PLexp : float, + power law exponent for shear (-) + x, y, z : float, + x-, y-, and z-location of turbine, respectively + if z is None, z is set to 0 + cmax : float, + maximum blade chord (m). If not specified, set to NREL 5MW value. + fmax : float, + maximum excitation frequency (Hz). If not specified set to NREL 5MW tower value. + boxType : str, + box type, either 'lowres' or 'highres'. Sets the appropriate dt, dy, and dz for discretization + Defaults to `highres` for backward compatibility + high_ext : float + extent of the high-res box around individual turbines (in D) """ # Turbine parameters self.Turb(D, HubHt, cmax, fmax) # Turbine location self.turbLocs(x,y,z) # Discretization - self.discretization(Vhub, TI, PLexp) + self.discretization(Vhub, TI, PLexp, Cmeander=Cmeander, boxType=boxType) # Setup domain size - self.domainSize(zbot=zbot, Cmeander=Cmeander) + self.domainSize(zbot=zbot, Cmeander=Cmeander, boxType=boxType, high_ext=high_ext) + # Determine origin + # self.originLoc() def Turb(self, D, HubHt, cmax=5.0, fmax=5.0): """ @@ -85,34 +93,75 @@ def turbLocs(self,x,y,z=None): else: self.z = np.asarray(z) - def discretization(self,Vhub,TI,Shear): + def discretization(self,Vhub,TI,Shear, Cmeander=1.9, boxType='highres'): + ''' + Specify discretization for both the high-res and low-res boxes. Follows guidelines present at + https://openfast.readthedocs.io/en/main/source/user/fast.farm/ModelGuidance.html#low-resolution-domain + + ''' self.URef = Vhub self.TI = TI self.PLexp = Shear # Derived properties - self.dt = 1.0/(2.0*self.fmax) - self.dy = self.cmax - self.dz = self.cmax + if boxType == 'lowres': + self.dt = Cmeander*self.D/(10*Vhub) + ds_low = Cmeander*self.D*Vhub/150 + ds_high = self.cmax + self.dy = np.floor(ds_low/ds_high)*ds_high + self.dz = np.floor(ds_low/ds_high)*ds_high + + elif boxType == 'highres': + self.dt = 1.0/(2.0*self.fmax) + self.dy = self.cmax + self.dz = self.cmax + + else: + raise ValueError("boxType can only be 'lowres' or 'highres'. Stopping.") - def domainSize(self, zbot, Cmeander=1.9): + def domainSize(self, zbot, Cmeander=1.9, boxType='highres', high_ext=1.2): - ymin = min(self.y)-2.23313*Cmeander*self.D/2 - ymax = max(self.y)+2.23313*Cmeander*self.D/2 - - width_des = ymax-ymin - height_des = self.RefHt+self.D/2+2.23313*Cmeander*self.D/2 - - self.ny = round(width_des/self.dy)+1 - self.nz = round(height_des/self.dz)+1 - - self.Width = self.dy*(self.ny-1) - self.Height = self.dz*(self.nz-1) - - Dgrid=min(self.Height,self.Width) - self.HubHt_for_TS = zbot-0.5*Dgrid+self.Height + if boxType == 'lowres': + ymin = min(self.y)-2.23313*Cmeander*self.D/2 + ymax = max(self.y)+2.23313*Cmeander*self.D/2 + + Ydist_Low = ymax - ymin + Zdist_Low = self.RefHt + self.D/2 + 2.23313*Cmeander*self.D/2 + + self.ny = np.ceil(Ydist_Low/self.dy)+1 + self.nz = np.ceil(Zdist_Low/self.dz)+1 + + self.Width = self.dy*(self.ny-1) + self.Height = self.dz*(self.nz-1) + + Dgrid=min(self.Height,self.Width) + + # Set the hub height using half of the total grid height + self.HubHt_for_TS = zbot - 0.5*Dgrid + self.Height + elif boxType=='highres': + Ydist_high = high_ext*self.D + Zdist_high = self.RefHt + high_ext*self.D/2.0 - zbot + + self.ny = np.ceil(Ydist_high/self.dy)+1 + self.nz = np.ceil(Zdist_high/self.dz)+1 + + self.Width = self.dy*(self.ny-1) + self.Height = self.dz*(self.nz-1) + + Dgrid = min(self.Height,self.Width) + + # Set the hub height using half of the total grid height + self.HubHt_for_TS = zbot - 0.5*Dgrid + self.Height + + else: + raise ValueError("boxType can only be 'lowres' or 'highres'. Stopping.") + + + def originLoc(self): + raise NotImplementedError + def plotSetup(self, fig=None, ax=None): """ Plot a figure showing the turbine locations and the extent of the turbulence box""" From acb6e93a4af09f1800d9865e27e258dd553274bc Mon Sep 17 00:00:00 2001 From: Regis Thedin Date: Wed, 24 Aug 2022 16:25:22 -0600 Subject: [PATCH 002/124] Update TurbSim input file example template. See extended commit message Update comments based on the template available at https://openfast.readthedocs.io/en/main/_downloads/e864ec3914cc13b6e94c9a49323cf996/TurbSim.inp The file in the readthedocs linked above contains more parameters- these have not been added. I mainly fixed a very confusing comment about the `UserFile` usage. --- .../examples/SampleFiles/TestCase.inp | 106 +++++++++--------- 1 file changed, 53 insertions(+), 53 deletions(-) diff --git a/pyFAST/fastfarm/examples/SampleFiles/TestCase.inp b/pyFAST/fastfarm/examples/SampleFiles/TestCase.inp index 06a6b4b..a24cedb 100644 --- a/pyFAST/fastfarm/examples/SampleFiles/TestCase.inp +++ b/pyFAST/fastfarm/examples/SampleFiles/TestCase.inp @@ -1,73 +1,73 @@ --------TurbSim v2.00.* Input File------------------------ for Certification Test #1 (Kaimal Spectrum, formatted FF files). ---------Runtime Options----------------------------------- -False Echo - Echo input data to .ech (flag) -123456 RandSeed1 - First random seed (-2147483648 to 2147483647) -RanLux RandSeed2 - Second random seed (-2147483648 to 2147483647) for intrinsic pRNG, or an alternative pRNG: "RanLux" or "RNSNLW" -False WrBHHTP - Output hub-height turbulence parameters in binary form? (Generates RootName.bin) -False WrFHHTP - Output hub-height turbulence parameters in formatted form? (Generates RootName.dat) -False WrADHH - Output hub-height time-series data in AeroDyn form? (Generates RootName.hh) -True WrADFF - Output full-field time-series data in TurbSim/AeroDyn form? (Generates RootName.bts) -False WrBLFF - Output full-field time-series data in BLADED/AeroDyn form? (Generates RootName.wnd) -False WrADTWR - Output tower time-series data? (Generates RootName.twr) -False WrFMTFF - Output full-field time-series data in formatted (readable) form? (Generates RootName.u, RootName.v, RootName.w) -False WrACT - Output coherent turbulence time steps in AeroDyn form? (Generates RootName.cts) -True Clockwise - Clockwise rotation looking downwind? (used only for full-field binary files - not necessary for AeroDyn) -0 ScaleIEC - Scale IEC turbulence models to exact target standard deviation? [0=no additional scaling; 1=use hub scale uniformly; 2=use individual scales] +False Echo - Echo input data to .ech (flag) +123456 RandSeed1 - First random seed (-2147483648 to 2147483647) +RanLux RandSeed2 - Second random seed (-2147483648 to 2147483647) for intrinsic pRNG, or an alternative pRNG: "RanLux" or "RNSNLW" +False WrBHHTP - Output hub-height turbulence parameters in binary form? (Generates RootName.bin) +False WrFHHTP - Output hub-height turbulence parameters in formatted form? (Generates RootName.dat) +False WrADHH - Output hub-height time-series data in AeroDyn form? (Generates RootName.hh) +True WrADFF - Output full-field time-series data in TurbSim/AeroDyn form? (Generates RootName.bts) +False WrBLFF - Output full-field time-series data in BLADED/AeroDyn form? (Generates RootName.wnd) +False WrADTWR - Output tower time-series data? (Generates RootName.twr) +False WrFMTFF - Output full-field time-series data in formatted (readable) form? (Generates RootName.u, RootName.v, RootName.w) +False WrACT - Output coherent turbulence time steps in AeroDyn form? (Generates RootName.cts) +True Clockwise - Clockwise rotation looking downwind? (used only for full-field binary files - not necessary for AeroDyn) +0 ScaleIEC - Scale IEC turbulence models to exact target standard deviation? [0=no additional scaling; 1=use hub scale uniformly; 2=use individual scales] --------Turbine/Model Specifications----------------------- -57 NumGrid_Z - Vertical grid-point matrix dimension -76 NumGrid_Y - Horizontal grid-point matrix dimension -0.100000 TimeStep - Time step [seconds] -5.0000 AnalysisTime - Length of analysis time series [seconds] (program will add time if necessary: AnalysisTime = MAX(AnalysisTime, UsableTime+GridWidth/MeanHHWS) ) -"ALL" UsableTime - Usable length of output time series [seconds] (program will add GridWidth/MeanHHWS seconds unless UsableTime is "ALL") -141.000 HubHt - Hub height [m] (should be > 0.5*GridHeight) -280.000 GridHeight - Grid height [m] -375.000 GridWidth - Grid width [m] (should be >= 2*(RotorRadius+ShaftLength)) -0 VFlowAng - Vertical mean flow (uptilt) angle [degrees] -0 HFlowAng - Horizontal mean flow (skew) angle [degrees] +57 NumGrid_Z - Vertical grid-point matrix dimension +76 NumGrid_Y - Horizontal grid-point matrix dimension +0.100000 TimeStep - Time step [seconds] +5.0000 AnalysisTime - Length of analysis time series [seconds] (program will add time if necessary: AnalysisTime = MAX(AnalysisTime, UsableTime+GridWidth/MeanHHWS) ) +"ALL" UsableTime - Usable length of output time series [seconds] (program will add GridWidth/MeanHHWS seconds unless UsableTime is "ALL") +141.000 HubHt - Hub height [m] (should be > 0.5*GridHeight) +280.000 GridHeight - Grid height [m] +375.000 GridWidth - Grid width [m] (should be >= 2*(RotorRadius+ShaftLength)) +0 VFlowAng - Vertical mean flow (uptilt) angle [degrees] +0 HFlowAng - Horizontal mean flow (skew) angle [degrees] --------Meteorological Boundary Conditions------------------- -"IECKAI" TurbModel - Turbulence model ("IECKAI","IECVKM","GP_LLJ","NWTCUP","SMOOTH","WF_UPW","WF_07D","WF_14D","TIDAL","API","IECKAI","TIMESR", or "NONE") -"unused" UserFile - Name of the file that contains inputs for user-defined spectra or time series inputs (used only for "IECKAI" and "TIMESR" models) -1 IECstandard - Number of IEC 61400-x standard (x=1,2, or 3 with optional 61400-1 edition number (i.e. "1-Ed2") ) +"IECKAI" TurbModel - Turbulence model ("IECKAI","IECVKM","GP_LLJ","NWTCUP","SMOOTH","WF_UPW","WF_07D","WF_14D","TIDAL","API","USRINP","USRVKM","TIMESR", or "NONE") +"unused" UserFile - Name of the file that contains inputs for user-defined spectra or time series inputs (used only for "USRINP" and "TIMESR" models) +1 IECstandard - Number of IEC 61400-x standard (x=1,2, or 3 with optional 61400-1 edition number (i.e. "1-Ed2") ) "10.000 " IECturbc - IEC turbulence characteristic ("A", "B", "C" or the turbulence intensity in percent) ("KHTEST" option with NWTCUP model, not used for other models) -"NTM" IEC_WindType - IEC turbulence type ("NTM"=normal, "xETM"=extreme turbulence, "xEWM1"=extreme 1-year wind, "xEWM50"=extreme 50-year wind, where x=wind turbine class 1, 2, or 3) -"default" ETMc - IEC Extreme Turbulence Model "c" parameter [m/s] -"PL" WindProfileType - Velocity profile type ("LOG";"PL"=power law;"JET";"H2L"=Log law for TIDAL model;"API";"PL";"TS";"IEC"=PL on rotor disk, LOG elsewhere; or "default") +"NTM" IEC_WindType - IEC turbulence type ("NTM"=normal, "xETM"=extreme turbulence, "xEWM1"=extreme 1-year wind, "xEWM50"=extreme 50-year wind, where x=wind turbine class 1, 2, or 3) +"default" ETMc - IEC Extreme Turbulence Model "c" parameter [m/s] +"PL" WindProfileType - Velocity profile type ("LOG";"PL"=power law;"JET";"H2L"=Log law for TIDAL model;"API";"PL";"TS";"IEC"=PL on rotor disk, LOG elsewhere; or "default") "PowerLaw_6ms02.dat" ProfileFile - Name of the file that contains input profiles for WindProfileType="PL" and/or TurbModel="USRVKM" [-] -78.045 RefHt - Height of the reference velocity (URef) [m] -6.000 URef - Mean (total) velocity at the reference height [m/s] (or "default" for JET velocity profile) [must be 1-hr mean for API model; otherwise is the mean over AnalysisTime seconds] -350 ZJetMax - Jet height [m] (used only for JET velocity profile, valid 70-490 m) -"0.200" PLExp - Power law exponent [-] (or "default") -"default" Z0 - Surface roughness length [m] (or "default") +78.045 RefHt - Height of the reference velocity (URef) [m] +6.000 URef - Mean (total) velocity at the reference height [m/s] (or "default" for JET velocity profile) [must be 1-hr mean for API model; otherwise is the mean over AnalysisTime seconds] +350 ZJetMax - Jet height [m] (used only for JET velocity profile, valid 70-490 m) +"0.200" PLExp - Power law exponent [-] (or "default") +"default" Z0 - Surface roughness length [m] (or "default") --------Non-IEC Meteorological Boundary Conditions------------ "default" Latitude - Site latitude [degrees] (or "default") -0.05 RICH_NO - Gradient Richardson number [-] -"default" UStar - Friction or shear velocity [m/s] (or "default") -"default" ZI - Mixing layer depth [m] (or "default") -"default" PC_UW - Hub mean u'w' Reynolds stress [m^2/s^2] (or "default" or "none") -"default" PC_UV - Hub mean u'v' Reynolds stress [m^2/s^2] (or "default" or "none") -"default" PC_VW - Hub mean v'w' Reynolds stress [m^2/s^2] (or "default" or "none") +0.05 RICH_NO - Gradient Richardson number [-] +"default" UStar - Friction or shear velocity [m/s] (or "default") +"default" ZI - Mixing layer depth [m] (or "default") +"default" PC_UW - Hub mean u'w' Reynolds stress [m^2/s^2] (or "default" or "none") +"default" PC_UV - Hub mean u'v' Reynolds stress [m^2/s^2] (or "default" or "none") +"default" PC_VW - Hub mean v'w' Reynolds stress [m^2/s^2] (or "default" or "none") --------Spatial Coherence Parameters---------------------------- -"IEC" SCMod1 - u-component coherence model ("GENERAL","IEC","API","NONE", or "default") -"IEC" SCMod2 - v-component coherence model ("GENERAL","IEC","NONE", or "default") -"IEC" SCMod3 - w-component coherence model ("GENERAL","IEC","NONE", or "default") -"12.0 0.000659" InCDec1 - u-component coherence parameters for general or IEC models [-, m^-1] (e.g. "10.0 0.3e-3" in quotes) (or "default") -"12.0 0.000659" InCDec2 - v-component coherence parameters for general or IEC models [-, m^-1] (e.g. "10.0 0.3e-3" in quotes) (or "default") -"12.0 0.000659" InCDec3 - w-component coherence parameters for general or IEC models [-, m^-1] (e.g. "10.0 0.3e-3" in quotes) (or "default") -"0.0" CohExp - Coherence exponent for general model [-] (or "default") +"IEC" SCMod1 - u-component coherence model ("GENERAL","IEC","API","NONE", or "default") +"IEC" SCMod2 - v-component coherence model ("GENERAL","IEC","NONE", or "default") +"IEC" SCMod3 - w-component coherence model ("GENERAL","IEC","NONE", or "default") +"12.0 0.000659" InCDec1 - u-component coherence parameters for general or IEC models [-, m^-1] (e.g. "10.0 0.3e-3" in quotes) (or "default") +"12.0 0.000659" InCDec2 - v-component coherence parameters for general or IEC models [-, m^-1] (e.g. "10.0 0.3e-3" in quotes) (or "default") +"12.0 0.000659" InCDec3 - w-component coherence parameters for general or IEC models [-, m^-1] (e.g. "10.0 0.3e-3" in quotes) (or "default") +"0.0" CohExp - Coherence exponent for general model [-] (or "default") --------Coherent Turbulence Scaling Parameters------------------- -".\EventData" CTEventPath - Name of the path where event data files are located +".\EventData" CTEventPath - Name of the path where event data files are located "random" CTEventFile - Type of event files ("LES", "DNS", or "RANDOM") -true Randomize - Randomize the disturbance scale and locations? (true/false) -1 DistScl - Disturbance scale [-] (ratio of event dataset height to rotor disk). (Ignored when Randomize = true.) -0.5 CTLy - Fractional location of tower centerline from right [-] (looking downwind) to left side of the dataset. (Ignored when Randomize = true.) -0.5 CTLz - Fractional location of hub height from the bottom of the dataset. [-] (Ignored when Randomize = true.) -30 CTStartTime - Minimum start time for coherent structures in RootName.cts [seconds] +true Randomize - Randomize the disturbance scale and locations? (true/false) +1 DistScl - Disturbance scale [-] (ratio of event dataset height to rotor disk). (Ignored when Randomize = true.) +0.5 CTLy - Fractional location of tower centerline from right [-] (looking downwind) to left side of the dataset. (Ignored when Randomize = true.) +0.5 CTLz - Fractional location of hub height from the bottom of the dataset. [-] (Ignored when Randomize = true.) +30 CTStartTime - Minimum start time for coherent structures in RootName.cts [seconds] ==================================================== ! NOTE: Do not add or remove any lines in this file! From 142039bafc52c01911954e1e4b78c2c3d12ac3e9 Mon Sep 17 00:00:00 2001 From: Regis Thedin Date: Thu, 25 Aug 2022 08:35:40 -0600 Subject: [PATCH 003/124] Add extents option to TurbSim box creation --- pyFAST/fastfarm/TurbSimCaseCreation.py | 73 +++++++++++++++++--------- 1 file changed, 49 insertions(+), 24 deletions(-) diff --git a/pyFAST/fastfarm/TurbSimCaseCreation.py b/pyFAST/fastfarm/TurbSimCaseCreation.py index 88f517e..22a08e7 100644 --- a/pyFAST/fastfarm/TurbSimCaseCreation.py +++ b/pyFAST/fastfarm/TurbSimCaseCreation.py @@ -14,7 +14,7 @@ class TSCaseCreation: def __init__(self, D, HubHt, Vhub, TI, PLexp, x, y, z=None, zbot=1.0, cmax=5.0, fmax=5.0, - Cmeander=1.9, boxType='highres', high_ext=1.2): + Cmeander=1.9, boxType='highres', high_ext=1.2, low_ext=None): """ Instantiate the object. @@ -42,15 +42,33 @@ def __init__(self, D, HubHt, Vhub, TI, PLexp, x, y, z=None, zbot=1.0, cmax=5.0, Defaults to `highres` for backward compatibility high_ext : float extent of the high-res box around individual turbines (in D) + low_ext : list of floats [xmin, xmax, ymin, ymax, zabovehub] + extents for the low-res box. All values should be positive If not specified, resorts to + computations by the manual """ + + # Perform some checks on the input + if low_ext is not None and len(low_ext) != 5: + raise ValueError('low_ext not defined properly. It should be [xmin, xmax, ymin, ymax, zabovehub]') + + if low_ext is None: + manual_mode = False + else: + manual_mode = True + + # Set parameters for convenience + self.Cmeander = Cmeander + self.boxType = boxType + self.high_ext = high_ext + self.low_ext = low_ext # Turbine parameters self.Turb(D, HubHt, cmax, fmax) # Turbine location self.turbLocs(x,y,z) # Discretization - self.discretization(Vhub, TI, PLexp, Cmeander=Cmeander, boxType=boxType) + self.discretization(Vhub, TI, PLexp) # Setup domain size - self.domainSize(zbot=zbot, Cmeander=Cmeander, boxType=boxType, high_ext=high_ext) + self.domainSize(zbot=zbot, manual_mode=manual_mode) # Determine origin # self.originLoc() @@ -93,7 +111,7 @@ def turbLocs(self,x,y,z=None): else: self.z = np.asarray(z) - def discretization(self,Vhub,TI,Shear, Cmeander=1.9, boxType='highres'): + def discretization(self, Vhub, TI, Shear): ''' Specify discretization for both the high-res and low-res boxes. Follows guidelines present at https://openfast.readthedocs.io/en/main/source/user/fast.farm/ModelGuidance.html#low-resolution-domain @@ -105,14 +123,22 @@ def discretization(self,Vhub,TI,Shear, Cmeander=1.9, boxType='highres'): self.PLexp = Shear # Derived properties - if boxType == 'lowres': - self.dt = Cmeander*self.D/(10*Vhub) - ds_low = Cmeander*self.D*Vhub/150 + if self.boxType == 'lowres': + # this is what is on the manual. Commenting it out to get to KS's values + self.dt = self.Cmeander*self.D/(10*Vhub) + ds_low = self.Cmeander*self.D*Vhub/150 ds_high = self.cmax self.dy = np.floor(ds_low/ds_high)*ds_high self.dz = np.floor(ds_low/ds_high)*ds_high - elif boxType == 'highres': + # I'm getting dt of 7.1969 for wspd 6.6. KS had 0.5. let's hard code for a bit + self.dt = 0.5 + + #self.dt = 1.0/(2.0*self.fmax) + #self.dy = self.cmax + #self.dz = self.cmax + + elif self.boxType == 'highres': self.dt = 1.0/(2.0*self.fmax) self.dy = self.cmax self.dz = self.cmax @@ -120,14 +146,18 @@ def discretization(self,Vhub,TI,Shear, Cmeander=1.9, boxType='highres'): else: raise ValueError("boxType can only be 'lowres' or 'highres'. Stopping.") - def domainSize(self, zbot, Cmeander=1.9, boxType='highres', high_ext=1.2): + def domainSize(self, zbot, manual_mode=False): - if boxType == 'lowres': - ymin = min(self.y)-2.23313*Cmeander*self.D/2 - ymax = max(self.y)+2.23313*Cmeander*self.D/2 + if self.boxType == 'lowres': + if manual_mode: + self.ymin = min(self.y) - self.low_ext[2]*self.D + self.ymax = max(self.y) + self.low_ext[3]*self.D + else: + self.ymin = min(self.y)-2.23313*self.Cmeander*self.D/2 + self.ymax = max(self.y)+2.23313*self.Cmeander*self.D/2 - Ydist_Low = ymax - ymin - Zdist_Low = self.RefHt + self.D/2 + 2.23313*Cmeander*self.D/2 + Ydist_Low = self.ymax - self.ymin + Zdist_Low = self.RefHt + self.D/2 + 2.23313*self.Cmeander*self.D/2 self.ny = np.ceil(Ydist_Low/self.dy)+1 self.nz = np.ceil(Zdist_Low/self.dz)+1 @@ -140,9 +170,9 @@ def domainSize(self, zbot, Cmeander=1.9, boxType='highres', high_ext=1.2): # Set the hub height using half of the total grid height self.HubHt_for_TS = zbot - 0.5*Dgrid + self.Height - elif boxType=='highres': - Ydist_high = high_ext*self.D - Zdist_high = self.RefHt + high_ext*self.D/2.0 - zbot + elif self.boxType=='highres': + Ydist_high = self.high_ext*self.D + Zdist_high = self.RefHt + self.high_ext*self.D/2.0 - zbot self.ny = np.ceil(Ydist_high/self.dy)+1 self.nz = np.ceil(Zdist_high/self.dz)+1 @@ -172,11 +202,6 @@ def plotSetup(self, fig=None, ax=None): xmin = min(self.x)-self.D xmax = max(self.x)+self.D - ymid = np.mean(self.y) - ymin = -self.Width/2 + ymid - ymax = self.Width/2 + ymid - - # high-res boxes for wt in range(len(self.x)): ax.plot(self.x[wt],self.y[wt],'x',ms=8,mew=2,label="WT{0}".format(wt+1)) @@ -184,8 +209,8 @@ def plotSetup(self, fig=None, ax=None): # low-res box # ax.plot([xmin,xmax,xmax,xmin,xmin], # [ymin,ymin,ymax,ymax,ymin],'--k',lw=2,label='Low') - ax.axhline(ymin, ls='--', c='k', lw=2, label='Low') - ax.axhline(ymax, ls='--', c='k', lw=2) + ax.axhline(self.ymin, ls='--', c='k', lw=2, label='Low') + ax.axhline(self.ymax, ls='--', c='k', lw=2) ax.legend(bbox_to_anchor=(1.05,1.015),frameon=False) ax.set_xlabel("x-location [m]") From bc251c5b8e747eb32e2b7266f2048955d68409e1 Mon Sep 17 00:00:00 2001 From: Regis Thedin Date: Thu, 25 Aug 2022 08:38:27 -0600 Subject: [PATCH 004/124] Minor code clean-up --- pyFAST/fastfarm/TurbSimCaseCreation.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/pyFAST/fastfarm/TurbSimCaseCreation.py b/pyFAST/fastfarm/TurbSimCaseCreation.py index 22a08e7..1accece 100644 --- a/pyFAST/fastfarm/TurbSimCaseCreation.py +++ b/pyFAST/fastfarm/TurbSimCaseCreation.py @@ -124,16 +124,11 @@ def discretization(self, Vhub, TI, Shear): # Derived properties if self.boxType == 'lowres': - # this is what is on the manual. Commenting it out to get to KS's values self.dt = self.Cmeander*self.D/(10*Vhub) ds_low = self.Cmeander*self.D*Vhub/150 ds_high = self.cmax self.dy = np.floor(ds_low/ds_high)*ds_high self.dz = np.floor(ds_low/ds_high)*ds_high - - # I'm getting dt of 7.1969 for wspd 6.6. KS had 0.5. let's hard code for a bit - self.dt = 0.5 - #self.dt = 1.0/(2.0*self.fmax) #self.dy = self.cmax #self.dz = self.cmax From 0d0a9b167019828d81c725d59a327d15a425b3b3 Mon Sep 17 00:00:00 2001 From: Regis Thedin Date: Thu, 25 Aug 2022 10:35:01 -0600 Subject: [PATCH 005/124] Update default values for coherence function per IEC 61400-1 4th edition --- pyFAST/fastfarm/TurbSimCaseCreation.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyFAST/fastfarm/TurbSimCaseCreation.py b/pyFAST/fastfarm/TurbSimCaseCreation.py index 1accece..bbd3f07 100644 --- a/pyFAST/fastfarm/TurbSimCaseCreation.py +++ b/pyFAST/fastfarm/TurbSimCaseCreation.py @@ -291,9 +291,9 @@ def WriteTSFile(fileIn, fileOut, params, NewFile=True, tpath=None, tmax=50): f.write('"IEC"\tSCMod1\t\t- u-component coherence model ("GENERAL","IEC","API","NONE", or "default")\n') f.write('"IEC"\tSCMod2\t\t- v-component coherence model ("GENERAL","IEC","NONE", or "default")\n') f.write('"IEC"\tSCMod3\t\t- w-component coherence model ("GENERAL","IEC","NONE", or "default")\n') - f.write('"12.0 0.000659"\tInCDec1\t- u-component coherence parameters for general or IEC models [-, m^-1] (e.g. "10.0 0.3e-3" in quotes) (or "default")\n') - f.write('"12.0 0.000659"\tInCDec2\t- v-component coherence parameters for general or IEC models [-, m^-1] (e.g. "10.0 0.3e-3" in quotes) (or "default")\n') - f.write('"12.0 0.000659"\tInCDec3\t- w-component coherence parameters for general or IEC models [-, m^-1] (e.g. "10.0 0.3e-3" in quotes) (or "default")\n') + f.write(f'"12.0 {0.12/(8.1*42):.8f}"\tInCDec1\t- u-component coherence parameters for general or IEC models [-, m^-1] (e.g. "10.0 0.3e-3" in quotes) (or "default")\n') + f.write(f'"12.0 {0.12/(8.1*42):.8f}"\tInCDec2\t- v-component coherence parameters for general or IEC models [-, m^-1] (e.g. "10.0 0.3e-3" in quotes) (or "default")\n') + f.write(f'"12.0 {0.12/(8.1*42):.8f}"\tInCDec3\t- w-component coherence parameters for general or IEC models [-, m^-1] (e.g. "10.0 0.3e-3" in quotes) (or "default")\n') f.write('"0.0"\tCohExp\t\t- Coherence exponent for general model [-] (or "default")\n') f.write('\n') f.write('--------Coherent Turbulence Scaling Parameters-------------------\n') From e0547cf35f36a94851f199982c0ace95aa90cc18 Mon Sep 17 00:00:00 2001 From: Regis Thedin Date: Fri, 2 Dec 2022 10:15:50 -0700 Subject: [PATCH 006/124] Add and fix several details regarding case creation for TurbSim --- pyFAST/fastfarm/TurbSimCaseCreation.py | 84 +++++++++++++++++++++++--- 1 file changed, 74 insertions(+), 10 deletions(-) diff --git a/pyFAST/fastfarm/TurbSimCaseCreation.py b/pyFAST/fastfarm/TurbSimCaseCreation.py index bbd3f07..c525aa9 100644 --- a/pyFAST/fastfarm/TurbSimCaseCreation.py +++ b/pyFAST/fastfarm/TurbSimCaseCreation.py @@ -14,7 +14,7 @@ class TSCaseCreation: def __init__(self, D, HubHt, Vhub, TI, PLexp, x, y, z=None, zbot=1.0, cmax=5.0, fmax=5.0, - Cmeander=1.9, boxType='highres', high_ext=1.2, low_ext=None): + Cmeander=1.9, boxType='highres', high_ext=1.2, low_ext=None, ds_low=None): """ Instantiate the object. @@ -41,7 +41,7 @@ def __init__(self, D, HubHt, Vhub, TI, PLexp, x, y, z=None, zbot=1.0, cmax=5.0, box type, either 'lowres' or 'highres'. Sets the appropriate dt, dy, and dz for discretization Defaults to `highres` for backward compatibility high_ext : float - extent of the high-res box around individual turbines (in D) + extent of the high-res box around individual turbines (in D). This is the total length low_ext : list of floats [xmin, xmax, ymin, ymax, zabovehub] extents for the low-res box. All values should be positive If not specified, resorts to computations by the manual @@ -56,17 +56,23 @@ def __init__(self, D, HubHt, Vhub, TI, PLexp, x, y, z=None, zbot=1.0, cmax=5.0, else: manual_mode = True + if ds_low is None: + manual_ds_low = False + else: + manual_ds_low = True + # Set parameters for convenience self.Cmeander = Cmeander self.boxType = boxType self.high_ext = high_ext self.low_ext = low_ext + self.ds_low = ds_low # Turbine parameters self.Turb(D, HubHt, cmax, fmax) # Turbine location self.turbLocs(x,y,z) # Discretization - self.discretization(Vhub, TI, PLexp) + self.discretization(Vhub, TI, PLexp, manual_ds_low) # Setup domain size self.domainSize(zbot=zbot, manual_mode=manual_mode) # Determine origin @@ -111,7 +117,7 @@ def turbLocs(self,x,y,z=None): else: self.z = np.asarray(z) - def discretization(self, Vhub, TI, Shear): + def discretization(self, Vhub, TI, Shear, manual_ds_low=False): ''' Specify discretization for both the high-res and low-res boxes. Follows guidelines present at https://openfast.readthedocs.io/en/main/source/user/fast.farm/ModelGuidance.html#low-resolution-domain @@ -126,6 +132,8 @@ def discretization(self, Vhub, TI, Shear): if self.boxType == 'lowres': self.dt = self.Cmeander*self.D/(10*Vhub) ds_low = self.Cmeander*self.D*Vhub/150 + if manual_ds_low: + ds_low = self.ds_low ds_high = self.cmax self.dy = np.floor(ds_low/ds_high)*ds_high self.dz = np.floor(ds_low/ds_high)*ds_high @@ -156,6 +164,13 @@ def domainSize(self, zbot, manual_mode=False): self.ny = np.ceil(Ydist_Low/self.dy)+1 self.nz = np.ceil(Zdist_Low/self.dz)+1 + + # We need to make sure the number of points is odd. + if self.ny%2 == 0: + self.ny += 1 + if self.nz%2 == 0: + self.nz += 1 + self.Width = self.dy*(self.ny-1) self.Height = self.dz*(self.nz-1) @@ -167,11 +182,17 @@ def domainSize(self, zbot, manual_mode=False): elif self.boxType=='highres': Ydist_high = self.high_ext*self.D - Zdist_high = self.RefHt + self.high_ext*self.D/2.0 - zbot + Zdist_high = self.RefHt + self.high_ext*self.D/2 - zbot self.ny = np.ceil(Ydist_high/self.dy)+1 self.nz = np.ceil(Zdist_high/self.dz)+1 + # We need to make sure the number of points is odd. + if self.ny%2 == 0: + self.ny += 1 + if self.nz%2 == 0: + self.nz += 1 + self.Width = self.dy*(self.ny-1) self.Height = self.dz*(self.nz-1) @@ -188,6 +209,7 @@ def originLoc(self): raise NotImplementedError + def plotSetup(self, fig=None, ax=None): """ Plot a figure showing the turbine locations and the extent of the turbulence box""" if fig is None: @@ -213,23 +235,34 @@ def plotSetup(self, fig=None, ax=None): fig.tight_layout return fig, ax - def writeTSFile(self, fileIn, fileOut, NewFile=True, tpath=None, tmax=50): + def writeTSFile(self, fileIn, fileOut, NewFile=True, tpath=None, tmax=50, turb=None): """ Write a TurbSim primary input file, See WriteTSFile below. """ - WriteTSFile(fileIn, fileOut, self, NewFile=NewFile, tpath=tpath, tmax=tmax) + WriteTSFile(fileIn, fileOut, self, NewFile=NewFile, tpath=tpath, tmax=tmax, turb=turb) -def WriteTSFile(fileIn, fileOut, params, NewFile=True, tpath=None, tmax=50): +def WriteTSFile(fileIn, fileOut, params, NewFile=True, tpath=None, tmax=50, turb=None): """ Write a TurbSim primary input file, tpath: string, path to base turbine location (.fst) only used if NewFile is False + boxType: string, + Box type, either 'lowres' or 'highres'. Writes the proper `TurbModel` + if boxType=='highres', `turb` needs to be specified + turb: int, + turbine number to be printed on the time series file. Only needed + if boxType='highres' """ + if params.boxType=='highres' and not isinstance(turb, int): + raise ValueError("turb needs to be an integer when boxType is 'highres'") + if params.boxType=='lowres' and turb is not None: + print("WARNING: `turb` is not used when boxType is 'lowres'. Remove `turb` to dismiss this warning.") + if NewFile == True: print('Writing a new {0} file from scratch'.format(fileOut)) # --- Writing FFarm input file from scratch @@ -264,8 +297,14 @@ def WriteTSFile(fileIn, fileOut, params, NewFile=True, tpath=None, tmax=50): f.write('0\tHFlowAng\t\t- Horizontal mean flow (skew) angle [degrees]\n') f.write('\n') f.write('--------Meteorological Boundary Conditions-------------------\n') - f.write('"IECKAI"\tTurbModel\t\t- Turbulence model ("IECKAI","IECVKM","GP_LLJ","NWTCUP","SMOOTH","WF_UPW","WF_07D","WF_14D","TIDAL","API","IECKAI","TIMESR", or "NONE")\n') - f.write('"unused"\tUserFile\t\t- Name of the file that contains inputs for user-defined spectra or time series inputs (used only for "IECKAI" and "TIMESR" models)\n') + if params.boxType=='lowres': + f.write('"IECKAI"\tTurbModel\t\t- Turbulence model ("IECKAI","IECVKM","GP_LLJ","NWTCUP","SMOOTH","WF_UPW","WF_07D","WF_14D","TIDAL","API","IECKAI","TIMESR", or "NONE")\n') + f.write('"unused"\tUserFile\t\t- Name of the file that contains inputs for user-defined spectra or time series inputs (used only for "IECKAI" and "TIMESR" models)\n') + elif params.boxType=='highres': + f.write( '"TIMESR"\tTurbModel\t\t- Turbulence model ("IECKAI","IECVKM","GP_LLJ","NWTCUP","SMOOTH","WF_UPW","WF_07D","WF_14D","TIDAL","API","IECKAI","TIMESR", or "NONE")\n') + f.write(f'"USRTimeSeries_T{turb}.txt"\tUserFile\t\t- Name of the file that contains inputs for user-defined spectra or time series inputs (used only for "IECKAI" and "TIMESR" models)\n') + else: + raise ValueError("boxType can only be 'lowres' or 'highres'. Stopping.") f.write('1\tIECstandard\t\t- Number of IEC 61400-x standard (x=1,2, or 3 with optional 61400-1 edition number (i.e. "1-Ed2") )\n') f.write('"{:.3f}\t"\tIECturbc\t\t- IEC turbulence characteristic ("A", "B", "C" or the turbulence intensity in percent) ("KHTEST" option with NWTCUP model, not used for other models)\n'.format(params.TI)) f.write('"NTM"\tIEC_WindType\t\t- IEC turbulence type ("NTM"=normal, "xETM"=extreme turbulence, "xEWM1"=extreme 1-year wind, "xEWM50"=extreme 50-year wind, where x=wind turbine class 1, 2, or 3)\n') @@ -326,3 +365,28 @@ def WriteTSFile(fileIn, fileOut, params, NewFile=True, tpath=None, tmax=50): newline =str('{:.3f}\t\t{:.3f}\t\t{:.3f}\t\t{}_WT{:d}.fst"\t{:.3f}\t\t{:.3f}\t\t{:.3f}\t\t{:.3f}\t\t{:.3f}\t\t{:.3f}\n'.format(params.x[wt],params.y[wt],params.z[wt],tpath,wt+1,params.X0_High[wt],params.Y0_High[wt],params.Z0_High,params.dX_High,params.dY_High,params.dZ_High)) wt+=1 new_file.write(newline) + + +def writeTimeSeriesFile(fileOut,yloc,zloc,u,v,w,time): + """ Write a TurbSim primary input file, + + """ + + print('Writing {0}'.format(fileOut)) + # --- Writing TurbSim user-defined time series file + with open(fileOut, 'w') as f: + f.write( '--------------TurbSim v2.00.* User Time Series Input File-----------------------\n') + f.write( ' Time series input from low-res turbsim run\n') + f.write( '--------------------------------------------------------------------------------\n') + f.write( ' 3 nComp - Number of velocity components in the file\n') + f.write( ' 1 nPoints - Number of time series points contained in this file (-)\n') + f.write( ' 1 RefPtID - Index of the reference point (1-nPoints)\n') + f.write( ' Pointyi Pointzi ! nPoints listed in order of increasing height\n') + f.write( ' (m) (m)\n') + f.write(f' {yloc:.5f} {zloc:.5f}\n') + f.write( '--------Time Series-------------------------------------------------------------\n') + f.write( 'Elapsed Time Point01u Point01v Point01w\n') + f.write( ' (s) (m/s) (m/s) (m/s)\n') + for i in range(len(time)): + f.write(f'\t{time[i]:.2f}\t\t\t {u[i]:.5f}\t\t\t {v[i]:.5f}\t\t\t {w[i]:.5f}\n') + From 4b478097c5a69293ca95473356f946f2dbd76750 Mon Sep 17 00:00:00 2001 From: Regis Thedin Date: Fri, 2 Dec 2022 10:17:22 -0700 Subject: [PATCH 007/124] Add option for leading-zero numbering of turbines --- pyFAST/fastfarm/fastfarm.py | 28 ++++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/pyFAST/fastfarm/fastfarm.py b/pyFAST/fastfarm/fastfarm.py index 478ecca..32bd427 100644 --- a/pyFAST/fastfarm/fastfarm.py +++ b/pyFAST/fastfarm/fastfarm.py @@ -10,7 +10,7 @@ # --------------------------------------------------------------------------------} # --- Small helper functions # --------------------------------------------------------------------------------{ -def insertTN(s,i,nWT=1000): +def insertTN(s,i,nWT=1000, noLeadingZero=False): """ insert turbine number in name """ if nWT<10: fmt='{:d}' @@ -18,8 +18,15 @@ def insertTN(s,i,nWT=1000): fmt='{:02d}' else: fmt='{:03d}' + + if noLeadingZero: + fmt='{:d}' + if s.find('T1')>=0: s=s.replace('T1','T'+fmt.format(i)) + elif s.find('T0')>=0: + print('this should not be printed') + s=s.replace('T0','T'+fmt.format(i)) else: sp=os.path.splitext(s) s=sp[0]+'_T'+fmt.format(i)+sp[1] @@ -249,7 +256,7 @@ def fastFarmBoxExtent(yBox, zBox, tBox, meanU, hubHeight, D, xWT, yWT, nX_Low = int(np.ceil(LX_Low/dX_Low)) nY_Low = int(np.ceil(LY_Low/dY_Low)) nZ_Low = int(np.ceil(LZ_Low/dZ_Low)) - # Make sure we don't exceed box in Y and Z + # Make sure we don't exceed box in Y and Z #rt: this essentially gives us 1 less grid point than what is on the inp/bst files if (nY_Low*dY_Low>LY_Box): nY_Low=nY_Low-1 if (nZ_Low*dZ_Low>LZ_Box): nZ_Low=nZ_Low-1 @@ -307,15 +314,19 @@ def fastFarmBoxExtent(yBox, zBox, tBox, meanU, hubHeight, D, xWT, yWT, dY = Y_rel - np.round(Y_rel) # Should be close to zero if any(abs(dX)>1e-3): print('Deltas:',dX) - raise Exception('Some X0_High are not on an integer multiple of the high-res grid') + print('Exception has been raise. I put this print statement instead. Check with EB.') + print('Exception: Some X0_High are not on an integer multiple of the high-res grid') + #raise Exception('Some X0_High are not on an integer multiple of the high-res grid') if any(abs(dY)>1e-3): print('Deltas:',dY) - raise Exception('Some Y0_High are not on an integer multiple of the high-res grid') + print('Exception has been raise. I put this print statement instead. Check with EB.') + print('Exception: Some Y0_High are not on an integer multiple of the high-res grid') + #raise Exception('Some Y0_High are not on an integer multiple of the high-res grid') return d -def writeFastFarm(outputFile, templateFile, xWT, yWT, zWT, FFTS=None, OutListT1=None): +def writeFastFarm(outputFile, templateFile, xWT, yWT, zWT, FFTS=None, OutListT1=None, noLeadingZero=False): """ Write FastFarm input file based on a template, a TurbSimFile and the Layout outputFile: .fstf file to be written @@ -349,7 +360,7 @@ def writeFastFarm(outputFile, templateFile, xWT, yWT, zWT, FFTS=None, OutListT1= WT[iWT,0]=x WT[iWT,1]=y WT[iWT,2]=z - WT[iWT,3]=insertTN(ref_path,iWT+1,nWT) + WT[iWT,3]=insertTN(ref_path,iWT+1,nWT,noLeadingZero=noLeadingZero) if FFTS is not None: WT[iWT,4]=FFTS['X0_High'][iWT] WT[iWT,5]=FFTS['Y0_High'][iWT] @@ -378,7 +389,7 @@ def setFastFarmOutputs(fastFarmFile, OutListT1): fst.write(fastFarmFile) -def plotFastFarmSetup(fastFarmFile, grid=True, fig=None, D=None, plane='XY', hubHeight=None): +def plotFastFarmSetup(fastFarmFile, grid=True, fig=None, D=None, plane='XY', hubHeight=None, showLegend=True): """ """ import matplotlib.pyplot as plt @@ -475,7 +486,8 @@ def boundingBox(x, y): ax.plot(x, y, '-', lw=2, c=col(wt)) #plt.legend(bbox_to_anchor=(1.05,1.015),frameon=False) - ax.legend() + if showLegend: + ax.legend() if plane=='XY': ax.set_xlabel("x [m]") ax.set_ylabel("y [m]") From 7383831a48ec1c75e42313760c8344636d14d755 Mon Sep 17 00:00:00 2001 From: Regis Thedin Date: Fri, 2 Dec 2022 10:21:05 -0700 Subject: [PATCH 008/124] Adjust precision of time step valus to be printed to file --- pyFAST/input_output/turbsim_file.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyFAST/input_output/turbsim_file.py b/pyFAST/input_output/turbsim_file.py index fc09362..ac05275 100644 --- a/pyFAST/input_output/turbsim_file.py +++ b/pyFAST/input_output/turbsim_file.py @@ -98,11 +98,11 @@ def read(self, filename=None, header_only=False): self['uTwr'] = uTwr self['info'] = info self['ID'] = ID - self['dt'] = dt + self['dt'] = np.round(dt,3) # dt is stored in single precision in the TurbSim output self['y'] = np.arange(ny)*dy self['y'] -= np.mean(self['y']) # y always centered on 0 self['z'] = np.arange(nz)*dz +zBottom - self['t'] = np.arange(nt)*dt + self['t'] = np.round(np.arange(nt)*dt, 3) self['zTwr'] =-np.arange(nTwr)*dz + zBottom self['zRef'] = zHub self['uRef'] = uHub From 00b6d7b8919de57c69c3c38a5dcb9b2beed9d77d Mon Sep 17 00:00:00 2001 From: Regis Thedin Date: Fri, 2 Dec 2022 10:22:09 -0700 Subject: [PATCH 009/124] Increase number of output channel lines --- pyFAST/input_output/fast_input_file.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyFAST/input_output/fast_input_file.py b/pyFAST/input_output/fast_input_file.py index ab4b204..7f282b5 100644 --- a/pyFAST/input_output/fast_input_file.py +++ b/pyFAST/input_output/fast_input_file.py @@ -1133,7 +1133,7 @@ def parseFASTInputLine(line_raw,i,allowSpaceSeparatedList=False): def parseFASTOutList(lines,iStart): OutList=[] i = iStart - MAX=200 + MAX=500 while i Date: Fri, 2 Dec 2022 10:46:05 -0700 Subject: [PATCH 010/124] Fix path of --- pyFAST/fastfarm/fastfarm.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyFAST/fastfarm/fastfarm.py b/pyFAST/fastfarm/fastfarm.py index 16137da..6290158 100644 --- a/pyFAST/fastfarm/fastfarm.py +++ b/pyFAST/fastfarm/fastfarm.py @@ -5,7 +5,7 @@ from pyFAST.input_output.fast_input_file import FASTInputFile from pyFAST.input_output.fast_output_file import FASTOutputFile from pyFAST.input_output.turbsim_file import TurbSimFile -import pyFAST.input_output.postpro as fastlib +import pyFAST.postpro as fastlib # --------------------------------------------------------------------------------} # --- Small helper functions From 218917ee362c15f4f9cbc6716784e9059635dd44 Mon Sep 17 00:00:00 2001 From: Regis Thedin Date: Mon, 9 Jan 2023 17:22:03 -0700 Subject: [PATCH 011/124] Placeholder for FAST.Farm case setup class --- pyFAST/fastfarm/FASTFarmCaseCreation.py | 181 ++++++++++++++++++++++++ 1 file changed, 181 insertions(+) create mode 100644 pyFAST/fastfarm/FASTFarmCaseCreation.py diff --git a/pyFAST/fastfarm/FASTFarmCaseCreation.py b/pyFAST/fastfarm/FASTFarmCaseCreation.py new file mode 100644 index 0000000..056dbc2 --- /dev/null +++ b/pyFAST/fastfarm/FASTFarmCaseCreation.py @@ -0,0 +1,181 @@ +import pandas as pd +import numpy as np +import os, sys, shutil +import subprocess +import numpy as np +import xarray as xr +import matplotlib.pyplot as plt + +from pyFAST.input_output import FASTInputFile, FASTOutputFile, TurbSimFile, VTKFile +from pyFAST.fastfarm import writeFastFarm, fastFarmTurbSimExtent, plotFastFarmSetup +from pyFAST.fastfarm.TurbSimCaseCreation import TSCaseCreation, writeTimeSeriesFile + +def cosd(t): return np.cos(np.deg2rad(t)) +def sind(t): return np.sin(np.deg2rad(t)) + + +class FFCaseCreation: + + + def __init__(self, wts, cmax, fmax, Cmeander, tmax, zbot, vhub, shear, TIvalue, path, dt_high_les, ds_high_les, extent_high, dt_low_les, ds_low_les, extent_low, ffbin, ADmodel=None, EDmodel=None, yaw=None, nSeeds=6, LESpath=None, sweepWakeSteering=False, sweepYawMisalignment=False, seedValues=None): + ''' + + ffbin: str + Full path of the FAST.Farm binary to be executed + ''' + + self.wts = wts + self.cmax = cmax + self.fmax = fmax + self.Cmeander = Cmeander + self.tmax = tmax + self.zbot = zbot + self.vhub = vhub + self.shear = shear + self.TIvalue = TIvalue + self.path = path + self.dt_high_les = dt_high_les + self.ds_high_les = ds_high_les + self.dt_low_les = dt_low_les + self.ds_low_les = ds_low_les + self.extent_low = extent_low + self.extent_high = extent_high + self.ffbin = ffbin + self.ADmodel = ADmodel + self.EDmodel = EDmodel + self.yaw = yaw + self.nSeeds = nSeeds + self.LESpath = LESpath + self.sweepWS = sweepWakeSteering + self.sweepYM = sweepYawMisalignment + self.seedValues = seedValues + + + self._setAndCheckInputs() + + + self.createAuxArrays() + + + def createAuxArrays(self): + + + + def _setAndCheckInputs(self): + + if not isinstance(self.wts,dict): + raise ValueError (f'`wts` needs to be a dictionary with the following entries for each turbine: x, y, z, D, zhub.') + if not isinstance(self.wts[0]['x'],(float,int)): + raise ValueError (f'The `x` value for the turbines should be an integer or float') + # todo: check if all entries of the dict are the same regarding D and zhub + self.D = self.wts[0]['D'] + + + # Auxiliary variables and some checks + if self.seedValues is None: + self.seedValues = [2318573, 122299, 123456, 389432, -432443, 9849898] + + + self.nTurbines = len(self.wts) + + + self._setRotorParameters() + + # Ensure quantities are list + self.vhub = [vhub] if isinstance(vhub,(float,int)) else vhub + self.shear = [shear] if isinstance(shear,(float,int)) else shear + self.TIvalue = [TIvalue] if isinstance(TIvalue,(float,int)) else TIvalue + + if self.ADmodel is None: + self.ADmodel = np.tile(['ADyn'],(1,self.nTurbines)) + + if self.EDmodel is None: + self.EDmodel = np.tile(['FED'],(1,self.nTurbines)) + + if self.yaw is None: + self.yaw = np.zeros((1,self.nTurbines))) + + if not os.path.isfile(self.ffbin): + raise ValueError (f'The FAST.Farm binary given does not appear to exist') + + + + if self.LESpath is None: + print('Setting up FAST.Farm based on TurbSim inflow') + self.inflowStr = 'TurbSim' + else: + print('Seeing up FAST.Farm based on LES inflow') + if not os.path.isdir(self.LESpath): + raise ValueError (f'The path {self.LESpath} does not exist') + self.inflowStr = 'LES' + + + + # Create case path is doesn't exist + if not os.path.exists(self.path): + os.makedirs(self.path) + + # Perform some checks + if len(self.seedValues) != self.nSeeds: + raise ValueError(f'Number of seeds is {self.nSeeds} but {len(self.seedValues)} seed values were given. Adjust the seedValues array accordingly') + + if not (np.array(self.extent_low)>=0).all(): + raise ValueError(f'The array for low-res box extents should be given with positive values') + + if self.dt_low_les < self.dt_high_les: + raise ValueError(f'The temporal resolution dT_High should not be greater than dT_Low on the LES side') + + if self.ds_low_les < ds_high_les: + raise ValueError(f'The grid resolution dS_High should not be greater than dS_Low on the LES side') + + assert isinstance(self.extent_high, (float,int)) + if self.extent_high<=0: + raise ValueError(f'The extent of high boxes should be positive') + + #assert dt_low_desired%(1/(2*fmax)) < 1e-10 + + + if np.shape(self.ADmodel) != np.shape(self.EDmodel): + raise ValueError('Every case should have the aerodynamic and elastic model selected. The number of cases (lines) in `ADmodel` and `EDmodel` should be the same') + + if self.nTurbines != np.shape(self.ADmodel)[1]: + raise ValueError(f'The number of turbines in wts ({len(wts)}) should match the number of turbines in the ADmodel and EDmodel arrays ({np.shape(ADmodel)[1]})') + + if len(inflow_deg) != len(yaw_init): + raise ValueError(f'One row for each inflow angle should be given in yaw_init. Currently {len(inflow_deg)} inflow angle(s) and {len(yaw_init)} yaw entrie(s)') + + + + + def _setRotorParameters(self): + + + if self.D == 220: # 12 MW turbine + self.bins = xr.Dataset({'WaveHs': (['wspd'], [ 1.429, 1.429]), # 1.429 comes from Matt's hydrodyn input file + 'WaveTp': (['wspd'], [ 7.073, 7.073]), # 7.073 comes from Matt's hydrodyn input file + 'RotSpeed': (['wspd'], [ 4.0, 4.0]), # 4 rpm comes from Matt's ED input file + 'BlPitch': (['wspd'], [ 0.0, 0.0]), # 0 deg comes from Matt's ED input file + #'WvHiCOffD': (['wspd'], [0, 0]), # 2nd order wave info. Unused for now + #'WvLowCOffS': (['wspd'], [0, 0]), # 2nd order wave info. Unused for now + }, coords={'wspd': [10, 15]} ) # 15 m/s is 'else', since method='nearest' is used on the variable `bins` + + elif self.D == 250: # IEA 15 MW + self.bins = xr.Dataset({'WaveHs': (['wspd'], [1.172, 1.323, 1.523, 1.764, 2.255]), # higher values on default input from the repository (4.52) + 'WaveTp': (['wspd'], [7.287, 6.963, 7.115, 6.959, 7.067]), # higher values on default input from the repository (9.45) + 'RotSpeed': (['wspd'], [4.995, 6.087, 7.557, 7.557, 7.557]), + 'BlPitch': (['wspd'], [0.315, 0, 0.645, 7.6, 13.8 ]), + #'WvHiCOffD': (['wspd'], [0, 0, 0, 0, 0 ]), # 2nd order wave info. Unused for now. 3.04292 from repo; 0.862 from KS + #'WvLowCOffS': (['wspd'], [0, 0, 0, 0, 0 ]), # 2nd order wave info. Unused for now 0.314159 from repo; 0.862 from KS + }, coords={'wspd': [6.6, 8.6, 10.6, 12.6, 15]} ) # 15 m/s is 'else', since method='nearest' is used on the variable `bins` + + else: + raise ValueError(f'Unknown turbine with diameter {self.D}. Add values to the `_setRotorParameters` function.') + + + + + + + + + From dae78a46805241767a45241beaa7b2ef43af532d Mon Sep 17 00:00:00 2001 From: Regis Thedin Date: Wed, 11 Jan 2023 12:42:02 -0700 Subject: [PATCH 012/124] CaseGen: Add verbose option to TS case creation --- pyFAST/fastfarm/TurbSimCaseCreation.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pyFAST/fastfarm/TurbSimCaseCreation.py b/pyFAST/fastfarm/TurbSimCaseCreation.py index c525aa9..cb672d6 100644 --- a/pyFAST/fastfarm/TurbSimCaseCreation.py +++ b/pyFAST/fastfarm/TurbSimCaseCreation.py @@ -235,15 +235,15 @@ def plotSetup(self, fig=None, ax=None): fig.tight_layout return fig, ax - def writeTSFile(self, fileIn, fileOut, NewFile=True, tpath=None, tmax=50, turb=None): + def writeTSFile(self, fileIn, fileOut, NewFile=True, tpath=None, tmax=50, turb=None, verbose=0): """ Write a TurbSim primary input file, See WriteTSFile below. """ - WriteTSFile(fileIn, fileOut, self, NewFile=NewFile, tpath=tpath, tmax=tmax, turb=turb) + WriteTSFile(fileIn, fileOut, self, NewFile=NewFile, tpath=tpath, tmax=tmax, turb=turb, verbose=verbose) -def WriteTSFile(fileIn, fileOut, params, NewFile=True, tpath=None, tmax=50, turb=None): +def WriteTSFile(fileIn, fileOut, params, NewFile=True, tpath=None, tmax=50, turb=None, verbose=0): """ Write a TurbSim primary input file, tpath: string, @@ -264,7 +264,7 @@ def WriteTSFile(fileIn, fileOut, params, NewFile=True, tpath=None, tmax=50, turb print("WARNING: `turb` is not used when boxType is 'lowres'. Remove `turb` to dismiss this warning.") if NewFile == True: - print('Writing a new {0} file from scratch'.format(fileOut)) + if verbose>1: print('Writing a new {0} file from scratch'.format(fileOut)) # --- Writing FFarm input file from scratch with open(fileOut, 'w') as f: f.write('--------TurbSim v2.00.* Input File------------------------\n') From a2254fa7417514b988fd34f834ac1f05cba99ec1 Mon Sep 17 00:00:00 2001 From: Regis Thedin Date: Wed, 11 Jan 2023 12:43:23 -0700 Subject: [PATCH 013/124] FASTFarm: add new class for complete case creation --- pyFAST/fastfarm/FASTFarmCaseCreation.py | 1584 ++++++++++++++++++++++- 1 file changed, 1518 insertions(+), 66 deletions(-) diff --git a/pyFAST/fastfarm/FASTFarmCaseCreation.py b/pyFAST/fastfarm/FASTFarmCaseCreation.py index 056dbc2..247f1f3 100644 --- a/pyFAST/fastfarm/FASTFarmCaseCreation.py +++ b/pyFAST/fastfarm/FASTFarmCaseCreation.py @@ -17,13 +17,16 @@ def sind(t): return np.sin(np.deg2rad(t)) class FFCaseCreation: - def __init__(self, wts, cmax, fmax, Cmeander, tmax, zbot, vhub, shear, TIvalue, path, dt_high_les, ds_high_les, extent_high, dt_low_les, ds_low_les, extent_low, ffbin, ADmodel=None, EDmodel=None, yaw=None, nSeeds=6, LESpath=None, sweepWakeSteering=False, sweepYawMisalignment=False, seedValues=None): + def __init__(self, path, wts, cmax, fmax, Cmeander, tmax, zbot, vhub, shear, TIvalue, inflow_deg, dt_high_les, ds_high_les, extent_high, dt_low_les, ds_low_les, extent_low, ffbin, yaw_init=None, ADmodel=None, EDmodel=None, nSeeds=6, LESpath=None, sweepWakeSteering=False, sweepYawMisalignment=False, seedValues=None, refTurb_rot=0, verbose=0): ''' ffbin: str Full path of the FAST.Farm binary to be executed + refTurb_rot: int + Index of reference turbine which the rotation of the farm will occur. Default is 0, the first one. ''' + self.path = path self.wts = wts self.cmax = cmax self.fmax = fmax @@ -33,7 +36,7 @@ def __init__(self, wts, cmax, fmax, Cmeander, tmax, zbot, vhub, shear, TIvalue, self.vhub = vhub self.shear = shear self.TIvalue = TIvalue - self.path = path + self.inflow_deg = inflow_deg self.dt_high_les = dt_high_les self.ds_high_les = ds_high_les self.dt_low_les = dt_low_les @@ -41,115 +44,691 @@ def __init__(self, wts, cmax, fmax, Cmeander, tmax, zbot, vhub, shear, TIvalue, self.extent_low = extent_low self.extent_high = extent_high self.ffbin = ffbin + self.yaw_init = yaw_init self.ADmodel = ADmodel self.EDmodel = EDmodel - self.yaw = yaw self.nSeeds = nSeeds self.LESpath = LESpath self.sweepWS = sweepWakeSteering self.sweepYM = sweepYawMisalignment self.seedValues = seedValues + self.refTurb_rot = refTurb_rot + self.verbose = verbose + + + if self.verbose>0: print(f'Checking inputs...', end='\r') + self._checkInputs() + if self.verbose>0: print(f'Checking inputs... Done.') + + + if self.verbose>0: print(f'Setting rotor parameters...', end='\r') + self._setRotorParameters() + if self.verbose>0: print(f'Setting rotor parameters... Done.') + + + if self.verbose>0: print(f'Creating auxiliary arrays for all conditions and cases...', end='\r') + self.createAuxArrays() + if self.verbose>0: print(f'Creating auxiliary arrays for all conditions and cases... Done.') + + + if self.verbose>0: print(f'Creating directory structure and copying files...', end='\r') + self._create_dir_structure() + if self.verbose>0: print(f'Creating directory structure and copying files... Done.') + + + #if self.verbose: print(f'Setting up FAST.Farm based on {self.inflowStr} inflow...', end='\r') + #if self.verbose: print(f'Setting up FAST.Farm based on {self.inflowStr} inflow... Done.' + + + def _checkInputs(self): + + # Create case path is doesn't exist + if not os.path.exists(self.path): + os.makedirs(self.path) + + # Check the wind turbine dict + if not isinstance(self.wts,dict): + raise ValueError (f'`wts` needs to be a dictionary with the following entries for each turbine: x, y, z, D, zhub.') + self.nTurbines = len(self.wts) + self.D = self.wts[0]['D'] + self.zhub = self.wts[0]['zhub'] + + # Check values of each turbine + for t in range(self.nTurbines): + t_x = self.wts[t]['x'] + t_y = self.wts[t]['y'] + t_z = self.wts[t]['z'] + t_D = self.wts[t]['D'] + t_zhub = self.wts[t]['zhub'] + if t_D != self.D: + raise ValueError(f'Different turbines are not currently supported. Turbine {t+1} has a different diamenter.') + if t_zhub != self.zhub: + raise ValueError(f'Different turbines are not currently supported. Turbine {t+1} has a different hub height.') + if not isinstance(t_x,(float,int)): + raise ValueError (f'The `x` value for the turbine {t+1} should be an integer or float. Received {t_x}.') + if not isinstance(t_y,(float,int)): + raise ValueError (f'The `y` value for the turbine {t+1} should be an integer or float. Received {t_y}.') + if not isinstance(t_z,(float,int)): + raise ValueError (f'The `z` value for the turbine {t+1} should be an integer or float. Received {t_z}.') + if not isinstance(t_D,(float,int)): + raise ValueError (f'The `D` value for the turbine {t+1} should be an integer or float. Received {t_D}.') + if not isinstance(t_zhub,(float,int)): + raise ValueError (f'The `zhub` value for the turbine {t+1} should be an integer or float. Received {t_zhub}.') + + # Check general variables + if self.cmax <= 0: raise ValueError('cmax cannot be negative') + if self.fmax <= 0: raise ValueError('fmax cannot be negative') + if self.Cmeander <= 0: raise ValueError('Cmeander cannot be negative') + if self.tmax <= 0: raise ValueError('A positive tmax should be requested') + if self.zbot <= 0: raise ValueError('zbot should be greater than 0 (recommended 1)') + + # Ensure quantities are list + self.vhub = [self.vhub] if isinstance(self.vhub,(float,int)) else self.vhub + self.shear = [self.shear] if isinstance(self.shear,(float,int)) else self.shear + self.TIvalue = [self.TIvalue] if isinstance(self.TIvalue,(float,int)) else self.TIvalue + self.inflow_deg = [self.inflow_deg] if isinstance(self.inflow_deg,(float,int)) else self.inflow_deg + + # Fill turbine parameters arrays if not given + if self.yaw_init is None: + yaw = np.ones((1,self.nTurbines))*0 + self.yaw_init = np.repeat(yaw, len(self.inflow_deg), axis=0) + + + # Check the ds and dt for the high- and low-res boxes + if not (np.array(self.extent_low)>=0).all(): + raise ValueError(f'The array for low-res box extents should be given with positive values') + if self.dt_low_les < self.dt_high_les: + raise ValueError(f'The temporal resolution dT_High should not be greater than dT_Low on the LES side') + if self.ds_low_les < self.ds_high_les: + raise ValueError(f'The grid resolution dS_High should not be greater than dS_Low on the LES side') + if not isinstance(self.extent_high, (float,int)): + raise ValueError(f'The extent_high should be a scalar') + if self.extent_high<=0: + raise ValueError(f'The extent of high boxes should be positive') + + + # Check the FAST.Farm binary + if not os.path.isfile(self.ffbin): + raise ValueError (f'The FAST.Farm binary given does not appear to exist') + + + # Check turbine conditions arrays for consistency + if len(self.inflow_deg) != len(self.yaw_init): + raise ValueError(f'One row for each inflow angle should be given in yaw_init. '\ + f'Currently {len(self.inflow_deg)} inflow angle(s) and {len(self.yaw_init)} yaw entrie(s)') + if self.ADmodel is None: + self.ADmodel = np.tile(['ADyn'],(1,self.nTurbines)) + if self.EDmodel is None: + self.EDmodel = np.tile(['FED'],(1,self.nTurbines)) + if np.shape(self.ADmodel) != np.shape(self.EDmodel): + raise ValueError('Every case should have the aerodynamic and elastic model selected. The number of cases '\ + '(lines) in `ADmodel` and `EDmodel` should be the same') + if self.nTurbines != np.shape(self.ADmodel)[1]: + raise ValueError(f'The number of turbines in wts ({len(self.wts)}) should match the number of turbines '\ + f'in the ADmodel and EDmodel arrays ({np.shape(self.ADmodel)[1]})') + + # Auxiliary variables and some checks + if self.seedValues is None: + self.seedValues = [2318573, 122299, 123456, 389432, -432443, 9849898] + if len(self.seedValues) != self.nSeeds: + raise ValueError(f'Number of seeds is {self.nSeeds} but {len(self.seedValues)} seed values were given. ' + f'Adjust the seedValues array accordingly') + + # Check LES parameters + if self.LESpath is None: + self.inflowStr = 'TurbSim' + else: + if not os.path.isdir(self.LESpath): + raise ValueError (f'The path {self.LESpath} does not exist') + self.inflowStr = 'LES' + + # Check the reference turbine for rotation + if self.refTurb_rot >= self.nTurbines: + raise ValueError(f'The index for the reference turbine for the farm to be rotated around is greater than the number of turbines') + + # Set aux variable + self.templateFilesCreatedBool = False + self.TSlowBoxFilesCreatedBool = False + + + def _create_dir_structure(self): + # Create directory structure CondXX_*/CaseYY_*/Seed_Z/TurbSim; and CondXX_*/Seed_Y + # Also saves the dir structure on array to write on SLURM script in the future. + condDirList = [] + caseDirList_ = [] + for cond in range(self.nConditions): + # Recover information about current condition for directory naming purposes + Vhub_ = self.allCond['vhub' ].isel(cond=cond).values + shear_ = self.allCond['shear' ].isel(cond=cond).values + tivalue_ = self.allCond['TIvalue' ].isel(cond=cond).values + + # Set current path name string + condStr = f'Cond{cond:02d}_v{Vhub_:04.1f}_PL{shear_}_TI{tivalue_}' + condDirList.append(condStr) + condPath = os.path.join(self.path, condStr) + + for case in range(self.nCases): + # Recover information about current case for directory naming purposes + inflow_deg_ = self.allCases['inflow_deg' ].sel(case=case).values + wakeSteering_ = self.allCases['wakeSteering' ].sel(case=case).values + misalignment_ = self.allCases['misalignment' ].sel(case=case).values + nADyn_ = self.allCases['nFullAeroDyn' ].sel(case=case).values + nFED_ = self.allCases['nFulllElastoDyn'].sel(case=case).values + yawCase_ = self.allCases['yawCase' ].sel(case=case).values + + # Set current path name string. The case is of the following form: Case00_wdirp10_WSfalse_YMfalse_12fED_12ADyn + caseStr = f"Case{case:02d}_wdir{f'{int(inflow_deg_):+03d}'.replace('+','p').replace('-','m')}"\ + f"_WS{str(wakeSteering_).lower()}_YM{str(misalignment_).lower()}"\ + f"_{nFED_}fED_{nADyn_}ADyn" + # If sweeping on yaw, then add yaw case to dir name + if len(np.unique(self.allCases.yawCase)) > 1: + caseStr = caseStr + f"_yawCase{yawCase_}" + + caseDirList_.append(caseStr) + casePath = os.path.join(condPath, caseStr) + if not os.path.exists(casePath): os.makedirs(casePath) + + for seed in range(self.nSeeds): + seedPath = os.path.join(casePath, f'Seed_{seed}') + if not os.path.exists(seedPath): os.makedirs(seedPath) + turbsimPath = os.path.join(seedPath, 'TurbSim') + if not os.path.exists(turbsimPath): os.makedirs(turbsimPath) + + # The following loop creates the turbsim files for low box. That should really only happen if inflowStr is `TurbSim`, as I have commented out below. + # However, as of now I need to copy some files to obtain the limits of the turbsim box to be able to shift the coordinate system. Later on i delete these dirs + # if inflowStr == 'TurbSim': + # for seed in range(nSeeds): + # seedPath = os.path.join(condPath, f'Seed_{seed}') + # if not os.path.exists(seedPath): os.makedirs(seedPath) + for seed in range(self.nSeeds): + seedPath = os.path.join(condPath, f'Seed_{seed}') + if not os.path.exists(seedPath): os.makedirs(seedPath) + + # Get rid of duplicate entries due to the nature of the loop (equiv to only getting the first nCases entries) + self.condDirList = condDirList + self.caseDirList = sorted(list(set(caseDirList_))) + assert self.caseDirList==caseDirList_[:self.nCases] + + + + def copyTurbineFilesForEachCase(self, writeFiles=True): + + if not self.templateFilesCreatedBool: + raise SyntaxError('Template files not set. Call `setTemplateFilename` before calling this function.') + + # Loops on all conditions/cases creating DISCON and *Dyn files + for cond in range(self.nConditions): + for case in range(self.nCases): + currPath = os.path.join(self.path, self.condDirList[cond], self.caseDirList[case]) + + # Recover info about the current CondXX_*/CaseYY_* + Vhub_ = self.allCond.sel(cond=cond)['vhub'].values + # Update parameters to be changed in the *Dyn files + self.HydroDynFile['WaveHs'] = self.bins.sel(wspd=Vhub_, method='nearest').WaveHs.values + self.HydroDynFile['WaveTp'] = self.bins.sel(wspd=Vhub_, method='nearest').WaveTp.values + self.HydroDynFile['WvHiCOffD'] = 2.0*np.pi/self.HydroDynFile['WaveTp'] # !!!!!!!!!!!!!! check with JJ + self.HydroDynFile['WvLowCOffS'] = 2.0*np.pi/self.HydroDynFile['WaveTp'] + # !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! do i need to change the first 3 values of HD? can they be 'default'? JJ + + self.ElastoDynFile['RotSpeed'] = self.bins.sel(wspd=Vhub_, method='nearest').RotSpeed.values + self.ElastoDynFile['BlPitch(1)'] = self.bins.sel(wspd=Vhub_, method='nearest').BlPitch.values + self.ElastoDynFile['BlPitch(2)'] = self.bins.sel(wspd=Vhub_, method='nearest').BlPitch.values + self.ElastoDynFile['BlPitch(3)'] = self.bins.sel(wspd=Vhub_, method='nearest').BlPitch.values + + self.SElastoDynFile['RotSpeed'] = self.bins.sel(wspd=Vhub_, method='nearest').RotSpeed.values + self.SElastoDynFile['BlPitch'] = self.bins.sel(wspd=Vhub_, method='nearest').BlPitch.values + + # Write updated DISCON and *Dyn files. + if writeFiles: + self.HydroDynFile.write(os.path.join(currPath, self.HDfilename)) + status = os.system(f'cp {self.templatePath}/{self.controllerInputfilename} {currPath}/{self.controllerInputfilename}') + + # Depending on the controller, it might need to be in the same level as the fstf input file. The ideal solution would be give the full + # path to the controller input, but we have no control over the compilation process and it is likely that a very long string with the full + # path will get cut. So we need to give the relative path. We give the path as the current one, so here we create a link to ensure it will + # work regardless of how the controller was compiled. There is no hard in having this extra link even if it's not needed. + notepath = os.getcwd(); os.chdir(self.path) + for seed in range(self.nSeeds): + try: + src = os.path.join('../', self.controllerInputfilename) + dst = os.path.join(self.condDirList[cond], self.caseDirList[case], f'Seed_{seed}', self.controllerInputfilename) + if writeFiles: + os.symlink(src, dst) + except FileExistsError: + pass + os.chdir(notepath) + + # Write InflowWind files + self.InflowWindFile['WindType'] = 3 + self.InflowWindFile['PropagationDir'] = 0 + self.InflowWindFile['Filename_BTS'] = '"./TurbSim"' + if writeFiles: + self.InflowWindFile.write( os.path.join(currPath,f'InflowWind.dat')) + + for t in range(self.nTurbines): + # Recover info about the current turbine in CondXX_*/CaseYY_ + yaw_deg_ = self.allCases.sel(case=case, turbine=t)['yaw'].values + yaw_mis_deg_ = self.allCases.sel(case=case, turbine=t)['yawmis'].values + ADmodel_ = self.allCases.sel(case=case, turbine=t)['ADmodel'].values + EDmodel_ = self.allCases.sel(case=case, turbine=t)['EDmodel'].values + + # Quickly check that yaw misaligned value is zero if case does not contain yaw misalignment + if self.allCases.sel(case=case, turbine=t)['misalignment'].values: + assert yaw_mis_deg_ != 0 + else: + assert yaw_mis_deg_ == 0 + + if EDmodel_ == 'FED': + # Update each turbine's ElastoDyn + self.ElastoDynFile['NacYaw'] = yaw_deg_ + yaw_mis_deg_ + # check if this change of EDfile to a variable works as it should with quotes and stuff + self.ElastoDynFile['BldFile1'] = self.ElastoDynFile['BldFile2'] = self.ElastoDynFile['BldFile3'] = f'"{self.bladefilename}"' + self.ElastoDynFile['TwrFile'] = f'"{self.towerfilename}"' + self.ElastoDynFile['Azimuth'] = round(np.random.uniform(low=0, high=360)) # start at a random value + if writeFiles: + self.ElastoDynFile.write(os.path.join(currPath,f'{self.EDfilename}{t+1}_mod.dat')) + + elif EDmodel_ == 'SED': + # Update each turbine's Simplified ElastoDyn + self.SElastoDynFile['BlPitch'] = self.bins.sel(wspd=Vhub_, method='nearest').BlPitch.values + self.SElastoDynFile['RotSpeed'] = self.bins.sel(wspd=Vhub_, method='nearest').RotSpeed.values + self.SElastoDynFile['NacYaw'] = yaw_deg_ + yaw_mis_deg_ + if writeFiles: + self.SElastoDynFile.write(os.path.join(currPath,f'{self.SEDfilename}{t+1}_mod.dat')) + + # Update each turbine's ServoDyn + self.ServoDynFile['YawNeut'] = yaw_deg_ + yaw_mis_deg_ + self.ServoDynFile['DLL_FileName'] = f'"{self.DLLfilepath}{t+1}.so"' + if writeFiles: + self.ServoDynFile.write( os.path.join(currPath,f'{self.SrvDfilename}{t+1}_mod.dat')) + + # Update each turbine's OpenFAST input + self.turbineFile['TMax'] = self.tmax + self.turbineFile['CompInflow'] = 1 # 1: InflowWind; 2: OpenFoam (fully coupled; not VTK input to FF) + + if EDmodel_ == 'FED': + self.turbineFile['CompElast'] = 1 # 1: full ElastoDyn; 2: full ElastoDyn + BeamDyn; 3: Simplified ElastoDyn + self.turbineFile['CompSub'] = 1 + self.turbineFile['CompHydro'] = 1 + self.turbineFile['EDFile'] = f'"./{self.EDfilename}{t+1}_mod.dat"' + elif EDmodel_ == 'SED': + self.turbineFile['CompElast'] = 3 # 1: full ElastoDyn; 2: full ElastoDyn + BeamDyn; 3: Simplified ElastoDyn + self.turbineFile['CompSub'] = 0 # need to be disabled with SED + self.turbineFile['CompHydro'] = 0 # need to be disabled with SED + self.turbineFile['IntMethod'] = 3 + self.turbineFile['EDFile'] = f'"./{self.SEDfilename}{t+1}_mod.dat"' + self.turbineFile['BDBldFile(1)'] = f'"{self.BDfilepath}"' + self.turbineFile['BDBldFile(2)'] = f'"{self.BDfilepath}"' + self.turbineFile['BDBldFile(3)'] = f'"{self.BDfilepath}"' + self.turbineFile['InflowFile'] = f'"./{self.IWfilename}"' + if ADmodel_ == 'ADyn': + self.turbineFile['CompAero'] = 2 # 1: AeroDyn v14; 2: AeroDyn v15; 3: AeroDisk + self.turbineFile['AeroFile'] = f'"{self.ADfilepath}"' + elif ADmodel_ == 'ADsk': + # from Andy's email on 2022-11-01 So if you use AeroDisk with ElastoDyn, just set the blade DOFs to false for now. + self.turbineFile['CompAero'] = 3 # 1: AeroDyn v14; 2: AeroDyn v15; 3: AeroDisk + self.turbineFile['AeroFile'] = f'"{self.ADskfilepath}"' + if writeFiles: + status = os.system(f'cp {self.coeffTablefilepath} {os.path.join(currPath,self.coeffTablefilename)}') + self.turbineFile['ServoFile'] = f'"./{self.SrvDfilename}{t+1}_mod.dat"' + self.turbineFile['HydroFile'] = f'"./{self.HDfilename}"' + self.turbineFile['SubFile'] = f'"{self.SubDfilepath}"' + self.turbineFile['MooringFile'] = f'"unused"' + self.turbineFile['IceFile'] = f'"unused"' + self.turbineFile['TStart'] = 0 # start saving openfast output from time 0 (to see transient) + self.turbineFile['OutFileFmt'] = 3 # 1: .out; 2: .outb; 3: both + + if writeFiles: + self.turbineFile.write( os.path.join(currPath,f'{self.turbfilename}{t+1}.fst')) + + + + def setTemplateFilename(self, templatePath=None, EDfilename=None, SEDfilename=None, HDfilename=None, SrvDfilename=None, ADfilename=None, ADskfilename=None, SubDfilename=None, IWfilename=None, BDfilepath=None, bladefilename=None, towerfilename=None, turbfilename=None, libdisconfilepath=None, controllerInputfilename=None, coeffTablefilename=None, turbsimLowfilepath=None, turbsimHighfilepath=None, FFfilename=None): + ''' + + *filename: str + The filename of the current O penFAST submodule, no complete path. Assumes it is + inside `templatePath` + *filepath: str + Complete path of the file. Ma y or may not be inside `templatePath` + + ''' - self._setAndCheckInputs() + self.EDfilename = "unused" + self.SEDfilename = "unused" + self.HDfilename = "unused" + self.SrvDfilename = "unused" + self.ADfilename = "unused" + self.ADskfilename = "unused" + self.SubDfilename = "unused" + self.IWfilename = "unused" + self.BDfilepath = "unused" + self.bladefilename = "unused" + self.towerfilename = "unused" - self.createAuxArrays() + if templatePath is None: + print(f'--- WARNING: No template files given. Complete setup will not be possible') + return + if not os.path.isdir(templatePath): + raise ValueError (f'Template path {templatePath} does not seem to exist.') - def createAuxArrays(self): + self.templatePath = templatePath + def checkIfExists(f): + if f == 'unused': + return + if not os.path.isfile(f): + raise ValueError (f'File {f} does not exist.') - def _setAndCheckInputs(self): + if EDfilename is not None: + if EDfilename != 'unused' and not EDfilename.endswith('.T'): + raise ValueError (f'Name the template ED file "*.T.dat" and give "*.T" as `EDfilename`') + self.EDfilepath = os.path.join(self.templatePath,f"{EDfilename}.dat") + checkIfExists(self.EDfilepath) + self.EDfilename = EDfilename - if not isinstance(self.wts,dict): - raise ValueError (f'`wts` needs to be a dictionary with the following entries for each turbine: x, y, z, D, zhub.') - if not isinstance(self.wts[0]['x'],(float,int)): - raise ValueError (f'The `x` value for the turbines should be an integer or float') - # todo: check if all entries of the dict are the same regarding D and zhub - self.D = self.wts[0]['D'] + if SEDfilename is not None: + if SEDfilename != 'unused' and not SEDfilename.endswith('.T'): + raise ValueError (f'Name the template SED file "*.T.dat" and give "*.T" as `SEDfilename`') + self.SEDfilepath = os.path.join(self.templatePath,f"{SEDfilename}.dat") + checkIfExists(self.SEDfilepath) + self.SEDfilename = SEDfilename + if HDfilename is not None: + if HDfilename != 'unused' and not HDfilename.endswith('.dat'): + raise ValueError (f'The HydroDyn filename should end in `.dat`.') + self.HDfilepath = os.path.join(self.templatePath,HDfilename) + checkIfExists(self.HDfilepath) + self.HDfilename = HDfilename - # Auxiliary variables and some checks - if self.seedValues is None: - self.seedValues = [2318573, 122299, 123456, 389432, -432443, 9849898] + if SrvDfilename is not None: + if not EDfilename.endswith('.T'): + raise ValueError (f'Name the template ServoDyn file "*.T.dat" and give "*.T" as `SrvDfilename`') + self.SrvDfilepath = os.path.join(self.templatePath,f"{SrvDfilename}.dat") + checkIfExists(self.SrvDfilepath) + self.SrvDfilename = SrvDfilename + if ADfilename is not None: + if ADfilename != 'unused' and not ADfilename.endswith('.dat'): + raise ValueError (f'The AeroDyn filename should end in `.dat`.') + self.ADfilepath = os.path.join(self.templatePath,ADfilename) + checkIfExists(self.ADfilepath) + self.ADfilename = ADfilename - self.nTurbines = len(self.wts) + if ADskfilename is not None: + if ADskfilename != 'unused' and not ADskfilename.endswith('.dat'): + raise ValueError (f'The AeroDisk filename should end in `.dat`.') + self.ADskfilepath = os.path.join(self.templatePath,ADskfilename) + checkIfExists(self.ADskfilepath) + self.ADskfilename = ADskfilename + if SubDfilename is not None: + if not SubDfilename.endswith('.dat'): + raise ValueError (f'The SubDyn filename should end in `.dat`.') + self.SubDfilepath = os.path.join(self.templatePath,SubDfilename) + checkIfExists(self.SubDfilepath) + self.SubDfilename = SubDfilename - self._setRotorParameters() + if IWfilename is not None: + if IWfilename != 'unused' and not IWfilename.endswith('.dat'): + raise ValueError (f'The InflowWind filename should end in `.dat`.') + self.IWfilepath = os.path.join(self.templatePath,IWfilename) + checkIfExists(self.IWfilepath) + self.IWfilename = IWfilename - # Ensure quantities are list - self.vhub = [vhub] if isinstance(vhub,(float,int)) else vhub - self.shear = [shear] if isinstance(shear,(float,int)) else shear - self.TIvalue = [TIvalue] if isinstance(TIvalue,(float,int)) else TIvalue + if BDfilepath is not None: + if BDfilepath != 'unused' and not BDfilepath.endswith('.dat'): + raise ValueError (f'The BeamDyn filename should end in `.dat`.') + self.BDfilepath = BDfilepath + checkIfExists(self.BDfilepath) - if self.ADmodel is None: - self.ADmodel = np.tile(['ADyn'],(1,self.nTurbines)) + if bladefilename is not None: + if not bladefilename.endswith('.dat'): + raise ValueError (f'The blade filename should end in `.dat`.') + self.bladefilepath = os.path.join(self.templatePath,bladefilename) + checkIfExists(self.bladefilepath) + self.bladefilename = bladefilename - if self.EDmodel is None: - self.EDmodel = np.tile(['FED'],(1,self.nTurbines)) + if towerfilename is not None: + if not towerfilename.endswith('.dat'): + raise ValueError (f'The tower filename should end in `.dat`.') + self.towerfilepath = os.path.join(self.templatePath,towerfilename) + checkIfExists(self.towerfilepath) + self.towerfilename = towerfilename - if self.yaw is None: - self.yaw = np.zeros((1,self.nTurbines))) + if turbfilename is not None: + if not turbfilename.endswith('.T'): + raise ValueError (f'Name the template turbine file "*.T.fst" and give "*.T" as `turbfilename`') + self.turbfilepath = os.path.join(self.templatePath,f"{turbfilename}.fst") + checkIfExists(self.turbfilepath) + self.turbfilename = turbfilename - if not os.path.isfile(self.ffbin): - raise ValueError (f'The FAST.Farm binary given does not appear to exist') + if libdisconfilepath is not None: + if not libdisconfilepath.endswith('.so'): + raise ValueError (f'The libdiscon `libdisconfilepath` file should end in "*.so"') + self.libdisconfilepath = libdisconfilepath + checkIfExists(self.libdisconfilepath) + self._create_copy_libdiscon() + if controllerInputfilename is not None: + if not controllerInputfilename.endswith('.IN'): + print(f'--- WARNING: The controller input file typically ends in "*.IN". Currently {controllerInputfilename}. Double check.') + self.controllerInputfilepath = os.path.join(self.templatePath, controllerInputfilename) + checkIfExists(self.controllerInputfilepath) + self.controllerInputfilename = controllerInputfilename + if coeffTablefilename is not None: + if not coeffTablefilename.endswith('.csv'): + raise ValueError (f'The performance table `coeffTablefilename` file should end in "*.csv"') + self.coeffTablefilepath = os.path.join(templatePath, coeffTablefilename) + checkIfExists(self.coeffTablefilepath) + self.coeffTablefilename = coeffTablefilename - if self.LESpath is None: - print('Setting up FAST.Farm based on TurbSim inflow') - self.inflowStr = 'TurbSim' - else: - print('Seeing up FAST.Farm based on LES inflow') - if not os.path.isdir(self.LESpath): - raise ValueError (f'The path {self.LESpath} does not exist') - self.inflowStr = 'LES' + if turbsimLowfilepath is not None: + if not turbsimLowfilepath.endswith('.inp'): + raise ValueError (f'TurbSim file input for low-res box `turbsimLowfilepath` should end in ".inp".') + self.turbsimLowfilepath = turbsimLowfilepath + checkIfExists(self.turbsimLowfilepath) + + if turbsimHighfilepath is not None: + if not turbsimHighfilepath.endswith('.inp'): + raise ValueError (f'TurbSim file input for high-res box `turbsimHighfilepath` should end in ".inp".') + self.turbsimHighfilepath = turbsimHighfilepath + checkIfExists(self.turbsimHighfilepath) + + if FFfilename is not None: + if not FFfilename.endswith('.fstf'): + raise ValueError (f'FAST.Farm input file `FFfilename` should end in ".fstf".') + self.FFfilepath = os.path.join(self.templatePath,FFfilename) + checkIfExists(self.FFfilepath) + self.FFfilename = FFfilename + + self._open_template_files() + + self.templateFilesCreatedBool = True + return - # Create case path is doesn't exist - if not os.path.exists(self.path): - os.makedirs(self.path) - - # Perform some checks - if len(self.seedValues) != self.nSeeds: - raise ValueError(f'Number of seeds is {self.nSeeds} but {len(self.seedValues)} seed values were given. Adjust the seedValues array accordingly') - - if not (np.array(self.extent_low)>=0).all(): - raise ValueError(f'The array for low-res box extents should be given with positive values') + def _create_copy_libdiscon(self): + # Make copies of libdiscon for each turbine if they don't exist + copied = False + for t in range(self.nTurbines): + libdisconfilename = os.path.splitext(os.path.basename(self.libdisconfilepath))[0] + currLibdiscon = os.path.join(os.path.dirname(self.libdisconfilepath), f'{libdisconfilename}.T{t+1}.so') + self.DLLfilepath = os.path.join(os.path.dirname(self.libdisconfilepath), f'{libdisconfilename}.T') + if not os.path.isfile(currLibdiscon): + if self.verbose>0: print(f' Creating a copy of the controller {libdisconfilename}.so in {currLibdiscon}') + status = os.system(f'cp {self.libdisconfilepath} {currLibdiscon}') + copied=True + + if copied == False and self.verbose>0: + print(f' Copies of the controller {libdisconfilename}.T[1-{self.nTurbines}].so already exists in {os.path.dirname(self.libdisconfilepath)}. Skipped step.') + - if self.dt_low_les < self.dt_high_les: - raise ValueError(f'The temporal resolution dT_High should not be greater than dT_Low on the LES side') + def _open_template_files(self): - if self.ds_low_les < ds_high_les: - raise ValueError(f'The grid resolution dS_High should not be greater than dS_Low on the LES side') + # Open template files + def _check_and_open(f): + if f != 'unused': + return FASTInputFile(f) - assert isinstance(self.extent_high, (float,int)) - if self.extent_high<=0: - raise ValueError(f'The extent of high boxes should be positive') - - #assert dt_low_desired%(1/(2*fmax)) < 1e-10 + self.ElastoDynFile = _check_and_open(self.EDfilepath) #FASTInputFile(os.path.join(self.templatePath, f'{ElastoDynFileName}.dat')) + self.SElastoDynFile = _check_and_open(self.SEDfilepath) #FASTInputFile(os.path.join(self.templatePath, f'{SElastoDynFileName}.dat')) + self.HydroDynFile = _check_and_open(self.HDfilepath) #FASTInputFile(os.path.join(self.templatePath, HydroDynFileName)) + self.ServoDynFile = _check_and_open(self.SrvDfilepath) #FASTInputFile(os.path.join(self.templatePath, f'{ServoDynFileName}.dat')) + self.AeroDiskFile = _check_and_open(self.ADskfilepath) #FASTInputFile(os.path.join(self.templatePath, AeroDiskFileName)) + self.turbineFile = _check_and_open(self.turbfilepath) #FASTInputFile(os.path.join(self.templatePath, f'{turbineFileName}.fst')) + self.InflowWindFile = _check_and_open(self.IWfilepath) #FASTInputFile(os.path.join(self.templatePath, InflowWindFileName)) + - if np.shape(self.ADmodel) != np.shape(self.EDmodel): - raise ValueError('Every case should have the aerodynamic and elastic model selected. The number of cases (lines) in `ADmodel` and `EDmodel` should be the same') - - if self.nTurbines != np.shape(self.ADmodel)[1]: - raise ValueError(f'The number of turbines in wts ({len(wts)}) should match the number of turbines in the ADmodel and EDmodel arrays ({np.shape(ADmodel)[1]})') - - if len(inflow_deg) != len(yaw_init): - raise ValueError(f'One row for each inflow angle should be given in yaw_init. Currently {len(inflow_deg)} inflow angle(s) and {len(yaw_init)} yaw entrie(s)') + def print_template_files(self): + raise NotImplementedError (f'Placeholder. Not implemented.') + def createAuxArrays(self): + + self._rotate_wts() + self._create_all_cond() + self._create_all_cases() + + + + def _create_all_cond(self): + self.nConditions = len(self.vhub) * len(self.shear) * len(self.TIvalue) + + # Repeat arrays as necessary to build xarray Dataset + vhub_repeat = np.repeat(self.vhub, self.nConditions/len(self.vhub), axis=0) + shear_repeat = np.repeat(self.shear, self.nConditions/len(self.shear), axis=0) + TIvalue_repeat = np.repeat(self.TIvalue, self.nConditions/len(self.TIvalue), axis=0) + + self.allCond = xr.Dataset({'vhub': (['cond'], vhub_repeat ), + 'shear': (['cond'], shear_repeat ), + 'TIvalue': (['cond'], TIvalue_repeat)}, + coords={'cond': np.arange(self.nConditions)} ) + + + def _create_all_cases(self): + # Generate the different "cases" (inflow angle, and misalignment and wakesteer bools). She reads from ../NewFFParams_Shell.csv + # If misalignment true, then the actual yaw is yaw[turb]=np.random.uniform(low=-8.0, high=8.0). CaseSetup/ParameterManipulation.py:166 + + # Calculate the total number of cases given sweeps requested. Multipliers for wake steering, yaw misalignment, and reduced-order models + nWindDir = len(np.unique(self.inflow_deg)) + nCasesWSmultiplier = 2 if self.sweepWS else 1 + nCasesYMmultiplier = 2 if self.sweepYM else 1 + nCasesROmultiplier = len(self.EDmodel) + + # Yaw multiplier, setup in the form of repeated wind directions with changing yaw + nCasesYawmultiplier = int(len(self.inflow_deg)/len(np.unique(self.inflow_deg))) + + # Aux nCases vars + nCases = int(nWindDir * nCasesWSmultiplier * nCasesYMmultiplier * nCasesROmultiplier * nCasesYawmultiplier) + nCasesWSfalse = int(nCases/nCasesWSmultiplier) + nCasesWStrue = int(nCases - nCasesWSfalse) + nCasesYMfalse = int(nCases/nCasesYMmultiplier) + nCasesYMtrue = int(nCases - nCasesYMfalse) + if self.verbose>2: print(f' Cases: nWindDir = {nWindDir} ') + if self.verbose>2: print(f' Cases: nCases = {nCases}') + if self.verbose>2: print(f' Cases: nCasesWStrue = {nCasesWStrue}') + if self.verbose>2: print(f' Cases: nCasesWSfalse = {nCasesWSfalse}') + if self.verbose>2: print(f' Cases: nCasesYMtrue = {nCasesYMtrue}') + if self.verbose>2: print(f' Cases: nCasesYMfalse = {nCasesYMfalse}') + + # Build an array of wind directions, with repeated values to account for wake steering and yaw misalign bools, and ROM options + windDir = np.repeat(self.inflow_deg, nCasesWSmultiplier*nCasesYMmultiplier*nCasesROmultiplier) + yawInit = np.repeat(self.yaw_init, nCasesWSmultiplier*nCasesYMmultiplier*nCasesROmultiplier, axis=0) + + # Build arrays of wake steering and yaw misalignment bools (done this way for clarity) + if self.sweepWS and self.sweepYM: + wakeSteering = np.tile([False, True, False, True], nWindDir*nCasesROmultiplier*nCasesYawmultiplier) + misalignment = np.tile([False, False, True, True], nWindDir*nCasesROmultiplier*nCasesYawmultiplier) + elif self.sweepWS and not self.sweepYM: + wakeSteering = np.tile([False, True ], nWindDir*nCasesROmultiplier*nCasesYawmultiplier) + misalignment = np.tile([False, False], nWindDir*nCasesROmultiplier*nCasesYawmultiplier) + elif not self.sweepWS and self.sweepYM: + wakeSteering = np.tile([False, False], nWindDir*nCasesROmultiplier*nCasesYawmultiplier) + misalignment = np.tile([False, True ], nWindDir*nCasesROmultiplier*nCasesYawmultiplier) + elif not self.sweepWS and not self.sweepYM: + wakeSteering = np.tile([False], nWindDir*nCasesROmultiplier*nCasesYawmultiplier) + misalignment = np.tile([False], nWindDir*nCasesROmultiplier*nCasesYawmultiplier) + + + + # Create array of random numbers for yaw misalignment, and set it to zero where no yaw misalign is requested + yawMisalignedValue = np.random.uniform(size = [nCases,self.nTurbines], low=-8.0, high=8.0) + yawMisalignedValue[~misalignment,:] = 0 + + # Count number of simplified models to add that information to the xarray. If their length is 1, it means they weren't requested + if len(self.ADmodel) == 1: + nADyn = self.nTurbines + else: + nADyn = [ self.ADmodel[i].count('ADyn') for i in range(len(self.ADmodel)) ] + if len(self.EDmodel) == 1: + nFED = self.nTurbines + else: + nFED = [ self.EDmodel[i].count('FED') for i in range(len(self.EDmodel)) ] + + # Come up with an ordered "yaw case" numbering for dir name + yawCase = np.arange(nCasesYawmultiplier)+1 + + # Assemble main case dataset, containing turbine info + self.nCases = nCases + self.allCases = xr.Dataset( + { + 'Tx': (['case','turbine'], np.repeat(self.wts_rot_ds['x'].values , nCasesWSmultiplier*nCasesYMmultiplier*nCasesROmultiplier*nCasesYawmultiplier, axis=0)), + 'Ty': (['case','turbine'], np.repeat(self.wts_rot_ds['y'].values , nCasesWSmultiplier*nCasesYMmultiplier*nCasesROmultiplier*nCasesYawmultiplier, axis=0)), + 'Tz': (['case','turbine'], np.repeat(self.wts_rot_ds['z'].values , nCasesWSmultiplier*nCasesYMmultiplier*nCasesROmultiplier*nCasesYawmultiplier, axis=0)), + 'D': (['case','turbine'], np.repeat(self.wts_rot_ds['D'].values , nCasesWSmultiplier*nCasesYMmultiplier*nCasesROmultiplier*nCasesYawmultiplier, axis=0)), + 'zhub': (['case','turbine'], np.repeat(self.wts_rot_ds['zhub'].values, nCasesWSmultiplier*nCasesYMmultiplier*nCasesROmultiplier*nCasesYawmultiplier, axis=0)), + 'yawmis': (['case','turbine'], yawMisalignedValue), + 'yaw': (['case','turbine'], yawInit), + 'yawCase': (['case'], np.repeat(yawCase, nWindDir*nCasesWSmultiplier*nCasesYMmultiplier*nCasesROmultiplier)), # i didn't have ROM multiplier + 'ADmodel': (['case','turbine'], np.tile(np.repeat(self.ADmodel, nCasesWSmultiplier*nCasesYMmultiplier*nCasesYawmultiplier, axis=0),(nWindDir,1)) ), + 'EDmodel': (['case','turbine'], np.tile(np.repeat(self.EDmodel, nCasesWSmultiplier*nCasesYMmultiplier*nCasesYawmultiplier, axis=0),(nWindDir,1)) ), + 'nFullAeroDyn': (['case'], np.repeat(np.tile(nADyn, nWindDir), nCasesWSmultiplier*nCasesYMmultiplier*nCasesYawmultiplier)), + 'nFulllElastoDyn': (['case'], np.repeat(np.tile(nFED, nWindDir), nCasesWSmultiplier*nCasesYMmultiplier*nCasesYawmultiplier)), + 'wakeSteering': (['case'], wakeSteering), + 'misalignment': (['case'], misalignment), + 'inflow_deg': (['case'], windDir), + }, + coords={ + 'case': range(nCases), + 'turbine': range(self.nTurbines), + }, + ) - def _setRotorParameters(self): + def _rotate_wts(self): + # Calculate the rotated positions of the turbines wrt the reference turbine + wts_rot={} + for inflow in self.inflow_deg: + for i , turb in self.wts.items(): + ref = self.wts[self.refTurb_rot] + + xori = self.wts[i]['x'] + x = ref['x'] + (self.wts[i]['x']-ref['x'])*cosd(inflow) - (self.wts[i]['y']-ref['y'])*sind(inflow) + yori = self.wts[i]['y'] + y = ref['y'] - (self.wts[i]['x']-ref['x'])*sind(-inflow) + (self.wts[i]['y']-ref['y'])*cosd(-inflow) + z = self.wts[i]['z'] + D = self.wts[i]['D'] + zhub = self.wts[i]['zhub'] + + wts_rot[inflow,i] = {'x':x, 'y':y, 'z':z, + 'D':D, 'zhub':zhub, + } + self.wts_rot_ds = pd.DataFrame.from_dict(wts_rot, orient='index').to_xarray().rename({'level_0':'inflow_deg','level_1':'turbine'}) + + + def _setRotorParameters(self): + + if self.D == 220: # 12 MW turbine self.bins = xr.Dataset({'WaveHs': (['wspd'], [ 1.429, 1.429]), # 1.429 comes from Matt's hydrodyn input file 'WaveTp': (['wspd'], [ 7.073, 7.073]), # 7.073 comes from Matt's hydrodyn input file @@ -170,6 +749,879 @@ def _setRotorParameters(self): else: raise ValueError(f'Unknown turbine with diameter {self.D}. Add values to the `_setRotorParameters` function.') + + + + + def TS_low_setup(self, writeFiles=True, runOnce=False): + # Loops on all conditions/seeds creating Low-res TurbSim box (following python-toolbox/pyFAST/fastfarm/examples/Ex1_TurbSimInputSetup.py) + boxType='lowres' + for cond in range(self.nConditions): + for seed in range(self.nSeeds): + seedPath = os.path.join(self.path, self.condDirList[cond], f'Seed_{seed}') + + # ---------------- TurbSim Low boxes setup ------------------ # + # Set file to be created + currentTSLowFile = os.path.join(seedPath, 'Low_stillToBeModified.inp') + + # Get properties needed for the creation of the low-res turbsim inp file + D_ = self.allCases['D' ].max().values + HubHt_ = self.allCases['zhub'].max().values + xlocs_ = self.allCases['Tx' ].values.flatten() # All turbines are needed for proper + ylocs_ = self.allCases['Ty' ].values.flatten() # and consistent extent calculation + Vhub_ = self.allCond.sel(cond=cond)['vhub' ].values + shear_ = self.allCond.sel(cond=cond)['shear' ].values + tivalue_ = self.allCond.sel(cond=cond)['TIvalue'].values + # Coherence parameters + a = 12; b=0.12 # IEC 61400-3 ed4, app C, eq C.16 + Lambda1 = 0.7*HubHt_ if HubHt_<60 else 42 # IEC 61400-3 ed4, sec 6.3.1, eq 5 + + # Create and write new Low.inp files creating the proper box with proper resolution + # By passing low_ext, manual mode for the domain size is activated, and by passing ds_low, manual mode + # for discretization (and further domain size) is also activated + currentTS = TSCaseCreation(D_, HubHt_, Vhub_, tivalue_, shear_, x=xlocs_, y=ylocs_, zbot=self.zbot, cmax=self.cmax, + fmax=self.fmax, Cmeander=self.Cmeander, boxType=boxType, low_ext=self.extent_low, ds_low=self.ds_low_les) + self.TSlowbox = currentTS + if runOnce: + return + currentTS.writeTSFile(self.turbsimLowfilepath, currentTSLowFile, tmax=self.tmax, verbose=self.verbose) + + # Modify some values and save file (some have already been set in the call above) + Lowinp = FASTInputFile(currentTSLowFile) + Lowinp['RandSeed1'] = self.seedValues[seed] + Lowinp['PLExp'] = shear_ + #Lowinp['latitude'] = latitude + Lowinp['InCDec1'] = Lowinp['InCDec2'] = Lowinp['InCDec3'] = f'"{a} {b/(8.1*Lambda1):.8f}"' + # The dt was computed for a proper low-res box but here we will want to compare with the high-res + # and it is convenient to have the same time step. Let's do that change here + Lowinp['TimeStep'] = 1/(2*self.fmax) + if writeFiles: + Lowinp.write( os.path.join(seedPath, 'Low.inp') ) + + # Let's remove the original file + os.remove(os.path.join(seedPath, 'Low_stillToBeModified.inp')) + + self.TSlowBoxFilesCreatedBool = True + + + def TS_low_slurm_prepare(self, slurmfilepath): + # -------------------------------------------------- + # ----- Prepare SLURM script for Low-res boxes ----- + # -------------------------------------------------- + + if not os.path.isfile(slurmfilepath): + raise ValueError (f'SLURM script for low-res box {slurmfilepath} does not exist.') + self.slurmfilename_low = os.path.basename(slurmfilepath) + + import subprocess + status = os.system(f'cp {slurmfilepath} {self.path}/{self.slurmfilename_low}') + + # Change job name (for convenience only) + _ = subprocess.call(f"sed -i 's|#SBATCH --job-name=lowBox|#SBATCH --job-name=lowBox_{os.path.basename(self.path)}|g' {self.slurmfilename_low}", cwd=self.path, shell=True) + # Change the path inside the script to the desired one + sed_command = f"sed -i 's|/projects/shellwind/rthedin/Task2_2regis|{self.path}|g' {self.slurmfilename_low}" + _ = subprocess.call(sed_command, cwd=self.path, shell=True) + # Change number of nodes values + _ = subprocess.call(f"sed -i 's|#SBATCH --nodes=2|#SBATCH --nodes={int(np.ceil(self.nConditions*self.nSeeds/6))}|g' {self.slurmfilename_low}", cwd=self.path, shell=True) + # Assemble list of conditions and write it + listtoprint = "' '".join(self.condDirList) + sed_command = f"""sed -i "s|^condList.*|condList=('{listtoprint}')|g" {self.slurmfilename_low}""" + _ = subprocess.call(sed_command, cwd=self.path, shell=True) + + + def TS_low_slurm_submit(self): + # --------------------------------- + # ----- Run turbSim Low boxes ----- + # --------------------------------- + # Submit the script to SLURM + _ = subprocess.call('sbatch {self.slurmfilename_low}', cwd=self.path, shell=True) + + + def TS_low_createSymlinks(self): + # Create symbolic links for all of the time-series and the Low.bts files too + + notepath = os.getcwd() + os.chdir(self.path) + for cond in range(self.nConditions): + for case in range(self.nCases): + for seed in range(self.nSeeds): + try: + src = os.path.join('../../../..', self.condDirList[cond], f'Seed_{seed}', 'Low.bts') + dst = os.path.join(self.condDirList[cond], self.caseDirList[case], f'Seed_{seed}', 'TurbSim', 'Low.bts') + os.symlink(src, dst) + except FileExistsError: + print(f' File {dst} already exists. Skipping symlink.') + os.chdir(notepath) + + + def getDomainParameters(self): + + # If the low box setup hasn't been called (e.g. LES run), do it once to get domain extents + if not self.TSlowBoxFilesCreatedBool: + self.TS_low_setup(writeFiles=False, runOnce=True) + + # Figure out how many (and which) high boxes actually need to be executed. Remember that wake steering, yaw misalignment, SED/ADsk models, + # and sweep in yaw do not require extra TurbSim runs + self.nHighBoxCases = len(np.unique(self.inflow_deg)) # some wind dir might be repeated for sweep on yaws + + self.allHighBoxCases = self.allCases.where(~self.allCases['wakeSteering'],drop=True).drop_vars('wakeSteering')\ + .where(~self.allCases['misalignment'], drop=True).drop_vars('misalignment')\ + .where(self.allCases['nFullAeroDyn']==self.nTurbines, drop=True).drop_vars('ADmodel')\ + .where(self.allCases['nFulllElastoDyn']==self.nTurbines, drop=True).drop_vars('EDmodel')\ + .where(self.allCases['yawCase']==1, drop=True).drop_vars('yawCase') + + if self.nHighBoxCases != len(self.allHighBoxCases.case): + raise ValueError(f'The number of cases do not match as expected. {self.nHighBoxCases} unique wind directions, but {len(self.allHighBoxCases.case)} unique cases.') + + # Determine offsets from turbines coordinate frame to TurbSim coordinate frame + self.yoffset_turbsOrigin2TSOrigin = -( (self.TSlowbox.ymax - self.TSlowbox.ymin)/2 + self.TSlowbox.ymin ) + self.xoffset_turbsOrigin2TSOrigin = - self.extent_low[0]*self.D + + if self.verbose>0: + print(f" The y offset between the turbine ref frame and turbsim is {self.yoffset_turbsOrigin2TSOrigin}") + print(f" The x offset between the turbine ref frame and turbsim is {self.xoffset_turbsOrigin2TSOrigin}") + + if self.verbose>2: + print(f'allHighBoxCases is:') + print(self.allHighBoxCases) + + + def TS_high_get_time_series(self): + + # Loop on all conditions/seeds extracting time series from the Low box at turbines location + boxType='highres' + for cond in range(self.nConditions): + for seed in range(self.nSeeds): + condSeedPath = os.path.join(path, self.condDirList[cond], f'Seed_{seed}') + + # Read output .bts for current seed + bts = TurbSimFile(os.path.join(condSeedPath, 'Low.bts')) + bts['t'] = np.round(bts['t'], 6) # rounding single precision read as double precision + bts['dt'] = np.round(bts['dt'], 6) + + for case in range(self.nHighBoxCases): + # Get actual case number given the high-box that need to be saved + case = self.allHighBoxCases.isel(case=case)['case'].values + + caseSeedPath = os.path.join(self.path, self.condDirList[cond], self.caseDirList[case], f'Seed_{seed}', 'TurbSim') + + for t in range(nTurbines): + # Recover turbine properties of the current case + HubHt_ = self.allCases.sel(case=case, turbine=t)['zhub'].values + xloc_ = self.allCases.sel(case=case, turbine=t)['Tx' ].values + yloc_ = self.allCases.sel(case=case, turbine=t)['Ty' ].values + + # Turbine location in TurbSim reference frame + xt = xloc_ + self.xoffset_turbsOrigin2TSOrigin + yt = yloc_ + self.yoffset_turbsOrigin2TSOrigin + + # Get indices of turbine location in TurbSim files + jTurb, kTurb = bts.closestPoint(y=yt,z=HubHt_) + Hub_series = bts['z'][kTurb] + + # Get indices of the half height position (TurbSim's hub height) + jMid, kMid = bts.iMid + + # Get time series at the box center to get mean vhub and create time array. JJ: get at the Turbsim hub height + #Vhub = bts['u'][0,:,jTurb,kTurb] + Vmid = bts['u'][0,:,jMid,kMid] + time = bts.t + + # The time-series need to be shifted depending on the turbine location, so we need to find how many + # grid points (time steps) the data have convected. We use the mean streamwise component for that + start_time_step = round( (xt/Vmid.mean())/bts.dt ) + + # Get time-series given rolling + uvel = np.roll(bts['u'][0, :, jTurb, kTurb], start_time_step) + vvel = np.roll(bts['u'][1, :, jTurb, kTurb], start_time_step) + wvel = np.roll(bts['u'][2, :, jTurb, kTurb], start_time_step) + + # Checks + assert len(time)==len(uvel) + assert len(uvel)==len(vvel) + assert len(vvel)==len(wvel) + + # Save timeseries as CondXX/Seed_Z/USRTimeSeries_T*.txt. This file will later be copied to CondXX/CaseYY/Seed_Z + timeSeriesOutputFile = os.path.join(caseSeedPath, f'USRTimeSeries_T{t+1}.txt') + + # The reference frame used in the time-series is the inertial frame of the high-res box (local). + # Sometimes the point where we want to place the turbine at exists and then we can set y=0. For example, suppose the low-res + # grid has y = ..., 980, 1000, 1020, ..., and we want to place a turbine at y=1000. The high-res box has 5m resolution. Then, + # the time-series will be pulled from _exactly_ y=1000, and since the low-res grid has a grid point there too, so we can put + # y=0 on the time-series input file. However, if we want the turbine at y=998, we must account for the difference since this + # y value is not a grid point of the low-res box. So we compute an offset between the turbine location and the nearest grid + # point in the low-res box, and then pass this offset to the time-series file. In this example, the offset is 2 m, thus the + # time-series file will have a y of 2 m. + yoffset = bts['y'][jTurb] - yt + if yoffset != 0: + print(f"Seed {seed}, Case {case}: Turbine {t+1} is not at a grid point location. Tubine is at y={yloc_}",\ + f"on the turbine reference frame, which is y={yt} on the low-res TurbSim reference frame. The",\ + f"nearest grid point in y is {bts['y'][jTurb]} so printing y={yoffset} to the time-series file.") + writeTimeSeriesFile(timeSeriesOutputFile, yoffset, Hub_series, uvel, vvel, wvel, time) + + + + + def TS_high_setup(self, writeFiles=True): + + # Loop on all conditions/cases/seeds setting up the High boxes + boxType='highres' + for cond in range(self.nConditions): + for case in range(self.nHighBoxCases): + # Get actual case number given the high-box that need to be saved + case = self.allHighBoxCases.isel(case=case)['case'].values + if self.verbose>3: + print(f'Generating high-res box setup for cond {cond} ({self.condDirList[cond]}), case {case} ({self.caseDirList[case]}).') + for seed in range(self.nSeeds): + seedPath = os.path.join(self.path, self.condDirList[cond], self.caseDirList[case], f'Seed_{seed}', 'TurbSim') + + for t in range(self.nTurbines): + + # ---------------- TurbSim High boxes setup ------------------ # + currentTSHighFile = os.path.join(seedPath, f'HighT{t+1}_stillToBeModified.inp') + + # Get properties needed for the creation of the high-res turbsim inp file + D_ = self.allCases.sel(case=case, turbine=t)['D' ].values + HubHt_ = self.allCases.sel(case=case, turbine=t)['zhub'].values + xloc_ = self.allCases.sel(case=case, turbine=t)['Tx' ].values + yloc_ = self.allCases.sel(case=case, turbine=t)['Ty' ].values + Vhub_ = self.allCond.sel(cond=cond)['vhub' ].values + shear_ = self.allCond.sel(cond=cond)['shear' ].values + tivalue_ = self.allCond.sel(cond=cond)['TIvalue'].values + + # Coherence parameters + a = 12; b=0.12 # IEC 61400-3 ed4, app C, eq C.16 + Lambda1 = 0.7*HubHt_ if HubHt_<60 else 42 # IEC 61400-3 ed4, sec 6.3.1, eq 5 + + # Create and write new Low.inp files creating the proper box with proper resolution + currentTS = TSCaseCreation(D_, HubHt_, Vhub_, tivalue_, shear_, x=xloc_, y=yloc_, zbot=self.zbot, + cmax=self.cmax, fmax=self.fmax, Cmeander=self.Cmeander, boxType=boxType, high_ext=self.extent_high) + currentTS.writeTSFile(self.turbsimHighfilepath, currentTSHighFile, tmax=self.tmax, turb=t, verbose=self.verbose) + + # Modify some values and save file (some have already been set in the call above) + Highinp = FASTInputFile(currentTSHighFile) + Highinp['RandSeed1'] = self.seedValues[seed] + Highinp['TimeStep'] = 1/(2*self.fmax) + Highinp['TurbModel'] = f'"TIMESR"' + Highinp['UserFile'] = f'"USRTimeSeries_T{t+1}.txt"' + Highinp['RefHt'] = HubHt_ + Highinp['URef'] = Vhub_ + Highinp['PLExp'] = shear_ + #Highinp['latitude'] = latitude + Highinp['InCDec1'] = Highinp['InCDec2'] = Highinp['InCDec3'] = f'"{a} {b/(8.1*Lambda1):.8f}"' + if writeFiles: + Highinp.write( os.path.join(seedPath, f'HighT{t+1}.inp') ) + + # Let's remove the original file + os.remove(os.path.join(seedPath, f'HighT{t+1}_stillToBeModified.inp')) + + + def TS_high_slurm_prepare(self, slurmfilepath): + # --------------------------------------------------- + # ----- Prepare SLURM script for High-res boxes ----- + # --------------------------------------------------- + + if not os.path.isfile(slurmfilepath): + raise ValueError (f'SLURM script for high-res box {slurmfilepath} does not exist.') + self.slurmfilename_high = os.path.basename(slurmfilepath) + + ntasks = self.nConditions*self.nHighBoxCases*self.nSeeds*self.nTurbines + status = os.system(f'cp {slurmfilepath} {self.path}/{self.slurmfilename_high}') + + # Change job name (for convenience only) + _ = subprocess.call(f"sed -i 's|#SBATCH --job-name=highBox|#SBATCH --job-name=highBox_{os.path.basename(self.path)}|g' {self.slurmfilename_high}", cwd=self.path, shell=True) + # Change the path inside the script to the desired one + sed_command = f"sed -i 's|/projects/shellwind/rthedin/Task2_2regis|{self.path}|g' {self.slurmfilename_high}" + _ = subprocess.call(sed_command, cwd=self.path, shell=True) + # Change number of turbines + _ = subprocess.call(f"sed -i 's|nTurbines=12|nTurbines={self.nTurbines}|g' {self.slurmfilename_high}", cwd=self.path, shell=True) + # Change number of nodes values + _ = subprocess.call(f"sed -i 's|#SBATCH --nodes=3|#SBATCH --nodes={int(np.ceil(ntasks/36))}|g' {self.slurmfilename_high}", cwd=self.path, shell=True) + # Assemble list of conditions and write it + listtoprint = "' '".join(self.condDirList) + sed_command = f"""sed -i "s|^condList.*|condList=('{listtoprint}')|g" {self.slurmfilename_high}""" + _ = subprocess.call(sed_command, cwd=self.path, shell=True) + # Assemble list of cases and write it + highBoxesCaseDirList = [self.caseDirList[c] for c in self.allHighBoxCases.case.values] + listtoprint = "' '".join(highBoxesCaseDirList) + sed_command = f"""sed -i "s|^caseList.*|caseList=('{listtoprint}')|g" {self.slurmfilename_high}""" + _ = subprocess.call(sed_command, cwd=self.path, shell=True) + + + + def TS_high_slurm_submit(self): + # ---------------------------------- + # ----- Run turbSim High boxes ----- + # ---------------------------------- + # Submit the script to SLURM + _ = subprocess.call('sbatch {self.slurmfilename_high}', cwd=self.path, shell=True) + + + def TS_high_create_symlink(self): + # Create symlink of all the high boxes for the cases with wake steering and yaw misalignment. These are the "repeated" boxes + + notepath = os.getcwd() + os.chdir(self.path) + for cond in range(self.nConditions): + for t in range(self.nTurbines): + for seed in range(self.nSeeds): + for case in range(self.nCases): + # Let's check if the current case is source (has bts) or destination (needs a symlink to bts) + varsToDrop = ['wakeSteering','misalignment','yawmis','yaw','yawCase','ADmodel','EDmodel','nFullAeroDyn','nFulllElastoDyn'] + if case in self.allHighBoxCases['case']: + src = os.path.join('../../../..', self.condDirList[cond], self.caseDirList[case], f'Seed_{seed}', 'TurbSim', f'HighT{t+1}.bts') + xr_src = self.allCases.sel(case=case, drop=True).drop_vars(varsToDrop) + continue + else: + dst = os.path.join(self.condDirList[cond], self.caseDirList[case], f'Seed_{seed}', 'TurbSim', f'HighT{t+1}.bts') + xr_dst = self.allCases.sel(case=case, drop=True).drop_vars(varsToDrop) + + # Let's make sure the src and destination are the same case, except wake steering and yaw misalign bools + xr.testing.assert_equal(xr_src, xr_dst) + + try: + os.symlink(src, dst) + except FileExistsError: + if self.verbose>1: print(f'File {dst} already exists. Skipping symlink.') + os.chdir(notepath) + + + + def FF_setup(self, outlistFF=None, **kwargs): + ''' + + **kwargs: + seedsToKeep: int + For the LES setup. Often 1, but if you want to run multiple times the same thing, pick a different value + ''' + + if outlistFF is None: + # Output list for FAST.Farm runs. Use 1 at the end for turbines (they will be replicated for all turbines). Combination of sample input file and KS's input + outlistFF = [ + "RtAxsXT1 , RtAxsYT1 , RtAxsZT1", + "RtPosXT1 , RtPosYT1 , RtPosZT1", + "RtDiamT1", + "YawErrT1", + "TIAmbT1", + 'RtVAmbT1', + 'RtVRelT1', + 'W1VAmbX, W1VAmbY, W1VAmbZ', + "W1VDisX, W1VDisY, W1VDisZ", + "CtT1N01 , CtT1N02 , CtT1N03 , CtT1N04 , CtT1N05 , CtT1N06 , CtT1N07 , CtT1N08 , CtT1N09 , CtT1N10 , CtT1N11 , CtT1N12 , CtT1N13 , CtT1N14 , CtT1N15 , CtT1N16 , CtT1N17 , CtT1N18 , CtT1N19 , CtT1N20", + # "WkAxsXT1D1 , WkAxsXT1D2 , WkAxsXT1D3 , WkAxsXT1D4 , WkAxsXT1D5 , WkAxsXT1D6 , WkAxsXT1D7", + # "WkAxsYT1D1 , WkAxsYT1D2 , WkAxsYT1D3 , WkAxsYT1D4 , WkAxsYT1D5 , WkAxsYT1D6 , WkAxsYT1D7", + # "WkAxsZT1D1 , WkAxsZT1D2 , WkAxsZT1D3 , WkAxsZT1D4 , WkAxsZT1D5 , WkAxsZT1D6 , WkAxsZT1D7", + # "WkPosXT1D1 , WkPosXT1D2 , WkPosXT1D3 , WkPosXT1D4 , WkPosXT1D5 , WkPosXT1D6 , WkPosXT1D7", + # "WkPosYT1D1 , WkPosYT1D2 , WkPosYT1D3 , WkPosYT1D4 , WkPosYT1D5 , WkPosYT1D6 , WkPosYT1D7", + # "WkPosZT1D1 , WkPosZT1D2 , WkPosZT1D3 , WkPosZT1D4 , WkPosZT1D5 , WkPosZT1D6 , WkPosZT1D7", + # "WkDfVxT1N01D1, WkDfVxT1N02D1, WkDfVxT1N03D1, WkDfVxT1N04D1, WkDfVxT1N05D1, WkDfVxT1N06D1, WkDfVxT1N07D1, WkDfVxT1N08D1, WkDfVxT1N09D1, WkDfVxT1N10D1, WkDfVxT1N11D1, WkDfVxT1N12D1, WkDfVxT1N13D1, WkDfVxT1N14D1, WkDfVxT1N15D1, WkDfVxT1N16D1, WkDfVxT1N17D1, WkDfVxT1N18D1, WkDfVxT1N19D1, WkDfVxT1N20D1", + # "WkDfVxT1N01D2, WkDfVxT1N02D2, WkDfVxT1N03D2, WkDfVxT1N04D2, WkDfVxT1N05D2, WkDfVxT1N06D2, WkDfVxT1N07D2, WkDfVxT1N08D2, WkDfVxT1N09D2, WkDfVxT1N10D2, WkDfVxT1N11D2, WkDfVxT1N12D2, WkDfVxT1N13D2, WkDfVxT1N14D2, WkDfVxT1N15D2, WkDfVxT1N16D2, WkDfVxT1N17D2, WkDfVxT1N18D2, WkDfVxT1N19D2, WkDfVxT1N20D2", + # "WkDfVxT1N01D3, WkDfVxT1N02D3, WkDfVxT1N03D3, WkDfVxT1N04D3, WkDfVxT1N05D3, WkDfVxT1N06D3, WkDfVxT1N07D3, WkDfVxT1N08D3, WkDfVxT1N09D3, WkDfVxT1N10D3, WkDfVxT1N11D3, WkDfVxT1N12D3, WkDfVxT1N13D3, WkDfVxT1N14D3, WkDfVxT1N15D3, WkDfVxT1N16D3, WkDfVxT1N17D3, WkDfVxT1N18D3, WkDfVxT1N19D3, WkDfVxT1N20D3", + # "WkDfVxT1N01D4, WkDfVxT1N02D4, WkDfVxT1N03D4, WkDfVxT1N04D4, WkDfVxT1N05D4, WkDfVxT1N06D4, WkDfVxT1N07D4, WkDfVxT1N08D4, WkDfVxT1N09D4, WkDfVxT1N10D4, WkDfVxT1N11D4, WkDfVxT1N12D4, WkDfVxT1N13D4, WkDfVxT1N14D4, WkDfVxT1N15D4, WkDfVxT1N16D4, WkDfVxT1N17D4, WkDfVxT1N18D4, WkDfVxT1N19D4, WkDfVxT1N20D4", + # "WkDfVxT1N01D5, WkDfVxT1N02D5, WkDfVxT1N03D5, WkDfVxT1N04D5, WkDfVxT1N05D5, WkDfVxT1N06D5, WkDfVxT1N07D5, WkDfVxT1N08D5, WkDfVxT1N09D5, WkDfVxT1N10D5, WkDfVxT1N11D5, WkDfVxT1N12D5, WkDfVxT1N13D5, WkDfVxT1N14D5, WkDfVxT1N15D5, WkDfVxT1N16D5, WkDfVxT1N17D5, WkDfVxT1N18D5, WkDfVxT1N19D5, WkDfVxT1N20D5", + # "WkDfVxT1N01D6, WkDfVxT1N02D6, WkDfVxT1N03D6, WkDfVxT1N04D6, WkDfVxT1N05D6, WkDfVxT1N06D6, WkDfVxT1N07D6, WkDfVxT1N08D6, WkDfVxT1N09D6, WkDfVxT1N10D6, WkDfVxT1N11D6, WkDfVxT1N12D6, WkDfVxT1N13D6, WkDfVxT1N14D6, WkDfVxT1N15D6, WkDfVxT1N16D6, WkDfVxT1N17D6, WkDfVxT1N18D6, WkDfVxT1N19D6, WkDfVxT1N20D6", + # "WkDfVxT1N01D7, WkDfVxT1N02D7, WkDfVxT1N03D7, WkDfVxT1N04D7, WkDfVxT1N05D7, WkDfVxT1N06D7, WkDfVxT1N07D7, WkDfVxT1N08D7, WkDfVxT1N09D7, WkDfVxT1N10D7, WkDfVxT1N11D7, WkDfVxT1N12D7, WkDfVxT1N13D7, WkDfVxT1N14D7, WkDfVxT1N15D7, WkDfVxT1N16D7, WkDfVxT1N17D7, WkDfVxT1N18D7, WkDfVxT1N19D7, WkDfVxT1N20D7", + # "WkDfVrT1N01D1, WkDfVrT1N02D1, WkDfVrT1N03D1, WkDfVrT1N04D1, WkDfVrT1N05D1, WkDfVrT1N06D1, WkDfVrT1N07D1, WkDfVrT1N08D1, WkDfVrT1N09D1, WkDfVrT1N10D1, WkDfVrT1N11D1, WkDfVrT1N12D1, WkDfVrT1N13D1, WkDfVrT1N14D1, WkDfVrT1N15D1, WkDfVrT1N16D1, WkDfVrT1N17D1, WkDfVrT1N18D1, WkDfVrT1N19D1, WkDfVrT1N20D1", + # "WkDfVrT1N01D2, WkDfVrT1N02D2, WkDfVrT1N03D2, WkDfVrT1N04D2, WkDfVrT1N05D2, WkDfVrT1N06D2, WkDfVrT1N07D2, WkDfVrT1N08D2, WkDfVrT1N09D2, WkDfVrT1N10D2, WkDfVrT1N11D2, WkDfVrT1N12D2, WkDfVrT1N13D2, WkDfVrT1N14D2, WkDfVrT1N15D2, WkDfVrT1N16D2, WkDfVrT1N17D2, WkDfVrT1N18D2, WkDfVrT1N19D2, WkDfVrT1N20D2", + # "WkDfVrT1N01D3, WkDfVrT1N02D3, WkDfVrT1N03D3, WkDfVrT1N04D3, WkDfVrT1N05D3, WkDfVrT1N06D3, WkDfVrT1N07D3, WkDfVrT1N08D3, WkDfVrT1N09D3, WkDfVrT1N10D3, WkDfVrT1N11D3, WkDfVrT1N12D3, WkDfVrT1N13D3, WkDfVrT1N14D3, WkDfVrT1N15D3, WkDfVrT1N16D3, WkDfVrT1N17D3, WkDfVrT1N18D3, WkDfVrT1N19D3, WkDfVrT1N20D3", + # "WkDfVrT1N01D4, WkDfVrT1N02D4, WkDfVrT1N03D4, WkDfVrT1N04D4, WkDfVrT1N05D4, WkDfVrT1N06D4, WkDfVrT1N07D4, WkDfVrT1N08D4, WkDfVrT1N09D4, WkDfVrT1N10D4, WkDfVrT1N11D4, WkDfVrT1N12D4, WkDfVrT1N13D4, WkDfVrT1N14D4, WkDfVrT1N15D4, WkDfVrT1N16D4, WkDfVrT1N17D4, WkDfVrT1N18D4, WkDfVrT1N19D4, WkDfVrT1N20D4", + # "WkDfVrT1N01D5, WkDfVrT1N02D5, WkDfVrT1N03D5, WkDfVrT1N04D5, WkDfVrT1N05D5, WkDfVrT1N06D5, WkDfVrT1N07D5, WkDfVrT1N08D5, WkDfVrT1N09D5, WkDfVrT1N10D5, WkDfVrT1N11D5, WkDfVrT1N12D5, WkDfVrT1N13D5, WkDfVrT1N14D5, WkDfVrT1N15D5, WkDfVrT1N16D5, WkDfVrT1N17D5, WkDfVrT1N18D5, WkDfVrT1N19D5, WkDfVrT1N20D5", + # "WkDfVrT1N01D6, WkDfVrT1N02D6, WkDfVrT1N03D6, WkDfVrT1N04D6, WkDfVrT1N05D6, WkDfVrT1N06D6, WkDfVrT1N07D6, WkDfVrT1N08D6, WkDfVrT1N09D6, WkDfVrT1N10D6, WkDfVrT1N11D6, WkDfVrT1N12D6, WkDfVrT1N13D6, WkDfVrT1N14D6, WkDfVrT1N15D6, WkDfVrT1N16D6, WkDfVrT1N17D6, WkDfVrT1N18D6, WkDfVrT1N19D6, WkDfVrT1N20D6", + # "WkDfVrT1N01D7, WkDfVrT1N02D7, WkDfVrT1N03D7, WkDfVrT1N04D7, WkDfVrT1N05D7, WkDfVrT1N06D7, WkDfVrT1N07D7, WkDfVrT1N08D7, WkDfVrT1N09D7, WkDfVrT1N10D7, WkDfVrT1N11D7, WkDfVrT1N12D7, WkDfVrT1N13D7, WkDfVrT1N14D7, WkDfVrT1N15D7, WkDfVrT1N16D7, WkDfVrT1N17D7, WkDfVrT1N18D7, WkDfVrT1N19D7, WkDfVrT1N20D7", + ] + self.outlistFF = outlistFF + + + # Planes to save in FAST.Farm. We want the planes through the original farm, so let's get the position of the turbines at wdir=0 + alignedTurbs = self.allCases.where(self.allCases['inflow_deg']==0, drop=True).isel(case=0) + if self.inflowStr == 'TurbSim': + # Turbine location in TurbSim reference frame + xWT = alignedTurbs['Tx'].values + self.xoffset_turbsOrigin2TSOrigin + yWT = alignedTurbs['Ty'].values + self.yoffset_turbsOrigin2TSOrigin + elif self.inflowStr == 'LES': + # Turbine location in LES reference frame + xWT = alignedTurbs['Tx'].values + yWT = alignedTurbs['Ty'].values + + offset=50 + planes_xy = [self.zhub+self.zbot] + planes_yz = np.unique(xWT+offset) + planes_xz = np.unique(yWT) + + # Number of planes must be at most 9 + self.planes_xy = planes_xy[0:9] + self.planes_yz = planes_yz[0:9] + self.planes_xz = planes_xz[0:9] + + + if self.inflowStr == 'LES': + self._FF_setup_LES(**kwargs) + + elif self.inflowStr == 'TurbSim': + # We need to make sure the TurbSim boxes have been executed. Let's check the last line of the logfile + highboxlog_path = os.path.join(self.path, self.condDirList[0], self.caseDirList[0], 'Seed_0', 'TurbSim', 'log.hight1.seed0.txt') + if not os.path.isfile(highboxlog_path): + raise ValueError(f'All TurbSim boxes need to be completed before this step can be done.') + + with open(highboxlog_path) as f: + last = None + for last in (line for line in f if line.rstrip('\n')): pass + + if 'TurbSim terminated normally' not in last: + raise ValueError(f'All TurbSim boxes need to be completed before this step can be done.') + + self._FF_setup_TS(**kwargs) + + + def _FF_setup_LES(self, seedsToKeep=1): + + self.seedsToKeep = seedsToKeep + + # Clean unnecessary directories and files created by the general setup + for cond in range(self.nConditions): + for seed in range(self.nSeeds): + os.system(f"rm -rf {os.path.join(self.path,self.condDirList[cond],f'Seed_{seed}')}") + + for case in range(self.nCases): + #os.system(f"rm -rf {os.path.join(path,condDirList[cond],caseDirList[case], f'Seed_0','InflowWind.dat')}") # needs to exist + for seed in range(seedsToKeep,self.nSeeds): + os.system(f"rm -rf {os.path.join(self.path,self.condDirList[cond],self.caseDirList[case], f'Seed_{seed}')}") + + + + # Create symlinks for the processed-and-renamed vtk files + LESboxesDirName = 'LESboxes' + + for cond in range(self.nConditions): + for case in range(self.nCases): + for seed in range(self.seedsToKeep): + # Remove TurbSim dir and create LES boxes dir + _ = os.system(f"rm -rf {os.path.join(self.path,self.condDirList[cond],self.caseDirList[case], f'Seed_{seed}', 'TurbSim')}") + if not os.path.exists(os.path.join(self.path,self.condDirList[cond],self.caseDirList[case],f'Seed_{seed}',LESboxesDirName)): + os.makedirs(os.path.join(self.path,self.condDirList[cond],self.caseDirList[case],f'Seed_{seed}',LESboxesDirName)) + + # Low-res box + try: + src = os.path.join(self.LESpath, 'Low') + dst = os.path.join(self.path, self.condDirList[cond], self.caseDirList[case], f'Seed_{seed}', LESboxesDirName, 'Low') + os.symlink(src, dst) + except FileExistsError: + print(f'Directory {dst} already exists. Skipping symlink.') + + # High-res boxes + for t in range(self.nTurbines): + try: + src = os.path.join(self.LESpath, f"HighT{t+1}_inflow{str(self.allCases.sel(case=case).inflow_deg.values).replace('-','m')}deg") + dst = os.path.join(self.path,self.condDirList[cond], self.caseDirList[case], f'Seed_{seed}', LESboxesDirName, f'HighT{t+1}') + os.symlink(src, dst) + except FileExistsError: + print(f'Directory {dst} already exists. Skipping symlink.') + + # Make a link for Amb.t0.vtk pointing to Amb.t1.vtk + + + + # Loops on all conditions/cases and cases for FAST.Farm (following python-toolbox/pyFAST/fastfarm/examples/Ex2_FFarmInputSetup.py) + for cond in range(self.nConditions): + for case in range(self.nCases): + for seed in range(seedsToKeep): + seedPath = os.path.join(self.path, self.condDirList[cond], self.caseDirList[case], f'Seed_{seed}') + + # Recover case properties + D_ = self.allCases['D' ].max().values # Getting the maximum in case different turbines are present + Vhub_ = self.allCond.sel(cond=cond)['vhub' ].values + # Recover turbine properties (array of length nTurbines) + xWT = self.allCases.sel(case=case)['Tx'].values + yWT = self.allCases.sel(case=case)['Ty'].values + zWT = self.allCases.sel(case=case)['Tz'].values + + # --------------- FAST.Farm ----------------- # + templateFSTF = os.path.join(self.templatePath, self.FFfilename) + outputFSTF = os.path.join(seedPath, 'FFarm_mod.fstf') + + # Write the file (mostly for turbine locations here + writeFastFarm(outputFSTF, templateFSTF, xWT, yWT, zWT, FFTS=None, OutListT1=self.outlistFF, noLeadingZero=True) + + # Open saved file and change additional values manually or make sure we have the correct ones + ff_file = FASTInputFile(outputFSTF) + + # Open output file and change additional values manually or make sure we have the correct ones + ff_file['InflowFile'] = f'"../{self.IWfilename}"' + ff_file['Mod_AmbWind'] = 1 # 1: LES boxes; 2: single TurbSim; 3: multiple TurbSim + ff_file['TMax'] = self.tmax + + # LES-related parameters + ff_file['DT_Low-VTK'] = self.dt_low_les + ff_file['DT_High-VTK'] = self.dt_high_les + ff_file['WindFilePath'] = f'''"{os.path.join(seedPath, LESboxesDirName)}"''' + #if checkWindFiles: + # ff_file['ChkWndFiles'] = 'TRUE' + + # Super controller + ff_file['UseSC'] = False + ff_file['SC_FileName'] = '/path/to/SC_DLL.dll' + + # Wake dynamics + ff_file['dr'] = self.cmax + ff_file['NumRadii'] = int(np.ceil(3*D_/(2*self.cmax) + 1)) + ff_file['NumPlanes'] = int(np.ceil( 20*D_/(self.dt_low_les*Vhub_*(1-1/6)) ) ) + if self.path == '/projects/shellwind/rthedin/task_equinor_empire_LES_10': + # Custom 15D downstream distance for number of wake planes for large Equinor case + ff_file['NumPlanes'] = int(np.ceil( 15*D_/(self.dt_low_les*Vhub_*(1-1/6)) ) ) + + # Vizualization outputs + ff_file['WrDisWind'] = 'False' + ff_file['WrDisDT'] = ff_file['DT_Low-VTK']*10 # writeFastFarm sets this to be the same as DT_Low + ff_file['NOutDisWindXY'] = len(self.planes_xy) + ff_file['OutDisWindZ'] = ' '.join(map(str, self.planes_xy)) + ff_file['NOutDisWindYZ'] = len(self.planes_yz) + ff_file['OutDisWindX'] = ' '.join(map(str, self.planes_yz)) + ff_file['NOutDisWindXZ'] = len(self.planes_xz) + ff_file['OutDisWindY'] = ' '.join(map(str, self.planes_xz)) + + # Modify wake outputs + ff_file['NOutDist'] = 7 + ff_file['OutDist'] = ' '.join(map(str, [1,1.5,2,2.5,3,3.5,4]*D_)) + # Mofidy wind output + ff_file['NWindVel'] = 9 + ff_file['WindVelX'] = ' '.join(map(str, xWT[:9])) + ff_file['WindVelY'] = ' '.join(map(str, yWT[:9])) + ff_file['WindVelZ'] = ' '.join(map(str, zWT[:9])) + + ff_file.write(outputFSTF) + + # Update the number of seeds variable for the LES case + self.nSeeds = self.seedsToKeep + + + + def _FF_setup_TS(self): + + # Loops on all conditions/cases and cases for FAST.Farm (following python-toolbox/pyFAST/fastfarm/examples/Ex2_FFarmInputSetup.py) + for cond in range(self.nConditions): + for case in range(self.nCases): + for seed in range(self.nSeeds): + seedPath = os.path.join(self.path, self.condDirList[cond], self.caseDirList[case], f'Seed_{seed}') + + # Recover case properties + D_ = self.allCases['D' ].max().values # Getting the maximum in case different turbines are present + HubHt_ = self.allCases['zhub'].max().values # Getting the maximum in case different turbines are present + Vhub_ = self.allCond.sel(cond=cond)['vhub' ].values + shear_ = self.allCond.sel(cond=cond)['shear' ].values + tivalue_ = self.allCond.sel(cond=cond)['TIvalue'].values + # Recover turbine properties (array of length nTurbines) + xWT = self.allCases.sel(case=case)['Tx'].values + yWT = self.allCases.sel(case=case)['Ty'].values + zWT = self.allCases.sel(case=case)['Tz'].values + + # # Turbine location in TurbSim reference frame + xt = xWT + self.xoffset_turbsOrigin2TSOrigin + yt = yWT + self.yoffset_turbsOrigin2TSOrigin + + + # --------------- FAST.Farm ----------------- # + templateFSTF = os.path.join(self.templatePath, self.FFfilename) + outputFSTF = os.path.join(seedPath, 'FFarm_mod.fstf') + + # Open TurbSim outputs for the Low box and one High box (they are all of the same size) + lowbts = TurbSimFile(os.path.join(seedPath,'TurbSim', 'Low.bts')) + highbts = TurbSimFile(os.path.join(seedPath,'TurbSim', f'HighT1.bts')) + + # Get dictionary with all the D{X,Y,Z,t}, L{X,Y,Z,t}, N{X,Y,Z,t}, {X,Y,Z}0 + dt_low_desired = self.Cmeander*D_/(10*Vhub_) # will be made multiple of dT_High inside _getBoxesParamsForFF + d = self._getBoxesParamsForFF(lowbts, highbts, dt_low_desired, D_, HubHt_, xWT, yt) + + # Write the file + writeFastFarm(outputFSTF, templateFSTF, xWT, yt, zWT, d, OutListT1=self.outlistFF, noLeadingZero=True) + + + # Open saved file and change additional values manually or make sure we have the correct ones + ff_file = FASTInputFile(outputFSTF) + ff_file['InflowFile'] = f'"../{self.IWfilename}"' #!!!!!!!! this path is not filled. should it be? + #ff_file['DT']=1.0 + ff_file['Mod_AmbWind'] = 3 # 1: LES boxes; 2: single TurbSim; 3: multiple TurbSim + ff_file['TMax'] = self.tmax + + # Super controller + ff_file['UseSC'] = False + ff_file['SC_FileName'] = '/path/to/SC_DLL.dll' + + # Wake dynamics # !!!!!!!!!!!!!!!!!!!!!! are these values good? (Emmanuel's sample file had 3, 50, 80. KS had 5, 75, 80) + ff_file['dr'] = self.cmax + ff_file['NumRadii'] = int(np.ceil(3*D_/(2*self.cmax) + 1)) + ff_file['NumPlanes'] = int(np.ceil( 20*D_/(dt_low_desired*Vhub_*(1-1/6)) ) ) + + # Vizualization outputs + ff_file['WrDisWind'] = 'False' + ff_file['WrDisDT'] = ff_file['DT_Low']*10 # writeFastFarm sets this to be the same as DT_Low + ff_file['NOutDisWindXY'] = len(self.planes_xy) + ff_file['OutDisWindZ'] = ' '.join(map(str, self.planes_xy)) + ff_file['NOutDisWindYZ'] = len(self.planes_yz) + ff_file['OutDisWindX'] = ' '.join(map(str, self.planes_yz)) + ff_file['NOutDisWindXZ'] = len(self.planes_xz) + ff_file['OutDisWindY'] = ' '.join(map(str, self.planes_xz)) + + # Modify wake outputs + ff_file['NOutDist'] = 7 + ff_file['OutDist'] = ' '.join(map(str, [1,1.5,2,2.5,3,3.5,4]*D_)) + # Mofidy wind output # !!!!! JJ why only 9? + ff_file['NWindVel'] = 9 + ff_file['WindVelX'] = ' '.join(map(str, xWT[:9])) + ff_file['WindVelY'] = ' '.join(map(str, yWT[:9])) + ff_file['WindVelZ'] = ' '.join(map(str, zWT[:9])) + + ff_file.write(outputFSTF) + + return + + + + def _getMultipleOf(self, val, multipleof): + valmult = int(val/multipleof)*multipleof + return round(valmult, 4) + + + def _getBoxesParamsForFF(self, lowbts, highbts, dt_low_desired, D, HubHt, xWT, yt): + # Get mean wind speeds at the half height location (advection speed) + _, meanU_High = highbts.midValues() # !!!!!!!!!!!!!!!! JJ: does it make sense to get both? the meanu for low will be a lot higher than vhub, + _, meanU_Low = lowbts.midValues() # !!!!!!!!!!!!!!!! JJ: and the meanu for high can be a bit higher than vhub + + dT_High = np.round(highbts.dt, 4) # 0.3 + # dX_High can sometimes be too high. So get the closest to the cmax, but multiple of what should have been + dX_High = round(meanU_High*dT_High) + if self.verbose>1: + print(f'original dX_High is {dX_High}') + dX_High = round(self.cmax/dX_High) * dX_High + if self.verbose>1: + print(f'after adjusting to closes multiple of cmax, dX_High is {dX_High}') + dY_High = highbts.y[1] - highbts.y[0] + dZ_High = highbts.z[1] - highbts.z[0] + + + # ----- Low + dT_Low = self._getMultipleOf(dt_low_desired, multipleof=dT_High) + dX_Low = self._getMultipleOf(meanU_Low*dT_Low, multipleof=dX_High) # dy and dz high are 5. + dY_Low = lowbts.y[1] - lowbts.y[0] + dZ_Low = lowbts.z[1] - lowbts.z[0] + + LY_Low = lowbts.y[-1]-lowbts.y[0] + LZ_Low = lowbts.z[-1]-lowbts.z[0] + LT_Low = np.round(lowbts.t[-1]-lowbts.t[0], 4) + + X0_Low = np.floor( (min(xWT) - self.extent_low[0]*D ))# - dX_Low)) # # JJ!!!!!!! # removing EB's -dX_Low from the X0_Low specification + X0_Low = self._getMultipleOf(X0_Low, multipleof=dX_Low) + Y0_Low = np.floor( -LY_Low/2 ) # Starting on integer value for aesthetics + Z0_Low = lowbts.z[0] # we start at lowest to include tower + + XMax_Low = self._getMultipleOf(max(xWT) + self.extent_low[1]*D, multipleof=dX_Low) + LX_Low = XMax_Low-X0_Low + + nX_Low = int(np.ceil(LX_Low/dX_Low)+1) # plus 1 from the guidance + nY_Low = len(lowbts.y) # !!!!!!! JJ: different from what EB has + nZ_Low = len(lowbts.z) + + assert nY_Low == int(np.ceil(LY_Low/dY_Low)+1) + assert nZ_Low == int(np.ceil(LZ_Low/dZ_Low)+1) + assert (nY_Low-1)*dY_Low == LY_Low + assert (nZ_Low-1)*dZ_Low == LZ_Low + + + # ----- High + Z0_High = highbts.z[0] # we start at lowest to include tower + + LX_High = self.extent_high*D + LY_High = highbts.y[-1]-highbts.y[0] + LZ_High = highbts.z[-1]-highbts.z[0] + LT_High = np.round(highbts.t[-1]-highbts.t[0], 4) + # !!!!!!!!!!!!!!!!! JJ: EB had these below. Why is that? + #Max_High = HubHt+extent_high*D # !!!!!!!!!!!!!!!!!! actual hh or midpoint? + #LY_High = min(LY_Box, extent_YZ*D ) # Bounding to not exceed the box dimension + #LZ_High = min(LZ_Box, ZMax_High-Z0_High) # Bounding to not exceed the box dimension + + nX_High = int(np.ceil(LX_High/dX_High) + 1) # plus 1 from the guidance + nY_High = len(highbts.y) + nZ_High = len(highbts.z) + + assert nY_High == int(np.ceil(LY_High/dY_High)+1) + assert nZ_High == int(np.ceil(LZ_High/dZ_High)+1) + assert (nY_High-1)*dY_High == LY_High + assert (nZ_High-1)*dZ_High == LZ_High + + # --- High-res location per turbine + X0_desired = np.asarray(xWT)-LX_High/2 # high-res is centered on turbine location + Y0_desired = np.asarray(yt)-LY_High/2 # high-res is centered on turbine location + X0_High = X0_Low + np.floor((X0_desired-X0_Low)/dX_High)*dX_High + Y0_High = Y0_Low + np.floor((Y0_desired-Y0_Low)/dY_High)*dY_High + + if self.verbose>2: + print(f' Low Box \t\t High box ') + print(f'dT_Low: {dT_Low}\t\t dT_High: {dT_High}') + print(f'dX_Low: {dX_Low}\t\t dX_High: {dX_High}') + print(f'dY_Low: {dY_Low}\t\t dY_High: {dY_High}') + print(f'dZ_Low: {dZ_Low}\t\t dZ_High: {dZ_High}') + print(f'LX_Low: {LX_Low}\t\t LX_High: {LX_High}') + print(f'LY_Low: {LY_Low}\t\t LY_High: {LY_High}') + print(f'LZ_Low: {LZ_Low}\t\t LZ_High: {LZ_High}') + print(f'LT_Low: {LT_Low}\t\t LT_High: {LT_High}') + print(f'nX_Low: {nX_Low}\t\t nX_High: {nX_High}') + print(f'nY_Low: {nY_Low}\t\t nY_High: {nY_High}') + print(f'nZ_Low: {nZ_Low}\t\t nZ_High: {nZ_High}') + print(f'X0_Low: {X0_Low}\t\t X0_High: {X0_High}') + print(f'Y0_Low: {Y0_Low} \t Y0_High: {Y0_High}') + print(f'Z0_Low: {Z0_Low}\t\t Z0_High: {Z0_High}') + + + # Fill dictionary with all values + d = dict() + d['DT_Low'] = np.around(dT_Low ,4) + d['DT_High'] = np.around(dT_High,4) + d['NX_Low'] = nX_Low + d['NY_Low'] = nY_Low + d['NZ_Low'] = nZ_Low + d['X0_Low'] = np.around(X0_Low,4) + d['Y0_Low'] = np.around(Y0_Low,4) + d['Z0_Low'] = np.around(Z0_Low,4) + d['dX_Low'] = np.around(dX_Low,4) + d['dY_Low'] = np.around(dY_Low,4) + d['dZ_Low'] = np.around(dZ_Low,4) + d['NX_High'] = nX_High + d['NY_High'] = nY_High + d['NZ_High'] = nZ_High + # --- High extent info for turbine outputs + d['dX_High'] = np.around(dX_High,4) + d['dY_High'] = np.around(dY_High,4) + d['dZ_High'] = np.around(dZ_High,4) + d['X0_High'] = np.around(X0_High,4) + d['Y0_High'] = np.around(Y0_High,4) + d['Z0_High'] = np.around(Z0_High,4) + # --- Misc + d['U_mean_Low'] = meanU_Low + d['U_mean_High'] = meanU_High + + + # --- Sanity check: check that the high res is at "almost" an integer location + X_rel = (np.array(d['X0_High'])-d['X0_Low'])/d['dX_High'] + Y_rel = (np.array(d['Y0_High'])-d['Y0_Low'])/d['dY_High'] + dX = X_rel - np.round(X_rel) # Should be close to zero + dY = Y_rel - np.round(Y_rel) # Should be close to zero + + if any(abs(dX)>1e-3): + print('Deltas:',dX) + raise Exception('Some X0_High are not on an integer multiple of the high-res grid') + if any(abs(dY)>1e-3): + print('Deltas:',dY) + raise Exception('Some Y0_High are not on an integer multiple of the high-res grid') + + return d + + + + + + def FF_slurm_prepare(self, slurmfilepath): + # ---------------------------------------------- + # ----- Prepare SLURM script for FAST.Farm ----- + # ------------- ONE SCRIPT PER CASE ------------ + # ---------------------------------------------- + + if not os.path.isfile(slurmfilepath): + raise ValueError (f'SLURM script for FAST.Farm {slurmfilepath} does not exist.') + self.slurmfilename_ff = os.path.basename(slurmfilepath) + + + for cond in range(self.nConditions): + for case in range(self.nCases): + for seed in range(self.nSeeds): + + fname = f'runFASTFarm_cond{cond}_case{case}_seed{seed}.sh' + status = os.system(f'cp {slurmfilepath} {os.path.join(self.path,fname)}') + + # Change job name (for convenience only) + sed_command = f"sed -i 's|#SBATCH --job-name=runFF|#SBATCH --job-name=c{cond}_c{case}_s{seed}_runFF_{os.path.basename(self.path)}|g' {fname}" + _ = subprocess.call(sed_command, cwd=self.path, shell=True) + # Change logfile name (for convenience only) + sed_command = f"sed -i 's|#SBATCH --output log.fastfarm_c0_c0_seed0|#SBATCH --output log.fastfarm_c{cond}_c{case}_s{seed}|g' {fname}" + _ = subprocess.call(sed_command, cwd=self.path, shell=True) + # Change the fastfarm binary to be called + sed_command = f"""sed -i "s|^fastfarmbin.*|fastfarmbin='{self.ffbin}'|g" {fname}""" + _ = subprocess.call(sed_command, cwd=self.path, shell=True) + # Change the path inside the script to the desired one + sed_command = f"sed -i 's|/projects/shellwind/rthedin/Task2_2regis|{self.path}|g' {fname}" + _ = subprocess.call(sed_command, cwd=self.path, shell=True) + # Write condition + sed_command = f"""sed -i "s|^cond.*|cond='{self.condDirList[cond]}'|g" {fname}""" + _ = subprocess.call(sed_command, cwd=self.path, shell=True) + # Write case + sed_command = f"""sed -i "s|^case.*|case='{self.caseDirList[case]}'|g" {fname}""" + _ = subprocess.call(sed_command, cwd=self.path, shell=True) + # Write seed + sed_command = f"""sed -i "s|^seed.*|seed={seed}|g" {fname}""" + _ = subprocess.call(sed_command, cwd=self.path, shell=True) + + + + def FF_slurm_submit(self): + + # ---------------------------------- + # ---------- Run FAST.Farm --------- + # ------- ONE SCRIPT PER CASE ------ + # ---------------------------------- + import time + + for cond in range(self.nConditions): + for case in range(self.nCases): + for seed in range(self.nSeeds): + + # Submit the script to SLURM + fname = f'runFASTFarm_cond{cond}_case{case}_seed{seed}.sh' + sub_command = f"sbatch {fname}" + print(f'Calling: {sub_command}') + subprocess.call(sub_command, cwd=self.path, shell=True) + time.sleep(4) # Sometimes the same job gets submitted twice. This gets around it. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 505612915a86f00541376467d1b67bf3ec32c05a Mon Sep 17 00:00:00 2001 From: Regis Thedin Date: Wed, 11 Jan 2023 14:01:29 -0700 Subject: [PATCH 014/124] FASTFarm setup: Add check of dt for high and low boxes --- pyFAST/fastfarm/FASTFarmCaseCreation.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyFAST/fastfarm/FASTFarmCaseCreation.py b/pyFAST/fastfarm/FASTFarmCaseCreation.py index 247f1f3..33627ee 100644 --- a/pyFAST/fastfarm/FASTFarmCaseCreation.py +++ b/pyFAST/fastfarm/FASTFarmCaseCreation.py @@ -137,6 +137,8 @@ def _checkInputs(self): # Check the ds and dt for the high- and low-res boxes if not (np.array(self.extent_low)>=0).all(): raise ValueError(f'The array for low-res box extents should be given with positive values') + if self.dt_low_les%self.dt_high_les > 1e-14: + raise ValueError(f'The temporal resolution dT_Low should be a multiple of dT_High') if self.dt_low_les < self.dt_high_les: raise ValueError(f'The temporal resolution dT_High should not be greater than dT_Low on the LES side') if self.ds_low_les < self.ds_high_les: From 4eeb05111e40fde48944e9e5bb2d58f57388dfc4 Mon Sep 17 00:00:00 2001 From: Regis Thedin Date: Wed, 11 Jan 2023 14:02:46 -0700 Subject: [PATCH 015/124] FASTFarm setup: Add example --- .../examples/Ex3_FFarmCompleteSetup.py | 150 ++++++++++++++++++ pyFAST/fastfarm/examples/README.md | 2 + .../examples/SampleFiles/runAllHighBox.sh | 49 ++++++ .../examples/SampleFiles/runAllLowBox.sh | 49 ++++++ .../runFASTFarm_cond0_case0_seed0.sh | 42 +++++ .../template_HighT1_InflowXX_SeedY.inp | 74 +++++++++ .../template_Low_InflowXX_SeedY.inp | 74 +++++++++ 7 files changed, 440 insertions(+) create mode 100644 pyFAST/fastfarm/examples/Ex3_FFarmCompleteSetup.py create mode 100644 pyFAST/fastfarm/examples/SampleFiles/runAllHighBox.sh create mode 100644 pyFAST/fastfarm/examples/SampleFiles/runAllLowBox.sh create mode 100644 pyFAST/fastfarm/examples/SampleFiles/runFASTFarm_cond0_case0_seed0.sh create mode 100644 pyFAST/fastfarm/examples/SampleFiles/template_HighT1_InflowXX_SeedY.inp create mode 100644 pyFAST/fastfarm/examples/SampleFiles/template_Low_InflowXX_SeedY.inp diff --git a/pyFAST/fastfarm/examples/Ex3_FFarmCompleteSetup.py b/pyFAST/fastfarm/examples/Ex3_FFarmCompleteSetup.py new file mode 100644 index 0000000..517f6b2 --- /dev/null +++ b/pyFAST/fastfarm/examples/Ex3_FFarmCompleteSetup.py @@ -0,0 +1,150 @@ +""" +Setup a FAST.Farm suite of cases based on input parameters. + +The extent of the high res and low res domain are setup according to the guidelines: + https://openfast.readthedocs.io/en/dev/source/user/fast.farm/ModelGuidance.html + +NOTE: If driving FAST.Farm using TurbSim inflow, the resulting boxes are necessary to + build the final FAST.Farm case and are not provided as part of this repository. + If driving FAST.Farm using LES inflow, the VTK boxes are not necessary to exist. + +""" + +from pyFAST.fastfarm.FASTFarmCaseCreation import FFCaseCreation + +def main(): + + # ----------------------------------------------------------------------------- + # USER INPUT: Modify these + # ----------------------------------------------------------------------------- + + # ----------- Case absolute path + path = '/complete/path/of/your/case' + + # ----------- Wind farm + wts = { + 0 :{'x':0.0, 'y':0, 'z':0.0, 'D':250, 'zhub':150}, + 1 :{'x':1852.0, 'y':0, 'z':0.0, 'D':250, 'zhub':150}, + 2 :{'x':3704.0, 'y':0, 'z':0.0, 'D':250, 'zhub':150}, + 3 :{'x':5556.0, 'y':0, 'z':0.0, 'D':250, 'zhub':150}, + 4 :{'x':7408.0, 'y':0, 'z':0.0, 'D':250, 'zhub':150}, + 5 :{'x':1852.0, 'y':1852.0, 'z':0.0, 'D':250, 'zhub':150}, + 6 :{'x':3704.0, 'y':1852.0, 'z':0.0, 'D':250, 'zhub':150}, + 7 :{'x':5556.0, 'y':1852.0, 'z':0.0, 'D':250, 'zhub':150}, + 8 :{'x':7408.0, 'y':1852.0, 'z':0.0, 'D':250, 'zhub':150}, + 9 :{'x':3704.0, 'y':3704.0, 'z':0.0, 'D':250, 'zhub':150}, + 10:{'x':5556.0, 'y':3704.0, 'z':0.0, 'D':250, 'zhub':150}, + 11:{'x':7408.0, 'y':3704.0, 'z':0.0, 'D':250, 'zhub':150}, + } + refTurb_rot = 0 + + # ----------- General hard-coded parameters + cmax = 5 # maximum blade chord (m) + fmax = 10/6 # maximum excitation frequency (Hz) + Cmeander = 1.9 # Meandering constant (-) + + # ----------- Additional variables + tmax = 1800 + nSeeds = 6 + zbot = 1 + + # ----------- Desired sweeps + vhub = [10] + shear = [0.2] + TIvalue = [10] + inflow_deg = [0] + + # ----------- Turbine parameters + # Set the yaw of each turbine for wind dir. One row for each wind direction. + yaw_init = [ [0,0,0,0,0,0,0,0,0,0,0,0] ] + + # ----------- Low- and high-res boxes parameters + # Should match LES if comparisons are to be made; otherwise, set desired values + # High-res boxes settings + dt_high_les = 0.6 # sampling frequency of high-res files + ds_high_les = 10.0 # dx, dy, dz that you want these high-res files at + extent_high = 1.2 # high-res box extent in y and x for each turbine, in D. + # Low-res boxes settings + dt_low_les = 3 # sampling frequency of low-res files + ds_low_les = 20.0 # dx, dy, dz of low-res files + extent_low = [3, 8, 3, 3, 2] + + + # ----------- Execution parameters + ffbin = '/full/path/to/your/binary/.../bin/FAST.Farm' + + # ----------- LES parameters. This variable will dictate whether it is a TurbSim-driven or LES-driven case + LESpath = '/full/path/to/the/LES/case' + #LESpath = None # set as None if TurbSim-driven is desired + + + # ----------------------------------------------------------------------------- + # ----------- Template files + templatePath = '/full/path/where/template/files/are' + + # Put 'unused' to any input that is not applicable to your case + EDfilename = 'ElastoDyn.T' + SEDfilename = 'SimplifiedElastoDyn.T' + HDfilename = 'HydroDyn.dat' + SrvDfilename = 'ServoDyn.T' + ADfilename = 'AeroDyn.dat' + ADskfilename = 'AeroDisk.dat' + SubDfilename = 'SubDyn.dat' + IWfilename = 'InflowWind.dat' + BDfilepath = 'unused' + bladefilename = 'Blade.dat' + towerfilename = 'Tower.dat' + turbfilename = 'Model.T' + libdisconfilepath = '/full/path/to/controller/libdiscon.so' + controllerInputfilename = 'DISCON.IN' + coeffTablefilename = 'CpCtCq.csv' + FFfilename = 'Model_FFarm.fstf' + + # TurbSim setups + turbsimLowfilepath = './SampleFiles/template_Low_InflowXX_SeedY.inp' + turbsimHighfilepath = './SampleFiles/template_HighT1_InflowXX_SeedY.inp' + + # SLURM scripts + slurm_TS_high = './SampleFiles/runAllHighBox.sh' + slurm_TS_low = './SampleFiles/runAllLowBox.sh' + slurm_FF_single = './SampleFiles/runFASTFarm_cond0_case0_seed0.sh' + + + # ----------------------------------------------------------------------------- + # END OF USER INPUT + # ----------------------------------------------------------------------------- + + + # Initial setup + case = FFCaseCreation(path, wts, cmax, fmax, Cmeander, tmax, zbot, vhub, shear, + TIvalue, inflow_deg, dt_high_les, ds_high_les, extent_high, + dt_low_les, ds_low_les, extent_low, ffbin, LESpath=LESpath, + verbose=1) + + case.setTemplateFilename(templatePath, EDfilename, SEDfilename, HDfilename, SrvDfilename, ADfilename, + ADskfilename, SubDfilename, IWfilename, BDfilepath, bladefilename, towerfilename, + turbfilename, libdisconfilepath, controllerInputfilename, coeffTablefilename, + turbsimLowfilepath, turbsimHighfilepath, FFfilename) + + case.copyTurbineFilesForEachCase() + case.getDomainParameters() + + # TurbSim setup + if LESpath is None: + case.TS_low_setup() + case.TS_low_slurm_prepare(slurm_TS_low) + #case.TS_low_slurm_submit() + + case.TS_high_setup() + case.TS_high_slurm_prepare(slurm_TS_high) + #case.TS_high_slurm_submit() + + # Final setup + case.FF_setup() + case.FF_slurm_prepare(slurm_FF_single) + #case.FF_slurm_submit() + + +if __name__ == '__main__': + # This example cannot be fully run if TurbSim inflow is requested. + main() diff --git a/pyFAST/fastfarm/examples/README.md b/pyFAST/fastfarm/examples/README.md index 75aba50..51b5b5d 100644 --- a/pyFAST/fastfarm/examples/README.md +++ b/pyFAST/fastfarm/examples/README.md @@ -4,5 +4,7 @@ Example 1: Create a TurbSim input file for a FAST.Farm simulation, with proper b Example 2: Setup the high and low res parameters of FAST.Farm input using a TurbSim box +Example 3: Creation of complete setup of FAST.Farm, including TurbSim inputs for high- and low-res boxes. Combination of Examples 1 and 2 above. + NOTE: the interface of these scripts is still in a preliminary phase and might be updated in the future. diff --git a/pyFAST/fastfarm/examples/SampleFiles/runAllHighBox.sh b/pyFAST/fastfarm/examples/SampleFiles/runAllHighBox.sh new file mode 100644 index 0000000..0a36ced --- /dev/null +++ b/pyFAST/fastfarm/examples/SampleFiles/runAllHighBox.sh @@ -0,0 +1,49 @@ +#!/bin/bash +#SBATCH --job-name=highBox +#SBATCH --output log.highBox +#SBATCH --nodes=3 +#SBATCH --ntasks-per-node=36 +#SBATCH --time=4-00 +#SBATCH --account=car + +source $HOME/.bash_profile + +echo "Working directory is" $SLURM_SUBMIT_DIR +echo "Job name is" $SLURM_JOB_NAME +echo "Job ID is " $SLURM_JOBID +echo "Job took the following nodes (SLURM_NODELIST)" $SLURM_NODELIST +echo "Submit time is" $(squeue -u $USER -o '%30j %20V' | grep -e $SLURM_JOB_NAME | awk '{print $2}') +echo "Starting job at: " $(date) + +module purge +module use /nopt/nrel/apps/modules/centos77/modulefiles +module load mkl/2020.1.217 +module load comp-intel/2020.1.217 + +# ********************************** USER INPUT ********************************** # +turbsimbin='/home/rthedin/local/local_openfast_intelCompilers/bin/turbsim' +basepath='/projects/shellwind/rthedin/Task2_2regis' + +condList=('Cond00_v08.6_PL0.2_TI10' 'Cond01_v10.6_PL0.2_TI10' 'Cond02_v12.6_PL0.2_TI10') + +caseList=('Case00_wdirp00_WSfalse_YMfalse' 'Case01_wdirp00_WStrue_YMfalse') + +nSeeds=6 +nTurbines=12 +# ******************************************************************************** # + +for cond in ${condList[@]}; do + for case in ${caseList[@]}; do + for ((seed=0; seed<$nSeeds; seed++)); do + for ((t=1; t<=$nTurbines; t++)); do + dir=$(printf "%s/%s/%s/Seed_%01d/TurbSim" $basepath $cond $case $seed) + echo "Submitting $dir/HighT$t.inp" + srun -n1 -N1 --exclusive $turbsimbin $dir/HighT$t.inp > $dir/log.hight$t.seed$seed.txt 2>&1 & + done + done + done +done + +wait + +echo "Ending job at: " $(date) diff --git a/pyFAST/fastfarm/examples/SampleFiles/runAllLowBox.sh b/pyFAST/fastfarm/examples/SampleFiles/runAllLowBox.sh new file mode 100644 index 0000000..6576d41 --- /dev/null +++ b/pyFAST/fastfarm/examples/SampleFiles/runAllLowBox.sh @@ -0,0 +1,49 @@ +#!/bin/bash +#SBATCH --job-name=lowBox +#SBATCH --output log.lowBox +#SBATCH --nodes=2 +#SBATCH --ntasks-per-node=12 +#SBATCH --time=4-00 +#SBATCH --account=shellwind + +source $HOME/.bash_profile + +echo "Working directory is" $SLURM_SUBMIT_DIR +echo "Job name is" $SLURM_JOB_NAME +echo "Job ID is " $SLURM_JOBID +echo "Job took the following nodes (SLURM_NODELIST)" $SLURM_NODELIST +echo "Submit time is" $(squeue -u $USER -o '%30j %20V' | grep -e $SLURM_JOB_NAME | awk '{print $2}') +echo "Starting job at: " $(date) + +nodelist=`scontrol show hostname $SLURM_NODELIST` +nodelist=($nodelist) +echo "Formatted list of nodes is: $nodelist" + +module purge +module use /nopt/nrel/apps/modules/centos77/modulefiles +module load mkl/2020.1.217 +module load comp-intel/2020.1.217 + +# ********************************** USER INPUT ********************************** # +turbsimbin='/home/rthedin/local/local_openfast_intelCompilers/bin/turbsim' +basepath='/projects/shellwind/rthedin/Task2_2regis' + +condList=('Cond00_v08.6_PL0.2_TI10' 'Cond01_v10.6_PL0.2_TI10' 'Cond02_v12.6_PL0.2_TI10') + +nSeeds=6 +# ******************************************************************************** # + +nodeToUse=0 +for cond in ${condList[@]}; do + currNode=${nodelist[$nodeToUse]} + for((seed=0; seed<$nSeeds; seed++)); do + dir=$(printf "%s/%s/Seed_%01d" $basepath $cond $seed) + echo "Submitting $dir/Low.inp in node $currNode" + srun -n1 -N1 --exclusive --nodelist=$currNode $turbsimbin $dir/Low.inp > $dir/log.low.seed$seed.txt & + done + (( nodeToUse++ )) +done + +wait + +echo "Ending job at: " $(date) diff --git a/pyFAST/fastfarm/examples/SampleFiles/runFASTFarm_cond0_case0_seed0.sh b/pyFAST/fastfarm/examples/SampleFiles/runFASTFarm_cond0_case0_seed0.sh new file mode 100644 index 0000000..46ba147 --- /dev/null +++ b/pyFAST/fastfarm/examples/SampleFiles/runFASTFarm_cond0_case0_seed0.sh @@ -0,0 +1,42 @@ +#!/bin/bash +#SBATCH --job-name=runFF +#SBATCH --output log.fastfarm_c0_c0_seed0 +#SBATCH --nodes=1 +#SBATCH --ntasks-per-node=36 +#SBATCH --time=10-00 +#SBATCH --account=mmc + +source $HOME/.bash_profile + +echo "Working directory is" $SLURM_SUBMIT_DIR +echo "Job name is" $SLURM_JOB_NAME +echo "Job ID is " $SLURM_JOBID +echo "Job took the following nodes (SLURM_NODELIST)" $SLURM_NODELIST +echo "Submit time is" $(squeue -u $USER -o '%30j %20V' | grep -e $SLURM_JOB_NAME | awk '{print $2}') +echo "Starting job at: " $(date) + +nodelist=`scontrol show hostname $SLURM_NODELIST` +nodelist=($nodelist) +echo "Formatted list of nodes is: $nodelist" + +module purge +module use /nopt/nrel/apps/modules/centos77/modulefiles +module load mkl/2020.1.217 +module load comp-intel/2020.1.217 + +# ********************************** USER INPUT ********************************** # +fastfarmbin='/home/rthedin/local/local_openfast_intelCompilers_3.1.0_openmpPrints/bin/FAST.Farm' +basepath='/projects/shellwind/rthedin/Task2_2regis' + +cond='Cond00_v08.6_PL0.2_TI10' +case='Case00_wdirp00_WSfalse_YMfalse_12fED_12ADyn' + +seed=0 +# ******************************************************************************** # + +dir=$(printf "%s/%s/%s/Seed_%01d" $basepath $cond $case $seed) +cd $dir +echo "Submitting $dir/FFarm_mod.fstf" +$fastfarmbin $dir/FFarm_mod.fstf > $dir/log.fastfarm.seed$seed.txt 2>&1 + +echo "Ending job at: " $(date) diff --git a/pyFAST/fastfarm/examples/SampleFiles/template_HighT1_InflowXX_SeedY.inp b/pyFAST/fastfarm/examples/SampleFiles/template_HighT1_InflowXX_SeedY.inp new file mode 100644 index 0000000..66c01ca --- /dev/null +++ b/pyFAST/fastfarm/examples/SampleFiles/template_HighT1_InflowXX_SeedY.inp @@ -0,0 +1,74 @@ +--------TurbSim v2.00.* Input File------------------------ +for Certification Test #1 (Kaimal Spectrum, formatted FF files). +---------Runtime Options----------------------------------- +False Echo - Echo input data to .ech (flag) +1432403720 RandSeed1 - First random seed (-2147483648 to 2147483647) +RanLux RandSeed2 - Second random seed (-2147483648 to 2147483647) for intrinsic pRNG, or an alternative pRNG: "RanLux" or "RNSNLW" +False WrBHHTP - Output hub-height turbulence parameters in binary form? (Generates RootName.bin) +False WrFHHTP - Output hub-height turbulence parameters in formatted form? (Generates RootName.dat) +False WrADHH - Output hub-height time-series data in AeroDyn form? (Generates RootName.hh) +True WrADFF - Output full-field time-series data in TurbSim/AeroDyn form? (Generates RootName.bts) +False WrBLFF - Output full-field time-series data in BLADED/AeroDyn form? (Generates RootName.wnd) +False WrADTWR - Output tower time-series data? (Generates RootName.twr) +False WrFMTFF - Output full-field time-series data in formatted (readable) form? (Generates RootName.u, RootName.v, RootName.w) +False WrACT - Output coherent turbulence time steps in AeroDyn form? (Generates RootName.cts) +True Clockwise - Clockwise rotation looking downwind? (used only for full-field binary files - not necessary for AeroDyn) +0 ScaleIEC - Scale IEC turbulence models to exact target standard deviation? [0=no additional scaling; 1=use hub scale uniformly; 2=use individual scales] + +--------Turbine/Model Specifications----------------------- +59 NumGrid_Z - Vertical grid-point matrix dimension +60 NumGrid_Y - Horizontal grid-point matrix dimension +0.5000 TimeStep - Time step [seconds] +1750.00 AnalysisTime - Length of analysis time series [seconds] (program will add time if necessary: AnalysisTime = MAX(AnalysisTime, UsableTime+GridWidth/MeanHHWS) ) +"ALL" UsableTime - Usable length of output time series [seconds] (program will add GridWidth/MeanHHWS seconds unless UsableTime is "ALL") +150.000 HubHt - Hub height [m] (should be > 0.5*GridHeight) +290.000 GridHeight - Grid height [m] +295.000 GridWidth - Grid width [m] (should be >= 2*(RotorRadius+ShaftLength)) +0 VFlowAng - Vertical mean flow (uptilt) angle [degrees] +0 HFlowAng - Horizontal mean flow (skew) angle [degrees] + +--------Meteorological Boundary Conditions------------------- +"TIMESR" TurbModel - Turbulence model ("IECKAI","IECVKM","GP_LLJ","NWTCUP","SMOOTH","WF_UPW","WF_07D","WF_14D","TIDAL","API","IECKAI","TIMESR", or "NONE") +"USRTimeSeries_T1.txt" UserFile - Name of the file that contains inputs for user-defined spectra or time series inputs (used only for "IECKAI" and "TIMESR" models) +1 IECstandard - Number of IEC 61400-x standard (x=1,2, or 3 with optional 61400-1 edition number (i.e. "1-Ed2") ) +"D" IECturbc - IEC turbulence characteristic ("A", "B", "C" or the turbulence intensity in percent) ("KHTEST" option with NWTCUP model, not used for other models) +"NTM" IEC_WindType - IEC turbulence type ("NTM"=normal, "xETM"=extreme turbulence, "xEWM1"=extreme 1-year wind, "xEWM50"=extreme 50-year wind, where x=wind turbine class 1, 2, or 3) +"default" ETMc - IEC Extreme Turbulence Model "c" parameter [m/s] +"PL" WindProfileType - Velocity profile type ("LOG";"PL"=power law;"JET";"H2L"=Log law for TIDAL model;"API";"PL";"TS";"IEC"=PL on rotor disk, LOG elsewhere; or "default") +"PowerLaw_6ms02.dat" ProfileFile - Name of the file that contains input profiles for WindProfileType="USR" and/or TurbModel="USRVKM" [-] +148.840 RefHt - Height of the reference velocity (URef) [m] +6.600 URef - Mean (total) velocity at the reference height [m/s] (or "default" for JET velocity profile) [must be 1-hr mean for API model; otherwise is the mean over AnalysisTime seconds] +350 ZJetMax - Jet height [m] (used only for JET velocity profile, valid 70-490 m) +"0.100" PLExp - Power law exponent [-] (or "default") +"default" Z0 - Surface roughness length [m] (or "default") + +--------Non-IEC Meteorological Boundary Conditions------------ +"default" Latitude - Site latitude [degrees] (or "default") +0.05 RICH_NO - Gradient Richardson number [-] +"default" UStar - Friction or shear velocity [m/s] (or "default") +"default" ZI - Mixing layer depth [m] (or "default") +"default" PC_UW - Hub mean u'w' Reynolds stress [m^2/s^2] (or "default" or "none") +"default" PC_UV - Hub mean u'v' Reynolds stress [m^2/s^2] (or "default" or "none") +"default" PC_VW - Hub mean v'w' Reynolds stress [m^2/s^2] (or "default" or "none") + +--------Spatial Coherence Parameters---------------------------- +"IEC" SCMod1 - u-component coherence model ("GENERAL","IEC","API","NONE", or "default") +"IEC" SCMod2 - v-component coherence model ("GENERAL","IEC","NONE", or "default") +"IEC" SCMod3 - w-component coherence model ("GENERAL","IEC","NONE", or "default") +"12.0 0.0003527" InCDec1 - u-component coherence parameters for general or IEC models [-, m^-1] (e.g. "10.0 0.3e-3" in quotes) (or "default") +"12.0 0.001058" InCDec2 - v-component coherence parameters for general or IEC models [-, m^-1] (e.g. "10.0 0.3e-3" in quotes) (or "default") +"12.0 0.004329" InCDec3 - w-component coherence parameters for general or IEC models [-, m^-1] (e.g. "10.0 0.3e-3" in quotes) (or "default") +"0.0" CohExp - Coherence exponent for general model [-] (or "default") + +--------Coherent Turbulence Scaling Parameters------------------- +".\EventData" CTEventPath - Name of the path where event data files are located +"random" CTEventFile - Type of event files ("LES", "DNS", or "RANDOM") +true Randomize - Randomize the disturbance scale and locations? (true/false) +1 DistScl - Disturbance scale [-] (ratio of event dataset height to rotor disk). (Ignored when Randomize = true.) +0.5 CTLy - Fractional location of tower centerline from right [-] (looking downwind) to left side of the dataset. (Ignored when Randomize = true.) +0.5 CTLz - Fractional location of hub height from the bottom of the dataset. [-] (Ignored when Randomize = true.) +30 CTStartTime - Minimum start time for coherent structures in RootName.cts [seconds] + +==================================================== +! NOTE: Do not add or remove any lines in this file! +==================================================== diff --git a/pyFAST/fastfarm/examples/SampleFiles/template_Low_InflowXX_SeedY.inp b/pyFAST/fastfarm/examples/SampleFiles/template_Low_InflowXX_SeedY.inp new file mode 100644 index 0000000..3945ea8 --- /dev/null +++ b/pyFAST/fastfarm/examples/SampleFiles/template_Low_InflowXX_SeedY.inp @@ -0,0 +1,74 @@ +--------TurbSim v2.00.* Input File------------------------ +for Certification Test #1 (Kaimal Spectrum, formatted FF files). +---------Runtime Options----------------------------------- +False Echo - Echo input data to .ech (flag) +1432403720 RandSeed1 - First random seed (-2147483648 to 2147483647) +RanLux RandSeed2 - Second random seed (-2147483648 to 2147483647) for intrinsic pRNG, or an alternative pRNG: "RanLux" or "RNSNLW" +False WrBHHTP - Output hub-height turbulence parameters in binary form? (Generates RootName.bin) +False WrFHHTP - Output hub-height turbulence parameters in formatted form? (Generates RootName.dat) +False WrADHH - Output hub-height time-series data in AeroDyn form? (Generates RootName.hh) +True WrADFF - Output full-field time-series data in TurbSim/AeroDyn form? (Generates RootName.bts) +False WrBLFF - Output full-field time-series data in BLADED/AeroDyn form? (Generates RootName.wnd) +False WrADTWR - Output tower time-series data? (Generates RootName.twr) +False WrFMTFF - Output full-field time-series data in formatted (readable) form? (Generates RootName.u, RootName.v, RootName.w) +False WrACT - Output coherent turbulence time steps in AeroDyn form? (Generates RootName.cts) +True Clockwise - Clockwise rotation looking downwind? (used only for full-field binary files - not necessary for AeroDyn) +0 ScaleIEC - Scale IEC turbulence models to exact target standard deviation? [0=no additional scaling; 1=use hub scale uniformly; 2=use individual scales] + +--------Turbine/Model Specifications----------------------- +40 NumGrid_Z - Vertical grid-point matrix dimension +481 NumGrid_Y - Horizontal grid-point matrix dimension +0.5000 TimeStep - Time step [seconds] +1750.00 AnalysisTime - Length of analysis time series [seconds] (program will add time if necessary: AnalysisTime = MAX(AnalysisTime, UsableTime+GridWidth/MeanHHWS) ) +"ALL" UsableTime - Usable length of output time series [seconds] (program will add GridWidth/MeanHHWS seconds unless UsableTime is "ALL") +395.000 HubHt - Hub height [m] (should be > 0.5*GridHeight) +780.000 GridHeight - Grid height [m] +9600.000 GridWidth - Grid width [m] (should be >= 2*(RotorRadius+ShaftLength)) +0 VFlowAng - Vertical mean flow (uptilt) angle [degrees] +0 HFlowAng - Horizontal mean flow (skew) angle [degrees] + +--------Meteorological Boundary Conditions------------------- +"IECKAI" TurbModel - Turbulence model ("IECKAI","IECVKM","GP_LLJ","NWTCUP","SMOOTH","WF_UPW","WF_07D","WF_14D","TIDAL","API","USRINP","USRVKM","TIMESR", or "NONE") +"unused" UserFile - Name of the file that contains inputs for user-defined spectra or time series inputs (used only for "USRINP" and "TIMESR" models) +1 IECstandard - Number of IEC 61400-x standard (x=1,2, or 3 with optional 61400-1 edition number (i.e. "1-Ed2") ) +"D" IECturbc - IEC turbulence characteristic ("A", "B", "C" or the turbulence intensity in percent) ("KHTEST" option with NWTCUP model, not used for other models) +"NTM" IEC_WindType - IEC turbulence type ("NTM"=normal, "xETM"=extreme turbulence, "xEWM1"=extreme 1-year wind, "xEWM50"=extreme 50-year wind, where x=wind turbine class 1, 2, or 3) +"default" ETMc - IEC Extreme Turbulence Model "c" parameter [m/s] +"PL" WindProfileType - Velocity profile type ("LOG";"PL"=power law;"JET";"H2L"=Log law for TIDAL model;"API";"USR";"TS";"IEC"=PL on rotor disk, LOG elsewhere; or "default") +"PowerLaw_6ms02.dat" ProfileFile - Name of the file that contains input profiles for WindProfileType="USR" and/or TurbModel="USRVKM" [-] +148.840 RefHt - Height of the reference velocity (URef) [m] +6.600 URef - Mean (total) velocity at the reference height [m/s] (or "default" for JET velocity profile) [must be 1-hr mean for API model; otherwise is the mean over AnalysisTime seconds] +350 ZJetMax - Jet height [m] (used only for JET velocity profile, valid 70-490 m) +"0.100" PLExp - Power law exponent [-] (or "default") +"default" Z0 - Surface roughness length [m] (or "default") + +--------Non-IEC Meteorological Boundary Conditions------------ +"default" Latitude - Site latitude [degrees] (or "default") +0.05 RICH_NO - Gradient Richardson number [-] +"default" UStar - Friction or shear velocity [m/s] (or "default") +"default" ZI - Mixing layer depth [m] (or "default") +"default" PC_UW - Hub mean u'w' Reynolds stress [m^2/s^2] (or "default" or "none") +"default" PC_UV - Hub mean u'v' Reynolds stress [m^2/s^2] (or "default" or "none") +"default" PC_VW - Hub mean v'w' Reynolds stress [m^2/s^2] (or "default" or "none") + +--------Spatial Coherence Parameters---------------------------- +"IEC" SCMod1 - u-component coherence model ("GENERAL","IEC","API","NONE", or "default") +"IEC" SCMod2 - v-component coherence model ("GENERAL","IEC","NONE", or "default") +"IEC" SCMod3 - w-component coherence model ("GENERAL","IEC","NONE", or "default") +"12.0 0.0003527" InCDec1 - u-component coherence parameters for general or IEC models [-, m^-1] (e.g. "10.0 0.3e-3" in quotes) (or "default") +"12.0 0.001058" InCDec2 - v-component coherence parameters for general or IEC models [-, m^-1] (e.g. "10.0 0.3e-3" in quotes) (or "default") +"12.0 0.004329" InCDec3 - w-component coherence parameters for general or IEC models [-, m^-1] (e.g. "10.0 0.3e-3" in quotes) (or "default") +"0.0" CohExp - Coherence exponent for general model [-] (or "default") + +--------Coherent Turbulence Scaling Parameters------------------- +".\EventData" CTEventPath - Name of the path where event data files are located +"random" CTEventFile - Type of event files ("LES", "DNS", or "RANDOM") +true Randomize - Randomize the disturbance scale and locations? (true/false) +1 DistScl - Disturbance scale [-] (ratio of event dataset height to rotor disk). (Ignored when Randomize = true.) +0.5 CTLy - Fractional location of tower centerline from right [-] (looking downwind) to left side of the dataset. (Ignored when Randomize = true.) +0.5 CTLz - Fractional location of hub height from the bottom of the dataset. [-] (Ignored when Randomize = true.) +30 CTStartTime - Minimum start time for coherent structures in RootName.cts [seconds] + +==================================================== +! NOTE: Do not add or remove any lines in this file! +==================================================== From d03cd83468f720d393fa673ff216ecd254306b81 Mon Sep 17 00:00:00 2001 From: Regis Thedin Date: Wed, 11 Jan 2023 17:55:50 -0700 Subject: [PATCH 016/124] Remove hard-coded setting --- pyFAST/fastfarm/FASTFarmCaseCreation.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/pyFAST/fastfarm/FASTFarmCaseCreation.py b/pyFAST/fastfarm/FASTFarmCaseCreation.py index 33627ee..0551752 100644 --- a/pyFAST/fastfarm/FASTFarmCaseCreation.py +++ b/pyFAST/fastfarm/FASTFarmCaseCreation.py @@ -1267,9 +1267,6 @@ def _FF_setup_LES(self, seedsToKeep=1): ff_file['dr'] = self.cmax ff_file['NumRadii'] = int(np.ceil(3*D_/(2*self.cmax) + 1)) ff_file['NumPlanes'] = int(np.ceil( 20*D_/(self.dt_low_les*Vhub_*(1-1/6)) ) ) - if self.path == '/projects/shellwind/rthedin/task_equinor_empire_LES_10': - # Custom 15D downstream distance for number of wake planes for large Equinor case - ff_file['NumPlanes'] = int(np.ceil( 15*D_/(self.dt_low_les*Vhub_*(1-1/6)) ) ) # Vizualization outputs ff_file['WrDisWind'] = 'False' From 4f019b598848a286cfb93f3d087ddbd47e9852b2 Mon Sep 17 00:00:00 2001 From: Regis Thedin Date: Thu, 12 Jan 2023 12:54:53 -0700 Subject: [PATCH 017/124] FASTFarm: Fix parsing of requested output files channels --- pyFAST/fastfarm/fastfarm.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pyFAST/fastfarm/fastfarm.py b/pyFAST/fastfarm/fastfarm.py index 6290158..8d60ceb 100644 --- a/pyFAST/fastfarm/fastfarm.py +++ b/pyFAST/fastfarm/fastfarm.py @@ -381,8 +381,12 @@ def setFastFarmOutputs(fastFarmFile, OutListT1): OutList=[''] for s in OutListT1: s=s.strip('"') - if s.find('T1'): + if 'T1' in s: OutList+=['"'+s.replace('T1','T{:d}'.format(iWT+1))+'"' for iWT in np.arange(nWTOut) ] + elif 'W1VAmb' in s: # special case for ambient wind + OutList+=['"'+s.replace('1','{:d}'.format(iWT+1))+'"' for iWT in np.arange(nWTOut) ] + elif 'W1VDis' in s: # special case for disturbed wind + OutList+=['"'+s.replace('1','{:d}'.format(iWT+1))+'"' for iWT in np.arange(nWTOut) ] else: OutList+='"'+s+'"' fst['OutList']=OutList From d72459ae35dfd1bff53da4429c6888934ffd7955 Mon Sep 17 00:00:00 2001 From: Regis Thedin Date: Mon, 23 Jan 2023 15:51:15 -0700 Subject: [PATCH 018/124] TS: Add manual specification of Z extent for low-res box --- pyFAST/fastfarm/TurbSimCaseCreation.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/pyFAST/fastfarm/TurbSimCaseCreation.py b/pyFAST/fastfarm/TurbSimCaseCreation.py index cb672d6..dba1033 100644 --- a/pyFAST/fastfarm/TurbSimCaseCreation.py +++ b/pyFAST/fastfarm/TurbSimCaseCreation.py @@ -155,12 +155,13 @@ def domainSize(self, zbot, manual_mode=False): if manual_mode: self.ymin = min(self.y) - self.low_ext[2]*self.D self.ymax = max(self.y) + self.low_ext[3]*self.D + Zdist_Low = self.RefHt + self.low_ext[4]*self.D else: - self.ymin = min(self.y)-2.23313*self.Cmeander*self.D/2 - self.ymax = max(self.y)+2.23313*self.Cmeander*self.D/2 + self.ymin = min(self.y)-2.23313*self.Cmeander*self.D/2 # JJ: I don't recall where these recommendations came from. I can't find them on the modelling guidance document + self.ymax = max(self.y)+2.23313*self.Cmeander*self.D/2 # JJ: I only see the y0_Low <= WT_Y_min -3*D recommendation + Zdist_Low = self.RefHt + self.D/2 + 2.23313*self.Cmeander*self.D/2 # JJ: ditto Ydist_Low = self.ymax - self.ymin - Zdist_Low = self.RefHt + self.D/2 + 2.23313*self.Cmeander*self.D/2 self.ny = np.ceil(Ydist_Low/self.dy)+1 self.nz = np.ceil(Zdist_Low/self.dz)+1 From 720f958e0f0106ebe8b19c07593862fdef9159c2 Mon Sep 17 00:00:00 2001 From: Regis Thedin Date: Tue, 24 Jan 2023 10:14:20 -0700 Subject: [PATCH 019/124] FF setup: adjust number of seeds on slurm scripts --- pyFAST/fastfarm/FASTFarmCaseCreation.py | 104 ++++++++++++++++++------ 1 file changed, 81 insertions(+), 23 deletions(-) diff --git a/pyFAST/fastfarm/FASTFarmCaseCreation.py b/pyFAST/fastfarm/FASTFarmCaseCreation.py index 0551752..49ca6d4 100644 --- a/pyFAST/fastfarm/FASTFarmCaseCreation.py +++ b/pyFAST/fastfarm/FASTFarmCaseCreation.py @@ -137,7 +137,7 @@ def _checkInputs(self): # Check the ds and dt for the high- and low-res boxes if not (np.array(self.extent_low)>=0).all(): raise ValueError(f'The array for low-res box extents should be given with positive values') - if self.dt_low_les%self.dt_high_les > 1e-14: + if self.dt_low_les%(self.dt_high_les-1e-15) > 1e-14: raise ValueError(f'The temporal resolution dT_Low should be a multiple of dT_High') if self.dt_low_les < self.dt_high_les: raise ValueError(f'The temporal resolution dT_High should not be greater than dT_Low on the LES side') @@ -169,7 +169,9 @@ def _checkInputs(self): raise ValueError(f'The number of turbines in wts ({len(self.wts)}) should match the number of turbines '\ f'in the ADmodel and EDmodel arrays ({np.shape(self.ADmodel)[1]})') - # Auxiliary variables and some checks + # Check on seed parameters + if not isinstance(self.nSeeds,int): + raise ValueError(f'An integer number of seeds should be requested. Got {self.nSeeds}.') if self.seedValues is None: self.seedValues = [2318573, 122299, 123456, 389432, -432443, 9849898] if len(self.seedValues) != self.nSeeds: @@ -397,17 +399,17 @@ def setTemplateFilename(self, templatePath=None, EDfilename=None, SEDfilename=No ''' - self.EDfilename = "unused" - self.SEDfilename = "unused" - self.HDfilename = "unused" - self.SrvDfilename = "unused" - self.ADfilename = "unused" - self.ADskfilename = "unused" - self.SubDfilename = "unused" - self.IWfilename = "unused" - self.BDfilepath = "unused" - self.bladefilename = "unused" - self.towerfilename = "unused" + self.EDfilename = "unused"; self.EDfilepath = "unused" + self.SEDfilename = "unused"; self.SEDfilepath = "unused" + self.HDfilename = "unused"; self.HDfilepath = "unused" + self.SrvDfilename = "unused"; self.SrvDfilepath = "unused" + self.ADfilename = "unused"; self.ADfilepath = "unused" + self.ADskfilename = "unused"; self.ADskfilepath = "unused" + self.SubDfilename = "unused"; self.SubDfilepath = "unused" + self.IWfilename = "unused"; self.IWfilepath = "unused" + self.BDfilepath = "unused"; self.BDfilename = "unused" + self.bladefilename = "unused"; self.bladefilepath = "unused" + self.towerfilename = "unused"; self.towerfilepath = "unused" if templatePath is None: @@ -579,13 +581,13 @@ def _check_and_open(f): if f != 'unused': return FASTInputFile(f) - self.ElastoDynFile = _check_and_open(self.EDfilepath) #FASTInputFile(os.path.join(self.templatePath, f'{ElastoDynFileName}.dat')) - self.SElastoDynFile = _check_and_open(self.SEDfilepath) #FASTInputFile(os.path.join(self.templatePath, f'{SElastoDynFileName}.dat')) - self.HydroDynFile = _check_and_open(self.HDfilepath) #FASTInputFile(os.path.join(self.templatePath, HydroDynFileName)) - self.ServoDynFile = _check_and_open(self.SrvDfilepath) #FASTInputFile(os.path.join(self.templatePath, f'{ServoDynFileName}.dat')) - self.AeroDiskFile = _check_and_open(self.ADskfilepath) #FASTInputFile(os.path.join(self.templatePath, AeroDiskFileName)) - self.turbineFile = _check_and_open(self.turbfilepath) #FASTInputFile(os.path.join(self.templatePath, f'{turbineFileName}.fst')) - self.InflowWindFile = _check_and_open(self.IWfilepath) #FASTInputFile(os.path.join(self.templatePath, InflowWindFileName)) + self.ElastoDynFile = _check_and_open(self.EDfilepath) + self.SElastoDynFile = _check_and_open(self.SEDfilepath) + self.HydroDynFile = _check_and_open(self.HDfilepath) + self.ServoDynFile = _check_and_open(self.SrvDfilepath) + self.AeroDiskFile = _check_and_open(self.ADskfilepath) + self.turbineFile = _check_and_open(self.turbfilepath) + self.InflowWindFile = _check_and_open(self.IWfilepath) @@ -807,6 +809,50 @@ def TS_low_setup(self, writeFiles=True, runOnce=False): def TS_low_slurm_prepare(self, slurmfilepath): + + + + # # -------------------------------------------------- + # # ----- Prepare SLURM script for Low-res boxes ----- + # # --------------- ONE SCRIPT PER CASE -------------- + # # -------------------------------------------------- + # + # if not os.path.isfile(slurmfilepath): + # raise ValueError (f'SLURM script for FAST.Farm {slurmfilepath} does not exist.') + # self.slurmfilename_ff = os.path.basename(slurmfilepath) + + + # for cond in range(self.nConditions): + # for case in range(self.nCases): + # for seed in range(self.nSeeds): + # + # fname = f'runFASTFarm_cond{cond}_case{case}_seed{seed}.sh' + # status = os.system(f'cp {slurmfilepath} {os.path.join(self.path,fname)}') + # + # # Change job name (for convenience only) + # sed_command = f"sed -i 's|#SBATCH --job-name=runFF|#SBATCH --job-name=c{cond}_c{case}_s{seed}_runFF_{os.path.basename(self.path)}|g' {fname}" + # _ = subprocess.call(sed_command, cwd=self.path, shell=True) + # # Change logfile name (for convenience only) + # sed_command = f"sed -i 's|#SBATCH --output log.fastfarm_c0_c0_seed0|#SBATCH --output log.fastfarm_c{cond}_c{case}_s{seed}|g' {fname}" + # _ = subprocess.call(sed_command, cwd=self.path, shell=True) + # # Change the fastfarm binary to be called + # sed_command = f"""sed -i "s|^fastfarmbin.*|fastfarmbin='{self.ffbin}'|g" {fname}""" + # _ = subprocess.call(sed_command, cwd=self.path, shell=True) + # # Change the path inside the script to the desired one + # sed_command = f"sed -i 's|/projects/shellwind/rthedin/Task2_2regis|{self.path}|g' {fname}" + # _ = subprocess.call(sed_command, cwd=self.path, shell=True) + # # Write condition + # sed_command = f"""sed -i "s|^cond.*|cond='{self.condDirList[cond]}'|g" {fname}""" + # _ = subprocess.call(sed_command, cwd=self.path, shell=True) + # # Write case + # sed_command = f"""sed -i "s|^case.*|case='{self.caseDirList[case]}'|g" {fname}""" + # _ = subprocess.call(sed_command, cwd=self.path, shell=True) + # # Write seed + # sed_command = f"""sed -i "s|^seed.*|seed={seed}|g" {fname}""" + # _ = subprocess.call(sed_command, cwd=self.path, shell=True) + + + # -------------------------------------------------- # ----- Prepare SLURM script for Low-res boxes ----- # -------------------------------------------------- @@ -829,6 +875,9 @@ def TS_low_slurm_prepare(self, slurmfilepath): listtoprint = "' '".join(self.condDirList) sed_command = f"""sed -i "s|^condList.*|condList=('{listtoprint}')|g" {self.slurmfilename_low}""" _ = subprocess.call(sed_command, cwd=self.path, shell=True) + # Change the number of seeds + _ = subprocess.call(f"sed -i 's|nSeeds=6|nSeeds={self.nSeeds}|g' {self.slurmfilename_low}", cwd=self.path, shell=True) + def TS_low_slurm_submit(self): @@ -966,6 +1015,11 @@ def TS_high_get_time_series(self): def TS_high_setup(self, writeFiles=True): + #todo: Check if the low-res boxes were created successfully + + # Create symbolic links for the low-res boxes + self.TS_low_createSymlinks() + # Loop on all conditions/cases/seeds setting up the High boxes boxType='highres' for cond in range(self.nConditions): @@ -1036,7 +1090,9 @@ def TS_high_slurm_prepare(self, slurmfilepath): sed_command = f"sed -i 's|/projects/shellwind/rthedin/Task2_2regis|{self.path}|g' {self.slurmfilename_high}" _ = subprocess.call(sed_command, cwd=self.path, shell=True) # Change number of turbines - _ = subprocess.call(f"sed -i 's|nTurbines=12|nTurbines={self.nTurbines}|g' {self.slurmfilename_high}", cwd=self.path, shell=True) + _ = subprocess.call(f"sed -i 's|nTurbines=12|nTurbines={self.nTurbines}|g' {self.slurmfilename_high}", cwd=self.path, shell=True) + # Change number of seeds + _ = subprocess.call(f"sed -i 's|nSeeds=6|nSeeds={self.nSeeds}|g' {self.slurmfilename_high}", cwd=self.path, shell=True) # Change number of nodes values _ = subprocess.call(f"sed -i 's|#SBATCH --nodes=3|#SBATCH --nodes={int(np.ceil(ntasks/36))}|g' {self.slurmfilename_high}", cwd=self.path, shell=True) # Assemble list of conditions and write it @@ -1102,11 +1158,13 @@ def FF_setup(self, outlistFF=None, **kwargs): outlistFF = [ "RtAxsXT1 , RtAxsYT1 , RtAxsZT1", "RtPosXT1 , RtPosYT1 , RtPosZT1", - "RtDiamT1", + #"RtDiamT1", "YawErrT1", "TIAmbT1", 'RtVAmbT1', 'RtVRelT1', + 'RtSkewT1', + 'RtCtAvgT1', 'W1VAmbX, W1VAmbY, W1VAmbZ', "W1VDisX, W1VDisY, W1VDisZ", "CtT1N01 , CtT1N02 , CtT1N03 , CtT1N04 , CtT1N05 , CtT1N06 , CtT1N07 , CtT1N08 , CtT1N09 , CtT1N10 , CtT1N11 , CtT1N12 , CtT1N13 , CtT1N14 , CtT1N15 , CtT1N16 , CtT1N17 , CtT1N18 , CtT1N19 , CtT1N20", @@ -1367,7 +1425,7 @@ def _FF_setup_TS(self): ff_file['NWindVel'] = 9 ff_file['WindVelX'] = ' '.join(map(str, xWT[:9])) ff_file['WindVelY'] = ' '.join(map(str, yWT[:9])) - ff_file['WindVelZ'] = ' '.join(map(str, zWT[:9])) + ff_file['WindVelZ'] = ' '.join(map(str, zWT[:9]+self.zhub)) ff_file.write(outputFSTF) From 88638ba70ece408df911776139ef11226d49644e Mon Sep 17 00:00:00 2001 From: Regis Thedin Date: Wed, 25 Jan 2023 14:27:53 -0700 Subject: [PATCH 020/124] FF: Fix logic on the sweep of paramters; minor clean-up --- pyFAST/fastfarm/FASTFarmCaseCreation.py | 52 +++++++++++++++++-------- 1 file changed, 36 insertions(+), 16 deletions(-) diff --git a/pyFAST/fastfarm/FASTFarmCaseCreation.py b/pyFAST/fastfarm/FASTFarmCaseCreation.py index 49ca6d4..b53525b 100644 --- a/pyFAST/fastfarm/FASTFarmCaseCreation.py +++ b/pyFAST/fastfarm/FASTFarmCaseCreation.py @@ -133,6 +133,10 @@ def _checkInputs(self): yaw = np.ones((1,self.nTurbines))*0 self.yaw_init = np.repeat(yaw, len(self.inflow_deg), axis=0) + # Check TI values if given in percent + for t in self.TIvalue: + if t<0: raise ValueError(f'TI cannot be negative. Received {t}.') + if t<1: raise ValueError(f'TI should be given in percentage (e.g. "10" for a 10% TI). Received {t}.') # Check the ds and dt for the high- and low-res boxes if not (np.array(self.extent_low)>=0).all(): @@ -271,9 +275,9 @@ def copyTurbineFilesForEachCase(self, writeFiles=True): # Update parameters to be changed in the *Dyn files self.HydroDynFile['WaveHs'] = self.bins.sel(wspd=Vhub_, method='nearest').WaveHs.values self.HydroDynFile['WaveTp'] = self.bins.sel(wspd=Vhub_, method='nearest').WaveTp.values - self.HydroDynFile['WvHiCOffD'] = 2.0*np.pi/self.HydroDynFile['WaveTp'] # !!!!!!!!!!!!!! check with JJ + self.HydroDynFile['WvHiCOffD'] = 2.0*np.pi/self.HydroDynFile['WaveTp'] # check with JJ self.HydroDynFile['WvLowCOffS'] = 2.0*np.pi/self.HydroDynFile['WaveTp'] - # !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! do i need to change the first 3 values of HD? can they be 'default'? JJ + # !!! JJ: do i need to change the first 3 values of HD? can they be 'default'? self.ElastoDynFile['RotSpeed'] = self.bins.sel(wspd=Vhub_, method='nearest').RotSpeed.values self.ElastoDynFile['BlPitch(1)'] = self.bins.sel(wspd=Vhub_, method='nearest').BlPitch.values @@ -605,17 +609,32 @@ def createAuxArrays(self): def _create_all_cond(self): - self.nConditions = len(self.vhub) * len(self.shear) * len(self.TIvalue) - - # Repeat arrays as necessary to build xarray Dataset - vhub_repeat = np.repeat(self.vhub, self.nConditions/len(self.vhub), axis=0) - shear_repeat = np.repeat(self.shear, self.nConditions/len(self.shear), axis=0) - TIvalue_repeat = np.repeat(self.TIvalue, self.nConditions/len(self.TIvalue), axis=0) + + if len(self.vhub)==len(self.shear) and len(self.shear)==len(self.TIvalue): + self.nConditions = len(self.vhub) + + if self.verbose>1: print(f'The length of vhub, shear, and TI are the same. Assuming each position is a condition.') + if self.verbose>0: print(f'Creating {self.nConditions} conditions') + + self.allCond = xr.Dataset({'vhub': (['cond'], self.vhub ), + 'shear': (['cond'], self.shear ), + 'TIvalue': (['cond'], self.TIvalue)}, + coords={'cond': np.arange(self.nConditions)} ) + + else: + import itertools + self.nConditions = len(self.vhub) * len(self.shear) * len(self.TIvalue) + + if self.verbose>1: print(f'The length of vhub, shear, and TI are different. Assuming sweep on each of them.') + if self.verbose>0: print(f'Creating {self.nConditions} conditions') - self.allCond = xr.Dataset({'vhub': (['cond'], vhub_repeat ), - 'shear': (['cond'], shear_repeat ), - 'TIvalue': (['cond'], TIvalue_repeat)}, - coords={'cond': np.arange(self.nConditions)} ) + # Repeat arrays as necessary to build xarray Dataset + combination = np.vstack(list(itertools.product(self.vhub,self.shear,self.TIvalue))) + + self.allCond = xr.Dataset({'vhub': (['cond'], combination[:,0]), + 'shear': (['cond'], combination[:,1]), + 'TIvalue': (['cond'], combination[:,2])}, + coords={'cond': np.arange(self.nConditions)} ) def _create_all_cases(self): @@ -662,7 +681,6 @@ def _create_all_cases(self): wakeSteering = np.tile([False], nWindDir*nCasesROmultiplier*nCasesYawmultiplier) misalignment = np.tile([False], nWindDir*nCasesROmultiplier*nCasesYawmultiplier) - # Create array of random numbers for yaw misalignment, and set it to zero where no yaw misalign is requested yawMisalignedValue = np.random.uniform(size = [nCases,self.nTurbines], low=-8.0, high=8.0) @@ -692,7 +710,7 @@ def _create_all_cases(self): 'zhub': (['case','turbine'], np.repeat(self.wts_rot_ds['zhub'].values, nCasesWSmultiplier*nCasesYMmultiplier*nCasesROmultiplier*nCasesYawmultiplier, axis=0)), 'yawmis': (['case','turbine'], yawMisalignedValue), 'yaw': (['case','turbine'], yawInit), - 'yawCase': (['case'], np.repeat(yawCase, nWindDir*nCasesWSmultiplier*nCasesYMmultiplier*nCasesROmultiplier)), # i didn't have ROM multiplier + 'yawCase': (['case'], np.repeat(yawCase, nWindDir*nCasesWSmultiplier*nCasesYMmultiplier*nCasesROmultiplier)), 'ADmodel': (['case','turbine'], np.tile(np.repeat(self.ADmodel, nCasesWSmultiplier*nCasesYMmultiplier*nCasesYawmultiplier, axis=0),(nWindDir,1)) ), 'EDmodel': (['case','turbine'], np.tile(np.repeat(self.EDmodel, nCasesWSmultiplier*nCasesYMmultiplier*nCasesYawmultiplier, axis=0),(nWindDir,1)) ), 'nFullAeroDyn': (['case'], np.repeat(np.tile(nADyn, nWindDir), nCasesWSmultiplier*nCasesYMmultiplier*nCasesYawmultiplier)), @@ -794,7 +812,7 @@ def TS_low_setup(self, writeFiles=True, runOnce=False): Lowinp = FASTInputFile(currentTSLowFile) Lowinp['RandSeed1'] = self.seedValues[seed] Lowinp['PLExp'] = shear_ - #Lowinp['latitude'] = latitude + #Lowinp['latitude'] = latitude # Not used when IECKAI model is selected. Lowinp['InCDec1'] = Lowinp['InCDec2'] = Lowinp['InCDec3'] = f'"{a} {b/(8.1*Lambda1):.8f}"' # The dt was computed for a proper low-res box but here we will want to compare with the high-res # and it is convenient to have the same time step. Let's do that change here @@ -879,6 +897,8 @@ def TS_low_slurm_prepare(self, slurmfilepath): _ = subprocess.call(f"sed -i 's|nSeeds=6|nSeeds={self.nSeeds}|g' {self.slurmfilename_low}", cwd=self.path, shell=True) + if self.nSeeds != 6: + print(f'--- WARNING: The memory-per-cpu on the low-res boxes SLURM script is configured for 6 seeds, not {self.nSeeds}.') def TS_low_slurm_submit(self): # --------------------------------- @@ -1063,7 +1083,7 @@ def TS_high_setup(self, writeFiles=True): Highinp['RefHt'] = HubHt_ Highinp['URef'] = Vhub_ Highinp['PLExp'] = shear_ - #Highinp['latitude'] = latitude + #Highinp['latitude'] = latitude # Not used when IECKAI model is selected. Highinp['InCDec1'] = Highinp['InCDec2'] = Highinp['InCDec3'] = f'"{a} {b/(8.1*Lambda1):.8f}"' if writeFiles: Highinp.write( os.path.join(seedPath, f'HighT{t+1}.inp') ) From 38e140b698dac1224c25c0390979903a4bf6861b Mon Sep 17 00:00:00 2001 From: Regis Thedin Date: Wed, 25 Jan 2023 14:34:03 -0700 Subject: [PATCH 021/124] Replace `format` with f-strings --- pyFAST/fastfarm/TurbSimCaseCreation.py | 154 ++++++++++++------------- 1 file changed, 77 insertions(+), 77 deletions(-) diff --git a/pyFAST/fastfarm/TurbSimCaseCreation.py b/pyFAST/fastfarm/TurbSimCaseCreation.py index dba1033..29350cb 100644 --- a/pyFAST/fastfarm/TurbSimCaseCreation.py +++ b/pyFAST/fastfarm/TurbSimCaseCreation.py @@ -222,7 +222,7 @@ def plotSetup(self, fig=None, ax=None): # high-res boxes for wt in range(len(self.x)): - ax.plot(self.x[wt],self.y[wt],'x',ms=8,mew=2,label="WT{0}".format(wt+1)) + ax.plot(self.x[wt],self.y[wt],'x',ms=8,mew=2,label=f"WT{wt+1}") # low-res box # ax.plot([xmin,xmax,xmax,xmin,xmin], @@ -265,92 +265,92 @@ def WriteTSFile(fileIn, fileOut, params, NewFile=True, tpath=None, tmax=50, turb print("WARNING: `turb` is not used when boxType is 'lowres'. Remove `turb` to dismiss this warning.") if NewFile == True: - if verbose>1: print('Writing a new {0} file from scratch'.format(fileOut)) + if verbose>1: print(f'Writing a new {fileOut} file from scratch') # --- Writing FFarm input file from scratch with open(fileOut, 'w') as f: - f.write('--------TurbSim v2.00.* Input File------------------------\n') - f.write('for Certification Test #1 (Kaimal Spectrum, formatted FF files).\n') - f.write('---------Runtime Options-----------------------------------\n') - f.write('False\tEcho\t\t- Echo input data to .ech (flag)\n') - f.write('123456\tRandSeed1\t\t- First random seed (-2147483648 to 2147483647)\n') - f.write('RanLux\tRandSeed2\t\t- Second random seed (-2147483648 to 2147483647) for intrinsic pRNG, or an alternative pRNG: "RanLux" or "RNSNLW"\n') - f.write('False\tWrBHHTP\t\t- Output hub-height turbulence parameters in binary form? (Generates RootName.bin)\n') - f.write('False\tWrFHHTP\t\t- Output hub-height turbulence parameters in formatted form? (Generates RootName.dat)\n') - f.write('False\tWrADHH\t\t- Output hub-height time-series data in AeroDyn form? (Generates RootName.hh)\n') - f.write('True\tWrADFF\t\t- Output full-field time-series data in TurbSim/AeroDyn form? (Generates RootName.bts)\n') - f.write('False\tWrBLFF\t\t- Output full-field time-series data in BLADED/AeroDyn form? (Generates RootName.wnd)\n') - f.write('False\tWrADTWR\t\t- Output tower time-series data? (Generates RootName.twr)\n') - f.write('False\tWrFMTFF\t\t- Output full-field time-series data in formatted (readable) form? (Generates RootName.u, RootName.v, RootName.w)\n') - f.write('False\tWrACT\t\t- Output coherent turbulence time steps in AeroDyn form? (Generates RootName.cts)\n') - f.write('True\tClockwise\t\t- Clockwise rotation looking downwind? (used only for full-field binary files - not necessary for AeroDyn)\n') - f.write('0\tScaleIEC\t\t- Scale IEC turbulence models to exact target standard deviation? [0=no additional scaling; 1=use hub scale uniformly; 2=use individual scales]\n') - f.write('\n') - f.write('--------Turbine/Model Specifications-----------------------\n') - f.write('{:.0f}\tNumGrid_Z\t\t- Vertical grid-point matrix dimension\n'.format(params.nz)) - f.write('{:.0f}\tNumGrid_Y\t\t- Horizontal grid-point matrix dimension\n'.format(params.ny)) - f.write('{:.6f}\tTimeStep\t\t- Time step [seconds]\n'.format(params.dt)) - f.write('{:.4f}\tAnalysisTime\t\t- Length of analysis time series [seconds] (program will add time if necessary: AnalysisTime = MAX(AnalysisTime, UsableTime+GridWidth/MeanHHWS) )\n'.format(tmax)) - f.write('"ALL"\tUsableTime\t\t- Usable length of output time series [seconds] (program will add GridWidth/MeanHHWS seconds unless UsableTime is "ALL")\n') - f.write('{:.3f}\tHubHt\t\t- Hub height [m] (should be > 0.5*GridHeight)\n'.format(params.HubHt_for_TS)) - f.write('{:.3f}\tGridHeight\t\t- Grid height [m]\n'.format(params.Height)) - f.write('{:.3f}\tGridWidth\t\t- Grid width [m] (should be >= 2*(RotorRadius+ShaftLength))\n'.format(params.Width)) - f.write('0\tVFlowAng\t\t- Vertical mean flow (uptilt) angle [degrees]\n') - f.write('0\tHFlowAng\t\t- Horizontal mean flow (skew) angle [degrees]\n') - f.write('\n') - f.write('--------Meteorological Boundary Conditions-------------------\n') + f.write(f'--------TurbSim v2.00.* Input File------------------------\n') + f.write(f'for Certification Test #1 (Kaimal Spectrum, formatted FF files).\n') + f.write(f'---------Runtime Options-----------------------------------\n') + f.write(f'False\tEcho\t\t- Echo input data to .ech (flag)\n') + f.write(f'123456\tRandSeed1\t\t- First random seed (-2147483648 to 2147483647)\n') + f.write(f'RanLux\tRandSeed2\t\t- Second random seed (-2147483648 to 2147483647) for intrinsic pRNG, or an alternative pRNG: "RanLux" or "RNSNLW"\n') + f.write(f'False\tWrBHHTP\t\t- Output hub-height turbulence parameters in binary form? (Generates RootName.bin)\n') + f.write(f'False\tWrFHHTP\t\t- Output hub-height turbulence parameters in formatted form? (Generates RootName.dat)\n') + f.write(f'False\tWrADHH\t\t- Output hub-height time-series data in AeroDyn form? (Generates RootName.hh)\n') + f.write(f'True\tWrADFF\t\t- Output full-field time-series data in TurbSim/AeroDyn form? (Generates RootName.bts)\n') + f.write(f'False\tWrBLFF\t\t- Output full-field time-series data in BLADED/AeroDyn form? (Generates RootName.wnd)\n') + f.write(f'False\tWrADTWR\t\t- Output tower time-series data? (Generates RootName.twr)\n') + f.write(f'False\tWrFMTFF\t\t- Output full-field time-series data in formatted (readable) form? (Generates RootName.u, RootName.v, RootName.w)\n') + f.write(f'False\tWrACT\t\t- Output coherent turbulence time steps in AeroDyn form? (Generates RootName.cts)\n') + f.write(f'True\tClockwise\t\t- Clockwise rotation looking downwind? (used only for full-field binary files - not necessary for AeroDyn)\n') + f.write(f'0\tScaleIEC\t\t- Scale IEC turbulence models to exact target standard deviation? [0=no additional scaling; 1=use hub scale uniformly; 2=use individual scales]\n') + f.write(f'\n') + f.write(f'--------Turbine/Model Specifications-----------------------\n') + f.write(f'{params.nz:.0f}\tNumGrid_Z\t\t- Vertical grid-point matrix dimension\n') + f.write(f'{params.ny:.0f}\tNumGrid_Y\t\t- Horizontal grid-point matrix dimension\n') + f.write(f'{params.dt:.6f}\tTimeStep\t\t- Time step [seconds]\n') + f.write(f'{tmax:.4f}\tAnalysisTime\t\t- Length of analysis time series [seconds] (program will add time if necessary: AnalysisTime = MAX(AnalysisTime, UsableTime+GridWidth/MeanHHWS) )\n') + f.write(f'"ALL"\tUsableTime\t\t- Usable length of output time series [seconds] (program will add GridWidth/MeanHHWS seconds unless UsableTime is "ALL")\n') + f.write(f'{params.HubHt_for_TS:.3f}\tHubHt\t\t- Hub height [m] (should be > 0.5*GridHeight)\n') + f.write(f'{params.Height:.3f}\tGridHeight\t\t- Grid height [m]\n') + f.write(f'{params.Width:.3f}\tGridWidth\t\t- Grid width [m] (should be >= 2*(RotorRadius+ShaftLength))\n') + f.write(f'0\tVFlowAng\t\t- Vertical mean flow (uptilt) angle [degrees]\n') + f.write(f'0\tHFlowAng\t\t- Horizontal mean flow (skew) angle [degrees]\n') + f.write(f'\n') + f.write(f'--------Meteorological Boundary Conditions-------------------\n') if params.boxType=='lowres': - f.write('"IECKAI"\tTurbModel\t\t- Turbulence model ("IECKAI","IECVKM","GP_LLJ","NWTCUP","SMOOTH","WF_UPW","WF_07D","WF_14D","TIDAL","API","IECKAI","TIMESR", or "NONE")\n') - f.write('"unused"\tUserFile\t\t- Name of the file that contains inputs for user-defined spectra or time series inputs (used only for "IECKAI" and "TIMESR" models)\n') + f.write(f'"IECKAI"\tTurbModel\t\t- Turbulence model ("IECKAI","IECVKM","GP_LLJ","NWTCUP","SMOOTH","WF_UPW","WF_07D","WF_14D","TIDAL","API","IECKAI","TIMESR", or "NONE")\n') + f.write(f'"unused"\tUserFile\t\t- Name of the file that contains inputs for user-defined spectra or time series inputs (used only for "IECKAI" and "TIMESR" models)\n') elif params.boxType=='highres': - f.write( '"TIMESR"\tTurbModel\t\t- Turbulence model ("IECKAI","IECVKM","GP_LLJ","NWTCUP","SMOOTH","WF_UPW","WF_07D","WF_14D","TIDAL","API","IECKAI","TIMESR", or "NONE")\n') + f.write(f'"TIMESR"\tTurbModel\t\t- Turbulence model ("IECKAI","IECVKM","GP_LLJ","NWTCUP","SMOOTH","WF_UPW","WF_07D","WF_14D","TIDAL","API","USRINP","TIMESR", or "NONE")\n') f.write(f'"USRTimeSeries_T{turb}.txt"\tUserFile\t\t- Name of the file that contains inputs for user-defined spectra or time series inputs (used only for "IECKAI" and "TIMESR" models)\n') else: raise ValueError("boxType can only be 'lowres' or 'highres'. Stopping.") - f.write('1\tIECstandard\t\t- Number of IEC 61400-x standard (x=1,2, or 3 with optional 61400-1 edition number (i.e. "1-Ed2") )\n') - f.write('"{:.3f}\t"\tIECturbc\t\t- IEC turbulence characteristic ("A", "B", "C" or the turbulence intensity in percent) ("KHTEST" option with NWTCUP model, not used for other models)\n'.format(params.TI)) - f.write('"NTM"\tIEC_WindType\t\t- IEC turbulence type ("NTM"=normal, "xETM"=extreme turbulence, "xEWM1"=extreme 1-year wind, "xEWM50"=extreme 50-year wind, where x=wind turbine class 1, 2, or 3)\n') - f.write('"default"\tETMc\t\t- IEC Extreme Turbulence Model "c" parameter [m/s]\n') - f.write('"PL"\tWindProfileType\t\t- Velocity profile type ("LOG";"PL"=power law;"JET";"H2L"=Log law for TIDAL model;"API";"PL";"TS";"IEC"=PL on rotor disk, LOG elsewhere; or "default")\n') - f.write('"PowerLaw_6ms02.dat"\tProfileFile\t\t- Name of the file that contains input profiles for WindProfileType="PL" and/or TurbModel="USRVKM" [-]\n') - f.write('{:.3f}\tRefHt\t\t- Height of the reference velocity (URef) [m]\n'.format(params.RefHt)) - f.write('{:.3f}\tURef\t\t- Mean (total) velocity at the reference height [m/s] (or "default" for JET velocity profile) [must be 1-hr mean for API model; otherwise is the mean over AnalysisTime seconds]\n'.format(params.URef)) - f.write('350\tZJetMax\t\t- Jet height [m] (used only for JET velocity profile, valid 70-490 m)\n') - f.write('"{:.3f}"\tPLExp\t\t- Power law exponent [-] (or "default")\n'.format(params.PLexp)) - f.write('"default"\tZ0\t\t- Surface roughness length [m] (or "default")\n') - f.write('\n') - f.write('--------Non-IEC Meteorological Boundary Conditions------------\n') - f.write('"default"\tLatitude\t\t- Site latitude [degrees] (or "default")\n') - f.write('0.05\tRICH_NO\t\t- Gradient Richardson number [-]\n') - f.write('"default"\tUStar\t\t- Friction or shear velocity [m/s] (or "default")\n') - f.write('"default"\tZI\t\t- Mixing layer depth [m] (or "default")\n') - f.write('"default"\tPC_UW\t\t- Hub mean u\'w\' Reynolds stress [m^2/s^2] (or "default" or "none")\n') - f.write('"default"\tPC_UV\t\t- Hub mean u\'v\' Reynolds stress [m^2/s^2] (or "default" or "none")\n') - f.write('"default"\tPC_VW\t\t- Hub mean v\'w\' Reynolds stress [m^2/s^2] (or "default" or "none")\n') - f.write('\n') - f.write('--------Spatial Coherence Parameters----------------------------\n') - f.write('"IEC"\tSCMod1\t\t- u-component coherence model ("GENERAL","IEC","API","NONE", or "default")\n') - f.write('"IEC"\tSCMod2\t\t- v-component coherence model ("GENERAL","IEC","NONE", or "default")\n') - f.write('"IEC"\tSCMod3\t\t- w-component coherence model ("GENERAL","IEC","NONE", or "default")\n') + f.write(f'1\tIECstandard\t\t- Number of IEC 61400-x standard (x=1,2, or 3 with optional 61400-1 edition number (i.e. "1-Ed2") )\n') + f.write(f'"{params.TI:.3f}\t"\tIECturbc\t\t- IEC turbulence characteristic ("A", "B", "C" or the turbulence intensity in percent) ("KHTEST" option with NWTCUP model, not used for other models)\n') + f.write(f'"NTM"\tIEC_WindType\t\t- IEC turbulence type ("NTM"=normal, "xETM"=extreme turbulence, "xEWM1"=extreme 1-year wind, "xEWM50"=extreme 50-year wind, where x=wind turbine class 1, 2, or 3)\n') + f.write(f'"default"\tETMc\t\t- IEC Extreme Turbulence Model "c" parameter [m/s]\n') + f.write(f'"PL"\tWindProfileType\t\t- Velocity profile type ("LOG";"PL"=power law;"JET";"H2L"=Log law for TIDAL model;"API";"PL";"TS";"IEC"=PL on rotor disk, LOG elsewhere; or "default")\n') + f.write(f'"unused"\tProfileFile\t\t- Name of the file that contains input profiles for WindProfileType="USR" and/or TurbModel="USRVKM" [-]\n') + f.write(f'{params.RefHt:.3f}\tRefHt\t\t- Height of the reference velocity (URef) [m]\n') + f.write(f'{params.URef:.3f}\tURef\t\t- Mean (total) velocity at the reference height [m/s] (or "default" for JET velocity profile) [must be 1-hr mean for API model; otherwise is the mean over AnalysisTime seconds]\n') + f.write(f'350\tZJetMax\t\t- Jet height [m] (used only for JET velocity profile, valid 70-490 m)\n') + f.write(f'"{params.PLexp:.3f}"\tPLExp\t\t- Power law exponent [-] (or "default")\n') + f.write(f'"default"\tZ0\t\t- Surface roughness length [m] (or "default")\n') + f.write(f'\n') + f.write(f'--------Non-IEC Meteorological Boundary Conditions------------\n') + f.write(f'"default"\tLatitude\t\t- Site latitude [degrees] (or "default")\n') + f.write(f'0.05\tRICH_NO\t\t- Gradient Richardson number [-]\n') + f.write(f'"default"\tUStar\t\t- Friction or shear velocity [m/s] (or "default")\n') + f.write(f'"default"\tZI\t\t- Mixing layer depth [m] (or "default")\n') + f.write(f'"default"\tPC_UW\t\t- Hub mean u\'w\' Reynolds stress [m^2/s^2] (or "default" or "none")\n') + f.write(f'"default"\tPC_UV\t\t- Hub mean u\'v\' Reynolds stress [m^2/s^2] (or "default" or "none")\n') + f.write(f'"default"\tPC_VW\t\t- Hub mean v\'w\' Reynolds stress [m^2/s^2] (or "default" or "none")\n') + f.write(f'\n') + f.write(f'--------Spatial Coherence Parameters----------------------------\n') + f.write(f'"IEC"\tSCMod1\t\t- u-component coherence model ("GENERAL","IEC","API","NONE", or "default")\n') + f.write(f'"IEC"\tSCMod2\t\t- v-component coherence model ("GENERAL","IEC","NONE", or "default")\n') + f.write(f'"IEC"\tSCMod3\t\t- w-component coherence model ("GENERAL","IEC","NONE", or "default")\n') f.write(f'"12.0 {0.12/(8.1*42):.8f}"\tInCDec1\t- u-component coherence parameters for general or IEC models [-, m^-1] (e.g. "10.0 0.3e-3" in quotes) (or "default")\n') f.write(f'"12.0 {0.12/(8.1*42):.8f}"\tInCDec2\t- v-component coherence parameters for general or IEC models [-, m^-1] (e.g. "10.0 0.3e-3" in quotes) (or "default")\n') f.write(f'"12.0 {0.12/(8.1*42):.8f}"\tInCDec3\t- w-component coherence parameters for general or IEC models [-, m^-1] (e.g. "10.0 0.3e-3" in quotes) (or "default")\n') - f.write('"0.0"\tCohExp\t\t- Coherence exponent for general model [-] (or "default")\n') - f.write('\n') - f.write('--------Coherent Turbulence Scaling Parameters-------------------\n') - f.write('".\EventData"\tCTEventPath\t\t- Name of the path where event data files are located\n') - f.write('"random"\tCTEventFile\t\t- Type of event files ("LES", "DNS", or "RANDOM")\n') - f.write('true\tRandomize\t\t- Randomize the disturbance scale and locations? (true/false)\n') - f.write('1\tDistScl\t\t- Disturbance scale [-] (ratio of event dataset height to rotor disk). (Ignored when Randomize = true.)\n') - f.write('0.5\tCTLy\t\t- Fractional location of tower centerline from right [-] (looking downwind) to left side of the dataset. (Ignored when Randomize = true.)\n') - f.write('0.5\tCTLz\t\t- Fractional location of hub height from the bottom of the dataset. [-] (Ignored when Randomize = true.)\n') - f.write('30\tCTStartTime\t\t- Minimum start time for coherent structures in RootName.cts [seconds]\n') - f.write('\n') - f.write('====================================================\n') - f.write('! NOTE: Do not add or remove any lines in this file!\n') - f.write('====================================================\n') + f.write(f'"0.0"\tCohExp\t\t- Coherence exponent for general model [-] (or "default")\n') + f.write(f'\n') + f.write(f'--------Coherent Turbulence Scaling Parameters-------------------\n') + f.write(f'".\EventData"\tCTEventPath\t\t- Name of the path where event data files are located\n') + f.write(f'"random"\tCTEventFile\t\t- Type of event files ("LES", "DNS", or "RANDOM")\n') + f.write(f'true\tRandomize\t\t- Randomize the disturbance scale and locations? (true/false)\n') + f.write(f'1\tDistScl\t\t- Disturbance scale [-] (ratio of event dataset height to rotor disk). (Ignored when Randomize = true.)\n') + f.write(f'0.5\tCTLy\t\t- Fractional location of tower centerline from right [-] (looking downwind) to left side of the dataset. (Ignored when Randomize = true.)\n') + f.write(f'0.5\tCTLz\t\t- Fractional location of hub height from the bottom of the dataset. [-] (Ignored when Randomize = true.)\n') + f.write(f'30\tCTStartTime\t\t- Minimum start time for coherent structures in RootName.cts [seconds]\n') + f.write(f'\n') + f.write(f'====================================================\n') + f.write(f'! NOTE: Do not add or remove any lines in this file!\n') + f.write(f'====================================================\n') else: - print('Modifying {0} to be {1}'.format(fileIn,fileOut)) + print(f'Modifying {fileIn} to be {fileOut}') NewPars = [int(params.nz), int(params.ny), int(params.dt), format(params.HubHt,'.2f'), format(params.Height,'.2f'), format(params.Width,'.2f'), format(params.TI,'.2f'), format(params.RefHt,'.2f'), format(params.URef,'.2f'), int(params.PLexp)] ModVars = ['NumGrid_Z','NumGrid_Y','TimeStep','HubHt','GridHeight','GridWidth','IECturb','RefHt','URef','PLExp'] @@ -363,7 +363,7 @@ def WriteTSFile(fileIn, fileOut, params, NewFile=True, tpath=None, tmax=50, turb if tmpVar in line: newline = str(NewPars[index])+'\t!!Orig is: '+line if '.fst' in line: - newline =str('{:.3f}\t\t{:.3f}\t\t{:.3f}\t\t{}_WT{:d}.fst"\t{:.3f}\t\t{:.3f}\t\t{:.3f}\t\t{:.3f}\t\t{:.3f}\t\t{:.3f}\n'.format(params.x[wt],params.y[wt],params.z[wt],tpath,wt+1,params.X0_High[wt],params.Y0_High[wt],params.Z0_High,params.dX_High,params.dY_High,params.dZ_High)) + newline =str('{params.x[wt]:.3f}\t\t{params.y[wt]:.3f}\t\t{params.z[wt]:.3f}\t\t{tpath}_WT{wt+1:d}.fst"\t{params.X0_High[wt]:.3f}\t\t{params.Y0_High[wt]:.3f}\t\t{params.Z0_High:.3f}\t\t{params.dX_High:.3f}\t\t{params.dY_High:.3f}\t\t{params.dZ_High:.3f}\n' wt+=1 new_file.write(newline) @@ -373,7 +373,7 @@ def writeTimeSeriesFile(fileOut,yloc,zloc,u,v,w,time): """ - print('Writing {0}'.format(fileOut)) + print(f'Writing {fileOut}') # --- Writing TurbSim user-defined time series file with open(fileOut, 'w') as f: f.write( '--------------TurbSim v2.00.* User Time Series Input File-----------------------\n') From 2ac6619374e48aeee0a6ff2d187b4296c0ffe7b7 Mon Sep 17 00:00:00 2001 From: Regis Thedin Date: Wed, 25 Jan 2023 15:52:34 -0700 Subject: [PATCH 022/124] Fix minor syntax bug --- pyFAST/fastfarm/TurbSimCaseCreation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyFAST/fastfarm/TurbSimCaseCreation.py b/pyFAST/fastfarm/TurbSimCaseCreation.py index 29350cb..cc9c57a 100644 --- a/pyFAST/fastfarm/TurbSimCaseCreation.py +++ b/pyFAST/fastfarm/TurbSimCaseCreation.py @@ -363,7 +363,7 @@ def WriteTSFile(fileIn, fileOut, params, NewFile=True, tpath=None, tmax=50, turb if tmpVar in line: newline = str(NewPars[index])+'\t!!Orig is: '+line if '.fst' in line: - newline =str('{params.x[wt]:.3f}\t\t{params.y[wt]:.3f}\t\t{params.z[wt]:.3f}\t\t{tpath}_WT{wt+1:d}.fst"\t{params.X0_High[wt]:.3f}\t\t{params.Y0_High[wt]:.3f}\t\t{params.Z0_High:.3f}\t\t{params.dX_High:.3f}\t\t{params.dY_High:.3f}\t\t{params.dZ_High:.3f}\n' + newline =str('{params.x[wt]:.3f}\t\t{params.y[wt]:.3f}\t\t{params.z[wt]:.3f}\t\t{tpath}_WT{wt+1:d}.fst"\t{params.X0_High[wt]:.3f}\t\t{params.Y0_High[wt]:.3f}\t\t{params.Z0_High:.3f}\t\t{params.dX_High:.3f}\t\t{params.dY_High:.3f}\t\t{params.dZ_High:.3f}\n') wt+=1 new_file.write(newline) From 16c2cd7e6d260c223969f38850eb09b6de5bca08 Mon Sep 17 00:00:00 2001 From: Regis Thedin Date: Fri, 27 Jan 2023 10:59:53 -0700 Subject: [PATCH 023/124] TS: add `toDataset` option for TurbSim files --- pyFAST/input_output/turbsim_file.py | 36 +++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/pyFAST/input_output/turbsim_file.py b/pyFAST/input_output/turbsim_file.py index c24d465..2d5d8c8 100644 --- a/pyFAST/input_output/turbsim_file.py +++ b/pyFAST/input_output/turbsim_file.py @@ -595,6 +595,42 @@ def __repr__(self): return s + def toDataSet(self, datetime=False): + import xarray as xr + + if datetime: + timearray = pd.to_datetime(self['t'], unit='s', origin=pd.to_datetime('2000-01-01 00:00:00')) + timestr = 'datetime' + else: + timearray = self['t'] + timestr = 'time' + + ds = xr.Dataset( + data_vars=dict( + u=([timestr,'y','z'], self['u'][0,:,:,:]), + v=([timestr,'y','z'], self['u'][1,:,:,:]), + w=([timestr,'y','z'], self['u'][2,:,:,:]), + ), + coords={ + timestr : timearray, + 'y' : self['y'], + 'z' : self['z'], + }, + ) + + # Add mean computations + ds['up'] = ds['u'] - ds['u'].mean(dim=timestr) + ds['vp'] = ds['v'] - ds['v'].mean(dim=timestr) + ds['wp'] = ds['w'] - ds['w'].mean(dim=timestr) + + if datetime: + # Add time (in s) to the variable list + ds['time'] = (('datetime'), self['t']) + + return ds + + + def toDataFrame(self): dfs={} From 608701cbd643e0bc0f3b4ca05c4d56ba46b5ad72 Mon Sep 17 00:00:00 2001 From: Regis Thedin Date: Wed, 8 Feb 2023 11:51:03 -0700 Subject: [PATCH 024/124] FF: Add placeholder for automatic computation of box parameters --- pyFAST/fastfarm/FASTFarmCaseCreation.py | 55 +++++++++++++++++++------ 1 file changed, 42 insertions(+), 13 deletions(-) diff --git a/pyFAST/fastfarm/FASTFarmCaseCreation.py b/pyFAST/fastfarm/FASTFarmCaseCreation.py index b53525b..17be35e 100644 --- a/pyFAST/fastfarm/FASTFarmCaseCreation.py +++ b/pyFAST/fastfarm/FASTFarmCaseCreation.py @@ -17,7 +17,7 @@ def sind(t): return np.sin(np.deg2rad(t)) class FFCaseCreation: - def __init__(self, path, wts, cmax, fmax, Cmeander, tmax, zbot, vhub, shear, TIvalue, inflow_deg, dt_high_les, ds_high_les, extent_high, dt_low_les, ds_low_les, extent_low, ffbin, yaw_init=None, ADmodel=None, EDmodel=None, nSeeds=6, LESpath=None, sweepWakeSteering=False, sweepYawMisalignment=False, seedValues=None, refTurb_rot=0, verbose=0): + def __init__(self, path, wts, cmax, fmax, Cmeander, tmax, zbot, vhub, shear, TIvalue, inflow_deg, dt_high_les=None, ds_high_les=None, extent_high=None, dt_low_les=None, ds_low_les=None, extent_low=None, ffbin=None, yaw_init=None, ADmodel=None, EDmodel=None, nSeeds=6, LESpath=None, sweepWakeSteering=False, sweepYawMisalignment=False, seedValues=None, refTurb_rot=0, verbose=0): ''' ffbin: str @@ -71,6 +71,11 @@ def __init__(self, path, wts, cmax, fmax, Cmeander, tmax, zbot, vhub, shear, TIv if self.verbose>0: print(f'Creating auxiliary arrays for all conditions and cases... Done.') + if self.verbose>0: print(f'Determining box parameters...', end='\r') + self._determineBoxParamters() + if self.verbose>0: print(f'Determining box paramters... Done.') + + if self.verbose>0: print(f'Creating directory structure and copying files...', end='\r') self._create_dir_structure() if self.verbose>0: print(f'Creating directory structure and copying files... Done.') @@ -139,21 +144,33 @@ def _checkInputs(self): if t<1: raise ValueError(f'TI should be given in percentage (e.g. "10" for a 10% TI). Received {t}.') # Check the ds and dt for the high- and low-res boxes - if not (np.array(self.extent_low)>=0).all(): - raise ValueError(f'The array for low-res box extents should be given with positive values') - if self.dt_low_les%(self.dt_high_les-1e-15) > 1e-14: - raise ValueError(f'The temporal resolution dT_Low should be a multiple of dT_High') - if self.dt_low_les < self.dt_high_les: - raise ValueError(f'The temporal resolution dT_High should not be greater than dT_Low on the LES side') - if self.ds_low_les < self.ds_high_les: - raise ValueError(f'The grid resolution dS_High should not be greater than dS_Low on the LES side') - if not isinstance(self.extent_high, (float,int)): - raise ValueError(f'The extent_high should be a scalar') - if self.extent_high<=0: - raise ValueError(f'The extent of high boxes should be positive') + if None not in (self.dt_high_les, self.dt_low_les, self.ds_high_les, self.ds_low_les, self.extent_high, self.extent_low): + # All values for ds, dt, and extent were given + if not (np.array(self.extent_low)>=0).all(): + raise ValueError(f'The array for low-res box extents should be given with positive values') + if self.dt_low_les%(self.dt_high_les-1e-15) > 1e-14: + raise ValueError(f'The temporal resolution dT_Low should be a multiple of dT_High') + if self.dt_low_les < self.dt_high_les: + raise ValueError(f'The temporal resolution dT_High should not be greater than dT_Low on the LES side') + if self.ds_low_les < self.ds_high_les: + raise ValueError(f'The grid resolution dS_High should not be greater than dS_Low on the LES side') + if not isinstance(self.extent_high, (float,int)): + raise ValueError(f'The extent_high should be a scalar') + if self.extent_high<=0: + raise ValueError(f'The extent of high boxes should be positive') + else: + # At least one of the ds, dt, extent are None. If that is the case, all of them should be None + if None in (self.dt_high_les, self.dt_low_les, self.ds_high_les, self.ds_low_les, self.extent_high, self.extent_low): + raise ValueError (f'The dt, ds, and extent for high and low res boxes need to either be fully' \ + f'given or computed. Some were given, but some were not.') # Check the FAST.Farm binary + if self.ffbin is None: + import shutil + self.ffbin = shutil.which('FAST.Farm') + if verbose>1: + print(f'FAST.Farm binary not given. Using the following from your $PATH: {self.ffbin}.') if not os.path.isfile(self.ffbin): raise ValueError (f'The FAST.Farm binary given does not appear to exist') @@ -747,6 +764,18 @@ def _rotate_wts(self): self.wts_rot_ds = pd.DataFrame.from_dict(wts_rot, orient='index').to_xarray().rename({'level_0':'inflow_deg','level_1':'turbine'}) + + def _determine_box_parameters(self): + + if self.dt_high_les is not None: + # Box paramters given. Only one check is needed since it passed `checkInputs` + return + + # todo: compute the boxes paraters + raise NotImplementedError(f'The ability to automatically determine the box paraters is not implemented yet.') + + + def _setRotorParameters(self): From 4887d092094f120710fd853674a0b91d5cd5a16e Mon Sep 17 00:00:00 2001 From: Regis Thedin Date: Wed, 8 Feb 2023 11:58:12 -0700 Subject: [PATCH 025/124] FF: Fix issue on high-res TurbSim setup --- pyFAST/fastfarm/FASTFarmCaseCreation.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/pyFAST/fastfarm/FASTFarmCaseCreation.py b/pyFAST/fastfarm/FASTFarmCaseCreation.py index 17be35e..63db165 100644 --- a/pyFAST/fastfarm/FASTFarmCaseCreation.py +++ b/pyFAST/fastfarm/FASTFarmCaseCreation.py @@ -775,6 +775,13 @@ def _determine_box_parameters(self): raise NotImplementedError(f'The ability to automatically determine the box paraters is not implemented yet.') + for cond in range(self.nConditions): + for case in range(self.nCases): + for t in range(self.nTurbines): + + + + def _setRotorParameters(self): @@ -1069,6 +1076,12 @@ def TS_high_setup(self, writeFiles=True): # Create symbolic links for the low-res boxes self.TS_low_createSymlinks() + # Get proper list of cases to loop (some can be repetead, e.g., ADyn/ADisk models) + self.getDomainParameters() + + # Open low-res and get time-series file + self.TS_high_get_time_series() + # Loop on all conditions/cases/seeds setting up the High boxes boxType='highres' for cond in range(self.nConditions): From 640814aaaecc0af56301b89d516c6ba2e2d9776a Mon Sep 17 00:00:00 2001 From: Regis Thedin Date: Wed, 8 Feb 2023 12:02:49 -0700 Subject: [PATCH 026/124] FF: Minor code reorganization --- pyFAST/fastfarm/FASTFarmCaseCreation.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/pyFAST/fastfarm/FASTFarmCaseCreation.py b/pyFAST/fastfarm/FASTFarmCaseCreation.py index 63db165..962d1d0 100644 --- a/pyFAST/fastfarm/FASTFarmCaseCreation.py +++ b/pyFAST/fastfarm/FASTFarmCaseCreation.py @@ -961,7 +961,7 @@ def TS_low_createSymlinks(self): os.chdir(notepath) - def getDomainParameters(self): + def _get_domain_parameters(self): # If the low box setup hasn't been called (e.g. LES run), do it once to get domain extents if not self.TSlowBoxFilesCreatedBool: @@ -980,6 +980,12 @@ def getDomainParameters(self): if self.nHighBoxCases != len(self.allHighBoxCases.case): raise ValueError(f'The number of cases do not match as expected. {self.nHighBoxCases} unique wind directions, but {len(self.allHighBoxCases.case)} unique cases.') + if self.verbose>2: + print(f'allHighBoxCases is:') + print(self.allHighBoxCases) + + def _get_offset_turbsOrigin2TSOrigin(self): + # Determine offsets from turbines coordinate frame to TurbSim coordinate frame self.yoffset_turbsOrigin2TSOrigin = -( (self.TSlowbox.ymax - self.TSlowbox.ymin)/2 + self.TSlowbox.ymin ) self.xoffset_turbsOrigin2TSOrigin = - self.extent_low[0]*self.D @@ -988,10 +994,6 @@ def getDomainParameters(self): print(f" The y offset between the turbine ref frame and turbsim is {self.yoffset_turbsOrigin2TSOrigin}") print(f" The x offset between the turbine ref frame and turbsim is {self.xoffset_turbsOrigin2TSOrigin}") - if self.verbose>2: - print(f'allHighBoxCases is:') - print(self.allHighBoxCases) - def TS_high_get_time_series(self): @@ -1077,7 +1079,10 @@ def TS_high_setup(self, writeFiles=True): self.TS_low_createSymlinks() # Get proper list of cases to loop (some can be repetead, e.g., ADyn/ADisk models) - self.getDomainParameters() + self._get_domain_parameters() + + # Get offset between given reference frame and TurbSim's + self._get_offset_turbsOrigin2TSOrigin() # Open low-res and get time-series file self.TS_high_get_time_series() From f7bff19bce049d996762480332205466065e6665 Mon Sep 17 00:00:00 2001 From: Regis Thedin Date: Wed, 8 Feb 2023 12:12:03 -0700 Subject: [PATCH 027/124] FF: Add placeholder to write AMR-Wind refinements --- pyFAST/fastfarm/FASTFarmCaseCreation.py | 35 +++++++++++++++++++++---- 1 file changed, 30 insertions(+), 5 deletions(-) diff --git a/pyFAST/fastfarm/FASTFarmCaseCreation.py b/pyFAST/fastfarm/FASTFarmCaseCreation.py index 962d1d0..57275a4 100644 --- a/pyFAST/fastfarm/FASTFarmCaseCreation.py +++ b/pyFAST/fastfarm/FASTFarmCaseCreation.py @@ -72,7 +72,7 @@ def __init__(self, path, wts, cmax, fmax, Cmeander, tmax, zbot, vhub, shear, TIv if self.verbose>0: print(f'Determining box parameters...', end='\r') - self._determineBoxParamters() + self.DetermineBoxParameters() if self.verbose>0: print(f'Determining box paramters... Done.') @@ -765,23 +765,48 @@ def _rotate_wts(self): self.wts_rot_ds = pd.DataFrame.from_dict(wts_rot, orient='index').to_xarray().rename({'level_0':'inflow_deg','level_1':'turbine'}) - def _determine_box_parameters(self): + def DetermineBoxParameters(self): if self.dt_high_les is not None: # Box paramters given. Only one check is needed since it passed `checkInputs` return + # todo: compute the boxes paraters raise NotImplementedError(f'The ability to automatically determine the box paraters is not implemented yet.') - for cond in range(self.nConditions): - for case in range(self.nCases): - for t in range(self.nTurbines): + + + + def writeAMRWindRefinement(self): + raise NotImplementedError(f'writeAMRWindRefinement is not implemented.') + # Get list of proper non-repeated cases + self._get_domain_parameters() + + # Loop over all turbines of all cases/conditions + for cond in range(self.nConditions): + for case in range(self.nHighBoxCases): + # Get actual case number given the high-box that need to be saved + case = self.allHighBoxCases.isel(case=case)['case'].values + if self.verbose>3: + print(f'Generating AMR-Wind input file for cond {cond} ({self.condDirList[cond]}),'\ + f'case {case} ({self.caseDirList[case]}).') + for t in range(self.nTurbines): + # Recover turbine properties + D_ = self.allCases.sel(case=case, turbine=t)['D' ].values + HubHt_ = self.allCases.sel(case=case, turbine=t)['zhub'].values + xloc_ = self.allCases.sel(case=case, turbine=t)['Tx' ].values + yloc_ = self.allCases.sel(case=case, turbine=t)['Ty' ].values + + # todo: maybe get information about your grid from amr-wind. You want to sample at the cell center + # todo: call another function to spit out the refinements in the proper syntax + + def _setRotorParameters(self): From 7d69fe29a5ed0b38b0e038d494ee9cfe730efa78 Mon Sep 17 00:00:00 2001 From: Emmanuel Branlard Date: Wed, 8 Feb 2023 21:35:43 -0700 Subject: [PATCH 028/124] Fatigue: update of interface for equivalent_load function --- .../examples/Example_EquivalentLoad.py | 7 +++--- pyFAST/tools/fatigue.py | 22 +++++++++++++------ 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/pyFAST/postpro/examples/Example_EquivalentLoad.py b/pyFAST/postpro/examples/Example_EquivalentLoad.py index 727c59d..40e2511 100644 --- a/pyFAST/postpro/examples/Example_EquivalentLoad.py +++ b/pyFAST/postpro/examples/Example_EquivalentLoad.py @@ -18,11 +18,10 @@ # Compute equivalent load for one signal and Wohler slope -T = df['Time_[s]'].values[-1] # number of 1Hz load cycles (time series length in second) -m = 10 # Wohler slope -Leq = equivalent_load(df['RootMyc1_[kN-m]'], Teq=T, m=1) +m = 1 # Wohler slope +Leq = equivalent_load(df['Time_[s]'], df['RootMyc1_[kN-m]'], m=m) print('Leq ',Leq) -# Leq = equivalent_load(df['RootMyc1_[kN-m]'], Teq=T, m=1, method='fatpack') # requires package fatpack +# Leq = equivalent_load(df['Time_[s]'], df['RootMyc1_[kN-m]'], m=m, method='fatpack') # requires package fatpack if __name__ == '__main__': diff --git a/pyFAST/tools/fatigue.py b/pyFAST/tools/fatigue.py index 3a254d8..2b0ada4 100644 --- a/pyFAST/tools/fatigue.py +++ b/pyFAST/tools/fatigue.py @@ -31,16 +31,17 @@ __all__ = ['rainflow_astm', 'rainflow_windap','eq_load','eq_load_and_cycles','cycle_matrix','cycle_matrix2'] -def equivalent_load(signal, m=3, Teq=1, nBins=46, method='rainflow_windap'): +def equivalent_load(time, signal, m=3, Teq=1, nBins=100, method='rainflow_windap'): """Equivalent load calculation Calculate the equivalent loads for a list of Wohler exponent Parameters ---------- - signals : array-like, the signal + time : array-like, the time values corresponding to the signal (s) + signals : array-like, the load signal m : Wohler exponent (default is 3) - Teq : The equivalent number of load cycles (default is 1, but normally the time duration in seconds is used) + Teq : The equivalent period (Default 1Hz) nBins : Number of bins in rainflow count histogram method: 'rainflow_windap, rainflow_astm, fatpack @@ -48,21 +49,28 @@ def equivalent_load(signal, m=3, Teq=1, nBins=46, method='rainflow_windap'): ------- Leq : the equivalent load for given m and Tea """ + time = np.asarray(time) signal = np.asarray(signal) + T = time[-1]-time[0] # time length of signal (s) + neq = T/Teq # number of equivalent periods rainflow_func_dict = {'rainflow_windap':rainflow_windap, 'rainflow_astm':rainflow_astm} if method in rainflow_func_dict.keys(): # Call wetb function for one m - Leq = eq_load(signal, m=[m], neq=Teq, no_bins=nBins, rainflow_func=rainflow_func_dict[method])[0][0] + Leq = eq_load(signal, m=[m], neq=neq, no_bins=nBins, rainflow_func=rainflow_func_dict[method])[0][0] elif method=='fatpack': import fatpack # find rainflow ranges - ranges = fatpack.find_rainflow_ranges(signal) + try: + ranges = fatpack.find_rainflow_ranges(signal) + except IndexError: + # Currently fails for constant signal + return np.nan # find range count and bin Nrf, Srf = fatpack.find_range_count(ranges, nBins) - # get DEL - DELs = Srf**m * Nrf / Teq + # get DEL + DELs = Srf**m * Nrf / neq Leq = DELs.sum() ** (1/m) else: From acb70777a7c7908e85336cffcdf3b1823e96928a Mon Sep 17 00:00:00 2001 From: Alex Rybchuk Date: Fri, 10 Feb 2023 20:09:00 -0700 Subject: [PATCH 029/124] Add notebook with reference code that will be added into scripts --- pyFAST/fastfarm/AMRWindSamplingCreation.ipynb | 399 ++++++++++++++++++ 1 file changed, 399 insertions(+) create mode 100644 pyFAST/fastfarm/AMRWindSamplingCreation.ipynb diff --git a/pyFAST/fastfarm/AMRWindSamplingCreation.ipynb b/pyFAST/fastfarm/AMRWindSamplingCreation.ipynb new file mode 100644 index 0000000..46560b2 --- /dev/null +++ b/pyFAST/fastfarm/AMRWindSamplingCreation.ipynb @@ -0,0 +1,399 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "id": "verbal-ceramic", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'\\nWrite out information that is useful for setting up AMR-Wind sampling planes for FAST.Farm,\\n following guidance from https://openfast.readthedocs.io/en/main/source/user/fast.farm/ModelGuidance.html\\n'" + ] + }, + "execution_count": 1, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "'''\n", + "Write out information that is useful for setting up AMR-Wind sampling planes for FAST.Farm,\n", + " following guidance from https://openfast.readthedocs.io/en/main/source/user/fast.farm/ModelGuidance.html\n", + "'''" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "excessive-roommate", + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np" + ] + }, + { + "cell_type": "markdown", + "id": "formal-upper", + "metadata": {}, + "source": [ + "# User input" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "assigned-spelling", + "metadata": {}, + "outputs": [], + "source": [ + "# ----------- Wind farm\n", + "wts = {\n", + " 0 :{'x':1280.0, 'y':2560, 'z':0.0, 'D':126.9, 'zhub':86.5, 'cmax':5, 'fmax':10/6, 'Cmeander':1.9, 'name':'T0'},\n", + " 1 :{'x':1280.0, 'y':3200, 'z':0.0, 'D':126.9, 'zhub':86.5, 'cmax':5, 'fmax':10/6, 'Cmeander':1.9, 'name':'T1'},\n", + " 2 :{'x':1280.0, 'y':3840, 'z':0.0, 'D':126.9, 'zhub':86.5, 'cmax':5, 'fmax':10/6, 'Cmeander':1.9, 'name':'T2'},\n", + " }\n", + "\n", + "# ----------- Desired sweeps\n", + "# inflow_deg = [-5, 0, 5] # inflow angle sweep. Determines how large the low-res box should be; TODO: Use this input\n", + "\n", + "# ----------- Turbine parameters\n", + "# Set the yaw of each turbine for wind dir. One row for each wind direction.\n", + "# yaw_init = [ [0,0,0,0,0,0,0,0,0,0,0,0] ] # TODO: Use this input\n", + "\n", + "# ----------- AMR-Wind parameters\n", + "amr_fixed_dt = 0.25\n", + "amr_prob_lo = (0.0, 0.0, 0.0)\n", + "amr_prob_hi = (2560.0, 6400.0, 1280.0)\n", + "assert (amr_prob_hi[0] > amr_prob_lo[0]) & (amr_prob_hi[1] > amr_prob_lo[1]) & (amr_prob_hi[2] > amr_prob_lo[2]), \"AMR-Wind high/low bounds misspecified!\"\n", + "amr_n_cell = (256, 640, 128)\n", + "amr_max_level = 1 # Number of grid refinement levels\n", + "\n", + "incflo_velocity_hh = (0.0, 10.0, 0.0) # Hub-height velocity\n", + "postproc_name = 'sampling'\n" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "tribal-diana", + "metadata": {}, + "outputs": [], + "source": [ + "### Calculate AMR-Wind parameters that will be useful throughout\n", + "## AMR-Wind resolution\n", + "amr_dx0 = (amr_prob_hi[0] - amr_prob_lo[0]) / amr_n_cell[0] # dx for Level 0\n", + "amr_dy0 = (amr_prob_hi[0] - amr_prob_lo[0]) / amr_n_cell[0]\n", + "amr_dz0 = (amr_prob_hi[0] - amr_prob_lo[0]) / amr_n_cell[0]\n", + "amr_ds0_max = max(amr_dx0, amr_dy0, amr_dz0)\n", + "\n", + "amr_dx_refine = amr_dx0/(2**amr_max_level) # dx for the maximum refinement Level\n", + "amr_dy_refine = amr_dy0/(2**amr_max_level)\n", + "amr_dz_refine = amr_dz0/(2**amr_max_level)\n", + "amr_ds_refine_max = max(amr_dx_refine, amr_dy_refine, amr_dz_refine)" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "unable-mapping", + "metadata": {}, + "outputs": [], + "source": [ + "### Calculate timestep output values and sampling frequency\n", + "## Low resolution domain, dt_lr\n", + "cmeander_min = float(\"inf\")\n", + "Dwake_min = float(\"inf\")\n", + "for turbkey in wts:\n", + " cmeander_min = min(cmeander_min, wts[turbkey]['Cmeander'])\n", + " Dwake_min = min(Dwake_min, wts[turbkey]['D']) # Approximate D_wake as D_rotor\n", + "Vhub = np.sqrt(incflo_velocity_hh[0]**2 + incflo_velocity_hh[1]**2)\n", + "\n", + "dt_lr_max = cmeander_min * Dwake_min / (10 * Vhub)\n", + "dt_lr = amr_fixed_dt * np.floor(dt_lr_max/amr_fixed_dt) # Ensure that dt_lr is a multiple of the AMR-Wind timestep\n", + "assert dt_lr >= amr_fixed_dt, \"AMR-Wind timestep too coarse for low resolution domain!\"\n", + "\n", + "## High resolution domain, dt_hr\n", + "fmax_max = 0\n", + "for turbkey in wts:\n", + " fmax_max = max(0, wts[turbkey]['fmax'])\n", + "dt_hr_max = 1 / (2 * fmax_max)\n", + "dt_hr = amr_fixed_dt * np.floor(dt_hr_max/amr_fixed_dt) # Ensure that dt_hr is a multiple of the AMR-Wind timestep\n", + "assert dt_hr >= amr_fixed_dt, \"AMR-Wind timestep too coarse for high resolution domain!\"\n", + "assert dt_hr <= dt_lr, \"Low resolution timestep is finer than high resolution timestep!\"\n", + "\n", + "## Sampling frequency\n", + "out_output_frequency = int(dt_hr/amr_fixed_dt)" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "female-license", + "metadata": {}, + "outputs": [], + "source": [ + "### Calculate grid resolutions\n", + "## Low resolution domain, ds_lr (s = x/y/z)\n", + "## ASSUME: FAST.Farm LR zone uses Level 0 AMR-Wind grid spacing\n", + "## NOTE: ds_lr is calculated independent of any x/y/z requirements,\n", + "## just time step and velocity requiements\n", + "ds_lr_max = dt_lr * Vhub**2 / 15\n", + "ds_lr = amr_ds0_max * np.floor(ds_lr_max/amr_ds0_max) # Ensure that ds_lr is a multiple of the coarse AMR-Wind grid spacing\n", + "assert ds_lr >= amr_dx0, \"AMR-Wind Level 0 x-grid spacing too coarse for low resolution domain!\"\n", + "assert ds_lr >= amr_dy0, \"AMR-Wind Level 0 y-grid spacing too coarse for low resolution domain!\"\n", + "assert ds_lr >= amr_dz0, \"AMR-Wind Level 0 z-grid spacing too coarse for low resolution domain!\"\n", + "\n", + "## High resolution domain, ds_hr\n", + "## ASSUME: FAST.Farm HR zone lies within the region of maxmum AMR-Wind grid refinement\n", + "## NOTE: ds_hr is calculated independent of any x/y/z requirements,\n", + "## just blade chord length requirements\n", + "cmax_min = float(\"inf\")\n", + "for turbkey in wts:\n", + " cmax_min = min(cmax_min, wts[turbkey]['cmax'])\n", + "ds_hr_max = cmax_min\n", + "ds_hr = amr_ds_refine_max * np.floor(ds_hr_max/amr_ds_refine_max) # Ensure that ds_hr is a multiple of the refined AMR-Wind grid spacing\n", + "assert ds_hr >= amr_ds_refine_max, \"AMR-Wind grid spacing too coarse for high resolution domain!\"" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "threatened-joseph", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "# Sampling info generated by AMRWindSamplingCreation.py\n", + "incflo.post_processing = sampling # averaging\n", + "sampling.output_frequency = 1\n", + "sampling.fields = velocity # temperature tke\n", + "sampling.labels = Low HighT0_inflow0deg HighT1_inflow0deg HighT2_inflow0deg\n" + ] + } + ], + "source": [ + "### Calculate high-level info for sampling\n", + "out_labels = [\"Low\"]\n", + "for turbkey in wts:\n", + " out_labels.append(f\"High{wts[turbkey]['name']}_inflow0deg\")\n", + "out_labels_str = \" \".join(str(item) for item in out_labels)\n", + "\n", + "print(f\"# Sampling info generated by AMRWindSamplingCreation.py\")\n", + "print(f\"incflo.post_processing = {postproc_name} # averaging\")\n", + "print(f\"{postproc_name}.output_frequency = {out_output_frequency}\")\n", + "print(f\"{postproc_name}.fields = velocity # temperature tke\")\n", + "\n", + "print(f\"{postproc_name}.labels = {out_labels_str}\")" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "mechanical-agent", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "# Low sampling grid spacing: 10.0 m\n", + "sampling.Low.type = PlaneSampler\n", + "sampling.Low.num_points = 78 206\n", + "sampling.Low.origin = 885.0 2165.0\n", + "sampling.Low.axis1 = 770.0 0.0 0.0\n", + "sampling.Low.axis2 = 0.0 2050.0 0.0\n", + "sampling.Low.normal = 0.0 0.0 1.0\n", + "sampling.Low.offsets = 0 10 20 30 40 50 60 70 80 90 100 110 120 130 140 150 160 170 180 190 200 210 220 230 240 250 260 270 280\n" + ] + } + ], + "source": [ + "### Calculate low resolution grid placement\n", + "# Calculate minimum/maximum LR domain extents\n", + "wt_all_x_min = float(\"inf\") # Minimum x-value of any turbine\n", + "wt_all_x_max = -1*float(\"inf\")\n", + "wt_all_y_min = float(\"inf\")\n", + "wt_all_y_max = -1*float(\"inf\")\n", + "wt_all_z_max = -1*float(\"inf\") # Tallest rotor disk point of any turbine\n", + "Drot_max = -1*float(\"inf\")\n", + "for turbkey in wts:\n", + " wt_all_x_min = min(wt_all_x_min, wts[turbkey]['x'])\n", + " wt_all_x_max = max(wt_all_x_max, wts[turbkey]['x'])\n", + " wt_all_y_min = min(wt_all_y_min, wts[turbkey]['y'])\n", + " wt_all_y_max = max(wt_all_x_min, wts[turbkey]['y'])\n", + " wt_all_z_max = max(wt_all_z_max, wts[turbkey]['zhub'] + 0.5*wts[turbkey]['D'])\n", + " Drot_max = max(Drot_max, wts[turbkey]['D'])\n", + " \n", + "x_buffer_lr = 3 * Drot_max\n", + "y_buffer_lr = 3 * Drot_max\n", + "z_buffer_lr = 1 * Drot_max\n", + "xlow_lr_min = wt_all_x_min - x_buffer_lr\n", + "xhigh_lr_max = wt_all_x_max + x_buffer_lr\n", + "ylow_lr_min = wt_all_y_min - y_buffer_lr\n", + "yhigh_lr_max = wt_all_y_max + y_buffer_lr\n", + "zhigh_lr_max = wt_all_z_max + z_buffer_lr\n", + "\n", + "# Calculate the minimum/maximum LR domain coordinate lengths & number of grid cells\n", + "xdist_lr_min = xhigh_lr_max - xlow_lr_min # Minumum possible length of x-extent of LR domain\n", + "xdist_lr = ds_lr * np.ceil(xdist_lr_min/ds_lr) # The `+ ds_lr` comes from the +1 to NS_LOW in Sec. 4.2.15.6.4.1.1\n", + "# TODO: adjust xdist_lr calculation by also using `inflow_deg`\n", + "nx_lr = int(xdist_lr/ds_lr) + 1\n", + "\n", + "ydist_lr_min = yhigh_lr_max - ylow_lr_min\n", + "ydist_lr = ds_lr * np.ceil(ydist_lr_min/ds_lr)\n", + "# TODO: adjust ydist_lr calculation by also using `inflow_deg`\n", + "ny_lr = int(ydist_lr/ds_lr) + 1\n", + "\n", + "zdist_lr = ds_lr * np.ceil(zhigh_lr_max/ds_lr)\n", + "nz_lr = int(zdist_lr/ds_lr) + 1\n", + "\n", + "# Calculate actual LR domain extent\n", + "# NOTE: Sampling planes should measure at AMR-Wind cell centers, not cell edges\n", + "# NOTE: Should I be using dx/dy/dz values here or ds_lr?\n", + "# - I think it's correct to use ds_lr to get to the xlow values,\n", + "# but then offset by 0.5*amr_dx0\n", + "xlow_lr = ds_lr * np.floor(xlow_lr_min/ds_lr) - 0.5*amr_dx0\n", + "xhigh_lr = xlow_lr + xdist_lr\n", + "ylow_lr = ds_lr * np.floor(ylow_lr_min/ds_lr) - 0.5*amr_dy0\n", + "yhigh_lr = ylow_lr + ydist_lr\n", + "zlow_lr = 0.5 * amr_dz0 # Lowest z point is half the height of the lowest grid cell\n", + "zhigh_lr = zlow_lr + zdist_lr\n", + "zoffsets_lr = np.arange(zlow_lr, zhigh_lr+ds_lr, ds_lr) - zlow_lr\n", + "zoffsets_lr_str = \" \".join(str(int(item)) for item in zoffsets_lr)\n", + "\n", + "assert xhigh_lr < amr_prob_hi[0], \"LR domain extends beyond maximum AMR-Wind x-extent!\"\n", + "assert xlow_lr > amr_prob_lo[0], \"LR domain extends beyond minimum AMR-Wind x-extent!\"\n", + "assert yhigh_lr < amr_prob_hi[1], \"LR domain extends beyond maximum AMR-Wind y-extent!\"\n", + "assert ylow_lr > amr_prob_lo[1], \"LR domain extends beyond minimum AMR-Wind y-extent!\"\n", + "assert zhigh_lr < amr_prob_hi[2], \"LR domain extends beyond maximum AMR-Wind z-extent!\"\n", + "assert zlow_lr > amr_prob_lo[2], \"LR domain extends beyond minimum AMR-Wind z-extent!\"\n", + "\n", + "# Reformat info for AMR-Wind input file\n", + "print(f\"# Low sampling grid spacing: {ds_lr} m\")\n", + "print(f\"{postproc_name}.Low.type = PlaneSampler\")\n", + "print(f\"{postproc_name}.Low.num_points = {nx_lr} {ny_lr}\")\n", + "print(f\"{postproc_name}.Low.origin = {xlow_lr:.1f} {ylow_lr:.1f}\") # Round the float output\n", + "print(f\"{postproc_name}.Low.axis1 = {xdist_lr:.1f} 0.0 0.0\") # Assume: axis1 oriented parallel to AMR-Wind x-axis\n", + "print(f\"{postproc_name}.Low.axis2 = 0.0 {ydist_lr:.1f} 0.0\") # Assume: axis2 oriented parallel to AMR-Wind y-axis\n", + "print(f\"{postproc_name}.Low.normal = 0.0 0.0 1.0\")\n", + "print(f\"{postproc_name}.Low.offsets = {zoffsets_lr_str}\")" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "overhead-hypothesis", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "# Turbine T0 at (x,y) = (1280.0, 2560), with D = 126.9, grid spacing = 5.0\n", + "sampling.T0.type = PlaneSampler\n", + "sampling.T0.num_points = 32 32\n", + "sampling.T0.origin = 1197.5 2477.5\n", + "sampling.T0.axis1 = 155.0 0.0 0.0\n", + "sampling.T0.axis2 = 0.0 155.0 0.0\n", + "sampling.T0.normal = 0.0 0.0 1.0\n", + "sampling.T0.offsets = 0 5 10 15 20 25 30 35 40 45 50 55 60 65 70 75 80 85 90 95 100 105 110 115 120 125 130 135 140 145 150 155 160 165 170 175 180 185 190 195 200 205 210 215 220 225 230\n" + ] + } + ], + "source": [ + "### Calculate high resolution grid placements\n", + "turbkey = 0\n", + "wt_x = wts[turbkey]['x']\n", + "wt_y = wts[turbkey]['y']\n", + "wt_z = wts[turbkey]['zhub'] + 0.5*wts[turbkey]['D']\n", + "wt_D = wts[turbkey]['D']\n", + "wt_name = wts[turbkey]['name']\n", + "\n", + "# Calculate minimum/maximum HR domain extents\n", + "x_buffer_hr = 0.6 * wt_D\n", + "y_buffer_hr = 0.6 * wt_D\n", + "z_buffer_hr = 0.6 * wt_D\n", + "\n", + "xlow_hr_min = wt_x - x_buffer_hr\n", + "xhigh_hr_max = wt_x + x_buffer_hr\n", + "ylow_hr_min = wt_y - y_buffer_hr\n", + "yhigh_hr_max = wt_y + y_buffer_hr\n", + "zhigh_hr_max = wt_z + z_buffer_hr\n", + "\n", + "# Calculate the minimum/maximum HR domain coordinate lengths & number of grid cells\n", + "xdist_hr_min = xhigh_hr_max - xlow_hr_min # Minumum possible length of x-extent of HR domain\n", + "xdist_hr = ds_hr * np.ceil(xdist_hr_min/ds_hr) \n", + "nx_hr = int(xdist_hr/ds_hr) + 1\n", + "\n", + "ydist_hr_min = yhigh_hr_max - ylow_hr_min\n", + "ydist_hr = ds_hr * np.ceil(ydist_hr_min/ds_hr)\n", + "ny_hr = int(ydist_hr/ds_hr) + 1\n", + "\n", + "zdist_hr = ds_hr * np.ceil(zhigh_hr_max/ds_hr)\n", + "nz_hr = int(zdist_hr/ds_hr) + 1\n", + "\n", + "# Calculate actual HR domain extent\n", + "# NOTE: Sampling planes should measure at AMR-Wind cell centers, not cell edges\n", + "xlow_hr = ds_hr * np.floor(xlow_hr_min/ds_hr) - 0.5*amr_dx_refine\n", + "xhigh_hr = xlow_hr + xdist_hr\n", + "ylow_hr = ds_hr * np.floor(ylow_hr_min/ds_hr) - 0.5*amr_dy_refine\n", + "yhigh_hr = ylow_hr + ydist_hr\n", + "zlow_hr = zlow_lr\n", + "zhigh_hr = zlow_hr + zdist_hr\n", + "zoffsets_hr = np.arange(zlow_hr, zhigh_hr+ds_hr, ds_hr) - zlow_hr\n", + "zoffsets_hr_str = \" \".join(str(int(item)) for item in zoffsets_hr)\n", + "\n", + "assert xhigh_hr < amr_prob_hi[0], f\"HR domain for {wt_name} extends beyond maximum AMR-Wind x-extent!\"\n", + "assert xlow_hr > amr_prob_lo[0], f\"HR domain for {wt_name} extends beyond minimum AMR-Wind x-extent!\"\n", + "assert yhigh_hr < amr_prob_hi[1], f\"HR domain for {wt_name} extends beyond maximum AMR-Wind y-extent!\"\n", + "assert ylow_hr > amr_prob_lo[1], f\"HR domain for {wt_name} extends beyond minimum AMR-Wind y-extent!\"\n", + "assert zhigh_hr < amr_prob_hi[2], f\"HR domain for {wt_name} extends beyond maximum AMR-Wind z-extent!\"\n", + "assert zlow_hr > amr_prob_lo[2], f\"HR domain for {wt_name} extends beyond minimum AMR-Wind z-extent!\"\n", + "\n", + "# Reformat info for AMR-Wind input file\n", + "print(f\"# Turbine {wt_name} at (x,y) = ({wt_x}, {wt_y}), with D = {wt_D}, grid spacing = {ds_hr}\")\n", + "print(f\"{postproc_name}.{wt_name}.type = PlaneSampler\")\n", + "print(f\"{postproc_name}.{wt_name}.num_points = {nx_hr} {ny_hr}\")\n", + "print(f\"{postproc_name}.{wt_name}.origin = {xlow_hr:.1f} {ylow_hr:.1f}\") # Round the float output\n", + "print(f\"{postproc_name}.{wt_name}.axis1 = {xdist_hr:.1f} 0.0 0.0\") # Assume: axis1 oriented parallel to AMR-Wind x-axis\n", + "print(f\"{postproc_name}.{wt_name}.axis2 = 0.0 {ydist_hr:.1f} 0.0\") # Assume: axis2 oriented parallel to AMR-Wind y-axis\n", + "print(f\"{postproc_name}.{wt_name}.normal = 0.0 0.0 1.0\")\n", + "print(f\"{postproc_name}.{wt_name}.offsets = {zoffsets_hr_str}\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "classical-portland", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "wfp", + "language": "python", + "name": "wfp" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.7.10" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} From 45aac3280dd13865cf8b051794a4ea2b07730613 Mon Sep 17 00:00:00 2001 From: Alex Rybchuk Date: Sat, 11 Feb 2023 18:47:47 -0700 Subject: [PATCH 030/124] Create class to hold AMR-Wind simulation inputs --- pyFAST/fastfarm/AMRWindSimulation.py | 46 ++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 pyFAST/fastfarm/AMRWindSimulation.py diff --git a/pyFAST/fastfarm/AMRWindSimulation.py b/pyFAST/fastfarm/AMRWindSimulation.py new file mode 100644 index 0000000..465e288 --- /dev/null +++ b/pyFAST/fastfarm/AMRWindSimulation.py @@ -0,0 +1,46 @@ +class AMRWindSimulation: + ''' + This class is used to help prepare sampling planes for an AMR-Wind + simulation. The sampling planes will be used to generate inflow + data for FAST.Farm simulations. + Specifically, this class contains info from the AMR-Wind input file + ''' + + def __init__(self, dt: float, prob_lo: tuple, prob_hi: tuple, + n_cell: tuple, max_level: int, + incflo_velocity_hh: tuple, + postproc_name='sampling'): + ''' + Values from the AMR-Wind input file + Inputs: + * dt: this should be a fixed dt value + * incflo_velocity_hh: velocity vector, specifically at hub height + ''' + self.dt = dt + self.prob_lo = prob_lo + self.prob_hi = prob_hi + self.n_cell = n_cell + self.max_level = max_level + self.incflo_velocity_hh = incflo_velocity_hh + self.postproc_name = postproc_name + + self._checkInputs() + + def _checkInputs(self): + ''' + Check that the AMR-Wind inputs make sense + ''' + if len(self.prob_lo != 3): + raise ValueError(f"prob_lo must contain 3 elements, but it has {len(self.prob_lo)}") + if len(self.prob_hi != 3): + raise ValueError(f"prob_hi must contain 3 elements, but it has {len(self.prob_hi)}") + if len(self.incflo_velocity_hh != 3): + raise ValueError(f"incflo_velocity_hh must contain 3 elements, but it has {len(self.incflo_velocity_hh)}") + if (self.prob_lo[0] >= self.prob_hi[0]): + raise ValueError("x-component of prob_lo larger than x-component of prob_hi") + if (self.prob_lo[1] >= self.prob_hi[1]): + raise ValueError("y-component of prob_lo larger than y-component of prob_hi") + if (self.prob_lo[2] >= self.prob_hi[2]): + raise ValueError("z-component of prob_lo larger than z-component of prob_hi") + + From 8ffb2f87f893ebf50019ab46401ded7dd60e2089 Mon Sep 17 00:00:00 2001 From: Alex Rybchuk Date: Sat, 11 Feb 2023 18:56:00 -0700 Subject: [PATCH 031/124] Fix typos in AMRWindSimulation --- pyFAST/fastfarm/AMRWindSimulation.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyFAST/fastfarm/AMRWindSimulation.py b/pyFAST/fastfarm/AMRWindSimulation.py index 465e288..f899c81 100644 --- a/pyFAST/fastfarm/AMRWindSimulation.py +++ b/pyFAST/fastfarm/AMRWindSimulation.py @@ -30,11 +30,11 @@ def _checkInputs(self): ''' Check that the AMR-Wind inputs make sense ''' - if len(self.prob_lo != 3): + if len(self.prob_lo) != 3: raise ValueError(f"prob_lo must contain 3 elements, but it has {len(self.prob_lo)}") - if len(self.prob_hi != 3): + if len(self.prob_hi) != 3: raise ValueError(f"prob_hi must contain 3 elements, but it has {len(self.prob_hi)}") - if len(self.incflo_velocity_hh != 3): + if len(self.incflo_velocity_hh) != 3: raise ValueError(f"incflo_velocity_hh must contain 3 elements, but it has {len(self.incflo_velocity_hh)}") if (self.prob_lo[0] >= self.prob_hi[0]): raise ValueError("x-component of prob_lo larger than x-component of prob_hi") From 6a9fa58927d332f0d32538384468ac5493a4056f Mon Sep 17 00:00:00 2001 From: Alex Rybchuk Date: Sat, 11 Feb 2023 18:59:18 -0700 Subject: [PATCH 032/124] FF class reads info from possible AMR class --- pyFAST/fastfarm/FASTFarmCaseCreation.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/pyFAST/fastfarm/FASTFarmCaseCreation.py b/pyFAST/fastfarm/FASTFarmCaseCreation.py index 57275a4..b547108 100644 --- a/pyFAST/fastfarm/FASTFarmCaseCreation.py +++ b/pyFAST/fastfarm/FASTFarmCaseCreation.py @@ -9,6 +9,7 @@ from pyFAST.input_output import FASTInputFile, FASTOutputFile, TurbSimFile, VTKFile from pyFAST.fastfarm import writeFastFarm, fastFarmTurbSimExtent, plotFastFarmSetup from pyFAST.fastfarm.TurbSimCaseCreation import TSCaseCreation, writeTimeSeriesFile +from pyFAST.fastfarm.AMRWindSimulation import AMRWindSimulation def cosd(t): return np.cos(np.deg2rad(t)) def sind(t): return np.sin(np.deg2rad(t)) @@ -17,7 +18,7 @@ def sind(t): return np.sin(np.deg2rad(t)) class FFCaseCreation: - def __init__(self, path, wts, cmax, fmax, Cmeander, tmax, zbot, vhub, shear, TIvalue, inflow_deg, dt_high_les=None, ds_high_les=None, extent_high=None, dt_low_les=None, ds_low_les=None, extent_low=None, ffbin=None, yaw_init=None, ADmodel=None, EDmodel=None, nSeeds=6, LESpath=None, sweepWakeSteering=False, sweepYawMisalignment=False, seedValues=None, refTurb_rot=0, verbose=0): + def __init__(self, path, wts, cmax, fmax, Cmeander, tmax, zbot, vhub, shear, TIvalue, inflow_deg, dt_high_les=None, ds_high_les=None, extent_high=None, dt_low_les=None, ds_low_les=None, extent_low=None, ffbin=None, yaw_init=None, ADmodel=None, EDmodel=None, nSeeds=6, LESpath=None, sweepWakeSteering=False, sweepYawMisalignment=False, seedValues=None, refTurb_rot=0, amr=None, verbose=0): ''' ffbin: str @@ -53,6 +54,7 @@ def __init__(self, path, wts, cmax, fmax, Cmeander, tmax, zbot, vhub, shear, TIv self.sweepYM = sweepYawMisalignment self.seedValues = seedValues self.refTurb_rot = refTurb_rot + self.amr = amr self.verbose = verbose @@ -207,6 +209,11 @@ def _checkInputs(self): raise ValueError (f'The path {self.LESpath} does not exist') self.inflowStr = 'LES' + # Check class of amr + if self.amr is not None: + if not isinstance(self.amr, AMRWindSimulation): + raise ValueError(f"The class of amr should be AMRWindSimulation, but is {type(self.amr)}") + # Check the reference turbine for rotation if self.refTurb_rot >= self.nTurbines: raise ValueError(f'The index for the reference turbine for the farm to be rotated around is greater than the number of turbines') @@ -766,6 +773,10 @@ def _rotate_wts(self): def DetermineBoxParameters(self): + ''' + Calculate dt_high_les, ds_high_les, extent_high, dt_low_les, ds_low_les, extent_low, and ffbin + + ''' if self.dt_high_les is not None: # Box paramters given. Only one check is needed since it passed `checkInputs` From e4cbf7fb8b99a15e925e66109e97fccfa662f619 Mon Sep 17 00:00:00 2001 From: Alex Rybchuk Date: Sat, 11 Feb 2023 19:19:06 -0700 Subject: [PATCH 033/124] Calculate dx/dy/dz for amr --- pyFAST/fastfarm/AMRWindSimulation.py | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/pyFAST/fastfarm/AMRWindSimulation.py b/pyFAST/fastfarm/AMRWindSimulation.py index f899c81..c0340d4 100644 --- a/pyFAST/fastfarm/AMRWindSimulation.py +++ b/pyFAST/fastfarm/AMRWindSimulation.py @@ -3,7 +3,9 @@ class AMRWindSimulation: This class is used to help prepare sampling planes for an AMR-Wind simulation. The sampling planes will be used to generate inflow data for FAST.Farm simulations. - Specifically, this class contains info from the AMR-Wind input file + Specifically, this class contains info from the AMR-Wind input file, + and it carries out simple calculations about the AMR-Wind + simulation ''' def __init__(self, dt: float, prob_lo: tuple, prob_hi: tuple, @@ -42,5 +44,19 @@ def _checkInputs(self): raise ValueError("y-component of prob_lo larger than y-component of prob_hi") if (self.prob_lo[2] >= self.prob_hi[2]): raise ValueError("z-component of prob_lo larger than z-component of prob_hi") - + + def calc_params(self): + ''' + Calculate simulation parameters, given simulation inputs + ''' + # Grid resolution at Level 0 + self.dx0 = (self.prob_hi[0] - self.prob_lo[0]) / self.n_cell[0] + self.dy0 = (self.prob_hi[1] - self.prob_lo[1]) / self.n_cell[1] + self.dz0 = (self.prob_hi[2] - self.prob_lo[2]) / self.n_cell[2] + + # Grid resolution at finest refinement level + self.dx_refine = self.dx0/(2**self.max_level) + self.dy_refine = self.dy0/(2**self.max_level) + self.dz_refine = self.dz0/(2**self.max_level) + self.refine_max = max(self.dx_refine, self.dy_refine, self.dz_refine) From 5e027cecc0a69b9406d7994d54ec2b55b2079dc4 Mon Sep 17 00:00:00 2001 From: Alex Rybchuk Date: Sat, 11 Feb 2023 19:35:49 -0700 Subject: [PATCH 034/124] Add timestep and sampling frequency calcs --- pyFAST/fastfarm/AMRWindSimulation.py | 9 +++++ pyFAST/fastfarm/FASTFarmCaseCreation.py | 46 ++++++++++++++++++++----- 2 files changed, 46 insertions(+), 9 deletions(-) diff --git a/pyFAST/fastfarm/AMRWindSimulation.py b/pyFAST/fastfarm/AMRWindSimulation.py index c0340d4..4564735 100644 --- a/pyFAST/fastfarm/AMRWindSimulation.py +++ b/pyFAST/fastfarm/AMRWindSimulation.py @@ -1,3 +1,5 @@ +import numpy as np + class AMRWindSimulation: ''' This class is used to help prepare sampling planes for an AMR-Wind @@ -18,6 +20,7 @@ def __init__(self, dt: float, prob_lo: tuple, prob_hi: tuple, * dt: this should be a fixed dt value * incflo_velocity_hh: velocity vector, specifically at hub height ''' + # Process inputs self.dt = dt self.prob_lo = prob_lo self.prob_hi = prob_hi @@ -26,6 +29,10 @@ def __init__(self, dt: float, prob_lo: tuple, prob_hi: tuple, self.incflo_velocity_hh = incflo_velocity_hh self.postproc_name = postproc_name + # Placeholder variables, to be calculated by FFCaseCreation + self.output_frequency = None + + # Run extra functions self._checkInputs() def _checkInputs(self): @@ -60,3 +67,5 @@ def calc_params(self): self.dz_refine = self.dz0/(2**self.max_level) self.refine_max = max(self.dx_refine, self.dy_refine, self.dz_refine) + # Hub height wind speed + self.vhub = np.sqrt(self.incflo_velocity_hh[0]**2 + self.incflo_velocity_hh[1]**2) diff --git a/pyFAST/fastfarm/FASTFarmCaseCreation.py b/pyFAST/fastfarm/FASTFarmCaseCreation.py index b547108..0c90d7d 100644 --- a/pyFAST/fastfarm/FASTFarmCaseCreation.py +++ b/pyFAST/fastfarm/FASTFarmCaseCreation.py @@ -74,7 +74,9 @@ def __init__(self, path, wts, cmax, fmax, Cmeander, tmax, zbot, vhub, shear, TIv if self.verbose>0: print(f'Determining box parameters...', end='\r') - self.DetermineBoxParameters() + if self.dt_high_les is not None: + self.DetermineBoxParameters() + self.CheckAutoBoxParameters() if self.verbose>0: print(f'Determining box paramters... Done.') @@ -774,21 +776,47 @@ def _rotate_wts(self): def DetermineBoxParameters(self): ''' - Calculate dt_high_les, ds_high_les, extent_high, dt_low_les, ds_low_les, extent_low, and ffbin - + Calculate dt_high_les, ds_high_les, extent_high, dt_low_les, ds_low_les, extent_low, and ffbin, + given inputs from amr ''' + amr = self.amr - if self.dt_high_les is not None: - # Box paramters given. Only one check is needed since it passed `checkInputs` - return + ### Calculate timestep values and AMR-Wind plane sampling frequency + ## Low resolution domain, dt_low_les + cmeander_min = float("inf") + Dwake_min = float("inf") + for turbkey in self.wts: + cmeander_min = min(cmeander_min, self.wts[turbkey]['Cmeander']) + Dwake_min = min(Dwake_min, self.wts[turbkey]['D']) # Approximate D_wake as D_rotor + + dt_lr_max = cmeander_min * Dwake_min / (10 * amr.vhub) + self.dt_low_les = amr.dt * np.floor(dt_lr_max/amr.dt) # Ensure that dt_lr is a multiple of the AMR-Wind timestep + ## High resolution domain, dt_high_les + fmax_max = 0 + for turbkey in self.wts: + fmax_max = max(0, self.wts[turbkey]['fmax']) + dt_hr_max = 1 / (2 * fmax_max) + self.dt_high_les = amr.dt * np.floor(dt_hr_max/amr.dt) # Ensure that dt_hr is a multiple of the AMR-Wind timestep - # todo: compute the boxes paraters - raise NotImplementedError(f'The ability to automatically determine the box paraters is not implemented yet.') + ## Sampling frequency + amr.output_frequency = int(self.dt_high_les/amr.dt) + + + def CheckAutoBoxParameters(self): + ''' + Check the values of parameters that were calculated by DetermineBoxParameters + ''' + ## Timestep checks + if self.dt_low_les >= self.amr.dt: + raise ValueError("AMR-Wind timestep too coarse for low resolution domain!") + if self.dt_high_les >= self.amr.dt: + raise ValueError("AMR-Wind timestep too coarse for high resolution domain!") + if self.dt_high_les <= self.dt_low_les: + raise ValueError("Low resolution timestep is finer than high resolution timestep!") - def writeAMRWindRefinement(self): From 542db829aebeb96ea6682fc2a2e9dbd619616a46 Mon Sep 17 00:00:00 2001 From: Alex Rybchuk Date: Sat, 11 Feb 2023 19:43:14 -0700 Subject: [PATCH 035/124] Calculate grid resolutions --- pyFAST/fastfarm/AMRWindSimulation.py | 1 + pyFAST/fastfarm/FASTFarmCaseCreation.py | 30 ++++++++++++++++++++++++- 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/pyFAST/fastfarm/AMRWindSimulation.py b/pyFAST/fastfarm/AMRWindSimulation.py index 4564735..ce684a0 100644 --- a/pyFAST/fastfarm/AMRWindSimulation.py +++ b/pyFAST/fastfarm/AMRWindSimulation.py @@ -60,6 +60,7 @@ def calc_params(self): self.dx0 = (self.prob_hi[0] - self.prob_lo[0]) / self.n_cell[0] self.dy0 = (self.prob_hi[1] - self.prob_lo[1]) / self.n_cell[1] self.dz0 = (self.prob_hi[2] - self.prob_lo[2]) / self.n_cell[2] + self.ds0_max = max(self.dx0, self.dy0, self.dz0) # Grid resolution at finest refinement level self.dx_refine = self.dx0/(2**self.max_level) diff --git a/pyFAST/fastfarm/FASTFarmCaseCreation.py b/pyFAST/fastfarm/FASTFarmCaseCreation.py index 0c90d7d..670437b 100644 --- a/pyFAST/fastfarm/FASTFarmCaseCreation.py +++ b/pyFAST/fastfarm/FASTFarmCaseCreation.py @@ -781,7 +781,7 @@ def DetermineBoxParameters(self): ''' amr = self.amr - ### Calculate timestep values and AMR-Wind plane sampling frequency + ### ~~~~~~~~~ Calculate timestep values and AMR-Wind plane sampling frequency ~~~~~~~~~ ## Low resolution domain, dt_low_les cmeander_min = float("inf") Dwake_min = float("inf") @@ -802,6 +802,25 @@ def DetermineBoxParameters(self): ## Sampling frequency amr.output_frequency = int(self.dt_high_les/amr.dt) + ### ~~~~~~~~~ Calculate grid resolutions ~~~~~~~~~ + ## Low resolution domain, ds_lr (s = x/y/z) + # ASSUME: FAST.Farm LR zone uses Level 0 AMR-Wind grid spacing + # NOTE: ds_lr is calculated independent of any x/y/z requirements, + # just time step and velocity requiements + ds_lr_max = self.dt_low_les * amr.vhub**2 / 15 + self.ds_low_les = amr.ds0_max * np.floor(ds_lr_max/amr.ds0_max) # Ensure that ds_lr is a multiple of the coarse AMR-Wind grid spacing + + ## High resolution domain, ds_hr + # ASSUME: FAST.Farm HR zone lies within the region of maxmum AMR-Wind grid refinement + # NOTE: ds_hr is calculated independent of any x/y/z requirements, + # just blade chord length requirements + cmax_min = float("inf") + for turbkey in self.wts: + cmax_min = min(cmax_min, self.wts[turbkey]['cmax']) + ds_hr_max = cmax_min + self.ds_high_les = amr.ds_refine_max * np.floor(ds_hr_max/amr.ds_refine_max) # Ensure that ds_hr is a multiple of the refined AMR-Wind grid spacing + assert self.ds_high_les >= amr.ds_refine_max, "AMR-Wind grid spacing too coarse for high resolution domain!" + def CheckAutoBoxParameters(self): ''' @@ -816,6 +835,15 @@ def CheckAutoBoxParameters(self): if self.dt_high_les <= self.dt_low_les: raise ValueError("Low resolution timestep is finer than high resolution timestep!") + ## Grid resolution checks + if self.ds_low_les >= self.amr.dx0: + raise ValueError("AMR-Wind Level 0 x-grid spacing too coarse for low resolution domain!") + if self.ds_low_les >= self.amr.dy0: + raise ValueError("AMR-Wind Level 0 y-grid spacing too coarse for low resolution domain!") + if self.ds_low_les >= self.amr.dz0: + raise ValueError("AMR-Wind Level 0 z-grid spacing too coarse for low resolution domain!") + if self.ds_high_les >= self.amr.ds_refine_max: + raise ValueError("AMR-Wind grid spacing too coarse for high resolution domain!") def writeAMRWindRefinement(self): From ce024a9ecbb3ede666f261b02f41145a29d7a9c7 Mon Sep 17 00:00:00 2001 From: Alex Rybchuk Date: Sat, 11 Feb 2023 19:49:26 -0700 Subject: [PATCH 036/124] Add sampling label calcs --- pyFAST/fastfarm/AMRWindSimulation.py | 1 + pyFAST/fastfarm/FASTFarmCaseCreation.py | 12 ++++++++++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/pyFAST/fastfarm/AMRWindSimulation.py b/pyFAST/fastfarm/AMRWindSimulation.py index ce684a0..ba9b0fb 100644 --- a/pyFAST/fastfarm/AMRWindSimulation.py +++ b/pyFAST/fastfarm/AMRWindSimulation.py @@ -31,6 +31,7 @@ def __init__(self, dt: float, prob_lo: tuple, prob_hi: tuple, # Placeholder variables, to be calculated by FFCaseCreation self.output_frequency = None + self.sampling_labels = None # Run extra functions self._checkInputs() diff --git a/pyFAST/fastfarm/FASTFarmCaseCreation.py b/pyFAST/fastfarm/FASTFarmCaseCreation.py index 670437b..30a90b8 100644 --- a/pyFAST/fastfarm/FASTFarmCaseCreation.py +++ b/pyFAST/fastfarm/FASTFarmCaseCreation.py @@ -776,11 +776,19 @@ def _rotate_wts(self): def DetermineBoxParameters(self): ''' - Calculate dt_high_les, ds_high_les, extent_high, dt_low_les, ds_low_les, extent_low, and ffbin, - given inputs from amr + Calculate the following variables for FAST.Farm: + dt_high_les, ds_high_les, extent_high, dt_low_les, ds_low_les, extent_low, and ffbin + And calculate information for the AMR-Wind simulation: + TODO: sampling_labels, output_frequency, ... ''' amr = self.amr + ### ~~~~~~~~~ Calculate high-level info for AMR-Wind sampling ~~~~~~~~~ + sampling_labels = ["Low"] + for turbkey in self.wts: + sampling_labels.append(f"High{turbkey}_inflow0deg") + amr.sampling_labels = sampling_labels + ### ~~~~~~~~~ Calculate timestep values and AMR-Wind plane sampling frequency ~~~~~~~~~ ## Low resolution domain, dt_low_les cmeander_min = float("inf") From 2505766c2012aa125fc90d5c98dab3ada6a31f43 Mon Sep 17 00:00:00 2001 From: Alex Rybchuk Date: Sat, 11 Feb 2023 21:30:54 -0700 Subject: [PATCH 037/124] Calculate position of LR grid --- pyFAST/fastfarm/AMRWindSimulation.py | 12 ++++ pyFAST/fastfarm/FASTFarmCaseCreation.py | 83 ++++++++++++++++++++++++- 2 files changed, 93 insertions(+), 2 deletions(-) diff --git a/pyFAST/fastfarm/AMRWindSimulation.py b/pyFAST/fastfarm/AMRWindSimulation.py index ba9b0fb..7085670 100644 --- a/pyFAST/fastfarm/AMRWindSimulation.py +++ b/pyFAST/fastfarm/AMRWindSimulation.py @@ -32,6 +32,18 @@ def __init__(self, dt: float, prob_lo: tuple, prob_hi: tuple, # Placeholder variables, to be calculated by FFCaseCreation self.output_frequency = None self.sampling_labels = None + self.ds_lr = None + self.ds_hr = None + self.nx_lr = None + self.ny_lr = None + self.nz_lr = None + self.xlow_lr = None + self.xhigh_lr = None + self.ylow_lr = None + self.yhigh_lr = None + self.zlow_lr = None + self.zhigh_lr = None + self.zoffsets_lr = None # Run extra functions self._checkInputs() diff --git a/pyFAST/fastfarm/FASTFarmCaseCreation.py b/pyFAST/fastfarm/FASTFarmCaseCreation.py index 30a90b8..2478535 100644 --- a/pyFAST/fastfarm/FASTFarmCaseCreation.py +++ b/pyFAST/fastfarm/FASTFarmCaseCreation.py @@ -817,6 +817,7 @@ def DetermineBoxParameters(self): # just time step and velocity requiements ds_lr_max = self.dt_low_les * amr.vhub**2 / 15 self.ds_low_les = amr.ds0_max * np.floor(ds_lr_max/amr.ds0_max) # Ensure that ds_lr is a multiple of the coarse AMR-Wind grid spacing + amr.ds_lr = self.ds_low_les ## High resolution domain, ds_hr # ASSUME: FAST.Farm HR zone lies within the region of maxmum AMR-Wind grid refinement @@ -827,8 +828,73 @@ def DetermineBoxParameters(self): cmax_min = min(cmax_min, self.wts[turbkey]['cmax']) ds_hr_max = cmax_min self.ds_high_les = amr.ds_refine_max * np.floor(ds_hr_max/amr.ds_refine_max) # Ensure that ds_hr is a multiple of the refined AMR-Wind grid spacing - assert self.ds_high_les >= amr.ds_refine_max, "AMR-Wind grid spacing too coarse for high resolution domain!" - + amr.ds_hr = self.ds_high_les + + ### ~~~~~~~~~ Calculate low resolution grid placement ~~~~~~~~~ + # Calculate minimum/maximum LR domain extents + wt_all_x_min = float("inf") # Minimum x-value of any turbine + wt_all_x_max = -1*float("inf") + wt_all_y_min = float("inf") + wt_all_y_max = -1*float("inf") + wt_all_z_max = -1*float("inf") # Tallest rotor disk point of any turbine + Drot_max = -1*float("inf") + for turbkey in self.wts: + wt_all_x_min = min(wt_all_x_min, self.wts[turbkey]['x']) + wt_all_x_max = max(wt_all_x_max, self.wts[turbkey]['x']) + wt_all_y_min = min(wt_all_y_min, self.wts[turbkey]['y']) + wt_all_y_max = max(wt_all_x_min, self.wts[turbkey]['y']) + wt_all_z_max = max(wt_all_z_max, self.wts[turbkey]['zhub'] + 0.5*self.wts[turbkey]['D']) + Drot_max = max(Drot_max, self.wts[turbkey]['D']) + + x_buffer_lr = 3 * Drot_max + y_buffer_lr = 3 * Drot_max + z_buffer_lr = 1 * Drot_max # This buffer size was arbitrarily chosen + xlow_lr_min = wt_all_x_min - x_buffer_lr + xhigh_lr_max = wt_all_x_max + x_buffer_lr + ylow_lr_min = wt_all_y_min - y_buffer_lr + yhigh_lr_max = wt_all_y_max + y_buffer_lr + zhigh_lr_max = wt_all_z_max + z_buffer_lr + + # Calculate the minimum/maximum LR domain coordinate lengths & number of grid cells + xdist_lr_min = xhigh_lr_max - xlow_lr_min # Minumum possible length of x-extent of LR domain + xdist_lr = self.ds_low_les * np.ceil(xdist_lr_min/self.ds_low_les) # The `+ ds_lr` comes from the +1 to NS_LOW in Sec. 4.2.15.6.4.1.1 + # TODO: adjust xdist_lr calculation by also using `inflow_deg` + nx_lr = int(xdist_lr/self.ds_low_les) + 1 + + ydist_lr_min = yhigh_lr_max - ylow_lr_min + ydist_lr = self.ds_low_les * np.ceil(ydist_lr_min/self.ds_low_les) + # TODO: adjust ydist_lr calculation by also using `inflow_deg` + ny_lr = int(ydist_lr/self.ds_low_les) + 1 + + zdist_lr = self.ds_low_les * np.ceil(zhigh_lr_max/self.ds_low_les) + nz_lr = int(zdist_lr/self.ds_low_les) + 1 + + ## Calculate actual LR domain extent + # NOTE: Sampling planes should measure at AMR-Wind cell centers, not cell edges + # NOTE: Should we use dx/dy/dz values here or ds_lr? + # - AR: I think it's correct to use ds_lr to get to the xlow values, + # but then offset by 0.5*amr_dx0 + xlow_lr = self.ds_low_les * np.floor(xlow_lr_min/self.ds_low_les) - 0.5*amr.dx0 + xhigh_lr = xlow_lr + xdist_lr + ylow_lr = self.ds_low_les * np.floor(ylow_lr_min/self.ds_low_les) - 0.5*amr.dy0 + yhigh_lr = ylow_lr + ydist_lr + zlow_lr = 0.5 * amr.dz0 # Lowest z point is half the height of the lowest grid cell + zhigh_lr = zlow_lr + zdist_lr + zoffsets_lr = np.arange(zlow_lr, zhigh_lr+self.ds_low_les, self.ds_low_les) - zlow_lr + + ## Save out info + # self.extent_low = ? # TODO: How should this be formatted? + + amr.nx_lr = nx_lr + amr.ny_lr = ny_lr + amr.nz_lr = nz_lr + amr.xlow_lr = xlow_lr + amr.xhigh_lr = xhigh_lr + amr.ylow_lr = ylow_lr + amr.yhigh_lr = yhigh_lr + amr.zlow_lr = zlow_lr + amr.zhigh_lr = zhigh_lr + amr.zoffsets_lr = zoffsets_lr def CheckAutoBoxParameters(self): ''' @@ -853,6 +919,19 @@ def CheckAutoBoxParameters(self): if self.ds_high_les >= self.amr.ds_refine_max: raise ValueError("AMR-Wind grid spacing too coarse for high resolution domain!") + ## Low resolution domain extent checks + if self.amr.xhigh_lr < self.amr.prob_hi[0]: + raise ValueError("LR domain extends beyond maximum AMR-Wind x-extent!") + if self.amr.xlow_lr > self.amr.prob_lo[0]: + raise ValueError("LR domain extends beyond minimum AMR-Wind x-extent!") + if self.amr.yhigh_lr < self.amr.prob_hi[1]: + raise ValueError("LR domain extends beyond maximum AMR-Wind y-extent!") + if self.amr.ylow_lr > self.amr.prob_lo[1]: + raise ValueError("LR domain extends beyond minimum AMR-Wind y-extent!") + if self.amr.zhigh_lr < self.amr.prob_hi[2]: + raise ValueError("LR domain extends beyond maximum AMR-Wind z-extent!") + if self.amr.zlow_lr > self.amr.prob_lo[2]: + raise ValueError("LR domain extends beyond minimum AMR-Wind z-extent!") def writeAMRWindRefinement(self): From d78e66254fc1c16a232ead43a0b8a45726d0b364 Mon Sep 17 00:00:00 2001 From: Alex Rybchuk Date: Sat, 11 Feb 2023 22:06:23 -0700 Subject: [PATCH 038/124] Calculate position of HR grid and write out info --- pyFAST/fastfarm/AMRWindSimulation.py | 57 +++++++++++++++++++++++- pyFAST/fastfarm/FASTFarmCaseCreation.py | 58 ++++++++++++++++++++++++- 2 files changed, 111 insertions(+), 4 deletions(-) diff --git a/pyFAST/fastfarm/AMRWindSimulation.py b/pyFAST/fastfarm/AMRWindSimulation.py index 7085670..1fcd4f6 100644 --- a/pyFAST/fastfarm/AMRWindSimulation.py +++ b/pyFAST/fastfarm/AMRWindSimulation.py @@ -1,4 +1,5 @@ import numpy as np +from pathlib import Path class AMRWindSimulation: ''' @@ -44,9 +45,11 @@ def __init__(self, dt: float, prob_lo: tuple, prob_hi: tuple, self.zlow_lr = None self.zhigh_lr = None self.zoffsets_lr = None + self.hr_domains = None # Run extra functions self._checkInputs() + self._calc_amr_params() def _checkInputs(self): ''' @@ -65,9 +68,9 @@ def _checkInputs(self): if (self.prob_lo[2] >= self.prob_hi[2]): raise ValueError("z-component of prob_lo larger than z-component of prob_hi") - def calc_params(self): + def _calc_amr_params(self): ''' - Calculate simulation parameters, given simulation inputs + Calculate simulation parameters, given only AMR-Wind inputs ''' # Grid resolution at Level 0 self.dx0 = (self.prob_hi[0] - self.prob_lo[0]) / self.n_cell[0] @@ -83,3 +86,53 @@ def calc_params(self): # Hub height wind speed self.vhub = np.sqrt(self.incflo_velocity_hh[0]**2 + self.incflo_velocity_hh[1]**2) + + def write_sampling_params(self, outdir): + ''' + Write out text that can be used for the sampling planes in an + AMR-Wind input file + ''' + outfile = Path(outdir, 'sampling_config.i') + if outfile.is_file(): + raise FileExistsError(f"{str(outfile)} already exists! Aborting...") + + with outfile.open("w") as out: + # Write high-level info for sampling + out.write(f"# Sampling info generated by AMRWindSamplingCreation.py\n") + out.write(f"incflo.post_processing = {self.postproc_name} # averaging\n") + out.write(f"{self.postproc_name}.output_frequency = {self.output_frequency}\n") + out.write(f"{self.postproc_name}.fields = velocity # temperature tke\n") + out.write(f"{self.postproc_name}.labels = {self.sampling_labels}\n") + + # Write out low resolution sampling plane info + zoffsets_lr_str = " ".join(str(int(item)) for item in self.zoffsets_lr) + + out.write(f"\n# Low sampling grid spacing: {self.ds_lr} m\n") + out.write(f"{self.postproc_name}.Low.type = PlaneSampler\n") + out.write(f"{self.postproc_name}.Low.num_points = {self.nx_lr} {self.ny_lr}\n") + out.write(f"{self.postproc_name}.Low.origin = {self.xlow_lr:.1f} {self.ylow_lr:.1f}\n") # Round the float output + out.write(f"{self.postproc_name}.Low.axis1 = {self.xdist_lr:.1f} 0.0 0.0\n") # Assume: axis1 oriented parallel to AMR-Wind x-axis + out.write(f"{self.postproc_name}.Low.axis2 = 0.0 {self.ydist_lr:.1f} 0.0\n") # Assume: axis2 oriented parallel to AMR-Wind y-axis + out.write(f"{self.postproc_name}.Low.normal = 0.0 0.0 1.0\n") + out.write(f"{self.postproc_name}.Low.offsets = {zoffsets_lr_str}\n") + + # Write out high resolution sampling plane info + for turbkey in self.hr_domains: + wt_name = f'T{turbkey}' + nx_hr = self.hr_domains[turbkey]['nx_hr'] + ny_hr = self.hr_domains[turbkey]['ny_hr'] + xlow_hr = self.hr_domains[turbkey]['xlow_hr'] + ylow_hr = self.hr_domains[turbkey]['ylow_hr'] + xdist_hr = self.hr_domains[turbkey]['xdist_hr'] + ydist_hr = self.hr_domains[turbkey]['ydist_hr'] + zoffsets_hr = self.hr_domains[turbkey]['zoffsets_hr'] + zoffsets_hr_str = " ".join(str(int(item)) for item in zoffsets_hr) + + out.write(f"\n# Turbine {wt_name}\n") + out.write(f"{self.postproc_name}.{wt_name}.type = PlaneSampler\n") + out.write(f"{self.postproc_name}.{wt_name}.num_points = {nx_hr} {ny_hr}\n") + out.write(f"{self.postproc_name}.{wt_name}.origin = {xlow_hr:.1f} {ylow_hr:.1f}\n") # Round the float output + out.write(f"{self.postproc_name}.{wt_name}.axis1 = {xdist_hr:.1f} 0.0 0.0\n") # Assume: axis1 oriented parallel to AMR-Wind x-axis + out.write(f"{self.postproc_name}.{wt_name}.axis2 = 0.0 {ydist_hr:.1f} 0.0\n") # Assume: axis2 oriented parallel to AMR-Wind y-axis + out.write(f"{self.postproc_name}.{wt_name}.normal = 0.0 0.0 1.0\n") + out.write(f"{self.postproc_name}.{wt_name}.offsets = {zoffsets_hr_str}\n") \ No newline at end of file diff --git a/pyFAST/fastfarm/FASTFarmCaseCreation.py b/pyFAST/fastfarm/FASTFarmCaseCreation.py index 2478535..18b755b 100644 --- a/pyFAST/fastfarm/FASTFarmCaseCreation.py +++ b/pyFAST/fastfarm/FASTFarmCaseCreation.py @@ -777,9 +777,9 @@ def _rotate_wts(self): def DetermineBoxParameters(self): ''' Calculate the following variables for FAST.Farm: - dt_high_les, ds_high_les, extent_high, dt_low_les, ds_low_les, extent_low, and ffbin + dt_high_les, ds_high_les, extent_high, dt_low_les, ds_low_les, and extent_low And calculate information for the AMR-Wind simulation: - TODO: sampling_labels, output_frequency, ... + sampling_labels, output_frequency, specs for lr domain, specs for hr domains ''' amr = self.amr @@ -896,6 +896,60 @@ def DetermineBoxParameters(self): amr.zhigh_lr = zhigh_lr amr.zoffsets_lr = zoffsets_lr + ### ~~~~~~~~~ Calculate high resolution grid placement ~~~~~~~~~ + hr_domains = {} + for turbkey in self.wts: + wt_x = self.wts[turbkey]['x'] + wt_y = self.wts[turbkey]['y'] + wt_z = self.wts[turbkey]['zhub'] + 0.5*self.wts[turbkey]['D'] + wt_D = self.wts[turbkey]['D'] + + # Calculate minimum/maximum HR domain extents + x_buffer_hr = 0.6 * wt_D + y_buffer_hr = 0.6 * wt_D + z_buffer_hr = 0.6 * wt_D + + xlow_hr_min = wt_x - x_buffer_hr + xhigh_hr_max = wt_x + x_buffer_hr + ylow_hr_min = wt_y - y_buffer_hr + yhigh_hr_max = wt_y + y_buffer_hr + zhigh_hr_max = wt_z + z_buffer_hr + + # Calculate the minimum/maximum HR domain coordinate lengths & number of grid cells + xdist_hr_min = xhigh_hr_max - xlow_hr_min # Minumum possible length of x-extent of HR domain + xdist_hr = self.ds_high_les * np.ceil(xdist_hr_min/self.ds_high_les) + nx_hr = int(xdist_hr/self.ds_high_les) + 1 + + ydist_hr_min = yhigh_hr_max - ylow_hr_min + ydist_hr = self.ds_high_les * np.ceil(ydist_hr_min/self.ds_high_les) + ny_hr = int(ydist_hr/self.ds_high_les) + 1 + + zdist_hr = self.ds_high_les * np.ceil(zhigh_hr_max/self.ds_high_les) + nz_hr = int(zdist_hr/self.ds_high_les) + 1 + + # Calculate actual HR domain extent + # NOTE: Sampling planes should measure at AMR-Wind cell centers, not cell edges + xlow_hr = self.ds_high_les * np.floor(xlow_hr_min/self.ds_high_les) - 0.5*amr.dx_refine + xhigh_hr = xlow_hr + xdist_hr + ylow_hr = self.ds_high_les * np.floor(ylow_hr_min/self.ds_high_les) - 0.5*amr.dy_refine + yhigh_hr = ylow_hr + ydist_hr + zlow_hr = zlow_lr + zhigh_hr = zlow_hr + zdist_hr + zoffsets_hr = np.arange(zlow_hr, zhigh_hr+self.ds_high_les, self.ds_high_les) - zlow_hr + + # Save info + # self.extent_high = ? # TODO: How should this be formatted? + + hr_turb_info = {'nx_hr': nx_hr, 'ny_hr': ny_hr, 'nz_hr': nz_hr, + 'xlow_hr': xlow_hr, 'ylow_hr': ylow_hr, 'zlow_hr': zlow_hr, + 'xhigh_hr': xhigh_hr, 'yhigh_hr': yhigh_hr, 'zhigh_hr': zhigh_hr, + 'zoffsets_hr': zoffsets_hr} + hr_domains[turbkey] = hr_turb_info + amr.hr_domains = hr_domains + + ### ~~~~~~~~~ Write out sampling plane info ~~~~~~~~~ + amr.write_sampling_params(self.path) + def CheckAutoBoxParameters(self): ''' Check the values of parameters that were calculated by DetermineBoxParameters From 11fac924cd0995b4c480fb84a2ca9df94401510c Mon Sep 17 00:00:00 2001 From: Alex Rybchuk Date: Mon, 13 Feb 2023 08:29:22 -0700 Subject: [PATCH 039/124] Remove writeAMRWindRefinement --- pyFAST/fastfarm/FASTFarmCaseCreation.py | 27 ------------------------- 1 file changed, 27 deletions(-) diff --git a/pyFAST/fastfarm/FASTFarmCaseCreation.py b/pyFAST/fastfarm/FASTFarmCaseCreation.py index 18b755b..7ef058b 100644 --- a/pyFAST/fastfarm/FASTFarmCaseCreation.py +++ b/pyFAST/fastfarm/FASTFarmCaseCreation.py @@ -987,33 +987,6 @@ def CheckAutoBoxParameters(self): if self.amr.zlow_lr > self.amr.prob_lo[2]: raise ValueError("LR domain extends beyond minimum AMR-Wind z-extent!") - def writeAMRWindRefinement(self): - - - raise NotImplementedError(f'writeAMRWindRefinement is not implemented.') - - - # Get list of proper non-repeated cases - self._get_domain_parameters() - - # Loop over all turbines of all cases/conditions - for cond in range(self.nConditions): - for case in range(self.nHighBoxCases): - # Get actual case number given the high-box that need to be saved - case = self.allHighBoxCases.isel(case=case)['case'].values - if self.verbose>3: - print(f'Generating AMR-Wind input file for cond {cond} ({self.condDirList[cond]}),'\ - f'case {case} ({self.caseDirList[case]}).') - for t in range(self.nTurbines): - # Recover turbine properties - D_ = self.allCases.sel(case=case, turbine=t)['D' ].values - HubHt_ = self.allCases.sel(case=case, turbine=t)['zhub'].values - xloc_ = self.allCases.sel(case=case, turbine=t)['Tx' ].values - yloc_ = self.allCases.sel(case=case, turbine=t)['Ty' ].values - - # todo: maybe get information about your grid from amr-wind. You want to sample at the cell center - # todo: call another function to spit out the refinements in the proper syntax - From d8770a338d9e1b6297033adee2292e80eda2f254 Mon Sep 17 00:00:00 2001 From: Alex Rybchuk Date: Mon, 13 Feb 2023 08:36:52 -0700 Subject: [PATCH 040/124] Remove AMRWindSamplingCreation.ipynb --- pyFAST/fastfarm/AMRWindSamplingCreation.ipynb | 399 ------------------ 1 file changed, 399 deletions(-) delete mode 100644 pyFAST/fastfarm/AMRWindSamplingCreation.ipynb diff --git a/pyFAST/fastfarm/AMRWindSamplingCreation.ipynb b/pyFAST/fastfarm/AMRWindSamplingCreation.ipynb deleted file mode 100644 index 46560b2..0000000 --- a/pyFAST/fastfarm/AMRWindSamplingCreation.ipynb +++ /dev/null @@ -1,399 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": 1, - "id": "verbal-ceramic", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "'\\nWrite out information that is useful for setting up AMR-Wind sampling planes for FAST.Farm,\\n following guidance from https://openfast.readthedocs.io/en/main/source/user/fast.farm/ModelGuidance.html\\n'" - ] - }, - "execution_count": 1, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "'''\n", - "Write out information that is useful for setting up AMR-Wind sampling planes for FAST.Farm,\n", - " following guidance from https://openfast.readthedocs.io/en/main/source/user/fast.farm/ModelGuidance.html\n", - "'''" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "id": "excessive-roommate", - "metadata": {}, - "outputs": [], - "source": [ - "import numpy as np" - ] - }, - { - "cell_type": "markdown", - "id": "formal-upper", - "metadata": {}, - "source": [ - "# User input" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "id": "assigned-spelling", - "metadata": {}, - "outputs": [], - "source": [ - "# ----------- Wind farm\n", - "wts = {\n", - " 0 :{'x':1280.0, 'y':2560, 'z':0.0, 'D':126.9, 'zhub':86.5, 'cmax':5, 'fmax':10/6, 'Cmeander':1.9, 'name':'T0'},\n", - " 1 :{'x':1280.0, 'y':3200, 'z':0.0, 'D':126.9, 'zhub':86.5, 'cmax':5, 'fmax':10/6, 'Cmeander':1.9, 'name':'T1'},\n", - " 2 :{'x':1280.0, 'y':3840, 'z':0.0, 'D':126.9, 'zhub':86.5, 'cmax':5, 'fmax':10/6, 'Cmeander':1.9, 'name':'T2'},\n", - " }\n", - "\n", - "# ----------- Desired sweeps\n", - "# inflow_deg = [-5, 0, 5] # inflow angle sweep. Determines how large the low-res box should be; TODO: Use this input\n", - "\n", - "# ----------- Turbine parameters\n", - "# Set the yaw of each turbine for wind dir. One row for each wind direction.\n", - "# yaw_init = [ [0,0,0,0,0,0,0,0,0,0,0,0] ] # TODO: Use this input\n", - "\n", - "# ----------- AMR-Wind parameters\n", - "amr_fixed_dt = 0.25\n", - "amr_prob_lo = (0.0, 0.0, 0.0)\n", - "amr_prob_hi = (2560.0, 6400.0, 1280.0)\n", - "assert (amr_prob_hi[0] > amr_prob_lo[0]) & (amr_prob_hi[1] > amr_prob_lo[1]) & (amr_prob_hi[2] > amr_prob_lo[2]), \"AMR-Wind high/low bounds misspecified!\"\n", - "amr_n_cell = (256, 640, 128)\n", - "amr_max_level = 1 # Number of grid refinement levels\n", - "\n", - "incflo_velocity_hh = (0.0, 10.0, 0.0) # Hub-height velocity\n", - "postproc_name = 'sampling'\n" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "id": "tribal-diana", - "metadata": {}, - "outputs": [], - "source": [ - "### Calculate AMR-Wind parameters that will be useful throughout\n", - "## AMR-Wind resolution\n", - "amr_dx0 = (amr_prob_hi[0] - amr_prob_lo[0]) / amr_n_cell[0] # dx for Level 0\n", - "amr_dy0 = (amr_prob_hi[0] - amr_prob_lo[0]) / amr_n_cell[0]\n", - "amr_dz0 = (amr_prob_hi[0] - amr_prob_lo[0]) / amr_n_cell[0]\n", - "amr_ds0_max = max(amr_dx0, amr_dy0, amr_dz0)\n", - "\n", - "amr_dx_refine = amr_dx0/(2**amr_max_level) # dx for the maximum refinement Level\n", - "amr_dy_refine = amr_dy0/(2**amr_max_level)\n", - "amr_dz_refine = amr_dz0/(2**amr_max_level)\n", - "amr_ds_refine_max = max(amr_dx_refine, amr_dy_refine, amr_dz_refine)" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "id": "unable-mapping", - "metadata": {}, - "outputs": [], - "source": [ - "### Calculate timestep output values and sampling frequency\n", - "## Low resolution domain, dt_lr\n", - "cmeander_min = float(\"inf\")\n", - "Dwake_min = float(\"inf\")\n", - "for turbkey in wts:\n", - " cmeander_min = min(cmeander_min, wts[turbkey]['Cmeander'])\n", - " Dwake_min = min(Dwake_min, wts[turbkey]['D']) # Approximate D_wake as D_rotor\n", - "Vhub = np.sqrt(incflo_velocity_hh[0]**2 + incflo_velocity_hh[1]**2)\n", - "\n", - "dt_lr_max = cmeander_min * Dwake_min / (10 * Vhub)\n", - "dt_lr = amr_fixed_dt * np.floor(dt_lr_max/amr_fixed_dt) # Ensure that dt_lr is a multiple of the AMR-Wind timestep\n", - "assert dt_lr >= amr_fixed_dt, \"AMR-Wind timestep too coarse for low resolution domain!\"\n", - "\n", - "## High resolution domain, dt_hr\n", - "fmax_max = 0\n", - "for turbkey in wts:\n", - " fmax_max = max(0, wts[turbkey]['fmax'])\n", - "dt_hr_max = 1 / (2 * fmax_max)\n", - "dt_hr = amr_fixed_dt * np.floor(dt_hr_max/amr_fixed_dt) # Ensure that dt_hr is a multiple of the AMR-Wind timestep\n", - "assert dt_hr >= amr_fixed_dt, \"AMR-Wind timestep too coarse for high resolution domain!\"\n", - "assert dt_hr <= dt_lr, \"Low resolution timestep is finer than high resolution timestep!\"\n", - "\n", - "## Sampling frequency\n", - "out_output_frequency = int(dt_hr/amr_fixed_dt)" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "id": "female-license", - "metadata": {}, - "outputs": [], - "source": [ - "### Calculate grid resolutions\n", - "## Low resolution domain, ds_lr (s = x/y/z)\n", - "## ASSUME: FAST.Farm LR zone uses Level 0 AMR-Wind grid spacing\n", - "## NOTE: ds_lr is calculated independent of any x/y/z requirements,\n", - "## just time step and velocity requiements\n", - "ds_lr_max = dt_lr * Vhub**2 / 15\n", - "ds_lr = amr_ds0_max * np.floor(ds_lr_max/amr_ds0_max) # Ensure that ds_lr is a multiple of the coarse AMR-Wind grid spacing\n", - "assert ds_lr >= amr_dx0, \"AMR-Wind Level 0 x-grid spacing too coarse for low resolution domain!\"\n", - "assert ds_lr >= amr_dy0, \"AMR-Wind Level 0 y-grid spacing too coarse for low resolution domain!\"\n", - "assert ds_lr >= amr_dz0, \"AMR-Wind Level 0 z-grid spacing too coarse for low resolution domain!\"\n", - "\n", - "## High resolution domain, ds_hr\n", - "## ASSUME: FAST.Farm HR zone lies within the region of maxmum AMR-Wind grid refinement\n", - "## NOTE: ds_hr is calculated independent of any x/y/z requirements,\n", - "## just blade chord length requirements\n", - "cmax_min = float(\"inf\")\n", - "for turbkey in wts:\n", - " cmax_min = min(cmax_min, wts[turbkey]['cmax'])\n", - "ds_hr_max = cmax_min\n", - "ds_hr = amr_ds_refine_max * np.floor(ds_hr_max/amr_ds_refine_max) # Ensure that ds_hr is a multiple of the refined AMR-Wind grid spacing\n", - "assert ds_hr >= amr_ds_refine_max, \"AMR-Wind grid spacing too coarse for high resolution domain!\"" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "id": "threatened-joseph", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "# Sampling info generated by AMRWindSamplingCreation.py\n", - "incflo.post_processing = sampling # averaging\n", - "sampling.output_frequency = 1\n", - "sampling.fields = velocity # temperature tke\n", - "sampling.labels = Low HighT0_inflow0deg HighT1_inflow0deg HighT2_inflow0deg\n" - ] - } - ], - "source": [ - "### Calculate high-level info for sampling\n", - "out_labels = [\"Low\"]\n", - "for turbkey in wts:\n", - " out_labels.append(f\"High{wts[turbkey]['name']}_inflow0deg\")\n", - "out_labels_str = \" \".join(str(item) for item in out_labels)\n", - "\n", - "print(f\"# Sampling info generated by AMRWindSamplingCreation.py\")\n", - "print(f\"incflo.post_processing = {postproc_name} # averaging\")\n", - "print(f\"{postproc_name}.output_frequency = {out_output_frequency}\")\n", - "print(f\"{postproc_name}.fields = velocity # temperature tke\")\n", - "\n", - "print(f\"{postproc_name}.labels = {out_labels_str}\")" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "id": "mechanical-agent", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "# Low sampling grid spacing: 10.0 m\n", - "sampling.Low.type = PlaneSampler\n", - "sampling.Low.num_points = 78 206\n", - "sampling.Low.origin = 885.0 2165.0\n", - "sampling.Low.axis1 = 770.0 0.0 0.0\n", - "sampling.Low.axis2 = 0.0 2050.0 0.0\n", - "sampling.Low.normal = 0.0 0.0 1.0\n", - "sampling.Low.offsets = 0 10 20 30 40 50 60 70 80 90 100 110 120 130 140 150 160 170 180 190 200 210 220 230 240 250 260 270 280\n" - ] - } - ], - "source": [ - "### Calculate low resolution grid placement\n", - "# Calculate minimum/maximum LR domain extents\n", - "wt_all_x_min = float(\"inf\") # Minimum x-value of any turbine\n", - "wt_all_x_max = -1*float(\"inf\")\n", - "wt_all_y_min = float(\"inf\")\n", - "wt_all_y_max = -1*float(\"inf\")\n", - "wt_all_z_max = -1*float(\"inf\") # Tallest rotor disk point of any turbine\n", - "Drot_max = -1*float(\"inf\")\n", - "for turbkey in wts:\n", - " wt_all_x_min = min(wt_all_x_min, wts[turbkey]['x'])\n", - " wt_all_x_max = max(wt_all_x_max, wts[turbkey]['x'])\n", - " wt_all_y_min = min(wt_all_y_min, wts[turbkey]['y'])\n", - " wt_all_y_max = max(wt_all_x_min, wts[turbkey]['y'])\n", - " wt_all_z_max = max(wt_all_z_max, wts[turbkey]['zhub'] + 0.5*wts[turbkey]['D'])\n", - " Drot_max = max(Drot_max, wts[turbkey]['D'])\n", - " \n", - "x_buffer_lr = 3 * Drot_max\n", - "y_buffer_lr = 3 * Drot_max\n", - "z_buffer_lr = 1 * Drot_max\n", - "xlow_lr_min = wt_all_x_min - x_buffer_lr\n", - "xhigh_lr_max = wt_all_x_max + x_buffer_lr\n", - "ylow_lr_min = wt_all_y_min - y_buffer_lr\n", - "yhigh_lr_max = wt_all_y_max + y_buffer_lr\n", - "zhigh_lr_max = wt_all_z_max + z_buffer_lr\n", - "\n", - "# Calculate the minimum/maximum LR domain coordinate lengths & number of grid cells\n", - "xdist_lr_min = xhigh_lr_max - xlow_lr_min # Minumum possible length of x-extent of LR domain\n", - "xdist_lr = ds_lr * np.ceil(xdist_lr_min/ds_lr) # The `+ ds_lr` comes from the +1 to NS_LOW in Sec. 4.2.15.6.4.1.1\n", - "# TODO: adjust xdist_lr calculation by also using `inflow_deg`\n", - "nx_lr = int(xdist_lr/ds_lr) + 1\n", - "\n", - "ydist_lr_min = yhigh_lr_max - ylow_lr_min\n", - "ydist_lr = ds_lr * np.ceil(ydist_lr_min/ds_lr)\n", - "# TODO: adjust ydist_lr calculation by also using `inflow_deg`\n", - "ny_lr = int(ydist_lr/ds_lr) + 1\n", - "\n", - "zdist_lr = ds_lr * np.ceil(zhigh_lr_max/ds_lr)\n", - "nz_lr = int(zdist_lr/ds_lr) + 1\n", - "\n", - "# Calculate actual LR domain extent\n", - "# NOTE: Sampling planes should measure at AMR-Wind cell centers, not cell edges\n", - "# NOTE: Should I be using dx/dy/dz values here or ds_lr?\n", - "# - I think it's correct to use ds_lr to get to the xlow values,\n", - "# but then offset by 0.5*amr_dx0\n", - "xlow_lr = ds_lr * np.floor(xlow_lr_min/ds_lr) - 0.5*amr_dx0\n", - "xhigh_lr = xlow_lr + xdist_lr\n", - "ylow_lr = ds_lr * np.floor(ylow_lr_min/ds_lr) - 0.5*amr_dy0\n", - "yhigh_lr = ylow_lr + ydist_lr\n", - "zlow_lr = 0.5 * amr_dz0 # Lowest z point is half the height of the lowest grid cell\n", - "zhigh_lr = zlow_lr + zdist_lr\n", - "zoffsets_lr = np.arange(zlow_lr, zhigh_lr+ds_lr, ds_lr) - zlow_lr\n", - "zoffsets_lr_str = \" \".join(str(int(item)) for item in zoffsets_lr)\n", - "\n", - "assert xhigh_lr < amr_prob_hi[0], \"LR domain extends beyond maximum AMR-Wind x-extent!\"\n", - "assert xlow_lr > amr_prob_lo[0], \"LR domain extends beyond minimum AMR-Wind x-extent!\"\n", - "assert yhigh_lr < amr_prob_hi[1], \"LR domain extends beyond maximum AMR-Wind y-extent!\"\n", - "assert ylow_lr > amr_prob_lo[1], \"LR domain extends beyond minimum AMR-Wind y-extent!\"\n", - "assert zhigh_lr < amr_prob_hi[2], \"LR domain extends beyond maximum AMR-Wind z-extent!\"\n", - "assert zlow_lr > amr_prob_lo[2], \"LR domain extends beyond minimum AMR-Wind z-extent!\"\n", - "\n", - "# Reformat info for AMR-Wind input file\n", - "print(f\"# Low sampling grid spacing: {ds_lr} m\")\n", - "print(f\"{postproc_name}.Low.type = PlaneSampler\")\n", - "print(f\"{postproc_name}.Low.num_points = {nx_lr} {ny_lr}\")\n", - "print(f\"{postproc_name}.Low.origin = {xlow_lr:.1f} {ylow_lr:.1f}\") # Round the float output\n", - "print(f\"{postproc_name}.Low.axis1 = {xdist_lr:.1f} 0.0 0.0\") # Assume: axis1 oriented parallel to AMR-Wind x-axis\n", - "print(f\"{postproc_name}.Low.axis2 = 0.0 {ydist_lr:.1f} 0.0\") # Assume: axis2 oriented parallel to AMR-Wind y-axis\n", - "print(f\"{postproc_name}.Low.normal = 0.0 0.0 1.0\")\n", - "print(f\"{postproc_name}.Low.offsets = {zoffsets_lr_str}\")" - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "id": "overhead-hypothesis", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "# Turbine T0 at (x,y) = (1280.0, 2560), with D = 126.9, grid spacing = 5.0\n", - "sampling.T0.type = PlaneSampler\n", - "sampling.T0.num_points = 32 32\n", - "sampling.T0.origin = 1197.5 2477.5\n", - "sampling.T0.axis1 = 155.0 0.0 0.0\n", - "sampling.T0.axis2 = 0.0 155.0 0.0\n", - "sampling.T0.normal = 0.0 0.0 1.0\n", - "sampling.T0.offsets = 0 5 10 15 20 25 30 35 40 45 50 55 60 65 70 75 80 85 90 95 100 105 110 115 120 125 130 135 140 145 150 155 160 165 170 175 180 185 190 195 200 205 210 215 220 225 230\n" - ] - } - ], - "source": [ - "### Calculate high resolution grid placements\n", - "turbkey = 0\n", - "wt_x = wts[turbkey]['x']\n", - "wt_y = wts[turbkey]['y']\n", - "wt_z = wts[turbkey]['zhub'] + 0.5*wts[turbkey]['D']\n", - "wt_D = wts[turbkey]['D']\n", - "wt_name = wts[turbkey]['name']\n", - "\n", - "# Calculate minimum/maximum HR domain extents\n", - "x_buffer_hr = 0.6 * wt_D\n", - "y_buffer_hr = 0.6 * wt_D\n", - "z_buffer_hr = 0.6 * wt_D\n", - "\n", - "xlow_hr_min = wt_x - x_buffer_hr\n", - "xhigh_hr_max = wt_x + x_buffer_hr\n", - "ylow_hr_min = wt_y - y_buffer_hr\n", - "yhigh_hr_max = wt_y + y_buffer_hr\n", - "zhigh_hr_max = wt_z + z_buffer_hr\n", - "\n", - "# Calculate the minimum/maximum HR domain coordinate lengths & number of grid cells\n", - "xdist_hr_min = xhigh_hr_max - xlow_hr_min # Minumum possible length of x-extent of HR domain\n", - "xdist_hr = ds_hr * np.ceil(xdist_hr_min/ds_hr) \n", - "nx_hr = int(xdist_hr/ds_hr) + 1\n", - "\n", - "ydist_hr_min = yhigh_hr_max - ylow_hr_min\n", - "ydist_hr = ds_hr * np.ceil(ydist_hr_min/ds_hr)\n", - "ny_hr = int(ydist_hr/ds_hr) + 1\n", - "\n", - "zdist_hr = ds_hr * np.ceil(zhigh_hr_max/ds_hr)\n", - "nz_hr = int(zdist_hr/ds_hr) + 1\n", - "\n", - "# Calculate actual HR domain extent\n", - "# NOTE: Sampling planes should measure at AMR-Wind cell centers, not cell edges\n", - "xlow_hr = ds_hr * np.floor(xlow_hr_min/ds_hr) - 0.5*amr_dx_refine\n", - "xhigh_hr = xlow_hr + xdist_hr\n", - "ylow_hr = ds_hr * np.floor(ylow_hr_min/ds_hr) - 0.5*amr_dy_refine\n", - "yhigh_hr = ylow_hr + ydist_hr\n", - "zlow_hr = zlow_lr\n", - "zhigh_hr = zlow_hr + zdist_hr\n", - "zoffsets_hr = np.arange(zlow_hr, zhigh_hr+ds_hr, ds_hr) - zlow_hr\n", - "zoffsets_hr_str = \" \".join(str(int(item)) for item in zoffsets_hr)\n", - "\n", - "assert xhigh_hr < amr_prob_hi[0], f\"HR domain for {wt_name} extends beyond maximum AMR-Wind x-extent!\"\n", - "assert xlow_hr > amr_prob_lo[0], f\"HR domain for {wt_name} extends beyond minimum AMR-Wind x-extent!\"\n", - "assert yhigh_hr < amr_prob_hi[1], f\"HR domain for {wt_name} extends beyond maximum AMR-Wind y-extent!\"\n", - "assert ylow_hr > amr_prob_lo[1], f\"HR domain for {wt_name} extends beyond minimum AMR-Wind y-extent!\"\n", - "assert zhigh_hr < amr_prob_hi[2], f\"HR domain for {wt_name} extends beyond maximum AMR-Wind z-extent!\"\n", - "assert zlow_hr > amr_prob_lo[2], f\"HR domain for {wt_name} extends beyond minimum AMR-Wind z-extent!\"\n", - "\n", - "# Reformat info for AMR-Wind input file\n", - "print(f\"# Turbine {wt_name} at (x,y) = ({wt_x}, {wt_y}), with D = {wt_D}, grid spacing = {ds_hr}\")\n", - "print(f\"{postproc_name}.{wt_name}.type = PlaneSampler\")\n", - "print(f\"{postproc_name}.{wt_name}.num_points = {nx_hr} {ny_hr}\")\n", - "print(f\"{postproc_name}.{wt_name}.origin = {xlow_hr:.1f} {ylow_hr:.1f}\") # Round the float output\n", - "print(f\"{postproc_name}.{wt_name}.axis1 = {xdist_hr:.1f} 0.0 0.0\") # Assume: axis1 oriented parallel to AMR-Wind x-axis\n", - "print(f\"{postproc_name}.{wt_name}.axis2 = 0.0 {ydist_hr:.1f} 0.0\") # Assume: axis2 oriented parallel to AMR-Wind y-axis\n", - "print(f\"{postproc_name}.{wt_name}.normal = 0.0 0.0 1.0\")\n", - "print(f\"{postproc_name}.{wt_name}.offsets = {zoffsets_hr_str}\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "classical-portland", - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "wfp", - "language": "python", - "name": "wfp" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.7.10" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} From de02f5fa1b14031339a9d067a491250d810ded1a Mon Sep 17 00:00:00 2001 From: Alex Rybchuk Date: Mon, 13 Feb 2023 12:18:22 -0700 Subject: [PATCH 041/124] Move sampling plane calcs fully into AMRWind class --- pyFAST/fastfarm/AMRWindSimulation.py | 284 ++++++++++++++++++++++++++- 1 file changed, 275 insertions(+), 9 deletions(-) diff --git a/pyFAST/fastfarm/AMRWindSimulation.py b/pyFAST/fastfarm/AMRWindSimulation.py index 1fcd4f6..f655d40 100644 --- a/pyFAST/fastfarm/AMRWindSimulation.py +++ b/pyFAST/fastfarm/AMRWindSimulation.py @@ -11,7 +11,8 @@ class AMRWindSimulation: simulation ''' - def __init__(self, dt: float, prob_lo: tuple, prob_hi: tuple, + def __init__(self, wts:dict, + dt: float, prob_lo: tuple, prob_hi: tuple, n_cell: tuple, max_level: int, incflo_velocity_hh: tuple, postproc_name='sampling'): @@ -22,6 +23,7 @@ def __init__(self, dt: float, prob_lo: tuple, prob_hi: tuple, * incflo_velocity_hh: velocity vector, specifically at hub height ''' # Process inputs + self.wts = wts self.dt = dt self.prob_lo = prob_lo self.prob_hi = prob_hi @@ -49,7 +51,9 @@ def __init__(self, dt: float, prob_lo: tuple, prob_hi: tuple, # Run extra functions self._checkInputs() - self._calc_amr_params() + self._calc_simple_params() + self._calc_sampling_params() + self._check_sampling_params() def _checkInputs(self): ''' @@ -68,7 +72,7 @@ def _checkInputs(self): if (self.prob_lo[2] >= self.prob_hi[2]): raise ValueError("z-component of prob_lo larger than z-component of prob_hi") - def _calc_amr_params(self): + def _calc_simple_params(self): ''' Calculate simulation parameters, given only AMR-Wind inputs ''' @@ -82,11 +86,266 @@ def _calc_amr_params(self): self.dx_refine = self.dx0/(2**self.max_level) self.dy_refine = self.dy0/(2**self.max_level) self.dz_refine = self.dz0/(2**self.max_level) - self.refine_max = max(self.dx_refine, self.dy_refine, self.dz_refine) + self.ds_refine_max = max(self.dx_refine, self.dy_refine, self.dz_refine) # Hub height wind speed self.vhub = np.sqrt(self.incflo_velocity_hh[0]**2 + self.incflo_velocity_hh[1]**2) + def _calc_sampling_params(self): + ''' + Calculate parameters for sampling planes + ''' + + ### ~~~~~~~~~ Calculate high-level info for AMR-Wind sampling ~~~~~~~~~ + sampling_labels = ["Low"] + for turbkey in self.wts: + if 'name' in self.wts[turbkey].keys(): + wt_name = self.wts[turbkey]['name'] + else: + wt_name = f'T{turbkey}' + sampling_labels.append(f"High{wt_name}_inflow0deg") + self.sampling_labels = sampling_labels + + ### ~~~~~~~~~ Calculate timestep values and AMR-Wind plane sampling frequency ~~~~~~~~~ + ## Low resolution domain, dt_low_les + cmeander_min = float("inf") + Dwake_min = float("inf") + for turbkey in self.wts: + cmeander_min = min(cmeander_min, self.wts[turbkey]['Cmeander']) + Dwake_min = min(Dwake_min, self.wts[turbkey]['D']) # Approximate D_wake as D_rotor + + dt_lr_max = cmeander_min * Dwake_min / (10 * self.vhub) + self.dt_low_les = self.dt * np.floor(dt_lr_max/self.dt) # Ensure that dt_lr is a multiple of the AMR-Wind timestep + + ## High resolution domain, dt_high_les + fmax_max = 0 + for turbkey in self.wts: + fmax_max = max(0, self.wts[turbkey]['fmax']) + dt_hr_max = 1 / (2 * fmax_max) + self.dt_high_les = self.dt * np.floor(dt_hr_max/self.dt) # Ensure that dt_hr is a multiple of the AMR-Wind timestep + + ## Sampling frequency + self.output_frequency = int(self.dt_high_les/self.dt) + + ### ~~~~~~~~~ Calculate grid resolutions ~~~~~~~~~ + ## Low resolution domain, ds_lr (s = x/y/z) + # ASSUME: FAST.Farm LR zone uses Level 0 AMR-Wind grid spacing + # NOTE: ds_lr is calculated independent of any x/y/z requirements, + # just time step and velocity requiements + ds_lr_max = self.dt_low_les * self.vhub**2 / 15 + self.ds_low_les = self.ds0_max * np.floor(ds_lr_max/self.ds0_max) # Ensure that ds_lr is a multiple of the coarse AMR-Wind grid spacing + self.ds_lr = self.ds_low_les + + ## High resolution domain, ds_hr + # ASSUME: FAST.Farm HR zone lies within the region of maxmum AMR-Wind grid refinement + # NOTE: ds_hr is calculated independent of any x/y/z requirements, + # just blade chord length requirements + cmax_min = float("inf") + for turbkey in self.wts: + cmax_min = min(cmax_min, self.wts[turbkey]['cmax']) + ds_hr_max = cmax_min + self.ds_high_les = self.ds_refine_max * np.floor(ds_hr_max/self.ds_refine_max) # Ensure that ds_hr is a multiple of the refined AMR-Wind grid spacing + self.ds_hr = self.ds_high_les + + ### ~~~~~~~~~ Calculate low resolution grid placement ~~~~~~~~~ + # Calculate minimum/maximum LR domain extents + wt_all_x_min = float("inf") # Minimum x-value of any turbine + wt_all_x_max = -1*float("inf") + wt_all_y_min = float("inf") + wt_all_y_max = -1*float("inf") + wt_all_z_max = -1*float("inf") # Tallest rotor disk point of any turbine + Drot_max = -1*float("inf") + for turbkey in self.wts: + wt_all_x_min = min(wt_all_x_min, self.wts[turbkey]['x']) + wt_all_x_max = max(wt_all_x_max, self.wts[turbkey]['x']) + wt_all_y_min = min(wt_all_y_min, self.wts[turbkey]['y']) + wt_all_y_max = max(wt_all_x_min, self.wts[turbkey]['y']) + wt_all_z_max = max(wt_all_z_max, self.wts[turbkey]['zhub'] + 0.5*self.wts[turbkey]['D']) + Drot_max = max(Drot_max, self.wts[turbkey]['D']) + + x_buffer_lr = 3 * Drot_max + y_buffer_lr = 3 * Drot_max + z_buffer_lr = 1 * Drot_max # This buffer size was arbitrarily chosen + xlow_lr_min = wt_all_x_min - x_buffer_lr + xhigh_lr_max = wt_all_x_max + x_buffer_lr + ylow_lr_min = wt_all_y_min - y_buffer_lr + yhigh_lr_max = wt_all_y_max + y_buffer_lr + zhigh_lr_max = wt_all_z_max + z_buffer_lr + + # Calculate the minimum/maximum LR domain coordinate lengths & number of grid cells + xdist_lr_min = xhigh_lr_max - xlow_lr_min # Minumum possible length of x-extent of LR domain + self.xdist_lr = self.ds_low_les * np.ceil(xdist_lr_min/self.ds_low_les) # The `+ ds_lr` comes from the +1 to NS_LOW in Sec. 4.2.15.6.4.1.1 + # TODO: adjust xdist_lr calculation by also using `inflow_deg` + self.nx_lr = int(self.xdist_lr/self.ds_low_les) + 1 + + ydist_lr_min = yhigh_lr_max - ylow_lr_min + self.ydist_lr = self.ds_low_les * np.ceil(ydist_lr_min/self.ds_low_les) + # TODO: adjust ydist_lr calculation by also using `inflow_deg` + self.ny_lr = int(self.ydist_lr/self.ds_low_les) + 1 + + self.zdist_lr = self.ds_low_les * np.ceil(zhigh_lr_max/self.ds_low_les) + self.nz_lr = int(self.zdist_lr/self.ds_low_les) + 1 + + ## Calculate actual LR domain extent + # NOTE: Sampling planes should measure at AMR-Wind cell centers, not cell edges + # NOTE: Should we use dx/dy/dz values here or ds_lr? + # - AR: I think it's correct to use ds_lr to get to the xlow values, + # but then offset by 0.5*amr_dx0 + self.xlow_lr = self.ds_low_les * np.floor(xlow_lr_min/self.ds_low_les) - 0.5*self.dx0 + self.xhigh_lr = self.xlow_lr + self.xdist_lr + self.ylow_lr = self.ds_low_les * np.floor(ylow_lr_min/self.ds_low_les) - 0.5*self.dy0 + self.yhigh_lr = self.ylow_lr + self.ydist_lr + self.zlow_lr = 0.5 * self.dz0 # Lowest z point is half the height of the lowest grid cell + self.zhigh_lr = self.zlow_lr + self.zdist_lr + self.zoffsets_lr = np.arange(self.zlow_lr, self.zhigh_lr+self.ds_low_les, self.ds_low_les) - self.zlow_lr + + ## Save out info + # self.extent_low = ? # TODO: How should this be formatted? + + ### ~~~~~~~~~ Calculate high resolution grid placement ~~~~~~~~~ + hr_domains = {} + for turbkey in self.wts: + wt_x = self.wts[turbkey]['x'] + wt_y = self.wts[turbkey]['y'] + wt_z = self.wts[turbkey]['zhub'] + 0.5*self.wts[turbkey]['D'] + wt_D = self.wts[turbkey]['D'] + + # Calculate minimum/maximum HR domain extents + x_buffer_hr = 0.6 * wt_D + y_buffer_hr = 0.6 * wt_D + z_buffer_hr = 0.6 * wt_D + + xlow_hr_min = wt_x - x_buffer_hr + xhigh_hr_max = wt_x + x_buffer_hr + ylow_hr_min = wt_y - y_buffer_hr + yhigh_hr_max = wt_y + y_buffer_hr + zhigh_hr_max = wt_z + z_buffer_hr + + # Calculate the minimum/maximum HR domain coordinate lengths & number of grid cells + xdist_hr_min = xhigh_hr_max - xlow_hr_min # Minumum possible length of x-extent of HR domain + xdist_hr = self.ds_high_les * np.ceil(xdist_hr_min/self.ds_high_les) + nx_hr = int(xdist_hr/self.ds_high_les) + 1 + + ydist_hr_min = yhigh_hr_max - ylow_hr_min + ydist_hr = self.ds_high_les * np.ceil(ydist_hr_min/self.ds_high_les) + ny_hr = int(ydist_hr/self.ds_high_les) + 1 + + zdist_hr = self.ds_high_les * np.ceil(zhigh_hr_max/self.ds_high_les) + nz_hr = int(zdist_hr/self.ds_high_les) + 1 + + # Calculate actual HR domain extent + # NOTE: Sampling planes should measure at AMR-Wind cell centers, not cell edges + xlow_hr = self.ds_high_les * np.floor(xlow_hr_min/self.ds_high_les) - 0.5*self.dx_refine + xhigh_hr = xlow_hr + xdist_hr + ylow_hr = self.ds_high_les * np.floor(ylow_hr_min/self.ds_high_les) - 0.5*self.dy_refine + yhigh_hr = ylow_hr + ydist_hr + zlow_hr = self.zlow_lr / (2**self.max_level) + zhigh_hr = zlow_hr + zdist_hr + zoffsets_hr = np.arange(zlow_hr, zhigh_hr+self.ds_high_les, self.ds_high_les) - zlow_hr + + # Save info + # self.extent_high = ? # TODO: How should this be formatted? + + hr_turb_info = {'nx_hr': nx_hr, 'ny_hr': ny_hr, 'nz_hr': nz_hr, + 'xdist_hr': xdist_hr, 'ydist_hr': ydist_hr, 'zdist_hr': zdist_hr, + 'xlow_hr': xlow_hr, 'ylow_hr': ylow_hr, 'zlow_hr': zlow_hr, + 'xhigh_hr': xhigh_hr, 'yhigh_hr': yhigh_hr, 'zhigh_hr': zhigh_hr, + 'zoffsets_hr': zoffsets_hr} + hr_domains[turbkey] = hr_turb_info + self.hr_domains = hr_domains + + def _check_sampling_params(self): + ''' + Check the values of parameters that were calculated by _calc_sampling_params + ''' + + ## Timestep checks + if self.dt_low_les < self.dt: + raise ValueError(f"AMR-Wind timestep too coarse for low resolution domain! AMR-Wind timestep must be at least {self.dt_low_les} sec.") + if self.dt_high_les < self.dt: + raise ValueError(f"AMR-Wind timestep too coarse for high resolution domain! AMR-Wind timestep must be at least {self.dt_high_les} sec.") + if self.dt_high_les > self.dt_low_les: + raise ValueError(f"Low resolution timestep ({self.dt_low_les}) is finer than high resolution timestep ({self.dt_high_les})!") + + ## Grid resolution checks + if self.ds_low_les < self.dx0: + raise ValueError(f"AMR-Wind Level 0 x-grid spacing too coarse for low resolution domain! AMR-Wind x-grid spacing must be at least {self.ds_low_les} m.") + if self.ds_low_les < self.dy0: + raise ValueError(f"AMR-Wind Level 0 y-grid spacing too coarse for low resolution domain! AMR-Wind y-grid spacing must be at least {self.ds_low_les} m.") + if self.ds_low_les < self.dz0: + raise ValueError(f"AMR-Wind Level 0 z-grid spacing too coarse for low resolution domain! AMR-Wind z-grid spacing must be at least {self.ds_low_les} m.") + if self.ds_high_les < self.ds_refine_max: + raise ValueError(f"AMR-Wind grid spacing too coarse for high resolution domain! AMR-Wind grid spacing must be at least {self.ds_high_les} m.") + + ## Low resolution domain extent checks + if self.xhigh_lr > self.prob_hi[0]: + raise ValueError("LR domain extends beyond maximum AMR-Wind x-extent!") + if self.xlow_lr < self.prob_lo[0]: + raise ValueError("LR domain extends beyond minimum AMR-Wind x-extent!") + if self.yhigh_lr > self.prob_hi[1]: + raise ValueError("LR domain extends beyond maximum AMR-Wind y-extent!") + if self.ylow_lr < self.prob_lo[1]: + raise ValueError("LR domain extends beyond minimum AMR-Wind y-extent!") + if self.zhigh_lr > self.prob_hi[2]: + raise ValueError("LR domain extends beyond maximum AMR-Wind z-extent!") + if self.zlow_lr < self.prob_lo[2]: + raise ValueError("LR domain extends beyond minimum AMR-Wind z-extent!") + + ## Check that sampling grids are at cell centers + # Low resolution grid + amr_xgrid_level0 = np.arange(self.prob_lo[0], self.prob_hi[0], self.dx0) + amr_ygrid_level0 = np.arange(self.prob_lo[1], self.prob_hi[1], self.dy0) + amr_zgrid_level0 = np.arange(self.prob_lo[2], self.prob_hi[2], self.dz0) + + amr_xgrid_level0_cc = amr_xgrid_level0 + 0.5*self.dx0 # Cell-centered AMR-Wind x-grid + amr_ygrid_level0_cc = amr_ygrid_level0 + 0.5*self.dy0 + amr_zgrid_level0_cc = amr_zgrid_level0 + 0.5*self.dz0 + + sampling_xgrid_lr = self.xlow_lr + self.ds_lr*np.arange(self.nx_lr) + sampling_ygrid_lr = self.ylow_lr + self.ds_lr*np.arange(self.ny_lr) + sampling_zgrid_lr = self.zlow_lr + self.zoffsets_lr + + # TODO: These for loops could be replaced with a faster operation + for coord in sampling_xgrid_lr: + if coord not in amr_xgrid_level0_cc: + raise ValueError("Low resolution x-sampling grid is not cell cenetered with AMR-Wind's grid!") + for coord in sampling_ygrid_lr: + if coord not in amr_ygrid_level0_cc: + raise ValueError("Low resolution y-sampling grid is not cell cenetered with AMR-Wind's grid!") + for coord in sampling_zgrid_lr: + if coord not in amr_zgrid_level0_cc: + raise ValueError("Low resolution z-sampling grid is not cell cenetered with AMR-Wind's grid!") + + # High resolution grids (span the entire domain to make this check easier) + amr_xgrid_refine = np.arange(self.prob_lo[0], self.prob_hi[0], self.dx_refine) + amr_ygrid_refine = np.arange(self.prob_lo[1], self.prob_hi[1], self.dy_refine) + amr_zgrid_refine = np.arange(self.prob_lo[2], self.prob_hi[2], self.dz_refine) + + amr_xgrid_refine_cc = amr_xgrid_refine + 0.5*self.dx_refine + amr_ygrid_refine_cc = amr_ygrid_refine + 0.5*self.dy_refine + amr_zgrid_refine_cc = amr_zgrid_refine + 0.5*self.dz_refine + + for turbkey in self.hr_domains: + nx_hr = self.hr_domains[turbkey]['nx_hr'] + ny_hr = self.hr_domains[turbkey]['ny_hr'] + xlow_hr = self.hr_domains[turbkey]['xlow_hr'] + ylow_hr = self.hr_domains[turbkey]['ylow_hr'] + + sampling_xgrid_hr = xlow_hr + self.ds_hr*np.arange(nx_hr) + sampling_ygrid_hr = ylow_hr + self.ds_hr*np.arange(ny_hr) + sampling_zgrid_hr = self.hr_domains[turbkey]['zlow_hr'] + self.hr_domains[turbkey]['zoffsets_hr'] + + # TODO: These for loops could be replaced with a faster operation + for coord in sampling_xgrid_hr: + if coord not in amr_xgrid_refine_cc: + raise ValueError("High resolution x-sampling grid is not cell cenetered with AMR-Wind's grid!") + for coord in sampling_ygrid_hr: + if coord not in amr_ygrid_refine_cc: + raise ValueError("High resolution y-sampling grid is not cell cenetered with AMR-Wind's grid!") + for coord in sampling_zgrid_hr: + if coord not in amr_zgrid_refine_cc: + raise ValueError("High resolution z-sampling grid is not cell cenetered with AMR-Wind's grid!") + def write_sampling_params(self, outdir): ''' Write out text that can be used for the sampling planes in an @@ -107,10 +366,10 @@ def write_sampling_params(self, outdir): # Write out low resolution sampling plane info zoffsets_lr_str = " ".join(str(int(item)) for item in self.zoffsets_lr) - out.write(f"\n# Low sampling grid spacing: {self.ds_lr} m\n") + out.write(f"\n# Low sampling grid spacing = {self.ds_lr} m\n") out.write(f"{self.postproc_name}.Low.type = PlaneSampler\n") out.write(f"{self.postproc_name}.Low.num_points = {self.nx_lr} {self.ny_lr}\n") - out.write(f"{self.postproc_name}.Low.origin = {self.xlow_lr:.1f} {self.ylow_lr:.1f}\n") # Round the float output + out.write(f"{self.postproc_name}.Low.origin = {self.xlow_lr:.1f} {self.ylow_lr:.1f} {self.zlow_lr:.1f}\n") # Round the float output out.write(f"{self.postproc_name}.Low.axis1 = {self.xdist_lr:.1f} 0.0 0.0\n") # Assume: axis1 oriented parallel to AMR-Wind x-axis out.write(f"{self.postproc_name}.Low.axis2 = 0.0 {self.ydist_lr:.1f} 0.0\n") # Assume: axis2 oriented parallel to AMR-Wind y-axis out.write(f"{self.postproc_name}.Low.normal = 0.0 0.0 1.0\n") @@ -118,20 +377,27 @@ def write_sampling_params(self, outdir): # Write out high resolution sampling plane info for turbkey in self.hr_domains: - wt_name = f'T{turbkey}' + wt_x = self.wts[turbkey]['x'] + wt_y = self.wts[turbkey]['y'] + wt_D = self.wts[turbkey]['D'] + if 'name' in self.wts[turbkey].keys(): + wt_name = self.wts[turbkey]['name'] + else: + wt_name = f'T{turbkey}' nx_hr = self.hr_domains[turbkey]['nx_hr'] ny_hr = self.hr_domains[turbkey]['ny_hr'] xlow_hr = self.hr_domains[turbkey]['xlow_hr'] ylow_hr = self.hr_domains[turbkey]['ylow_hr'] + zlow_hr = self.hr_domains[turbkey]['zlow_hr'] xdist_hr = self.hr_domains[turbkey]['xdist_hr'] ydist_hr = self.hr_domains[turbkey]['ydist_hr'] zoffsets_hr = self.hr_domains[turbkey]['zoffsets_hr'] zoffsets_hr_str = " ".join(str(int(item)) for item in zoffsets_hr) - out.write(f"\n# Turbine {wt_name}\n") + out.write(f"\n# Turbine {wt_name} at (x,y) = ({wt_x}, {wt_y}), with D = {wt_D}, grid spacing = {self.ds_hr} m\n") out.write(f"{self.postproc_name}.{wt_name}.type = PlaneSampler\n") out.write(f"{self.postproc_name}.{wt_name}.num_points = {nx_hr} {ny_hr}\n") - out.write(f"{self.postproc_name}.{wt_name}.origin = {xlow_hr:.1f} {ylow_hr:.1f}\n") # Round the float output + out.write(f"{self.postproc_name}.{wt_name}.origin = {xlow_hr:.1f} {ylow_hr:.1f} {zlow_hr:.1f}\n") # Round the float output out.write(f"{self.postproc_name}.{wt_name}.axis1 = {xdist_hr:.1f} 0.0 0.0\n") # Assume: axis1 oriented parallel to AMR-Wind x-axis out.write(f"{self.postproc_name}.{wt_name}.axis2 = 0.0 {ydist_hr:.1f} 0.0\n") # Assume: axis2 oriented parallel to AMR-Wind y-axis out.write(f"{self.postproc_name}.{wt_name}.normal = 0.0 0.0 1.0\n") From 4b62524a1aa8dd6a788912327a5ce9859828d07e Mon Sep 17 00:00:00 2001 From: Alex Rybchuk Date: Mon, 13 Feb 2023 19:07:39 -0700 Subject: [PATCH 042/124] Revert FFarm class to 88638ba, before AMRWind mods --- pyFAST/fastfarm/FASTFarmCaseCreation.py | 344 ++---------------------- 1 file changed, 19 insertions(+), 325 deletions(-) diff --git a/pyFAST/fastfarm/FASTFarmCaseCreation.py b/pyFAST/fastfarm/FASTFarmCaseCreation.py index 7ef058b..23c3df9 100644 --- a/pyFAST/fastfarm/FASTFarmCaseCreation.py +++ b/pyFAST/fastfarm/FASTFarmCaseCreation.py @@ -9,7 +9,6 @@ from pyFAST.input_output import FASTInputFile, FASTOutputFile, TurbSimFile, VTKFile from pyFAST.fastfarm import writeFastFarm, fastFarmTurbSimExtent, plotFastFarmSetup from pyFAST.fastfarm.TurbSimCaseCreation import TSCaseCreation, writeTimeSeriesFile -from pyFAST.fastfarm.AMRWindSimulation import AMRWindSimulation def cosd(t): return np.cos(np.deg2rad(t)) def sind(t): return np.sin(np.deg2rad(t)) @@ -18,7 +17,7 @@ def sind(t): return np.sin(np.deg2rad(t)) class FFCaseCreation: - def __init__(self, path, wts, cmax, fmax, Cmeander, tmax, zbot, vhub, shear, TIvalue, inflow_deg, dt_high_les=None, ds_high_les=None, extent_high=None, dt_low_les=None, ds_low_les=None, extent_low=None, ffbin=None, yaw_init=None, ADmodel=None, EDmodel=None, nSeeds=6, LESpath=None, sweepWakeSteering=False, sweepYawMisalignment=False, seedValues=None, refTurb_rot=0, amr=None, verbose=0): + def __init__(self, path, wts, cmax, fmax, Cmeander, tmax, zbot, vhub, shear, TIvalue, inflow_deg, dt_high_les, ds_high_les, extent_high, dt_low_les, ds_low_les, extent_low, ffbin, yaw_init=None, ADmodel=None, EDmodel=None, nSeeds=6, LESpath=None, sweepWakeSteering=False, sweepYawMisalignment=False, seedValues=None, refTurb_rot=0, verbose=0): ''' ffbin: str @@ -54,7 +53,6 @@ def __init__(self, path, wts, cmax, fmax, Cmeander, tmax, zbot, vhub, shear, TIv self.sweepYM = sweepYawMisalignment self.seedValues = seedValues self.refTurb_rot = refTurb_rot - self.amr = amr self.verbose = verbose @@ -73,13 +71,6 @@ def __init__(self, path, wts, cmax, fmax, Cmeander, tmax, zbot, vhub, shear, TIv if self.verbose>0: print(f'Creating auxiliary arrays for all conditions and cases... Done.') - if self.verbose>0: print(f'Determining box parameters...', end='\r') - if self.dt_high_les is not None: - self.DetermineBoxParameters() - self.CheckAutoBoxParameters() - if self.verbose>0: print(f'Determining box paramters... Done.') - - if self.verbose>0: print(f'Creating directory structure and copying files...', end='\r') self._create_dir_structure() if self.verbose>0: print(f'Creating directory structure and copying files... Done.') @@ -148,33 +139,21 @@ def _checkInputs(self): if t<1: raise ValueError(f'TI should be given in percentage (e.g. "10" for a 10% TI). Received {t}.') # Check the ds and dt for the high- and low-res boxes - if None not in (self.dt_high_les, self.dt_low_les, self.ds_high_les, self.ds_low_les, self.extent_high, self.extent_low): - # All values for ds, dt, and extent were given - if not (np.array(self.extent_low)>=0).all(): - raise ValueError(f'The array for low-res box extents should be given with positive values') - if self.dt_low_les%(self.dt_high_les-1e-15) > 1e-14: - raise ValueError(f'The temporal resolution dT_Low should be a multiple of dT_High') - if self.dt_low_les < self.dt_high_les: - raise ValueError(f'The temporal resolution dT_High should not be greater than dT_Low on the LES side') - if self.ds_low_les < self.ds_high_les: - raise ValueError(f'The grid resolution dS_High should not be greater than dS_Low on the LES side') - if not isinstance(self.extent_high, (float,int)): - raise ValueError(f'The extent_high should be a scalar') - if self.extent_high<=0: - raise ValueError(f'The extent of high boxes should be positive') - else: - # At least one of the ds, dt, extent are None. If that is the case, all of them should be None - if None in (self.dt_high_les, self.dt_low_les, self.ds_high_les, self.ds_low_les, self.extent_high, self.extent_low): - raise ValueError (f'The dt, ds, and extent for high and low res boxes need to either be fully' \ - f'given or computed. Some were given, but some were not.') + if not (np.array(self.extent_low)>=0).all(): + raise ValueError(f'The array for low-res box extents should be given with positive values') + if self.dt_low_les%(self.dt_high_les-1e-15) > 1e-14: + raise ValueError(f'The temporal resolution dT_Low should be a multiple of dT_High') + if self.dt_low_les < self.dt_high_les: + raise ValueError(f'The temporal resolution dT_High should not be greater than dT_Low on the LES side') + if self.ds_low_les < self.ds_high_les: + raise ValueError(f'The grid resolution dS_High should not be greater than dS_Low on the LES side') + if not isinstance(self.extent_high, (float,int)): + raise ValueError(f'The extent_high should be a scalar') + if self.extent_high<=0: + raise ValueError(f'The extent of high boxes should be positive') # Check the FAST.Farm binary - if self.ffbin is None: - import shutil - self.ffbin = shutil.which('FAST.Farm') - if verbose>1: - print(f'FAST.Farm binary not given. Using the following from your $PATH: {self.ffbin}.') if not os.path.isfile(self.ffbin): raise ValueError (f'The FAST.Farm binary given does not appear to exist') @@ -211,11 +190,6 @@ def _checkInputs(self): raise ValueError (f'The path {self.LESpath} does not exist') self.inflowStr = 'LES' - # Check class of amr - if self.amr is not None: - if not isinstance(self.amr, AMRWindSimulation): - raise ValueError(f"The class of amr should be AMRWindSimulation, but is {type(self.amr)}") - # Check the reference turbine for rotation if self.refTurb_rot >= self.nTurbines: raise ValueError(f'The index for the reference turbine for the farm to be rotated around is greater than the number of turbines') @@ -773,222 +747,6 @@ def _rotate_wts(self): self.wts_rot_ds = pd.DataFrame.from_dict(wts_rot, orient='index').to_xarray().rename({'level_0':'inflow_deg','level_1':'turbine'}) - - def DetermineBoxParameters(self): - ''' - Calculate the following variables for FAST.Farm: - dt_high_les, ds_high_les, extent_high, dt_low_les, ds_low_les, and extent_low - And calculate information for the AMR-Wind simulation: - sampling_labels, output_frequency, specs for lr domain, specs for hr domains - ''' - amr = self.amr - - ### ~~~~~~~~~ Calculate high-level info for AMR-Wind sampling ~~~~~~~~~ - sampling_labels = ["Low"] - for turbkey in self.wts: - sampling_labels.append(f"High{turbkey}_inflow0deg") - amr.sampling_labels = sampling_labels - - ### ~~~~~~~~~ Calculate timestep values and AMR-Wind plane sampling frequency ~~~~~~~~~ - ## Low resolution domain, dt_low_les - cmeander_min = float("inf") - Dwake_min = float("inf") - for turbkey in self.wts: - cmeander_min = min(cmeander_min, self.wts[turbkey]['Cmeander']) - Dwake_min = min(Dwake_min, self.wts[turbkey]['D']) # Approximate D_wake as D_rotor - - dt_lr_max = cmeander_min * Dwake_min / (10 * amr.vhub) - self.dt_low_les = amr.dt * np.floor(dt_lr_max/amr.dt) # Ensure that dt_lr is a multiple of the AMR-Wind timestep - - ## High resolution domain, dt_high_les - fmax_max = 0 - for turbkey in self.wts: - fmax_max = max(0, self.wts[turbkey]['fmax']) - dt_hr_max = 1 / (2 * fmax_max) - self.dt_high_les = amr.dt * np.floor(dt_hr_max/amr.dt) # Ensure that dt_hr is a multiple of the AMR-Wind timestep - - ## Sampling frequency - amr.output_frequency = int(self.dt_high_les/amr.dt) - - ### ~~~~~~~~~ Calculate grid resolutions ~~~~~~~~~ - ## Low resolution domain, ds_lr (s = x/y/z) - # ASSUME: FAST.Farm LR zone uses Level 0 AMR-Wind grid spacing - # NOTE: ds_lr is calculated independent of any x/y/z requirements, - # just time step and velocity requiements - ds_lr_max = self.dt_low_les * amr.vhub**2 / 15 - self.ds_low_les = amr.ds0_max * np.floor(ds_lr_max/amr.ds0_max) # Ensure that ds_lr is a multiple of the coarse AMR-Wind grid spacing - amr.ds_lr = self.ds_low_les - - ## High resolution domain, ds_hr - # ASSUME: FAST.Farm HR zone lies within the region of maxmum AMR-Wind grid refinement - # NOTE: ds_hr is calculated independent of any x/y/z requirements, - # just blade chord length requirements - cmax_min = float("inf") - for turbkey in self.wts: - cmax_min = min(cmax_min, self.wts[turbkey]['cmax']) - ds_hr_max = cmax_min - self.ds_high_les = amr.ds_refine_max * np.floor(ds_hr_max/amr.ds_refine_max) # Ensure that ds_hr is a multiple of the refined AMR-Wind grid spacing - amr.ds_hr = self.ds_high_les - - ### ~~~~~~~~~ Calculate low resolution grid placement ~~~~~~~~~ - # Calculate minimum/maximum LR domain extents - wt_all_x_min = float("inf") # Minimum x-value of any turbine - wt_all_x_max = -1*float("inf") - wt_all_y_min = float("inf") - wt_all_y_max = -1*float("inf") - wt_all_z_max = -1*float("inf") # Tallest rotor disk point of any turbine - Drot_max = -1*float("inf") - for turbkey in self.wts: - wt_all_x_min = min(wt_all_x_min, self.wts[turbkey]['x']) - wt_all_x_max = max(wt_all_x_max, self.wts[turbkey]['x']) - wt_all_y_min = min(wt_all_y_min, self.wts[turbkey]['y']) - wt_all_y_max = max(wt_all_x_min, self.wts[turbkey]['y']) - wt_all_z_max = max(wt_all_z_max, self.wts[turbkey]['zhub'] + 0.5*self.wts[turbkey]['D']) - Drot_max = max(Drot_max, self.wts[turbkey]['D']) - - x_buffer_lr = 3 * Drot_max - y_buffer_lr = 3 * Drot_max - z_buffer_lr = 1 * Drot_max # This buffer size was arbitrarily chosen - xlow_lr_min = wt_all_x_min - x_buffer_lr - xhigh_lr_max = wt_all_x_max + x_buffer_lr - ylow_lr_min = wt_all_y_min - y_buffer_lr - yhigh_lr_max = wt_all_y_max + y_buffer_lr - zhigh_lr_max = wt_all_z_max + z_buffer_lr - - # Calculate the minimum/maximum LR domain coordinate lengths & number of grid cells - xdist_lr_min = xhigh_lr_max - xlow_lr_min # Minumum possible length of x-extent of LR domain - xdist_lr = self.ds_low_les * np.ceil(xdist_lr_min/self.ds_low_les) # The `+ ds_lr` comes from the +1 to NS_LOW in Sec. 4.2.15.6.4.1.1 - # TODO: adjust xdist_lr calculation by also using `inflow_deg` - nx_lr = int(xdist_lr/self.ds_low_les) + 1 - - ydist_lr_min = yhigh_lr_max - ylow_lr_min - ydist_lr = self.ds_low_les * np.ceil(ydist_lr_min/self.ds_low_les) - # TODO: adjust ydist_lr calculation by also using `inflow_deg` - ny_lr = int(ydist_lr/self.ds_low_les) + 1 - - zdist_lr = self.ds_low_les * np.ceil(zhigh_lr_max/self.ds_low_les) - nz_lr = int(zdist_lr/self.ds_low_les) + 1 - - ## Calculate actual LR domain extent - # NOTE: Sampling planes should measure at AMR-Wind cell centers, not cell edges - # NOTE: Should we use dx/dy/dz values here or ds_lr? - # - AR: I think it's correct to use ds_lr to get to the xlow values, - # but then offset by 0.5*amr_dx0 - xlow_lr = self.ds_low_les * np.floor(xlow_lr_min/self.ds_low_les) - 0.5*amr.dx0 - xhigh_lr = xlow_lr + xdist_lr - ylow_lr = self.ds_low_les * np.floor(ylow_lr_min/self.ds_low_les) - 0.5*amr.dy0 - yhigh_lr = ylow_lr + ydist_lr - zlow_lr = 0.5 * amr.dz0 # Lowest z point is half the height of the lowest grid cell - zhigh_lr = zlow_lr + zdist_lr - zoffsets_lr = np.arange(zlow_lr, zhigh_lr+self.ds_low_les, self.ds_low_les) - zlow_lr - - ## Save out info - # self.extent_low = ? # TODO: How should this be formatted? - - amr.nx_lr = nx_lr - amr.ny_lr = ny_lr - amr.nz_lr = nz_lr - amr.xlow_lr = xlow_lr - amr.xhigh_lr = xhigh_lr - amr.ylow_lr = ylow_lr - amr.yhigh_lr = yhigh_lr - amr.zlow_lr = zlow_lr - amr.zhigh_lr = zhigh_lr - amr.zoffsets_lr = zoffsets_lr - - ### ~~~~~~~~~ Calculate high resolution grid placement ~~~~~~~~~ - hr_domains = {} - for turbkey in self.wts: - wt_x = self.wts[turbkey]['x'] - wt_y = self.wts[turbkey]['y'] - wt_z = self.wts[turbkey]['zhub'] + 0.5*self.wts[turbkey]['D'] - wt_D = self.wts[turbkey]['D'] - - # Calculate minimum/maximum HR domain extents - x_buffer_hr = 0.6 * wt_D - y_buffer_hr = 0.6 * wt_D - z_buffer_hr = 0.6 * wt_D - - xlow_hr_min = wt_x - x_buffer_hr - xhigh_hr_max = wt_x + x_buffer_hr - ylow_hr_min = wt_y - y_buffer_hr - yhigh_hr_max = wt_y + y_buffer_hr - zhigh_hr_max = wt_z + z_buffer_hr - - # Calculate the minimum/maximum HR domain coordinate lengths & number of grid cells - xdist_hr_min = xhigh_hr_max - xlow_hr_min # Minumum possible length of x-extent of HR domain - xdist_hr = self.ds_high_les * np.ceil(xdist_hr_min/self.ds_high_les) - nx_hr = int(xdist_hr/self.ds_high_les) + 1 - - ydist_hr_min = yhigh_hr_max - ylow_hr_min - ydist_hr = self.ds_high_les * np.ceil(ydist_hr_min/self.ds_high_les) - ny_hr = int(ydist_hr/self.ds_high_les) + 1 - - zdist_hr = self.ds_high_les * np.ceil(zhigh_hr_max/self.ds_high_les) - nz_hr = int(zdist_hr/self.ds_high_les) + 1 - - # Calculate actual HR domain extent - # NOTE: Sampling planes should measure at AMR-Wind cell centers, not cell edges - xlow_hr = self.ds_high_les * np.floor(xlow_hr_min/self.ds_high_les) - 0.5*amr.dx_refine - xhigh_hr = xlow_hr + xdist_hr - ylow_hr = self.ds_high_les * np.floor(ylow_hr_min/self.ds_high_les) - 0.5*amr.dy_refine - yhigh_hr = ylow_hr + ydist_hr - zlow_hr = zlow_lr - zhigh_hr = zlow_hr + zdist_hr - zoffsets_hr = np.arange(zlow_hr, zhigh_hr+self.ds_high_les, self.ds_high_les) - zlow_hr - - # Save info - # self.extent_high = ? # TODO: How should this be formatted? - - hr_turb_info = {'nx_hr': nx_hr, 'ny_hr': ny_hr, 'nz_hr': nz_hr, - 'xlow_hr': xlow_hr, 'ylow_hr': ylow_hr, 'zlow_hr': zlow_hr, - 'xhigh_hr': xhigh_hr, 'yhigh_hr': yhigh_hr, 'zhigh_hr': zhigh_hr, - 'zoffsets_hr': zoffsets_hr} - hr_domains[turbkey] = hr_turb_info - amr.hr_domains = hr_domains - - ### ~~~~~~~~~ Write out sampling plane info ~~~~~~~~~ - amr.write_sampling_params(self.path) - - def CheckAutoBoxParameters(self): - ''' - Check the values of parameters that were calculated by DetermineBoxParameters - ''' - - ## Timestep checks - if self.dt_low_les >= self.amr.dt: - raise ValueError("AMR-Wind timestep too coarse for low resolution domain!") - if self.dt_high_les >= self.amr.dt: - raise ValueError("AMR-Wind timestep too coarse for high resolution domain!") - if self.dt_high_les <= self.dt_low_les: - raise ValueError("Low resolution timestep is finer than high resolution timestep!") - - ## Grid resolution checks - if self.ds_low_les >= self.amr.dx0: - raise ValueError("AMR-Wind Level 0 x-grid spacing too coarse for low resolution domain!") - if self.ds_low_les >= self.amr.dy0: - raise ValueError("AMR-Wind Level 0 y-grid spacing too coarse for low resolution domain!") - if self.ds_low_les >= self.amr.dz0: - raise ValueError("AMR-Wind Level 0 z-grid spacing too coarse for low resolution domain!") - if self.ds_high_les >= self.amr.ds_refine_max: - raise ValueError("AMR-Wind grid spacing too coarse for high resolution domain!") - - ## Low resolution domain extent checks - if self.amr.xhigh_lr < self.amr.prob_hi[0]: - raise ValueError("LR domain extends beyond maximum AMR-Wind x-extent!") - if self.amr.xlow_lr > self.amr.prob_lo[0]: - raise ValueError("LR domain extends beyond minimum AMR-Wind x-extent!") - if self.amr.yhigh_lr < self.amr.prob_hi[1]: - raise ValueError("LR domain extends beyond maximum AMR-Wind y-extent!") - if self.amr.ylow_lr > self.amr.prob_lo[1]: - raise ValueError("LR domain extends beyond minimum AMR-Wind y-extent!") - if self.amr.zhigh_lr < self.amr.prob_hi[2]: - raise ValueError("LR domain extends beyond maximum AMR-Wind z-extent!") - if self.amr.zlow_lr > self.amr.prob_lo[2]: - raise ValueError("LR domain extends beyond minimum AMR-Wind z-extent!") - - - def _setRotorParameters(self): @@ -1167,7 +925,7 @@ def TS_low_createSymlinks(self): os.chdir(notepath) - def _get_domain_parameters(self): + def getDomainParameters(self): # If the low box setup hasn't been called (e.g. LES run), do it once to get domain extents if not self.TSlowBoxFilesCreatedBool: @@ -1186,12 +944,6 @@ def _get_domain_parameters(self): if self.nHighBoxCases != len(self.allHighBoxCases.case): raise ValueError(f'The number of cases do not match as expected. {self.nHighBoxCases} unique wind directions, but {len(self.allHighBoxCases.case)} unique cases.') - if self.verbose>2: - print(f'allHighBoxCases is:') - print(self.allHighBoxCases) - - def _get_offset_turbsOrigin2TSOrigin(self): - # Determine offsets from turbines coordinate frame to TurbSim coordinate frame self.yoffset_turbsOrigin2TSOrigin = -( (self.TSlowbox.ymax - self.TSlowbox.ymin)/2 + self.TSlowbox.ymin ) self.xoffset_turbsOrigin2TSOrigin = - self.extent_low[0]*self.D @@ -1200,6 +952,10 @@ def _get_offset_turbsOrigin2TSOrigin(self): print(f" The y offset between the turbine ref frame and turbsim is {self.yoffset_turbsOrigin2TSOrigin}") print(f" The x offset between the turbine ref frame and turbsim is {self.xoffset_turbsOrigin2TSOrigin}") + if self.verbose>2: + print(f'allHighBoxCases is:') + print(self.allHighBoxCases) + def TS_high_get_time_series(self): @@ -1284,15 +1040,6 @@ def TS_high_setup(self, writeFiles=True): # Create symbolic links for the low-res boxes self.TS_low_createSymlinks() - # Get proper list of cases to loop (some can be repetead, e.g., ADyn/ADisk models) - self._get_domain_parameters() - - # Get offset between given reference frame and TurbSim's - self._get_offset_turbsOrigin2TSOrigin() - - # Open low-res and get time-series file - self.TS_high_get_time_series() - # Loop on all conditions/cases/seeds setting up the High boxes boxType='highres' for cond in range(self.nConditions): @@ -1907,57 +1654,4 @@ def FF_slurm_submit(self): sub_command = f"sbatch {fname}" print(f'Calling: {sub_command}') subprocess.call(sub_command, cwd=self.path, shell=True) - time.sleep(4) # Sometimes the same job gets submitted twice. This gets around it. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + time.sleep(4) # Sometimes the same job gets submitted twice. This gets around it. \ No newline at end of file From f10d3e0cc43cd481385f1e7e9891e3f7b70b9201 Mon Sep 17 00:00:00 2001 From: Alex Rybchuk Date: Mon, 13 Feb 2023 19:19:02 -0700 Subject: [PATCH 043/124] Add example of AMRWind class usage --- pyFAST/fastfarm/AMRWindSimulation.py | 2 + .../examples/Ex4_AMRWindSamplingSetup.py | 92 +++++++++++++++++++ 2 files changed, 94 insertions(+) create mode 100644 pyFAST/fastfarm/examples/Ex4_AMRWindSamplingSetup.py diff --git a/pyFAST/fastfarm/AMRWindSimulation.py b/pyFAST/fastfarm/AMRWindSimulation.py index f655d40..71fc02f 100644 --- a/pyFAST/fastfarm/AMRWindSimulation.py +++ b/pyFAST/fastfarm/AMRWindSimulation.py @@ -9,6 +9,7 @@ class AMRWindSimulation: Specifically, this class contains info from the AMR-Wind input file, and it carries out simple calculations about the AMR-Wind simulation + For reference, see https://openfast.readthedocs.io/en/dev/source/user/fast.farm/ModelGuidance.html ''' def __init__(self, wts:dict, @@ -355,6 +356,7 @@ def write_sampling_params(self, outdir): if outfile.is_file(): raise FileExistsError(f"{str(outfile)} already exists! Aborting...") + print(f"Writing to {outfile} ...") with outfile.open("w") as out: # Write high-level info for sampling out.write(f"# Sampling info generated by AMRWindSamplingCreation.py\n") diff --git a/pyFAST/fastfarm/examples/Ex4_AMRWindSamplingSetup.py b/pyFAST/fastfarm/examples/Ex4_AMRWindSamplingSetup.py new file mode 100644 index 0000000..1612ce3 --- /dev/null +++ b/pyFAST/fastfarm/examples/Ex4_AMRWindSamplingSetup.py @@ -0,0 +1,92 @@ +""" +Set up sampling planes for AMR-Wind to use for inflow winds. +""" + +from pyFAST.fastfarm.AMRWindSimulation import AMRWindSimulation + +def main(): + # ----------------------------------------------------------------------------- + # USER INPUT: Modify these + # ----------------------------------------------------------------------------- + + # ----------- Wind farm + wts = { + 0 :{'x':1280.0, 'y':2560, 'z':0.0, 'D':126.9, 'zhub':86.5, 'cmax':5, 'fmax':10/6, 'Cmeander':1.9, 'name':'T0'}, + 1 :{'x':1280.0, 'y':3200, 'z':0.0, 'D':126.9, 'zhub':86.5, 'cmax':5, 'fmax':10/6, 'Cmeander':1.9, 'name':'T1'}, + 2 :{'x':1280.0, 'y':3840, 'z':0.0, 'D':126.9, 'zhub':86.5, 'cmax':5, 'fmax':10/6, 'Cmeander':1.9, 'name':'T2'}, + } + + # ----------- AMR-Wind parameters + fixed_dt = 0.25 + prob_lo = (0.0, 0.0, 0.0) + prob_hi = (2560.0, 6400.0, 1280.0) + n_cell = (256, 640, 128) + max_level = 1 # Number of grid refinement levels + + incflo_velocity_hh = (0.0, 10.0, 0.0) # Hub-height velocity + postproc_name = 'sampling' + + # ----------- I/O + outdir = '/Users/orybchuk/Research/OpenFAST-python-toolbox' + + # ----------------------------------------------------------------------------- + # END OF USER INPUT + # ----------------------------------------------------------------------------- + + # Initial setup + amr = AMRWindSimulation(wts, fixed_dt, prob_lo, prob_hi, + n_cell, max_level, incflo_velocity_hh, + postproc_name) + + # Write out sampling parameters + amr.write_sampling_params(outdir) + + ''' + Below are the generated contents of sampling_config.i: + # Sampling info generated by AMRWindSamplingCreation.py + incflo.post_processing = sampling # averaging + sampling.output_frequency = 1 + sampling.fields = velocity # temperature tke + sampling.labels = ['Low', 'HighT0_inflow0deg', 'HighT1_inflow0deg', 'HighT2_inflow0deg'] + + # Low sampling grid spacing = 10.0 m + sampling.Low.type = PlaneSampler + sampling.Low.num_points = 78 206 + sampling.Low.origin = 885.0 2165.0 5.0 + sampling.Low.axis1 = 770.0 0.0 0.0 + sampling.Low.axis2 = 0.0 2050.0 0.0 + sampling.Low.normal = 0.0 0.0 1.0 + sampling.Low.offsets = 0 10 20 30 40 50 60 70 80 90 100 110 120 130 140 150 160 170 180 190 200 210 220 230 240 250 260 270 280 + + # Turbine T0 at (x,y) = (1280.0, 2560), with D = 126.9, grid spacing = 5.0 m + sampling.T0.type = PlaneSampler + sampling.T0.num_points = 32 32 + sampling.T0.origin = 1197.5 2477.5 2.5 + sampling.T0.axis1 = 155.0 0.0 0.0 + sampling.T0.axis2 = 0.0 155.0 0.0 + sampling.T0.normal = 0.0 0.0 1.0 + sampling.T0.offsets = 0 5 10 15 20 25 30 35 40 45 50 55 60 65 70 75 80 85 90 95 100 105 110 115 120 125 130 135 140 145 150 155 160 165 170 175 180 185 190 195 200 205 210 215 220 225 230 + + # Turbine T1 at (x,y) = (1280.0, 3200), with D = 126.9, grid spacing = 5.0 m + sampling.T1.type = PlaneSampler + sampling.T1.num_points = 32 32 + sampling.T1.origin = 1197.5 3117.5 2.5 + sampling.T1.axis1 = 155.0 0.0 0.0 + sampling.T1.axis2 = 0.0 155.0 0.0 + sampling.T1.normal = 0.0 0.0 1.0 + sampling.T1.offsets = 0 5 10 15 20 25 30 35 40 45 50 55 60 65 70 75 80 85 90 95 100 105 110 115 120 125 130 135 140 145 150 155 160 165 170 175 180 185 190 195 200 205 210 215 220 225 230 + + # Turbine T2 at (x,y) = (1280.0, 3840), with D = 126.9, grid spacing = 5.0 m + sampling.T2.type = PlaneSampler + sampling.T2.num_points = 32 32 + sampling.T2.origin = 1197.5 3757.5 2.5 + sampling.T2.axis1 = 155.0 0.0 0.0 + sampling.T2.axis2 = 0.0 155.0 0.0 + sampling.T2.normal = 0.0 0.0 1.0 + sampling.T2.offsets = 0 5 10 15 20 25 30 35 40 45 50 55 60 65 70 75 80 85 90 95 100 105 110 115 120 125 130 135 140 145 150 155 160 165 170 175 180 185 190 195 200 205 210 215 220 225 230 + + + ''' + +if __name__ == '__main__': + main() \ No newline at end of file From 56a62b0678a74f3263ed546d0437e8f881e58a38 Mon Sep 17 00:00:00 2001 From: Alex Rybchuk Date: Mon, 13 Feb 2023 19:39:03 -0700 Subject: [PATCH 044/124] Tweak output text --- pyFAST/fastfarm/AMRWindSimulation.py | 18 ++++---- .../examples/Ex4_AMRWindSamplingSetup.py | 46 +++++++++---------- 2 files changed, 32 insertions(+), 32 deletions(-) diff --git a/pyFAST/fastfarm/AMRWindSimulation.py b/pyFAST/fastfarm/AMRWindSimulation.py index 71fc02f..e1c71c1 100644 --- a/pyFAST/fastfarm/AMRWindSimulation.py +++ b/pyFAST/fastfarm/AMRWindSimulation.py @@ -359,11 +359,12 @@ def write_sampling_params(self, outdir): print(f"Writing to {outfile} ...") with outfile.open("w") as out: # Write high-level info for sampling + sampling_labels_str = " ".join(str(item) for item in self.sampling_labels) out.write(f"# Sampling info generated by AMRWindSamplingCreation.py\n") out.write(f"incflo.post_processing = {self.postproc_name} # averaging\n") out.write(f"{self.postproc_name}.output_frequency = {self.output_frequency}\n") out.write(f"{self.postproc_name}.fields = velocity # temperature tke\n") - out.write(f"{self.postproc_name}.labels = {self.sampling_labels}\n") + out.write(f"{self.postproc_name}.labels = {sampling_labels_str}\n") # Write out low resolution sampling plane info zoffsets_lr_str = " ".join(str(int(item)) for item in self.zoffsets_lr) @@ -386,6 +387,7 @@ def write_sampling_params(self, outdir): wt_name = self.wts[turbkey]['name'] else: wt_name = f'T{turbkey}' + sampling_name = f"High{wt_name}_inflow0deg" nx_hr = self.hr_domains[turbkey]['nx_hr'] ny_hr = self.hr_domains[turbkey]['ny_hr'] xlow_hr = self.hr_domains[turbkey]['xlow_hr'] @@ -397,10 +399,10 @@ def write_sampling_params(self, outdir): zoffsets_hr_str = " ".join(str(int(item)) for item in zoffsets_hr) out.write(f"\n# Turbine {wt_name} at (x,y) = ({wt_x}, {wt_y}), with D = {wt_D}, grid spacing = {self.ds_hr} m\n") - out.write(f"{self.postproc_name}.{wt_name}.type = PlaneSampler\n") - out.write(f"{self.postproc_name}.{wt_name}.num_points = {nx_hr} {ny_hr}\n") - out.write(f"{self.postproc_name}.{wt_name}.origin = {xlow_hr:.1f} {ylow_hr:.1f} {zlow_hr:.1f}\n") # Round the float output - out.write(f"{self.postproc_name}.{wt_name}.axis1 = {xdist_hr:.1f} 0.0 0.0\n") # Assume: axis1 oriented parallel to AMR-Wind x-axis - out.write(f"{self.postproc_name}.{wt_name}.axis2 = 0.0 {ydist_hr:.1f} 0.0\n") # Assume: axis2 oriented parallel to AMR-Wind y-axis - out.write(f"{self.postproc_name}.{wt_name}.normal = 0.0 0.0 1.0\n") - out.write(f"{self.postproc_name}.{wt_name}.offsets = {zoffsets_hr_str}\n") \ No newline at end of file + out.write(f"{self.postproc_name}.{sampling_name}.type = PlaneSampler\n") + out.write(f"{self.postproc_name}.{sampling_name}.num_points = {nx_hr} {ny_hr}\n") + out.write(f"{self.postproc_name}.{sampling_name}.origin = {xlow_hr:.1f} {ylow_hr:.1f} {zlow_hr:.1f}\n") # Round the float output + out.write(f"{self.postproc_name}.{sampling_name}.axis1 = {xdist_hr:.1f} 0.0 0.0\n") # Assume: axis1 oriented parallel to AMR-Wind x-axis + out.write(f"{self.postproc_name}.{sampling_name}.axis2 = 0.0 {ydist_hr:.1f} 0.0\n") # Assume: axis2 oriented parallel to AMR-Wind y-axis + out.write(f"{self.postproc_name}.{sampling_name}.normal = 0.0 0.0 1.0\n") + out.write(f"{self.postproc_name}.{sampling_name}.offsets = {zoffsets_hr_str}\n") \ No newline at end of file diff --git a/pyFAST/fastfarm/examples/Ex4_AMRWindSamplingSetup.py b/pyFAST/fastfarm/examples/Ex4_AMRWindSamplingSetup.py index 1612ce3..ff98173 100644 --- a/pyFAST/fastfarm/examples/Ex4_AMRWindSamplingSetup.py +++ b/pyFAST/fastfarm/examples/Ex4_AMRWindSamplingSetup.py @@ -47,7 +47,7 @@ def main(): incflo.post_processing = sampling # averaging sampling.output_frequency = 1 sampling.fields = velocity # temperature tke - sampling.labels = ['Low', 'HighT0_inflow0deg', 'HighT1_inflow0deg', 'HighT2_inflow0deg'] + sampling.labels = Low HighT0_inflow0deg HighT1_inflow0deg HighT2_inflow0deg # Low sampling grid spacing = 10.0 m sampling.Low.type = PlaneSampler @@ -59,33 +59,31 @@ def main(): sampling.Low.offsets = 0 10 20 30 40 50 60 70 80 90 100 110 120 130 140 150 160 170 180 190 200 210 220 230 240 250 260 270 280 # Turbine T0 at (x,y) = (1280.0, 2560), with D = 126.9, grid spacing = 5.0 m - sampling.T0.type = PlaneSampler - sampling.T0.num_points = 32 32 - sampling.T0.origin = 1197.5 2477.5 2.5 - sampling.T0.axis1 = 155.0 0.0 0.0 - sampling.T0.axis2 = 0.0 155.0 0.0 - sampling.T0.normal = 0.0 0.0 1.0 - sampling.T0.offsets = 0 5 10 15 20 25 30 35 40 45 50 55 60 65 70 75 80 85 90 95 100 105 110 115 120 125 130 135 140 145 150 155 160 165 170 175 180 185 190 195 200 205 210 215 220 225 230 + sampling.HighT0_inflow0deg.type = PlaneSampler + sampling.HighT0_inflow0deg.num_points = 32 32 + sampling.HighT0_inflow0deg.origin = 1197.5 2477.5 2.5 + sampling.HighT0_inflow0deg.axis1 = 155.0 0.0 0.0 + sampling.HighT0_inflow0deg.axis2 = 0.0 155.0 0.0 + sampling.HighT0_inflow0deg.normal = 0.0 0.0 1.0 + sampling.HighT0_inflow0deg.offsets = 0 5 10 15 20 25 30 35 40 45 50 55 60 65 70 75 80 85 90 95 100 105 110 115 120 125 130 135 140 145 150 155 160 165 170 175 180 185 190 195 200 205 210 215 220 225 230 # Turbine T1 at (x,y) = (1280.0, 3200), with D = 126.9, grid spacing = 5.0 m - sampling.T1.type = PlaneSampler - sampling.T1.num_points = 32 32 - sampling.T1.origin = 1197.5 3117.5 2.5 - sampling.T1.axis1 = 155.0 0.0 0.0 - sampling.T1.axis2 = 0.0 155.0 0.0 - sampling.T1.normal = 0.0 0.0 1.0 - sampling.T1.offsets = 0 5 10 15 20 25 30 35 40 45 50 55 60 65 70 75 80 85 90 95 100 105 110 115 120 125 130 135 140 145 150 155 160 165 170 175 180 185 190 195 200 205 210 215 220 225 230 + sampling.HighT1_inflow0deg.type = PlaneSampler + sampling.HighT1_inflow0deg.num_points = 32 32 + sampling.HighT1_inflow0deg.origin = 1197.5 3117.5 2.5 + sampling.HighT1_inflow0deg.axis1 = 155.0 0.0 0.0 + sampling.HighT1_inflow0deg.axis2 = 0.0 155.0 0.0 + sampling.HighT1_inflow0deg.normal = 0.0 0.0 1.0 + sampling.HighT1_inflow0deg.offsets = 0 5 10 15 20 25 30 35 40 45 50 55 60 65 70 75 80 85 90 95 100 105 110 115 120 125 130 135 140 145 150 155 160 165 170 175 180 185 190 195 200 205 210 215 220 225 230 # Turbine T2 at (x,y) = (1280.0, 3840), with D = 126.9, grid spacing = 5.0 m - sampling.T2.type = PlaneSampler - sampling.T2.num_points = 32 32 - sampling.T2.origin = 1197.5 3757.5 2.5 - sampling.T2.axis1 = 155.0 0.0 0.0 - sampling.T2.axis2 = 0.0 155.0 0.0 - sampling.T2.normal = 0.0 0.0 1.0 - sampling.T2.offsets = 0 5 10 15 20 25 30 35 40 45 50 55 60 65 70 75 80 85 90 95 100 105 110 115 120 125 130 135 140 145 150 155 160 165 170 175 180 185 190 195 200 205 210 215 220 225 230 - - + sampling.HighT2_inflow0deg.type = PlaneSampler + sampling.HighT2_inflow0deg.num_points = 32 32 + sampling.HighT2_inflow0deg.origin = 1197.5 3757.5 2.5 + sampling.HighT2_inflow0deg.axis1 = 155.0 0.0 0.0 + sampling.HighT2_inflow0deg.axis2 = 0.0 155.0 0.0 + sampling.HighT2_inflow0deg.normal = 0.0 0.0 1.0 + sampling.HighT2_inflow0deg.offsets = 0 5 10 15 20 25 30 35 40 45 50 55 60 65 70 75 80 85 90 95 100 105 110 115 120 125 130 135 140 145 150 155 160 165 170 175 180 185 190 195 200 205 210 215 220 225 230 ''' if __name__ == '__main__': From fd068fc6358f05052fd2a2d3794a8dc5aa61cce1 Mon Sep 17 00:00:00 2001 From: Regis Thedin Date: Wed, 15 Feb 2023 12:25:43 -0700 Subject: [PATCH 045/124] AMR-Wind/FF: Fix bug on cell-centered sampling --- pyFAST/fastfarm/AMRWindSimulation.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyFAST/fastfarm/AMRWindSimulation.py b/pyFAST/fastfarm/AMRWindSimulation.py index e1c71c1..9611165 100644 --- a/pyFAST/fastfarm/AMRWindSimulation.py +++ b/pyFAST/fastfarm/AMRWindSimulation.py @@ -192,9 +192,9 @@ def _calc_sampling_params(self): # NOTE: Should we use dx/dy/dz values here or ds_lr? # - AR: I think it's correct to use ds_lr to get to the xlow values, # but then offset by 0.5*amr_dx0 - self.xlow_lr = self.ds_low_les * np.floor(xlow_lr_min/self.ds_low_les) - 0.5*self.dx0 + self.xlow_lr = self.ds_low_les * np.floor(xlow_lr_min/self.ds_low_les) - 0.5*self.dx0 + self.prob_lo[0]%self.ds_low_les self.xhigh_lr = self.xlow_lr + self.xdist_lr - self.ylow_lr = self.ds_low_les * np.floor(ylow_lr_min/self.ds_low_les) - 0.5*self.dy0 + self.ylow_lr = self.ds_low_les * np.floor(ylow_lr_min/self.ds_low_les) - 0.5*self.dy0 + self.prob_lo[1]%self.ds_low_les self.yhigh_lr = self.ylow_lr + self.ydist_lr self.zlow_lr = 0.5 * self.dz0 # Lowest z point is half the height of the lowest grid cell self.zhigh_lr = self.zlow_lr + self.zdist_lr @@ -405,4 +405,4 @@ def write_sampling_params(self, outdir): out.write(f"{self.postproc_name}.{sampling_name}.axis1 = {xdist_hr:.1f} 0.0 0.0\n") # Assume: axis1 oriented parallel to AMR-Wind x-axis out.write(f"{self.postproc_name}.{sampling_name}.axis2 = 0.0 {ydist_hr:.1f} 0.0\n") # Assume: axis2 oriented parallel to AMR-Wind y-axis out.write(f"{self.postproc_name}.{sampling_name}.normal = 0.0 0.0 1.0\n") - out.write(f"{self.postproc_name}.{sampling_name}.offsets = {zoffsets_hr_str}\n") \ No newline at end of file + out.write(f"{self.postproc_name}.{sampling_name}.offsets = {zoffsets_hr_str}\n") From bf27a750421f0ce977cdda7a903725617476e82b Mon Sep 17 00:00:00 2001 From: Regis Thedin Date: Wed, 15 Feb 2023 14:12:33 -0700 Subject: [PATCH 046/124] AMR/FF: Add more robust buffer zone specification --- pyFAST/fastfarm/AMRWindSimulation.py | 72 ++++++++++++++-------------- 1 file changed, 37 insertions(+), 35 deletions(-) diff --git a/pyFAST/fastfarm/AMRWindSimulation.py b/pyFAST/fastfarm/AMRWindSimulation.py index 9611165..90f1538 100644 --- a/pyFAST/fastfarm/AMRWindSimulation.py +++ b/pyFAST/fastfarm/AMRWindSimulation.py @@ -16,22 +16,28 @@ def __init__(self, wts:dict, dt: float, prob_lo: tuple, prob_hi: tuple, n_cell: tuple, max_level: int, incflo_velocity_hh: tuple, - postproc_name='sampling'): + postproc_name='sampling', + buffer_lr = [3,6,3,3,2], + buffer_hr = 0.6,): ''' Values from the AMR-Wind input file Inputs: - * dt: this should be a fixed dt value + * dt: this should be a fixed dt value from the LES run * incflo_velocity_hh: velocity vector, specifically at hub height + * buffer_lr: buffer for [xmin, xmax, ymin, ymax, zmax] in low-res box, in D + * buffer_hr: buffer for all directions (constant) in high-res box, in D ''' # Process inputs - self.wts = wts - self.dt = dt - self.prob_lo = prob_lo - self.prob_hi = prob_hi - self.n_cell = n_cell - self.max_level = max_level + self.wts = wts + self.dt = dt + self.prob_lo = prob_lo + self.prob_hi = prob_hi + self.n_cell = n_cell + self.max_level = max_level self.incflo_velocity_hh = incflo_velocity_hh - self.postproc_name = postproc_name + self.postproc_name = postproc_name + self.buffer_lr = buffer_lr + self.buffer_hr = buffer_hr # Placeholder variables, to be calculated by FFCaseCreation self.output_frequency = None @@ -164,14 +170,11 @@ def _calc_sampling_params(self): wt_all_z_max = max(wt_all_z_max, self.wts[turbkey]['zhub'] + 0.5*self.wts[turbkey]['D']) Drot_max = max(Drot_max, self.wts[turbkey]['D']) - x_buffer_lr = 3 * Drot_max - y_buffer_lr = 3 * Drot_max - z_buffer_lr = 1 * Drot_max # This buffer size was arbitrarily chosen - xlow_lr_min = wt_all_x_min - x_buffer_lr - xhigh_lr_max = wt_all_x_max + x_buffer_lr - ylow_lr_min = wt_all_y_min - y_buffer_lr - yhigh_lr_max = wt_all_y_max + y_buffer_lr - zhigh_lr_max = wt_all_z_max + z_buffer_lr + xlow_lr_min = wt_all_x_min - selfbuffer_lr[0] * Drot_max + xhigh_lr_max = wt_all_x_max + selfbuffer_lr[1] * Drot_max + ylow_lr_min = wt_all_y_min - selfbuffer_lr[2] * Drot_max + yhigh_lr_max = wt_all_y_max + selfbuffer_lr[3] * Drot_max + zhigh_lr_max = wt_all_z_max + selfbuffer_lr[4] * Drot_max # Calculate the minimum/maximum LR domain coordinate lengths & number of grid cells xdist_lr_min = xhigh_lr_max - xlow_lr_min # Minumum possible length of x-extent of LR domain @@ -191,7 +194,7 @@ def _calc_sampling_params(self): # NOTE: Sampling planes should measure at AMR-Wind cell centers, not cell edges # NOTE: Should we use dx/dy/dz values here or ds_lr? # - AR: I think it's correct to use ds_lr to get to the xlow values, - # but then offset by 0.5*amr_dx0 + # but then offset by 0.5*amr_dx0 if need be self.xlow_lr = self.ds_low_les * np.floor(xlow_lr_min/self.ds_low_les) - 0.5*self.dx0 + self.prob_lo[0]%self.ds_low_les self.xhigh_lr = self.xlow_lr + self.xdist_lr self.ylow_lr = self.ds_low_les * np.floor(ylow_lr_min/self.ds_low_les) - 0.5*self.dy0 + self.prob_lo[1]%self.ds_low_les @@ -200,8 +203,8 @@ def _calc_sampling_params(self): self.zhigh_lr = self.zlow_lr + self.zdist_lr self.zoffsets_lr = np.arange(self.zlow_lr, self.zhigh_lr+self.ds_low_les, self.ds_low_les) - self.zlow_lr - ## Save out info - # self.extent_low = ? # TODO: How should this be formatted? + ## Save out info for FFCaseCreation + self.extent_low = self.buffer_lr ### ~~~~~~~~~ Calculate high resolution grid placement ~~~~~~~~~ hr_domains = {} @@ -212,15 +215,11 @@ def _calc_sampling_params(self): wt_D = self.wts[turbkey]['D'] # Calculate minimum/maximum HR domain extents - x_buffer_hr = 0.6 * wt_D - y_buffer_hr = 0.6 * wt_D - z_buffer_hr = 0.6 * wt_D - - xlow_hr_min = wt_x - x_buffer_hr - xhigh_hr_max = wt_x + x_buffer_hr - ylow_hr_min = wt_y - y_buffer_hr - yhigh_hr_max = wt_y + y_buffer_hr - zhigh_hr_max = wt_z + z_buffer_hr + xlow_hr_min = wt_x - self.buffer_hr * wt_D + xhigh_hr_max = wt_x + self.buffer_hr * wt_D + ylow_hr_min = wt_y - self.buffer_hr * wt_D + yhigh_hr_max = wt_y + self.buffer_hr * wt_D + zhigh_hr_max = wt_z + self.buffer_hr * wt_D # Calculate the minimum/maximum HR domain coordinate lengths & number of grid cells xdist_hr_min = xhigh_hr_max - xlow_hr_min # Minumum possible length of x-extent of HR domain @@ -244,8 +243,8 @@ def _calc_sampling_params(self): zhigh_hr = zlow_hr + zdist_hr zoffsets_hr = np.arange(zlow_hr, zhigh_hr+self.ds_high_les, self.ds_high_les) - zlow_hr - # Save info - # self.extent_high = ? # TODO: How should this be formatted? + # Save out info for FFCaseCreation + self.extent_high = self.buffer_hr hr_turb_info = {'nx_hr': nx_hr, 'ny_hr': ny_hr, 'nz_hr': nz_hr, 'xdist_hr': xdist_hr, 'ydist_hr': ydist_hr, 'zdist_hr': zdist_hr, @@ -347,10 +346,13 @@ def _check_sampling_params(self): if coord not in amr_zgrid_refine_cc: raise ValueError("High resolution z-sampling grid is not cell cenetered with AMR-Wind's grid!") - def write_sampling_params(self, outdir): + def write_sampling_params(self, outdir=None): ''' Write out text that can be used for the sampling planes in an AMR-Wind input file + + outdir: str + Input file ''' outfile = Path(outdir, 'sampling_config.i') if outfile.is_file(): @@ -361,10 +363,10 @@ def write_sampling_params(self, outdir): # Write high-level info for sampling sampling_labels_str = " ".join(str(item) for item in self.sampling_labels) out.write(f"# Sampling info generated by AMRWindSamplingCreation.py\n") - out.write(f"incflo.post_processing = {self.postproc_name} # averaging\n") + out.write(f"incflo.post_processing = {self.postproc_name} # averaging\n") out.write(f"{self.postproc_name}.output_frequency = {self.output_frequency}\n") - out.write(f"{self.postproc_name}.fields = velocity # temperature tke\n") - out.write(f"{self.postproc_name}.labels = {sampling_labels_str}\n") + out.write(f"{self.postproc_name}.fields = velocity # temperature tke\n") + out.write(f"{self.postproc_name}.labels = {sampling_labels_str}\n") # Write out low resolution sampling plane info zoffsets_lr_str = " ".join(str(int(item)) for item in self.zoffsets_lr) From 67ad29a6fc4c852ed1bb41b06e7c1436c2e72e88 Mon Sep 17 00:00:00 2001 From: Regis Thedin Date: Wed, 15 Feb 2023 16:00:51 -0700 Subject: [PATCH 047/124] AMR/FF: Add option to specify high-res resolution --- pyFAST/fastfarm/AMRWindSimulation.py | 33 ++++++++++++++++++---------- 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/pyFAST/fastfarm/AMRWindSimulation.py b/pyFAST/fastfarm/AMRWindSimulation.py index 90f1538..754d927 100644 --- a/pyFAST/fastfarm/AMRWindSimulation.py +++ b/pyFAST/fastfarm/AMRWindSimulation.py @@ -18,7 +18,8 @@ def __init__(self, wts:dict, incflo_velocity_hh: tuple, postproc_name='sampling', buffer_lr = [3,6,3,3,2], - buffer_hr = 0.6,): + buffer_hr = 0.6, + ds_hr = None, ds_lr = None): ''' Values from the AMR-Wind input file Inputs: @@ -38,12 +39,12 @@ def __init__(self, wts:dict, self.postproc_name = postproc_name self.buffer_lr = buffer_lr self.buffer_hr = buffer_hr + self.ds_hr = ds_hr + self.ds_lr = ds_lr # Placeholder variables, to be calculated by FFCaseCreation self.output_frequency = None self.sampling_labels = None - self.ds_lr = None - self.ds_hr = None self.nx_lr = None self.ny_lr = None self.nz_lr = None @@ -151,8 +152,16 @@ def _calc_sampling_params(self): for turbkey in self.wts: cmax_min = min(cmax_min, self.wts[turbkey]['cmax']) ds_hr_max = cmax_min - self.ds_high_les = self.ds_refine_max * np.floor(ds_hr_max/self.ds_refine_max) # Ensure that ds_hr is a multiple of the refined AMR-Wind grid spacing - self.ds_hr = self.ds_high_les + + if self.ds_hr is None: + if ds_hr_max < self.ds_refine_max: + raise ValueError(f"AMR-Wind grid spacing of {self.ds_refine_max} is too coarse for high resolution domain! The high-resolution domain requires "\ + f"AMR-Wind grid spacing to be at least {ds_hr_max} m. If a coarser high-res domain is acceptable, then manually specify the "\ + f"high-resolution grid spacing to be at least {self.ds_refine_max} with ds_hr = {self.ds_refine_max}.") + self.ds_high_les = self.ds_refine_max * np.floor(ds_hr_max/self.ds_refine_max) # Ensure that ds_hr is a multiple of the refined AMR-Wind grid spacing + self.ds_hr = self.ds_high_les + else: + self.ds_high_les = self.ds_hr ### ~~~~~~~~~ Calculate low resolution grid placement ~~~~~~~~~ # Calculate minimum/maximum LR domain extents @@ -170,11 +179,11 @@ def _calc_sampling_params(self): wt_all_z_max = max(wt_all_z_max, self.wts[turbkey]['zhub'] + 0.5*self.wts[turbkey]['D']) Drot_max = max(Drot_max, self.wts[turbkey]['D']) - xlow_lr_min = wt_all_x_min - selfbuffer_lr[0] * Drot_max - xhigh_lr_max = wt_all_x_max + selfbuffer_lr[1] * Drot_max - ylow_lr_min = wt_all_y_min - selfbuffer_lr[2] * Drot_max - yhigh_lr_max = wt_all_y_max + selfbuffer_lr[3] * Drot_max - zhigh_lr_max = wt_all_z_max + selfbuffer_lr[4] * Drot_max + xlow_lr_min = wt_all_x_min - self.buffer_lr[0] * Drot_max + xhigh_lr_max = wt_all_x_max + self.buffer_lr[1] * Drot_max + ylow_lr_min = wt_all_y_min - self.buffer_lr[2] * Drot_max + yhigh_lr_max = wt_all_y_max + self.buffer_lr[3] * Drot_max + zhigh_lr_max = wt_all_z_max + self.buffer_lr[4] * Drot_max # Calculate the minimum/maximum LR domain coordinate lengths & number of grid cells xdist_lr_min = xhigh_lr_max - xlow_lr_min # Minumum possible length of x-extent of LR domain @@ -235,9 +244,9 @@ def _calc_sampling_params(self): # Calculate actual HR domain extent # NOTE: Sampling planes should measure at AMR-Wind cell centers, not cell edges - xlow_hr = self.ds_high_les * np.floor(xlow_hr_min/self.ds_high_les) - 0.5*self.dx_refine + xlow_hr = self.ds_high_les * np.floor(xlow_hr_min/self.ds_high_les) - 0.5*self.dx_refine + self.prob_lo[0]%self.ds_high_les xhigh_hr = xlow_hr + xdist_hr - ylow_hr = self.ds_high_les * np.floor(ylow_hr_min/self.ds_high_les) - 0.5*self.dy_refine + ylow_hr = self.ds_high_les * np.floor(ylow_hr_min/self.ds_high_les) - 0.5*self.dy_refine + self.prob_lo[1]%self.ds_high_les yhigh_hr = ylow_hr + ydist_hr zlow_hr = self.zlow_lr / (2**self.max_level) zhigh_hr = zlow_hr + zdist_hr From 4a47018911977c08e4140a5c28fb5f341fb04dea Mon Sep 17 00:00:00 2001 From: Regis Thedin Date: Wed, 15 Feb 2023 16:16:25 -0700 Subject: [PATCH 048/124] AMR/FF: fix bug on computation of high-res box extents --- pyFAST/fastfarm/AMRWindSimulation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyFAST/fastfarm/AMRWindSimulation.py b/pyFAST/fastfarm/AMRWindSimulation.py index 754d927..38536d4 100644 --- a/pyFAST/fastfarm/AMRWindSimulation.py +++ b/pyFAST/fastfarm/AMRWindSimulation.py @@ -220,7 +220,7 @@ def _calc_sampling_params(self): for turbkey in self.wts: wt_x = self.wts[turbkey]['x'] wt_y = self.wts[turbkey]['y'] - wt_z = self.wts[turbkey]['zhub'] + 0.5*self.wts[turbkey]['D'] + wt_z = self.wts[turbkey]['zhub'] wt_D = self.wts[turbkey]['D'] # Calculate minimum/maximum HR domain extents From 58254ddd6d3adaa2226be13912beef92ea948f17 Mon Sep 17 00:00:00 2001 From: Regis Thedin Date: Wed, 15 Feb 2023 16:22:15 -0700 Subject: [PATCH 049/124] AMR/FF: writing output format for sampling planes --- pyFAST/fastfarm/AMRWindSimulation.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pyFAST/fastfarm/AMRWindSimulation.py b/pyFAST/fastfarm/AMRWindSimulation.py index 38536d4..4598940 100644 --- a/pyFAST/fastfarm/AMRWindSimulation.py +++ b/pyFAST/fastfarm/AMRWindSimulation.py @@ -373,6 +373,7 @@ def write_sampling_params(self, outdir=None): sampling_labels_str = " ".join(str(item) for item in self.sampling_labels) out.write(f"# Sampling info generated by AMRWindSamplingCreation.py\n") out.write(f"incflo.post_processing = {self.postproc_name} # averaging\n") + out.write(f"{self.postproc_name}.output_format = netcdf\n") out.write(f"{self.postproc_name}.output_frequency = {self.output_frequency}\n") out.write(f"{self.postproc_name}.fields = velocity # temperature tke\n") out.write(f"{self.postproc_name}.labels = {sampling_labels_str}\n") From dea6dcb1195d4ba0ab289731db2f670e9911a2ad Mon Sep 17 00:00:00 2001 From: Alex Rybchuk Date: Wed, 15 Feb 2023 16:37:42 -0700 Subject: [PATCH 050/124] Delete sampling_config.i contents in Ex4 --- .../examples/Ex4_AMRWindSamplingSetup.py | 45 ------------------- 1 file changed, 45 deletions(-) diff --git a/pyFAST/fastfarm/examples/Ex4_AMRWindSamplingSetup.py b/pyFAST/fastfarm/examples/Ex4_AMRWindSamplingSetup.py index ff98173..8b99712 100644 --- a/pyFAST/fastfarm/examples/Ex4_AMRWindSamplingSetup.py +++ b/pyFAST/fastfarm/examples/Ex4_AMRWindSamplingSetup.py @@ -41,50 +41,5 @@ def main(): # Write out sampling parameters amr.write_sampling_params(outdir) - ''' - Below are the generated contents of sampling_config.i: - # Sampling info generated by AMRWindSamplingCreation.py - incflo.post_processing = sampling # averaging - sampling.output_frequency = 1 - sampling.fields = velocity # temperature tke - sampling.labels = Low HighT0_inflow0deg HighT1_inflow0deg HighT2_inflow0deg - - # Low sampling grid spacing = 10.0 m - sampling.Low.type = PlaneSampler - sampling.Low.num_points = 78 206 - sampling.Low.origin = 885.0 2165.0 5.0 - sampling.Low.axis1 = 770.0 0.0 0.0 - sampling.Low.axis2 = 0.0 2050.0 0.0 - sampling.Low.normal = 0.0 0.0 1.0 - sampling.Low.offsets = 0 10 20 30 40 50 60 70 80 90 100 110 120 130 140 150 160 170 180 190 200 210 220 230 240 250 260 270 280 - - # Turbine T0 at (x,y) = (1280.0, 2560), with D = 126.9, grid spacing = 5.0 m - sampling.HighT0_inflow0deg.type = PlaneSampler - sampling.HighT0_inflow0deg.num_points = 32 32 - sampling.HighT0_inflow0deg.origin = 1197.5 2477.5 2.5 - sampling.HighT0_inflow0deg.axis1 = 155.0 0.0 0.0 - sampling.HighT0_inflow0deg.axis2 = 0.0 155.0 0.0 - sampling.HighT0_inflow0deg.normal = 0.0 0.0 1.0 - sampling.HighT0_inflow0deg.offsets = 0 5 10 15 20 25 30 35 40 45 50 55 60 65 70 75 80 85 90 95 100 105 110 115 120 125 130 135 140 145 150 155 160 165 170 175 180 185 190 195 200 205 210 215 220 225 230 - - # Turbine T1 at (x,y) = (1280.0, 3200), with D = 126.9, grid spacing = 5.0 m - sampling.HighT1_inflow0deg.type = PlaneSampler - sampling.HighT1_inflow0deg.num_points = 32 32 - sampling.HighT1_inflow0deg.origin = 1197.5 3117.5 2.5 - sampling.HighT1_inflow0deg.axis1 = 155.0 0.0 0.0 - sampling.HighT1_inflow0deg.axis2 = 0.0 155.0 0.0 - sampling.HighT1_inflow0deg.normal = 0.0 0.0 1.0 - sampling.HighT1_inflow0deg.offsets = 0 5 10 15 20 25 30 35 40 45 50 55 60 65 70 75 80 85 90 95 100 105 110 115 120 125 130 135 140 145 150 155 160 165 170 175 180 185 190 195 200 205 210 215 220 225 230 - - # Turbine T2 at (x,y) = (1280.0, 3840), with D = 126.9, grid spacing = 5.0 m - sampling.HighT2_inflow0deg.type = PlaneSampler - sampling.HighT2_inflow0deg.num_points = 32 32 - sampling.HighT2_inflow0deg.origin = 1197.5 3757.5 2.5 - sampling.HighT2_inflow0deg.axis1 = 155.0 0.0 0.0 - sampling.HighT2_inflow0deg.axis2 = 0.0 155.0 0.0 - sampling.HighT2_inflow0deg.normal = 0.0 0.0 1.0 - sampling.HighT2_inflow0deg.offsets = 0 5 10 15 20 25 30 35 40 45 50 55 60 65 70 75 80 85 90 95 100 105 110 115 120 125 130 135 140 145 150 155 160 165 170 175 180 185 190 195 200 205 210 215 220 225 230 - ''' - if __name__ == '__main__': main() \ No newline at end of file From 199c7fb9f787e478e919b9f8582371eb6f88a26f Mon Sep 17 00:00:00 2001 From: Alex Rybchuk Date: Wed, 15 Feb 2023 16:49:05 -0700 Subject: [PATCH 051/124] Distinguish between lr and hr sampling freqs --- pyFAST/fastfarm/AMRWindSimulation.py | 68 +++++++++++++++++----------- 1 file changed, 41 insertions(+), 27 deletions(-) diff --git a/pyFAST/fastfarm/AMRWindSimulation.py b/pyFAST/fastfarm/AMRWindSimulation.py index 4598940..65fe202 100644 --- a/pyFAST/fastfarm/AMRWindSimulation.py +++ b/pyFAST/fastfarm/AMRWindSimulation.py @@ -36,15 +36,18 @@ def __init__(self, wts:dict, self.n_cell = n_cell self.max_level = max_level self.incflo_velocity_hh = incflo_velocity_hh - self.postproc_name = postproc_name + self.postproc_name_lr = f"{postproc_name}_lr" + self.postproc_name_hr = f"{postproc_name}_hr" self.buffer_lr = buffer_lr self.buffer_hr = buffer_hr self.ds_hr = ds_hr self.ds_lr = ds_lr # Placeholder variables, to be calculated by FFCaseCreation - self.output_frequency = None - self.sampling_labels = None + self.output_frequency_lr = None + self.output_frequency_hr = None + self.sampling_labels_lr = None + self.sampling_labels_hr = None self.nx_lr = None self.ny_lr = None self.nz_lr = None @@ -105,14 +108,18 @@ def _calc_sampling_params(self): ''' ### ~~~~~~~~~ Calculate high-level info for AMR-Wind sampling ~~~~~~~~~ - sampling_labels = ["Low"] + sampling_labels_lr = ["Low"] + self.sampling_labels_lr = sampling_labels_lr + + sampling_labels_hr = [] for turbkey in self.wts: if 'name' in self.wts[turbkey].keys(): wt_name = self.wts[turbkey]['name'] else: wt_name = f'T{turbkey}' - sampling_labels.append(f"High{wt_name}_inflow0deg") - self.sampling_labels = sampling_labels + sampling_labels_hr.append(f"High{wt_name}_inflow0deg") + + self.sampling_labels_hr = sampling_labels_hr ### ~~~~~~~~~ Calculate timestep values and AMR-Wind plane sampling frequency ~~~~~~~~~ ## Low resolution domain, dt_low_les @@ -133,7 +140,8 @@ def _calc_sampling_params(self): self.dt_high_les = self.dt * np.floor(dt_hr_max/self.dt) # Ensure that dt_hr is a multiple of the AMR-Wind timestep ## Sampling frequency - self.output_frequency = int(self.dt_high_les/self.dt) + self.output_frequency_lr = int(np.floor(self.dt_low_les/self.dt)) + self.output_frequency_hr = int(np.floor(self.dt_high_les/self.dt)) ### ~~~~~~~~~ Calculate grid resolutions ~~~~~~~~~ ## Low resolution domain, ds_lr (s = x/y/z) @@ -370,25 +378,31 @@ def write_sampling_params(self, outdir=None): print(f"Writing to {outfile} ...") with outfile.open("w") as out: # Write high-level info for sampling - sampling_labels_str = " ".join(str(item) for item in self.sampling_labels) + sampling_labels_lr_str = " ".join(str(item) for item in self.sampling_labels_lr) + sampling_labels_hr_str = " ".join(str(item) for item in self.sampling_labels_hr) out.write(f"# Sampling info generated by AMRWindSamplingCreation.py\n") - out.write(f"incflo.post_processing = {self.postproc_name} # averaging\n") - out.write(f"{self.postproc_name}.output_format = netcdf\n") - out.write(f"{self.postproc_name}.output_frequency = {self.output_frequency}\n") - out.write(f"{self.postproc_name}.fields = velocity # temperature tke\n") - out.write(f"{self.postproc_name}.labels = {sampling_labels_str}\n") + out.write(f"incflo.post_processing = {self.postproc_name_lr} {self.postproc_name_hr} # averaging\n\n") + out.write(f"{self.postproc_name_lr}.output_format = netcdf\n") + out.write(f"{self.postproc_name_lr}.output_frequency = {self.output_frequency_lr}\n") + out.write(f"{self.postproc_name_lr}.fields = velocity # temperature tke\n") + out.write(f"{self.postproc_name_lr}.labels = {sampling_labels_lr_str}\n\n") + + out.write(f"{self.postproc_name_hr}.output_format = netcdf\n") + out.write(f"{self.postproc_name_hr}.output_frequency = {self.output_frequency_hr}\n") + out.write(f"{self.postproc_name_hr}.fields = velocity # temperature tke\n") + out.write(f"{self.postproc_name_hr}.labels = {sampling_labels_hr_str}\n") # Write out low resolution sampling plane info zoffsets_lr_str = " ".join(str(int(item)) for item in self.zoffsets_lr) out.write(f"\n# Low sampling grid spacing = {self.ds_lr} m\n") - out.write(f"{self.postproc_name}.Low.type = PlaneSampler\n") - out.write(f"{self.postproc_name}.Low.num_points = {self.nx_lr} {self.ny_lr}\n") - out.write(f"{self.postproc_name}.Low.origin = {self.xlow_lr:.1f} {self.ylow_lr:.1f} {self.zlow_lr:.1f}\n") # Round the float output - out.write(f"{self.postproc_name}.Low.axis1 = {self.xdist_lr:.1f} 0.0 0.0\n") # Assume: axis1 oriented parallel to AMR-Wind x-axis - out.write(f"{self.postproc_name}.Low.axis2 = 0.0 {self.ydist_lr:.1f} 0.0\n") # Assume: axis2 oriented parallel to AMR-Wind y-axis - out.write(f"{self.postproc_name}.Low.normal = 0.0 0.0 1.0\n") - out.write(f"{self.postproc_name}.Low.offsets = {zoffsets_lr_str}\n") + out.write(f"{self.postproc_name_lr}.Low.type = PlaneSampler\n") + out.write(f"{self.postproc_name_lr}.Low.num_points = {self.nx_lr} {self.ny_lr}\n") + out.write(f"{self.postproc_name_lr}.Low.origin = {self.xlow_lr:.1f} {self.ylow_lr:.1f} {self.zlow_lr:.1f}\n") # Round the float output + out.write(f"{self.postproc_name_lr}.Low.axis1 = {self.xdist_lr:.1f} 0.0 0.0\n") # Assume: axis1 oriented parallel to AMR-Wind x-axis + out.write(f"{self.postproc_name_lr}.Low.axis2 = 0.0 {self.ydist_lr:.1f} 0.0\n") # Assume: axis2 oriented parallel to AMR-Wind y-axis + out.write(f"{self.postproc_name_lr}.Low.normal = 0.0 0.0 1.0\n") + out.write(f"{self.postproc_name_lr}.Low.offsets = {zoffsets_lr_str}\n") # Write out high resolution sampling plane info for turbkey in self.hr_domains: @@ -411,10 +425,10 @@ def write_sampling_params(self, outdir=None): zoffsets_hr_str = " ".join(str(int(item)) for item in zoffsets_hr) out.write(f"\n# Turbine {wt_name} at (x,y) = ({wt_x}, {wt_y}), with D = {wt_D}, grid spacing = {self.ds_hr} m\n") - out.write(f"{self.postproc_name}.{sampling_name}.type = PlaneSampler\n") - out.write(f"{self.postproc_name}.{sampling_name}.num_points = {nx_hr} {ny_hr}\n") - out.write(f"{self.postproc_name}.{sampling_name}.origin = {xlow_hr:.1f} {ylow_hr:.1f} {zlow_hr:.1f}\n") # Round the float output - out.write(f"{self.postproc_name}.{sampling_name}.axis1 = {xdist_hr:.1f} 0.0 0.0\n") # Assume: axis1 oriented parallel to AMR-Wind x-axis - out.write(f"{self.postproc_name}.{sampling_name}.axis2 = 0.0 {ydist_hr:.1f} 0.0\n") # Assume: axis2 oriented parallel to AMR-Wind y-axis - out.write(f"{self.postproc_name}.{sampling_name}.normal = 0.0 0.0 1.0\n") - out.write(f"{self.postproc_name}.{sampling_name}.offsets = {zoffsets_hr_str}\n") + out.write(f"{self.postproc_name_hr}.{sampling_name}.type = PlaneSampler\n") + out.write(f"{self.postproc_name_hr}.{sampling_name}.num_points = {nx_hr} {ny_hr}\n") + out.write(f"{self.postproc_name_hr}.{sampling_name}.origin = {xlow_hr:.1f} {ylow_hr:.1f} {zlow_hr:.1f}\n") # Round the float output + out.write(f"{self.postproc_name_hr}.{sampling_name}.axis1 = {xdist_hr:.1f} 0.0 0.0\n") # Assume: axis1 oriented parallel to AMR-Wind x-axis + out.write(f"{self.postproc_name_hr}.{sampling_name}.axis2 = 0.0 {ydist_hr:.1f} 0.0\n") # Assume: axis2 oriented parallel to AMR-Wind y-axis + out.write(f"{self.postproc_name_hr}.{sampling_name}.normal = 0.0 0.0 1.0\n") + out.write(f"{self.postproc_name_hr}.{sampling_name}.offsets = {zoffsets_hr_str}\n") From 8e7be56263a721202686d34109224769ab9f321e Mon Sep 17 00:00:00 2001 From: Regis Thedin Date: Wed, 15 Feb 2023 17:00:02 -0700 Subject: [PATCH 052/124] AMR/FF: Create output dir if needed --- pyFAST/fastfarm/AMRWindSimulation.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/pyFAST/fastfarm/AMRWindSimulation.py b/pyFAST/fastfarm/AMRWindSimulation.py index 4598940..93a1a97 100644 --- a/pyFAST/fastfarm/AMRWindSimulation.py +++ b/pyFAST/fastfarm/AMRWindSimulation.py @@ -1,5 +1,5 @@ import numpy as np -from pathlib import Path +import os class AMRWindSimulation: ''' @@ -363,12 +363,15 @@ def write_sampling_params(self, outdir=None): outdir: str Input file ''' - outfile = Path(outdir, 'sampling_config.i') - if outfile.is_file(): + outfile = os.path.join(outdir, 'sampling_config.i') + if not os.path.exists(outdir): + print(f'Path {outdir} does not exist. Creating it') + os.makedirs(outdir) + if os.path.isfile(outfile): raise FileExistsError(f"{str(outfile)} already exists! Aborting...") print(f"Writing to {outfile} ...") - with outfile.open("w") as out: + with open(outfile,"w") as out: # Write high-level info for sampling sampling_labels_str = " ".join(str(item) for item in self.sampling_labels) out.write(f"# Sampling info generated by AMRWindSamplingCreation.py\n") From e749a3bd5313ca9d6a3a16b62b8aa633a3f81d5f Mon Sep 17 00:00:00 2001 From: Alex Rybchuk Date: Wed, 15 Feb 2023 18:43:22 -0700 Subject: [PATCH 053/124] Ensure LR output freq is a multiple of HR --- pyFAST/fastfarm/AMRWindSimulation.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pyFAST/fastfarm/AMRWindSimulation.py b/pyFAST/fastfarm/AMRWindSimulation.py index f2fd7cd..b7a43c3 100644 --- a/pyFAST/fastfarm/AMRWindSimulation.py +++ b/pyFAST/fastfarm/AMRWindSimulation.py @@ -140,8 +140,9 @@ def _calc_sampling_params(self): self.dt_high_les = self.dt * np.floor(dt_hr_max/self.dt) # Ensure that dt_hr is a multiple of the AMR-Wind timestep ## Sampling frequency - self.output_frequency_lr = int(np.floor(self.dt_low_les/self.dt)) self.output_frequency_hr = int(np.floor(self.dt_high_les/self.dt)) + output_frequency_lr_max = int(np.floor(self.dt_low_les/self.dt)) + self.output_frequency_lr = self.output_frequency_hr * np.floor(output_frequency_lr_max/self.output_frequency_hr) ### ~~~~~~~~~ Calculate grid resolutions ~~~~~~~~~ ## Low resolution domain, ds_lr (s = x/y/z) @@ -283,6 +284,8 @@ def _check_sampling_params(self): raise ValueError(f"AMR-Wind timestep too coarse for high resolution domain! AMR-Wind timestep must be at least {self.dt_high_les} sec.") if self.dt_high_les > self.dt_low_les: raise ValueError(f"Low resolution timestep ({self.dt_low_les}) is finer than high resolution timestep ({self.dt_high_les})!") + if self.output_frequency_lr % self.output_frequency_hr != 0: + raise ValueError(f"Low resolution output frequency of {self.output_frequency_lr} not a multiple of the high resolution frequency {self.output_frequency_hr}!") ## Grid resolution checks if self.ds_low_les < self.dx0: From 76f6cc1d2b16b47969a76989451652880789d945 Mon Sep 17 00:00:00 2001 From: Alex Rybchuk Date: Thu, 16 Feb 2023 14:20:40 -0700 Subject: [PATCH 054/124] Major: Break main function into smaller funcs and move error checks locally --- pyFAST/fastfarm/AMRWindSimulation.py | 238 ++++++++++++++++----------- 1 file changed, 143 insertions(+), 95 deletions(-) diff --git a/pyFAST/fastfarm/AMRWindSimulation.py b/pyFAST/fastfarm/AMRWindSimulation.py index b7a43c3..6a624fc 100644 --- a/pyFAST/fastfarm/AMRWindSimulation.py +++ b/pyFAST/fastfarm/AMRWindSimulation.py @@ -64,7 +64,6 @@ def __init__(self, wts:dict, self._checkInputs() self._calc_simple_params() self._calc_sampling_params() - self._check_sampling_params() def _checkInputs(self): ''' @@ -107,7 +106,15 @@ def _calc_sampling_params(self): Calculate parameters for sampling planes ''' - ### ~~~~~~~~~ Calculate high-level info for AMR-Wind sampling ~~~~~~~~~ + self._calc_sampling_labels() + self._calc_sampling_time() + self._calc_grid_resolution() + self._calc_grid_placement() + + def _calc_sampling_labels(self): + ''' + Calculate labels for AMR-Wind sampling + ''' sampling_labels_lr = ["Low"] self.sampling_labels_lr = sampling_labels_lr @@ -121,7 +128,21 @@ def _calc_sampling_params(self): self.sampling_labels_hr = sampling_labels_hr - ### ~~~~~~~~~ Calculate timestep values and AMR-Wind plane sampling frequency ~~~~~~~~~ + def _calc_sampling_time(self): + ''' + Calculate timestep values and AMR-Wind plane sampling frequency + ''' + + ## High resolution domain, dt_high_les + fmax_max = 0 + for turbkey in self.wts: + fmax_max = max(0, self.wts[turbkey]['fmax']) + dt_hr_max = 1 / (2 * fmax_max) + self.dt_high_les = self.dt * np.floor(dt_hr_max/self.dt) # Ensure that dt_hr is a multiple of the AMR-Wind timestep + + if self.dt_high_les < self.dt: + raise ValueError(f"AMR-Wind timestep {self.dt} too coarse for high resolution domain! AMR-Wind timestep must be at least {self.dt_high_les} sec.") + ## Low resolution domain, dt_low_les cmeander_min = float("inf") Dwake_min = float("inf") @@ -130,28 +151,24 @@ def _calc_sampling_params(self): Dwake_min = min(Dwake_min, self.wts[turbkey]['D']) # Approximate D_wake as D_rotor dt_lr_max = cmeander_min * Dwake_min / (10 * self.vhub) - self.dt_low_les = self.dt * np.floor(dt_lr_max/self.dt) # Ensure that dt_lr is a multiple of the AMR-Wind timestep + self.dt_low_les = self.dt_high_les * np.floor(dt_lr_max/self.dt_high_les) # Ensure that dt_lr is a multiple of the high res sampling timestep - ## High resolution domain, dt_high_les - fmax_max = 0 - for turbkey in self.wts: - fmax_max = max(0, self.wts[turbkey]['fmax']) - dt_hr_max = 1 / (2 * fmax_max) - self.dt_high_les = self.dt * np.floor(dt_hr_max/self.dt) # Ensure that dt_hr is a multiple of the AMR-Wind timestep + if self.dt_low_les < self.dt: + raise ValueError(f"AMR-Wind timestep {self.dt} too coarse for low resolution domain! AMR-Wind timestep must be at least {self.dt_low_les} sec.") + if self.dt_high_les > self.dt_low_les: + raise ValueError(f"Low resolution timestep ({self.dt_low_les}) is finer than high resolution timestep ({self.dt_high_les})!") ## Sampling frequency self.output_frequency_hr = int(np.floor(self.dt_high_les/self.dt)) - output_frequency_lr_max = int(np.floor(self.dt_low_les/self.dt)) - self.output_frequency_lr = self.output_frequency_hr * np.floor(output_frequency_lr_max/self.output_frequency_hr) + self.output_frequency_lr = self.output_frequency_hr * np.floor(self.dt_low_les/self.dt_high_les) - ### ~~~~~~~~~ Calculate grid resolutions ~~~~~~~~~ - ## Low resolution domain, ds_lr (s = x/y/z) - # ASSUME: FAST.Farm LR zone uses Level 0 AMR-Wind grid spacing - # NOTE: ds_lr is calculated independent of any x/y/z requirements, - # just time step and velocity requiements - ds_lr_max = self.dt_low_les * self.vhub**2 / 15 - self.ds_low_les = self.ds0_max * np.floor(ds_lr_max/self.ds0_max) # Ensure that ds_lr is a multiple of the coarse AMR-Wind grid spacing - self.ds_lr = self.ds_low_les + if self.output_frequency_lr % self.output_frequency_hr != 0: + raise ValueError(f"Low resolution output frequency of {self.output_frequency_lr} not a multiple of the high resolution frequency {self.output_frequency_hr}!") + + def _calc_grid_resolution(self): + ''' + Calculate sampling grid resolutions + ''' ## High resolution domain, ds_hr # ASSUME: FAST.Farm HR zone lies within the region of maxmum AMR-Wind grid refinement @@ -162,7 +179,7 @@ def _calc_sampling_params(self): cmax_min = min(cmax_min, self.wts[turbkey]['cmax']) ds_hr_max = cmax_min - if self.ds_hr is None: + if self.ds_hr is None: # Calculate ds_hr if it is not specified as an input if ds_hr_max < self.ds_refine_max: raise ValueError(f"AMR-Wind grid spacing of {self.ds_refine_max} is too coarse for high resolution domain! The high-resolution domain requires "\ f"AMR-Wind grid spacing to be at least {ds_hr_max} m. If a coarser high-res domain is acceptable, then manually specify the "\ @@ -172,58 +189,35 @@ def _calc_sampling_params(self): else: self.ds_high_les = self.ds_hr - ### ~~~~~~~~~ Calculate low resolution grid placement ~~~~~~~~~ - # Calculate minimum/maximum LR domain extents - wt_all_x_min = float("inf") # Minimum x-value of any turbine - wt_all_x_max = -1*float("inf") - wt_all_y_min = float("inf") - wt_all_y_max = -1*float("inf") - wt_all_z_max = -1*float("inf") # Tallest rotor disk point of any turbine - Drot_max = -1*float("inf") - for turbkey in self.wts: - wt_all_x_min = min(wt_all_x_min, self.wts[turbkey]['x']) - wt_all_x_max = max(wt_all_x_max, self.wts[turbkey]['x']) - wt_all_y_min = min(wt_all_y_min, self.wts[turbkey]['y']) - wt_all_y_max = max(wt_all_x_min, self.wts[turbkey]['y']) - wt_all_z_max = max(wt_all_z_max, self.wts[turbkey]['zhub'] + 0.5*self.wts[turbkey]['D']) - Drot_max = max(Drot_max, self.wts[turbkey]['D']) - - xlow_lr_min = wt_all_x_min - self.buffer_lr[0] * Drot_max - xhigh_lr_max = wt_all_x_max + self.buffer_lr[1] * Drot_max - ylow_lr_min = wt_all_y_min - self.buffer_lr[2] * Drot_max - yhigh_lr_max = wt_all_y_max + self.buffer_lr[3] * Drot_max - zhigh_lr_max = wt_all_z_max + self.buffer_lr[4] * Drot_max - - # Calculate the minimum/maximum LR domain coordinate lengths & number of grid cells - xdist_lr_min = xhigh_lr_max - xlow_lr_min # Minumum possible length of x-extent of LR domain - self.xdist_lr = self.ds_low_les * np.ceil(xdist_lr_min/self.ds_low_les) # The `+ ds_lr` comes from the +1 to NS_LOW in Sec. 4.2.15.6.4.1.1 - # TODO: adjust xdist_lr calculation by also using `inflow_deg` - self.nx_lr = int(self.xdist_lr/self.ds_low_les) + 1 + if self.ds_high_les < self.ds_refine_max: + raise ValueError(f"AMR-Wind fine grid spacing {self.ds_refine_max} too coarse for high resolution domain! AMR-Wind grid spacing must be at least {self.ds_high_les} m.") - ydist_lr_min = yhigh_lr_max - ylow_lr_min - self.ydist_lr = self.ds_low_les * np.ceil(ydist_lr_min/self.ds_low_les) - # TODO: adjust ydist_lr calculation by also using `inflow_deg` - self.ny_lr = int(self.ydist_lr/self.ds_low_les) + 1 + ## Low resolution domain, ds_lr (s = x/y/z) + # ASSUME: FAST.Farm LR zone uses Level 0 AMR-Wind grid spacing + # NOTE: ds_lr is calculated independent of any x/y/z requirements, + # just time step and velocity requiements + if self.ds_lr is None: + ds_lr_max = self.dt_low_les * self.vhub**2 / 15 + self.ds_low_les = self.ds0_max * np.floor(ds_lr_max/self.ds0_max) # Ensure that ds_lr is a multiple of the coarse AMR-Wind grid spacing + self.ds_lr = self.ds_low_les + else: + self.ds_low_les = self.ds_lr - self.zdist_lr = self.ds_low_les * np.ceil(zhigh_lr_max/self.ds_low_les) - self.nz_lr = int(self.zdist_lr/self.ds_low_les) + 1 + if self.ds_low_les < self.ds0_max: + raise ValueError(f"AMR-Wind coarse grid spacing {self.ds0_max} too coarse for high resolution domain! AMR-Wind grid spacing must be at least {self.ds_low_les} m.") - ## Calculate actual LR domain extent - # NOTE: Sampling planes should measure at AMR-Wind cell centers, not cell edges - # NOTE: Should we use dx/dy/dz values here or ds_lr? - # - AR: I think it's correct to use ds_lr to get to the xlow values, - # but then offset by 0.5*amr_dx0 if need be - self.xlow_lr = self.ds_low_les * np.floor(xlow_lr_min/self.ds_low_les) - 0.5*self.dx0 + self.prob_lo[0]%self.ds_low_les - self.xhigh_lr = self.xlow_lr + self.xdist_lr - self.ylow_lr = self.ds_low_les * np.floor(ylow_lr_min/self.ds_low_les) - 0.5*self.dy0 + self.prob_lo[1]%self.ds_low_les - self.yhigh_lr = self.ylow_lr + self.ydist_lr - self.zlow_lr = 0.5 * self.dz0 # Lowest z point is half the height of the lowest grid cell - self.zhigh_lr = self.zlow_lr + self.zdist_lr - self.zoffsets_lr = np.arange(self.zlow_lr, self.zhigh_lr+self.ds_low_les, self.ds_low_les) - self.zlow_lr + def _calc_grid_placement(self): + ''' + Calculate placement of sampling grids + ''' - ## Save out info for FFCaseCreation - self.extent_low = self.buffer_lr + self._calc_hr_grid_placement() + self._calc_lr_grid_placement() + def _calc_hr_grid_placement(self): + ''' + Calculate placement of high resolution grids + ''' ### ~~~~~~~~~ Calculate high resolution grid placement ~~~~~~~~~ hr_domains = {} for turbkey in self.wts: @@ -261,6 +255,20 @@ def _calc_sampling_params(self): zhigh_hr = zlow_hr + zdist_hr zoffsets_hr = np.arange(zlow_hr, zhigh_hr+self.ds_high_les, self.ds_high_les) - zlow_hr + # Check domain extents + if xhigh_hr > self.prob_hi[0]: + raise ValueError(f"HR domain point {xhigh_hr} extends beyond maximum AMR-Wind x-extent!") + if xlow_hr < self.prob_lo[0]: + raise ValueError(f"HR domain point {xlow_hr} extends beyond minimum AMR-Wind x-extent!") + if yhigh_hr > self.prob_hi[1]: + raise ValueError(f"HR domain point {yhigh_hr} extends beyond maximum AMR-Wind y-extent!") + if ylow_hr < self.prob_lo[1]: + raise ValueError(f"HR domain point {ylow_hr} extends beyond minimum AMR-Wind y-extent!") + if zhigh_hr > self.prob_hi[2]: + raise ValueError(f"HR domain point {zhigh_hr} extends beyond maximum AMR-Wind z-extent!") + if zlow_hr < self.prob_lo[2]: + raise ValueError(f"HR domain point {zlow_hr} extends beyond minimum AMR-Wind z-extent!") + # Save out info for FFCaseCreation self.extent_high = self.buffer_hr @@ -272,44 +280,84 @@ def _calc_sampling_params(self): hr_domains[turbkey] = hr_turb_info self.hr_domains = hr_domains - def _check_sampling_params(self): + def _calc_lr_grid_placement(self): ''' - Check the values of parameters that were calculated by _calc_sampling_params + Calculate placement of low resolution grid ''' - ## Timestep checks - if self.dt_low_les < self.dt: - raise ValueError(f"AMR-Wind timestep too coarse for low resolution domain! AMR-Wind timestep must be at least {self.dt_low_les} sec.") - if self.dt_high_les < self.dt: - raise ValueError(f"AMR-Wind timestep too coarse for high resolution domain! AMR-Wind timestep must be at least {self.dt_high_les} sec.") - if self.dt_high_les > self.dt_low_les: - raise ValueError(f"Low resolution timestep ({self.dt_low_les}) is finer than high resolution timestep ({self.dt_high_les})!") - if self.output_frequency_lr % self.output_frequency_hr != 0: - raise ValueError(f"Low resolution output frequency of {self.output_frequency_lr} not a multiple of the high resolution frequency {self.output_frequency_hr}!") + ### ~~~~~~~~~ Calculate low resolution grid placement ~~~~~~~~~ + # Calculate minimum/maximum LR domain extents + wt_all_x_min = float("inf") # Minimum x-value of any turbine + wt_all_x_max = -1*float("inf") + wt_all_y_min = float("inf") + wt_all_y_max = -1*float("inf") + wt_all_z_max = -1*float("inf") # Tallest rotor disk point of any turbine + Drot_max = -1*float("inf") + for turbkey in self.wts: + wt_all_x_min = min(wt_all_x_min, self.wts[turbkey]['x']) + wt_all_x_max = max(wt_all_x_max, self.wts[turbkey]['x']) + wt_all_y_min = min(wt_all_y_min, self.wts[turbkey]['y']) + wt_all_y_max = max(wt_all_x_min, self.wts[turbkey]['y']) + wt_all_z_max = max(wt_all_z_max, self.wts[turbkey]['zhub'] + 0.5*self.wts[turbkey]['D']) + Drot_max = max(Drot_max, self.wts[turbkey]['D']) + + xlow_lr_min = wt_all_x_min - self.buffer_lr[0] * Drot_max + xhigh_lr_max = wt_all_x_max + self.buffer_lr[1] * Drot_max + ylow_lr_min = wt_all_y_min - self.buffer_lr[2] * Drot_max + yhigh_lr_max = wt_all_y_max + self.buffer_lr[3] * Drot_max + zhigh_lr_max = wt_all_z_max + self.buffer_lr[4] * Drot_max - ## Grid resolution checks - if self.ds_low_les < self.dx0: - raise ValueError(f"AMR-Wind Level 0 x-grid spacing too coarse for low resolution domain! AMR-Wind x-grid spacing must be at least {self.ds_low_les} m.") - if self.ds_low_les < self.dy0: - raise ValueError(f"AMR-Wind Level 0 y-grid spacing too coarse for low resolution domain! AMR-Wind y-grid spacing must be at least {self.ds_low_les} m.") - if self.ds_low_les < self.dz0: - raise ValueError(f"AMR-Wind Level 0 z-grid spacing too coarse for low resolution domain! AMR-Wind z-grid spacing must be at least {self.ds_low_les} m.") - if self.ds_high_les < self.ds_refine_max: - raise ValueError(f"AMR-Wind grid spacing too coarse for high resolution domain! AMR-Wind grid spacing must be at least {self.ds_high_les} m.") + # Calculate the minimum/maximum LR domain coordinate lengths & number of grid cells + xdist_lr_min = xhigh_lr_max - xlow_lr_min # Minumum possible length of x-extent of LR domain + self.xdist_lr = self.ds_low_les * np.ceil(xdist_lr_min/self.ds_low_les) # The `+ ds_lr` comes from the +1 to NS_LOW in Sec. 4.2.15.6.4.1.1 + # TODO: adjust xdist_lr calculation by also using `inflow_deg` + self.nx_lr = int(self.xdist_lr/self.ds_low_les) + 1 + + ydist_lr_min = yhigh_lr_max - ylow_lr_min + self.ydist_lr = self.ds_low_les * np.ceil(ydist_lr_min/self.ds_low_les) + # TODO: adjust ydist_lr calculation by also using `inflow_deg` + self.ny_lr = int(self.ydist_lr/self.ds_low_les) + 1 + + self.zdist_lr = self.ds_low_les * np.ceil(zhigh_lr_max/self.ds_low_les) + self.nz_lr = int(self.zdist_lr/self.ds_low_les) + 1 + + ## Calculate actual LR domain extent + # NOTE: Sampling planes should measure at AMR-Wind cell centers, not cell edges + # NOTE: Should we use dx/dy/dz values here or ds_lr? + # - AR: I think it's correct to use ds_lr to get to the xlow values, + # but then offset by 0.5*amr_dx0 if need be + self.xlow_lr = self.ds_low_les * np.floor(xlow_lr_min/self.ds_low_les) - 0.5*self.dx0 + self.prob_lo[0]%self.ds_low_les + self.xhigh_lr = self.xlow_lr + self.xdist_lr + self.ylow_lr = self.ds_low_les * np.floor(ylow_lr_min/self.ds_low_les) - 0.5*self.dy0 + self.prob_lo[1]%self.ds_low_les + self.yhigh_lr = self.ylow_lr + self.ydist_lr + self.zlow_lr = 0.5 * self.dz0 # Lowest z point is half the height of the lowest grid cell + self.zhigh_lr = self.zlow_lr + self.zdist_lr + self.zoffsets_lr = np.arange(self.zlow_lr, self.zhigh_lr+self.ds_low_les, self.ds_low_les) - self.zlow_lr - ## Low resolution domain extent checks + ## Check domain extents if self.xhigh_lr > self.prob_hi[0]: - raise ValueError("LR domain extends beyond maximum AMR-Wind x-extent!") + raise ValueError(f"LR domain point {self.xhigh_lr} extends beyond maximum AMR-Wind x-extent!") if self.xlow_lr < self.prob_lo[0]: - raise ValueError("LR domain extends beyond minimum AMR-Wind x-extent!") + raise ValueError(f"LR domain point {self.xlow_lr} extends beyond minimum AMR-Wind x-extent!") if self.yhigh_lr > self.prob_hi[1]: - raise ValueError("LR domain extends beyond maximum AMR-Wind y-extent!") + raise ValueError(f"LR domain point {self.yhigh_lr} extends beyond maximum AMR-Wind y-extent!") if self.ylow_lr < self.prob_lo[1]: - raise ValueError("LR domain extends beyond minimum AMR-Wind y-extent!") + raise ValueError(f"LR domain point {self.ylow_lr} extends beyond minimum AMR-Wind y-extent!") if self.zhigh_lr > self.prob_hi[2]: - raise ValueError("LR domain extends beyond maximum AMR-Wind z-extent!") + raise ValueError(f"LR domain point {self.zhigh_lr} extends beyond maximum AMR-Wind z-extent!") if self.zlow_lr < self.prob_lo[2]: - raise ValueError("LR domain extends beyond minimum AMR-Wind z-extent!") + raise ValueError(f"LR domain point {self.zlow_lr} extends beyond minimum AMR-Wind z-extent!") + + ## Check grid placement + self._check_grid_placement() + + ## Save out info for FFCaseCreation + self.extent_low = self.buffer_lr + + def _check_grid_placement(self): + ''' + Check the values of parameters that were calculated by _calc_sampling_params + ''' ## Check that sampling grids are at cell centers # Low resolution grid From 00bd380c04fd26428834d36039cb45c109c75fb3 Mon Sep 17 00:00:00 2001 From: Alex Rybchuk Date: Thu, 16 Feb 2023 14:25:01 -0700 Subject: [PATCH 055/124] Bugfix: height of lowest hr grid --- pyFAST/fastfarm/AMRWindSimulation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyFAST/fastfarm/AMRWindSimulation.py b/pyFAST/fastfarm/AMRWindSimulation.py index 6a624fc..b9036d2 100644 --- a/pyFAST/fastfarm/AMRWindSimulation.py +++ b/pyFAST/fastfarm/AMRWindSimulation.py @@ -251,7 +251,7 @@ def _calc_hr_grid_placement(self): xhigh_hr = xlow_hr + xdist_hr ylow_hr = self.ds_high_les * np.floor(ylow_hr_min/self.ds_high_les) - 0.5*self.dy_refine + self.prob_lo[1]%self.ds_high_les yhigh_hr = ylow_hr + ydist_hr - zlow_hr = self.zlow_lr / (2**self.max_level) + zlow_hr = 0.5 * self.dz0 / (2**self.max_level) zhigh_hr = zlow_hr + zdist_hr zoffsets_hr = np.arange(zlow_hr, zhigh_hr+self.ds_high_les, self.ds_high_les) - zlow_hr From 77ea6b94be23ac5d19bfa83880455da41ebd9d0c Mon Sep 17 00:00:00 2001 From: Alex Rybchuk Date: Thu, 16 Feb 2023 14:28:00 -0700 Subject: [PATCH 056/124] Add grid spacing check --- pyFAST/fastfarm/AMRWindSimulation.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyFAST/fastfarm/AMRWindSimulation.py b/pyFAST/fastfarm/AMRWindSimulation.py index b9036d2..9a71e0e 100644 --- a/pyFAST/fastfarm/AMRWindSimulation.py +++ b/pyFAST/fastfarm/AMRWindSimulation.py @@ -205,6 +205,8 @@ def _calc_grid_resolution(self): if self.ds_low_les < self.ds0_max: raise ValueError(f"AMR-Wind coarse grid spacing {self.ds0_max} too coarse for high resolution domain! AMR-Wind grid spacing must be at least {self.ds_low_les} m.") + if self.ds_low_les % self.ds_high_les != 0: + raise ValueError(f"Low resolution grid spacing of {self.ds_low_les} not a multiple of the high resolution grid spacing {self.ds_high_les}!") def _calc_grid_placement(self): ''' From cc1f83348371c8d145e2f3f4f40fca235ecc7240 Mon Sep 17 00:00:00 2001 From: Regis Thedin Date: Thu, 9 Mar 2023 18:07:32 -0700 Subject: [PATCH 057/124] Fix `os.system` calls; add `modifyProperty` --- pyFAST/fastfarm/FASTFarmCaseCreation.py | 56 ++++++++++++++++++------- 1 file changed, 41 insertions(+), 15 deletions(-) diff --git a/pyFAST/fastfarm/FASTFarmCaseCreation.py b/pyFAST/fastfarm/FASTFarmCaseCreation.py index 23c3df9..63710dc 100644 --- a/pyFAST/fastfarm/FASTFarmCaseCreation.py +++ b/pyFAST/fastfarm/FASTFarmCaseCreation.py @@ -290,7 +290,7 @@ def copyTurbineFilesForEachCase(self, writeFiles=True): # Write updated DISCON and *Dyn files. if writeFiles: self.HydroDynFile.write(os.path.join(currPath, self.HDfilename)) - status = os.system(f'cp {self.templatePath}/{self.controllerInputfilename} {currPath}/{self.controllerInputfilename}') + status = os.popen(f'cp {self.templatePath}/{self.controllerInputfilename} {currPath}/{self.controllerInputfilename}') # Depending on the controller, it might need to be in the same level as the fstf input file. The ideal solution would be give the full # path to the controller input, but we have no control over the compilation process and it is likely that a very long string with the full @@ -307,7 +307,8 @@ def copyTurbineFilesForEachCase(self, writeFiles=True): pass os.chdir(notepath) - # Write InflowWind files + # Write InflowWind files. For FAST.FARM, the IW file needs to be inside the Seed* directories. If running standalone openfast, + # it needs to be on the same level as the fst file. Here, we copy to both places so that the workflow is general self.InflowWindFile['WindType'] = 3 self.InflowWindFile['PropagationDir'] = 0 self.InflowWindFile['Filename_BTS'] = '"./TurbSim"' @@ -331,7 +332,9 @@ def copyTurbineFilesForEachCase(self, writeFiles=True): # Update each turbine's ElastoDyn self.ElastoDynFile['NacYaw'] = yaw_deg_ + yaw_mis_deg_ # check if this change of EDfile to a variable works as it should with quotes and stuff + status = os.popen(f'cp {self.templatePath}/{self.bladefilename} {currPath}/{self.bladefilename}') self.ElastoDynFile['BldFile1'] = self.ElastoDynFile['BldFile2'] = self.ElastoDynFile['BldFile3'] = f'"{self.bladefilename}"' + status = os.popen(f'cp {self.templatePath}/{self.towerfilename} {currPath}/{self.towerfilename}') self.ElastoDynFile['TwrFile'] = f'"{self.towerfilename}"' self.ElastoDynFile['Azimuth'] = round(np.random.uniform(low=0, high=360)) # start at a random value if writeFiles: @@ -378,7 +381,7 @@ def copyTurbineFilesForEachCase(self, writeFiles=True): self.turbineFile['CompAero'] = 3 # 1: AeroDyn v14; 2: AeroDyn v15; 3: AeroDisk self.turbineFile['AeroFile'] = f'"{self.ADskfilepath}"' if writeFiles: - status = os.system(f'cp {self.coeffTablefilepath} {os.path.join(currPath,self.coeffTablefilename)}') + status = os.popen(f'cp {self.coeffTablefilepath} {os.path.join(currPath,self.coeffTablefilename)}') self.turbineFile['ServoFile'] = f'"./{self.SrvDfilename}{t+1}_mod.dat"' self.turbineFile['HydroFile'] = f'"./{self.HDfilename}"' self.turbineFile['SubFile'] = f'"{self.SubDfilepath}"' @@ -390,16 +393,39 @@ def copyTurbineFilesForEachCase(self, writeFiles=True): if writeFiles: self.turbineFile.write( os.path.join(currPath,f'{self.turbfilename}{t+1}.fst')) + def modifyProperty(self, fullfilename, entry, value): + ''' + Modify specific properties of certain files + + Inputs + ====== + fullfilename: str + Full filepath of the file. + entry: str + Entry in the input file to be modified + value: + Value to go on the entry. No checks are made + + ''' + + # Open the proper file + f = FASTInputFile(fullfilename) + # Change the actual value + f[entry] = value + # Save the new file + f.write(fullfname) + + return def setTemplateFilename(self, templatePath=None, EDfilename=None, SEDfilename=None, HDfilename=None, SrvDfilename=None, ADfilename=None, ADskfilename=None, SubDfilename=None, IWfilename=None, BDfilepath=None, bladefilename=None, towerfilename=None, turbfilename=None, libdisconfilepath=None, controllerInputfilename=None, coeffTablefilename=None, turbsimLowfilepath=None, turbsimHighfilepath=None, FFfilename=None): ''' *filename: str - The filename of the current O penFAST submodule, no complete path. Assumes it is + The filename of the current OpenFAST submodule, no complete path. Assumes it is inside `templatePath` *filepath: str - Complete path of the file. Ma y or may not be inside `templatePath` + Complete path of the file. May or may not be inside `templatePath` ''' @@ -571,7 +597,7 @@ def _create_copy_libdiscon(self): self.DLLfilepath = os.path.join(os.path.dirname(self.libdisconfilepath), f'{libdisconfilename}.T') if not os.path.isfile(currLibdiscon): if self.verbose>0: print(f' Creating a copy of the controller {libdisconfilename}.so in {currLibdiscon}') - status = os.system(f'cp {self.libdisconfilepath} {currLibdiscon}') + status = os.popen(f'cp {self.libdisconfilepath} {currLibdiscon}') copied=True if copied == False and self.verbose>0: @@ -845,7 +871,7 @@ def TS_low_slurm_prepare(self, slurmfilepath): # for seed in range(self.nSeeds): # # fname = f'runFASTFarm_cond{cond}_case{case}_seed{seed}.sh' - # status = os.system(f'cp {slurmfilepath} {os.path.join(self.path,fname)}') + # status = os.popen(f'cp {slurmfilepath} {os.path.join(self.path,fname)}') # # # Change job name (for convenience only) # sed_command = f"sed -i 's|#SBATCH --job-name=runFF|#SBATCH --job-name=c{cond}_c{case}_s{seed}_runFF_{os.path.basename(self.path)}|g' {fname}" @@ -880,7 +906,7 @@ def TS_low_slurm_prepare(self, slurmfilepath): self.slurmfilename_low = os.path.basename(slurmfilepath) import subprocess - status = os.system(f'cp {slurmfilepath} {self.path}/{self.slurmfilename_low}') + status = os.popen(f'cp {slurmfilepath} {self.path}/{self.slurmfilename_low}') # Change job name (for convenience only) _ = subprocess.call(f"sed -i 's|#SBATCH --job-name=lowBox|#SBATCH --job-name=lowBox_{os.path.basename(self.path)}|g' {self.slurmfilename_low}", cwd=self.path, shell=True) @@ -1102,7 +1128,7 @@ def TS_high_slurm_prepare(self, slurmfilepath): self.slurmfilename_high = os.path.basename(slurmfilepath) ntasks = self.nConditions*self.nHighBoxCases*self.nSeeds*self.nTurbines - status = os.system(f'cp {slurmfilepath} {self.path}/{self.slurmfilename_high}') + status = os.popen(f'cp {slurmfilepath} {self.path}/{self.slurmfilename_high}') # Change job name (for convenience only) _ = subprocess.call(f"sed -i 's|#SBATCH --job-name=highBox|#SBATCH --job-name=highBox_{os.path.basename(self.path)}|g' {self.slurmfilename_high}", cwd=self.path, shell=True) @@ -1260,12 +1286,12 @@ def _FF_setup_LES(self, seedsToKeep=1): # Clean unnecessary directories and files created by the general setup for cond in range(self.nConditions): for seed in range(self.nSeeds): - os.system(f"rm -rf {os.path.join(self.path,self.condDirList[cond],f'Seed_{seed}')}") + os.popen(f"rm -rf {os.path.join(self.path,self.condDirList[cond],f'Seed_{seed}')}") for case in range(self.nCases): - #os.system(f"rm -rf {os.path.join(path,condDirList[cond],caseDirList[case], f'Seed_0','InflowWind.dat')}") # needs to exist + #os.popen(f"rm -rf {os.path.join(path,condDirList[cond],caseDirList[case], f'Seed_0','InflowWind.dat')}") # needs to exist for seed in range(seedsToKeep,self.nSeeds): - os.system(f"rm -rf {os.path.join(self.path,self.condDirList[cond],self.caseDirList[case], f'Seed_{seed}')}") + os.popen(f"rm -rf {os.path.join(self.path,self.condDirList[cond],self.caseDirList[case], f'Seed_{seed}')}") @@ -1276,7 +1302,7 @@ def _FF_setup_LES(self, seedsToKeep=1): for case in range(self.nCases): for seed in range(self.seedsToKeep): # Remove TurbSim dir and create LES boxes dir - _ = os.system(f"rm -rf {os.path.join(self.path,self.condDirList[cond],self.caseDirList[case], f'Seed_{seed}', 'TurbSim')}") + _ = os.popen(f"rm -rf {os.path.join(self.path,self.condDirList[cond],self.caseDirList[case], f'Seed_{seed}', 'TurbSim')}") if not os.path.exists(os.path.join(self.path,self.condDirList[cond],self.caseDirList[case],f'Seed_{seed}',LESboxesDirName)): os.makedirs(os.path.join(self.path,self.condDirList[cond],self.caseDirList[case],f'Seed_{seed}',LESboxesDirName)) @@ -1611,7 +1637,7 @@ def FF_slurm_prepare(self, slurmfilepath): for seed in range(self.nSeeds): fname = f'runFASTFarm_cond{cond}_case{case}_seed{seed}.sh' - status = os.system(f'cp {slurmfilepath} {os.path.join(self.path,fname)}') + status = os.popen(f'cp {slurmfilepath} {os.path.join(self.path,fname)}') # Change job name (for convenience only) sed_command = f"sed -i 's|#SBATCH --job-name=runFF|#SBATCH --job-name=c{cond}_c{case}_s{seed}_runFF_{os.path.basename(self.path)}|g' {fname}" @@ -1654,4 +1680,4 @@ def FF_slurm_submit(self): sub_command = f"sbatch {fname}" print(f'Calling: {sub_command}') subprocess.call(sub_command, cwd=self.path, shell=True) - time.sleep(4) # Sometimes the same job gets submitted twice. This gets around it. \ No newline at end of file + time.sleep(4) # Sometimes the same job gets submitted twice. This gets around it. From 0b42d247a465943cc14f43e834622b9b9e05939e Mon Sep 17 00:00:00 2001 From: Regis Thedin Date: Thu, 9 Mar 2023 18:08:54 -0700 Subject: [PATCH 058/124] FAST output: remove duplicate in dataframe --- pyFAST/input_output/fast_output_file.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pyFAST/input_output/fast_output_file.py b/pyFAST/input_output/fast_output_file.py index fedb308..baa2dc2 100644 --- a/pyFAST/input_output/fast_output_file.py +++ b/pyFAST/input_output/fast_output_file.py @@ -155,6 +155,9 @@ def toDataFrame(self): raise BrokenFormatError('Inconstistent number of columns between headers ({}) and data ({}) for file {}'.format(len(cols), self.data.shape[1], self.filename)) df = pd.DataFrame(data=self.data,columns=cols) + # Remove duplicate columns if they are present + df = df.loc[:,~df.columns.duplicated()].copy() + return df def writeDataFrame(self, df, filename, binary=True): From fe4eda889306ed390dda53e0b61c9246b8e12f60 Mon Sep 17 00:00:00 2001 From: Regis Thedin Date: Thu, 9 Mar 2023 18:52:38 -0700 Subject: [PATCH 059/124] FF/AMR: bugfix on box extent computation --- pyFAST/fastfarm/AMRWindSimulation.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyFAST/fastfarm/AMRWindSimulation.py b/pyFAST/fastfarm/AMRWindSimulation.py index 9a71e0e..27b7f72 100644 --- a/pyFAST/fastfarm/AMRWindSimulation.py +++ b/pyFAST/fastfarm/AMRWindSimulation.py @@ -198,7 +198,7 @@ def _calc_grid_resolution(self): # just time step and velocity requiements if self.ds_lr is None: ds_lr_max = self.dt_low_les * self.vhub**2 / 15 - self.ds_low_les = self.ds0_max * np.floor(ds_lr_max/self.ds0_max) # Ensure that ds_lr is a multiple of the coarse AMR-Wind grid spacing + self.ds_low_les = self.ds_hr * np.floor(ds_lr_max/self.ds_hr) # ds_hr is already a multiple of the AMR-Wind grid spacing, so here we need to make sure ds_lr is a multiple of ds_hr self.ds_lr = self.ds_low_les else: self.ds_low_les = self.ds_lr @@ -299,7 +299,7 @@ def _calc_lr_grid_placement(self): wt_all_x_min = min(wt_all_x_min, self.wts[turbkey]['x']) wt_all_x_max = max(wt_all_x_max, self.wts[turbkey]['x']) wt_all_y_min = min(wt_all_y_min, self.wts[turbkey]['y']) - wt_all_y_max = max(wt_all_x_min, self.wts[turbkey]['y']) + wt_all_y_max = max(wt_all_y_min, self.wts[turbkey]['y']) wt_all_z_max = max(wt_all_z_max, self.wts[turbkey]['zhub'] + 0.5*self.wts[turbkey]['D']) Drot_max = max(Drot_max, self.wts[turbkey]['D']) From 5bc598b444a7df312a8972e41ce00d05d81d0e21 Mon Sep 17 00:00:00 2001 From: Regis Thedin Date: Fri, 10 Mar 2023 09:02:46 -0700 Subject: [PATCH 060/124] FF/AMR: Allow non-integer offsets on sampling boxes --- pyFAST/fastfarm/AMRWindSimulation.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyFAST/fastfarm/AMRWindSimulation.py b/pyFAST/fastfarm/AMRWindSimulation.py index 27b7f72..f2285d6 100644 --- a/pyFAST/fastfarm/AMRWindSimulation.py +++ b/pyFAST/fastfarm/AMRWindSimulation.py @@ -449,7 +449,7 @@ def write_sampling_params(self, outdir=None): out.write(f"{self.postproc_name_hr}.labels = {sampling_labels_hr_str}\n") # Write out low resolution sampling plane info - zoffsets_lr_str = " ".join(str(int(item)) for item in self.zoffsets_lr) + zoffsets_lr_str = " ".join(str(item) for item in self.zoffsets_lr) out.write(f"\n# Low sampling grid spacing = {self.ds_lr} m\n") out.write(f"{self.postproc_name_lr}.Low.type = PlaneSampler\n") @@ -478,7 +478,7 @@ def write_sampling_params(self, outdir=None): xdist_hr = self.hr_domains[turbkey]['xdist_hr'] ydist_hr = self.hr_domains[turbkey]['ydist_hr'] zoffsets_hr = self.hr_domains[turbkey]['zoffsets_hr'] - zoffsets_hr_str = " ".join(str(int(item)) for item in zoffsets_hr) + zoffsets_hr_str = " ".join(str(item) for item in zoffsets_hr) out.write(f"\n# Turbine {wt_name} at (x,y) = ({wt_x}, {wt_y}), with D = {wt_D}, grid spacing = {self.ds_hr} m\n") out.write(f"{self.postproc_name_hr}.{sampling_name}.type = PlaneSampler\n") From fff93d630c32a7e8b94cb08184db9522ca06e439 Mon Sep 17 00:00:00 2001 From: Regis Thedin Date: Fri, 10 Mar 2023 10:29:38 -0700 Subject: [PATCH 061/124] AMR: fix integer frequency; tweak printed output --- pyFAST/fastfarm/AMRWindSimulation.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/pyFAST/fastfarm/AMRWindSimulation.py b/pyFAST/fastfarm/AMRWindSimulation.py index f2285d6..1497a19 100644 --- a/pyFAST/fastfarm/AMRWindSimulation.py +++ b/pyFAST/fastfarm/AMRWindSimulation.py @@ -160,7 +160,7 @@ def _calc_sampling_time(self): ## Sampling frequency self.output_frequency_hr = int(np.floor(self.dt_high_les/self.dt)) - self.output_frequency_lr = self.output_frequency_hr * np.floor(self.dt_low_les/self.dt_high_les) + self.output_frequency_lr = int(self.output_frequency_hr * np.floor(self.dt_low_les/self.dt_high_les)) if self.output_frequency_lr % self.output_frequency_hr != 0: raise ValueError(f"Low resolution output frequency of {self.output_frequency_lr} not a multiple of the high resolution frequency {self.output_frequency_hr}!") @@ -454,9 +454,9 @@ def write_sampling_params(self, outdir=None): out.write(f"\n# Low sampling grid spacing = {self.ds_lr} m\n") out.write(f"{self.postproc_name_lr}.Low.type = PlaneSampler\n") out.write(f"{self.postproc_name_lr}.Low.num_points = {self.nx_lr} {self.ny_lr}\n") - out.write(f"{self.postproc_name_lr}.Low.origin = {self.xlow_lr:.1f} {self.ylow_lr:.1f} {self.zlow_lr:.1f}\n") # Round the float output - out.write(f"{self.postproc_name_lr}.Low.axis1 = {self.xdist_lr:.1f} 0.0 0.0\n") # Assume: axis1 oriented parallel to AMR-Wind x-axis - out.write(f"{self.postproc_name_lr}.Low.axis2 = 0.0 {self.ydist_lr:.1f} 0.0\n") # Assume: axis2 oriented parallel to AMR-Wind y-axis + out.write(f"{self.postproc_name_lr}.Low.origin = {self.xlow_lr:.4f} {self.ylow_lr:.4f} {self.zlow_lr:.4f}\n") # Round the float output + out.write(f"{self.postproc_name_lr}.Low.axis1 = {self.xdist_lr:.4f} 0.0 0.0\n") # Assume: axis1 oriented parallel to AMR-Wind x-axis + out.write(f"{self.postproc_name_lr}.Low.axis2 = 0.0 {self.ydist_lr:.4f} 0.0\n") # Assume: axis2 oriented parallel to AMR-Wind y-axis out.write(f"{self.postproc_name_lr}.Low.normal = 0.0 0.0 1.0\n") out.write(f"{self.postproc_name_lr}.Low.offsets = {zoffsets_lr_str}\n") @@ -483,8 +483,8 @@ def write_sampling_params(self, outdir=None): out.write(f"\n# Turbine {wt_name} at (x,y) = ({wt_x}, {wt_y}), with D = {wt_D}, grid spacing = {self.ds_hr} m\n") out.write(f"{self.postproc_name_hr}.{sampling_name}.type = PlaneSampler\n") out.write(f"{self.postproc_name_hr}.{sampling_name}.num_points = {nx_hr} {ny_hr}\n") - out.write(f"{self.postproc_name_hr}.{sampling_name}.origin = {xlow_hr:.1f} {ylow_hr:.1f} {zlow_hr:.1f}\n") # Round the float output - out.write(f"{self.postproc_name_hr}.{sampling_name}.axis1 = {xdist_hr:.1f} 0.0 0.0\n") # Assume: axis1 oriented parallel to AMR-Wind x-axis - out.write(f"{self.postproc_name_hr}.{sampling_name}.axis2 = 0.0 {ydist_hr:.1f} 0.0\n") # Assume: axis2 oriented parallel to AMR-Wind y-axis + out.write(f"{self.postproc_name_hr}.{sampling_name}.origin = {xlow_hr:.4f} {ylow_hr:.4f} {zlow_hr:.4f}\n") # Round the float output + out.write(f"{self.postproc_name_hr}.{sampling_name}.axis1 = {xdist_hr:.4f} 0.0 0.0\n") # Assume: axis1 oriented parallel to AMR-Wind x-axis + out.write(f"{self.postproc_name_hr}.{sampling_name}.axis2 = 0.0 {ydist_hr:.4f} 0.0\n") # Assume: axis2 oriented parallel to AMR-Wind y-axis out.write(f"{self.postproc_name_hr}.{sampling_name}.normal = 0.0 0.0 1.0\n") out.write(f"{self.postproc_name_hr}.{sampling_name}.offsets = {zoffsets_hr_str}\n") From 1a9af502668fa260e424c332928509c3ee257212 Mon Sep 17 00:00:00 2001 From: Regis Thedin Date: Fri, 10 Mar 2023 11:16:58 -0700 Subject: [PATCH 062/124] FF/AMR: Add option to specify mod_wake, which will affect `dt_low` --- pyFAST/fastfarm/AMRWindSimulation.py | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/pyFAST/fastfarm/AMRWindSimulation.py b/pyFAST/fastfarm/AMRWindSimulation.py index 1497a19..b7486c5 100644 --- a/pyFAST/fastfarm/AMRWindSimulation.py +++ b/pyFAST/fastfarm/AMRWindSimulation.py @@ -19,7 +19,8 @@ def __init__(self, wts:dict, postproc_name='sampling', buffer_lr = [3,6,3,3,2], buffer_hr = 0.6, - ds_hr = None, ds_lr = None): + ds_hr = None, ds_lr = None, + wake_mod = 1): ''' Values from the AMR-Wind input file Inputs: @@ -27,6 +28,7 @@ def __init__(self, wts:dict, * incflo_velocity_hh: velocity vector, specifically at hub height * buffer_lr: buffer for [xmin, xmax, ymin, ymax, zmax] in low-res box, in D * buffer_hr: buffer for all directions (constant) in high-res box, in D + * wake_mod: Wake formulations within FAST.Farm. 1:polar; 2:curl; 3:cartesian. ''' # Process inputs self.wts = wts @@ -42,6 +44,7 @@ def __init__(self, wts:dict, self.buffer_hr = buffer_hr self.ds_hr = ds_hr self.ds_lr = ds_lr + self.wake_mod = wake_mod # Placeholder variables, to be calculated by FFCaseCreation self.output_frequency_lr = None @@ -81,6 +84,8 @@ def _checkInputs(self): raise ValueError("y-component of prob_lo larger than y-component of prob_hi") if (self.prob_lo[2] >= self.prob_hi[2]): raise ValueError("z-component of prob_lo larger than z-component of prob_hi") + if self.wake_mod not in [1,2,3]: + raise ValueError (f'Wake_mod parameter can only be 1 (polar), 2 (curl), or 3 (cartesian). Received {self.wake_mod}.') def _calc_simple_params(self): ''' @@ -150,7 +155,17 @@ def _calc_sampling_time(self): cmeander_min = min(cmeander_min, self.wts[turbkey]['Cmeander']) Dwake_min = min(Dwake_min, self.wts[turbkey]['D']) # Approximate D_wake as D_rotor - dt_lr_max = cmeander_min * Dwake_min / (10 * self.vhub) + cmax_min = float("inf") + for turbkey in self.wts: + self.cmax_min = min(cmax_min, self.wts[turbkey]['cmax']) + + if self.wake_mod == 1: + dt_lr_max = cmeander_min * Dwake_min / (10 * self.vhub) + else: # wake_mod == 2 or 3 + dr = self.cmax_min + dt_lr_max = dr / (2* self.vhub) + + self.dt_low_les = self.dt_high_les * np.floor(dt_lr_max/self.dt_high_les) # Ensure that dt_lr is a multiple of the high res sampling timestep if self.dt_low_les < self.dt: @@ -174,10 +189,7 @@ def _calc_grid_resolution(self): # ASSUME: FAST.Farm HR zone lies within the region of maxmum AMR-Wind grid refinement # NOTE: ds_hr is calculated independent of any x/y/z requirements, # just blade chord length requirements - cmax_min = float("inf") - for turbkey in self.wts: - cmax_min = min(cmax_min, self.wts[turbkey]['cmax']) - ds_hr_max = cmax_min + ds_hr_max = self.cmax_min if self.ds_hr is None: # Calculate ds_hr if it is not specified as an input if ds_hr_max < self.ds_refine_max: From 14703a58dcd3f5f2fe0bbbd4f006cabd3cc344c9 Mon Sep 17 00:00:00 2001 From: Regis Thedin Date: Fri, 10 Mar 2023 11:18:28 -0700 Subject: [PATCH 063/124] FF: Improvements to getMultiplesOf method --- pyFAST/fastfarm/FASTFarmCaseCreation.py | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/pyFAST/fastfarm/FASTFarmCaseCreation.py b/pyFAST/fastfarm/FASTFarmCaseCreation.py index 63710dc..34e5a23 100644 --- a/pyFAST/fastfarm/FASTFarmCaseCreation.py +++ b/pyFAST/fastfarm/FASTFarmCaseCreation.py @@ -13,6 +13,14 @@ def cosd(t): return np.cos(np.deg2rad(t)) def sind(t): return np.sin(np.deg2rad(t)) +def getMultipleOf(val, multipleof): + ''' + Get integer multiple of a quantity. + The val/multipleof quantity can be within numerical error of an integer + and so additional care must be take + ''' + valmult = int(round(val/multipleof,6))*multipleof + return round(valmult, 4) class FFCaseCreation: @@ -1479,11 +1487,6 @@ def _FF_setup_TS(self): - def _getMultipleOf(self, val, multipleof): - valmult = int(val/multipleof)*multipleof - return round(valmult, 4) - - def _getBoxesParamsForFF(self, lowbts, highbts, dt_low_desired, D, HubHt, xWT, yt): # Get mean wind speeds at the half height location (advection speed) _, meanU_High = highbts.midValues() # !!!!!!!!!!!!!!!! JJ: does it make sense to get both? the meanu for low will be a lot higher than vhub, @@ -1502,8 +1505,8 @@ def _getBoxesParamsForFF(self, lowbts, highbts, dt_low_desired, D, HubHt, xWT, y # ----- Low - dT_Low = self._getMultipleOf(dt_low_desired, multipleof=dT_High) - dX_Low = self._getMultipleOf(meanU_Low*dT_Low, multipleof=dX_High) # dy and dz high are 5. + dT_Low = self.getMultipleOf(dt_low_desired, multipleof=dT_High) + dX_Low = self.getMultipleOf(meanU_Low*dT_Low, multipleof=dX_High) # dy and dz high are 5. dY_Low = lowbts.y[1] - lowbts.y[0] dZ_Low = lowbts.z[1] - lowbts.z[0] @@ -1512,11 +1515,11 @@ def _getBoxesParamsForFF(self, lowbts, highbts, dt_low_desired, D, HubHt, xWT, y LT_Low = np.round(lowbts.t[-1]-lowbts.t[0], 4) X0_Low = np.floor( (min(xWT) - self.extent_low[0]*D ))# - dX_Low)) # # JJ!!!!!!! # removing EB's -dX_Low from the X0_Low specification - X0_Low = self._getMultipleOf(X0_Low, multipleof=dX_Low) + X0_Low = self.getMultipleOf(X0_Low, multipleof=dX_Low) Y0_Low = np.floor( -LY_Low/2 ) # Starting on integer value for aesthetics Z0_Low = lowbts.z[0] # we start at lowest to include tower - XMax_Low = self._getMultipleOf(max(xWT) + self.extent_low[1]*D, multipleof=dX_Low) + XMax_Low = self.getMultipleOf(max(xWT) + self.extent_low[1]*D, multipleof=dX_Low) LX_Low = XMax_Low-X0_Low nX_Low = int(np.ceil(LX_Low/dX_Low)+1) # plus 1 from the guidance From d17e7978fb61a307f1a1b77d160c668e21678a09 Mon Sep 17 00:00:00 2001 From: Regis Thedin Date: Fri, 10 Mar 2023 12:09:54 -0700 Subject: [PATCH 064/124] AMR/FF: use `getMultipleOf` where necessary --- pyFAST/fastfarm/AMRWindSimulation.py | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/pyFAST/fastfarm/AMRWindSimulation.py b/pyFAST/fastfarm/AMRWindSimulation.py index b7486c5..674e4ee 100644 --- a/pyFAST/fastfarm/AMRWindSimulation.py +++ b/pyFAST/fastfarm/AMRWindSimulation.py @@ -1,6 +1,8 @@ import numpy as np import os +from pyFAST.fastfarm.FASTFarmCaseCreation import getMultipleOf + class AMRWindSimulation: ''' This class is used to help prepare sampling planes for an AMR-Wind @@ -143,7 +145,7 @@ def _calc_sampling_time(self): for turbkey in self.wts: fmax_max = max(0, self.wts[turbkey]['fmax']) dt_hr_max = 1 / (2 * fmax_max) - self.dt_high_les = self.dt * np.floor(dt_hr_max/self.dt) # Ensure that dt_hr is a multiple of the AMR-Wind timestep + self.dt_high_les = getMultipleOf(dt_hr_max, multipleof=self.dt) # Ensure dt_hr is a multiple of the AMR-Wind timestep if self.dt_high_les < self.dt: raise ValueError(f"AMR-Wind timestep {self.dt} too coarse for high resolution domain! AMR-Wind timestep must be at least {self.dt_high_les} sec.") @@ -166,7 +168,8 @@ def _calc_sampling_time(self): dt_lr_max = dr / (2* self.vhub) - self.dt_low_les = self.dt_high_les * np.floor(dt_lr_max/self.dt_high_les) # Ensure that dt_lr is a multiple of the high res sampling timestep + self.dt_low_les = getMultipleOf(dt_lr_max, multipleof=self.dt_high_les) # Ensure that dt_lr is a multiple of the high res sampling timestep + if self.dt_low_les < self.dt: raise ValueError(f"AMR-Wind timestep {self.dt} too coarse for low resolution domain! AMR-Wind timestep must be at least {self.dt_low_les} sec.") @@ -175,7 +178,7 @@ def _calc_sampling_time(self): ## Sampling frequency self.output_frequency_hr = int(np.floor(self.dt_high_les/self.dt)) - self.output_frequency_lr = int(self.output_frequency_hr * np.floor(self.dt_low_les/self.dt_high_les)) + self.output_frequency_lr = getMultipleOf(self.dt_low_les/self.dt, multipleof=self.output_frequency_hr) if self.output_frequency_lr % self.output_frequency_hr != 0: raise ValueError(f"Low resolution output frequency of {self.output_frequency_lr} not a multiple of the high resolution frequency {self.output_frequency_hr}!") @@ -196,7 +199,8 @@ def _calc_grid_resolution(self): raise ValueError(f"AMR-Wind grid spacing of {self.ds_refine_max} is too coarse for high resolution domain! The high-resolution domain requires "\ f"AMR-Wind grid spacing to be at least {ds_hr_max} m. If a coarser high-res domain is acceptable, then manually specify the "\ f"high-resolution grid spacing to be at least {self.ds_refine_max} with ds_hr = {self.ds_refine_max}.") - self.ds_high_les = self.ds_refine_max * np.floor(ds_hr_max/self.ds_refine_max) # Ensure that ds_hr is a multiple of the refined AMR-Wind grid spacing + self.ds_high_les = getMultipleOf(ds_hr_max, multipleof=self.ds_refine_max) # Ensure that ds_hr is a multiple of the refined AMR-Wind grid spacing + self.ds_hr = self.ds_high_les else: self.ds_high_les = self.ds_hr @@ -210,7 +214,7 @@ def _calc_grid_resolution(self): # just time step and velocity requiements if self.ds_lr is None: ds_lr_max = self.dt_low_les * self.vhub**2 / 15 - self.ds_low_les = self.ds_hr * np.floor(ds_lr_max/self.ds_hr) # ds_hr is already a multiple of the AMR-Wind grid spacing, so here we need to make sure ds_lr is a multiple of ds_hr + self.ds_low_les = getMultipleOf(ds_lr_max, multipleof=self.ds_hr) # ds_hr is already a multiple of the AMR-Wind grid spacing, so here we need to make sure ds_lr is a multiple of ds_hr self.ds_lr = self.ds_low_les else: self.ds_low_les = self.ds_lr @@ -261,9 +265,9 @@ def _calc_hr_grid_placement(self): # Calculate actual HR domain extent # NOTE: Sampling planes should measure at AMR-Wind cell centers, not cell edges - xlow_hr = self.ds_high_les * np.floor(xlow_hr_min/self.ds_high_les) - 0.5*self.dx_refine + self.prob_lo[0]%self.ds_high_les + xlow_hr = getMultipleOf(xlow_hr_min, multipleof=self.ds_high_les) - 0.5*self.dx_refine + self.prob_lo[0]%self.ds_high_les xhigh_hr = xlow_hr + xdist_hr - ylow_hr = self.ds_high_les * np.floor(ylow_hr_min/self.ds_high_les) - 0.5*self.dy_refine + self.prob_lo[1]%self.ds_high_les + ylow_hr = getMultipleOf(ylow_hr_min, multipleof=self.ds_high_les) - 0.5*self.dy_refine + self.prob_lo[1]%self.ds_high_les yhigh_hr = ylow_hr + ydist_hr zlow_hr = 0.5 * self.dz0 / (2**self.max_level) zhigh_hr = zlow_hr + zdist_hr @@ -340,9 +344,9 @@ def _calc_lr_grid_placement(self): # NOTE: Should we use dx/dy/dz values here or ds_lr? # - AR: I think it's correct to use ds_lr to get to the xlow values, # but then offset by 0.5*amr_dx0 if need be - self.xlow_lr = self.ds_low_les * np.floor(xlow_lr_min/self.ds_low_les) - 0.5*self.dx0 + self.prob_lo[0]%self.ds_low_les + self.xlow_lr = getMultipleOf(xlow_lr_min, multipleof=self.ds_low_les) - 0.5*self.dx0 + self.prob_lo[0]%self.ds_low_les self.xhigh_lr = self.xlow_lr + self.xdist_lr - self.ylow_lr = self.ds_low_les * np.floor(ylow_lr_min/self.ds_low_les) - 0.5*self.dy0 + self.prob_lo[1]%self.ds_low_les + self.ylow_lr = getMultipleOf(ylow_lr_min, multipleof=self.ds_low_les) - 0.5*self.dy0 + self.prob_lo[1]%self.ds_low_les self.yhigh_lr = self.ylow_lr + self.ydist_lr self.zlow_lr = 0.5 * self.dz0 # Lowest z point is half the height of the lowest grid cell self.zhigh_lr = self.zlow_lr + self.zdist_lr From 73611d0c1c1e8d8c28863f69a9fa12932bafeb2f Mon Sep 17 00:00:00 2001 From: Regis Thedin Date: Fri, 10 Mar 2023 14:07:20 -0700 Subject: [PATCH 065/124] AMR/FF: Add `__repr__` --- pyFAST/fastfarm/AMRWindSimulation.py | 44 +++++++++++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/pyFAST/fastfarm/AMRWindSimulation.py b/pyFAST/fastfarm/AMRWindSimulation.py index 674e4ee..0dd9637 100644 --- a/pyFAST/fastfarm/AMRWindSimulation.py +++ b/pyFAST/fastfarm/AMRWindSimulation.py @@ -70,6 +70,48 @@ def __init__(self, wts:dict, self._calc_simple_params() self._calc_sampling_params() + + def __repr__(self): + s = f'<{type(self).__name__} object>\n' + s += f'Requested parameters:\n' + s += f' - Wake model: {self.wake_mod} (1:Polar; 2:Curl; 3:Cartesian)\n' + s += f' - Extent of high-res boxes: {self.extent_high} D to each side\n' + s += f' - Extent of low-res box: xmin={self.extent_low[0]} D, xmax={self.extent_low[1]} D, ymin={self.extent_low[2]} D, ymax={self.extent_low[3]} D, zmax={self.extent_low[4]} D\n' + + s += f'\n' + s += f'LES parameters:\n' + s += f' - velocity hub height: {self.incflo_velocity_hh} m/s\n' + s += f' - ds LES: ({self.dx0}, {self.dy0}, {self.dz0}) m\n' + s += f' - dt LES: {self.dt} s\n' + s += f' - Extents: ({self.prob_hi[0]-self.prob_lo[0]}, {self.prob_hi[1]-self.prob_lo[1]}, {self.prob_hi[2]-self.prob_lo[2]}) m\n' + s += f' - x: {self.prob_lo[0]}:{self.dx0}:{self.prob_hi[0]} m,\t ({self.n_cell[0]} points)\n' + s += f' - y: {self.prob_lo[1]}:{self.dy0}:{self.prob_hi[1]} m,\t ({self.n_cell[1]} points)\n' + s += f' - z: {self.prob_lo[2]}:{self.dz0}:{self.prob_hi[2]} m,\t ({self.n_cell[2]} points)\n' + + s += f'\n' + s += f'Low-res domain: \n' + s += f' - ds low: {self.ds_low_les} m\n' + s += f' - dt low: {self.dt_low_les} s (with LES dt = {self.dt} s, output frequency is {self.output_frequency_lr})\n' + s += f' - Sampling labels: {self.sampling_labels_lr}\n' + s += f' - Extents: ({self.xdist_lr}, {self.ydist_lr}, {self.zdist_lr}) m\n' + s += f' - x: {self.xlow_lr}:{self.ds_low_les}:{self.xhigh_lr} m,\t ({self.nx_lr} points)\n' + s += f' - y: {self.ylow_lr}:{self.ds_low_les}:{self.yhigh_lr} m,\t ({self.ny_lr} points)\n' + s += f' - z: {self.zlow_lr}:{self.ds_low_les}:{self.zhigh_lr} m,\t ({self.nz_lr} points)\n' + + s += f'\n' + s += f'High-res domain: \n' + s += f' - ds high: {self.ds_high_les} m\n' + s += f' - dt high: {self.dt_high_les} s (with LES dt = {self.dt} s, output frequency is {self.output_frequency_hr})\n' + s += f' - Sampling labels: {self.sampling_labels_hr}\n' + for t in np.arange(len(self.hr_domains)): + s += f" - Turbine {t}\n" + s += f" - Extents: ({self.hr_domains[t]['xdist_hr']}, {self.hr_domains[t]['ydist_hr']}, {self.hr_domains[t]['zdist_hr']}) m\n" + s += f" - x: {self.hr_domains[t]['xlow_hr']}:{self.ds_high_les}:{self.hr_domains[t]['xhigh_hr']} m,\t ({self.hr_domains[t]['nx_hr']} points)\n" + s += f" - y: {self.hr_domains[t]['ylow_hr']}:{self.ds_high_les}:{self.hr_domains[t]['yhigh_hr']} m,\t ({self.hr_domains[t]['ny_hr']} points)\n" + s += f" - z: {self.hr_domains[t]['zlow_hr']}:{self.ds_high_les}:{self.hr_domains[t]['zhigh_hr']} m,\t ({self.hr_domains[t]['nz_hr']} points)\n" + return s + + def _checkInputs(self): ''' Check that the AMR-Wind inputs make sense @@ -177,7 +219,7 @@ def _calc_sampling_time(self): raise ValueError(f"Low resolution timestep ({self.dt_low_les}) is finer than high resolution timestep ({self.dt_high_les})!") ## Sampling frequency - self.output_frequency_hr = int(np.floor(self.dt_high_les/self.dt)) + self.output_frequency_hr = int(np.floor(round(self.dt_high_les/self.dt,4))) self.output_frequency_lr = getMultipleOf(self.dt_low_les/self.dt, multipleof=self.output_frequency_hr) if self.output_frequency_lr % self.output_frequency_hr != 0: From b8ed4e43c0838750063cd8697f14f349ec9f3ba4 Mon Sep 17 00:00:00 2001 From: Regis Thedin Date: Tue, 14 Mar 2023 11:53:35 -0600 Subject: [PATCH 066/124] AMR/FF: Add curled wake recommendations; small fixes --- pyFAST/fastfarm/AMRWindSimulation.py | 153 +++++++++++++++------------ 1 file changed, 83 insertions(+), 70 deletions(-) diff --git a/pyFAST/fastfarm/AMRWindSimulation.py b/pyFAST/fastfarm/AMRWindSimulation.py index 0dd9637..e3c5b2c 100644 --- a/pyFAST/fastfarm/AMRWindSimulation.py +++ b/pyFAST/fastfarm/AMRWindSimulation.py @@ -22,7 +22,7 @@ def __init__(self, wts:dict, buffer_lr = [3,6,3,3,2], buffer_hr = 0.6, ds_hr = None, ds_lr = None, - wake_mod = 1): + wake_mod = None): ''' Values from the AMR-Wind input file Inputs: @@ -172,7 +172,7 @@ def _calc_sampling_labels(self): if 'name' in self.wts[turbkey].keys(): wt_name = self.wts[turbkey]['name'] else: - wt_name = f'T{turbkey}' + wt_name = f'T{turbkey+1}' sampling_labels_hr.append(f"High{wt_name}_inflow0deg") self.sampling_labels_hr = sampling_labels_hr @@ -204,10 +204,11 @@ def _calc_sampling_time(self): self.cmax_min = min(cmax_min, self.wts[turbkey]['cmax']) if self.wake_mod == 1: + self.dr = self.cmax_min dt_lr_max = cmeander_min * Dwake_min / (10 * self.vhub) else: # wake_mod == 2 or 3 - dr = self.cmax_min - dt_lr_max = dr / (2* self.vhub) + self.dr = Dwake_min/10 + dt_lr_max = self.dr / (2* self.vhub) self.dt_low_les = getMultipleOf(dt_lr_max, multipleof=self.dt_high_les) # Ensure that dt_lr is a multiple of the high res sampling timestep @@ -330,7 +331,7 @@ def _calc_hr_grid_placement(self): raise ValueError(f"HR domain point {zlow_hr} extends beyond minimum AMR-Wind z-extent!") # Save out info for FFCaseCreation - self.extent_high = self.buffer_hr + self.extent_high = self.buffer_hr*2 hr_turb_info = {'nx_hr': nx_hr, 'ny_hr': ny_hr, 'nz_hr': nz_hr, 'xdist_hr': xdist_hr, 'ydist_hr': ydist_hr, 'zdist_hr': zdist_hr, @@ -474,75 +475,87 @@ def _check_grid_placement(self): if coord not in amr_zgrid_refine_cc: raise ValueError("High resolution z-sampling grid is not cell cenetered with AMR-Wind's grid!") + + def write_sampling_params(self, outdir=None): ''' Write out text that can be used for the sampling planes in an AMR-Wind input file outdir: str - Input file + Input file to be written. If None, result is written to screen ''' - outfile = os.path.join(outdir, 'sampling_config.i') - if not os.path.exists(outdir): - print(f'Path {outdir} does not exist. Creating it') - os.makedirs(outdir) - if os.path.isfile(outfile): - raise FileExistsError(f"{str(outfile)} already exists! Aborting...") - - print(f"Writing to {outfile} ...") - with open(outfile,"w") as out: - # Write high-level info for sampling - sampling_labels_lr_str = " ".join(str(item) for item in self.sampling_labels_lr) - sampling_labels_hr_str = " ".join(str(item) for item in self.sampling_labels_hr) - out.write(f"# Sampling info generated by AMRWindSamplingCreation.py\n") - out.write(f"incflo.post_processing = {self.postproc_name_lr} {self.postproc_name_hr} # averaging\n\n") - out.write(f"{self.postproc_name_lr}.output_format = netcdf\n") - out.write(f"{self.postproc_name_lr}.output_frequency = {self.output_frequency_lr}\n") - out.write(f"{self.postproc_name_lr}.fields = velocity # temperature tke\n") - out.write(f"{self.postproc_name_lr}.labels = {sampling_labels_lr_str}\n\n") - - out.write(f"{self.postproc_name_hr}.output_format = netcdf\n") - out.write(f"{self.postproc_name_hr}.output_frequency = {self.output_frequency_hr}\n") - out.write(f"{self.postproc_name_hr}.fields = velocity # temperature tke\n") - out.write(f"{self.postproc_name_hr}.labels = {sampling_labels_hr_str}\n") - - # Write out low resolution sampling plane info - zoffsets_lr_str = " ".join(str(item) for item in self.zoffsets_lr) - - out.write(f"\n# Low sampling grid spacing = {self.ds_lr} m\n") - out.write(f"{self.postproc_name_lr}.Low.type = PlaneSampler\n") - out.write(f"{self.postproc_name_lr}.Low.num_points = {self.nx_lr} {self.ny_lr}\n") - out.write(f"{self.postproc_name_lr}.Low.origin = {self.xlow_lr:.4f} {self.ylow_lr:.4f} {self.zlow_lr:.4f}\n") # Round the float output - out.write(f"{self.postproc_name_lr}.Low.axis1 = {self.xdist_lr:.4f} 0.0 0.0\n") # Assume: axis1 oriented parallel to AMR-Wind x-axis - out.write(f"{self.postproc_name_lr}.Low.axis2 = 0.0 {self.ydist_lr:.4f} 0.0\n") # Assume: axis2 oriented parallel to AMR-Wind y-axis - out.write(f"{self.postproc_name_lr}.Low.normal = 0.0 0.0 1.0\n") - out.write(f"{self.postproc_name_lr}.Low.offsets = {zoffsets_lr_str}\n") - - # Write out high resolution sampling plane info - for turbkey in self.hr_domains: - wt_x = self.wts[turbkey]['x'] - wt_y = self.wts[turbkey]['y'] - wt_D = self.wts[turbkey]['D'] - if 'name' in self.wts[turbkey].keys(): - wt_name = self.wts[turbkey]['name'] - else: - wt_name = f'T{turbkey}' - sampling_name = f"High{wt_name}_inflow0deg" - nx_hr = self.hr_domains[turbkey]['nx_hr'] - ny_hr = self.hr_domains[turbkey]['ny_hr'] - xlow_hr = self.hr_domains[turbkey]['xlow_hr'] - ylow_hr = self.hr_domains[turbkey]['ylow_hr'] - zlow_hr = self.hr_domains[turbkey]['zlow_hr'] - xdist_hr = self.hr_domains[turbkey]['xdist_hr'] - ydist_hr = self.hr_domains[turbkey]['ydist_hr'] - zoffsets_hr = self.hr_domains[turbkey]['zoffsets_hr'] - zoffsets_hr_str = " ".join(str(item) for item in zoffsets_hr) - - out.write(f"\n# Turbine {wt_name} at (x,y) = ({wt_x}, {wt_y}), with D = {wt_D}, grid spacing = {self.ds_hr} m\n") - out.write(f"{self.postproc_name_hr}.{sampling_name}.type = PlaneSampler\n") - out.write(f"{self.postproc_name_hr}.{sampling_name}.num_points = {nx_hr} {ny_hr}\n") - out.write(f"{self.postproc_name_hr}.{sampling_name}.origin = {xlow_hr:.4f} {ylow_hr:.4f} {zlow_hr:.4f}\n") # Round the float output - out.write(f"{self.postproc_name_hr}.{sampling_name}.axis1 = {xdist_hr:.4f} 0.0 0.0\n") # Assume: axis1 oriented parallel to AMR-Wind x-axis - out.write(f"{self.postproc_name_hr}.{sampling_name}.axis2 = 0.0 {ydist_hr:.4f} 0.0\n") # Assume: axis2 oriented parallel to AMR-Wind y-axis - out.write(f"{self.postproc_name_hr}.{sampling_name}.normal = 0.0 0.0 1.0\n") - out.write(f"{self.postproc_name_hr}.{sampling_name}.offsets = {zoffsets_hr_str}\n") + + # Write high-level info for sampling + sampling_labels_lr_str = " ".join(str(item) for item in self.sampling_labels_lr) + sampling_labels_hr_str = " ".join(str(item) for item in self.sampling_labels_hr) + s = f"# Sampling info generated by AMRWindSamplingCreation.py\n" + s += f"incflo.post_processing = {self.postproc_name_lr} {self.postproc_name_hr} # averaging\n\n" + s += f"{self.postproc_name_lr}.output_format = netcdf\n" + s += f"{self.postproc_name_lr}.output_frequency = {self.output_frequency_lr}\n" + s += f"{self.postproc_name_lr}.fields = velocity # temperature tke\n" + s += f"{self.postproc_name_lr}.labels = {sampling_labels_lr_str}\n\n" + + s += f"{self.postproc_name_hr}.output_format = netcdf\n" + s += f"{self.postproc_name_hr}.output_frequency = {self.output_frequency_hr}\n" + s += f"{self.postproc_name_hr}.fields = velocity # temperature tke\n" + s += f"{self.postproc_name_hr}.labels = {sampling_labels_hr_str}\n" + + # Write out low resolution sampling plane info + zoffsets_lr_str = " ".join(str(item) for item in self.zoffsets_lr) + + s += f"\n# Low sampling grid spacing = {self.ds_lr} m\n" + s += f"{self.postproc_name_lr}.Low.type = PlaneSampler\n" + s += f"{self.postproc_name_lr}.Low.num_points = {self.nx_lr} {self.ny_lr}\n" + s += f"{self.postproc_name_lr}.Low.origin = {self.xlow_lr:.4f} {self.ylow_lr:.4f} {self.zlow_lr:.4f}\n" # Round the float output + s += f"{self.postproc_name_lr}.Low.axis1 = {self.xdist_lr:.4f} 0.0 0.0\n" # Assume: axis1 oriented parallel to AMR-Wind x-axis + s += f"{self.postproc_name_lr}.Low.axis2 = 0.0 {self.ydist_lr:.4f} 0.0\n" # Assume: axis2 oriented parallel to AMR-Wind y-axis + s += f"{self.postproc_name_lr}.Low.normal = 0.0 0.0 1.0\n" + s += f"{self.postproc_name_lr}.Low.offsets = {zoffsets_lr_str}\n" + + # Write out high resolution sampling plane info + for turbkey in self.hr_domains: + wt_x = self.wts[turbkey]['x'] + wt_y = self.wts[turbkey]['y'] + wt_D = self.wts[turbkey]['D'] + if 'name' in self.wts[turbkey].keys(): + wt_name = self.wts[turbkey]['name'] + else: + wt_name = f'T{turbkey+1}' + sampling_name = f"High{wt_name}_inflow0deg" + nx_hr = self.hr_domains[turbkey]['nx_hr'] + ny_hr = self.hr_domains[turbkey]['ny_hr'] + xlow_hr = self.hr_domains[turbkey]['xlow_hr'] + ylow_hr = self.hr_domains[turbkey]['ylow_hr'] + zlow_hr = self.hr_domains[turbkey]['zlow_hr'] + xdist_hr = self.hr_domains[turbkey]['xdist_hr'] + ydist_hr = self.hr_domains[turbkey]['ydist_hr'] + zoffsets_hr = self.hr_domains[turbkey]['zoffsets_hr'] + zoffsets_hr_str = " ".join(str(item) for item in zoffsets_hr) + + s += f"\n# Turbine {wt_name} at (x,y) = ({wt_x}, {wt_y}), with D = {wt_D}, grid spacing = {self.ds_hr} m\n" + s += f"{self.postproc_name_hr}.{sampling_name}.type = PlaneSampler\n" + s += f"{self.postproc_name_hr}.{sampling_name}.num_points = {nx_hr} {ny_hr}\n" + s += f"{self.postproc_name_hr}.{sampling_name}.origin = {xlow_hr:.4f} {ylow_hr:.4f} {zlow_hr:.4f}\n" # Round the float output + s += f"{self.postproc_name_hr}.{sampling_name}.axis1 = {xdist_hr:.4f} 0.0 0.0\n" # Assume: axis1 oriented parallel to AMR-Wind x-axis + s += f"{self.postproc_name_hr}.{sampling_name}.axis2 = 0.0 {ydist_hr:.4f} 0.0\n" # Assume: axis2 oriented parallel to AMR-Wind y-axis + s += f"{self.postproc_name_hr}.{sampling_name}.normal = 0.0 0.0 1.0\n" + s += f"{self.postproc_name_hr}.{sampling_name}.offsets = {zoffsets_hr_str}\n" + + + if outdir is None: + print(s) + else: + outfile = os.path.join(outdir, 'sampling_config.i') + if not os.path.exists(outdir): + print(f'Path {outdir} does not exist. Creating it') + os.makedirs(outdir) + if os.path.isfile(outfile): + raise FileExistsError(f"{str(outfile)} already exists! Aborting...") + + with open(outfile,"w") as out: + out.write(s) + + + + From e609c9ec2ff243c4bd367776a17cb5226acd3e41 Mon Sep 17 00:00:00 2001 From: Regis Thedin Date: Tue, 14 Mar 2023 11:56:57 -0600 Subject: [PATCH 067/124] FF: Allow specification of one LES path per case Note: backward compatible --- pyFAST/fastfarm/FASTFarmCaseCreation.py | 57 ++++++++++++++----------- 1 file changed, 33 insertions(+), 24 deletions(-) diff --git a/pyFAST/fastfarm/FASTFarmCaseCreation.py b/pyFAST/fastfarm/FASTFarmCaseCreation.py index 34e5a23..066638b 100644 --- a/pyFAST/fastfarm/FASTFarmCaseCreation.py +++ b/pyFAST/fastfarm/FASTFarmCaseCreation.py @@ -12,7 +12,6 @@ def cosd(t): return np.cos(np.deg2rad(t)) def sind(t): return np.sin(np.deg2rad(t)) - def getMultipleOf(val, multipleof): ''' Get integer multiple of a quantity. @@ -22,12 +21,17 @@ def getMultipleOf(val, multipleof): valmult = int(round(val/multipleof,6))*multipleof return round(valmult, 4) -class FFCaseCreation: +class FFCaseCreation: def __init__(self, path, wts, cmax, fmax, Cmeander, tmax, zbot, vhub, shear, TIvalue, inflow_deg, dt_high_les, ds_high_les, extent_high, dt_low_les, ds_low_les, extent_low, ffbin, yaw_init=None, ADmodel=None, EDmodel=None, nSeeds=6, LESpath=None, sweepWakeSteering=False, sweepYawMisalignment=False, seedValues=None, refTurb_rot=0, verbose=0): ''' + Full setup of a FAST.Farm simulations, can create setups for LES- or TurbSim-driven scenarios. + LESpath: str or list of strings + Full path of the LES data, if driven by LES. If None, the setup will be for TurbSim inflow. + LESpath can be a single path, or a list of paths of the same length as the sweep in conditions. + For example, if TIvalue=[8,10,12], then LESpath can be 3 paths, related to each condition. ffbin: str Full path of the FAST.Farm binary to be executed refTurb_rot: int @@ -194,9 +198,11 @@ def _checkInputs(self): if self.LESpath is None: self.inflowStr = 'TurbSim' else: - if not os.path.isdir(self.LESpath): - raise ValueError (f'The path {self.LESpath} does not exist') + if isinstance(self.LESpath,str): self.LESpath = [self.LESpath]*len(self.vhub) self.inflowStr = 'LES' + for p in self.LESpath: + if not os.path.isdir(p): + raise ValueError (f'The path {p} does not exist') # Check the reference turbine for rotation if self.refTurb_rot >= self.nTurbines: @@ -421,7 +427,7 @@ def modifyProperty(self, fullfilename, entry, value): # Change the actual value f[entry] = value # Save the new file - f.write(fullfname) + f.write(fullfilename) return @@ -787,21 +793,21 @@ def _setRotorParameters(self): if self.D == 220: # 12 MW turbine self.bins = xr.Dataset({'WaveHs': (['wspd'], [ 1.429, 1.429]), # 1.429 comes from Matt's hydrodyn input file - 'WaveTp': (['wspd'], [ 7.073, 7.073]), # 7.073 comes from Matt's hydrodyn input file - 'RotSpeed': (['wspd'], [ 4.0, 4.0]), # 4 rpm comes from Matt's ED input file - 'BlPitch': (['wspd'], [ 0.0, 0.0]), # 0 deg comes from Matt's ED input file - #'WvHiCOffD': (['wspd'], [0, 0]), # 2nd order wave info. Unused for now - #'WvLowCOffS': (['wspd'], [0, 0]), # 2nd order wave info. Unused for now - }, coords={'wspd': [10, 15]} ) # 15 m/s is 'else', since method='nearest' is used on the variable `bins` + 'WaveTp': (['wspd'], [ 7.073, 7.073]), # 7.073 comes from Matt's hydrodyn input file + 'RotSpeed': (['wspd'], [ 4.0, 4.0]), # 4 rpm comes from Matt's ED input file + 'BlPitch': (['wspd'], [ 0.0, 0.0]), # 0 deg comes from Matt's ED input file + #'WvHiCOffD': (['wspd'], [0, 0]), # 2nd order wave info. Unused for now + #'WvLowCOffS': (['wspd'], [0, 0]), # 2nd order wave info. Unused for now + }, coords={'wspd': [10, 15]} ) # 15 m/s is 'else', since method='nearest' is used on the variable `bins` - elif self.D == 250: # IEA 15 MW + elif self.D == 240: # IEA 15 MW self.bins = xr.Dataset({'WaveHs': (['wspd'], [1.172, 1.323, 1.523, 1.764, 2.255]), # higher values on default input from the repository (4.52) - 'WaveTp': (['wspd'], [7.287, 6.963, 7.115, 6.959, 7.067]), # higher values on default input from the repository (9.45) - 'RotSpeed': (['wspd'], [4.995, 6.087, 7.557, 7.557, 7.557]), - 'BlPitch': (['wspd'], [0.315, 0, 0.645, 7.6, 13.8 ]), - #'WvHiCOffD': (['wspd'], [0, 0, 0, 0, 0 ]), # 2nd order wave info. Unused for now. 3.04292 from repo; 0.862 from KS - #'WvLowCOffS': (['wspd'], [0, 0, 0, 0, 0 ]), # 2nd order wave info. Unused for now 0.314159 from repo; 0.862 from KS - }, coords={'wspd': [6.6, 8.6, 10.6, 12.6, 15]} ) # 15 m/s is 'else', since method='nearest' is used on the variable `bins` + 'WaveTp': (['wspd'], [7.287, 6.963, 7.115, 6.959, 7.067]), # higher values on default input from the repository (9.45) + 'RotSpeed': (['wspd'], [4.995, 6.087, 7.557, 7.557, 7.557]), + 'BlPitch': (['wspd'], [0.315, 0, 0.645, 7.6, 13.8 ]), + #'WvHiCOffD': (['wspd'], [0, 0, 0, 0, 0 ]), # 2nd order wave info. Unused for now. 3.04292 from repo; 0.862 from KS + #'WvLowCOffS': (['wspd'], [0, 0, 0, 0, 0 ]), # 2nd order wave info. Unused for now 0.314159 from repo; 0.862 from KS + }, coords={'wspd': [6.6, 8.6, 10.6, 12.6, 15]} ) # 15 m/s is 'else', since method='nearest' is used on the variable `bins` else: raise ValueError(f'Unknown turbine with diameter {self.D}. Add values to the `_setRotorParameters` function.') @@ -939,7 +945,7 @@ def TS_low_slurm_submit(self): # ----- Run turbSim Low boxes ----- # --------------------------------- # Submit the script to SLURM - _ = subprocess.call('sbatch {self.slurmfilename_low}', cwd=self.path, shell=True) + _ = subprocess.call(f'sbatch {self.slurmfilename_low}', cwd=self.path, shell=True) def TS_low_createSymlinks(self): @@ -997,7 +1003,7 @@ def TS_high_get_time_series(self): boxType='highres' for cond in range(self.nConditions): for seed in range(self.nSeeds): - condSeedPath = os.path.join(path, self.condDirList[cond], f'Seed_{seed}') + condSeedPath = os.path.join(self.path, self.condDirList[cond], f'Seed_{seed}') # Read output .bts for current seed bts = TurbSimFile(os.path.join(condSeedPath, 'Low.bts')) @@ -1010,7 +1016,7 @@ def TS_high_get_time_series(self): caseSeedPath = os.path.join(self.path, self.condDirList[cond], self.caseDirList[case], f'Seed_{seed}', 'TurbSim') - for t in range(nTurbines): + for t in range(self.nTurbines): # Recover turbine properties of the current case HubHt_ = self.allCases.sel(case=case, turbine=t)['zhub'].values xloc_ = self.allCases.sel(case=case, turbine=t)['Tx' ].values @@ -1074,6 +1080,9 @@ def TS_high_setup(self, writeFiles=True): # Create symbolic links for the low-res boxes self.TS_low_createSymlinks() + # Open low-res boxes and extract time-series at turbine locations + self.TS_high_get_time_series() + # Loop on all conditions/cases/seeds setting up the High boxes boxType='highres' for cond in range(self.nConditions): @@ -1166,7 +1175,7 @@ def TS_high_slurm_submit(self): # ----- Run turbSim High boxes ----- # ---------------------------------- # Submit the script to SLURM - _ = subprocess.call('sbatch {self.slurmfilename_high}', cwd=self.path, shell=True) + _ = subprocess.call(f'sbatch {self.slurmfilename_high}', cwd=self.path, shell=True) def TS_high_create_symlink(self): @@ -1316,7 +1325,7 @@ def _FF_setup_LES(self, seedsToKeep=1): # Low-res box try: - src = os.path.join(self.LESpath, 'Low') + src = os.path.join(self.LESpath[cond], 'Low') dst = os.path.join(self.path, self.condDirList[cond], self.caseDirList[case], f'Seed_{seed}', LESboxesDirName, 'Low') os.symlink(src, dst) except FileExistsError: @@ -1325,7 +1334,7 @@ def _FF_setup_LES(self, seedsToKeep=1): # High-res boxes for t in range(self.nTurbines): try: - src = os.path.join(self.LESpath, f"HighT{t+1}_inflow{str(self.allCases.sel(case=case).inflow_deg.values).replace('-','m')}deg") + src = os.path.join(self.LESpath[cond], f"HighT{t+1}_inflow{str(self.allCases.sel(case=case).inflow_deg.values).replace('-','m')}deg") dst = os.path.join(self.path,self.condDirList[cond], self.caseDirList[case], f'Seed_{seed}', LESboxesDirName, f'HighT{t+1}') os.symlink(src, dst) except FileExistsError: From b3f6bb82f0252b09f0f77baf243712f47016c69d Mon Sep 17 00:00:00 2001 From: Regis Thedin Date: Wed, 15 Mar 2023 09:55:00 -0600 Subject: [PATCH 068/124] AMR/FF: Add option to specify low-res dt as input --- pyFAST/fastfarm/AMRWindSimulation.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/pyFAST/fastfarm/AMRWindSimulation.py b/pyFAST/fastfarm/AMRWindSimulation.py index e3c5b2c..7745140 100644 --- a/pyFAST/fastfarm/AMRWindSimulation.py +++ b/pyFAST/fastfarm/AMRWindSimulation.py @@ -22,6 +22,7 @@ def __init__(self, wts:dict, buffer_lr = [3,6,3,3,2], buffer_hr = 0.6, ds_hr = None, ds_lr = None, + dt_hr = None, dt_lr = None, wake_mod = None): ''' Values from the AMR-Wind input file @@ -46,6 +47,8 @@ def __init__(self, wts:dict, self.buffer_hr = buffer_hr self.ds_hr = ds_hr self.ds_lr = ds_lr + self.dt_hr = dt_hr + self.dt_lr = dt_lr self.wake_mod = wake_mod # Placeholder variables, to be calculated by FFCaseCreation @@ -186,8 +189,13 @@ def _calc_sampling_time(self): fmax_max = 0 for turbkey in self.wts: fmax_max = max(0, self.wts[turbkey]['fmax']) - dt_hr_max = 1 / (2 * fmax_max) - self.dt_high_les = getMultipleOf(dt_hr_max, multipleof=self.dt) # Ensure dt_hr is a multiple of the AMR-Wind timestep + if self.dt_hr is None: + # Calculate dt of high-res per guidelines + dt_hr_max = 1 / (2 * fmax_max) + self.dt_high_les = getMultipleOf(dt_hr_max, multipleof=self.dt) # Ensure dt_hr is a multiple of the AMR-Wind timestep + else: + # dt of high-res is given + self.dt_high_les = self.dt_hr if self.dt_high_les < self.dt: raise ValueError(f"AMR-Wind timestep {self.dt} too coarse for high resolution domain! AMR-Wind timestep must be at least {self.dt_high_les} sec.") @@ -211,7 +219,12 @@ def _calc_sampling_time(self): dt_lr_max = self.dr / (2* self.vhub) - self.dt_low_les = getMultipleOf(dt_lr_max, multipleof=self.dt_high_les) # Ensure that dt_lr is a multiple of the high res sampling timestep + if self.dt_lr is None: + # Calculate dt of low-res per guidelines + self.dt_low_les = getMultipleOf(dt_lr_max, multipleof=self.dt_high_les) # Ensure that dt_lr is a multiple of the high res sampling timestep + else: + # dt of low-res is given + self.dt_low_les = self.dt_lr if self.dt_low_les < self.dt: From 4ed2a24f9749847d2a26421561b2f96f6ba1a04b Mon Sep 17 00:00:00 2001 From: Regis Thedin Date: Wed, 15 Mar 2023 09:55:56 -0600 Subject: [PATCH 069/124] FF: Add mod_wake option and curled wake parameters --- pyFAST/fastfarm/FASTFarmCaseCreation.py | 41 +++++++++++++++++-------- 1 file changed, 28 insertions(+), 13 deletions(-) diff --git a/pyFAST/fastfarm/FASTFarmCaseCreation.py b/pyFAST/fastfarm/FASTFarmCaseCreation.py index 066638b..3b547db 100644 --- a/pyFAST/fastfarm/FASTFarmCaseCreation.py +++ b/pyFAST/fastfarm/FASTFarmCaseCreation.py @@ -24,7 +24,7 @@ def getMultipleOf(val, multipleof): class FFCaseCreation: - def __init__(self, path, wts, cmax, fmax, Cmeander, tmax, zbot, vhub, shear, TIvalue, inflow_deg, dt_high_les, ds_high_les, extent_high, dt_low_les, ds_low_les, extent_low, ffbin, yaw_init=None, ADmodel=None, EDmodel=None, nSeeds=6, LESpath=None, sweepWakeSteering=False, sweepYawMisalignment=False, seedValues=None, refTurb_rot=0, verbose=0): + def __init__(self, path, wts, cmax, fmax, Cmeander, tmax, zbot, vhub, shear, TIvalue, inflow_deg, dt_high_les, ds_high_les, extent_high, dt_low_les, ds_low_les, extent_low, ffbin, mod_wake=1, yaw_init=None, ADmodel=None, EDmodel=None, nSeeds=6, LESpath=None, sweepWakeSteering=False, sweepYawMisalignment=False, seedValues=None, refTurb_rot=0, verbose=0): ''' Full setup of a FAST.Farm simulations, can create setups for LES- or TurbSim-driven scenarios. @@ -56,6 +56,7 @@ def __init__(self, path, wts, cmax, fmax, Cmeander, tmax, zbot, vhub, shear, TIv self.extent_low = extent_low self.extent_high = extent_high self.ffbin = ffbin + self.mod_wake = mod_wake self.yaw_init = yaw_init self.ADmodel = ADmodel self.EDmodel = EDmodel @@ -153,7 +154,7 @@ def _checkInputs(self): # Check the ds and dt for the high- and low-res boxes if not (np.array(self.extent_low)>=0).all(): raise ValueError(f'The array for low-res box extents should be given with positive values') - if self.dt_low_les%(self.dt_high_les-1e-15) > 1e-14: + if self.dt_low_les%(self.dt_high_les-1e-15) > 1e-12: raise ValueError(f'The temporal resolution dT_Low should be a multiple of dT_High') if self.dt_low_les < self.dt_high_les: raise ValueError(f'The temporal resolution dT_High should not be greater than dT_Low on the LES side') @@ -208,6 +209,10 @@ def _checkInputs(self): if self.refTurb_rot >= self.nTurbines: raise ValueError(f'The index for the reference turbine for the farm to be rotated around is greater than the number of turbines') + # Check the wake model (1:Polar; 2:Curl; 3:Cartesian) + if self.mod_wake not in [1,2,3]: + raise ValueError(f'Wake model `mod_wake` should be 1 (Polar), 2 (Curl), or 3 (Cartesian). Received {self.mod_wake}.') + # Set aux variable self.templateFilesCreatedBool = False self.TSlowBoxFilesCreatedBool = False @@ -1033,7 +1038,7 @@ def TS_high_get_time_series(self): # Get indices of the half height position (TurbSim's hub height) jMid, kMid = bts.iMid - # Get time series at the box center to get mean vhub and create time array. JJ: get at the Turbsim hub height + # Get time series at the box center to get mean vhub and create time array. #Vhub = bts['u'][0,:,jTurb,kTurb] Vmid = bts['u'][0,:,jMid,kMid] time = bts.t @@ -1234,9 +1239,9 @@ def FF_setup(self, outlistFF=None, **kwargs): # "WkAxsXT1D1 , WkAxsXT1D2 , WkAxsXT1D3 , WkAxsXT1D4 , WkAxsXT1D5 , WkAxsXT1D6 , WkAxsXT1D7", # "WkAxsYT1D1 , WkAxsYT1D2 , WkAxsYT1D3 , WkAxsYT1D4 , WkAxsYT1D5 , WkAxsYT1D6 , WkAxsYT1D7", # "WkAxsZT1D1 , WkAxsZT1D2 , WkAxsZT1D3 , WkAxsZT1D4 , WkAxsZT1D5 , WkAxsZT1D6 , WkAxsZT1D7", - # "WkPosXT1D1 , WkPosXT1D2 , WkPosXT1D3 , WkPosXT1D4 , WkPosXT1D5 , WkPosXT1D6 , WkPosXT1D7", - # "WkPosYT1D1 , WkPosYT1D2 , WkPosYT1D3 , WkPosYT1D4 , WkPosYT1D5 , WkPosYT1D6 , WkPosYT1D7", - # "WkPosZT1D1 , WkPosZT1D2 , WkPosZT1D3 , WkPosZT1D4 , WkPosZT1D5 , WkPosZT1D6 , WkPosZT1D7", + "WkPosXT1D1 , WkPosXT1D2 , WkPosXT1D3 , WkPosXT1D4 , WkPosXT1D5 , WkPosXT1D6 , WkPosXT1D7 , WkPosXT1D8 , WkPosXT1D9", + "WkPosYT1D1 , WkPosYT1D2 , WkPosYT1D3 , WkPosYT1D4 , WkPosYT1D5 , WkPosYT1D6 , WkPosYT1D7 , WkPosYT1D8 , WkPosYT1D9", + "WkPosZT1D1 , WkPosZT1D2 , WkPosZT1D3 , WkPosZT1D4 , WkPosZT1D5 , WkPosZT1D6 , WkPosZT1D7 , WkPosZT1D8 , WkPosZT1D9", # "WkDfVxT1N01D1, WkDfVxT1N02D1, WkDfVxT1N03D1, WkDfVxT1N04D1, WkDfVxT1N05D1, WkDfVxT1N06D1, WkDfVxT1N07D1, WkDfVxT1N08D1, WkDfVxT1N09D1, WkDfVxT1N10D1, WkDfVxT1N11D1, WkDfVxT1N12D1, WkDfVxT1N13D1, WkDfVxT1N14D1, WkDfVxT1N15D1, WkDfVxT1N16D1, WkDfVxT1N17D1, WkDfVxT1N18D1, WkDfVxT1N19D1, WkDfVxT1N20D1", # "WkDfVxT1N01D2, WkDfVxT1N02D2, WkDfVxT1N03D2, WkDfVxT1N04D2, WkDfVxT1N05D2, WkDfVxT1N06D2, WkDfVxT1N07D2, WkDfVxT1N08D2, WkDfVxT1N09D2, WkDfVxT1N10D2, WkDfVxT1N11D2, WkDfVxT1N12D2, WkDfVxT1N13D2, WkDfVxT1N14D2, WkDfVxT1N15D2, WkDfVxT1N16D2, WkDfVxT1N17D2, WkDfVxT1N18D2, WkDfVxT1N19D2, WkDfVxT1N20D2", # "WkDfVxT1N01D3, WkDfVxT1N02D3, WkDfVxT1N03D3, WkDfVxT1N04D3, WkDfVxT1N05D3, WkDfVxT1N06D3, WkDfVxT1N07D3, WkDfVxT1N08D3, WkDfVxT1N09D3, WkDfVxT1N10D3, WkDfVxT1N11D3, WkDfVxT1N12D3, WkDfVxT1N13D3, WkDfVxT1N14D3, WkDfVxT1N15D3, WkDfVxT1N16D3, WkDfVxT1N17D3, WkDfVxT1N18D3, WkDfVxT1N19D3, WkDfVxT1N20D3", @@ -1385,8 +1390,13 @@ def _FF_setup_LES(self, seedsToKeep=1): ff_file['SC_FileName'] = '/path/to/SC_DLL.dll' # Wake dynamics - ff_file['dr'] = self.cmax - ff_file['NumRadii'] = int(np.ceil(3*D_/(2*self.cmax) + 1)) + ff_file['Mod_Wake'] = self.mod_wake + if self.mod_wake == 1: # Polar model + self.dr = self.cmax + else: # Curled; Cartesian + self.dr = round(self.D/10) + ff_file['dr'] = self.dr + ff_file['NumRadii'] = int(np.ceil(3*D_/(2*self.dr) + 1)) ff_file['NumPlanes'] = int(np.ceil( 20*D_/(self.dt_low_les*Vhub_*(1-1/6)) ) ) # Vizualization outputs @@ -1457,7 +1467,7 @@ def _FF_setup_TS(self): # Open saved file and change additional values manually or make sure we have the correct ones ff_file = FASTInputFile(outputFSTF) - ff_file['InflowFile'] = f'"../{self.IWfilename}"' #!!!!!!!! this path is not filled. should it be? + ff_file['InflowFile'] = f'"../{self.IWfilename}"' #ff_file['DT']=1.0 ff_file['Mod_AmbWind'] = 3 # 1: LES boxes; 2: single TurbSim; 3: multiple TurbSim ff_file['TMax'] = self.tmax @@ -1466,9 +1476,14 @@ def _FF_setup_TS(self): ff_file['UseSC'] = False ff_file['SC_FileName'] = '/path/to/SC_DLL.dll' - # Wake dynamics # !!!!!!!!!!!!!!!!!!!!!! are these values good? (Emmanuel's sample file had 3, 50, 80. KS had 5, 75, 80) - ff_file['dr'] = self.cmax - ff_file['NumRadii'] = int(np.ceil(3*D_/(2*self.cmax) + 1)) + # Wake dynamics + ff_file['Mod_Wake'] = self.mod_wake + if self.mod_wake == 1: # Polar model + self.dr = self.cmax + else: # Curled; Cartesian + self.dr = round(self.D/10) + ff_file['dr'] = self.dr + ff_file['NumRadii'] = int(np.ceil(3*D_/(2*self.dr) + 1)) ff_file['NumPlanes'] = int(np.ceil( 20*D_/(dt_low_desired*Vhub_*(1-1/6)) ) ) # Vizualization outputs @@ -1484,7 +1499,7 @@ def _FF_setup_TS(self): # Modify wake outputs ff_file['NOutDist'] = 7 ff_file['OutDist'] = ' '.join(map(str, [1,1.5,2,2.5,3,3.5,4]*D_)) - # Mofidy wind output # !!!!! JJ why only 9? + # Mofidy wind output ff_file['NWindVel'] = 9 ff_file['WindVelX'] = ' '.join(map(str, xWT[:9])) ff_file['WindVelY'] = ' '.join(map(str, yWT[:9])) From e11f24f03bf7ccc13b66264aae6170556f07ddbf Mon Sep 17 00:00:00 2001 From: Regis Thedin Date: Wed, 15 Mar 2023 10:59:44 -0600 Subject: [PATCH 070/124] FF: replace cp and rm using `os.popen` with `shutil` --- pyFAST/fastfarm/FASTFarmCaseCreation.py | 34 +++++++++++++------------ 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/pyFAST/fastfarm/FASTFarmCaseCreation.py b/pyFAST/fastfarm/FASTFarmCaseCreation.py index 3b547db..47dc4ae 100644 --- a/pyFAST/fastfarm/FASTFarmCaseCreation.py +++ b/pyFAST/fastfarm/FASTFarmCaseCreation.py @@ -4,7 +4,6 @@ import subprocess import numpy as np import xarray as xr -import matplotlib.pyplot as plt from pyFAST.input_output import FASTInputFile, FASTOutputFile, TurbSimFile, VTKFile from pyFAST.fastfarm import writeFastFarm, fastFarmTurbSimExtent, plotFastFarmSetup @@ -309,7 +308,9 @@ def copyTurbineFilesForEachCase(self, writeFiles=True): # Write updated DISCON and *Dyn files. if writeFiles: self.HydroDynFile.write(os.path.join(currPath, self.HDfilename)) - status = os.popen(f'cp {self.templatePath}/{self.controllerInputfilename} {currPath}/{self.controllerInputfilename}') + shutil.copy2(os.path.join(self.templatePath,self.controllerInputfilename), os.path.join(currPath,self.controllerInputfilename)) + + # Depending on the controller, it might need to be in the same level as the fstf input file. The ideal solution would be give the full # path to the controller input, but we have no control over the compilation process and it is likely that a very long string with the full @@ -351,9 +352,9 @@ def copyTurbineFilesForEachCase(self, writeFiles=True): # Update each turbine's ElastoDyn self.ElastoDynFile['NacYaw'] = yaw_deg_ + yaw_mis_deg_ # check if this change of EDfile to a variable works as it should with quotes and stuff - status = os.popen(f'cp {self.templatePath}/{self.bladefilename} {currPath}/{self.bladefilename}') + shutil.copy2(os.path.join(self.templatePath,self.bladefilename), os.path.join(currPath,self.bladefilename)) self.ElastoDynFile['BldFile1'] = self.ElastoDynFile['BldFile2'] = self.ElastoDynFile['BldFile3'] = f'"{self.bladefilename}"' - status = os.popen(f'cp {self.templatePath}/{self.towerfilename} {currPath}/{self.towerfilename}') + shutil.copy2(os.path.join(self.templatePath,self.towerfilename), os.path.join(currPath,self.towerfilename)) self.ElastoDynFile['TwrFile'] = f'"{self.towerfilename}"' self.ElastoDynFile['Azimuth'] = round(np.random.uniform(low=0, high=360)) # start at a random value if writeFiles: @@ -400,7 +401,7 @@ def copyTurbineFilesForEachCase(self, writeFiles=True): self.turbineFile['CompAero'] = 3 # 1: AeroDyn v14; 2: AeroDyn v15; 3: AeroDisk self.turbineFile['AeroFile'] = f'"{self.ADskfilepath}"' if writeFiles: - status = os.popen(f'cp {self.coeffTablefilepath} {os.path.join(currPath,self.coeffTablefilename)}') + shutil.copy2(self.coeffTablefilepath, os.path.join(currPath,self.coeffTablefilename)) self.turbineFile['ServoFile'] = f'"./{self.SrvDfilename}{t+1}_mod.dat"' self.turbineFile['HydroFile'] = f'"./{self.HDfilename}"' self.turbineFile['SubFile'] = f'"{self.SubDfilepath}"' @@ -616,7 +617,7 @@ def _create_copy_libdiscon(self): self.DLLfilepath = os.path.join(os.path.dirname(self.libdisconfilepath), f'{libdisconfilename}.T') if not os.path.isfile(currLibdiscon): if self.verbose>0: print(f' Creating a copy of the controller {libdisconfilename}.so in {currLibdiscon}') - status = os.popen(f'cp {self.libdisconfilepath} {currLibdiscon}') + shutil.copy2(self.libdisconfilepath, currLibdiscon) copied=True if copied == False and self.verbose>0: @@ -890,7 +891,7 @@ def TS_low_slurm_prepare(self, slurmfilepath): # for seed in range(self.nSeeds): # # fname = f'runFASTFarm_cond{cond}_case{case}_seed{seed}.sh' - # status = os.popen(f'cp {slurmfilepath} {os.path.join(self.path,fname)}') + # shutil.copy2(slurmfilepath, os.path.join(self.path, fname)) # # # Change job name (for convenience only) # sed_command = f"sed -i 's|#SBATCH --job-name=runFF|#SBATCH --job-name=c{cond}_c{case}_s{seed}_runFF_{os.path.basename(self.path)}|g' {fname}" @@ -924,8 +925,7 @@ def TS_low_slurm_prepare(self, slurmfilepath): raise ValueError (f'SLURM script for low-res box {slurmfilepath} does not exist.') self.slurmfilename_low = os.path.basename(slurmfilepath) - import subprocess - status = os.popen(f'cp {slurmfilepath} {self.path}/{self.slurmfilename_low}') + shutil.copy2(slurmfilepath, os.path.join(self.path, self.slurmfilename_low)) # Change job name (for convenience only) _ = subprocess.call(f"sed -i 's|#SBATCH --job-name=lowBox|#SBATCH --job-name=lowBox_{os.path.basename(self.path)}|g' {self.slurmfilename_low}", cwd=self.path, shell=True) @@ -1150,7 +1150,7 @@ def TS_high_slurm_prepare(self, slurmfilepath): self.slurmfilename_high = os.path.basename(slurmfilepath) ntasks = self.nConditions*self.nHighBoxCases*self.nSeeds*self.nTurbines - status = os.popen(f'cp {slurmfilepath} {self.path}/{self.slurmfilename_high}') + shutil.copy2(slurmfilepath, os.path.join(self.path, self.slurmfilename_high)) # Change job name (for convenience only) _ = subprocess.call(f"sed -i 's|#SBATCH --job-name=highBox|#SBATCH --job-name=highBox_{os.path.basename(self.path)}|g' {self.slurmfilename_high}", cwd=self.path, shell=True) @@ -1295,7 +1295,7 @@ def FF_setup(self, outlistFF=None, **kwargs): last = None for last in (line for line in f if line.rstrip('\n')): pass - if 'TurbSim terminated normally' not in last: + if last is None or 'TurbSim terminated normally' not in last: raise ValueError(f'All TurbSim boxes need to be completed before this step can be done.') self._FF_setup_TS(**kwargs) @@ -1308,12 +1308,12 @@ def _FF_setup_LES(self, seedsToKeep=1): # Clean unnecessary directories and files created by the general setup for cond in range(self.nConditions): for seed in range(self.nSeeds): - os.popen(f"rm -rf {os.path.join(self.path,self.condDirList[cond],f'Seed_{seed}')}") + shutil.rmtree(os.path.join(self.path, self.condDirList[cond], f'Seed_{seed}')) for case in range(self.nCases): - #os.popen(f"rm -rf {os.path.join(path,condDirList[cond],caseDirList[case], f'Seed_0','InflowWind.dat')}") # needs to exist + #shutil.rmtree(os.path.join(path, condDirList[cond], caseDirList[case], f'Seed_0','InflowWind.dat')) # needs to exist for seed in range(seedsToKeep,self.nSeeds): - os.popen(f"rm -rf {os.path.join(self.path,self.condDirList[cond],self.caseDirList[case], f'Seed_{seed}')}") + shutil.rmtree(os.path.join(self.path, self.condDirList[cond], self.caseDirList[case], f'Seed_{seed}')) @@ -1324,7 +1324,7 @@ def _FF_setup_LES(self, seedsToKeep=1): for case in range(self.nCases): for seed in range(self.seedsToKeep): # Remove TurbSim dir and create LES boxes dir - _ = os.popen(f"rm -rf {os.path.join(self.path,self.condDirList[cond],self.caseDirList[case], f'Seed_{seed}', 'TurbSim')}") + shutil.rmtree(os.path.join(self.path, self.condDirList[cond], self.caseDirList[case], f'Seed_{seed}', 'TurbSim')) if not os.path.exists(os.path.join(self.path,self.condDirList[cond],self.caseDirList[case],f'Seed_{seed}',LESboxesDirName)): os.makedirs(os.path.join(self.path,self.condDirList[cond],self.caseDirList[case],f'Seed_{seed}',LESboxesDirName)) @@ -1664,7 +1664,7 @@ def FF_slurm_prepare(self, slurmfilepath): for seed in range(self.nSeeds): fname = f'runFASTFarm_cond{cond}_case{case}_seed{seed}.sh' - status = os.popen(f'cp {slurmfilepath} {os.path.join(self.path,fname)}') + shutil.copy2(slurmfilepath, os.path.join(self.path, fname)) # Change job name (for convenience only) sed_command = f"sed -i 's|#SBATCH --job-name=runFF|#SBATCH --job-name=c{cond}_c{case}_s{seed}_runFF_{os.path.basename(self.path)}|g' {fname}" @@ -1708,3 +1708,5 @@ def FF_slurm_submit(self): print(f'Calling: {sub_command}') subprocess.call(sub_command, cwd=self.path, shell=True) time.sleep(4) # Sometimes the same job gets submitted twice. This gets around it. + + From 1dceaa58cc7eec0a9bfaf58c696437ee9d219021 Mon Sep 17 00:00:00 2001 From: Regis Thedin Date: Thu, 16 Mar 2023 16:47:21 -0600 Subject: [PATCH 071/124] FF: minor adjustments --- pyFAST/fastfarm/AMRWindSimulation.py | 19 +++++++++++-------- pyFAST/fastfarm/FASTFarmCaseCreation.py | 4 ++-- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/pyFAST/fastfarm/AMRWindSimulation.py b/pyFAST/fastfarm/AMRWindSimulation.py index 7745140..b42998a 100644 --- a/pyFAST/fastfarm/AMRWindSimulation.py +++ b/pyFAST/fastfarm/AMRWindSimulation.py @@ -503,28 +503,31 @@ def write_sampling_params(self, outdir=None): sampling_labels_lr_str = " ".join(str(item) for item in self.sampling_labels_lr) sampling_labels_hr_str = " ".join(str(item) for item in self.sampling_labels_hr) s = f"# Sampling info generated by AMRWindSamplingCreation.py\n" - s += f"incflo.post_processing = {self.postproc_name_lr} {self.postproc_name_hr} # averaging\n\n" + s += f"incflo.post_processing = {self.postproc_name_lr} {self.postproc_name_hr} # averaging\n\n\n" + + s += f"# ---- Low-res sampling parameters ----\n" s += f"{self.postproc_name_lr}.output_format = netcdf\n" s += f"{self.postproc_name_lr}.output_frequency = {self.output_frequency_lr}\n" s += f"{self.postproc_name_lr}.fields = velocity # temperature tke\n" s += f"{self.postproc_name_lr}.labels = {sampling_labels_lr_str}\n\n" - s += f"{self.postproc_name_hr}.output_format = netcdf\n" - s += f"{self.postproc_name_hr}.output_frequency = {self.output_frequency_hr}\n" - s += f"{self.postproc_name_hr}.fields = velocity # temperature tke\n" - s += f"{self.postproc_name_hr}.labels = {sampling_labels_hr_str}\n" - # Write out low resolution sampling plane info zoffsets_lr_str = " ".join(str(item) for item in self.zoffsets_lr) - s += f"\n# Low sampling grid spacing = {self.ds_lr} m\n" + s += f"# Low sampling grid spacing = {self.ds_lr} m\n" s += f"{self.postproc_name_lr}.Low.type = PlaneSampler\n" s += f"{self.postproc_name_lr}.Low.num_points = {self.nx_lr} {self.ny_lr}\n" s += f"{self.postproc_name_lr}.Low.origin = {self.xlow_lr:.4f} {self.ylow_lr:.4f} {self.zlow_lr:.4f}\n" # Round the float output s += f"{self.postproc_name_lr}.Low.axis1 = {self.xdist_lr:.4f} 0.0 0.0\n" # Assume: axis1 oriented parallel to AMR-Wind x-axis s += f"{self.postproc_name_lr}.Low.axis2 = 0.0 {self.ydist_lr:.4f} 0.0\n" # Assume: axis2 oriented parallel to AMR-Wind y-axis s += f"{self.postproc_name_lr}.Low.normal = 0.0 0.0 1.0\n" - s += f"{self.postproc_name_lr}.Low.offsets = {zoffsets_lr_str}\n" + s += f"{self.postproc_name_lr}.Low.offsets = {zoffsets_lr_str}\n\n\n" + + s += f"# ---- High-res sampling parameters ----\n" + s += f"{self.postproc_name_hr}.output_format = netcdf\n" + s += f"{self.postproc_name_hr}.output_frequency = {self.output_frequency_hr}\n" + s += f"{self.postproc_name_hr}.fields = velocity # temperature tke\n" + s += f"{self.postproc_name_hr}.labels = {sampling_labels_hr_str}\n" # Write out high resolution sampling plane info for turbkey in self.hr_domains: diff --git a/pyFAST/fastfarm/FASTFarmCaseCreation.py b/pyFAST/fastfarm/FASTFarmCaseCreation.py index 47dc4ae..dfddebe 100644 --- a/pyFAST/fastfarm/FASTFarmCaseCreation.py +++ b/pyFAST/fastfarm/FASTFarmCaseCreation.py @@ -1410,8 +1410,8 @@ def _FF_setup_LES(self, seedsToKeep=1): ff_file['OutDisWindY'] = ' '.join(map(str, self.planes_xz)) # Modify wake outputs - ff_file['NOutDist'] = 7 - ff_file['OutDist'] = ' '.join(map(str, [1,1.5,2,2.5,3,3.5,4]*D_)) + ff_file['NOutDist'] = 9 + ff_file['OutDist'] = ' '.join(map(str, [d*D_ for d in [0.5,1,1.5,2,3,4,5,6,7]])) # Mofidy wind output ff_file['NWindVel'] = 9 ff_file['WindVelX'] = ' '.join(map(str, xWT[:9])) From ce840416d9ec6ca75bf5bedfe55d8e5136fd5347 Mon Sep 17 00:00:00 2001 From: Regis Thedin Date: Fri, 17 Mar 2023 12:54:05 -0600 Subject: [PATCH 072/124] FF: Update example; minor fixes --- pyFAST/fastfarm/FASTFarmCaseCreation.py | 63 +++++++++---------- .../examples/Ex3_FFarmCompleteSetup.py | 58 +++++++++-------- 2 files changed, 63 insertions(+), 58 deletions(-) diff --git a/pyFAST/fastfarm/FASTFarmCaseCreation.py b/pyFAST/fastfarm/FASTFarmCaseCreation.py index dfddebe..a4519fb 100644 --- a/pyFAST/fastfarm/FASTFarmCaseCreation.py +++ b/pyFAST/fastfarm/FASTFarmCaseCreation.py @@ -20,6 +20,27 @@ def getMultipleOf(val, multipleof): valmult = int(round(val/multipleof,6))*multipleof return round(valmult, 4) +def modifyProperty(fullfilename, entry, value): + ''' + Modify specific properties of certain files + + Inputs + ====== + fullfilename: str + Full filepath of the file. + entry: str + Entry in the input file to be modified + value: + Value to go on the entry. No checks are made + + ''' + # Open the proper file + f = FASTInputFile(fullfilename) + # Change the actual value + f[entry] = value + # Save the new file + f.write(fullfilename) + return class FFCaseCreation: @@ -413,30 +434,6 @@ def copyTurbineFilesForEachCase(self, writeFiles=True): if writeFiles: self.turbineFile.write( os.path.join(currPath,f'{self.turbfilename}{t+1}.fst')) - def modifyProperty(self, fullfilename, entry, value): - ''' - Modify specific properties of certain files - - Inputs - ====== - fullfilename: str - Full filepath of the file. - entry: str - Entry in the input file to be modified - value: - Value to go on the entry. No checks are made - - ''' - - # Open the proper file - f = FASTInputFile(fullfilename) - # Change the actual value - f[entry] = value - # Save the new file - f.write(fullfilename) - - return - def setTemplateFilename(self, templatePath=None, EDfilename=None, SEDfilename=None, HDfilename=None, SrvDfilename=None, ADfilename=None, ADskfilename=None, SubDfilename=None, IWfilename=None, BDfilepath=None, bladefilename=None, towerfilename=None, turbfilename=None, libdisconfilepath=None, controllerInputfilename=None, coeffTablefilename=None, turbsimLowfilepath=None, turbsimHighfilepath=None, FFfilename=None): ''' @@ -1403,20 +1400,20 @@ def _FF_setup_LES(self, seedsToKeep=1): ff_file['WrDisWind'] = 'False' ff_file['WrDisDT'] = ff_file['DT_Low-VTK']*10 # writeFastFarm sets this to be the same as DT_Low ff_file['NOutDisWindXY'] = len(self.planes_xy) - ff_file['OutDisWindZ'] = ' '.join(map(str, self.planes_xy)) + ff_file['OutDisWindZ'] = ', '.join(map(str, self.planes_xy)) ff_file['NOutDisWindYZ'] = len(self.planes_yz) - ff_file['OutDisWindX'] = ' '.join(map(str, self.planes_yz)) + ff_file['OutDisWindX'] = ', '.join(map(str, self.planes_yz)) ff_file['NOutDisWindXZ'] = len(self.planes_xz) - ff_file['OutDisWindY'] = ' '.join(map(str, self.planes_xz)) + ff_file['OutDisWindY'] = ', '.join(map(str, self.planes_xz)) # Modify wake outputs ff_file['NOutDist'] = 9 - ff_file['OutDist'] = ' '.join(map(str, [d*D_ for d in [0.5,1,1.5,2,3,4,5,6,7]])) + ff_file['OutDist'] = ', '.join(map(str, [d*D_ for d in [0.5,1,1.5,2,3,4,5,6,7]])) # Mofidy wind output - ff_file['NWindVel'] = 9 - ff_file['WindVelX'] = ' '.join(map(str, xWT[:9])) - ff_file['WindVelY'] = ' '.join(map(str, yWT[:9])) - ff_file['WindVelZ'] = ' '.join(map(str, zWT[:9])) + ff_file['NWindVel'] = len(xWT[:9]) + ff_file['WindVelX'] = ', '.join(map(str, xWT[:9])) + ff_file['WindVelY'] = ', '.join(map(str, yWT[:9])) + ff_file['WindVelZ'] = ', '.join(map(str, zWT[:9]+self.zhub)) ff_file.write(outputFSTF) @@ -1706,7 +1703,7 @@ def FF_slurm_submit(self): fname = f'runFASTFarm_cond{cond}_case{case}_seed{seed}.sh' sub_command = f"sbatch {fname}" print(f'Calling: {sub_command}') - subprocess.call(sub_command, cwd=self.path, shell=True) + _ = subprocess.call(sub_command, cwd=self.path, shell=True) time.sleep(4) # Sometimes the same job gets submitted twice. This gets around it. diff --git a/pyFAST/fastfarm/examples/Ex3_FFarmCompleteSetup.py b/pyFAST/fastfarm/examples/Ex3_FFarmCompleteSetup.py index 517f6b2..a53418d 100644 --- a/pyFAST/fastfarm/examples/Ex3_FFarmCompleteSetup.py +++ b/pyFAST/fastfarm/examples/Ex3_FFarmCompleteSetup.py @@ -16,37 +16,41 @@ def main(): # ----------------------------------------------------------------------------- # USER INPUT: Modify these + # For the d{t,s}_{high,low}_les paramters, use AMRWindSimulation.py # ----------------------------------------------------------------------------- # ----------- Case absolute path path = '/complete/path/of/your/case' - # ----------- Wind farm - wts = { - 0 :{'x':0.0, 'y':0, 'z':0.0, 'D':250, 'zhub':150}, - 1 :{'x':1852.0, 'y':0, 'z':0.0, 'D':250, 'zhub':150}, - 2 :{'x':3704.0, 'y':0, 'z':0.0, 'D':250, 'zhub':150}, - 3 :{'x':5556.0, 'y':0, 'z':0.0, 'D':250, 'zhub':150}, - 4 :{'x':7408.0, 'y':0, 'z':0.0, 'D':250, 'zhub':150}, - 5 :{'x':1852.0, 'y':1852.0, 'z':0.0, 'D':250, 'zhub':150}, - 6 :{'x':3704.0, 'y':1852.0, 'z':0.0, 'D':250, 'zhub':150}, - 7 :{'x':5556.0, 'y':1852.0, 'z':0.0, 'D':250, 'zhub':150}, - 8 :{'x':7408.0, 'y':1852.0, 'z':0.0, 'D':250, 'zhub':150}, - 9 :{'x':3704.0, 'y':3704.0, 'z':0.0, 'D':250, 'zhub':150}, - 10:{'x':5556.0, 'y':3704.0, 'z':0.0, 'D':250, 'zhub':150}, - 11:{'x':7408.0, 'y':3704.0, 'z':0.0, 'D':250, 'zhub':150}, - } - refTurb_rot = 0 - # ----------- General hard-coded parameters cmax = 5 # maximum blade chord (m) fmax = 10/6 # maximum excitation frequency (Hz) Cmeander = 1.9 # Meandering constant (-) + + # ----------- Wind farm + D = 240 + zhub = 150 + wts = { + 0 :{'x':0.0, 'y':0, 'z':0.0, 'D':D, 'zhub':zhub, 'cmax':cmax, 'fmax':fmax, 'Cmeander':Cmeander}}, + 1 :{'x':1852.0, 'y':0, 'z':0.0, 'D':D, 'zhub':zhub, 'cmax':cmax, 'fmax':fmax, 'Cmeander':Cmeander}}, + 2 :{'x':3704.0, 'y':0, 'z':0.0, 'D':D, 'zhub':zhub, 'cmax':cmax, 'fmax':fmax, 'Cmeander':Cmeander}}, + 3 :{'x':5556.0, 'y':0, 'z':0.0, 'D':D, 'zhub':zhub, 'cmax':cmax, 'fmax':fmax, 'Cmeander':Cmeander}}, + 4 :{'x':7408.0, 'y':0, 'z':0.0, 'D':D, 'zhub':zhub, 'cmax':cmax, 'fmax':fmax, 'Cmeander':Cmeander}}, + 5 :{'x':1852.0, 'y':1852.0, 'z':0.0, 'D':D, 'zhub':zhub, 'cmax':cmax, 'fmax':fmax, 'Cmeander':Cmeander}}, + 6 :{'x':3704.0, 'y':1852.0, 'z':0.0, 'D':D, 'zhub':zhub, 'cmax':cmax, 'fmax':fmax, 'Cmeander':Cmeander}}, + 7 :{'x':5556.0, 'y':1852.0, 'z':0.0, 'D':D, 'zhub':zhub, 'cmax':cmax, 'fmax':fmax, 'Cmeander':Cmeander}}, + 8 :{'x':7408.0, 'y':1852.0, 'z':0.0, 'D':D, 'zhub':zhub, 'cmax':cmax, 'fmax':fmax, 'Cmeander':Cmeander}}, + 9 :{'x':3704.0, 'y':3704.0, 'z':0.0, 'D':D, 'zhub':zhub, 'cmax':cmax, 'fmax':fmax, 'Cmeander':Cmeander}}, + 10:{'x':5556.0, 'y':3704.0, 'z':0.0, 'D':D, 'zhub':zhub, 'cmax':cmax, 'fmax':fmax, 'Cmeander':Cmeander}}, + 11:{'x':7408.0, 'y':3704.0, 'z':0.0, 'D':D, 'zhub':zhub, 'cmax':cmax, 'fmax':fmax, 'Cmeander':Cmeander}}, + } + refTurb_rot = 0 # ----------- Additional variables - tmax = 1800 - nSeeds = 6 - zbot = 1 + tmax = 1800 # Total simulation time + nSeeds = 6 # Number of different seeds + zbot = 1 # Bottom of your domain + mod_wake = 1 # Wake model. 1: Polar, 2: Curl, 3: Cartesian # ----------- Desired sweeps vhub = [10] @@ -67,7 +71,7 @@ def main(): # Low-res boxes settings dt_low_les = 3 # sampling frequency of low-res files ds_low_les = 20.0 # dx, dy, dz of low-res files - extent_low = [3, 8, 3, 3, 2] + extent_low = [3, 8, 3, 3, 2] # extent in xmin, xmax, ymin, ymax, zmax, in D # ----------- Execution parameters @@ -83,6 +87,7 @@ def main(): templatePath = '/full/path/where/template/files/are' # Put 'unused' to any input that is not applicable to your case + # Files should be in templatePath EDfilename = 'ElastoDyn.T' SEDfilename = 'SimplifiedElastoDyn.T' HDfilename = 'HydroDyn.dat' @@ -118,7 +123,7 @@ def main(): # Initial setup case = FFCaseCreation(path, wts, cmax, fmax, Cmeander, tmax, zbot, vhub, shear, TIvalue, inflow_deg, dt_high_les, ds_high_les, extent_high, - dt_low_les, ds_low_les, extent_low, ffbin, LESpath=LESpath, + dt_low_les, ds_low_les, extent_low, ffbin, mod_wake, LESpath=LESpath, verbose=1) case.setTemplateFilename(templatePath, EDfilename, SEDfilename, HDfilename, SrvDfilename, ADfilename, @@ -126,9 +131,12 @@ def main(): turbfilename, libdisconfilepath, controllerInputfilename, coeffTablefilename, turbsimLowfilepath, turbsimHighfilepath, FFfilename) - case.copyTurbineFilesForEachCase() + # Get domain paramters case.getDomainParameters() + # Organize file structure + case.copyTurbineFilesForEachCase() + # TurbSim setup if LESpath is None: case.TS_low_setup() @@ -146,5 +154,5 @@ def main(): if __name__ == '__main__': - # This example cannot be fully run if TurbSim inflow is requested. - main() + # This example cannot be fully run. + pass From d4ab6ab14ac8dce219da317b6c56753096cefc32 Mon Sep 17 00:00:00 2001 From: Regis Thedin Date: Tue, 21 Mar 2023 12:11:34 -0600 Subject: [PATCH 073/124] FF: code clean-up --- pyFAST/fastfarm/FASTFarmCaseCreation.py | 110 ++++++------------------ 1 file changed, 27 insertions(+), 83 deletions(-) diff --git a/pyFAST/fastfarm/FASTFarmCaseCreation.py b/pyFAST/fastfarm/FASTFarmCaseCreation.py index a4519fb..0142fcb 100644 --- a/pyFAST/fastfarm/FASTFarmCaseCreation.py +++ b/pyFAST/fastfarm/FASTFarmCaseCreation.py @@ -109,10 +109,6 @@ def __init__(self, path, wts, cmax, fmax, Cmeander, tmax, zbot, vhub, shear, TIv if self.verbose>0: print(f'Creating directory structure and copying files... Done.') - #if self.verbose: print(f'Setting up FAST.Farm based on {self.inflowStr} inflow...', end='\r') - #if self.verbose: print(f'Setting up FAST.Farm based on {self.inflowStr} inflow... Done.' - - def _checkInputs(self): # Create case path is doesn't exist @@ -281,12 +277,8 @@ def _create_dir_structure(self): turbsimPath = os.path.join(seedPath, 'TurbSim') if not os.path.exists(turbsimPath): os.makedirs(turbsimPath) - # The following loop creates the turbsim files for low box. That should really only happen if inflowStr is `TurbSim`, as I have commented out below. - # However, as of now I need to copy some files to obtain the limits of the turbsim box to be able to shift the coordinate system. Later on i delete these dirs - # if inflowStr == 'TurbSim': - # for seed in range(nSeeds): - # seedPath = os.path.join(condPath, f'Seed_{seed}') - # if not os.path.exists(seedPath): os.makedirs(seedPath) + # The following loop creates the turbsim files for low box. That should really only happen if inflowStr is `TurbSim`. + # It does happen regardless because when the inflow is LES, it will be later on deleted. for seed in range(self.nSeeds): seedPath = os.path.join(condPath, f'Seed_{seed}') if not os.path.exists(seedPath): os.makedirs(seedPath) @@ -331,12 +323,11 @@ def copyTurbineFilesForEachCase(self, writeFiles=True): self.HydroDynFile.write(os.path.join(currPath, self.HDfilename)) shutil.copy2(os.path.join(self.templatePath,self.controllerInputfilename), os.path.join(currPath,self.controllerInputfilename)) - - - # Depending on the controller, it might need to be in the same level as the fstf input file. The ideal solution would be give the full - # path to the controller input, but we have no control over the compilation process and it is likely that a very long string with the full - # path will get cut. So we need to give the relative path. We give the path as the current one, so here we create a link to ensure it will - # work regardless of how the controller was compiled. There is no hard in having this extra link even if it's not needed. + # Depending on the controller, the controller input file might need to be in the same level as the .fstf input file. + # The ideal solution would be to give the full path to the controller input file, but we may not have control over + # the compilation process and it is likely that a very long string with the full path will get cut. So we need to + # give the relative path. We give the path as the current one, so here we create a link to ensure it will work + # regardless of how the controller was compiled. There is no harm in having this extra link even if it's not needed. notepath = os.getcwd(); os.chdir(self.path) for seed in range(self.nSeeds): try: @@ -372,12 +363,11 @@ def copyTurbineFilesForEachCase(self, writeFiles=True): if EDmodel_ == 'FED': # Update each turbine's ElastoDyn self.ElastoDynFile['NacYaw'] = yaw_deg_ + yaw_mis_deg_ - # check if this change of EDfile to a variable works as it should with quotes and stuff shutil.copy2(os.path.join(self.templatePath,self.bladefilename), os.path.join(currPath,self.bladefilename)) self.ElastoDynFile['BldFile1'] = self.ElastoDynFile['BldFile2'] = self.ElastoDynFile['BldFile3'] = f'"{self.bladefilename}"' shutil.copy2(os.path.join(self.templatePath,self.towerfilename), os.path.join(currPath,self.towerfilename)) self.ElastoDynFile['TwrFile'] = f'"{self.towerfilename}"' - self.ElastoDynFile['Azimuth'] = round(np.random.uniform(low=0, high=360)) # start at a random value + self.ElastoDynFile['Azimuth'] = round(np.random.uniform(low=0, high=360)) # start at a random value #JJ is this needed? if writeFiles: self.ElastoDynFile.write(os.path.join(currPath,f'{self.EDfilename}{t+1}_mod.dat')) @@ -418,7 +408,7 @@ def copyTurbineFilesForEachCase(self, writeFiles=True): self.turbineFile['CompAero'] = 2 # 1: AeroDyn v14; 2: AeroDyn v15; 3: AeroDisk self.turbineFile['AeroFile'] = f'"{self.ADfilepath}"' elif ADmodel_ == 'ADsk': - # from Andy's email on 2022-11-01 So if you use AeroDisk with ElastoDyn, just set the blade DOFs to false for now. + # If you use AeroDisk with ElastoDyn, set the blade DOFs to false. self.turbineFile['CompAero'] = 3 # 1: AeroDyn v14; 2: AeroDyn v15; 3: AeroDisk self.turbineFile['AeroFile'] = f'"{self.ADskfilepath}"' if writeFiles: @@ -797,11 +787,11 @@ def _setRotorParameters(self): if self.D == 220: # 12 MW turbine self.bins = xr.Dataset({'WaveHs': (['wspd'], [ 1.429, 1.429]), # 1.429 comes from Matt's hydrodyn input file 'WaveTp': (['wspd'], [ 7.073, 7.073]), # 7.073 comes from Matt's hydrodyn input file - 'RotSpeed': (['wspd'], [ 4.0, 4.0]), # 4 rpm comes from Matt's ED input file - 'BlPitch': (['wspd'], [ 0.0, 0.0]), # 0 deg comes from Matt's ED input file - #'WvHiCOffD': (['wspd'], [0, 0]), # 2nd order wave info. Unused for now - #'WvLowCOffS': (['wspd'], [0, 0]), # 2nd order wave info. Unused for now - }, coords={'wspd': [10, 15]} ) # 15 m/s is 'else', since method='nearest' is used on the variable `bins` + 'RotSpeed': (['wspd'], [ 4.0, 4.0]), # 4 rpm comes from Matt's ED input file + 'BlPitch': (['wspd'], [ 0.0, 0.0]), # 0 deg comes from Matt's ED input file + #'WvHiCOffD': (['wspd'], [0, 0]), # 2nd order wave info. Unused for now # JJ should we have this? + #'WvLowCOffS': (['wspd'], [0, 0]), # 2nd order wave info. Unused for now # JJ: ditto + }, coords={'wspd': [10, 15]} ) # 15 m/s is 'else', since method='nearest' is used on the variable `bins` elif self.D == 240: # IEA 15 MW self.bins = xr.Dataset({'WaveHs': (['wspd'], [1.172, 1.323, 1.523, 1.764, 2.255]), # higher values on default input from the repository (4.52) @@ -842,8 +832,8 @@ def TS_low_setup(self, writeFiles=True, runOnce=False): Lambda1 = 0.7*HubHt_ if HubHt_<60 else 42 # IEC 61400-3 ed4, sec 6.3.1, eq 5 # Create and write new Low.inp files creating the proper box with proper resolution - # By passing low_ext, manual mode for the domain size is activated, and by passing ds_low, manual mode - # for discretization (and further domain size) is also activated + # By passing low_ext, manual mode for the domain size is activated, and by passing ds_low, + # manual mode for discretization (and further domain size) is also activated currentTS = TSCaseCreation(D_, HubHt_, Vhub_, tivalue_, shear_, x=xlocs_, y=ylocs_, zbot=self.zbot, cmax=self.cmax, fmax=self.fmax, Cmeander=self.Cmeander, boxType=boxType, low_ext=self.extent_low, ds_low=self.ds_low_les) self.TSlowbox = currentTS @@ -855,7 +845,7 @@ def TS_low_setup(self, writeFiles=True, runOnce=False): Lowinp = FASTInputFile(currentTSLowFile) Lowinp['RandSeed1'] = self.seedValues[seed] Lowinp['PLExp'] = shear_ - #Lowinp['latitude'] = latitude # Not used when IECKAI model is selected. + #Lowinp['latitude'] = latitude # Not used when IECKAI model is selected. Lowinp['InCDec1'] = Lowinp['InCDec2'] = Lowinp['InCDec3'] = f'"{a} {b/(8.1*Lambda1):.8f}"' # The dt was computed for a proper low-res box but here we will want to compare with the high-res # and it is convenient to have the same time step. Let's do that change here @@ -871,49 +861,6 @@ def TS_low_setup(self, writeFiles=True, runOnce=False): def TS_low_slurm_prepare(self, slurmfilepath): - - - # # -------------------------------------------------- - # # ----- Prepare SLURM script for Low-res boxes ----- - # # --------------- ONE SCRIPT PER CASE -------------- - # # -------------------------------------------------- - # - # if not os.path.isfile(slurmfilepath): - # raise ValueError (f'SLURM script for FAST.Farm {slurmfilepath} does not exist.') - # self.slurmfilename_ff = os.path.basename(slurmfilepath) - - - # for cond in range(self.nConditions): - # for case in range(self.nCases): - # for seed in range(self.nSeeds): - # - # fname = f'runFASTFarm_cond{cond}_case{case}_seed{seed}.sh' - # shutil.copy2(slurmfilepath, os.path.join(self.path, fname)) - # - # # Change job name (for convenience only) - # sed_command = f"sed -i 's|#SBATCH --job-name=runFF|#SBATCH --job-name=c{cond}_c{case}_s{seed}_runFF_{os.path.basename(self.path)}|g' {fname}" - # _ = subprocess.call(sed_command, cwd=self.path, shell=True) - # # Change logfile name (for convenience only) - # sed_command = f"sed -i 's|#SBATCH --output log.fastfarm_c0_c0_seed0|#SBATCH --output log.fastfarm_c{cond}_c{case}_s{seed}|g' {fname}" - # _ = subprocess.call(sed_command, cwd=self.path, shell=True) - # # Change the fastfarm binary to be called - # sed_command = f"""sed -i "s|^fastfarmbin.*|fastfarmbin='{self.ffbin}'|g" {fname}""" - # _ = subprocess.call(sed_command, cwd=self.path, shell=True) - # # Change the path inside the script to the desired one - # sed_command = f"sed -i 's|/projects/shellwind/rthedin/Task2_2regis|{self.path}|g' {fname}" - # _ = subprocess.call(sed_command, cwd=self.path, shell=True) - # # Write condition - # sed_command = f"""sed -i "s|^cond.*|cond='{self.condDirList[cond]}'|g" {fname}""" - # _ = subprocess.call(sed_command, cwd=self.path, shell=True) - # # Write case - # sed_command = f"""sed -i "s|^case.*|case='{self.caseDirList[case]}'|g" {fname}""" - # _ = subprocess.call(sed_command, cwd=self.path, shell=True) - # # Write seed - # sed_command = f"""sed -i "s|^seed.*|seed={seed}|g" {fname}""" - # _ = subprocess.call(sed_command, cwd=self.path, shell=True) - - - # -------------------------------------------------- # ----- Prepare SLURM script for Low-res boxes ----- # -------------------------------------------------- @@ -1181,8 +1128,8 @@ def TS_high_slurm_submit(self): def TS_high_create_symlink(self): + # Create symlink of all the high boxes for the cases with wake steering and yaw misalignment. These are the "repeated" boxes - notepath = os.getcwd() os.chdir(self.path) for cond in range(self.nConditions): @@ -1219,11 +1166,10 @@ def FF_setup(self, outlistFF=None, **kwargs): ''' if outlistFF is None: - # Output list for FAST.Farm runs. Use 1 at the end for turbines (they will be replicated for all turbines). Combination of sample input file and KS's input + # Output list for FAST.Farm runs. Use 1 at the end for turbines (they will be replicated for all turbines) outlistFF = [ "RtAxsXT1 , RtAxsYT1 , RtAxsZT1", "RtPosXT1 , RtPosYT1 , RtPosZT1", - #"RtDiamT1", "YawErrT1", "TIAmbT1", 'RtVAmbT1', @@ -1268,7 +1214,7 @@ def FF_setup(self, outlistFF=None, **kwargs): xWT = alignedTurbs['Tx'].values yWT = alignedTurbs['Ty'].values - offset=50 + offset=10 planes_xy = [self.zhub+self.zbot] planes_yz = np.unique(xWT+offset) planes_xz = np.unique(yWT) @@ -1342,8 +1288,6 @@ def _FF_setup_LES(self, seedsToKeep=1): except FileExistsError: print(f'Directory {dst} already exists. Skipping symlink.') - # Make a link for Amb.t0.vtk pointing to Amb.t1.vtk - # Loops on all conditions/cases and cases for FAST.Farm (following python-toolbox/pyFAST/fastfarm/examples/Ex2_FFarmInputSetup.py) @@ -1424,7 +1368,7 @@ def _FF_setup_LES(self, seedsToKeep=1): def _FF_setup_TS(self): - # Loops on all conditions/cases and cases for FAST.Farm (following python-toolbox/pyFAST/fastfarm/examples/Ex2_FFarmInputSetup.py) + # Loops on all conditions/cases and cases for FAST.Farm for cond in range(self.nConditions): for case in range(self.nCases): for seed in range(self.nSeeds): @@ -1513,7 +1457,7 @@ def _getBoxesParamsForFF(self, lowbts, highbts, dt_low_desired, D, HubHt, xWT, y _, meanU_High = highbts.midValues() # !!!!!!!!!!!!!!!! JJ: does it make sense to get both? the meanu for low will be a lot higher than vhub, _, meanU_Low = lowbts.midValues() # !!!!!!!!!!!!!!!! JJ: and the meanu for high can be a bit higher than vhub - dT_High = np.round(highbts.dt, 4) # 0.3 + dT_High = np.round(highbts.dt, 4) # dX_High can sometimes be too high. So get the closest to the cmax, but multiple of what should have been dX_High = round(meanU_High*dT_High) if self.verbose>1: @@ -1526,8 +1470,8 @@ def _getBoxesParamsForFF(self, lowbts, highbts, dt_low_desired, D, HubHt, xWT, y # ----- Low - dT_Low = self.getMultipleOf(dt_low_desired, multipleof=dT_High) - dX_Low = self.getMultipleOf(meanU_Low*dT_Low, multipleof=dX_High) # dy and dz high are 5. + dT_Low = getMultipleOf(dt_low_desired, multipleof=dT_High) + dX_Low = getMultipleOf(meanU_Low*dT_Low, multipleof=dX_High) dY_Low = lowbts.y[1] - lowbts.y[0] dZ_Low = lowbts.z[1] - lowbts.z[0] @@ -1536,14 +1480,14 @@ def _getBoxesParamsForFF(self, lowbts, highbts, dt_low_desired, D, HubHt, xWT, y LT_Low = np.round(lowbts.t[-1]-lowbts.t[0], 4) X0_Low = np.floor( (min(xWT) - self.extent_low[0]*D ))# - dX_Low)) # # JJ!!!!!!! # removing EB's -dX_Low from the X0_Low specification - X0_Low = self.getMultipleOf(X0_Low, multipleof=dX_Low) + X0_Low = getMultipleOf(X0_Low, multipleof=dX_Low) Y0_Low = np.floor( -LY_Low/2 ) # Starting on integer value for aesthetics Z0_Low = lowbts.z[0] # we start at lowest to include tower - XMax_Low = self.getMultipleOf(max(xWT) + self.extent_low[1]*D, multipleof=dX_Low) + XMax_Low = getMultipleOf(max(xWT) + self.extent_low[1]*D, multipleof=dX_Low) LX_Low = XMax_Low-X0_Low - nX_Low = int(np.ceil(LX_Low/dX_Low)+1) # plus 1 from the guidance + nX_Low = int(np.ceil(LX_Low/dX_Low)+1) nY_Low = len(lowbts.y) # !!!!!!! JJ: different from what EB has nZ_Low = len(lowbts.z) From af9faa4da9fb4441b7fceb1c932aa95d50035d43 Mon Sep 17 00:00:00 2001 From: Regis Thedin Date: Wed, 22 Mar 2023 12:10:24 -0600 Subject: [PATCH 074/124] FF: Fix missing comma on list of coordinates for sampling --- pyFAST/fastfarm/FASTFarmCaseCreation.py | 31 ++++++++++++++----------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/pyFAST/fastfarm/FASTFarmCaseCreation.py b/pyFAST/fastfarm/FASTFarmCaseCreation.py index 0142fcb..91ceb08 100644 --- a/pyFAST/fastfarm/FASTFarmCaseCreation.py +++ b/pyFAST/fastfarm/FASTFarmCaseCreation.py @@ -346,6 +346,9 @@ def copyTurbineFilesForEachCase(self, writeFiles=True): self.InflowWindFile['Filename_BTS'] = '"./TurbSim"' if writeFiles: self.InflowWindFile.write( os.path.join(currPath,f'InflowWind.dat')) + for seed in range(self.nSeeds): + self.InflowWindFile.write( os.path.join(currPath,f'Seed_{seed}','InflowWind.dat')) + for t in range(self.nTurbines): # Recover info about the current turbine in CondXX_*/CaseYY_ @@ -1315,7 +1318,7 @@ def _FF_setup_LES(self, seedsToKeep=1): ff_file = FASTInputFile(outputFSTF) # Open output file and change additional values manually or make sure we have the correct ones - ff_file['InflowFile'] = f'"../{self.IWfilename}"' + ff_file['InflowFile'] = f'"./{self.IWfilename}"' ff_file['Mod_AmbWind'] = 1 # 1: LES boxes; 2: single TurbSim; 3: multiple TurbSim ff_file['TMax'] = self.tmax @@ -1400,7 +1403,8 @@ def _FF_setup_TS(self): # Get dictionary with all the D{X,Y,Z,t}, L{X,Y,Z,t}, N{X,Y,Z,t}, {X,Y,Z}0 dt_low_desired = self.Cmeander*D_/(10*Vhub_) # will be made multiple of dT_High inside _getBoxesParamsForFF - d = self._getBoxesParamsForFF(lowbts, highbts, dt_low_desired, D_, HubHt_, xWT, yt) + #d = self._getBoxesParamsForFF(lowbts, highbts, dt_low_desired, D_, HubHt_, xWT, yt) + d = self._getBoxesParamsForFF(lowbts, highbts, self.dt_low_les, D_, HubHt_, xWT, yt) # Write the file writeFastFarm(outputFSTF, templateFSTF, xWT, yt, zWT, d, OutListT1=self.outlistFF, noLeadingZero=True) @@ -1408,7 +1412,7 @@ def _FF_setup_TS(self): # Open saved file and change additional values manually or make sure we have the correct ones ff_file = FASTInputFile(outputFSTF) - ff_file['InflowFile'] = f'"../{self.IWfilename}"' + ff_file['InflowFile'] = f'"./{self.IWfilename}"' #ff_file['DT']=1.0 ff_file['Mod_AmbWind'] = 3 # 1: LES boxes; 2: single TurbSim; 3: multiple TurbSim ff_file['TMax'] = self.tmax @@ -1425,26 +1429,27 @@ def _FF_setup_TS(self): self.dr = round(self.D/10) ff_file['dr'] = self.dr ff_file['NumRadii'] = int(np.ceil(3*D_/(2*self.dr) + 1)) - ff_file['NumPlanes'] = int(np.ceil( 20*D_/(dt_low_desired*Vhub_*(1-1/6)) ) ) + #ff_file['NumPlanes'] = int(np.ceil( 20*D_/(dt_low_desired*Vhub_*(1-1/6)) ) ) + ff_file['NumPlanes'] = int(np.ceil( 20*D_/(self.dt_low_les*Vhub_*(1-1/6)) ) ) # Vizualization outputs ff_file['WrDisWind'] = 'False' ff_file['WrDisDT'] = ff_file['DT_Low']*10 # writeFastFarm sets this to be the same as DT_Low ff_file['NOutDisWindXY'] = len(self.planes_xy) - ff_file['OutDisWindZ'] = ' '.join(map(str, self.planes_xy)) + ff_file['OutDisWindZ'] = ', '.join(map(str, self.planes_xy)) ff_file['NOutDisWindYZ'] = len(self.planes_yz) - ff_file['OutDisWindX'] = ' '.join(map(str, self.planes_yz)) + ff_file['OutDisWindX'] = ', '.join(map(str, self.planes_yz)) ff_file['NOutDisWindXZ'] = len(self.planes_xz) - ff_file['OutDisWindY'] = ' '.join(map(str, self.planes_xz)) + ff_file['OutDisWindY'] = ', '.join(map(str, self.planes_xz)) # Modify wake outputs - ff_file['NOutDist'] = 7 - ff_file['OutDist'] = ' '.join(map(str, [1,1.5,2,2.5,3,3.5,4]*D_)) + ff_file['NOutDist'] = 9 + ff_file['OutDist'] = ', '.join(map(str, [1,1.5,2,2.5,3,3.5,4,5,6]*D_)) # Mofidy wind output - ff_file['NWindVel'] = 9 - ff_file['WindVelX'] = ' '.join(map(str, xWT[:9])) - ff_file['WindVelY'] = ' '.join(map(str, yWT[:9])) - ff_file['WindVelZ'] = ' '.join(map(str, zWT[:9]+self.zhub)) + ff_file['NWindVel'] = len(xWT[:9]) + ff_file['WindVelX'] = ', '.join(map(str, xWT[:9])) + ff_file['WindVelY'] = ', '.join(map(str, yWT[:9])) + ff_file['WindVelZ'] = ', '.join(map(str, zWT[:9]+self.zhub)) ff_file.write(outputFSTF) From ae947126ac80232386ee0888e278a24dafc0994e Mon Sep 17 00:00:00 2001 From: Regis Thedin Date: Fri, 28 Apr 2023 10:26:45 -0600 Subject: [PATCH 075/124] FF: improve robustness of file writing --- pyFAST/fastfarm/FASTFarmCaseCreation.py | 157 ++++++++++++++++++++---- 1 file changed, 134 insertions(+), 23 deletions(-) diff --git a/pyFAST/fastfarm/FASTFarmCaseCreation.py b/pyFAST/fastfarm/FASTFarmCaseCreation.py index 91ceb08..0964347 100644 --- a/pyFAST/fastfarm/FASTFarmCaseCreation.py +++ b/pyFAST/fastfarm/FASTFarmCaseCreation.py @@ -11,6 +11,19 @@ def cosd(t): return np.cos(np.deg2rad(t)) def sind(t): return np.sin(np.deg2rad(t)) +def checkIfExists(f): + if os.path.isfile(f): + return True + else: + print(f'File {f} does not exist.') + return False + +def shutilcopy2_untilSuccessful(src, dst): + shutil.copy2(src, dst) + if not checkIfExists(dst): + print(f'File {dst} not created. Trying again.\n') + shutilcopy2_untilSuccessful(src,dst) + def getMultipleOf(val, multipleof): ''' Get integer multiple of a quantity. @@ -42,6 +55,7 @@ def modifyProperty(fullfilename, entry, value): f.write(fullfilename) return + class FFCaseCreation: def __init__(self, path, wts, cmax, fmax, Cmeander, tmax, zbot, vhub, shear, TIvalue, inflow_deg, dt_high_les, ds_high_les, extent_high, dt_low_les, ds_low_les, extent_low, ffbin, mod_wake=1, yaw_init=None, ADmodel=None, EDmodel=None, nSeeds=6, LESpath=None, sweepWakeSteering=False, sweepYawMisalignment=False, seedValues=None, refTurb_rot=0, verbose=0): @@ -87,6 +101,7 @@ def __init__(self, path, wts, cmax, fmax, Cmeander, tmax, zbot, vhub, shear, TIv self.seedValues = seedValues self.refTurb_rot = refTurb_rot self.verbose = verbose + self.attempt = 1 if self.verbose>0: print(f'Checking inputs...', end='\r') @@ -260,12 +275,24 @@ def _create_dir_structure(self): yawCase_ = self.allCases['yawCase' ].sel(case=case).values # Set current path name string. The case is of the following form: Case00_wdirp10_WSfalse_YMfalse_12fED_12ADyn - caseStr = f"Case{case:02d}_wdir{f'{int(inflow_deg_):+03d}'.replace('+','p').replace('-','m')}"\ - f"_WS{str(wakeSteering_).lower()}_YM{str(misalignment_).lower()}"\ - f"_{nFED_}fED_{nADyn_}ADyn" + ndigits = len(str(self.nCases)) + caseStr = f"Case{case:0{ndigits}d}_wdir{f'{int(inflow_deg_):+03d}'.replace('+','p').replace('-','m')}" + # Add standard sweeps to the case name + if self.sweepWS: + caseStr += f"_WS{str(wakeSteering_).lower()}" + if self.sweepYM: + caseStr += f"_YM{str(misalignment_).lower()}" + if self.sweepEDmodel: + caseStr += f"_{nFED_}fED" + if self.sweepADmodel: + caseStr += f"_{nADyn_}ADyn" + + #caseStr = f"Case{case:0{ndigits}d}_wdir{f'{int(inflow_deg_):+03d}'.replace('+','p').replace('-','m')}"\ + # f"_WS{str(wakeSteering_).lower()}_YM{str(misalignment_).lower()}"\ + # f"_{nFED_}fED_{nADyn_}ADyn" # If sweeping on yaw, then add yaw case to dir name if len(np.unique(self.allCases.yawCase)) > 1: - caseStr = caseStr + f"_yawCase{yawCase_}" + caseStr += f"_yawCase{yawCase_}" caseDirList_.append(caseStr) casePath = os.path.join(condPath, caseStr) @@ -297,7 +324,9 @@ def copyTurbineFilesForEachCase(self, writeFiles=True): # Loops on all conditions/cases creating DISCON and *Dyn files for cond in range(self.nConditions): + print(f'Processing condition {self.condDirList[cond]}') for case in range(self.nCases): + print(f' Processing case {self.caseDirList[case]}', end='\r') currPath = os.path.join(self.path, self.condDirList[cond], self.caseDirList[case]) # Recover info about the current CondXX_*/CaseYY_* @@ -321,7 +350,7 @@ def copyTurbineFilesForEachCase(self, writeFiles=True): # Write updated DISCON and *Dyn files. if writeFiles: self.HydroDynFile.write(os.path.join(currPath, self.HDfilename)) - shutil.copy2(os.path.join(self.templatePath,self.controllerInputfilename), os.path.join(currPath,self.controllerInputfilename)) + shutilcopy2_untilSuccessful(os.path.join(self.templatePath,self.controllerInputfilename), os.path.join(currPath,self.controllerInputfilename)) # Depending on the controller, the controller input file might need to be in the same level as the .fstf input file. # The ideal solution would be to give the full path to the controller input file, but we may not have control over @@ -366,12 +395,12 @@ def copyTurbineFilesForEachCase(self, writeFiles=True): if EDmodel_ == 'FED': # Update each turbine's ElastoDyn self.ElastoDynFile['NacYaw'] = yaw_deg_ + yaw_mis_deg_ - shutil.copy2(os.path.join(self.templatePath,self.bladefilename), os.path.join(currPath,self.bladefilename)) self.ElastoDynFile['BldFile1'] = self.ElastoDynFile['BldFile2'] = self.ElastoDynFile['BldFile3'] = f'"{self.bladefilename}"' - shutil.copy2(os.path.join(self.templatePath,self.towerfilename), os.path.join(currPath,self.towerfilename)) - self.ElastoDynFile['TwrFile'] = f'"{self.towerfilename}"' + self.ElastoDynFile['TwrFile'] = f'"{self.towerfilename}"' self.ElastoDynFile['Azimuth'] = round(np.random.uniform(low=0, high=360)) # start at a random value #JJ is this needed? if writeFiles: + if t==0: shutilcopy2_untilSuccessful(os.path.join(self.templatePath,self.bladefilename), os.path.join(currPath,self.bladefilename)) + if t==0: shutilcopy2_untilSuccessful(os.path.join(self.templatePath,self.towerfilename), os.path.join(currPath,self.towerfilename)) self.ElastoDynFile.write(os.path.join(currPath,f'{self.EDfilename}{t+1}_mod.dat')) elif EDmodel_ == 'SED': @@ -415,7 +444,7 @@ def copyTurbineFilesForEachCase(self, writeFiles=True): self.turbineFile['CompAero'] = 3 # 1: AeroDyn v14; 2: AeroDyn v15; 3: AeroDisk self.turbineFile['AeroFile'] = f'"{self.ADskfilepath}"' if writeFiles: - shutil.copy2(self.coeffTablefilepath, os.path.join(currPath,self.coeffTablefilename)) + if t==0: shutilcopy2_untilSuccessful(self.coeffTablefilepath, os.path.join(currPath,self.coeffTablefilename)) self.turbineFile['ServoFile'] = f'"./{self.SrvDfilename}{t+1}_mod.dat"' self.turbineFile['HydroFile'] = f'"./{self.HDfilename}"' self.turbineFile['SubFile'] = f'"{self.SubDfilepath}"' @@ -427,6 +456,73 @@ def copyTurbineFilesForEachCase(self, writeFiles=True): if writeFiles: self.turbineFile.write( os.path.join(currPath,f'{self.turbfilename}{t+1}.fst')) + print(f'Done processing condition {self.condDirList[cond]} ') + + # Some files, for some reason, do not get copied properly. This leads to a case crashing due to missing file. + # Let's check if all files have been indded properly copied. If not, the copyTurbineFilesForEachCase will be + # called again until it does (up to 5 times) + if writeFiles: + if self._were_all_turbine_files_copied() == False and self.attempt<=5: + self.attempt += 1 + print(f'Not all files were copied successfully. Trying again. Attempt number {self.attempt}.') + self.copyTurbineFilesForEachCase() + elif self.attempt > 5: + print(f"WARNING: Not all turbine files were copied successfully after 5 tries.") + print(f" Check them manually. This shouldn't occur. Consider finding ") + print(f" and fixing the bug and submitting a PR.") + else: + print(f'Passed check: all files were copied successfully.') + + + def _were_all_turbine_files_copied(self): + ''' + Check if all files created in copyTurbineFilesForEachCase exist + ''' + + for cond in range(self.nConditions): + for case in range(self.nCases): + currPath = os.path.join(self.path, self.condDirList[cond], self.caseDirList[case]) + + _ = checkIfExists(os.path.join(currPath, self.HDfilename)) + if not _: return False + _ = checkIfExists(os.path.join(currPath,self.controllerInputfilename)) + if not _: return False + _ = checkIfExists( os.path.join(currPath,f'InflowWind.dat')) + if not _: return False + + for seed in range(self.nSeeds): + _ = checkIfExists(os.path.join(currPath,f'Seed_{seed}','InflowWind.dat')) + if not _: return False + + for t in range(self.nTurbines): + ADmodel_ = self.allCases.sel(case=case, turbine=t)['ADmodel'].values + EDmodel_ = self.allCases.sel(case=case, turbine=t)['EDmodel'].values + if EDmodel_ == 'FED': + _ = checkIfExists(os.path.join(currPath,self.bladefilename)) + if not _: return False + _ = checkIfExists(os.path.join(currPath,self.towerfilename)) + if not _: return False + _ = checkIfExists(os.path.join(currPath,f'{self.EDfilename}{t+1}_mod.dat')) + if not _: return False + + elif EDmodel_ == 'SED': + _ = checkIfExists(os.path.join(currPath,f'{self.SEDfilename}{t+1}_mod.dat')) + if not _: return False + + _ = checkIfExists(os.path.join(currPath,f'{self.SrvDfilename}{t+1}_mod.dat')) + if not _: return False + + if ADmodel_ == 'ADsk': + _ = checkIfExists(os.path.join(currPath,self.coeffTablefilename)) + if not _: return False + + _ = checkIfExists(os.path.join(currPath,f'{self.turbfilename}{t+1}.fst')) + if not _: return False + + # If we get to this point, all files exist + return True + + def setTemplateFilename(self, templatePath=None, EDfilename=None, SEDfilename=None, HDfilename=None, SrvDfilename=None, ADfilename=None, ADskfilename=None, SubDfilename=None, IWfilename=None, BDfilepath=None, bladefilename=None, towerfilename=None, turbfilename=None, libdisconfilepath=None, controllerInputfilename=None, coeffTablefilename=None, turbsimLowfilepath=None, turbsimHighfilepath=None, FFfilename=None): ''' @@ -674,8 +770,8 @@ def _create_all_cond(self): def _create_all_cases(self): - # Generate the different "cases" (inflow angle, and misalignment and wakesteer bools). She reads from ../NewFFParams_Shell.csv - # If misalignment true, then the actual yaw is yaw[turb]=np.random.uniform(low=-8.0, high=8.0). CaseSetup/ParameterManipulation.py:166 + # Generate the different "cases" (inflow angle, and misalignment and wakesteer bools). + # If misalignment true, then the actual yaw is yaw[turb]=np.random.uniform(low=-8.0, high=8.0). # Calculate the total number of cases given sweeps requested. Multipliers for wake steering, yaw misalignment, and reduced-order models nWindDir = len(np.unique(self.inflow_deg)) @@ -725,12 +821,16 @@ def _create_all_cases(self): # Count number of simplified models to add that information to the xarray. If their length is 1, it means they weren't requested if len(self.ADmodel) == 1: nADyn = self.nTurbines + self.sweepADmodel = False else: nADyn = [ self.ADmodel[i].count('ADyn') for i in range(len(self.ADmodel)) ] + self.sweepADmodel = True if len(self.EDmodel) == 1: nFED = self.nTurbines + self.sweepEDmodel = False else: nFED = [ self.EDmodel[i].count('FED') for i in range(len(self.EDmodel)) ] + self.sweepADmodel = True # Come up with an ordered "yaw case" numbering for dir name yawCase = np.arange(nCasesYawmultiplier)+1 @@ -1254,12 +1354,14 @@ def _FF_setup_LES(self, seedsToKeep=1): # Clean unnecessary directories and files created by the general setup for cond in range(self.nConditions): for seed in range(self.nSeeds): - shutil.rmtree(os.path.join(self.path, self.condDirList[cond], f'Seed_{seed}')) + currpath = os.path.join(self.path, self.condDirList[cond], f'Seed_{seed}') + if os.path.isdir(currpath): shutil.rmtree(currpath) for case in range(self.nCases): #shutil.rmtree(os.path.join(path, condDirList[cond], caseDirList[case], f'Seed_0','InflowWind.dat')) # needs to exist for seed in range(seedsToKeep,self.nSeeds): - shutil.rmtree(os.path.join(self.path, self.condDirList[cond], self.caseDirList[case], f'Seed_{seed}')) + currpath = os.path.join(self.path, self.condDirList[cond], self.caseDirList[case], f'Seed_{seed}') + if os.path.isdir(currpath): shutil.rmtree(currpath) @@ -1269,10 +1371,12 @@ def _FF_setup_LES(self, seedsToKeep=1): for cond in range(self.nConditions): for case in range(self.nCases): for seed in range(self.seedsToKeep): - # Remove TurbSim dir and create LES boxes dir - shutil.rmtree(os.path.join(self.path, self.condDirList[cond], self.caseDirList[case], f'Seed_{seed}', 'TurbSim')) - if not os.path.exists(os.path.join(self.path,self.condDirList[cond],self.caseDirList[case],f'Seed_{seed}',LESboxesDirName)): - os.makedirs(os.path.join(self.path,self.condDirList[cond],self.caseDirList[case],f'Seed_{seed}',LESboxesDirName)) + # Remove TurbSim dir + currpath = os.path.join(self.path, self.condDirList[cond], self.caseDirList[case], f'Seed_{seed}', 'TurbSim') + if os.path.isdir(currpath): shutil.rmtree(currpath) + # Create LES boxes dir + currpath = os.path.join(self.path,self.condDirList[cond],self.caseDirList[case],f'Seed_{seed}',LESboxesDirName) + if not os.path.isdir(currpath): os.makedirs(currpath) # Low-res box try: @@ -1293,7 +1397,7 @@ def _FF_setup_LES(self, seedsToKeep=1): - # Loops on all conditions/cases and cases for FAST.Farm (following python-toolbox/pyFAST/fastfarm/examples/Ex2_FFarmInputSetup.py) + # Loops on all conditions/cases and cases for FAST.Farm for cond in range(self.nConditions): for case in range(self.nCases): for seed in range(seedsToKeep): @@ -1345,7 +1449,7 @@ def _FF_setup_LES(self, seedsToKeep=1): # Vizualization outputs ff_file['WrDisWind'] = 'False' - ff_file['WrDisDT'] = ff_file['DT_Low-VTK']*10 # writeFastFarm sets this to be the same as DT_Low + ff_file['WrDisDT'] = ff_file['DT_Low-VTK'] # default is the same as DT_Low-VTK ff_file['NOutDisWindXY'] = len(self.planes_xy) ff_file['OutDisWindZ'] = ', '.join(map(str, self.planes_xy)) ff_file['NOutDisWindYZ'] = len(self.planes_yz) @@ -1434,7 +1538,7 @@ def _FF_setup_TS(self): # Vizualization outputs ff_file['WrDisWind'] = 'False' - ff_file['WrDisDT'] = ff_file['DT_Low']*10 # writeFastFarm sets this to be the same as DT_Low + ff_file['WrDisDT'] = ff_file['DT_Low'] # default is the same as DT_Low ff_file['NOutDisWindXY'] = len(self.planes_xy) ff_file['OutDisWindZ'] = ', '.join(map(str, self.planes_xy)) ff_file['NOutDisWindYZ'] = len(self.planes_yz) @@ -1636,7 +1740,7 @@ def FF_slurm_prepare(self, slurmfilepath): - def FF_slurm_submit(self): + def FF_slurm_submit(self, A=None, t=None, delay=4): # ---------------------------------- # ---------- Run FAST.Farm --------- @@ -1650,9 +1754,16 @@ def FF_slurm_submit(self): # Submit the script to SLURM fname = f'runFASTFarm_cond{cond}_case{case}_seed{seed}.sh' - sub_command = f"sbatch {fname}" + + options = '' + if A is not None: + options += f'-A {A} ' + if t is not None: + options += f'-t {t}' + + sub_command = f"sbatch {options} {fname}" print(f'Calling: {sub_command}') _ = subprocess.call(sub_command, cwd=self.path, shell=True) - time.sleep(4) # Sometimes the same job gets submitted twice. This gets around it. + time.sleep(delay) # Sometimes the same job gets submitted twice. This gets around it. From c8c8ac260d9dcab4229d33e04450d2ca7c0a0c5c Mon Sep 17 00:00:00 2001 From: Regis Thedin Date: Fri, 28 Apr 2023 10:28:18 -0600 Subject: [PATCH 076/124] FF: improve multi-node TurbSim SLURM scripts --- pyFAST/fastfarm/examples/SampleFiles/runAllHighBox.sh | 10 +++++++--- pyFAST/fastfarm/examples/SampleFiles/runAllLowBox.sh | 7 ++++--- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/pyFAST/fastfarm/examples/SampleFiles/runAllHighBox.sh b/pyFAST/fastfarm/examples/SampleFiles/runAllHighBox.sh index 0a36ced..1d58539 100644 --- a/pyFAST/fastfarm/examples/SampleFiles/runAllHighBox.sh +++ b/pyFAST/fastfarm/examples/SampleFiles/runAllHighBox.sh @@ -3,8 +3,9 @@ #SBATCH --output log.highBox #SBATCH --nodes=3 #SBATCH --ntasks-per-node=36 -#SBATCH --time=4-00 -#SBATCH --account=car +#SBATCH --time=4:00:00 +#SBATCH --mem=150G +#SBATCH --account=osw source $HOME/.bash_profile @@ -32,13 +33,16 @@ nSeeds=6 nTurbines=12 # ******************************************************************************** # +rampercpu=$((149000/36)) + for cond in ${condList[@]}; do for case in ${caseList[@]}; do for ((seed=0; seed<$nSeeds; seed++)); do for ((t=1; t<=$nTurbines; t++)); do dir=$(printf "%s/%s/%s/Seed_%01d/TurbSim" $basepath $cond $case $seed) echo "Submitting $dir/HighT$t.inp" - srun -n1 -N1 --exclusive $turbsimbin $dir/HighT$t.inp > $dir/log.hight$t.seed$seed.txt 2>&1 & + srun -n1 -N1 --exclusive --mem-per-cpu=$rampercpu $turbsimbin $dir/HighT$t.inp > $dir/log.hight$t.seed$seed.txt 2>&1 & + sleep 0.1 done done done diff --git a/pyFAST/fastfarm/examples/SampleFiles/runAllLowBox.sh b/pyFAST/fastfarm/examples/SampleFiles/runAllLowBox.sh index 6576d41..4b65c4c 100644 --- a/pyFAST/fastfarm/examples/SampleFiles/runAllLowBox.sh +++ b/pyFAST/fastfarm/examples/SampleFiles/runAllLowBox.sh @@ -3,8 +3,9 @@ #SBATCH --output log.lowBox #SBATCH --nodes=2 #SBATCH --ntasks-per-node=12 -#SBATCH --time=4-00 -#SBATCH --account=shellwind +#SBATCH --time=2-00 +#SBATCH --mem=150G +#SBATCH --account=osw source $HOME/.bash_profile @@ -39,7 +40,7 @@ for cond in ${condList[@]}; do for((seed=0; seed<$nSeeds; seed++)); do dir=$(printf "%s/%s/Seed_%01d" $basepath $cond $seed) echo "Submitting $dir/Low.inp in node $currNode" - srun -n1 -N1 --exclusive --nodelist=$currNode $turbsimbin $dir/Low.inp > $dir/log.low.seed$seed.txt & + srun -n1 -N1 --exclusive --nodelist=$currNode --mem-per-cpu=25000M $turbsimbin $dir/Low.inp > $dir/log.low.seed$seed.txt 2>&1 & done (( nodeToUse++ )) done From a578e37e9ac79d4a5577db793ced24560237eb78 Mon Sep 17 00:00:00 2001 From: Regis Thedin Date: Fri, 28 Apr 2023 12:07:24 -0600 Subject: [PATCH 077/124] FF: Allows automatic computation of high- and low-res boxes resolutions Uses the underlying AMR-Wind simulation class with a dummy grid to get the values. Lots of information is printed on the screen to help the user make the appropriate choice. --- pyFAST/fastfarm/FASTFarmCaseCreation.py | 112 ++++++++++++++++++++---- 1 file changed, 94 insertions(+), 18 deletions(-) diff --git a/pyFAST/fastfarm/FASTFarmCaseCreation.py b/pyFAST/fastfarm/FASTFarmCaseCreation.py index 0964347..2c805d6 100644 --- a/pyFAST/fastfarm/FASTFarmCaseCreation.py +++ b/pyFAST/fastfarm/FASTFarmCaseCreation.py @@ -58,7 +58,7 @@ def modifyProperty(fullfilename, entry, value): class FFCaseCreation: - def __init__(self, path, wts, cmax, fmax, Cmeander, tmax, zbot, vhub, shear, TIvalue, inflow_deg, dt_high_les, ds_high_les, extent_high, dt_low_les, ds_low_les, extent_low, ffbin, mod_wake=1, yaw_init=None, ADmodel=None, EDmodel=None, nSeeds=6, LESpath=None, sweepWakeSteering=False, sweepYawMisalignment=False, seedValues=None, refTurb_rot=0, verbose=0): + def __init__(self, path, wts, cmax, fmax, Cmeander, tmax, zbot, vhub, shear, TIvalue, inflow_deg, dt_high_les=None, ds_high_les=None, extent_high=None, dt_low_les=None, ds_low_les=None, extent_low=None, ffbin=None, mod_wake=1, yaw_init=None, ADmodel=None, EDmodel=None, nSeeds=6, LESpath=None, sweepWakeSteering=False, sweepYawMisalignment=False, seedValues=None, refTurb_rot=0, wake_mod=1, verbose=0): ''' Full setup of a FAST.Farm simulations, can create setups for LES- or TurbSim-driven scenarios. @@ -70,6 +70,8 @@ def __init__(self, path, wts, cmax, fmax, Cmeander, tmax, zbot, vhub, shear, TIv Full path of the FAST.Farm binary to be executed refTurb_rot: int Index of reference turbine which the rotation of the farm will occur. Default is 0, the first one. + wake_mod: int + Wake model to be used on the computation of high- and low-res boxes temporal and spatial resolutions ''' self.path = path @@ -101,6 +103,7 @@ def __init__(self, path, wts, cmax, fmax, Cmeander, tmax, zbot, vhub, shear, TIv self.seedValues = seedValues self.refTurb_rot = refTurb_rot self.verbose = verbose + self.wake_mod = wake_mod self.attempt = 1 @@ -181,16 +184,18 @@ def _checkInputs(self): for t in self.TIvalue: if t<0: raise ValueError(f'TI cannot be negative. Received {t}.') if t<1: raise ValueError(f'TI should be given in percentage (e.g. "10" for a 10% TI). Received {t}.') + + # Set domain extents defaults if needed + default_extent_low = [3,6,3,3,2] + default_extent_high = 0.6 + if self.extent_low is None: + self.extent_low = default_extent_low + if self.extent_high is None: + self.extent_high = default_extent_high - # Check the ds and dt for the high- and low-res boxes + # Check domain extents if not (np.array(self.extent_low)>=0).all(): raise ValueError(f'The array for low-res box extents should be given with positive values') - if self.dt_low_les%(self.dt_high_les-1e-15) > 1e-12: - raise ValueError(f'The temporal resolution dT_Low should be a multiple of dT_High') - if self.dt_low_les < self.dt_high_les: - raise ValueError(f'The temporal resolution dT_High should not be greater than dT_Low on the LES side') - if self.ds_low_les < self.ds_high_les: - raise ValueError(f'The grid resolution dS_High should not be greater than dS_Low on the LES side') if not isinstance(self.extent_high, (float,int)): raise ValueError(f'The extent_high should be a scalar') if self.extent_high<=0: @@ -198,8 +203,14 @@ def _checkInputs(self): # Check the FAST.Farm binary - if not os.path.isfile(self.ffbin): - raise ValueError (f'The FAST.Farm binary given does not appear to exist') + if self.ffbin is None: + self.ffbin = shutil.which('FAST.Farm') + if not self.ffbin: + raise ValueError(f'No FAST.Farm binary was given and none could be found in $PATH.') + if self.verbose>1: + print('WARNING: No FAST.Farm binary has been given. Using {self.ffbin}') + elif not os.path.isfile(self.ffbin): + raise ValueError (f'The FAST.Farm binary given does not exist.') # Check turbine conditions arrays for consistency @@ -223,7 +234,7 @@ def _checkInputs(self): if self.seedValues is None: self.seedValues = [2318573, 122299, 123456, 389432, -432443, 9849898] if len(self.seedValues) != self.nSeeds: - raise ValueError(f'Number of seeds is {self.nSeeds} but {len(self.seedValues)} seed values were given. ' + raise ValueError(f'Number of seeds is {self.nSeeds} but {len(self.seedValues)} seed values were given. '\ f'Adjust the seedValues array accordingly') # Check LES parameters @@ -234,21 +245,86 @@ def _checkInputs(self): self.inflowStr = 'LES' for p in self.LESpath: if not os.path.isdir(p): - raise ValueError (f'The path {p} does not exist') - - # Check the reference turbine for rotation - if self.refTurb_rot >= self.nTurbines: - raise ValueError(f'The index for the reference turbine for the farm to be rotated around is greater than the number of turbines') + raise ValueError (f'The LES path {p} does not exist') + # LES is requested, so domain limits must be given + if None in (self.dt_high_les, self.ds_high_les, self.dt_low_les, self.ds_low_les): + raise ValueError (f'An LES-driven case was requested, but one or more grid parameters were not given. '\ + 'Set `dt_high_les`, `ds_high_les`, `dt_low_les`, and `ds_low_les` based on your LES boxes.') + # Check the wake model (1:Polar; 2:Curl; 3:Cartesian) if self.mod_wake not in [1,2,3]: raise ValueError(f'Wake model `mod_wake` should be 1 (Polar), 2 (Curl), or 3 (Cartesian). Received {self.mod_wake}.') + + # Check the ds and dt for the high- and low-res boxes. If not given, call the + # AMR-Wind auxiliary function with dummy domain limits. + if None in (self.dt_high_les, self.ds_high_les, self.dt_low_les, self.ds_low_les): + wake_mod_str = ['','polar', 'curled', 'cartesian'] + print(f'WARNING: One or more temporal or spatial resolution for low- and high-res domains were not given.') + print(f' Estimated values for {wake_mod_str[self.wake_mod]} wake model shown below.') + self._determine_resolutions_from_dummy_amrwind_grid() + + # Check the domain extents + if self.dt_low_les%(self.dt_high_les-1e-15) > 1e-12: + raise ValueError(f'The temporal resolution dT_Low should be a multiple of dT_High') + if self.dt_low_les < self.dt_high_les: + raise ValueError(f'The temporal resolution dT_High should not be greater than dT_Low on the LES side') + if self.ds_low_les < self.ds_high_les: + raise ValueError(f'The grid resolution dS_High should not be greater than dS_Low on the LES side') + + + + # Check the reference turbine for rotation + if self.refTurb_rot >= self.nTurbines: + raise ValueError(f'The index for the reference turbine for the farm to be rotated around is greater than the number of turbines') + # Set aux variable self.templateFilesCreatedBool = False self.TSlowBoxFilesCreatedBool = False + + def _determine_resolutions_from_dummy_amrwind_grid(self): + + from pyFAST.fastfarm.AMRWindSimulation import AMRWindSimulation + + # Create values and keep variable names consistent across interfaces + dummy_dt = 0.1 + dummy_ds = 1 + prob_lo = (-10005, -10005, 0) # The 5 m offset is such that we + prob_hi = ( 10005, 10005, 1000) # have a cell center at (0,0) + n_cell = ((prob_hi[0]-prob_lo[0])/dummy_ds, + (prob_hi[1]-prob_lo[1])/dummy_ds, + (prob_hi[2]-prob_lo[2])/dummy_ds) + max_level = 0 + incflo_velocity_hh = (max(self.vhub), 0, 0) + buffer_lr = self.extent_low + buffer_hr = self.extent_high + + amr = AMRWindSimulation(self.wts, dummy_dt, prob_lo, prob_hi, + n_cell, max_level, incflo_velocity_hh, + buffer_lr = self.extent_low, + buffer_hr = self.extent_high, + wake_mod = self.wake_mod) + + print(f' High-resolution: ds: {amr.ds_hr} m, dt: {amr.dt_high_les} s') + print(f' Low-resolution: ds: {amr.ds_lr} m, dt: {amr.dt_low_les} s\n') + print(f'WARNING: If the above values are too fine or manual tuning is warranted, specify them manually.') + print(f' To do that, specify, e.g., `dt_high_les = {2*amr.dt_high_les}` to the call to `FFCaseCreation`.') + print(f' `ds_high_les = {2*amr.ds_high_les}`') + print(f' `dt_low_les = {2*amr.dt_low_les}`') + print(f' `ds_low_les = {2*amr.ds_low_les}`') + print(f' If the values above are okay, you can safely ignore this warning.\n') + + self.dt_high_les = amr.dt_high_les + self.ds_high_les = amr.dt_high_les + self.dt_low_les = amr.dt_low_les + self.ds_low_les = amr.dt_low_les + + + + def _create_dir_structure(self): # Create directory structure CondXX_*/CaseYY_*/Seed_Z/TurbSim; and CondXX_*/Seed_Y # Also saves the dir structure on array to write on SLURM script in the future. @@ -745,8 +821,8 @@ def _create_all_cond(self): if len(self.vhub)==len(self.shear) and len(self.shear)==len(self.TIvalue): self.nConditions = len(self.vhub) - if self.verbose>1: print(f'The length of vhub, shear, and TI are the same. Assuming each position is a condition.') - if self.verbose>0: print(f'Creating {self.nConditions} conditions') + if self.verbose>1: print(f'\nThe length of vhub, shear, and TI are the same. Assuming each position is a condition.', end='\r') + if self.verbose>0: print(f'\nCreating {self.nConditions} conditions') self.allCond = xr.Dataset({'vhub': (['cond'], self.vhub ), 'shear': (['cond'], self.shear ), From 331b48dae22f2aae562c11ed9c11fc039043f2ce Mon Sep 17 00:00:00 2001 From: Regis Thedin Date: Fri, 28 Apr 2023 13:00:42 -0600 Subject: [PATCH 078/124] FF: Add some documentation; code clean-up; update examples --- pyFAST/fastfarm/AMRWindSimulation.py | 16 +- pyFAST/fastfarm/FASTFarmCaseCreation.py | 184 ++++++++++++++---- .../examples/Ex3_FFarmCompleteSetup.py | 5 +- .../examples/Ex4_AMRWindSamplingSetup.py | 25 ++- 4 files changed, 168 insertions(+), 62 deletions(-) diff --git a/pyFAST/fastfarm/AMRWindSimulation.py b/pyFAST/fastfarm/AMRWindSimulation.py index b42998a..a171852 100644 --- a/pyFAST/fastfarm/AMRWindSimulation.py +++ b/pyFAST/fastfarm/AMRWindSimulation.py @@ -23,7 +23,7 @@ def __init__(self, wts:dict, buffer_hr = 0.6, ds_hr = None, ds_lr = None, dt_hr = None, dt_lr = None, - wake_mod = None): + mod_wake = None): ''' Values from the AMR-Wind input file Inputs: @@ -31,7 +31,7 @@ def __init__(self, wts:dict, * incflo_velocity_hh: velocity vector, specifically at hub height * buffer_lr: buffer for [xmin, xmax, ymin, ymax, zmax] in low-res box, in D * buffer_hr: buffer for all directions (constant) in high-res box, in D - * wake_mod: Wake formulations within FAST.Farm. 1:polar; 2:curl; 3:cartesian. + * mod_wake: Wake formulations within FAST.Farm. 1:polar; 2:curl; 3:cartesian. ''' # Process inputs self.wts = wts @@ -49,7 +49,7 @@ def __init__(self, wts:dict, self.ds_lr = ds_lr self.dt_hr = dt_hr self.dt_lr = dt_lr - self.wake_mod = wake_mod + self.mod_wake = mod_wake # Placeholder variables, to be calculated by FFCaseCreation self.output_frequency_lr = None @@ -77,7 +77,7 @@ def __init__(self, wts:dict, def __repr__(self): s = f'<{type(self).__name__} object>\n' s += f'Requested parameters:\n' - s += f' - Wake model: {self.wake_mod} (1:Polar; 2:Curl; 3:Cartesian)\n' + s += f' - Wake model: {self.mod_wake} (1:Polar; 2:Curl; 3:Cartesian)\n' s += f' - Extent of high-res boxes: {self.extent_high} D to each side\n' s += f' - Extent of low-res box: xmin={self.extent_low[0]} D, xmax={self.extent_low[1]} D, ymin={self.extent_low[2]} D, ymax={self.extent_low[3]} D, zmax={self.extent_low[4]} D\n' @@ -131,8 +131,8 @@ def _checkInputs(self): raise ValueError("y-component of prob_lo larger than y-component of prob_hi") if (self.prob_lo[2] >= self.prob_hi[2]): raise ValueError("z-component of prob_lo larger than z-component of prob_hi") - if self.wake_mod not in [1,2,3]: - raise ValueError (f'Wake_mod parameter can only be 1 (polar), 2 (curl), or 3 (cartesian). Received {self.wake_mod}.') + if self.mod_wake not in [1,2,3]: + raise ValueError (f'mod_wake parameter can only be 1 (polar), 2 (curl), or 3 (cartesian). Received {self.mod_wake}.') def _calc_simple_params(self): ''' @@ -211,10 +211,10 @@ def _calc_sampling_time(self): for turbkey in self.wts: self.cmax_min = min(cmax_min, self.wts[turbkey]['cmax']) - if self.wake_mod == 1: + if self.mod_wake == 1: self.dr = self.cmax_min dt_lr_max = cmeander_min * Dwake_min / (10 * self.vhub) - else: # wake_mod == 2 or 3 + else: # mod_wake == 2 or 3 self.dr = Dwake_min/10 dt_lr_max = self.dr / (2* self.vhub) diff --git a/pyFAST/fastfarm/FASTFarmCaseCreation.py b/pyFAST/fastfarm/FASTFarmCaseCreation.py index 2c805d6..0eb0049 100644 --- a/pyFAST/fastfarm/FASTFarmCaseCreation.py +++ b/pyFAST/fastfarm/FASTFarmCaseCreation.py @@ -56,29 +56,106 @@ def modifyProperty(fullfilename, entry, value): return + class FFCaseCreation: - def __init__(self, path, wts, cmax, fmax, Cmeander, tmax, zbot, vhub, shear, TIvalue, inflow_deg, dt_high_les=None, ds_high_les=None, extent_high=None, dt_low_les=None, ds_low_les=None, extent_low=None, ffbin=None, mod_wake=1, yaw_init=None, ADmodel=None, EDmodel=None, nSeeds=6, LESpath=None, sweepWakeSteering=False, sweepYawMisalignment=False, seedValues=None, refTurb_rot=0, wake_mod=1, verbose=0): + def __init__(self, + path, + wts, + tmax, + zbot, + vhub, + shear, + TIvalue, + inflow_deg, + dt_high_les = None, + ds_high_les = None, + extent_high = None, + dt_low_les = None, + ds_low_les = None, + extent_low = None, + ffbin = None, + mod_wake = 1, + yaw_init = None, + ADmodel = None, + EDmodel = None, + nSeeds = 6, + seedValues = None, + LESpath = None, + sweepWakeSteering = False, + sweepYawMisalignment = False, + refTurb_rot = 0, + verbose = 0): ''' Full setup of a FAST.Farm simulations, can create setups for LES- or TurbSim-driven scenarios. + Inputs + ------ + path: str + Full path for the target case directory + wts: dictionary + Wind farm layout and turbine parameters in dictionary form + tmax: scalar + Max simulation time given in seconds + vhub: list of scalars or single scalar + Wind speeds at hub height to sweep on. Accepts a list or single value + shear: list of scalars or single scalar + Shear values (power-law exponents) to sweep on. Accepts a list or single value + TIvalue: list of scalars or single scalar + TI values at hub height to sweep on. Accepts a list or single value + inflow_deg: list of scalars or single scalar + Inflow angles to sweep on. Accepts a list or single value + dt_high_les: scalar + Time step of the desired high-resolution box. If LES boxes given, should + match LES box; otherwise desired TurbSim boxes. Default values as given in the + modeling guidances are used if none is given + ds_high_les: scalar + Grid resolution of the desired high-resolution box. If LES boxes given, should + match LES box; otherwise desired TurbSim boxes. Default values as given in the + modeling guidances are used if none is given + dt_low_les: scalar + Time step of the desired low-resolution box. If LES boxes given, should match + match LES box; otherwise desired TurbSim boxes. Default values as given in the + modeling guidances are used if none is given + ds_low_les: scalar + Grid resolution of the desired low-resolution box. If LES boxes given, should + match LES box; otherwise desired TurbSim boxes. Default values as given in the + modeling guidances are used if none is given + ff_bin: str + Full path of the FAST.Farm binary to be used. If not specified, the one available + in the $PATH will be used + mod_wake: int + Wake model to be used on the computation of high- and low-res boxes temporal and + spatial resolutions if those are not specified + yaw_init: list of scalars or single scalar + List of yaw to sweep on. Given as a 2-D array if len(inflow_deg)>1. One row of yaw + per wind direction given in inflow_deg. Each row has nTurbines values + ADmodel: list of strings + List of AeroDyn/AeroDisk models to use for each case + EDmodel: list of strings + List of ElastoDym/SimplifiedElastoDyn models to use for each case + nSeeds: int + Number of seeds used for TurbSim simulations. If changing this value, give seedValues + seedValues: list of int + Seed value for each seed of requested TurbSim simulations if nSeeds!=6 LESpath: str or list of strings Full path of the LES data, if driven by LES. If None, the setup will be for TurbSim inflow. LESpath can be a single path, or a list of paths of the same length as the sweep in conditions. For example, if TIvalue=[8,10,12], then LESpath can be 3 paths, related to each condition. - ffbin: str - Full path of the FAST.Farm binary to be executed + sweepWakeSteering: bool + Whether or not to perform a sweep with wake steering + sweepYawMisalignment: bool + Whether or not to perform a sweep with and without yaw misalignment perturbations refTurb_rot: int Index of reference turbine which the rotation of the farm will occur. Default is 0, the first one. - wake_mod: int - Wake model to be used on the computation of high- and low-res boxes temporal and spatial resolutions + Not fully tested. + verbose: int + Verbosity level, given as integers <5 + ''' self.path = path self.wts = wts - self.cmax = cmax - self.fmax = fmax - self.Cmeander = Cmeander self.tmax = tmax self.zbot = zbot self.vhub = vhub @@ -103,7 +180,7 @@ def __init__(self, path, wts, cmax, fmax, Cmeander, tmax, zbot, vhub, shear, TIv self.seedValues = seedValues self.refTurb_rot = refTurb_rot self.verbose = verbose - self.wake_mod = wake_mod + self.mod_wake = mod_wake self.attempt = 1 @@ -134,23 +211,36 @@ def _checkInputs(self): os.makedirs(self.path) # Check the wind turbine dict - if not isinstance(self.wts,dict): - raise ValueError (f'`wts` needs to be a dictionary with the following entries for each turbine: x, y, z, D, zhub.') + if not isinstance(self.wts,dict): + raise ValueError (f'`wts` needs to be a dictionary with the following entries for each turbine: x, y, z, D, zhub, cmax, fmax, Cmeander.') self.nTurbines = len(self.wts) self.D = self.wts[0]['D'] self.zhub = self.wts[0]['zhub'] + self.cmax = self.wts[0]['cmax'] + self.fmax = self.wts[0]['fmax'] + self.Cmeander = self.wts[0]['Cmeander'] # Check values of each turbine for t in range(self.nTurbines): - t_x = self.wts[t]['x'] - t_y = self.wts[t]['y'] - t_z = self.wts[t]['z'] - t_D = self.wts[t]['D'] - t_zhub = self.wts[t]['zhub'] + t_x = self.wts[t]['x'] + t_y = self.wts[t]['y'] + t_z = self.wts[t]['z'] + t_D = self.wts[t]['D'] + t_zhub = self.wts[t]['zhub'] + t_cmax = self.wts[t]['cmax'] + t_fmax = self.wts[t]['fmax'] + t_Cmeander = self.wts[t]['Cmeander'] if t_D != self.D: raise ValueError(f'Different turbines are not currently supported. Turbine {t+1} has a different diamenter.') if t_zhub != self.zhub: raise ValueError(f'Different turbines are not currently supported. Turbine {t+1} has a different hub height.') + if t_cmax != self.cmax: + raise ValueError(f'Different turbines are not currently supported. Turbine {t+1} has a different max chord.') + if t_fmax != self.fmax: + raise ValueError(f'Different turbines are not currently supported. Turbine {t+1} has a different max excitation frequency.') + if t_Cmeander != self.Cmeander: + raise ValueError(f'Different turbines are not currently supported. Turbine {t+1} has a different meandering constant.') + if not isinstance(t_x,(float,int)): raise ValueError (f'The `x` value for the turbine {t+1} should be an integer or float. Received {t_x}.') if not isinstance(t_y,(float,int)): @@ -187,7 +277,7 @@ def _checkInputs(self): # Set domain extents defaults if needed default_extent_low = [3,6,3,3,2] - default_extent_high = 0.6 + default_extent_high = 1.2 if self.extent_low is None: self.extent_low = default_extent_low if self.extent_high is None: @@ -260,9 +350,9 @@ def _checkInputs(self): # Check the ds and dt for the high- and low-res boxes. If not given, call the # AMR-Wind auxiliary function with dummy domain limits. if None in (self.dt_high_les, self.ds_high_les, self.dt_low_les, self.ds_low_les): - wake_mod_str = ['','polar', 'curled', 'cartesian'] + mod_wake_str = ['','polar', 'curled', 'cartesian'] print(f'WARNING: One or more temporal or spatial resolution for low- and high-res domains were not given.') - print(f' Estimated values for {wake_mod_str[self.wake_mod]} wake model shown below.') + print(f' Estimated values for {mod_wake_str[self.mod_wake]} wake model shown below.') self._determine_resolutions_from_dummy_amrwind_grid() # Check the domain extents @@ -306,7 +396,7 @@ def _determine_resolutions_from_dummy_amrwind_grid(self): n_cell, max_level, incflo_velocity_hh, buffer_lr = self.extent_low, buffer_hr = self.extent_high, - wake_mod = self.wake_mod) + mod_wake = self.mod_wake) print(f' High-resolution: ds: {amr.ds_hr} m, dt: {amr.dt_high_les} s') print(f' Low-resolution: ds: {amr.ds_lr} m, dt: {amr.dt_low_les} s\n') @@ -411,9 +501,8 @@ def copyTurbineFilesForEachCase(self, writeFiles=True): # Update parameters to be changed in the *Dyn files self.HydroDynFile['WaveHs'] = self.bins.sel(wspd=Vhub_, method='nearest').WaveHs.values self.HydroDynFile['WaveTp'] = self.bins.sel(wspd=Vhub_, method='nearest').WaveTp.values - self.HydroDynFile['WvHiCOffD'] = 2.0*np.pi/self.HydroDynFile['WaveTp'] # check with JJ + self.HydroDynFile['WvHiCOffD'] = 2.0*np.pi/self.HydroDynFile['WaveTp'] self.HydroDynFile['WvLowCOffS'] = 2.0*np.pi/self.HydroDynFile['WaveTp'] - # !!! JJ: do i need to change the first 3 values of HD? can they be 'default'? self.ElastoDynFile['RotSpeed'] = self.bins.sel(wspd=Vhub_, method='nearest').RotSpeed.values self.ElastoDynFile['BlPitch(1)'] = self.bins.sel(wspd=Vhub_, method='nearest').BlPitch.values @@ -473,7 +562,7 @@ def copyTurbineFilesForEachCase(self, writeFiles=True): self.ElastoDynFile['NacYaw'] = yaw_deg_ + yaw_mis_deg_ self.ElastoDynFile['BldFile1'] = self.ElastoDynFile['BldFile2'] = self.ElastoDynFile['BldFile3'] = f'"{self.bladefilename}"' self.ElastoDynFile['TwrFile'] = f'"{self.towerfilename}"' - self.ElastoDynFile['Azimuth'] = round(np.random.uniform(low=0, high=360)) # start at a random value #JJ is this needed? + self.ElastoDynFile['Azimuth'] = round(np.random.uniform(low=0, high=360)) # start at a random value if writeFiles: if t==0: shutilcopy2_untilSuccessful(os.path.join(self.templatePath,self.bladefilename), os.path.join(currPath,self.bladefilename)) if t==0: shutilcopy2_untilSuccessful(os.path.join(self.templatePath,self.towerfilename), os.path.join(currPath,self.towerfilename)) @@ -600,7 +689,26 @@ def _were_all_turbine_files_copied(self): - def setTemplateFilename(self, templatePath=None, EDfilename=None, SEDfilename=None, HDfilename=None, SrvDfilename=None, ADfilename=None, ADskfilename=None, SubDfilename=None, IWfilename=None, BDfilepath=None, bladefilename=None, towerfilename=None, turbfilename=None, libdisconfilepath=None, controllerInputfilename=None, coeffTablefilename=None, turbsimLowfilepath=None, turbsimHighfilepath=None, FFfilename=None): + def setTemplateFilename(self, + templatePath=None, + EDfilename=None, + SEDfilename=None, + HDfilename=None, + SrvDfilename=None, + ADfilename=None, + ADskfilename=None, + SubDfilename=None, + IWfilename=None, + BDfilepath=None, + bladefilename=None, + towerfilename=None, + turbfilename=None, + libdisconfilepath=None, + controllerInputfilename=None, + coeffTablefilename=None, + turbsimLowfilepath=None, + turbsimHighfilepath=None, + FFfilename=None): ''' *filename: str @@ -802,20 +910,16 @@ def _check_and_open(f): self.InflowWindFile = _check_and_open(self.IWfilepath) - - def print_template_files(self): raise NotImplementedError (f'Placeholder. Not implemented.') def createAuxArrays(self): - self._rotate_wts() self._create_all_cond() self._create_all_cases() - def _create_all_cond(self): if len(self.vhub)==len(self.shear) and len(self.shear)==len(self.TIvalue): @@ -960,16 +1064,16 @@ def _rotate_wts(self): self.wts_rot_ds = pd.DataFrame.from_dict(wts_rot, orient='index').to_xarray().rename({'level_0':'inflow_deg','level_1':'turbine'}) + def _setRotorParameters(self): - if self.D == 220: # 12 MW turbine self.bins = xr.Dataset({'WaveHs': (['wspd'], [ 1.429, 1.429]), # 1.429 comes from Matt's hydrodyn input file 'WaveTp': (['wspd'], [ 7.073, 7.073]), # 7.073 comes from Matt's hydrodyn input file 'RotSpeed': (['wspd'], [ 4.0, 4.0]), # 4 rpm comes from Matt's ED input file 'BlPitch': (['wspd'], [ 0.0, 0.0]), # 0 deg comes from Matt's ED input file - #'WvHiCOffD': (['wspd'], [0, 0]), # 2nd order wave info. Unused for now # JJ should we have this? - #'WvLowCOffS': (['wspd'], [0, 0]), # 2nd order wave info. Unused for now # JJ: ditto + #'WvHiCOffD': (['wspd'], [0, 0]), # 2nd order wave info. Unused for now + #'WvLowCOffS': (['wspd'], [0, 0]), # 2nd order wave info. Unused for now }, coords={'wspd': [10, 15]} ) # 15 m/s is 'else', since method='nearest' is used on the variable `bins` elif self.D == 240: # IEA 15 MW @@ -1068,6 +1172,7 @@ def TS_low_slurm_prepare(self, slurmfilepath): if self.nSeeds != 6: print(f'--- WARNING: The memory-per-cpu on the low-res boxes SLURM script is configured for 6 seeds, not {self.nSeeds}.') + def TS_low_slurm_submit(self): # --------------------------------- # ----- Run turbSim Low boxes ----- @@ -1423,6 +1528,7 @@ def FF_setup(self, outlistFF=None, **kwargs): self._FF_setup_TS(**kwargs) + def _FF_setup_LES(self, seedsToKeep=1): self.seedsToKeep = seedsToKeep @@ -1439,7 +1545,6 @@ def _FF_setup_LES(self, seedsToKeep=1): currpath = os.path.join(self.path, self.condDirList[cond], self.caseDirList[case], f'Seed_{seed}') if os.path.isdir(currpath): shutil.rmtree(currpath) - # Create symlinks for the processed-and-renamed vtk files LESboxesDirName = 'LESboxes' @@ -1472,7 +1577,6 @@ def _FF_setup_LES(self, seedsToKeep=1): print(f'Directory {dst} already exists. Skipping symlink.') - # Loops on all conditions/cases and cases for FAST.Farm for cond in range(self.nConditions): for case in range(self.nCases): @@ -1639,8 +1743,8 @@ def _FF_setup_TS(self): def _getBoxesParamsForFF(self, lowbts, highbts, dt_low_desired, D, HubHt, xWT, yt): # Get mean wind speeds at the half height location (advection speed) - _, meanU_High = highbts.midValues() # !!!!!!!!!!!!!!!! JJ: does it make sense to get both? the meanu for low will be a lot higher than vhub, - _, meanU_Low = lowbts.midValues() # !!!!!!!!!!!!!!!! JJ: and the meanu for high can be a bit higher than vhub + _, meanU_High = highbts.midValues() + _, meanU_Low = lowbts.midValues() dT_High = np.round(highbts.dt, 4) # dX_High can sometimes be too high. So get the closest to the cmax, but multiple of what should have been @@ -1664,7 +1768,7 @@ def _getBoxesParamsForFF(self, lowbts, highbts, dt_low_desired, D, HubHt, xWT, y LZ_Low = lowbts.z[-1]-lowbts.z[0] LT_Low = np.round(lowbts.t[-1]-lowbts.t[0], 4) - X0_Low = np.floor( (min(xWT) - self.extent_low[0]*D ))# - dX_Low)) # # JJ!!!!!!! # removing EB's -dX_Low from the X0_Low specification + X0_Low = np.floor( (min(xWT) - self.extent_low[0]*D )) X0_Low = getMultipleOf(X0_Low, multipleof=dX_Low) Y0_Low = np.floor( -LY_Low/2 ) # Starting on integer value for aesthetics Z0_Low = lowbts.z[0] # we start at lowest to include tower @@ -1673,7 +1777,7 @@ def _getBoxesParamsForFF(self, lowbts, highbts, dt_low_desired, D, HubHt, xWT, y LX_Low = XMax_Low-X0_Low nX_Low = int(np.ceil(LX_Low/dX_Low)+1) - nY_Low = len(lowbts.y) # !!!!!!! JJ: different from what EB has + nY_Low = len(lowbts.y) nZ_Low = len(lowbts.z) assert nY_Low == int(np.ceil(LY_Low/dY_Low)+1) @@ -1689,10 +1793,6 @@ def _getBoxesParamsForFF(self, lowbts, highbts, dt_low_desired, D, HubHt, xWT, y LY_High = highbts.y[-1]-highbts.y[0] LZ_High = highbts.z[-1]-highbts.z[0] LT_High = np.round(highbts.t[-1]-highbts.t[0], 4) - # !!!!!!!!!!!!!!!!! JJ: EB had these below. Why is that? - #Max_High = HubHt+extent_high*D # !!!!!!!!!!!!!!!!!! actual hh or midpoint? - #LY_High = min(LY_Box, extent_YZ*D ) # Bounding to not exceed the box dimension - #LZ_High = min(LZ_Box, ZMax_High-Z0_High) # Bounding to not exceed the box dimension nX_High = int(np.ceil(LX_High/dX_High) + 1) # plus 1 from the guidance nY_High = len(highbts.y) @@ -1772,8 +1872,6 @@ def _getBoxesParamsForFF(self, lowbts, highbts, dt_low_desired, D, HubHt, xWT, y - - def FF_slurm_prepare(self, slurmfilepath): # ---------------------------------------------- # ----- Prepare SLURM script for FAST.Farm ----- diff --git a/pyFAST/fastfarm/examples/Ex3_FFarmCompleteSetup.py b/pyFAST/fastfarm/examples/Ex3_FFarmCompleteSetup.py index a53418d..7ff6ad2 100644 --- a/pyFAST/fastfarm/examples/Ex3_FFarmCompleteSetup.py +++ b/pyFAST/fastfarm/examples/Ex3_FFarmCompleteSetup.py @@ -64,10 +64,11 @@ def main(): # ----------- Low- and high-res boxes parameters # Should match LES if comparisons are to be made; otherwise, set desired values + # For an automatic computation of such parameters, omit them from the call to FFCaseCreation # High-res boxes settings dt_high_les = 0.6 # sampling frequency of high-res files ds_high_les = 10.0 # dx, dy, dz that you want these high-res files at - extent_high = 1.2 # high-res box extent in y and x for each turbine, in D. + extent_high = 1.2 # high-res box extent in y and x for each turbine, in D. # Low-res boxes settings dt_low_les = 3 # sampling frequency of low-res files ds_low_les = 20.0 # dx, dy, dz of low-res files @@ -121,7 +122,7 @@ def main(): # Initial setup - case = FFCaseCreation(path, wts, cmax, fmax, Cmeander, tmax, zbot, vhub, shear, + case = FFCaseCreation(path, wts, tmax, zbot, vhub, shear, TIvalue, inflow_deg, dt_high_les, ds_high_les, extent_high, dt_low_les, ds_low_les, extent_low, ffbin, mod_wake, LESpath=LESpath, verbose=1) diff --git a/pyFAST/fastfarm/examples/Ex4_AMRWindSamplingSetup.py b/pyFAST/fastfarm/examples/Ex4_AMRWindSamplingSetup.py index 8b99712..58b9ff7 100644 --- a/pyFAST/fastfarm/examples/Ex4_AMRWindSamplingSetup.py +++ b/pyFAST/fastfarm/examples/Ex4_AMRWindSamplingSetup.py @@ -11,9 +11,9 @@ def main(): # ----------- Wind farm wts = { - 0 :{'x':1280.0, 'y':2560, 'z':0.0, 'D':126.9, 'zhub':86.5, 'cmax':5, 'fmax':10/6, 'Cmeander':1.9, 'name':'T0'}, - 1 :{'x':1280.0, 'y':3200, 'z':0.0, 'D':126.9, 'zhub':86.5, 'cmax':5, 'fmax':10/6, 'Cmeander':1.9, 'name':'T1'}, - 2 :{'x':1280.0, 'y':3840, 'z':0.0, 'D':126.9, 'zhub':86.5, 'cmax':5, 'fmax':10/6, 'Cmeander':1.9, 'name':'T2'}, + 0 :{'x':1280.0, 'y':2560, 'z':0.0, 'D':126.9, 'zhub':86.5, 'cmax':5, 'fmax':10/6, 'Cmeander':1.9, 'name':'T0'}, + 1 :{'x':1280.0, 'y':3200, 'z':0.0, 'D':126.9, 'zhub':86.5, 'cmax':5, 'fmax':10/6, 'Cmeander':1.9, 'name':'T1'}, + 2 :{'x':1280.0, 'y':3840, 'z':0.0, 'D':126.9, 'zhub':86.5, 'cmax':5, 'fmax':10/6, 'Cmeander':1.9, 'name':'T2'}, } # ----------- AMR-Wind parameters @@ -26,8 +26,8 @@ def main(): incflo_velocity_hh = (0.0, 10.0, 0.0) # Hub-height velocity postproc_name = 'sampling' - # ----------- I/O - outdir = '/Users/orybchuk/Research/OpenFAST-python-toolbox' + # ----------- Wake model (1: Polar; 2: Curl; 3: Cartesian) + mod_wake = 1 # ----------------------------------------------------------------------------- # END OF USER INPUT @@ -36,10 +36,17 @@ def main(): # Initial setup amr = AMRWindSimulation(wts, fixed_dt, prob_lo, prob_hi, n_cell, max_level, incflo_velocity_hh, - postproc_name) + postproc_name, + mod_wake = wake_mod) - # Write out sampling parameters - amr.write_sampling_params(outdir) + # You can print the complete instance by simply + print(amr) + + # The output of AMRWindSimulation can be directly used as input to FFCaseCreation: + # FFCaseCreation(..., dt_high_les=amr.dt_high_les, ds_high_les=amr.ds_high_les, + # dt_low_les=amr.dt_low_les, ds_low_les=amr.ds_low_les, + # extent_high=amr.extent_high, extent_low=amr.extent_low, + # ...) if __name__ == '__main__': - main() \ No newline at end of file + main() From 560e388faf17245c63ac73cb03839d44186ef9f7 Mon Sep 17 00:00:00 2001 From: Regis Thedin Date: Wed, 10 May 2023 09:01:32 -0600 Subject: [PATCH 079/124] Add xarray package to installation --- setup.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 43711c1..e0c83f3 100644 --- a/setup.py +++ b/setup.py @@ -38,7 +38,8 @@ "scipy", "sympy", "openpyxl", - "pytest" + "pytest", + "xarray" ], test_suite="pytest", tests_require=["pytest"], From 5ec44136afb037088de00158de877c5fd56318ce Mon Sep 17 00:00:00 2001 From: Regis Thedin Date: Tue, 30 May 2023 13:25:26 -0600 Subject: [PATCH 080/124] FF: Add `__repr__` and minor tweaks to slurm aux methods --- pyFAST/fastfarm/FASTFarmCaseCreation.py | 110 +++++++++++++++++++++--- 1 file changed, 99 insertions(+), 11 deletions(-) diff --git a/pyFAST/fastfarm/FASTFarmCaseCreation.py b/pyFAST/fastfarm/FASTFarmCaseCreation.py index 0eb0049..ebc3752 100644 --- a/pyFAST/fastfarm/FASTFarmCaseCreation.py +++ b/pyFAST/fastfarm/FASTFarmCaseCreation.py @@ -204,6 +204,72 @@ def __init__(self, if self.verbose>0: print(f'Creating directory structure and copying files... Done.') + def __repr__(self): + s = f'Requested parameters:\n' + s += f' - Case path: {self.path}\n' + s += f' - Wake model: {self.mod_wake} (1:Polar; 2:Curl; 3:Cartesian)\n' + if self.LESpath is None: + s += f' - Number of TurbSim seeds: {self.nSeeds}\n' + s += f' - End time: {self.tmax} s\n' + s += f'Requested farm:\n' + s += f' - Number of turbines: {self.nTurbines}\n' + s += f' - Diameter: {self.D} m\n' + s += f' - Hub height: {self.zhub} m\n' + s += f' - Max chord: {self.cmax} m\n' + s += f' - Max excitation freq: {self.fmax:.3f} Hz\n' + s += f' - Meandering constant: {self.Cmeander}\n' + s += f'Requested sweeps:\n' + s += f' - Wind speeds at hub height (m/s): {self.vhub}\n' + s += f' - Shear exponent: {self.shear}\n' + s += f' - TI (%): {self.TIvalue}\n' + s += f'\nCase details:\n' + s += f' - Number of conditions: {self.nConditions}\n' + for c in self.condDirList: + s += f" {c}\n" + s += f' - Number of cases for each condition: {self.nCases}\n' + if self.nCases < 11: + for c in self.caseDirList: + s += f" {c}\n" + else: + for c in self.caseDirList[:5]: + s += f" {c}\n" + s += f" ...\n" + for c in self.caseDirList[-5:]: + s += f" {c}\n" + s += f"\n\n" + + + if self.LESpath is None: + s += f'Turbulence boxes: TurbSim\n' + s += f'TurbSim turbulence boxes details:\n' + else: + s += f'Turbulence boxes: LES\n' + s += f'LES turbulence boxes details:\n' + s += f' Path: {self.LESpath}\n' + + + if self.TSlowBoxFilesCreatedBool or self.LESpath is not None: + s += f' Low-resolution domain: \n' + s += f' - ds low: {self.ds_low_les} m\n' + s += f' - dt low: {self.dt_low_les} s\n' + s += f' - Extent of low-res box (in D): xmin = {self.extent_low[0]}, xmax = {self.extent_low[1]}, ' + s += f'ymin = {self.extent_low[2]}, ymax = {self.extent_low[3]}, zmax = {self.extent_low[4]}\n' + else: + s += f'Low-res boxes not created yet.\n' + + + if self.TShighBoxFilesCreatedBool or self.LESpath is not None: + s += f' High-resolution domain: \n' + s += f' - ds high: {self.ds_high_les} m\n' + s += f' - dt high: {self.dt_high_les} s\n' + s += f' - Extent of high-res boxes: {self.extent_high} D total\n' + else: + s += f'High-res boxes not created yet.\n' + s += f"\n" + + return s + + def _checkInputs(self): # Create case path is doesn't exist @@ -277,7 +343,7 @@ def _checkInputs(self): # Set domain extents defaults if needed default_extent_low = [3,6,3,3,2] - default_extent_high = 1.2 + default_extent_high = 1.2 # total extent, half of that to each side. if self.extent_low is None: self.extent_low = default_extent_low if self.extent_high is None: @@ -290,6 +356,9 @@ def _checkInputs(self): raise ValueError(f'The extent_high should be a scalar') if self.extent_high<=0: raise ValueError(f'The extent of high boxes should be positive') + if self.extent_high<1: + raise ValueError(f'The extent of high boxes is not enough to cover the rotor diameter. '\ + 'The extent high is given as the total extent, and it needs to be greater than 1.') # Check the FAST.Farm binary @@ -370,8 +439,9 @@ def _checkInputs(self): raise ValueError(f'The index for the reference turbine for the farm to be rotated around is greater than the number of turbines') # Set aux variable - self.templateFilesCreatedBool = False - self.TSlowBoxFilesCreatedBool = False + self.templateFilesCreatedBool = False + self.TSlowBoxFilesCreatedBool = False + self.TShighBoxFilesCreatedBool = False @@ -1173,12 +1243,20 @@ def TS_low_slurm_prepare(self, slurmfilepath): print(f'--- WARNING: The memory-per-cpu on the low-res boxes SLURM script is configured for 6 seeds, not {self.nSeeds}.') - def TS_low_slurm_submit(self): + def TS_low_slurm_submit(self, qos='normal', A=None, t=None): # --------------------------------- # ----- Run turbSim Low boxes ----- # --------------------------------- # Submit the script to SLURM - _ = subprocess.call(f'sbatch {self.slurmfilename_low}', cwd=self.path, shell=True) + options = f"--qos='{qos}' " + if A is not None: + options += f'-A {A} ' + if t is not None: + options += f'-t {t} ' + + sub_command = f"sbatch {options}{self.slurmfilename_low}" + print(f'Calling: {sub_command}') + _ = subprocess.call(sub_command, cwd=self.path, shell=True) def TS_low_createSymlinks(self): @@ -1366,6 +1444,8 @@ def TS_high_setup(self, writeFiles=True): # Let's remove the original file os.remove(os.path.join(seedPath, f'HighT{t+1}_stillToBeModified.inp')) + + self.TShighBoxFilesCreatedBool = True def TS_high_slurm_prepare(self, slurmfilepath): @@ -1403,12 +1483,20 @@ def TS_high_slurm_prepare(self, slurmfilepath): - def TS_high_slurm_submit(self): + def TS_high_slurm_submit(self, qos='normal', A=None, t=None): # ---------------------------------- # ----- Run turbSim High boxes ----- # ---------------------------------- # Submit the script to SLURM - _ = subprocess.call(f'sbatch {self.slurmfilename_high}', cwd=self.path, shell=True) + options = f"--qos='{qos}' " + if A is not None: + options += f'-A {A} ' + if t is not None: + options += f'-t {t} ' + + sub_command = f"sbatch {options}{self.slurmfilename_high}" + print(f'Calling: {sub_command}') + _ = subprocess.call(sub_command, cwd=self.path, shell=True) def TS_high_create_symlink(self): @@ -1914,7 +2002,7 @@ def FF_slurm_prepare(self, slurmfilepath): - def FF_slurm_submit(self, A=None, t=None, delay=4): + def FF_slurm_submit(self, qos='normal', A=None, t=None, delay=4): # ---------------------------------- # ---------- Run FAST.Farm --------- @@ -1929,13 +2017,13 @@ def FF_slurm_submit(self, A=None, t=None, delay=4): # Submit the script to SLURM fname = f'runFASTFarm_cond{cond}_case{case}_seed{seed}.sh' - options = '' + options = f"--qos='{qos}' " if A is not None: options += f'-A {A} ' if t is not None: - options += f'-t {t}' + options += f'-t {t} ' - sub_command = f"sbatch {options} {fname}" + sub_command = f"sbatch {options}{fname}" print(f'Calling: {sub_command}') _ = subprocess.call(sub_command, cwd=self.path, shell=True) time.sleep(delay) # Sometimes the same job gets submitted twice. This gets around it. From ca8d4f19b1ccf33ccfaf78ec0a53bcd5f7076314 Mon Sep 17 00:00:00 2001 From: Regis Thedin Date: Tue, 30 May 2023 13:42:43 -0600 Subject: [PATCH 081/124] FF: fix bugs with optional input files --- pyFAST/fastfarm/FASTFarmCaseCreation.py | 47 ++++++++++++++----------- 1 file changed, 26 insertions(+), 21 deletions(-) diff --git a/pyFAST/fastfarm/FASTFarmCaseCreation.py b/pyFAST/fastfarm/FASTFarmCaseCreation.py index ebc3752..04ad2ad 100644 --- a/pyFAST/fastfarm/FASTFarmCaseCreation.py +++ b/pyFAST/fastfarm/FASTFarmCaseCreation.py @@ -568,24 +568,19 @@ def copyTurbineFilesForEachCase(self, writeFiles=True): # Recover info about the current CondXX_*/CaseYY_* Vhub_ = self.allCond.sel(cond=cond)['vhub'].values - # Update parameters to be changed in the *Dyn files - self.HydroDynFile['WaveHs'] = self.bins.sel(wspd=Vhub_, method='nearest').WaveHs.values - self.HydroDynFile['WaveTp'] = self.bins.sel(wspd=Vhub_, method='nearest').WaveTp.values - self.HydroDynFile['WvHiCOffD'] = 2.0*np.pi/self.HydroDynFile['WaveTp'] - self.HydroDynFile['WvLowCOffS'] = 2.0*np.pi/self.HydroDynFile['WaveTp'] - - self.ElastoDynFile['RotSpeed'] = self.bins.sel(wspd=Vhub_, method='nearest').RotSpeed.values - self.ElastoDynFile['BlPitch(1)'] = self.bins.sel(wspd=Vhub_, method='nearest').BlPitch.values - self.ElastoDynFile['BlPitch(2)'] = self.bins.sel(wspd=Vhub_, method='nearest').BlPitch.values - self.ElastoDynFile['BlPitch(3)'] = self.bins.sel(wspd=Vhub_, method='nearest').BlPitch.values - - self.SElastoDynFile['RotSpeed'] = self.bins.sel(wspd=Vhub_, method='nearest').RotSpeed.values - self.SElastoDynFile['BlPitch'] = self.bins.sel(wspd=Vhub_, method='nearest').BlPitch.values + # Update parameters to be changed in the HydroDyn files + if self.HydroDynFile != 'unused': + self.HydroDynFile['WaveHs'] = self.bins.sel(wspd=Vhub_, method='nearest').WaveHs.values + self.HydroDynFile['WaveTp'] = self.bins.sel(wspd=Vhub_, method='nearest').WaveTp.values + self.HydroDynFile['WvHiCOffD'] = 2.0*np.pi/self.HydroDynFile['WaveTp'] + self.HydroDynFile['WvLowCOffS'] = 2.0*np.pi/self.HydroDynFile['WaveTp'] + if writeFiles: + self.HydroDynFile.write(os.path.join(currPath, self.HDfilename)) - # Write updated DISCON and *Dyn files. + # Write updated DISCON if writeFiles: - self.HydroDynFile.write(os.path.join(currPath, self.HDfilename)) - shutilcopy2_untilSuccessful(os.path.join(self.templatePath,self.controllerInputfilename), os.path.join(currPath,self.controllerInputfilename)) + shutilcopy2_untilSuccessful(os.path.join(self.templatePath,self.controllerInputfilename), + os.path.join(currPath,self.controllerInputfilename)) # Depending on the controller, the controller input file might need to be in the same level as the .fstf input file. # The ideal solution would be to give the full path to the controller input file, but we may not have control over @@ -629,6 +624,11 @@ def copyTurbineFilesForEachCase(self, writeFiles=True): if EDmodel_ == 'FED': # Update each turbine's ElastoDyn + self.ElastoDynFile['RotSpeed'] = self.bins.sel(wspd=Vhub_, method='nearest').RotSpeed.values + self.ElastoDynFile['BlPitch(1)'] = self.bins.sel(wspd=Vhub_, method='nearest').BlPitch.values + self.ElastoDynFile['BlPitch(2)'] = self.bins.sel(wspd=Vhub_, method='nearest').BlPitch.values + self.ElastoDynFile['BlPitch(3)'] = self.bins.sel(wspd=Vhub_, method='nearest').BlPitch.values + self.ElastoDynFile['NacYaw'] = yaw_deg_ + yaw_mis_deg_ self.ElastoDynFile['BldFile1'] = self.ElastoDynFile['BldFile2'] = self.ElastoDynFile['BldFile3'] = f'"{self.bladefilename}"' self.ElastoDynFile['TwrFile'] = f'"{self.towerfilename}"' @@ -640,6 +640,9 @@ def copyTurbineFilesForEachCase(self, writeFiles=True): elif EDmodel_ == 'SED': # Update each turbine's Simplified ElastoDyn + self.SElastoDynFile['RotSpeed'] = self.bins.sel(wspd=Vhub_, method='nearest').RotSpeed.values + self.SElastoDynFile['BlPitch'] = self.bins.sel(wspd=Vhub_, method='nearest').BlPitch.values + self.SElastoDynFile['BlPitch'] = self.bins.sel(wspd=Vhub_, method='nearest').BlPitch.values self.SElastoDynFile['RotSpeed'] = self.bins.sel(wspd=Vhub_, method='nearest').RotSpeed.values self.SElastoDynFile['NacYaw'] = yaw_deg_ + yaw_mis_deg_ @@ -813,7 +816,7 @@ def setTemplateFilename(self, def checkIfExists(f): - if f == 'unused': + if os.path.basename(f) == 'unused': return if not os.path.isfile(f): raise ValueError (f'File {f} does not exist.') @@ -840,7 +843,7 @@ def checkIfExists(f): self.HDfilename = HDfilename if SrvDfilename is not None: - if not EDfilename.endswith('.T'): + if SrvDfilename != 'unused' and not SrvDfilename.endswith('.T'): raise ValueError (f'Name the template ServoDyn file "*.T.dat" and give "*.T" as `SrvDfilename`') self.SrvDfilepath = os.path.join(self.templatePath,f"{SrvDfilename}.dat") checkIfExists(self.SrvDfilepath) @@ -861,7 +864,7 @@ def checkIfExists(f): self.ADskfilename = ADskfilename if SubDfilename is not None: - if not SubDfilename.endswith('.dat'): + if SubDfilename != 'unused' and not SubDfilename.endswith('.dat'): raise ValueError (f'The SubDyn filename should end in `.dat`.') self.SubDfilepath = os.path.join(self.templatePath,SubDfilename) checkIfExists(self.SubDfilepath) @@ -968,8 +971,10 @@ def _open_template_files(self): # Open template files def _check_and_open(f): - if f != 'unused': - return FASTInputFile(f) + if os.path.basename(f) == 'unused': + return 'unused' + else: + return FASTInputFile(f) self.ElastoDynFile = _check_and_open(self.EDfilepath) self.SElastoDynFile = _check_and_open(self.SEDfilepath) From 442deaa1fcc6ac66f877790f7fc7a41929d8bc9f Mon Sep 17 00:00:00 2001 From: Regis Thedin Date: Thu, 1 Jun 2023 11:14:06 -0600 Subject: [PATCH 082/124] FF bugfix: proper handling of `'unused'` input files --- pyFAST/fastfarm/FASTFarmCaseCreation.py | 38 ++++++++++++------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/pyFAST/fastfarm/FASTFarmCaseCreation.py b/pyFAST/fastfarm/FASTFarmCaseCreation.py index 04ad2ad..1974498 100644 --- a/pyFAST/fastfarm/FASTFarmCaseCreation.py +++ b/pyFAST/fastfarm/FASTFarmCaseCreation.py @@ -821,64 +821,64 @@ def checkIfExists(f): if not os.path.isfile(f): raise ValueError (f'File {f} does not exist.') - if EDfilename is not None: - if EDfilename != 'unused' and not EDfilename.endswith('.T'): + if EDfilename is not None and EDfilename != 'unused': + if not EDfilename.endswith('.T'): raise ValueError (f'Name the template ED file "*.T.dat" and give "*.T" as `EDfilename`') self.EDfilepath = os.path.join(self.templatePath,f"{EDfilename}.dat") checkIfExists(self.EDfilepath) self.EDfilename = EDfilename - if SEDfilename is not None: - if SEDfilename != 'unused' and not SEDfilename.endswith('.T'): + if SEDfilename is not None and SEDfilename != 'unused': + if not SEDfilename.endswith('.T'): raise ValueError (f'Name the template SED file "*.T.dat" and give "*.T" as `SEDfilename`') self.SEDfilepath = os.path.join(self.templatePath,f"{SEDfilename}.dat") checkIfExists(self.SEDfilepath) self.SEDfilename = SEDfilename - if HDfilename is not None: - if HDfilename != 'unused' and not HDfilename.endswith('.dat'): + if HDfilename is not None and HDfilename != 'unused': + if not HDfilename.endswith('.dat'): raise ValueError (f'The HydroDyn filename should end in `.dat`.') self.HDfilepath = os.path.join(self.templatePath,HDfilename) checkIfExists(self.HDfilepath) self.HDfilename = HDfilename - if SrvDfilename is not None: - if SrvDfilename != 'unused' and not SrvDfilename.endswith('.T'): + if SrvDfilename is not None and SrvDfilename != 'unused': + if not SrvDfilename.endswith('.T'): raise ValueError (f'Name the template ServoDyn file "*.T.dat" and give "*.T" as `SrvDfilename`') self.SrvDfilepath = os.path.join(self.templatePath,f"{SrvDfilename}.dat") checkIfExists(self.SrvDfilepath) self.SrvDfilename = SrvDfilename - if ADfilename is not None: - if ADfilename != 'unused' and not ADfilename.endswith('.dat'): + if ADfilename is not None and ADfilename != 'unused': + if not ADfilename.endswith('.dat'): raise ValueError (f'The AeroDyn filename should end in `.dat`.') self.ADfilepath = os.path.join(self.templatePath,ADfilename) checkIfExists(self.ADfilepath) self.ADfilename = ADfilename - if ADskfilename is not None: - if ADskfilename != 'unused' and not ADskfilename.endswith('.dat'): + if ADskfilename is not None and ADskfilename != 'unused': + if not ADskfilename.endswith('.dat'): raise ValueError (f'The AeroDisk filename should end in `.dat`.') self.ADskfilepath = os.path.join(self.templatePath,ADskfilename) checkIfExists(self.ADskfilepath) self.ADskfilename = ADskfilename - if SubDfilename is not None: - if SubDfilename != 'unused' and not SubDfilename.endswith('.dat'): + if SubDfilename is not None and SubDfilename != 'unused': + if not SubDfilename.endswith('.dat'): raise ValueError (f'The SubDyn filename should end in `.dat`.') self.SubDfilepath = os.path.join(self.templatePath,SubDfilename) checkIfExists(self.SubDfilepath) self.SubDfilename = SubDfilename - if IWfilename is not None: - if IWfilename != 'unused' and not IWfilename.endswith('.dat'): + if IWfilename is not None and IWfilename != 'unused': + if not IWfilename.endswith('.dat'): raise ValueError (f'The InflowWind filename should end in `.dat`.') self.IWfilepath = os.path.join(self.templatePath,IWfilename) checkIfExists(self.IWfilepath) self.IWfilename = IWfilename - if BDfilepath is not None: - if BDfilepath != 'unused' and not BDfilepath.endswith('.dat'): + if BDfilepath is not None and BDfilepath != 'unused': + if not BDfilepath.endswith('.dat'): raise ValueError (f'The BeamDyn filename should end in `.dat`.') self.BDfilepath = BDfilepath checkIfExists(self.BDfilepath) @@ -918,7 +918,7 @@ def checkIfExists(f): checkIfExists(self.controllerInputfilepath) self.controllerInputfilename = controllerInputfilename - if coeffTablefilename is not None: + if coeffTablefilename is not None and coeffTablefilename != 'unused': if not coeffTablefilename.endswith('.csv'): raise ValueError (f'The performance table `coeffTablefilename` file should end in "*.csv"') self.coeffTablefilepath = os.path.join(templatePath, coeffTablefilename) From 0c4ea8fa4fb7b64a5f9c38746d3736faeddbb909 Mon Sep 17 00:00:00 2001 From: Emmanuel Branlard Date: Fri, 16 Jun 2023 16:40:05 -0600 Subject: [PATCH 083/124] Converter: adding quick and dirty elastodyn 2 hawc2 --- pyFAST/converters/beamDynToHawc2.py | 25 +- .../svgtex/BeamDynSectionCoord_Abstract.svg | 844 ++++++++++++++++++ pyFAST/converters/elastodyn.py | 79 ++ pyFAST/converters/hawc2.py | 32 + pyFAST/converters/openfastToHawc2.py | 112 ++- 5 files changed, 1045 insertions(+), 47 deletions(-) create mode 100644 pyFAST/converters/doc/svgtex/BeamDynSectionCoord_Abstract.svg create mode 100644 pyFAST/converters/elastodyn.py create mode 100644 pyFAST/converters/hawc2.py diff --git a/pyFAST/converters/beamDynToHawc2.py b/pyFAST/converters/beamDynToHawc2.py index ea0e955..6575113 100644 --- a/pyFAST/converters/beamDynToHawc2.py +++ b/pyFAST/converters/beamDynToHawc2.py @@ -11,6 +11,7 @@ from pyFAST.converters.beam import ComputeStiffnessProps, TransformCrossSectionMatrix from .beam import * +from .hawc2 import dfstructure2stfile def arc_length(points): @@ -141,17 +142,19 @@ def beamDynToHawc2(BD_mainfile, BD_bladefile, H2_htcfile=None, H2_stfile=None, b pass if verbose: print('Writing: ',H2_stfile) - with open(H2_stfile, 'w') as f: - f.write('%i ; number of sets, Nset\n' % 1) - f.write('-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------\n') - f.write('#%i ; set number\n' % 1) - if FPM: - cols=['r_[m]','m_[kg/m]','x_cg_[m]','y_cg_[m]','ri_x_[m]','ri_y_[m]','pitch_[deg]','x_e_[m]','y_e_[m]','K11','K12','K13','K14','K15','K16','K22','K23','K24','K25','K26','K33','K34','K35','K36','K44','K45','K46','K55','K56','K66'] - else: - cols=['r_[m]','m_[kg/m]','x_cg_[m]','y_cg_[m]','ri_x_[m]','ri_y_[m]', 'x_sh_[m]','y_sh_[m]','E_[N/m^2]','G_[N/m^2]','I_x_[m^4]','I_y_[m^4]','I_p_[m^4]','k_x_[-]','k_y_[-]','A_[m^2]','pitch_[deg]','x_e_[m]','y_e_[m]'] - f.write('\t'.join(['{:20s}'.format(s) for s in cols])+'\n') - f.write('$%i %i\n' % (1, dfStructure.shape[0])) - f.write('\n'.join('\t'.join('%19.13e' %x for x in y) for y in dfStructure.values)) + + dfstructure2stfile(dfStructure, H2_stfile) + #with open(H2_stfile, 'w') as f: + # f.write('%i ; number of sets, Nset\n' % 1) + # f.write('-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------\n') + # f.write('#%i ; set number\n' % 1) + # if FPM: + # cols=['r_[m]','m_[kg/m]','x_cg_[m]','y_cg_[m]','ri_x_[m]','ri_y_[m]','pitch_[deg]','x_e_[m]','y_e_[m]','K11','K12','K13','K14','K15','K16','K22','K23','K24','K25','K26','K33','K34','K35','K36','K44','K45','K46','K55','K56','K66'] + # else: + # cols=['r_[m]','m_[kg/m]','x_cg_[m]','y_cg_[m]','ri_x_[m]','ri_y_[m]', 'x_sh_[m]','y_sh_[m]','E_[N/m^2]','G_[N/m^2]','I_x_[m^4]','I_y_[m^4]','I_p_[m^4]','k_x_[-]','k_y_[-]','A_[m^2]','pitch_[deg]','x_e_[m]','y_e_[m]'] + # f.write('\t'.join(['{:20s}'.format(s) for s in cols])+'\n') + # f.write('$%i %i\n' % (1, dfStructure.shape[0])) + # f.write('\n'.join('\t'.join('%19.13e' %x for x in y) for y in dfStructure.values)) # --- Rewrite htc file if H2_htcfile is not None: diff --git a/pyFAST/converters/doc/svgtex/BeamDynSectionCoord_Abstract.svg b/pyFAST/converters/doc/svgtex/BeamDynSectionCoord_Abstract.svg new file mode 100644 index 0000000..0ddccc8 --- /dev/null +++ b/pyFAST/converters/doc/svgtex/BeamDynSectionCoord_Abstract.svg @@ -0,0 +1,844 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + diff --git a/pyFAST/converters/elastodyn.py b/pyFAST/converters/elastodyn.py new file mode 100644 index 0000000..506af9d --- /dev/null +++ b/pyFAST/converters/elastodyn.py @@ -0,0 +1,79 @@ +import numpy as np +import pandas as pd + + +def elastodynBlade2Hawc2_raw(dfBld, R, E=3e10): + """ + INPUTS: + - dfBld: dataframe resulting from reading an ElastoDyn blade file + with columns: [ 'BlFract_[-]', 'PitchAxis_[-]', 'StrcTwst_[deg]', 'BMassDen_[kg/m]', 'FlpStff_[Nm^2]', 'EdgStff_[Nm^2]' + - R : rotor radius [m] + OUTPUTS: + - dfMeanLine: hawc2 c2def as DataFrame with columns ['x_[m]','y_[m]','z_[m]','twist_[deg]'] + - dfStructure: hawc2 stdata as Dataframe + """ + r = dfBld['BlFract_[-]'].values*R + twist = dfBld['StrcTwst_[deg]'].values + m = dfBld['BMassDen_[kg/m]'].values + EIflap = dfBld['FlpStff_[Nm^2]'].values + EIedge = dfBld['EdgStff_[Nm^2]'].values + + + # --- ElastoDyn 2 Hawc2 Structural data + columns=['r_[m]','m_[kg/m]','x_cg_[m]','y_cg_[m]','ri_x_[m]','ri_y_[m]','x_sh_[m]','y_sh_[m]','E_[N/m^2]','G_[N/m^2]','I_x_[m^4]','I_y_[m^4]','I_p_[m^4]','k_x_[-]','k_y_[-]','A_[m^2]','pitch_[deg]','x_e_[m]','y_e_[m]'] + nr = len(r) + dfStructure = pd.DataFrame(data=np.zeros((nr,len(columns))), columns=columns) + dfStructure['r_[m]'] = r + dfStructure['m_[kg/m]'] = m + # 'x_cg_[m]','y_cg_[m]' = 0 + # 'ri_x_[m]','ri_y_[m]' # TODO + # 'x_sh_[m]','y_sh_[m]','E_[N/m^2]', + # dfStructure['x_sh_[m]'] = 0 + # dfStructure['y_sh_[m]']= 0 + dfStructure['E_[N/m^2]'] = E # Arbitrary value + dfStructure['G_[N/m^2]'] = R/2/(1+0.3)*100 # Arbitrary value, but large (ElastoDyn stiff in torsion) + dfStructure['I_x_[m^4]'] = EIedge / E + dfStructure['I_y_[m^4]'] = EIflap / E + dfStructure['I_p_[m^4]'] = np.mean(EIflap+EIedge)/2*E*100 # Ip approximated as mean value of Ix+Iy, large value (ElastoDyn stiff in torsion) + dfStructure['k_x_[-]'] = 0 + dfStructure['k_y_[-]'] = 0 + dfStructure['A_[m^2]'] = 1 # Arbitrary value + dfStructure['pitch_[deg]']= -twist # TODO Taken as structural twist + # 'x_e_[m]','y_e_[m]' =0 + # Hawc2 = BeamDyn + # x_cg = -y_G + # y_cg = x_G + # x_sh = -y_S + # y_sh = x_S + # x_e = -y_C + # y_e = x_C + # I_y = EIx/E # [m^4] Hawc2 Iy is wrt to principal bending ye axis + # I_x = EIy/E # [m^4] Hawc2 Ix is wrt to principal bending xe axis + # I_p = GKt/G # [m^4] + # k_y = kxsGA/(G*A) + # k_x = kysGA/(G*A) + # pitch = theta_p*180/np.pi # [deg] NOTE: could use theta_p, theta_i or theta_s + # if np.all(np.abs(m)<1e-16): + # ri_y = m*0 + # ri_x = m*0 + # else: + # ri_y = np.sqrt(Ixi/m) # [m] + # ri_x = np.sqrt(Iyi/m) # [m] + # # Curvilinear position of keypoints (only used to get max radius...) + # dr= np.sqrt((kp_x[1:]-kp_x[0:-1])**2 +(kp_y[1:]-kp_y[0:-1])**2 +(kp_z[1:]-kp_z[0:-1])**2) + # r_p= np.concatenate(([0],np.cumsum(dr))) + # r=r_bar * r_p[-1] + + + # --- Defining "c2def" meanline (NOTE: ElastoDyn has no prebend or presweep, so x&y are set to zero for now + MMeanLine = np.column_stack((r*0, r*0, r, -twist)) + dfMeanLine = pd.DataFrame(data = MMeanLine, columns=['x_[m]','y_[m]','z_[m]','twist_[deg]']) + # BeamDyn: + # Y_H2 = kp_x + # Z_H2 = kp_z + # twist_H2 = - twist*180/np.pi # -[deg] + # columns=['x_[m]', 'y_[m]', 'z_[m]', 'twist_[deg]'] + # data = np.column_stack((X_H2, Y_H2, Z_H2, twist_H2)) + # dfMeanLine = pd.DataFrame(data=data, columns=columns) + + return dfMeanLine, dfStructure diff --git a/pyFAST/converters/hawc2.py b/pyFAST/converters/hawc2.py new file mode 100644 index 0000000..8caedb9 --- /dev/null +++ b/pyFAST/converters/hawc2.py @@ -0,0 +1,32 @@ + + + +def dfstructure2stfile(dfStructure, H2_stfile): + """ + Write a HAWC2 st file from a pandas dataframe of structural data. + + For fully populated matrices, the columns are: + ['r_[m]','m_[kg/m]','x_cg_[m]','y_cg_[m]','ri_x_[m]','ri_y_[m]','pitch_[deg]','x_e_[m]','y_e_[m]','K11','K12','K13','K14','K15','K16','K22','K23','K24','K25','K26','K33','K34','K35','K36','K44','K45','K46','K55','K56','K66'] + For Timoshenko: + ['r_[m]','m_[kg/m]','x_cg_[m]','y_cg_[m]','ri_x_[m]','ri_y_[m]','x_sh_[m]','y_sh_[m]','E_[N/m^2]','G_[N/m^2]','I_x_[m^4]','I_y_[m^4]','I_p_[m^4]','k_x_[-]','k_y_[-]','A_[m^2]','pitch_[deg]','x_e_[m]','y_e_[m]'] + + + TODO: + - make the input independent of column order + - use hawc2_st_file.py instead + + """ + + FPM = 'K11' in dfStructure + if FPM: + cols=['r_[m]','m_[kg/m]','x_cg_[m]','y_cg_[m]','ri_x_[m]','ri_y_[m]','pitch_[deg]','x_e_[m]','y_e_[m]','K11','K12','K13','K14','K15','K16','K22','K23','K24','K25','K26','K33','K34','K35','K36','K44','K45','K46','K55','K56','K66'] + else: + cols=['r_[m]','m_[kg/m]','x_cg_[m]','y_cg_[m]','ri_x_[m]','ri_y_[m]', 'x_sh_[m]','y_sh_[m]','E_[N/m^2]','G_[N/m^2]','I_x_[m^4]','I_y_[m^4]','I_p_[m^4]','k_x_[-]','k_y_[-]','A_[m^2]','pitch_[deg]','x_e_[m]','y_e_[m]'] + + with open(H2_stfile, 'w') as f: + f.write('%i ; number of sets, Nset\n' % 1) + f.write('-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------\n') + f.write('#%i ; set number\n' % 1) + f.write('\t'.join(['{:20s}'.format(s) for s in cols])+'\n') + f.write('$%i %i\n' % (1, dfStructure.shape[0])) + f.write('\n'.join('\t'.join('%19.13e' %x for x in y) for y in dfStructure.values)) # TODO: this assumes a column order diff --git a/pyFAST/converters/openfastToHawc2.py b/pyFAST/converters/openfastToHawc2.py index adbce28..d045e03 100644 --- a/pyFAST/converters/openfastToHawc2.py +++ b/pyFAST/converters/openfastToHawc2.py @@ -10,6 +10,8 @@ from pyFAST.input_output.fast_input_deck import FASTInputDeck import pyFAST.converters.beamdyn as bd +import pyFAST.converters.elastodyn as ed +import pyFAST.converters.hawc2 as h2 def FAST2Hawc2(fstIn, htcTemplate, htcOut, OPfile=None, TwrFAFreq=0.1, TwrSSFreq=0.1, SftTorFreq=4, FPM = False, Bld_E=None, Bld_G=None, Bld_A=None, Bld_theta_p=None): @@ -27,6 +29,7 @@ def FAST2Hawc2(fstIn, htcTemplate, htcOut, OPfile=None, TwrFAFreq=0.1, TwrSSFreq twrOF = fst.fst_vt['ElastoDynTower'] BD = fst.fst_vt['BeamDyn'] BDbld = fst.fst_vt['BeamDynBlade'] + EDbld = fst.fst_vt['ElastoDynBlade'] # print(fst.ED.keys()) # print(fst.ED['TwrFile']) @@ -135,52 +138,87 @@ def FAST2Hawc2(fstIn, htcTemplate, htcOut, OPfile=None, TwrFAFreq=0.1, TwrSSFreq # --------------------------------------------------------------------------------} # --- Blade # --------------------------------------------------------------------------------{ - bdy = htc.bodyByName('blade1') # Mean line # val[:,3] = np.linspace(0, ED['HubRad'], val.shape[0]) + st_filefull = os.path.join(outDataDir, 'blade_st.st') # Full path of blade st file + st_file = os.path.relpath(st_filefull, outDir) # Relative to output dir + dfStructure = None + dfMeanLine = None + damp=[0, 0, 0] if BD is not None: + # --- Convert from BeamDyn to HAWC2 c2def and st data + # also writes st file # TODO A, E, G, theta_p - st_filefull = os.path.join(outDataDir, 'blade_st.st') dfMeanLine, dfStructure = bd.beamDynToHawc2(BD, BDbld, H2_stfile=st_filefull, A=Bld_A, E=Bld_E, G=Bld_G, theta_p_in = Bld_theta_p, FPM=FPM, verbose=True) - st_file = os.path.relpath(st_filefull, outDir) - - bdy.nbodies = dfMeanLine.shape[0]-1 # One body per station -1 - bdy.timoschenko_input.ts_filename = st_file - bdy.timoschenko_input.set.values = [1,1] - if FPM: - bdy.timoschenko_input.fpm = 1 - # Damping - #bdy['damping_aniso'] = bdy.damping_posdef.copy() - bdy.damping_posdef.name_='damping_aniso' - bdy.damping_posdef.name_='damping_aniso' - bdy.damping_posdef.values[0]=0 # no mass proportioanl damping - bdy.damping_posdef.values[1]=0 # no mass proportioanl damping - bdy.damping_posdef.values[2]=0 # no mass proportioanl damping - bdy.damping_posdef.values[3] = BDbld['DampingCoeffs'][0,1] # TODO Check order - bdy.damping_posdef.values[4] = BDbld['DampingCoeffs'][0,0] # TODO - bdy.damping_posdef.values[5] = BDbld['DampingCoeffs'][0,2] # TODO - #raise NotImplementedError('Damping for FPM') - print('>>>> TODO TODO TODO DAMPING ') - else: - bdy.timoschenko_input.fpm = 0 - # Damping - bdy.damping_posdef.values[3] = BDbld['DampingCoeffs'][0,1] - bdy.damping_posdef.values[4] = BDbld['DampingCoeffs'][0,0] - bdy.damping_posdef.values[5] = BDbld['DampingCoeffs'][0,2] - - # Meanline - c2_def = htc.bodyC2(bdy) - c2_def = np.column_stack((np.arange(1,len(dfMeanLine)+1), dfMeanLine.values)) - c2_def = np.around(c2_def, 5) - htc.setBodyC2(bdy, c2_def) + # --- Damping + damp[0] = BDbld['DampingCoeffs'][0,1] # TODO Check order + damp[1] = BDbld['DampingCoeffs'][0,0] # TODO + damp[2] = BDbld['DampingCoeffs'][0,2] # TODO + elif EDbld is not None: + print('[WARN] Blade with ElastoDyn is not fully implemented') + if FPM: + print('[WARN] Cannot do FPM with ElastoDyn for now') + FPM = False # Cannot do FPM with ElastoDyn + + # --- ElastoDyn blade properties + M = EDbld['BldProp'] + dfBld = EDbld.toDataFrame() # r/R, PitchAxis(unused), StructuralTwist, m, EIflap, EIdge + R = ED['TipRad'] - ED['HubRad'] + + # --- Convert from ElastoDyn to HAWC2 c2def and st data + dfMeanLine, dfStructure = ed.elastodynBlade2Hawc2_raw(dfBld, R) + # Write st file + h2.dfstructure2stfile(dfStructure, st_filefull) + + # --- + # TODO TODO TODO Damping is completely wrong here + print('[WARN] Damping values for blades are wrong when using ElastoDyn blade') + damp[0] = EDbld['BldFlDmp(1)']/100 # + damp[1] = EDbld['BldFlDmp(2)']/100 + damp[2] = EDbld['BldEdDmp(1)']/100 + + else: - # ElastoDyn - raise NotImplementedError('Blade with ElastoDyn') + raise Exception('No BeamDyn or ElastoDyn blade present') + # or throw a warning and skip the section below.. - # print(bdy) + # --- Setup HAWC2 "body" data + bdy = htc.bodyByName('blade1') + bdy.nbodies = dfMeanLine.shape[0]-1 # One body per station -1 + bdy.timoschenko_input.ts_filename = st_file + bdy.timoschenko_input.set.values = [1,1] + + if FPM: + bdy.timoschenko_input.fpm = 1 + # Damping + #bdy['damping_aniso'] = bdy.damping_posdef.copy() + bdy.damping_posdef.name_='damping_aniso' + bdy.damping_posdef.name_='damping_aniso' + bdy.damping_posdef.values[0]=0 # no mass proportional damping + bdy.damping_posdef.values[1]=0 # no mass proportional damping + bdy.damping_posdef.values[2]=0 # no mass proportional damping + bdy.damping_posdef.values[3] = damp[0] + bdy.damping_posdef.values[4] = damp[1] + bdy.damping_posdef.values[5] = damp[2] + #raise NotImplementedError('Damping for FPM') + print('>>>> TODO TODO TODO DAMPING ') + else: + bdy.timoschenko_input.fpm = 0 + # Damping + bdy.damping_posdef.values[3] = damp[0] + bdy.damping_posdef.values[4] = damp[1] + bdy.damping_posdef.values[5] = damp[2] + + # Meanline + c2_def = htc.bodyC2(bdy) + c2_def = np.column_stack((np.arange(1,len(dfMeanLine)+1), dfMeanLine.values)) + c2_def = np.around(c2_def, 5) + htc.setBodyC2(bdy, c2_def) + + #print(bdy) # --- Orientation @@ -199,7 +237,9 @@ def FAST2Hawc2(fstIn, htcTemplate, htcOut, OPfile=None, TwrFAFreq=0.1, TwrSSFreq # sec.relative__8.body2_eulerang.values[2] = - np.around(ED['BlPitch(3)'],4) # TODO sign #print(sec) + # --------------------------------------------------------------------------------} # --- Wind + # --------------------------------------------------------------------------------{ sec = htc.data.wind sec.density = AD['AirDens'] sec.wsp = IW['HWindSpeed'] From 7e911acfe99303c0c990a76bd543280e50b2f90d Mon Sep 17 00:00:00 2001 From: Emmanuel Branlard Date: Fri, 16 Jun 2023 16:46:25 -0600 Subject: [PATCH 084/124] Small fastfarm example fixes --- pyFAST/fastfarm/TurbSimCaseCreation.py | 8 +++++-- .../examples/Ex1_TurbSimInputSetup.py | 2 +- .../examples/Ex3_FFarmCompleteSetup.py | 24 +++++++++---------- 3 files changed, 19 insertions(+), 15 deletions(-) diff --git a/pyFAST/fastfarm/TurbSimCaseCreation.py b/pyFAST/fastfarm/TurbSimCaseCreation.py index cc9c57a..00b8192 100644 --- a/pyFAST/fastfarm/TurbSimCaseCreation.py +++ b/pyFAST/fastfarm/TurbSimCaseCreation.py @@ -77,6 +77,8 @@ def __init__(self, D, HubHt, Vhub, TI, PLexp, x, y, z=None, zbot=1.0, cmax=5.0, self.domainSize(zbot=zbot, manual_mode=manual_mode) # Determine origin # self.originLoc() + self.ymin = None + self.ymax = None def Turb(self, D, HubHt, cmax=5.0, fmax=5.0): """ @@ -227,8 +229,10 @@ def plotSetup(self, fig=None, ax=None): # low-res box # ax.plot([xmin,xmax,xmax,xmin,xmin], # [ymin,ymin,ymax,ymax,ymin],'--k',lw=2,label='Low') - ax.axhline(self.ymin, ls='--', c='k', lw=2, label='Low') - ax.axhline(self.ymax, ls='--', c='k', lw=2) + if self.ymin is not None: + ax.axhline(self.ymin, ls='--', c='k', lw=2, label='Low') + if self.ymax is not None: + ax.axhline(self.ymax, ls='--', c='k', lw=2) ax.legend(bbox_to_anchor=(1.05,1.015),frameon=False) ax.set_xlabel("x-location [m]") diff --git a/pyFAST/fastfarm/examples/Ex1_TurbSimInputSetup.py b/pyFAST/fastfarm/examples/Ex1_TurbSimInputSetup.py index 226c6f5..4da4642 100644 --- a/pyFAST/fastfarm/examples/Ex1_TurbSimInputSetup.py +++ b/pyFAST/fastfarm/examples/Ex1_TurbSimInputSetup.py @@ -35,7 +35,7 @@ # --- Use TurbSim Case Creation class to write a new TurbSim file Case = TSCaseCreation(D, HubHt, Vhub, TI, PLExp, x=xlocs, y=ylocs, zbot=zbot, cmax=cmax, fmax=fmax, Cmeander=Cmeander) # Rewrite TurbSim Input File -Case.writeTSFile(OldTSFile, NewTSFile, tmax=5) +Case.writeTSFile(OldTSFile, NewTSFile, tmax=5, turb=1) print('NOTE: run TurbSim to generate this new BTS file.') # --- Visualize low extent and turbine positions diff --git a/pyFAST/fastfarm/examples/Ex3_FFarmCompleteSetup.py b/pyFAST/fastfarm/examples/Ex3_FFarmCompleteSetup.py index 7ff6ad2..d44e8ab 100644 --- a/pyFAST/fastfarm/examples/Ex3_FFarmCompleteSetup.py +++ b/pyFAST/fastfarm/examples/Ex3_FFarmCompleteSetup.py @@ -31,18 +31,18 @@ def main(): D = 240 zhub = 150 wts = { - 0 :{'x':0.0, 'y':0, 'z':0.0, 'D':D, 'zhub':zhub, 'cmax':cmax, 'fmax':fmax, 'Cmeander':Cmeander}}, - 1 :{'x':1852.0, 'y':0, 'z':0.0, 'D':D, 'zhub':zhub, 'cmax':cmax, 'fmax':fmax, 'Cmeander':Cmeander}}, - 2 :{'x':3704.0, 'y':0, 'z':0.0, 'D':D, 'zhub':zhub, 'cmax':cmax, 'fmax':fmax, 'Cmeander':Cmeander}}, - 3 :{'x':5556.0, 'y':0, 'z':0.0, 'D':D, 'zhub':zhub, 'cmax':cmax, 'fmax':fmax, 'Cmeander':Cmeander}}, - 4 :{'x':7408.0, 'y':0, 'z':0.0, 'D':D, 'zhub':zhub, 'cmax':cmax, 'fmax':fmax, 'Cmeander':Cmeander}}, - 5 :{'x':1852.0, 'y':1852.0, 'z':0.0, 'D':D, 'zhub':zhub, 'cmax':cmax, 'fmax':fmax, 'Cmeander':Cmeander}}, - 6 :{'x':3704.0, 'y':1852.0, 'z':0.0, 'D':D, 'zhub':zhub, 'cmax':cmax, 'fmax':fmax, 'Cmeander':Cmeander}}, - 7 :{'x':5556.0, 'y':1852.0, 'z':0.0, 'D':D, 'zhub':zhub, 'cmax':cmax, 'fmax':fmax, 'Cmeander':Cmeander}}, - 8 :{'x':7408.0, 'y':1852.0, 'z':0.0, 'D':D, 'zhub':zhub, 'cmax':cmax, 'fmax':fmax, 'Cmeander':Cmeander}}, - 9 :{'x':3704.0, 'y':3704.0, 'z':0.0, 'D':D, 'zhub':zhub, 'cmax':cmax, 'fmax':fmax, 'Cmeander':Cmeander}}, - 10:{'x':5556.0, 'y':3704.0, 'z':0.0, 'D':D, 'zhub':zhub, 'cmax':cmax, 'fmax':fmax, 'Cmeander':Cmeander}}, - 11:{'x':7408.0, 'y':3704.0, 'z':0.0, 'D':D, 'zhub':zhub, 'cmax':cmax, 'fmax':fmax, 'Cmeander':Cmeander}}, + 0 :{'x':0.0, 'y':0, 'z':0.0, 'D':D, 'zhub':zhub, 'cmax':cmax, 'fmax':fmax, 'Cmeander':Cmeander}, + 1 :{'x':1852.0, 'y':0, 'z':0.0, 'D':D, 'zhub':zhub, 'cmax':cmax, 'fmax':fmax, 'Cmeander':Cmeander}, + 2 :{'x':3704.0, 'y':0, 'z':0.0, 'D':D, 'zhub':zhub, 'cmax':cmax, 'fmax':fmax, 'Cmeander':Cmeander}, + 3 :{'x':5556.0, 'y':0, 'z':0.0, 'D':D, 'zhub':zhub, 'cmax':cmax, 'fmax':fmax, 'Cmeander':Cmeander}, + 4 :{'x':7408.0, 'y':0, 'z':0.0, 'D':D, 'zhub':zhub, 'cmax':cmax, 'fmax':fmax, 'Cmeander':Cmeander}, + 5 :{'x':1852.0, 'y':1852.0, 'z':0.0, 'D':D, 'zhub':zhub, 'cmax':cmax, 'fmax':fmax, 'Cmeander':Cmeander}, + 6 :{'x':3704.0, 'y':1852.0, 'z':0.0, 'D':D, 'zhub':zhub, 'cmax':cmax, 'fmax':fmax, 'Cmeander':Cmeander}, + 7 :{'x':5556.0, 'y':1852.0, 'z':0.0, 'D':D, 'zhub':zhub, 'cmax':cmax, 'fmax':fmax, 'Cmeander':Cmeander}, + 8 :{'x':7408.0, 'y':1852.0, 'z':0.0, 'D':D, 'zhub':zhub, 'cmax':cmax, 'fmax':fmax, 'Cmeander':Cmeander}, + 9 :{'x':3704.0, 'y':3704.0, 'z':0.0, 'D':D, 'zhub':zhub, 'cmax':cmax, 'fmax':fmax, 'Cmeander':Cmeander}, + 10:{'x':5556.0, 'y':3704.0, 'z':0.0, 'D':D, 'zhub':zhub, 'cmax':cmax, 'fmax':fmax, 'Cmeander':Cmeander}, + 11:{'x':7408.0, 'y':3704.0, 'z':0.0, 'D':D, 'zhub':zhub, 'cmax':cmax, 'fmax':fmax, 'Cmeander':Cmeander}, } refTurb_rot = 0 From ff9584c1c2c297ecb147b0858e3a6092bbb37d20 Mon Sep 17 00:00:00 2001 From: Regis Thedin Date: Fri, 30 Jun 2023 00:33:37 -0600 Subject: [PATCH 085/124] FF: fix selection of unique cases for high-res boxes --- pyFAST/fastfarm/FASTFarmCaseCreation.py | 35 +++++++++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/pyFAST/fastfarm/FASTFarmCaseCreation.py b/pyFAST/fastfarm/FASTFarmCaseCreation.py index 1974498..69bc06b 100644 --- a/pyFAST/fastfarm/FASTFarmCaseCreation.py +++ b/pyFAST/fastfarm/FASTFarmCaseCreation.py @@ -1291,12 +1291,42 @@ def getDomainParameters(self): # and sweep in yaw do not require extra TurbSim runs self.nHighBoxCases = len(np.unique(self.inflow_deg)) # some wind dir might be repeated for sweep on yaws - self.allHighBoxCases = self.allCases.where(~self.allCases['wakeSteering'],drop=True).drop_vars('wakeSteering')\ + # Old method to get allHighBoxCases. Doesn't work well if I have weird manual sweeps + allHighBoxCases_old = self.allCases.where(~self.allCases['wakeSteering'],drop=True).drop_vars('wakeSteering')\ .where(~self.allCases['misalignment'], drop=True).drop_vars('misalignment')\ .where(self.allCases['nFullAeroDyn']==self.nTurbines, drop=True).drop_vars('ADmodel')\ .where(self.allCases['nFulllElastoDyn']==self.nTurbines, drop=True).drop_vars('EDmodel')\ .where(self.allCases['yawCase']==1, drop=True).drop_vars('yawCase') - + + # This is a new method, but I'm not sure if it will work always, so let's leave the one above and check it + uniquewdir = np.unique(self.allCases.inflow_deg) + allHighBoxCases = [] + for currwdir in uniquewdir: + # Get first case to have the wind direction currwdir + firstCaseWithInflow_i = self.allCases.where(self.allCases['inflow_deg'] == currwdir, drop=True).isel(case=0) + allHighBoxCases.append(firstCaseWithInflow_i) + self.allHighBoxCases = xr.concat(allHighBoxCases, dim='case') + # But, before I change the algorithm, I want to time-test it, so let's compare both ways + if not allHighBoxCases_old.identical(self.allHighBoxCases): + self.allHighBoxCases_old = allHighBoxCases_old + print(f'!!!!!! WARNING !!!!!!!!!') + print(f'The new method for computing all the high-box cases is not producing the same set of cases as the old algorithm.') + print(f'This should only happen if you have complex sweeps that you modified manually after the code creates the initial arrays') + print(f'Check the variable .allHighBoxCases_old to see the cases using the old algorithm') + print(f'Check the variable .allHighBoxCases to see the cases using the new algorithm') + print(f'You should check which xr.dataset has the correct, unique inflow_deg values. The correct array will only have unique values') + print(f'') + if len(self.allHighBoxCases_old['inflow_deg']) != len(np.unique(self.allHighBoxCases_old['inflow_deg'])): + print(f' Checking the inflow_deg variable, it looks like the old method has non-unique wind directions. The old method is wrong here.') + if len(self.allHighBoxCases['inflow_deg']) != len(np.unique(self.allHighBoxCases['inflow_deg'])): + print(f' Checking the inflow_deg variable, it looks like the new method has non-unique wind directions. The new method is wrong here.') + else: + print(' The new method appears to be correct here! Trust but verify') + print('') + print(f'!!!!!!!!!!!!!!!!!!!!!!!!') + print(f'') + + if self.nHighBoxCases != len(self.allHighBoxCases.case): raise ValueError(f'The number of cases do not match as expected. {self.nHighBoxCases} unique wind directions, but {len(self.allHighBoxCases.case)} unique cases.') @@ -1431,6 +1461,7 @@ def TS_high_setup(self, writeFiles=True): # Create and write new Low.inp files creating the proper box with proper resolution currentTS = TSCaseCreation(D_, HubHt_, Vhub_, tivalue_, shear_, x=xloc_, y=yloc_, zbot=self.zbot, cmax=self.cmax, fmax=self.fmax, Cmeander=self.Cmeander, boxType=boxType, high_ext=self.extent_high) + # !!!!!!!!!!! I have to give the resolution on the call above!!!! currentTS.writeTSFile(self.turbsimHighfilepath, currentTSHighFile, tmax=self.tmax, turb=t, verbose=self.verbose) # Modify some values and save file (some have already been set in the call above) From 6b7c6b9252cbecbfd44538c61428f4c12bb61f6b Mon Sep 17 00:00:00 2001 From: Regis Thedin Date: Mon, 3 Jul 2023 09:12:04 -0600 Subject: [PATCH 086/124] TS: change location where default grid extents are set --- pyFAST/fastfarm/TurbSimCaseCreation.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pyFAST/fastfarm/TurbSimCaseCreation.py b/pyFAST/fastfarm/TurbSimCaseCreation.py index 00b8192..3b6539b 100644 --- a/pyFAST/fastfarm/TurbSimCaseCreation.py +++ b/pyFAST/fastfarm/TurbSimCaseCreation.py @@ -77,8 +77,6 @@ def __init__(self, D, HubHt, Vhub, TI, PLexp, x, y, z=None, zbot=1.0, cmax=5.0, self.domainSize(zbot=zbot, manual_mode=manual_mode) # Determine origin # self.originLoc() - self.ymin = None - self.ymax = None def Turb(self, D, HubHt, cmax=5.0, fmax=5.0): """ @@ -153,6 +151,10 @@ def discretization(self, Vhub, TI, Shear, manual_ds_low=False): def domainSize(self, zbot, manual_mode=False): + # Set default + self.ymin = None + self.ymax = None + if self.boxType == 'lowres': if manual_mode: self.ymin = min(self.y) - self.low_ext[2]*self.D From 672b73f948ef7bf1732b5e1c0ca1f3a5e92cbe8c Mon Sep 17 00:00:00 2001 From: Regis Thedin Date: Mon, 3 Jul 2023 09:33:14 -0600 Subject: [PATCH 087/124] FF: Add plot of entire setup --- pyFAST/fastfarm/FASTFarmCaseCreation.py | 84 +++++++++++++++++++++++++ 1 file changed, 84 insertions(+) diff --git a/pyFAST/fastfarm/FASTFarmCaseCreation.py b/pyFAST/fastfarm/FASTFarmCaseCreation.py index 69bc06b..c72a744 100644 --- a/pyFAST/fastfarm/FASTFarmCaseCreation.py +++ b/pyFAST/fastfarm/FASTFarmCaseCreation.py @@ -2065,3 +2065,87 @@ def FF_slurm_submit(self, qos='normal', A=None, t=None, delay=4): time.sleep(delay) # Sometimes the same job gets submitted twice. This gets around it. + + + + def plot(self, figsize=(15,7), fontsize=14, saveFig=True, returnFig=False): + import matplotlib.pyplot as plt + + fig, ax = plt.subplots(figsize=figsize) + + # for plotting different inflow angles + alphas = np.append(1, np.linspace(0.5,0.2, len(self.wts_rot_ds['inflow_deg'])-1)) + + # low-res box + try: + ax.plot([self.TSlowbox.xmin, self.TSlowbox.xmax, self.TSlowbox.xmax, self.TSlowbox.xmin, self.TSlowbox.xmin], + [self.TSlowbox.ymin, self.TSlowbox.ymin, self.TSlowbox.ymax, self.TSlowbox.ymax, self.TSlowbox.ymin],'--k',lw=2,label='Low') + except AttributeError: + print(f'WARNING: The exact limits of the low-res box have not been computed yet. Showing extents given as inputs') + xmin = self.allCases['Tx'].min()-self.extent_low[0]*self.D + xmax = self.allCases['Tx'].max()+self.extent_low[1]*self.D + ymin = self.allCases['Ty'].min()-self.extent_low[2]*self.D + ymax = self.allCases['Ty'].max()+self.extent_low[3]*self.D + ax.plot([xmin, xmax, xmax, xmin, xmin], + [ymin, ymin, ymax, ymax, ymin],'--k',lw=2,label='Low') + + + for j, inflow in enumerate(self.wts_rot_ds['inflow_deg']): + ax.set_prop_cycle(None) # Reset the colormap for every inflow + for i, currTurbine in enumerate(self.wts_rot_ds.turbine): + color = next(ax._get_lines.prop_cycler)['color'] + + dst = self.wts_rot_ds.sel(turbine=currTurbine, inflow_deg=inflow) + + # plot high-res boxes + xmin, xmax = [dst.x-self.extent_high*dst.D/2, dst.x+self.extent_high*dst.D/2] + ymin, ymax = [dst.y-self.extent_high*dst.D/2, dst.y+self.extent_high*dst.D/2] + # Only add label entry on first (darker) curves + if j==0: + ax.plot([xmin, xmax, xmax, xmin, xmin], + [ymin, ymin, ymax, ymax, ymin], c=color, alpha=alphas[j], label=f'HighT{i+1}') + else: + ax.plot([xmin, xmax, xmax, xmin, xmin], + [ymin, ymin, ymax, ymax, ymin], c=color, alpha=alphas[j]) + + # plot turbine location + ax.scatter(dst.x, dst.y, s=dst.D/6, c=color, marker='o') #, label=f'WT{i+1}') + + # plot turbine disk accoding to all yaws in current wdir + allyaw_currwdir = self.allCases.where(self.allCases['inflow_deg']==inflow,drop=True).sel(turbine=currTurbine)['yaw'] + _, ind = np.unique(allyaw_currwdir, axis=0, return_index=True) + yaw_currwdir = allyaw_currwdir[np.sort(ind)].values # duplicates removed, same order as original array + for yaw in yaw_currwdir: + ax.plot([dst.x.values-(dst.D.values/2)*sind(yaw-inflow.values), dst.x.values+(dst.D.values/2)*sind(yaw-inflow.values)], + [dst.y.values-(dst.D.values/2)*cosd(yaw-inflow.values), dst.y.values+(dst.D.values/2)*cosd(yaw-inflow.values)], c=color, alpha=alphas[j]) + + # plot convex hull of farm (or line) for given inflow + turbs = self.wts_rot_ds.sel(inflow_deg=inflow)[['x','y']].to_array().transpose() + try: + hull = ConvexHull(turbs) + for simplex in hull.simplices: + ax.plot(turbs[simplex, 0], turbs[simplex, 1], 'gray', alpha=alphas[j], label=f'Inflow {inflow.values} deg') + except: + # All turbines are in a line. Plotting a line instead of convex hull + ax.plot(turbs[:,0], turbs[:,1], 'gray', alpha=alphas[j], label=f'Inflow {inflow.values} deg') + + + # Remove duplicate entries from legend + handles, labels = plt.gca().get_legend_handles_labels() + by_label = dict(zip(labels, handles)) + plt.legend(by_label.values(), by_label.keys(), loc='upper left', bbox_to_anchor=(1.02,1.015), fontsize=fontsize) + + ax.set_xlabel("x [m]", fontsize=fontsize) + ax.set_ylabel("y [m]", fontsize=fontsize) + ax.tick_params(axis='both', which='major', labelsize=fontsize) + ax.grid() + ax.set_aspect('equal') + + if saveFig: + fig.savefig(os.path.join(self.path,'farm.png'), bbox_inches='tight', facecolor='white', transparent=False) + + if returnFig: + return fig, ax + + + From b67ec6a7ffa5917271060105611d58907b4615ee Mon Sep 17 00:00:00 2001 From: Regis Thedin Date: Mon, 3 Jul 2023 09:37:52 -0600 Subject: [PATCH 088/124] FF: Significant changes to how `allCases` array is created The code from before was based on a lot of `repeat` and `tile`, which became hard to maintain and fix bugs. This commit: - Remove the bool for wake steering sweep, as there is no real use - Remove completely the `repeat`s and `tile`s, in favor of a more clear loop over the original cases, adding other options as per sweep requested - It is a tad slower, but significantly more clear and easier to maintain --- pyFAST/fastfarm/FASTFarmCaseCreation.py | 188 +++++++++++++----------- 1 file changed, 102 insertions(+), 86 deletions(-) diff --git a/pyFAST/fastfarm/FASTFarmCaseCreation.py b/pyFAST/fastfarm/FASTFarmCaseCreation.py index c72a744..5ad8e1f 100644 --- a/pyFAST/fastfarm/FASTFarmCaseCreation.py +++ b/pyFAST/fastfarm/FASTFarmCaseCreation.py @@ -504,7 +504,7 @@ def _create_dir_structure(self): for case in range(self.nCases): # Recover information about current case for directory naming purposes inflow_deg_ = self.allCases['inflow_deg' ].sel(case=case).values - wakeSteering_ = self.allCases['wakeSteering' ].sel(case=case).values + #wakeSteering_ = self.allCases['wakeSteering' ].sel(case=case).values misalignment_ = self.allCases['misalignment' ].sel(case=case).values nADyn_ = self.allCases['nFullAeroDyn' ].sel(case=case).values nFED_ = self.allCases['nFulllElastoDyn'].sel(case=case).values @@ -1024,97 +1024,114 @@ def _create_all_cond(self): coords={'cond': np.arange(self.nConditions)} ) + + + def _create_all_cases(self): # Generate the different "cases" (inflow angle, and misalignment and wakesteer bools). # If misalignment true, then the actual yaw is yaw[turb]=np.random.uniform(low=-8.0, high=8.0). - - # Calculate the total number of cases given sweeps requested. Multipliers for wake steering, yaw misalignment, and reduced-order models - nWindDir = len(np.unique(self.inflow_deg)) - nCasesWSmultiplier = 2 if self.sweepWS else 1 + + # Set sweep bools and multipliers nCasesYMmultiplier = 2 if self.sweepYM else 1 nCasesROmultiplier = len(self.EDmodel) - - # Yaw multiplier, setup in the form of repeated wind directions with changing yaw - nCasesYawmultiplier = int(len(self.inflow_deg)/len(np.unique(self.inflow_deg))) - - # Aux nCases vars - nCases = int(nWindDir * nCasesWSmultiplier * nCasesYMmultiplier * nCasesROmultiplier * nCasesYawmultiplier) - nCasesWSfalse = int(nCases/nCasesWSmultiplier) - nCasesWStrue = int(nCases - nCasesWSfalse) - nCasesYMfalse = int(nCases/nCasesYMmultiplier) - nCasesYMtrue = int(nCases - nCasesYMfalse) - if self.verbose>2: print(f' Cases: nWindDir = {nWindDir} ') - if self.verbose>2: print(f' Cases: nCases = {nCases}') - if self.verbose>2: print(f' Cases: nCasesWStrue = {nCasesWStrue}') - if self.verbose>2: print(f' Cases: nCasesWSfalse = {nCasesWSfalse}') - if self.verbose>2: print(f' Cases: nCasesYMtrue = {nCasesYMtrue}') - if self.verbose>2: print(f' Cases: nCasesYMfalse = {nCasesYMfalse}') - - # Build an array of wind directions, with repeated values to account for wake steering and yaw misalign bools, and ROM options - windDir = np.repeat(self.inflow_deg, nCasesWSmultiplier*nCasesYMmultiplier*nCasesROmultiplier) - yawInit = np.repeat(self.yaw_init, nCasesWSmultiplier*nCasesYMmultiplier*nCasesROmultiplier, axis=0) - - # Build arrays of wake steering and yaw misalignment bools (done this way for clarity) - if self.sweepWS and self.sweepYM: - wakeSteering = np.tile([False, True, False, True], nWindDir*nCasesROmultiplier*nCasesYawmultiplier) - misalignment = np.tile([False, False, True, True], nWindDir*nCasesROmultiplier*nCasesYawmultiplier) - elif self.sweepWS and not self.sweepYM: - wakeSteering = np.tile([False, True ], nWindDir*nCasesROmultiplier*nCasesYawmultiplier) - misalignment = np.tile([False, False], nWindDir*nCasesROmultiplier*nCasesYawmultiplier) - elif not self.sweepWS and self.sweepYM: - wakeSteering = np.tile([False, False], nWindDir*nCasesROmultiplier*nCasesYawmultiplier) - misalignment = np.tile([False, True ], nWindDir*nCasesROmultiplier*nCasesYawmultiplier) - elif not self.sweepWS and not self.sweepYM: - wakeSteering = np.tile([False], nWindDir*nCasesROmultiplier*nCasesYawmultiplier) - misalignment = np.tile([False], nWindDir*nCasesROmultiplier*nCasesYawmultiplier) - - - # Create array of random numbers for yaw misalignment, and set it to zero where no yaw misalign is requested - yawMisalignedValue = np.random.uniform(size = [nCases,self.nTurbines], low=-8.0, high=8.0) - yawMisalignedValue[~misalignment,:] = 0 - - # Count number of simplified models to add that information to the xarray. If their length is 1, it means they weren't requested if len(self.ADmodel) == 1: - nADyn = self.nTurbines + self.sweepEDmodel = False self.sweepADmodel = False else: - nADyn = [ self.ADmodel[i].count('ADyn') for i in range(len(self.ADmodel)) ] - self.sweepADmodel = True - if len(self.EDmodel) == 1: - nFED = self.nTurbines - self.sweepEDmodel = False - else: - nFED = [ self.EDmodel[i].count('FED') for i in range(len(self.EDmodel)) ] + self.sweepEDmodel = True self.sweepADmodel = True - - # Come up with an ordered "yaw case" numbering for dir name - yawCase = np.arange(nCasesYawmultiplier)+1 - - # Assemble main case dataset, containing turbine info - self.nCases = nCases - self.allCases = xr.Dataset( - { - 'Tx': (['case','turbine'], np.repeat(self.wts_rot_ds['x'].values , nCasesWSmultiplier*nCasesYMmultiplier*nCasesROmultiplier*nCasesYawmultiplier, axis=0)), - 'Ty': (['case','turbine'], np.repeat(self.wts_rot_ds['y'].values , nCasesWSmultiplier*nCasesYMmultiplier*nCasesROmultiplier*nCasesYawmultiplier, axis=0)), - 'Tz': (['case','turbine'], np.repeat(self.wts_rot_ds['z'].values , nCasesWSmultiplier*nCasesYMmultiplier*nCasesROmultiplier*nCasesYawmultiplier, axis=0)), - 'D': (['case','turbine'], np.repeat(self.wts_rot_ds['D'].values , nCasesWSmultiplier*nCasesYMmultiplier*nCasesROmultiplier*nCasesYawmultiplier, axis=0)), - 'zhub': (['case','turbine'], np.repeat(self.wts_rot_ds['zhub'].values, nCasesWSmultiplier*nCasesYMmultiplier*nCasesROmultiplier*nCasesYawmultiplier, axis=0)), - 'yawmis': (['case','turbine'], yawMisalignedValue), - 'yaw': (['case','turbine'], yawInit), - 'yawCase': (['case'], np.repeat(yawCase, nWindDir*nCasesWSmultiplier*nCasesYMmultiplier*nCasesROmultiplier)), - 'ADmodel': (['case','turbine'], np.tile(np.repeat(self.ADmodel, nCasesWSmultiplier*nCasesYMmultiplier*nCasesYawmultiplier, axis=0),(nWindDir,1)) ), - 'EDmodel': (['case','turbine'], np.tile(np.repeat(self.EDmodel, nCasesWSmultiplier*nCasesYMmultiplier*nCasesYawmultiplier, axis=0),(nWindDir,1)) ), - 'nFullAeroDyn': (['case'], np.repeat(np.tile(nADyn, nWindDir), nCasesWSmultiplier*nCasesYMmultiplier*nCasesYawmultiplier)), - 'nFulllElastoDyn': (['case'], np.repeat(np.tile(nFED, nWindDir), nCasesWSmultiplier*nCasesYMmultiplier*nCasesYawmultiplier)), - 'wakeSteering': (['case'], wakeSteering), - 'misalignment': (['case'], misalignment), - 'inflow_deg': (['case'], windDir), - }, - coords={ - 'case': range(nCases), - 'turbine': range(self.nTurbines), - }, - ) + + # Initialize an empty array to accumulate individual cases + allCases = [] + # Get list of unique yaws, keeping the order + _, ind = np.unique(self.yaw_init, axis=0, return_index=True) + yaw_unique = self.yaw_init[np.sort(ind)] # duplicates removed, same order as original array + + # The main sweep on wind dir and yaw has been given by the user, such that + # only one loop is needed here. + for icase in range(len(self.inflow_deg)): + wdir = self.inflow_deg[icase] + yaw = self.yaw_init[icase] + yaw_case = np.where(np.all(yaw_unique == yaw, axis=1))[0][0]+1 + # Get turbine info + x = self.wts_rot_ds.sel(inflow_deg=wdir)['x'].values + y = self.wts_rot_ds.sel(inflow_deg=wdir)['y'].values + z = self.wts_rot_ds.sel(inflow_deg=wdir)['z'].values + D = self.wts_rot_ds.sel(inflow_deg=wdir)['D'].values + zhub = self.wts_rot_ds.sel(inflow_deg=wdir)['zhub'].values + + oneCase = xr.Dataset({ + 'Tx': (['case','turbine'], [x ]), + 'Ty': (['case','turbine'], [y ]), + 'Tz': (['case','turbine'], [z ]), + 'D': (['case','turbine'], [D ]), + 'zhub': (['case','turbine'], [zhub]), + 'yaw': (['case','turbine'], [yaw] ), + 'inflow_deg': (['case'], [wdir]), + 'yawCase': (['case'], [yaw_case]), + }, + coords={ + 'case': [icase], + 'turbine': np.arange(self.nTurbines), + }, + ) + allCases.append(oneCase) + allCases = xr.concat(allCases, dim='case') + + # ------------------------------------------------------- SWEEP ROM MODELS + # Get the number of cases at before this current sweep + nCases_before_sweep = len(allCases.case) + + # Concat instances of allCases and adjust the case numbering + ds = xr.concat([allCases for i in range(nCasesROmultiplier)], dim='case') + ds['case'] = np.arange(len(ds['case'])) + + # Create an empty array to fill. This way have a generic variable of type object + data = np.empty_like(ds['Tx'].data, dtype=object); data[:] = None + ds['EDmodel'] = (('case','turbine'), data) + ds['ADmodel'] = (('case','turbine'), data) + ds['nFulllElastoDyn'] = (('case'), np.zeros_like(ds['inflow_deg'])) + ds['nFullAeroDyn'] = (('case'), np.zeros_like(ds['inflow_deg'])) + + # Now, we fill the array with the new values for the proper sweep + for multi in range(nCasesROmultiplier): + for c in range(nCases_before_sweep): + currCase = nCases_before_sweep*multi + c + currEDmodel = np.array(self.EDmodel)[multi]#,:] + currADmodel = np.array(self.ADmodel)[multi]#,:] + + ds['EDmodel'].loc[dict(case=currCase, turbine=slice(None))] = currEDmodel + nFED = np.count_nonzero(currEDmodel == 'FED') + ds['nFulllElastoDyn'].loc[dict(case=currCase)] = nFED + + ds['ADmodel'].loc[dict(case=currCase, turbine=slice(None))] = currADmodel + nADyn = np.count_nonzero(currADmodel == 'ADyn') + ds['nFullAeroDyn'].loc[dict(case=currCase)] = nADyn + + allCases = ds.copy() + + # ------------------------------------------------- SWEEP YAW MISALIGNMENT + # Get the number of cases at before this current sweep + nCases_before_sweep = len(allCases.case) + + # Concat instances of allCases and adjust the case numbering + ds = xr.concat([allCases for i in range(nCasesYMmultiplier)], dim='case') + ds['case'] = np.arange(len(ds['case'])) + + # Create an full no-misalignment array to fill when non-aligned + ds['yawmis'] = (('case','turbine'), np.zeros_like(ds['yaw'])) + ds['misalignment'] = (('case'), np.full_like(ds['inflow_deg'], False, dtype=bool)) + + if self.sweepYM: + # Now, we fill the array with the new values on the second half (first half has no misalignment) + for c in range(nCases_before_sweep): + currCase = nCases_before_sweep + c + ds['yawmis'].loc[dict(case=currCase, turbine=slice(None))] = np.random.uniform(size=case.nTurbines,low=-8,high=8) + ds['misalignment'].loc[dict(case=currCase)] = True + + self.allCases = ds.copy() + self.nCases = len(self.allCases['case']) + def _rotate_wts(self): @@ -1292,8 +1309,7 @@ def getDomainParameters(self): self.nHighBoxCases = len(np.unique(self.inflow_deg)) # some wind dir might be repeated for sweep on yaws # Old method to get allHighBoxCases. Doesn't work well if I have weird manual sweeps - allHighBoxCases_old = self.allCases.where(~self.allCases['wakeSteering'],drop=True).drop_vars('wakeSteering')\ - .where(~self.allCases['misalignment'], drop=True).drop_vars('misalignment')\ + allHighBoxCases_old = self.allCases.where(~self.allCases['misalignment'], drop=True).drop_vars('misalignment')\ .where(self.allCases['nFullAeroDyn']==self.nTurbines, drop=True).drop_vars('ADmodel')\ .where(self.allCases['nFulllElastoDyn']==self.nTurbines, drop=True).drop_vars('EDmodel')\ .where(self.allCases['yawCase']==1, drop=True).drop_vars('yawCase') @@ -1545,7 +1561,7 @@ def TS_high_create_symlink(self): for seed in range(self.nSeeds): for case in range(self.nCases): # Let's check if the current case is source (has bts) or destination (needs a symlink to bts) - varsToDrop = ['wakeSteering','misalignment','yawmis','yaw','yawCase','ADmodel','EDmodel','nFullAeroDyn','nFulllElastoDyn'] + varsToDrop = ['misalignment','yawmis','yaw','yawCase','ADmodel','EDmodel','nFullAeroDyn','nFulllElastoDyn'] if case in self.allHighBoxCases['case']: src = os.path.join('../../../..', self.condDirList[cond], self.caseDirList[case], f'Seed_{seed}', 'TurbSim', f'HighT{t+1}.bts') xr_src = self.allCases.sel(case=case, drop=True).drop_vars(varsToDrop) From 35970add173c4627d849c833716ea8a0e01da73c Mon Sep 17 00:00:00 2001 From: Regis Thedin Date: Wed, 5 Jul 2023 10:32:18 -0600 Subject: [PATCH 089/124] FF: Fix bug with ROM strings on array --- pyFAST/fastfarm/FASTFarmCaseCreation.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pyFAST/fastfarm/FASTFarmCaseCreation.py b/pyFAST/fastfarm/FASTFarmCaseCreation.py index 5ad8e1f..350d8c7 100644 --- a/pyFAST/fastfarm/FASTFarmCaseCreation.py +++ b/pyFAST/fastfarm/FASTFarmCaseCreation.py @@ -1088,17 +1088,17 @@ def _create_all_cases(self): # Create an empty array to fill. This way have a generic variable of type object data = np.empty_like(ds['Tx'].data, dtype=object); data[:] = None - ds['EDmodel'] = (('case','turbine'), data) - ds['ADmodel'] = (('case','turbine'), data) - ds['nFulllElastoDyn'] = (('case'), np.zeros_like(ds['inflow_deg'])) - ds['nFullAeroDyn'] = (('case'), np.zeros_like(ds['inflow_deg'])) + ds['EDmodel'] = (('case','turbine'), data.copy()) + ds['ADmodel'] = (('case','turbine'), data.copy()) + ds['nFulllElastoDyn'] = (('case'), np.zeros_like(ds['inflow_deg']).copy()) + ds['nFullAeroDyn'] = (('case'), np.zeros_like(ds['inflow_deg']).copy()) # Now, we fill the array with the new values for the proper sweep for multi in range(nCasesROmultiplier): for c in range(nCases_before_sweep): currCase = nCases_before_sweep*multi + c - currEDmodel = np.array(self.EDmodel)[multi]#,:] - currADmodel = np.array(self.ADmodel)[multi]#,:] + currEDmodel = np.array(self.EDmodel)[multi] + currADmodel = np.array(self.ADmodel)[multi] ds['EDmodel'].loc[dict(case=currCase, turbine=slice(None))] = currEDmodel nFED = np.count_nonzero(currEDmodel == 'FED') From 6e5796334070e789a3d6efdde8356795ec0a8382 Mon Sep 17 00:00:00 2001 From: Regis Thedin Date: Wed, 5 Jul 2023 10:40:39 -0600 Subject: [PATCH 090/124] FF/bugfix: Change logic of high-res boxes links --- pyFAST/fastfarm/FASTFarmCaseCreation.py | 59 ++++++++++++++++++------- 1 file changed, 42 insertions(+), 17 deletions(-) diff --git a/pyFAST/fastfarm/FASTFarmCaseCreation.py b/pyFAST/fastfarm/FASTFarmCaseCreation.py index 350d8c7..f29a3e3 100644 --- a/pyFAST/fastfarm/FASTFarmCaseCreation.py +++ b/pyFAST/fastfarm/FASTFarmCaseCreation.py @@ -1554,25 +1554,44 @@ def TS_high_slurm_submit(self, qos='normal', A=None, t=None): def TS_high_create_symlink(self): # Create symlink of all the high boxes for the cases with wake steering and yaw misalignment. These are the "repeated" boxes + + if self.verbose>0: + print(f'Creating symlinks for all the high-resolution boxes') + notepath = os.getcwd() os.chdir(self.path) for cond in range(self.nConditions): - for t in range(self.nTurbines): - for seed in range(self.nSeeds): - for case in range(self.nCases): - # Let's check if the current case is source (has bts) or destination (needs a symlink to bts) - varsToDrop = ['misalignment','yawmis','yaw','yawCase','ADmodel','EDmodel','nFullAeroDyn','nFulllElastoDyn'] - if case in self.allHighBoxCases['case']: - src = os.path.join('../../../..', self.condDirList[cond], self.caseDirList[case], f'Seed_{seed}', 'TurbSim', f'HighT{t+1}.bts') - xr_src = self.allCases.sel(case=case, drop=True).drop_vars(varsToDrop) - continue - else: - dst = os.path.join(self.condDirList[cond], self.caseDirList[case], f'Seed_{seed}', 'TurbSim', f'HighT{t+1}.bts') - xr_dst = self.allCases.sel(case=case, drop=True).drop_vars(varsToDrop) - - # Let's make sure the src and destination are the same case, except wake steering and yaw misalign bools - xr.testing.assert_equal(xr_src, xr_dst) - + for case in range(self.nCases): + # In order to do the symlink let's check if the current case is source (has bts). If so, skip if. If not, find its equivalent source + casematch = self.allHighBoxCases['case'] == case + if len(np.where(casematch)) != 1: + raise ValueError (f'Something is wrong with your allHighBoxCases array. Found repeated case number. Stopping') + + src_id = np.where(casematch)[0] + + if len(src_id) == 1: + # Current case is source (contains bts). Skipping + continue + + # If we are here, the case is destination. Let's find the first case with the same wdir for source + varsToDrop = ['misalignment','yawmis','yaw','yawCase','ADmodel','EDmodel','nFullAeroDyn','nFulllElastoDyn'] + dst_xr = self.allCases.sel(case=case, drop=True).drop_vars(varsToDrop) + currwdir = dst_xr['inflow_deg'] + + src_xr = self.allHighBoxCases.where(self.allHighBoxCases['inflow_deg'] == currwdir, drop=True).drop_vars(varsToDrop) + src_case = src_xr['case'].values[0] + src_xr = src_xr.sel(case=src_case, drop=True) + + # Let's make sure the src and destination are the same case, except yaw misalignment and ROM bools, and yaw angles + # The xarrays we are comparing here contains all self.nTurbines turbines and no info about seed + xr.testing.assert_equal(src_xr, dst_xr) + + # Now that we have the correct arrays, we perform the loop on the turbines and seeds + for t in range(self.nTurbines): + for seed in range(self.nSeeds): + src = os.path.join('../../../..', self.condDirList[cond], self.caseDirList[src_case], f'Seed_{seed}', 'TurbSim', f'HighT{t+1}.bts') + dst = os.path.join(self.condDirList[cond], self.caseDirList[case], f'Seed_{seed}', 'TurbSim', f'HighT{t+1}.bts') + try: os.symlink(src, dst) except FileExistsError: @@ -1580,7 +1599,6 @@ def TS_high_create_symlink(self): os.chdir(notepath) - def FF_setup(self, outlistFF=None, **kwargs): ''' @@ -1795,9 +1813,15 @@ def _FF_setup_LES(self, seedsToKeep=1): def _FF_setup_TS(self): + # Let's first create the symlinks for the high-res boxes. Remember that only the cases with + # unique winddir in self.allHighBoxCases have executed high-res boxes, the rest is all links + self.TS_high_create_symlink() + # Loops on all conditions/cases and cases for FAST.Farm for cond in range(self.nConditions): + print(f'Processing condition {self.condDirList[cond]}') for case in range(self.nCases): + print(f' Processing all {self.nSeeds} seeds of case {self.caseDirList[case]}', end='\r') for seed in range(self.nSeeds): seedPath = os.path.join(self.path, self.condDirList[cond], self.caseDirList[case], f'Seed_{seed}') @@ -1876,6 +1900,7 @@ def _FF_setup_TS(self): ff_file['WindVelZ'] = ', '.join(map(str, zWT[:9]+self.zhub)) ff_file.write(outputFSTF) + print(f'Done processing condition {self.condDirList[cond]} ') return From aac727c90c77b9f6ef5fc0251177b46c1af9a31e Mon Sep 17 00:00:00 2001 From: Regis Thedin Date: Wed, 5 Jul 2023 10:44:59 -0600 Subject: [PATCH 091/124] FF: Add check on FF output file --- pyFAST/fastfarm/FASTFarmCaseCreation.py | 26 +++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/pyFAST/fastfarm/FASTFarmCaseCreation.py b/pyFAST/fastfarm/FASTFarmCaseCreation.py index f29a3e3..d983952 100644 --- a/pyFAST/fastfarm/FASTFarmCaseCreation.py +++ b/pyFAST/fastfarm/FASTFarmCaseCreation.py @@ -1302,6 +1302,7 @@ def getDomainParameters(self): # If the low box setup hasn't been called (e.g. LES run), do it once to get domain extents if not self.TSlowBoxFilesCreatedBool: + if self.verbose>1: print(' Running a TurbSim setup once to get domain extents') self.TS_low_setup(writeFiles=False, runOnce=True) # Figure out how many (and which) high boxes actually need to be executed. Remember that wake steering, yaw misalignment, SED/ADsk models, @@ -2106,6 +2107,31 @@ def FF_slurm_submit(self, qos='normal', A=None, t=None, delay=4): time.sleep(delay) # Sometimes the same job gets submitted twice. This gets around it. + def FF_check_output(self): + ''' + Check all the FF output files and look for the termination string. + ''' + + ff_run_failed = False + for cond in range(self.nConditions): + for case in range(self.nCases): + for seed in range(self.nSeeds): + # Let's check the last line of the logfile + fflog_path = os.path.join(self.path, self.condDirList[cond], self.caseDirList[case], f'Seed_{seed}', f'log.fastfarm.seed{seed}.txt') + if not os.path.isfile(fflog_path): + print(f'{self.condDirList[cond]}, {self.caseDirList[case]}, seed {seed}: FAST.Farm log file does not exist.') + ff_run_failed=True + + else: + tail_command = ['tail', '-n', '2', fflog_path] + tail = subprocess.check_output(tail_command).decode('utf-8') + if tail.strip() != 'FAST.Farm terminated normally.': + print(f'{self.condDirList[cond]}, {self.caseDirList[case]}, seed {seed}: FAST.Farm did not complete successfully.') + ff_run_failed=True + + if ff_run_failed: + print('') + raise ValueError(f'Not all FAST.Farm runs were successful') From 58ad7ad7e6d3a21e8e5dcf128b61e31b347a76a4 Mon Sep 17 00:00:00 2001 From: Regis Thedin Date: Fri, 7 Jul 2023 10:16:07 -0600 Subject: [PATCH 092/124] FF: Add helpful progress messages --- pyFAST/fastfarm/FASTFarmCaseCreation.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pyFAST/fastfarm/FASTFarmCaseCreation.py b/pyFAST/fastfarm/FASTFarmCaseCreation.py index d983952..a93ebee 100644 --- a/pyFAST/fastfarm/FASTFarmCaseCreation.py +++ b/pyFAST/fastfarm/FASTFarmCaseCreation.py @@ -2115,6 +2115,7 @@ def FF_check_output(self): ff_run_failed = False for cond in range(self.nConditions): for case in range(self.nCases): + if self.verbose>1: print(f'Checking {self.condDirList[cond]}, {self.caseDirList[case]}') for seed in range(self.nSeeds): # Let's check the last line of the logfile fflog_path = os.path.join(self.path, self.condDirList[cond], self.caseDirList[case], f'Seed_{seed}', f'log.fastfarm.seed{seed}.txt') @@ -2132,6 +2133,8 @@ def FF_check_output(self): if ff_run_failed: print('') raise ValueError(f'Not all FAST.Farm runs were successful') + else: + print(f'All cases finished successfully.') From 6e215bdc5afa374622fb5d4ba124092fe91b8433 Mon Sep 17 00:00:00 2001 From: Emmanuel Branlard Date: Mon, 17 Jul 2023 22:37:19 -0600 Subject: [PATCH 093/124] IO: adding example for MannBox file --- data/example_files/MannBox_32x4x8.bin | Bin 0 -> 4096 bytes .../input_output/examples/Example_MannBox.py | 90 ++++++++++++++++++ pyFAST/input_output/mannbox_file.py | 17 ++-- .../tests/example_files/MannBox_2x4x8.bin | 1 + pyFAST/input_output/tests/test_mannbox.py | 23 +++++ 5 files changed, 122 insertions(+), 9 deletions(-) create mode 100644 data/example_files/MannBox_32x4x8.bin create mode 100644 pyFAST/input_output/examples/Example_MannBox.py create mode 100644 pyFAST/input_output/tests/example_files/MannBox_2x4x8.bin create mode 100644 pyFAST/input_output/tests/test_mannbox.py diff --git a/data/example_files/MannBox_32x4x8.bin b/data/example_files/MannBox_32x4x8.bin new file mode 100644 index 0000000000000000000000000000000000000000..ee12f2391ebf694d6ecef95fd800c96794ce6a8b GIT binary patch literal 4096 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 Dp^NP0 literal 0 HcmV?d00001 diff --git a/pyFAST/input_output/examples/Example_MannBox.py b/pyFAST/input_output/examples/Example_MannBox.py new file mode 100644 index 0000000..380841b --- /dev/null +++ b/pyFAST/input_output/examples/Example_MannBox.py @@ -0,0 +1,90 @@ +""" +Example usage for the MannBox class. +""" + +import os +import numpy as np +import matplotlib.pyplot as plt +from pyFAST.input_output.mannbox_file import MannBoxFile + + +scriptDir = os.path.dirname(__file__) + +# --- Inputs +file_u = os.path.join(scriptDir, '../../../data/example_files/MannBox_32x4x8.bin') +dy = 3 # horizontal spacing [m] +dz = 10 # vertical spacing [m] + +# --- Read Mann Box file and display main properties +mb = MannBoxFile(file_u, dy=dy, dz=dz) # create a MannBoxFile object, store it in mb +print(mb) # print misc info about the object +print(mb['field'].shape) # print the shape of your field. +nx, ny, nz = mb['field'].shape # Store the field dimension + + +# --- Plot values of the field at given indices. Note this example has only 2 values in x +# NOTE: can also be obtained directly using: +# u = mb.valuesAt(y=5, z=50) +t = mb.t(dx=1, U=10) +u = mb['field'][:, 3, 5] # Value at indices iy=3, iz=5 +plt.figure(1) +plt.plot(t, u, label='Input') +plt.xlabel('time [s]') +plt.ylabel('u [m/s]') +plt.title('Value of field at chosen location as function of x/t') + + +# --- Plot vertical profile averaged over all x and y +# NOTE: can also be obtained directly using: +# z, means, stds = mb.vertProfile +u_mean_vertical = np.mean(np.mean(mb['field'][:,:,:], axis=0), axis=0) +plt.figure(2) +plt.plot(u_mean_vertical, mb.z, label='Input') +plt.xlabel('u mean [m/s]') +plt.ylabel('z [m]') +plt.title('Average vertical profile for all x and y') + +# --- Compute the mean over all "x" values for each points in the y-z plane +UmeanYZ = np.mean(mb['field'][:,:,:],axis=0) # This has shape ny x nz + +# --- Modify the field (some examples) +mb['field'] -= UmeanYZ # remove the mean of all datapoints along x +mb['field'][:,:,:] *= 2 # multiply the field by 10 everywhere +mb['field'][:,:,:] += UmeanYZ # add the mean again +mb['field'][:,:,:] += 0.5 # add 0.5 everywhere in the field +mb['field'][:,:,0] = 0 # set the velocity to be zero for the first z index + + +# --- Plot value of the field again +u = mb['field'][:, 3, 5] # Value at indices iy=3, iz=5 +plt.figure(1) +plt.plot(t, u, label='Modified') +plt.xlabel('time [s]') +plt.ylabel('u [m/s]') + +# --- Plot the vertical profile again +u_mean_vertical2 = np.mean(np.mean(mb['field'][:,:,:], axis=0), axis=0) +plt.figure(2) +plt.plot(u_mean_vertical2, mb.z, '--', label='Modified') +plt.xlabel('u mean [m/s]') +plt.ylabel('z [m]') + + + + +# --- Write the modified field to disk with another filename +outFile = file_u.replace('.bin','_modified.bin') +mb.write(outFile) +print('File written: ', outFile) + + + +if __name__ == "__main__": + plt.show() + +if __name__=='__test__': + try: + os.remove(outFile) + except: + pass + diff --git a/pyFAST/input_output/mannbox_file.py b/pyFAST/input_output/mannbox_file.py index 3fa4c6f..d8dd6c4 100644 --- a/pyFAST/input_output/mannbox_file.py +++ b/pyFAST/input_output/mannbox_file.py @@ -47,8 +47,8 @@ class MannBoxFile(File): print(mb['field'].shape) # Use methods to extract relevant values - u,v,w = my.valuesAt(y=10.5, z=90) - z, means, stds = mb.vertProfile() + u = mb.valuesAt(y=10.5, z=90) + z, means, stds = mb.vertProfile # Write to a new file mb.write('Output_1024x16x16.u') @@ -119,6 +119,7 @@ def read(self, filename=None, N=None, dy=1, dz=1, y0=None, z0=0, zMid=None): else: raise BrokenFormatError('Reading a Mann box requires the knowledge of the dimensions. The dimensions can be inferred from the filename, for instance: `filebase_1024x32x32.u`. Try renaming your file such that the three last digits are the dimensions in x, y and z.') nx,ny,nz=N + def _read_buffered(): data=np.zeros((nx,ny,nz),dtype=np.float32) with open(self.filename, mode='rb') as f: @@ -143,10 +144,6 @@ def _read_nonbuffered(): self['y0']=y0 self['z0']=z0 self['zMid']=zMid -# print('1',self['field'][:,-1,0]) -# print('2',self['field'][0,-1::-1,0]) -# print('3',self['field'][0,-1,:]) - def write(self, filename=None): """ Write mann box """ @@ -169,11 +166,13 @@ def __repr__(self): s+='| min: {}, max: {}, mean: {} \n'.format(np.min(self['field']), np.max(self['field']), np.mean(self['field'])) s+='| - dy, dz: {}, {}\n'.format(self['dy'], self['dz']) s+='| - y0, z0 zMid: {}, {}, {}\n'.format(self['y0'], self['z0'], self['zMid']) - s+='|useful getters: y, z, _iMid, fromTurbSim\n' z=self.z y=self.y - s+='| y: [{} ... {}], dy: {}, n: {} \n'.format(y[0],y[-1],self['dy'],len(y)) - s+='| z: [{} ... {}], dz: {}, n: {} \n'.format(z[0],z[-1],self['dz'],len(z)) + s+='| * y: [{} ... {}], dy: {}, n: {} \n'.format(y[0],y[-1],self['dy'],len(y)) + s+='| * z: [{} ... {}], dz: {}, n: {} \n'.format(z[0],z[-1],self['dz'],len(z)) + s+='|useful functions:\n' + s+='| - t(dx, U)\n' + s+='| - valuesAt(y,z), vertProfile, fromTurbSim(*), _iMid()\n' return s diff --git a/pyFAST/input_output/tests/example_files/MannBox_2x4x8.bin b/pyFAST/input_output/tests/example_files/MannBox_2x4x8.bin new file mode 100644 index 0000000..b5f9c40 --- /dev/null +++ b/pyFAST/input_output/tests/example_files/MannBox_2x4x8.bin @@ -0,0 +1 @@ +.•À½ À­ÜÀhÜÀ1À"i^À’§\À”žÀ²¯Ö¿ÀMG¿êQ~¿'—?*î@w™&@6 ¥>y඿¯Ž?»Þ?-ø @¡Ö²@E=A’ AÉT„@%@#@`¢:À‘× Àd’ñ¿Âv#¿{QO@Fä:@µù>Ò=Àž–À“wÀŠ{À¼i‰ÀË(À€—jÀÚ\À‘ä›ÀÞì¿ h.¿kP«¿©s±?x…@-E@Ïý|?YE˜¿Î0?'6Â?§ª@ë-³@—‡Ak4ò@Ó*”@ËÑ"@<§-ÀGGÀ¶íÀ^ô-¿«„`@Êk@Ì/7>úÀ \ No newline at end of file diff --git a/pyFAST/input_output/tests/test_mannbox.py b/pyFAST/input_output/tests/test_mannbox.py new file mode 100644 index 0000000..a72895b --- /dev/null +++ b/pyFAST/input_output/tests/test_mannbox.py @@ -0,0 +1,23 @@ +import unittest +import os +import numpy as np +from pyFAST.input_output.mannbox_file import MannBoxFile +from pyFAST.input_output.tests.helpers_for_test import MyDir, reading_test + +class Test(unittest.TestCase): + + def test_001_read_all(self, DEBUG=True): + reading_test('MannBox_*.*', MannBoxFile) + + def test_MannBox(self): + # --- Test read/write + F = MannBoxFile(os.path.join(MyDir,'MannBox_2x4x8.bin')) + F.write( os.path.join(MyDir,'MannBox_2x4x8_TMP.bin')) + F2= MannBoxFile(os.path.join(MyDir,'MannBox_2x4x8_TMP.bin')) + os.remove( os.path.join(MyDir,'MannBox_2x4x8_TMP.bin')) + np.testing.assert_almost_equal(F['field'].shape ,[2,4,8]) + np.testing.assert_almost_equal(F['field'][:,:,:],F2['field'][:,:,:],8) + np.testing.assert_almost_equal(F['field'][1,3,5], -3.6654968, 6) + +if __name__ == '__main__': + unittest.main() From abcf1b0f72c0fd663bb55afc82de1fac1f293394 Mon Sep 17 00:00:00 2001 From: Emmanuel Branlard Date: Tue, 18 Jul 2023 20:37:15 -0600 Subject: [PATCH 094/124] IO:FastLin: slightly more robust read and standalone file --- .../input_output/fast_linearization_file.py | 111 ++++++++++++------ 1 file changed, 74 insertions(+), 37 deletions(-) diff --git a/pyFAST/input_output/fast_linearization_file.py b/pyFAST/input_output/fast_linearization_file.py index ed582e5..997ad5b 100644 --- a/pyFAST/input_output/fast_linearization_file.py +++ b/pyFAST/input_output/fast_linearization_file.py @@ -1,11 +1,10 @@ +import os import numpy as np -import pandas as pd import re try: from .file import File, WrongFormatError, BrokenFormatError except: File = dict - class WrongFormatError(Exception): pass class BrokenFormatError(Exception): pass class FASTLinearizationFile(File): @@ -42,6 +41,27 @@ def defaultExtensions(): def formatName(): return 'FAST linearization output' + def __init__(self, filename=None, **kwargs): + """ Class constructor. If a `filename` is given, the file is read. """ + self.filename = filename + if filename: + self.read(**kwargs) + + def read(self, filename=None, **kwargs): + """ Reads the file self.filename, or `filename` if provided """ + + # --- Standard tests and exceptions (generic code) + if filename: + self.filename = filename + if not self.filename: + raise Exception('No filename provided') + if not os.path.isfile(self.filename): + raise OSError(2,'File not found:',self.filename) + if os.stat(self.filename).st_size == 0: + raise EmptyFileError('File is empty:',self.filename) + # --- Calling (children) function to read + self._read(**kwargs) + def _read(self, *args, **kwargs): self['header']=[] @@ -61,7 +81,7 @@ def readToMarker(fid, marker, nMax): lines.append(line.strip()) return lines, line - def readOP(fid, n): + def readOP(fid, n, name=''): OP=[] Var = {'RotatingFrame': [], 'DerivativeOrder': [], 'Description': []} colNames=fid.readline().strip() @@ -86,14 +106,30 @@ def readOP(fid, n): Var['Description'].append(' '.join(sp[iRot+1:]).strip()) if i>=n-1: break + OP=np.asarray(OP) return OP, Var - def readMat(fid, n, m): - vals=[f.readline().strip().split() for i in np.arange(n)] -# try: - return np.array(vals).astype(float) -# except ValueError: -# import pdb; pdb.set_trace() + def readMat(fid, n, m, name=''): + pattern = re.compile(r"[\*]+") + vals=[pattern.sub(' inf ', fid.readline().strip() ).split() for i in np.arange(n)] + vals = np.array(vals) + try: + vals = np.array(vals).astype(float) # This could potentially fail + except: + raise Exception('Failed to convert into an array of float the matrix `{}`\n\tin linfile: {}'.format(name, self.filename)) + if vals.shape[0]!=n or vals.shape[1]!=m: + shape1 = vals.shape + shape2 = (n,m) + raise Exception('Shape of matrix `{}` has wrong dimension ({} instead of {})\n\tin linfile: {}'.format(name, shape1, shape2, name, self.filename)) + + nNaN = sum(np.isnan(vals.ravel())) + nInf = sum(np.isinf(vals.ravel())) + if nInf>0: + raise Exception('Some ill-formated/infinite values (e.g. `*******`) were found in the matrix `{}`\n\tin linflile: {}'.format(name, self.filename)) + if nNaN>0: + raise Exception('Some NaN values were found in the matrix `{}`\n\tin linfile: `{}`.'.format(name, self.filename)) + return vals + # Reading with open(self.filename, 'r', errors="surrogateescape") as f: @@ -119,35 +155,35 @@ def readMat(fid, n, m): except: self['WindSpeed'] = None - KEYS=['Order of','A:','B:','C:','D:','ED M:', 'dUdu','dUdy'] - for i, line in enumerate(f): line = line.strip() - KeyFound=any([line.find(k)>=0 for k in KEYS]) - if KeyFound: - if line.find('Order of continuous states:')>=0: - self['x'], self['x_info'] = readOP(f, nx) - elif line.find('Order of continuous state derivatives:')>=0: - self['xdot'], self['xdot_info'] = readOP(f, nx) - elif line.find('Order of inputs')>=0: - self['u'], self['u_info'] = readOP(f, nu) - elif line.find('Order of outputs')>=0: - self['y'], self['y_info'] = readOP(f, ny) - elif line.find('A:')>=0: - self['A'] = readMat(f, nx, nx) - elif line.find('B:')>=0: - self['B'] = readMat(f, nx, nu) - elif line.find('C:')>=0: - self['C'] = readMat(f, ny, nx) - elif line.find('D:')>=0: - self['D'] = readMat(f, ny, nu) - elif line.find('dUdu:')>=0: - self['dUdu'] = readMat(f, nu, nu) - elif line.find('dUdy:')>=0: - self['dUdy'] = readMat(f, nu, ny) - elif line.find('ED M:')>=0: - self['EDDOF'] = line[5:].split() - self['M'] = readMat(f, 24, 24) + if line.find('Order of continuous states:')>=0: + self['x'], self['x_info'] = readOP(f, nx, 'x') + elif line.find('Order of continuous state derivatives:')>=0: + self['xdot'], self['xdot_info'] = readOP(f, nx, 'xdot') + elif line.find('Order of inputs')>=0: + self['u'], self['u_info'] = readOP(f, nu, 'u') + elif line.find('Order of outputs')>=0: + self['y'], self['y_info'] = readOP(f, ny, 'y') + elif line.find('A:')>=0: + self['A'] = readMat(f, nx, nx, 'A') + elif line.find('B:')>=0: + self['B'] = readMat(f, nx, nu, 'B') + elif line.find('C:')>=0: + self['C'] = readMat(f, ny, nx, 'C') + elif line.find('D:')>=0: + self['D'] = readMat(f, ny, nu, 'D') + elif line.find('dUdu:')>=0: + self['dUdu'] = readMat(f, nu, nu,'dUdu') + elif line.find('dUdy:')>=0: + self['dUdy'] = readMat(f, nu, ny,'dUdy') + elif line.find('StateRotation:')>=0: + pass + # TODO + #StateRotation: + elif line.find('ED M:')>=0: + self['EDDOF'] = line[5:].split() + self['M'] = readMat(f, 24, 24,'M') def toString(self): s='' @@ -319,7 +355,8 @@ def udescr(self): else: return [] - def _toDataFrame(self): + def toDataFrame(self): + import pandas as pd dfs={} xdescr_short = self.xdescr() From c857a109e69c8b53962bed92f6177018500d955c Mon Sep 17 00:00:00 2001 From: Emmanuel Branlard Date: Wed, 19 Jul 2023 17:27:21 -0600 Subject: [PATCH 095/124] Lin: avoid warning in csv_read --- pyFAST/linearization/campbell.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyFAST/linearization/campbell.py b/pyFAST/linearization/campbell.py index 9b77f73..59c9c31 100644 --- a/pyFAST/linearization/campbell.py +++ b/pyFAST/linearization/campbell.py @@ -167,7 +167,7 @@ def postproMBC(xlsFile=None, csvModesIDFile=None, xlssheet=None, verbose=True, W for i,v in enumerate(WS): OPFile = csvBase+'Campbell_Point{:02d}{:}.csv'.format(i+1,suffix) #print(OPFile, WS[i], RPM[i]) - Points[i] = pd.read_csv(OPFile, sep = ',', header=None) + Points[i] = pd.read_csv(OPFile, sep = ',', header=None, dtype='object') else: raise Exception('Provide either an Excel file or a csv (ModesID) file') # --- Mode Identification @@ -258,7 +258,7 @@ def postproMBC(xlsFile=None, csvModesIDFile=None, xlssheet=None, verbose=True, W UnMapped_RPM = np.concatenate((UnMapped_RPM, rpm)) else: if all(ModeIndices==-1): - print('Skipping mode number ',iMode) + print('Mode not IDd: {}, name: {}.'.format(iMode, ModeName)) else: f=[] d=[] From 1bb67f93edcbca30a1e1a4667d0e0e094af2c5b0 Mon Sep 17 00:00:00 2001 From: Emmanuel Branlard Date: Mon, 24 Jul 2023 14:15:09 -0600 Subject: [PATCH 096/124] Fatigue: slight change of interface, more docs, unification of RFC algo --- pyFAST/tools/fatigue.py | 482 +++++++++++++++++++++++++---- pyFAST/tools/tests/test_fatigue.py | 103 +----- 2 files changed, 429 insertions(+), 156 deletions(-) diff --git a/pyFAST/tools/fatigue.py b/pyFAST/tools/fatigue.py index 2b0ada4..58ca008 100644 --- a/pyFAST/tools/fatigue.py +++ b/pyFAST/tools/fatigue.py @@ -1,63 +1,128 @@ -# --------------------------------------------------------------------------------} -# --- Info -# --------------------------------------------------------------------------------{ -# Tools for fatigue analysis -# -# Taken from: -# repository: wetb -# package: wetb.fatigue_tools, -# institution: DTU wind energy, Denmark -# main author: mmpe -''' -Created on 04/03/2013 -@author: mmpe +""" +Tools for fatigue analysis -'eq_load' calculate equivalent loads using one of the two rain flow counting methods -'cycle_matrix' calculates a matrix of cycles (binned on amplitude and mean value) -'eq_load_and_cycles' is used to calculate eq_loads of multiple time series (e.g. life time equivalent load) +Main functions: +- equivalent_load: calculate damage equivalent load for a given signal +- find_range_count: returns range and number of cycles for a given signal -The methods uses the rainflow counting routines (See documentation in top of methods): -- 'rainflow_windap': (Described in "Recommended Practices for Wind Turbine Testing - 3. Fatigue Loads", - 2. edition 1990, Appendix A) -or -- 'rainflow_astm' (based on the c-implementation by Adam Nieslony found at the MATLAB Central File Exchange - http://www.mathworks.com/matlabcentral/fileexchange/3026) -''' +Subfunctions: +- eq_load: calculate equivalent loads using one of the two rain flow counting methods +- cycle_matrix: calculates a matrix of cycles (binned on amplitude and mean value) +- eq_load_and_cycles: calculate eq_loads of multiple time series (e.g. life time equivalent load) + + +Main aglorithms for rain flow counting: +- rainflow_windap: taken from [2], based on [3] +- rainflow_astm: taken from [2], based [4] +- fatpack: using [5] + + +References: + [1] Hayman (2012) MLife theory manual for Version 1.00 + [2] Wind energy toolbox, wetb.fatigue_tools, DTU wind energy, Denmark + [3] "Recommended Practices for Wind Turbine Testing - 3. Fatigue Loads", 2. edition 1990, Appendix A + [4] Adam Nieslony - Rainflow Counting Algorithm, MATLAB Central File Exchange + http://www.mathworks.com/matlabcentral/fileexchange/3026) + [5] Fatpack - Python package + https://github.com/Gunnstein/fatpack + + +""" import warnings import numpy as np -__all__ = ['rainflow_astm', 'rainflow_windap','eq_load','eq_load_and_cycles','cycle_matrix','cycle_matrix2'] +__all__ = ['equivalent_load', 'find_range_count'] +__all__ += ['rainflow_astm', 'rainflow_windap','eq_load','eq_load_and_cycles','cycle_matrix','cycle_matrix2'] -def equivalent_load(time, signal, m=3, Teq=1, nBins=100, method='rainflow_windap'): +def equivalent_load(time, signal, m=3, Teq=1, bins=100, method='rainflow_windap', + meanBin=True, binStartAt0=False, + outputMore=False, debug=False): """Equivalent load calculation - Calculate the equivalent loads for a list of Wohler exponent - - Parameters - ---------- - time : array-like, the time values corresponding to the signal (s) - signals : array-like, the load signal - m : Wohler exponent (default is 3) - Teq : The equivalent period (Default 1Hz) - nBins : Number of bins in rainflow count histogram - method: 'rainflow_windap, rainflow_astm, fatpack - - Returns - ------- - Leq : the equivalent load for given m and Tea + Calculate the damage equivalent load for a given signal and a given Wohler exponent + + INPUTS + - time : array-like, the time values corresponding to the signal (s) + - signals : array-like, the load signal + - m : Wohler exponent (default is 3) + - Teq : The equivalent period (Default 1, for 1Hz) + - bins : Number of bins in rainflow count histogram + - method: rain flow counting algorithm: 'rainflow_windap', 'rainflow_astm' or 'fatpack' + - meanBin: if True, use the mean of the ranges within a bin (recommended) + otherwise use the middle of the bin (not recommended). + - binStartAt0: if True bins start at zero. Otherwise, start a lowest range + - outputMore: if True, returns range, cycles and bins as well + + OUTPUTS + - Leq : the equivalent load for given m and Teq + + or (if outputMore is True ) + + - Leq, S, N, bins, DELi: + - S: ranges + - N: cycles + - bins: bin edges + - DELi: component 'i' of the DEL (for cycle i) """ time = np.asarray(time) signal = np.asarray(signal) + + # Remove nan, might not be the cleanest + b = ~np.isnan(signal) + signal = signal[b] + time = time[b] + T = time[-1]-time[0] # time length of signal (s) - neq = T/Teq # number of equivalent periods - rainflow_func_dict = {'rainflow_windap':rainflow_windap, 'rainflow_astm':rainflow_astm} + neq = T/Teq # number of equivalent periods, see Eq. (26) of [1] + + # --- Range (S) and counts (N) + N, S, bins = find_range_count(signal, bins=bins, method=method, meanBin=meanBin, binStartAt0=binStartAt0) + + # --- get DEL + DELi = S**m * N / neq + Leq = DELi.sum() ** (1/m) # See e.g. eq. (30) of [1] + + if debug: + for i,(b,n,s,DEL) in enumerate(zip(bins, N, S, DELi)): + if n>0: + print('Bin {:3d}: [{:6.1f}-{:6.1f}] Mid:{:6.1f} - Mean:{:6.1f} Counts:{:4.1f} DEL:{:8.1f} Fraction:{:3.0f}%'.format(i,b,bins[i+1],(b+bins[i+1])/2,s,n,DEL,DEL/Leq**m*100)) + if outputMore: + return Leq, S, N, bins, DELi + else: + return Leq + + +def find_range_count(signal, bins, method='rainflow_windap', meanBin=True, binStartAt0=True): + """ + Returns number of cycles `N` for each range range `S` + Equidistant bins are setup based on the min/max of the signal. + INPUTS: + - signal: array + - bins : 1d-array, int + If bins is a sequence, left edges (and the rightmost edge) of the bins. + If bins is an int, a sequence is created dividing the range `min`--`max` of signal into `bins` number of equally sized bins. + OUTPUTS: + - N: number of cycles for each bin + - S: Ranges for each bin + S is either the center of the bin (meanBin=False) + or + S is the mean of the ranges within this bin (meanBin=True) + - S_bin_edges: edges of the bins + """ + if method in rainflow_func_dict.keys(): - # Call wetb function for one m - Leq = eq_load(signal, m=[m], neq=neq, no_bins=nBins, rainflow_func=rainflow_func_dict[method])[0][0] + rainflow_func = rainflow_func_dict[method] + N, S, S_bin_edges, _, _ = cycle_matrix(signal, ampl_bins=bins, mean_bins=1, rainflow_func=rainflow_func, binStartAt0=binStartAt0) + S_bin_edges = S_bin_edges.flatten() + N = N.flatten() + S = S.flatten() + S_mid = (S_bin_edges[:-1] + S_bin_edges[1:]) / 2 + if not meanBin: + S=S_mid elif method=='fatpack': import fatpack @@ -67,16 +132,74 @@ def equivalent_load(time, signal, m=3, Teq=1, nBins=100, method='rainflow_windap except IndexError: # Currently fails for constant signal return np.nan - # find range count and bin - Nrf, Srf = fatpack.find_range_count(ranges, nBins) - # get DEL - DELs = Srf**m * Nrf / neq - Leq = DELs.sum() ** (1/m) + # --- Legacy fatpack + # if (not binStartAt0) and (not meanBin): + # N, S = fatpack.find_range_count(ranges, bins) + # --- Setup bins + # If binStartAt0 is True, the same bins as WINDAP are used + S_bin_edges = create_bins(ranges, bins, binStartAt0=binStartAt0) + # --- Using bin_count to get value at center of bins + N, S = bin_count(ranges, S_bin_edges, meanBin=meanBin) else: - raise NotImplementedError(method) - - return Leq + raise NotImplementedError('Rain flow algorithm {}'.format(method)) + + # Remove NaN + b = np.isnan(S) + S[b] = 0 + N[b] = 0 + return N, S, S_bin_edges + +def create_bins(x, bins, binStartAt0=False): + """ + Equidistant bins are setup based on the min/max of the x, unless the user provided the bins as a sequence. + INPUTS: + - x: array + - bins : 1d-array, int + If bins is a sequence, left edges (and the rightmost edge) of the bins. + If bins is an int, a sequence is created dividing the range `min`--`max` of x into `bins` number of equally sized bins. + OUTPUTS: + - bins: + """ + if isinstance(bins, int): + xmax = np.max(x) + xmin, xmax = np.min(x), np.max(x) + if binStartAt0: + xmin = 0 + else: + xmin = np.min(x) + if xmin==xmax: + # I belive that's what's done by histogram. double check + xmin=xmin-0.5 + xmax=xmax+0.5 + bins = np.linspace(xmin, xmax, num=bins + 1) + return bins + + +def bin_count(x, bins, meanBin=True): + """ + Return counts of x within bins + """ + if not meanBin: + # Use the middle of the bin + N, bns = np.histogram(x, bins=bins) + S = bns[:-1] + np.diff(bns) / 2. + else: + bins = create_bins(x, bins, binStartAt0=False) + import pandas as pd + df = pd.DataFrame(data=x, columns=['x']) + xmid = (bins[:-1]+bins[1:])/2 + df['x_mid']= pd.cut(df['x'], bins= bins, labels = xmid ) # Adding a column that has bin attribute + df2 = df.groupby('x_mid').mean() # Average by bin + df['N'] = 1 + dfCount = df[['N','x_mid']].groupby('x_mid').sum() + df2['N'] = dfCount['N'] + # Just in case some bins are missing (will be nan) + df2 = df2.reindex(xmid) + df2 = df2.fillna(0) + S = df2['x'].values + N = df2['N'].values + return N, S @@ -286,11 +409,12 @@ def eq_load_and_cycles(signals, no_bins=46, m=[3, 4, 6, 8, 10, 12], neq=[10 ** 6 cycles, ampl_bin_mean = cycles.flatten(), ampl_bin_mean.flatten() with warnings.catch_warnings(): warnings.simplefilter("ignore") + #DEL = [[( (cycles * ampl_bin_mean ** _m) / _neq) for _m in np.atleast_1d(m)] for _neq in np.atleast_1d(neq)] eq_loads = [[((np.nansum(cycles * ampl_bin_mean ** _m) / _neq) ** (1. / _m)) for _m in np.atleast_1d(m)] for _neq in np.atleast_1d(neq)] return eq_loads, cycles, ampl_bin_mean, ampl_bin_edges -def cycle_matrix(signals, ampl_bins=10, mean_bins=10, rainflow_func=rainflow_windap): +def cycle_matrix(signals, ampl_bins=10, mean_bins=10, rainflow_func=rainflow_windap, binStartAt0=True): """Markow load cycle matrix Calculate the Markow load cycle matrix @@ -308,6 +432,8 @@ def cycle_matrix(signals, ampl_bins=10, mean_bins=10, rainflow_func=rainflow_win if array-like, the bin edges for mea rainflow_func : {rainflow_windap, rainflow_astm}, optional The rainflow counting function to use (default is rainflow_windap) + binStartAt0 : boolean + Start the bins at 0. Otherwise, start at the min of ranges Returns ------- @@ -336,7 +462,7 @@ def cycle_matrix(signals, ampl_bins=10, mean_bins=10, rainflow_func=rainflow_win ampls, means = rainflow_func(signals[:]) weights = np.ones_like(ampls) if isinstance(ampl_bins, int): - ampl_bins = np.linspace(0, 1, num=ampl_bins + 1) * ampls[weights>0].max() + ampl_bins = create_bins(ampls[weights>0], ampl_bins, binStartAt0=binStartAt0) cycles, ampl_edges, mean_edges = np.histogram2d(ampls, means, [ampl_bins, mean_bins], weights=weights) with warnings.catch_warnings(): warnings.simplefilter("ignore") @@ -790,11 +916,257 @@ def pair_range_amplitude_mean(x): # cpdef pair_range(np.ndarray[long,ndim=1] x return ampl_mean +rainflow_func_dict = {'rainflow_windap':rainflow_windap, 'rainflow_astm':rainflow_astm} +# --------------------------------------------------------------------------------} +# --- Unittests +# --------------------------------------------------------------------------------{ +import unittest + +class TestFatigue(unittest.TestCase): + + def test_leq_1hz(self): + """Simple test of wetb.fatigue.eq_load using a sine + signal. + """ + amplitude = 1 + m = 1 + point_per_deg = 100 + + for amplitude in [1,2,3]: + peak2peak = amplitude * 2 + # sine signal with 10 periods (20 peaks) + nr_periods = 10 + time = np.linspace(0, nr_periods*2*np.pi, point_per_deg*180) + neq = time[-1] + # mean value of the signal shouldn't matter + signal = amplitude * np.sin(time) + 5 + r_eq_1hz = eq_load(signal, no_bins=1, m=m, neq=neq)[0] + r_eq_1hz_expected = 2*((nr_periods*amplitude**m)/neq)**(1/m) + np.testing.assert_allclose(r_eq_1hz, r_eq_1hz_expected) + + # sine signal with 20 periods (40 peaks) + nr_periods = 20 + time = np.linspace(0, nr_periods*2*np.pi, point_per_deg*180) + neq = time[-1] + # mean value of the signal shouldn't matter + signal = amplitude * np.sin(time) + 9 + r_eq_1hz2 = eq_load(signal, no_bins=1, m=m, neq=neq)[0] + r_eq_1hz_expected2 = 2*((nr_periods*amplitude**m)/neq)**(1/m) + np.testing.assert_allclose(r_eq_1hz2, r_eq_1hz_expected2) + + # 1hz equivalent should be independent of the length of the signal + np.testing.assert_allclose(r_eq_1hz, r_eq_1hz2) + + def test_rainflow_combi(self): + # Signal with two frequencies and amplitudes + amplitude = 1 + # peak2peak = amplitude * 2 + m = 1 + point_per_deg = 100 + + nr_periods = 10 + time = np.linspace(0, nr_periods*2*np.pi, point_per_deg*180) + + signal = (amplitude*np.sin(time)) + 5 + (amplitude*0.2*np.cos(5*time)) + cycles, ampl_bin_mean, ampl_edges, mean_bin_mean, mean_edges = \ + cycle_matrix(signal, ampl_bins=10, mean_bins=5) + + cycles.sum() + + + + def test_astm1(self): + + signal = np.array([-2.0, 0.0, 1.0, 0.0, -3.0, 0.0, 5.0, 0.0, -1.0, 0.0, 3.0, 0.0, -4.0, 0.0, 4.0, 0.0, -2.0]) + + ampl, mean = rainflow_astm(signal) + np.testing.assert_array_equal(np.histogram2d(ampl, mean, [6, 4])[0], np.array([[ 0., 1., 0., 0.], + [ 1., 0., 0., 2.], + [ 0., 0., 0., 0.], + [ 0., 0., 0., 1.], + [ 0., 0., 0., 0.], + [ 0., 0., 1., 2.]])) + + def test_windap1(self): + signal = np.array([-2.0, 0.0, 1.0, 0.0, -3.0, 0.0, 5.0, 0.0, -1.0, 0.0, 3.0, 0.0, -4.0, 0.0, 4.0, 0.0, -2.0]) + ampl, mean = rainflow_windap(signal, 18, 2) + np.testing.assert_array_equal(np.histogram2d(ampl, mean, [6, 4])[0], np.array([[ 0., 0., 1., 0.], + [ 1., 0., 0., 2.], + [ 0., 0., 0., 0.], + [ 0., 0., 0., 1.], + [ 0., 0., 0., 0.], + [ 0., 0., 2., 1.]])) + + def test_eq_load_basic(self): + import numpy.testing + signal1 = np.array([-2.0, 0.0, 1.0, 0.0, -3.0, 0.0, 5.0, 0.0, -1.0, 0.0, 3.0, 0.0, -4.0, 0.0, 4.0, 0.0, -2.0]) + try: + M1=eq_load(signal1, no_bins=50, neq=[1, 17], m=[3, 4, 6], rainflow_func=rainflow_windap) + doTest=True + except FloatingPointError as e: + doTest=False + print('>>> Floating point error') + M1_ref=np.array([[10.348414123746581, 9.635653414943068, 9.122399471334054], [4.024613313976801, 4.745357541147315, 5.68897815218057]]) + #M1_ref=np.array([[10.311095426959747, 9.5942535021382174, 9.0789213365013932],[4.010099657859783, 4.7249689509841746, 5.6618639965313005]]) + numpy.testing.assert_almost_equal(M1,M1_ref,decimal=5) + #signal2 = signal1 * 1.1 + # print (eq_load(signal1, no_bins=50, neq=17, rainflow_func=rainflow_windap)) + # print (eq_load(signal1, no_bins=50, neq=17, rainflow_func=rainflow_astm)) + # # equivalent load for default wohler slopes + # # Cycle matrix with 4 amplitude bins and 4 mean value bins + # print (cycle_matrix(signal1, 4, 4, rainflow_func=rainflow_windap)) + # print (cycle_matrix(signal1, 4, 4, rainflow_func=rainflow_astm)) + # # Cycle matrix where signal1 and signal2 contributes with 50% each + # print (cycle_matrix([(.5, signal1), (.5, signal2)], 4, 8, rainflow_func=rainflow_astm)) + + + def test_equivalent_load(self): + """ Higher level interface """ + try: + import fatpack + hasFatpack=True + except: + hasFatpack=False + dt = 0.1 + f0 = 1 ; + A = 5 ; + t=np.arange(0,10,dt); + y=A*np.sin(2*np.pi*f0*t) + Leq = equivalent_load(t, y, m=10, bins=100, method='rainflow_windap') + np.testing.assert_almost_equal(Leq, 9.4714702, 3) -if __name__ == '__main__': - pass + Leq = equivalent_load(t, y, m=1, bins=100, method='rainflow_windap') + np.testing.assert_almost_equal(Leq, 9.4625320, 3) + + Leq = equivalent_load(t, y, m=4, bins=10, method='rainflow_windap') + np.testing.assert_almost_equal(Leq, 9.420937, 3) + if hasFatpack: + Leq = equivalent_load(t, y, m=4, bins=10, method='fatpack', binStartAt0=False, meanBin=False) + np.testing.assert_almost_equal(Leq, 9.584617089, 3) + + Leq = equivalent_load(t, y, m=4, bins=1, method='fatpack', binStartAt0=False, meanBin=False) + np.testing.assert_almost_equal(Leq, 9.534491302, 3) + + + + def test_equivalent_load_sines(self): + # Check analytical formulae for sine of various frequencies + # See welib.tools.examples.Example_Fatigue.py + try: + import fatpack + hasFatpack=True + except: + hasFatpack=False + + # --- Dependency on frequency + m = 2 # Wohler slope + A = 3 # Amplitude + nT = 100 # Number of periods + nPerT = 100 # Number of points per period + Teq = 1 # Equivalent period [s] + nBins = 10 # Number of bins + + vf =np.linspace(0.1,10,21) + vT = 1/vf + T_max=np.max(vT*nT) + vomega =vf*2*np.pi + Leq1 = np.zeros_like(vomega) + Leq2 = np.zeros_like(vomega) + Leq_ref = np.zeros_like(vomega) + for it, (T,omega) in enumerate(zip(vT,vomega)): + # --- Option 1 - Same number of periods + time = np.linspace(0, nT*T, nPerT*nT+1) + signal = A * np.sin(omega*time) # Mean does not matter + T_all=time[-1] + Leq1[it] = equivalent_load(time, signal, m=m, Teq=Teq, bins=nBins, method='rainflow_windap') + if hasFatpack: + Leq2[it] = equivalent_load(time, signal, m=m, Teq=Teq, bins=nBins, method='fatpack', binStartAt0=False) + Leq_ref = 2*A*(vf*Teq)**(1/m) + np.testing.assert_array_almost_equal( Leq1/A, Leq_ref/A, 2) + if hasFatpack: + np.testing.assert_array_almost_equal(Leq2/A, Leq_ref/A, 2) + #import matplotlib.pyplot as plt + #fig,ax = plt.subplots(1, 1, sharey=False, figsize=(6.4,4.8)) # (6.4,4.8) + #fig.subplots_adjust(left=0.12, right=0.95, top=0.95, bottom=0.11, hspace=0.20, wspace=0.20) + #ax.plot(vf, Leq_ref/A, 'kd' , label ='Theory') + #ax.plot(vf, Leq1 /A, 'o' , label ='Windap m={}'.format(m)) + #if hasFatpack: + # ax.plot(vf, Leq2/A, 'k.' , label ='Fatpack') + #ax.legend() + #plt.show() + + def test_equivalent_load_sines_sum(self): + # --- Sum of two sinusoids + try: + import fatpack + hasFatpack=True + except: + hasFatpack=False + bs0 = True # Bin Start at 0 + m = 2 # Wohler slope + nPerT = 100 # Number of points per period + Teq = 1 # Equivalent period [s] + nBins = 10 # Number of bins + nT1 = 10 # Number of periods + nT2 = 20 # Number of periods + T1 = 10 + T2 = 5 + A1 = 3 # Amplitude + A2 = 5 # Amplitude + # --- Signals + time1 = np.linspace(0, nT1*T1, nPerT*nT1+1) + time2 = np.linspace(0, nT2*T2, nPerT*nT2+1) + signal1 = A1 * np.sin(2*np.pi/T1*time1) + signal2 = A2 * np.sin(2*np.pi/T2*time2) + # --- Individual Leq + #print('----------------- SIGNAL 1') + DEL1 = (2*A1)**m * nT1/time1[-1] + Leq_th = (DEL1)**(1/m) + Leq1 = equivalent_load(time1, signal1, m=m, Teq=Teq, bins=nBins, method='rainflow_windap', binStartAt0=bs0) + if hasFatpack: + Leq2 = equivalent_load(time1, signal1, m=m, Teq=Teq, bins=nBins, method='fatpack', binStartAt0=bs0) + np.testing.assert_array_almost_equal(Leq2, Leq_th, 3) + np.testing.assert_array_almost_equal(Leq1, Leq_th, 1) + #print('>>> Leq1 ',Leq1) + #print('>>> Leq2 ',Leq2) + #print('>>> Leq TH ',Leq_th) + #print('----------------- SIGNAL 2') + DEL2 = (2*A2)**m * nT2/time2[-1] + Leq_th = (DEL2)**(1/m) + Leq1 = equivalent_load(time2, signal2, m=m, Teq=Teq, bins=nBins, method='rainflow_windap', binStartAt0=bs0) + if hasFatpack: + Leq2 = equivalent_load(time2, signal2, m=m, Teq=Teq, bins=nBins, method='fatpack', binStartAt0=bs0) + np.testing.assert_array_almost_equal(Leq2, Leq_th, 3) + np.testing.assert_array_almost_equal(Leq1, Leq_th, 1) + #print('>>> Leq1 ',Leq1) + #print('>>> Leq2 ',Leq2) + #print('>>> Leq TH ',Leq_th) + # --- Concatenation + #print('----------------- CONCATENATION') + signal = np.concatenate((signal1, signal2)) + time = np.concatenate((time1, time2+time1[-1])) + T_all=time[-1] + DEL1 = (2*A1)**m * nT1/T_all + DEL2 = (2*A2)**m * nT2/T_all + Leq_th = (DEL1+DEL2)**(1/m) + Leq1 = equivalent_load(time, signal, m=m, Teq=Teq, bins=nBins, method='rainflow_windap', binStartAt0=bs0) + if hasFatpack: + Leq2 = equivalent_load(time, signal, m=m, Teq=Teq, bins=nBins, method='fatpack', binStartAt0=bs0) + np.testing.assert_array_almost_equal(Leq2, Leq_th, 1) + np.testing.assert_array_almost_equal(Leq1, Leq_th, 1) + #print('>>> Leq1 ',Leq1) + #print('>>> Leq2 ',Leq2) + #print('>>> Leq TH ',Leq_th) + + + + + + +if __name__ == '__main__': + unittest.main() diff --git a/pyFAST/tools/tests/test_fatigue.py b/pyFAST/tools/tests/test_fatigue.py index 385e36b..eaa7f67 100644 --- a/pyFAST/tools/tests/test_fatigue.py +++ b/pyFAST/tools/tests/test_fatigue.py @@ -1,105 +1,6 @@ import unittest import numpy as np -from pyFAST.tools.fatigue import * - -class TestFatigue(unittest.TestCase): - - def test_leq_1hz(self): - """Simple test of wetb.fatigue.eq_load using a sine - signal. - """ - amplitude = 1 - m = 1 - point_per_deg = 100 - - for amplitude in [1,2,3]: - peak2peak = amplitude * 2 - # sine signal with 10 periods (20 peaks) - nr_periods = 10 - time = np.linspace(0, nr_periods*2*np.pi, point_per_deg*180) - neq = time[-1] - # mean value of the signal shouldn't matter - signal = amplitude * np.sin(time) + 5 - r_eq_1hz = eq_load(signal, no_bins=1, m=m, neq=neq)[0] - r_eq_1hz_expected = ((2*nr_periods*amplitude**m)/neq)**(1/m) - np.testing.assert_allclose(r_eq_1hz, r_eq_1hz_expected) - - # sine signal with 20 periods (40 peaks) - nr_periods = 20 - time = np.linspace(0, nr_periods*2*np.pi, point_per_deg*180) - neq = time[-1] - # mean value of the signal shouldn't matter - signal = amplitude * np.sin(time) + 9 - r_eq_1hz2 = eq_load(signal, no_bins=1, m=m, neq=neq)[0] - r_eq_1hz_expected2 = ((2*nr_periods*amplitude**m)/neq)**(1/m) - np.testing.assert_allclose(r_eq_1hz2, r_eq_1hz_expected2) - - # 1hz equivalent should be independent of the length of the signal - np.testing.assert_allclose(r_eq_1hz, r_eq_1hz2) - - def test_rainflow_combi(self): - # Signal with two frequencies and amplitudes - amplitude = 1 - # peak2peak = amplitude * 2 - m = 1 - point_per_deg = 100 - - nr_periods = 10 - time = np.linspace(0, nr_periods*2*np.pi, point_per_deg*180) - - signal = (amplitude*np.sin(time)) + 5 + (amplitude*0.2*np.cos(5*time)) - cycles, ampl_bin_mean, ampl_edges, mean_bin_mean, mean_edges = \ - cycle_matrix(signal, ampl_bins=10, mean_bins=5) - - cycles.sum() - - - - def test_astm1(self): - - signal = np.array([-2.0, 0.0, 1.0, 0.0, -3.0, 0.0, 5.0, 0.0, -1.0, 0.0, 3.0, 0.0, -4.0, 0.0, 4.0, 0.0, -2.0]) - - ampl, mean = rainflow_astm(signal) - np.testing.assert_array_equal(np.histogram2d(ampl, mean, [6, 4])[0], np.array([[ 0., 1., 0., 0.], - [ 1., 0., 0., 2.], - [ 0., 0., 0., 0.], - [ 0., 0., 0., 1.], - [ 0., 0., 0., 0.], - [ 0., 0., 1., 2.]])) - - def test_windap1(self): - signal = np.array([-2.0, 0.0, 1.0, 0.0, -3.0, 0.0, 5.0, 0.0, -1.0, 0.0, 3.0, 0.0, -4.0, 0.0, 4.0, 0.0, -2.0]) - ampl, mean = rainflow_windap(signal, 18, 2) - np.testing.assert_array_equal(np.histogram2d(ampl, mean, [6, 4])[0], np.array([[ 0., 0., 1., 0.], - [ 1., 0., 0., 2.], - [ 0., 0., 0., 0.], - [ 0., 0., 0., 1.], - [ 0., 0., 0., 0.], - [ 0., 0., 2., 1.]])) - - def test_eq_load_basic(self): - import numpy.testing - signal1 = np.array([-2.0, 0.0, 1.0, 0.0, -3.0, 0.0, 5.0, 0.0, -1.0, 0.0, 3.0, 0.0, -4.0, 0.0, 4.0, 0.0, -2.0]) - try: - M1=eq_load(signal1, no_bins=50, neq=[1, 17], m=[3, 4, 6], rainflow_func=rainflow_windap) - doTest=True - except FloatingPointError as e: - doTest=False - print('>>> Floating point error') - M1_ref=np.array([[10.348414123746581, 9.635653414943068, 9.122399471334054], [4.024613313976801, 4.745357541147315, 5.68897815218057]]) - #M1_ref=np.array([[10.311095426959747, 9.5942535021382174, 9.0789213365013932],[4.010099657859783, 4.7249689509841746, 5.6618639965313005]]) - numpy.testing.assert_almost_equal(M1,M1_ref,decimal=5) - #signal2 = signal1 * 1.1 - # print (eq_load(signal1, no_bins=50, neq=17, rainflow_func=rainflow_windap)) - # print (eq_load(signal1, no_bins=50, neq=17, rainflow_func=rainflow_astm)) - # # equivalent load for default wohler slopes - # # Cycle matrix with 4 amplitude bins and 4 mean value bins - # print (cycle_matrix(signal1, 4, 4, rainflow_func=rainflow_windap)) - # print (cycle_matrix(signal1, 4, 4, rainflow_func=rainflow_astm)) - # # Cycle matrix where signal1 and signal2 contributes with 50% each - # print (cycle_matrix([(.5, signal1), (.5, signal2)], 4, 8, rainflow_func=rainflow_astm)) - - - +from pyFAST.tools.fatigue import TestFatigue + if __name__ == '__main__': unittest.main() From cd94e85a0bc8b89125e608fb9b8086d8cc9e93c0 Mon Sep 17 00:00:00 2001 From: Emmanuel Branlard Date: Mon, 31 Jul 2023 14:19:09 -0600 Subject: [PATCH 097/124] IO: TurbSimFile: adding fromAMRWind_PD and AMRWind class --- pyFAST/input_output/amrwind_file.py | 125 ++++++++++++++++++ pyFAST/input_output/turbsim_file.py | 190 +++++++++++++++++++++------- 2 files changed, 272 insertions(+), 43 deletions(-) create mode 100644 pyFAST/input_output/amrwind_file.py diff --git a/pyFAST/input_output/amrwind_file.py b/pyFAST/input_output/amrwind_file.py new file mode 100644 index 0000000..c492779 --- /dev/null +++ b/pyFAST/input_output/amrwind_file.py @@ -0,0 +1,125 @@ +"""Read AMR-Wind NETCDF file + +""" +import xarray as xr +import numpy as np + +class AMRWindFile(dict): + """ + Read a AMR-Wind output file (.nc) + """ + + @staticmethod + def defaultExtensions(): + """ List of file extensions expected for this fileformat""" + return ['.nc'] + + @staticmethod + def formatName(): + """ Short string (~100 char) identifying the file format""" + return 'NetCDF plane sampling file from AMRWind' + + @staticmethod + def priority(): return 60 # Priority in weio.read fileformat list between 0=high and 100:low + + def __init__(self, filename=None, timestep=None, output_frequency=None, **kwargs): + self.filename = filename + self.amrwind_dt = timestep + self.output_dt = timestep * output_frequency + + if filename: + self.read(**kwargs) + + def read(self, group_name): + """ + Parameters + ---------- + + group_name : str, + group name inside netcdf file that you want to read, e.g. p_slice + + TODO: see if group_name can be avoided, and add a read_group function + """ + # --- Standard tests and exceptions (generic code) + if filename: + self.filename = filename + if not self.filename: + raise Exception('No filename provided') + if not os.path.isfile(self.filename): + raise OSError(2,'File not found:',self.filename) + if os.stat(self.filename).st_size == 0: + raise Exception('File is empty:',self.filename) + + + ds = xr.open_dataset(self.filename,group=group_name) + + coordinates = {"x":(0,"axial"), "y":(1,"lateral"),"z":(2,"vertical")} + c = {} + for coordinate,(i,desc) in coordinates.items(): + c[coordinate] = xr.IndexVariable( + dims=[coordinate], + data=np.sort(np.unique(ds['coordinates'].isel(ndim=i))), + attrs={"description":"{0} coordinate".format(desc),"units":"m"} + ) + c["t"] = xr.IndexVariable( + dims=["t"], + data=ds.num_time_steps*self.output_dt, + attrs={"description":"time from start of simulation","units":"s"} + ) + + self.nt = len(c["t"]) + self.nx = len(c["x"]) + self.ny = len(c["y"]) + self.nz = len(c["z"]) + + coordinates = {"x":(0,"axial","u"), "y":(1,"lateral","v"),"z":(2,"vertical","w")} + v = {} + for coordinate,(i,desc,u) in coordinates.items(): + v[u] = xr.DataArray(np.reshape(getattr(ds,"velocity{0}".format(coordinate)).values,(self.nt,self.nx,self.ny,self.nz)), + coords=c, + dims=["t","x","y","z"], + name="{0} velocity".format(desc), + attrs={"description":"velocity along {0}".format(coordinate),"units":"m/s"}) + + ds = xr.Dataset(data_vars=v, coords=v[u].coords) + ds.attrs = {"original file":self.filename} + + self.data = ds + + + def write(self, filename=None): + """ Rewrite object to file, or write object to `filename` if provided """ + if filename: + self.filename = filename + if not self.filename: + raise Exception('No filename provided') + raise NotImplementedError() + + def toDataFrame(self): + """ Returns object into one DataFrame, or a dictionary of DataFrames""" + # --- Example (returning one DataFrame): + # return pd.DataFrame(data=np.zeros((10,2)),columns=['Col1','Col2']) + # --- Example (returning dict of DataFrames): + #dfs={} + #cols=['Alpha_[deg]','Cl_[-]','Cd_[-]','Cm_[-]'] + #dfs['Polar1'] = pd.DataFrame(data=..., columns=cols) + #dfs['Polar1'] = pd.DataFrame(data=..., columns=cols) + # return dfs + raise NotImplementedError() + + # --- Optional functions + def __repr__(self): + """ String that is written to screen when the user calls `print()` on the object. + Provide short and relevant information to save time for the user. + """ + s='<{} object>:\n'.format(type(self).__name__) + s+='|Main attributes:\n' + s+='| - filename: {}\n'.format(self.filename) + # --- Example printing some relevant information for user + #s+='|Main keys:\n' + #s+='| - ID: {}\n'.format(self['ID']) + #s+='| - data : shape {}\n'.format(self['data'].shape) + s+='|Main methods:\n' + s+='| - read, write, toDataFrame, keys' + return s + diff --git a/pyFAST/input_output/turbsim_file.py b/pyFAST/input_output/turbsim_file.py index 2d5d8c8..098c05e 100644 --- a/pyFAST/input_output/turbsim_file.py +++ b/pyFAST/input_output/turbsim_file.py @@ -58,7 +58,7 @@ def __init__(self, filename=None, **kwargs): if filename: self.read(filename, **kwargs) - def read(self, filename=None, header_only=False): + def read(self, filename=None, header_only=False, tdecimals=8): """ read BTS file, with field: u (3 x nt x ny x nz) uTwr (3 x nt x nTwr) @@ -98,11 +98,11 @@ def read(self, filename=None, header_only=False): self['uTwr'] = uTwr self['info'] = info self['ID'] = ID - self['dt'] = np.round(dt,3) # dt is stored in single precision in the TurbSim output + self['dt'] = np.round(dt, tdecimals) # dt is stored in single precision in the TurbSim output self['y'] = np.arange(ny)*dy self['y'] -= np.mean(self['y']) # y always centered on 0 self['z'] = np.arange(nz)*dz +zBottom - self['t'] = np.round(np.arange(nt)*dt, 3) + self['t'] = np.round(np.arange(nt)*dt, tdecimals) self['zTwr'] =-np.arange(nTwr)*dz + zBottom self['zRef'] = zHub self['uRef'] = uHub @@ -592,45 +592,13 @@ def __repr__(self): s+=' ux: min: {}, max: {}, mean: {} \n'.format(np.min(ux), np.max(ux), np.mean(ux)) s+=' uy: min: {}, max: {}, mean: {} \n'.format(np.min(uy), np.max(uy), np.mean(uy)) s+=' uz: min: {}, max: {}, mean: {} \n'.format(np.min(uz), np.max(uz), np.mean(uz)) - + s += ' Useful methods:\n' + s += ' - read, write, toDataFrame, keys\n' + s += ' - valuesAt, vertProfile, horizontalPlane, verticalPlane, closestPoint\n' + s += ' - fitPowerLaw\n' + s += ' - makePeriodic, checkPeriodic\n' return s - def toDataSet(self, datetime=False): - import xarray as xr - - if datetime: - timearray = pd.to_datetime(self['t'], unit='s', origin=pd.to_datetime('2000-01-01 00:00:00')) - timestr = 'datetime' - else: - timearray = self['t'] - timestr = 'time' - - ds = xr.Dataset( - data_vars=dict( - u=([timestr,'y','z'], self['u'][0,:,:,:]), - v=([timestr,'y','z'], self['u'][1,:,:,:]), - w=([timestr,'y','z'], self['u'][2,:,:,:]), - ), - coords={ - timestr : timearray, - 'y' : self['y'], - 'z' : self['z'], - }, - ) - - # Add mean computations - ds['up'] = ds['u'] - ds['u'].mean(dim=timestr) - ds['vp'] = ds['v'] - ds['v'].mean(dim=timestr) - ds['wp'] = ds['w'] - ds['w'].mean(dim=timestr) - - if datetime: - # Add time (in s) to the variable list - ds['time'] = (('datetime'), self['t']) - - return ds - - - def toDataFrame(self): dfs={} @@ -713,31 +681,167 @@ def toDataFrame(self): # pass return dfs + def toDataset(self): + """ + Convert the data that was read in into a xarray Dataset + + # TODO SORT OUT THE DIFFERENCE WITH toDataSet + """ + from xarray import IndexVariable, DataArray, Dataset + + print('[TODO] pyFAST.input_output.turbsim_file.toDataset: merge with function toDataSet') + + y = IndexVariable("y", self.y, attrs={"description":"lateral coordinate","units":"m"}) + zround = np.asarray([np.round(zz,6) for zz in self.z]) #the open function here returns something like *.0000000001 which is annoying + z = IndexVariable("z", zround, attrs={"description":"vertical coordinate","units":"m"}) + time = IndexVariable("time", self.t, attrs={"description":"time since start of simulation","units":"s"}) + + da = {} + for component,direction,velname in zip([0,1,2],["x","y","z"],["u","v","w"]): + # the dataset produced here has y/z axes swapped relative to data stored in original object + velocity = np.swapaxes(self["u"][component,...],1,2) + da[velname] = DataArray(velocity, + coords={"time":time,"y":y,"z":z}, + dims=["time","y","z"], + name="velocity", + attrs={"description":"velocity along {0}".format(direction),"units":"m/s"}) + + return Dataset(data_vars=da, coords={"time":time,"y":y,"z":z}) + + def toDataSet(self, datetime=False): + """ + Convert the data that was read in into a xarray Dataset + + # TODO SORT OUT THE DIFFERENCE WITH toDataset + """ + import xarray as xr + + print('[TODO] pyFAST.input_output.turbsim_file.toDataSet: should be discontinued') + print('[TODO] pyFAST.input_output.turbsim_file.toDataSet: merge with function toDataset') + + if datetime: + timearray = pd.to_datetime(self['t'], unit='s', origin=pd.to_datetime('2000-01-01 00:00:00')) + timestr = 'datetime' + else: + timearray = self['t'] + timestr = 'time' + + ds = xr.Dataset( + data_vars=dict( + u=([timestr,'y','z'], self['u'][0,:,:,:]), + v=([timestr,'y','z'], self['u'][1,:,:,:]), + w=([timestr,'y','z'], self['u'][2,:,:,:]), + ), + coords={ + timestr : timearray, + 'y' : self['y'], + 'z' : self['z'], + }, + ) + + # Add mean computations + ds['up'] = ds['u'] - ds['u'].mean(dim=timestr) + ds['vp'] = ds['v'] - ds['v'].mean(dim=timestr) + ds['wp'] = ds['w'] - ds['w'].mean(dim=timestr) + + if datetime: + # Add time (in s) to the variable list + ds['time'] = (('datetime'), self['t']) + + return ds # Useful converters - def fromAMRWind(self, filename, dt, nt): + def fromAMRWind(self, filename, timestep, output_frequency, sampling_identifier, verbose=1, fileout=None, zref=None, xloc=None): + """ + Reads a AMRWind netcdf file, grabs a group of sampling planes (e.g. p_slice), + return an instance of TurbSimFile, optionally write turbsim file to disk + + + Parameters + ---------- + filename : str, + full path to netcdf file generated by amrwind + timestep : float, + amr-wind code timestep (time.fixed_dt) + output_frequency : int, + frequency chosen for sampling output in amrwind input file (sampling.output_frequency) + sampling_identifier : str, + identifier of the sampling being requested (an entry of sampling.labels in amrwind input file) + zref : float, + height to be written to turbsim as the reference height. if none is given, it is taken as the vertical centerpoint of the slice + """ + try: + from pyFAST.input_output.amrwind_file import AMRWindFile + except: + try: + from .amrwind_file import AMRWindFile + except: + from amrwind_file import AMRWindFile + + obj = AMRWindFile(filename,timestep,output_frequency, group_name=sampling_identifier) + + self["u"] = np.ndarray((3,obj.nt,obj.ny,obj.nz)) + + xloc = float(obj.data.x[0]) if xloc is None else xloc + if verbose: + print("Grabbing the slice at x={0} m".format(xloc)) + self['u'][0,:,:,:] = np.swapaxes(obj.data.u.sel(x=xloc).values,1,2) + self['u'][1,:,:,:] = np.swapaxes(obj.data.v.sel(x=xloc).values,1,2) + self['u'][2,:,:,:] = np.swapaxes(obj.data.w.sel(x=xloc).values,1,2) + self['t'] = obj.data.t.values + + self['y'] = obj.data.y.values + self['z'] = obj.data.z.values + self['dt'] = obj.output_dt + + self['ID'] = 7 + ltime = time.strftime('%d-%b-%Y at %H:%M:%S', time.localtime()) + self['info'] = 'Converted from AMRWind output file {0} {1:s}.'.format(filename,ltime) + + iz = int(obj.nz/2) + self['zRef'] = float(obj.data.z[iz]) if zref is None else zref + if verbose: + print("Setting the TurbSim file reference height to z={0} m".format(self["zRef"])) + + self['uRef'] = float(obj.data.u.sel(x=xloc).sel(y=0).sel(z=self["zRef"]).mean().values) + self['zRef'], self['uRef'], bHub = self.hubValues() + + if fileout is not None: + filebase = os.path.splitext(filename)[1] + fileout = filebase+".bts" + if verbose: + print("===> {0}".format(fileout)) + self.write(fileout) + + + def fromAMRWind_legacy(self, filename, dt, nt, y, z, sampling_identifier='p_sw2'): """ Convert current TurbSim file into one generated from AMR-Wind LES sampling data in .nc format Assumes: -- u, v, w (nt, nx * ny * nz) -- u is aligned with x-axis (flow is not rotated) - this consideration needs to be added + INPUTS: - filename: (string) full path to .nc sampling data file - - plane_label: (string) name of sampling plane group from .inp file (e.g. "p_sw2") + - sampling_identifier: (string) name of sampling plane group from .inp file (e.g. "p_sw2") - dt: timestep size [s] - nt: number of timesteps (sequential) you want to read in, starting at the first timestep available + INPUTS: TODO - y: user-defined vector of coordinate positions in y - z: user-defined vector of coordinate positions in z - uref: (float) reference mean velocity (e.g. 8.0 hub height mean velocity from input file) - zref: (float) hub height (e.t. 150.0) """ import xarray as xr + + print('[TODO] fromAMRWind_legacy: function might be unfinished. Merge with fromAMRWind') + print('[TODO] fromAMRWind_legacy: figure out y, and z from data (see fromAMRWind)') # read in sampling data plane ds = xr.open_dataset(filename, engine='netcdf4', - group=plane_label) + group=sampling_identifier) ny, nz, _ = ds.attrs['ijk_dims'] noffsets = len(ds.attrs['offsets']) t = np.arange(0, dt*(nt-0.5), dt) From 5b836be5e02df576dfc4cb2987ecada3188c683c Mon Sep 17 00:00:00 2001 From: Emmanuel Branlard Date: Mon, 31 Jul 2023 15:20:57 -0600 Subject: [PATCH 098/124] IO/Postpro/tools: updates from welib --- pyFAST/airfoils/DynamicStall.py | 506 ++++++++ pyFAST/airfoils/Polar.py | 16 +- pyFAST/airfoils/__init__.py | 2 + pyFAST/airfoils/examples/correction3D.py | 34 +- pyFAST/case_generation/case_gen.py | 40 +- pyFAST/case_generation/runner.py | 76 +- pyFAST/input_output/__init__.py | 273 +++++ pyFAST/input_output/bladed_out_file.py | 398 +++++++ pyFAST/input_output/cactus_element_file.py | 107 ++ pyFAST/input_output/cactus_file.py | 386 +++++++ pyFAST/input_output/converters.py | 64 ++ pyFAST/input_output/csv_file.py | 2 - pyFAST/input_output/fast_input_deck.py | 95 +- pyFAST/input_output/fast_input_file.py | 93 +- pyFAST/input_output/fast_output_file.py | 307 +++-- pyFAST/input_output/file.py | 53 +- pyFAST/input_output/file_formats.py | 29 + pyFAST/input_output/flex_blade_file.py | 139 +++ pyFAST/input_output/flex_doc_file.py | 208 ++++ pyFAST/input_output/flex_out_file.py | 205 ++++ pyFAST/input_output/flex_profile_file.py | 144 +++ pyFAST/input_output/flex_wavekin_file.py | 101 ++ pyFAST/input_output/mannbox_input_file.py | 191 ++++ pyFAST/input_output/matlabmat_file.py | 112 ++ pyFAST/input_output/netcdf_file.py | 40 + pyFAST/input_output/parquet_file.py | 48 + pyFAST/input_output/pickle_file.py | 168 +++ pyFAST/input_output/rosco_discon_file.py | 265 +++++ pyFAST/input_output/tdms_file.py | 225 ++++ pyFAST/input_output/tecplot_file.py | 222 ++++ .../tests/example_files/Bladed_out_ascii.$04 | 200 ++++ .../tests/example_files/Bladed_out_ascii.$41 | 800 +++++++++++++ .../tests/example_files/Bladed_out_ascii.%04 | 39 + .../tests/example_files/Bladed_out_ascii.%41 | 98 ++ .../tests/example_files/Bladed_out_binary.$04 | Bin 0 -> 21600 bytes .../tests/example_files/Bladed_out_binary.$41 | Bin 0 -> 76800 bytes .../tests/example_files/Bladed_out_binary.%04 | 39 + .../tests/example_files/Bladed_out_binary.%41 | 98 ++ .../example_files/Bladed_out_binary_case2.$04 | Bin 0 -> 360 bytes .../example_files/Bladed_out_binary_case2.$06 | Bin 0 -> 160 bytes .../example_files/Bladed_out_binary_case2.$09 | Bin 0 -> 680 bytes .../example_files/Bladed_out_binary_case2.$12 | Bin 0 -> 520 bytes .../example_files/Bladed_out_binary_case2.$23 | 1 + .../example_files/Bladed_out_binary_case2.$25 | Bin 0 -> 640 bytes .../example_files/Bladed_out_binary_case2.$31 | Bin 0 -> 120 bytes .../example_files/Bladed_out_binary_case2.$37 | 522 +++++++++ .../example_files/Bladed_out_binary_case2.$46 | Bin 0 -> 480 bytes .../example_files/Bladed_out_binary_case2.$55 | Bin 0 -> 200 bytes .../example_files/Bladed_out_binary_case2.$69 | Bin 0 -> 240 bytes .../example_files/Bladed_out_binary_case2.$PJ | 1013 ++++++++++++++++ .../example_files/Bladed_out_binary_case2.%04 | 39 + .../example_files/Bladed_out_binary_case2.%06 | 29 + .../example_files/Bladed_out_binary_case2.%09 | 59 + .../example_files/Bladed_out_binary_case2.%12 | 47 + .../example_files/Bladed_out_binary_case2.%23 | 37 + .../example_files/Bladed_out_binary_case2.%25 | 60 + .../example_files/Bladed_out_binary_case2.%31 | 31 + .../example_files/Bladed_out_binary_case2.%37 | 31 + .../example_files/Bladed_out_binary_case2.%46 | 56 + .../example_files/Bladed_out_binary_case2.%55 | 17 + .../example_files/Bladed_out_binary_case2.%69 | 33 + .../Bladed_out_binary_case2_fail.$55 | Bin 0 -> 200 bytes .../Bladed_out_binary_case2_fail.%55 | 17 + .../FASTIn_AD15_arf_multitabs.dat | 146 +++ .../example_files/FASTIn_ExtPtfm_SubSef.dat | 20 +- .../tests/example_files/FASTIn_HD2.dat | 269 +++++ .../example_files/FASTIn_HD_SeaState.dat | 96 ++ .../tests/example_files/FASTIn_HD_driver.dvr | 27 + .../tests/example_files/FLEXBlade000.bla | 32 + .../tests/example_files/FLEXBlade001.bld | 32 + .../tests/example_files/FLEXBlade002.bld | 56 + .../tests/example_files/FLEXBlade003.bld | 21 + .../tests/example_files/FLEXOutBinV0.int | Bin 0 -> 11352 bytes .../tests/example_files/FLEXOutBinV3.res | Bin 0 -> 186 bytes .../tests/example_files/FLEXProfile.pro | 1016 +++++++++++++++++ .../tests/example_files/FLEXWaveKin.wko | 24 + .../example_files/ParquetFile_test.parquet | Bin 0 -> 2951 bytes .../RoscoDISCON_PowerTracking.in | 151 +++ .../example_files/RoscoPerformance_CpCtCq.txt | 29 + .../TDMS_1Grp2Chan_TimeTrack.tdms | Bin 0 -> 560 bytes .../tests/example_files/TDMS_2Grp2Chan.tdms | Bin 0 -> 1019 bytes .../tests/example_files/TecplotASCII_1.dat | 11 + .../tests/example_files/TecplotASCII_2.dat | 28 + .../tests/example_files/TurbSimTS.txt | 17 + pyFAST/input_output/tests/test_csv.py | 5 +- pyFAST/input_output/tests/test_fast_input.py | 108 +- .../tests/test_fast_input_deck.py | 3 +- .../tests/test_fast_linearization.py | 3 +- pyFAST/input_output/tests/test_fast_output.py | 1 - .../input_output/tests/test_fast_summary.py | 4 +- pyFAST/input_output/tests/test_hawc.py | 13 +- pyFAST/input_output/tests/test_mannbox.py | 3 +- pyFAST/input_output/tests/test_parquet.py | 26 + pyFAST/input_output/tests/test_turbsim.py | 6 +- pyFAST/input_output/tests/test_vtk.py | 6 +- pyFAST/input_output/turbsim_ts_file.py | 102 ++ .../postpro/examples/Example_RadialInterp.py | 20 +- .../postpro/examples/Example_RadialPostPro.py | 5 +- pyFAST/postpro/examples/Example_Remap.py | 2 +- pyFAST/postpro/postpro.py | 610 +++++++--- pyFAST/tools/pandalib.py | 106 ++ pyFAST/tools/signal_analysis.py | 42 +- pyFAST/tools/tictoc.py | 75 ++ setup.py | 10 +- subdyn.py | 908 +++++++++++++++ 105 files changed, 11354 insertions(+), 494 deletions(-) create mode 100644 pyFAST/airfoils/DynamicStall.py create mode 100644 pyFAST/input_output/bladed_out_file.py create mode 100644 pyFAST/input_output/cactus_element_file.py create mode 100644 pyFAST/input_output/cactus_file.py create mode 100644 pyFAST/input_output/converters.py create mode 100644 pyFAST/input_output/file_formats.py create mode 100644 pyFAST/input_output/flex_blade_file.py create mode 100644 pyFAST/input_output/flex_doc_file.py create mode 100644 pyFAST/input_output/flex_out_file.py create mode 100644 pyFAST/input_output/flex_profile_file.py create mode 100644 pyFAST/input_output/flex_wavekin_file.py create mode 100644 pyFAST/input_output/mannbox_input_file.py create mode 100644 pyFAST/input_output/matlabmat_file.py create mode 100644 pyFAST/input_output/netcdf_file.py create mode 100644 pyFAST/input_output/parquet_file.py create mode 100644 pyFAST/input_output/pickle_file.py create mode 100644 pyFAST/input_output/rosco_discon_file.py create mode 100644 pyFAST/input_output/tdms_file.py create mode 100644 pyFAST/input_output/tecplot_file.py create mode 100644 pyFAST/input_output/tests/example_files/Bladed_out_ascii.$04 create mode 100644 pyFAST/input_output/tests/example_files/Bladed_out_ascii.$41 create mode 100644 pyFAST/input_output/tests/example_files/Bladed_out_ascii.%04 create mode 100644 pyFAST/input_output/tests/example_files/Bladed_out_ascii.%41 create mode 100644 pyFAST/input_output/tests/example_files/Bladed_out_binary.$04 create mode 100644 pyFAST/input_output/tests/example_files/Bladed_out_binary.$41 create mode 100644 pyFAST/input_output/tests/example_files/Bladed_out_binary.%04 create mode 100644 pyFAST/input_output/tests/example_files/Bladed_out_binary.%41 create mode 100644 pyFAST/input_output/tests/example_files/Bladed_out_binary_case2.$04 create mode 100644 pyFAST/input_output/tests/example_files/Bladed_out_binary_case2.$06 create mode 100644 pyFAST/input_output/tests/example_files/Bladed_out_binary_case2.$09 create mode 100644 pyFAST/input_output/tests/example_files/Bladed_out_binary_case2.$12 create mode 100644 pyFAST/input_output/tests/example_files/Bladed_out_binary_case2.$23 create mode 100644 pyFAST/input_output/tests/example_files/Bladed_out_binary_case2.$25 create mode 100644 pyFAST/input_output/tests/example_files/Bladed_out_binary_case2.$31 create mode 100644 pyFAST/input_output/tests/example_files/Bladed_out_binary_case2.$37 create mode 100644 pyFAST/input_output/tests/example_files/Bladed_out_binary_case2.$46 create mode 100644 pyFAST/input_output/tests/example_files/Bladed_out_binary_case2.$55 create mode 100644 pyFAST/input_output/tests/example_files/Bladed_out_binary_case2.$69 create mode 100644 pyFAST/input_output/tests/example_files/Bladed_out_binary_case2.$PJ create mode 100644 pyFAST/input_output/tests/example_files/Bladed_out_binary_case2.%04 create mode 100644 pyFAST/input_output/tests/example_files/Bladed_out_binary_case2.%06 create mode 100644 pyFAST/input_output/tests/example_files/Bladed_out_binary_case2.%09 create mode 100644 pyFAST/input_output/tests/example_files/Bladed_out_binary_case2.%12 create mode 100644 pyFAST/input_output/tests/example_files/Bladed_out_binary_case2.%23 create mode 100644 pyFAST/input_output/tests/example_files/Bladed_out_binary_case2.%25 create mode 100644 pyFAST/input_output/tests/example_files/Bladed_out_binary_case2.%31 create mode 100644 pyFAST/input_output/tests/example_files/Bladed_out_binary_case2.%37 create mode 100644 pyFAST/input_output/tests/example_files/Bladed_out_binary_case2.%46 create mode 100644 pyFAST/input_output/tests/example_files/Bladed_out_binary_case2.%55 create mode 100644 pyFAST/input_output/tests/example_files/Bladed_out_binary_case2.%69 create mode 100644 pyFAST/input_output/tests/example_files/Bladed_out_binary_case2_fail.$55 create mode 100644 pyFAST/input_output/tests/example_files/Bladed_out_binary_case2_fail.%55 create mode 100644 pyFAST/input_output/tests/example_files/FASTIn_AD15_arf_multitabs.dat create mode 100644 pyFAST/input_output/tests/example_files/FASTIn_HD2.dat create mode 100644 pyFAST/input_output/tests/example_files/FASTIn_HD_SeaState.dat create mode 100644 pyFAST/input_output/tests/example_files/FASTIn_HD_driver.dvr create mode 100644 pyFAST/input_output/tests/example_files/FLEXBlade000.bla create mode 100644 pyFAST/input_output/tests/example_files/FLEXBlade001.bld create mode 100644 pyFAST/input_output/tests/example_files/FLEXBlade002.bld create mode 100644 pyFAST/input_output/tests/example_files/FLEXBlade003.bld create mode 100644 pyFAST/input_output/tests/example_files/FLEXOutBinV0.int create mode 100644 pyFAST/input_output/tests/example_files/FLEXOutBinV3.res create mode 100644 pyFAST/input_output/tests/example_files/FLEXProfile.pro create mode 100644 pyFAST/input_output/tests/example_files/FLEXWaveKin.wko create mode 100644 pyFAST/input_output/tests/example_files/ParquetFile_test.parquet create mode 100644 pyFAST/input_output/tests/example_files/RoscoDISCON_PowerTracking.in create mode 100644 pyFAST/input_output/tests/example_files/RoscoPerformance_CpCtCq.txt create mode 100644 pyFAST/input_output/tests/example_files/TDMS_1Grp2Chan_TimeTrack.tdms create mode 100644 pyFAST/input_output/tests/example_files/TDMS_2Grp2Chan.tdms create mode 100644 pyFAST/input_output/tests/example_files/TecplotASCII_1.dat create mode 100644 pyFAST/input_output/tests/example_files/TecplotASCII_2.dat create mode 100644 pyFAST/input_output/tests/example_files/TurbSimTS.txt create mode 100644 pyFAST/input_output/tests/test_parquet.py create mode 100644 pyFAST/input_output/turbsim_ts_file.py create mode 100644 pyFAST/tools/tictoc.py create mode 100644 subdyn.py diff --git a/pyFAST/airfoils/DynamicStall.py b/pyFAST/airfoils/DynamicStall.py new file mode 100644 index 0000000..9b3d5a9 --- /dev/null +++ b/pyFAST/airfoils/DynamicStall.py @@ -0,0 +1,506 @@ +import numpy as np +import pandas as pd +from .Polar import Polar as Pol + + +# --------------------------------------------------------------------------------} +# --- Wagner function +# --------------------------------------------------------------------------------{ +A1_Jones, A2_Jones, b1_Jones, b2_Jones = 0.165 , 0.335 , 0.0455 , 0.3 +A1_FAST, A2_FAST, b1_FAST, b2_FAST = 0.3 , 0.7 , 0.14 , 0.53 + +def wagner(tau_t, constants=None, A1=None, A2=None, b1=None, b2=None): + """ + Lift coefficient, Cl, from Wagner function + INPUTS: + - tau_t: dimensionless time + - constants: string in ['Jones', 'OpenFAST'] or None + - A1, A2, b1, b2 : wagner constants, should be provided if constants is None + + Reference: Wagner - R.T Jones approximation (Jones 1938) + """ + if constants in ['Jones','HAWC2']: # R.T Jones approximation to Wagner's function (Jones 1938) + A1, A2, b1, b2 = A1_Jones, A2_Jones, b1_Jones, b2_Jones + elif constants=='OpenFAST': + A1, A2, b1, b2 = A1_FAST, A2_FAST, b1_FAST, b2_FAST + elif constants is None: + if all([A1, A2, b1, b2]): + pass # all good + else: + raise Exception('Provide A1, A2, b1, b2 if constants is None') + + + else: + raise NotImplementedError('Constants {}'.format(constants)) + + Cl = 1-A1_Jones*np.exp(-b1_Jones*tau_t)-A2_Jones*np.exp(-b2_Jones*tau_t) + + return Cl + + +# --------------------------------------------------------------------------------} +# --- MHH dynamic stall +# --------------------------------------------------------------------------------{ +def dynstall_mhh_sim(time, u, p, x0=None, prefix='', method='continuous'): + """ Perform simulation using the MHH/HGM dynamic stall model + Reference: + Hansen Gaunaa Madsen - Riso-R-1354 (2004) A Beddoes-Lieshman type dynamic stall model + + INPUTS: + - time: time vector + - u: dictionary of input functions of time + - p: dictionary of parameters + - x0: initial conditions for the 4 states of the model. If None, the steady steady values are used + - method: 'continuous' or 'discrete' to chose a formulation + - prefix: prefix used for channel names of the dataframe. Use 'AB1N001' to match OpenFAST. + OUTPUTS: + - df: dataframe with outputs similar to UA module of OpenFAST + """ + from scipy.integrate import solve_ivp + + # --- Initial conditions for states + if x0 is None: + # x0 = [0,0,0,0] + x0 = dynstall_mhh_steady(0,u,p) + + # --- Time Integration of states + if method=='continuous': + sol = solve_ivp(lambda t,x: dynstall_mhh_dxdt(t,x,u,p), t_span=[time[0],time[-1]], y0=x0, t_eval=time) + y = sol.y + elif method=='discrete': + y = np.zeros((8,len(time))) + xd = np.zeros(8) + xd[:4] = x0 + xd[4] = u['alpha_34'](time[0]) + xd[5] = 0 # Cl_p + xd[6] = 1.0 # fp + xd[7] = u['U'](time[0]) # U + y[:,0] = xd + for it,t in enumerate(time[1:]): + dt = t - time[it] # Note: time[it] is in fact t-dt + xd = dynstall_mhh_update_discr(t, dt, xd, u, p) + y[:,it+1] = xd + else: + raise NotImplementedError('Method: {}'.format(method)) + + # --- Compute outputs + df=pd.DataFrame() + #print('>>> HACK time 0.002') + df['Time_[s]'] = time + df[prefix + 'Vrel_[m/s]'] = np.zeros(len(time)) + df[prefix + 'alpha_34_[deg]'] = np.zeros(len(time)) + df[prefix + 'Cl_[-]'] = np.zeros(len(time)) + df[prefix + 'Cd_[-]'] = np.zeros(len(time)) + df[prefix + 'Cm_[-]'] = np.zeros(len(time)) + df[prefix + 'Tu_[-]'] = np.zeros(len(time)) + df[prefix + 'alphaE_[deg]'] = np.zeros(len(time)) + df[prefix + 'alphaF_[deg]'] = np.zeros(len(time)) + df[prefix + 'Clp_[-]'] = np.zeros(len(time)) + df[prefix + 'fs_aE_[-]'] = np.zeros(len(time)) + df[prefix + 'fs_aF_[-]'] = np.zeros(len(time)) + df['torsrate'] = np.zeros(len(time)) + df['alpha_34'] = np.zeros(len(time)) + df['U'] = np.zeros(len(time)) + df['T_0'] = np.zeros(len(time)) + df['x1'] = y[0,:] + df['x2'] = y[1,:] + df['x3'] = y[2,:] + df['x4'] = y[3,:] + df['alphaE'] = np.zeros(len(time)) + df['alphaF'] = np.zeros(len(time)) + df['ClP'] = np.zeros(len(time)) + for it,t in enumerate(time): + Cl, Cd, Cm, alphaE, Tu, fs_aE, Cl_fs, alpha_34, omega, U, alphaF, Clp, fs_aF = dynstall_mhh_outputs(t, y[:,it], u, p, calcOutput=True) + df.loc[it,prefix + 'Vrel_[m/s]'] = U + df.loc[it,prefix + 'alpha_34_[deg]'] = alpha_34*180/np.pi + df.loc[it,prefix + 'Cl_[-]'] = Cl + df.loc[it,prefix + 'Cd_[-]'] = Cd + df.loc[it,prefix + 'Cm_[-]'] = Cm + df.loc[it,prefix + 'Tu_[-]'] = Tu + df.loc[it,prefix + 'alphaE_[deg]'] = alphaE*180/np.pi + df.loc[it,prefix + 'alphaF_[deg]'] = alphaF*180/np.pi + df.loc[it,prefix + 'omega_[deg/s]'] = omega*180/np.pi + df.loc[it,prefix + 'Clp_[-]'] = Clp + df.loc[it,prefix + 'fs_aF_[-]'] = fs_aF + df.loc[it,prefix + 'fs_aE_[-]'] = fs_aE + df.loc[it,'U'] = U + df.loc[it,'alpha_34'] = alpha_34 + df.loc[it,'T_0'] = Tu + df.loc[it,'torsrate'] = omega + df.loc[it,'alphaE'] = alphaE + df.loc[it,'ClP'] = Clp + df.loc[it,'alphaF'] = alphaF + df[prefix + 'x1_[rad]'] = y[0,:] + df[prefix + 'x2_[rad]'] = y[1,:] + df[prefix + 'x3_[-]'] = y[2,:] + df[prefix + 'x4_[-]'] = y[3,:] + return df + + +def dynstall_mhh_param_from_polar(P, chord, Tf0=6.0, Tp0=1.5, A1=A1_Jones, A2=A2_Jones, b1=b1_Jones, b2=b2_Jones, constants='Jones', p=None): + if not isinstance(P,Pol): + raise Exception('Input should be an instance of the `Polar` class') + if not P._radians : + raise Exception('MHH dynamic stall implemented for polars in radians only') + + if constants in ['Jones','HAWC2']: + A1, A2, b1, b2 = A1_Jones, A2_Jones, b1_Jones, b2_Jones + Tf0 = 6.0 + Tp0 = 1.5 + elif constants=='OpenFAST': + A1, A2, b1, b2 = A1_FAST, A2_FAST, b1_FAST, b2_FAST + Tf0 = 3.0 + Tp0 = 1.7 + else: + raise NotImplementedError('Constants {}'.format(constants)) + + if p is None: + p=dict() + # Airfoil parameters + p['alpha0'] = P._alpha0 # TODO TODO requires compute params + p['Cla'] = P._linear_slope + if p['alpha0'] is None: + raise Exception('>>>> TODO need to compute params on polar for MHH dyn stall model') + if p['Cla'] is None: + raise Exception('>>>> TODO need to compute params on polar for MHH dyn stall model') + p['chord'] = chord + # Polar functions + p['F_st'] = P.fs_interp + p['Cl_fs'] = P.cl_fs_interp + p['Cl'] = P.cl_interp + p['Cd'] = P.cd_interp + p['Cm'] = P.cm_interp + # Dynamics constants + p['Tf0'] = Tf0 + p['Tp0'] = Tp0 + p['A1'] = A1 + p['A2'] = A2 + p['b1'] = b1 + p['b2'] = b2 + p['alpha0_in_x1x2'] = True + p['U_in_x1x2'] = False + p['scale_x1_x2'] = False + p['old_ClCd_dyn'] = True + return p + +def dynstall_mhh_dxdt(t,x,u,p): + """ Time derivative of states for continous formulation """ + # Inputs + U = u['U'](t) + U_dot = u['U_dot'](t) + omega = u['omega'](t) + alpha_34 = u['alpha_34'](t) + return dynstall_mhh_dxdt_simple(t, x, U, U_dot, omega, alpha_34, p) + +def dynstall_mhh_dxdt_simple(t, x, U, U_dot, omega, alpha_34, p): + """ Time derivative of states for continous formulation """ + # States + x1=x[0] # Downwash memory term 1 + x2=x[1] # Downwash memory term 2 + x3=x[2] # Clp', Lift coefficient with a time lag to the attached lift coeff + x4=x[3] # f'' , Final separation point function + # Parameters + alpha0 = p['alpha0'] + Cla = p['Cla'] + c = p['chord'] + A1 = p['A1'] + A2 = p['A2'] + b1 = p['b1'] + b2 = p['b2'] + F_st = p['F_st'] + # Variables derived from inputs + U = max(U, 0.01) + Tu = max(c/(2*U), 1e-4) # Eq. 23 + Tf = p['Tf0']*Tu # OLD was twice: Tf = p['Tf0']*c/U + Tp = p['Tp0']*Tu # OLD was twice: Tp = p['Tp0']*c/U + # Variables derived from states + if p['alpha0_in_x1x2']: + alphaE = alpha_34*(1-A1-A2)+ x1 + x2 # Eq. 12 + else: + alphaE = (alpha_34-alpha0)*(1-A1-A2)+ x1 + x2 + alpha0 # Eq. 12 + +# alphaE = u['alphaE'](t) # <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< HACK HACK TODO TODO TODO TODO TODO + + Clp = Cla * (alphaE-alpha0) + np.pi * Tu * omega # Eq. 13 + alphaF = x3/Cla+alpha0 # p. 13 + fs_aF = F_st(alphaF) # p. 13 + if(fs_aF<0): + print('Problematic fs:',fs_aF) + x4 = np.clip(x4, 1e-16, 1.0) # Constraining x4 between 0 and 1 increases numerical stability + # State equation + xdot = [0]*4 + if p['alpha0_in_x1x2']: + xdot[0] = -1/Tu * (b1 + c * U_dot/(2*U**2)) * x1 + b1 * A1 / Tu * alpha_34 + xdot[1] = -1/Tu * (b2 + c * U_dot/(2*U**2)) * x2 + b2 * A2 / Tu * alpha_34 + else: + xdot[0] = -1/Tu * (b1 + c * U_dot/(2*U**2)) * x1 + b1 * A1 / Tu * (alpha_34-alpha0) + xdot[1] = -1/Tu * (b2 + c * U_dot/(2*U**2)) * x2 + b2 * A2 / Tu * (alpha_34-alpha0) + xdot[2] = -1/Tp * x3 + 1/Tp * Clp + xdot[3] = -1/Tf * x4 + 1/Tf * fs_aF + return xdot + + + +def dynstall_mhh_update_discr(t, dt, xd_old, u, p): + """ Update discrete states + NOTE: discrete states include additional discrete states + """ + # States + x1_old = xd_old[0] # Downwash memory term 1 + x2_old = xd_old[1] # Downwash memory term 2 + x3_old = xd_old[2] # Clp', Lift coefficient with a time lag to the attached lift coeff + x4_old = xd_old[3] # f'' , Final separation point function + alpha_34_old = xd_old[4] # + Cl_p_old = xd_old[5] # + fs_aF_old = xd_old[6] # + U_old = xd_old[7] # + xd = xd_old.copy() + # Inputs + U = u['U'](t) + U_dot = u['U_dot'](t) + omega = u['omega'](t) + alpha_34 = u['alpha_34'](t) + # Parameters + alpha0 = p['alpha0'] + Cla = p['Cla'] + c = p['chord'] + A1 = p['A1'] + A2 = p['A2'] + b1 = p['b1'] + b2 = p['b2'] + F_st = p['F_st'] + Cl_fs = p['Cl_fs'] + Cd = p['Cd'] + # Variables derived from inputs + U = max(U, 0.01) + Tu = max(c/(2*U), 1e-4) # Eq. 23 + T1 = Tu/b1 + T2 = Tu/b2 + Tp = p['Tp0']*Tu + Tf = p['Tf0']*Tu + # Temporarily remove alpha0 (not necessary) + if p['alpha0_in_x1x2']: + alphaQS_old = alpha_34_old + alphaQS = alpha_34 + else: + alphaQS_old = alpha_34_old - alpha0 + alphaQS = alpha_34 - alpha0 + + eps=1e-4 + exp_val1=np.exp( np.clip(-dt/T1, np.log(eps), 0 )) + exp_val2=np.exp( np.clip(-dt/T2, np.log(eps), 0 )) + exp_val3=np.exp( np.clip(-dt/Tp, np.log(eps), 0 )) + exp_val4=np.exp( np.clip(-dt/Tf, np.log(eps), 0 )) + if ['scale_x1_x2']: + xd[0] = x1_old*exp_val1 + (alpha_34*U-alpha_34_old*U_old) * A1/b1*Tu/dt*(1-exp_val1) * x4_old + xd[1] = x2_old*exp_val2 + (alpha_34*U-alpha_34_old*U_old) * A2/b2*Tu/dt*(1-exp_val2) * x4_old + # x1_ = x1_old*exp + (alpha*U -alpha_old*U_old ) *A1/b1*Tu/dt*(1-exp)*x4 + + else: + if p['U_in_x1x2']: + xd[0] = x1_old*exp_val1 + 0.5*(alphaQS_old+alphaQS) * A1*U*(1-exp_val1) + xd[1] = x2_old*exp_val2 + 0.5*(alphaQS_old+alphaQS) * A2*U*(1-exp_val2) + else: + xd[0] = x1_old*exp_val1 + 0.5*(alphaQS_old+alphaQS) * A1*(1-exp_val1) + xd[1] = x2_old*exp_val2 + 0.5*(alphaQS_old+alphaQS) * A2*(1-exp_val2) + # Effective angle of attack + if ['scale_x1_x2']: + alphaE = alpha_34 - (xd[0]+xd[1])/U + else: + if p['U_in_x1x2']: + alphaE = alphaQS*(1-A1-A2) + (xd[0]+xd[1])/U + else: + alphaE = alphaQS*(1-A1-A2) + (xd[0]+xd[1]) + if p['alpha0_in_x1x2']: + pass + else: + alphaE += alpha0 + alphaQS += alpha0 + +# alphaE = u['alphaE'](t) # <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< HACK HACK TODO TODO TODO TODO TODO + + Cl_p = Cla*(alphaE-alpha0)+np.pi*Tu*omega + xd[2] = x3_old*exp_val3 + 0.5*(Cl_p_old+Cl_p)*(1-exp_val3) + alphaF = xd[2]/Cla + alpha0 + fs_aF = F_st(alphaF) # p. 13 + xd[3] = x4_old*exp_val4 + 0.5*(fs_aF_old + fs_aF)*(1-exp_val4) + # Store "old" values + xd[4] = alpha_34 + xd[5] = Cl_p + xd[6] = fs_aF + xd[7] = U + return xd + +def dynstall_mhh_steady(t, u, p): + """ Return steady state values for the 4 states of the MHH/HGM model""" + # Inputs + U = u['U'](t) + alpha_34 = u['alpha_34'](t) + return dynstall_mhh_steady_simple(U, alpha_34, p) + +def dynstall_mhh_steady_simple(U, alpha_34, p): + # Parameters + c = p['chord'] + alpha0 = p['alpha0'] + Cla = p['Cla'] + A1 = p['A1'] + A2 = p['A2'] + b1 = p['b1'] + b2 = p['b2'] + F_st = p['F_st'] + # Variables derived from inputs + U = max(U, 0.01) + Tu = max(c/(2*U), 1e-4) # Eq. 23 + # Steady states + if p['alpha0_in_x1x2']: + x1 = A1*alpha_34 + x2 = A2*alpha_34 + alphaE = alpha_34*(1-A1-A2) + x1 + x2 # Eq. 12 + else: + x1 = A1*(alpha_34 - alpha0) + x2 = A2*(alpha_34 - alpha0) + alphaE = (alpha_34-alpha0)*(1-A1-A2) + x1 + x2 + alpha0 # Eq. 12 + x3 = Cla * (alphaE-alpha0) + alphaF = x3/Cla+alpha0 # p. 13 + x4 = F_st(alphaF) + return [x1,x2,x3,x4] + +def dynstall_mhh_outputs(t, x, u, p, calcOutput=False): + # Inputs + U = u['U'](t) + U_dot = u['U_dot'](t) + alpha = u['alpha'](t) + omega = u['omega'](t) + alpha_34 = u['alpha_34'](t) + return dynstall_mhh_outputs_simple(t, x, U, U_dot, omega, alpha_34, p, calcOutput=calcOutput) + +def dynstall_mhh_outputs_simple(t, x, U, U_dot, omega, alpha_34, p, calcOutput=False): + # States + x1=x[0] # Downwash memory term 1 + x2=x[1] # Downwash memory term 2 + x3=x[2] # Clp', Lift coefficient with a time lag to the attached lift coeff + x4=x[3] # f'' , Final separation point function + # Parameters + alpha0 = p['alpha0'] + Cla = p['Cla'] + c = p['chord'] + A1 = p['A1'] + A2 = p['A2'] + b1 = p['b1'] + b2 = p['b2'] + F_st = p['F_st'] + Cl_fs = p['Cl_fs'] + Cl = p['Cl'] + Cd = p['Cd'] + Cm = p['Cm'] + + #Cd0 = fCd(alpha0) + #a_st = ?? + # Variables derived from inputs + U = max(U, 0.01) + Tu = max(c/(2*U), 1e-4) # Eq. 23 + + # Variables derived from states + if p['scale_x1_x2']: + alphaE = alpha_34 - (x[0]+x[1])/U + else: + if p['alpha0_in_x1x2']: + if p['U_in_x1x2']: + alphaE = alpha_34*(1-A1-A2)+ (x1 + x2)/U # Eq. 12 + else: + alphaE = alpha_34*(1-A1-A2)+ (x1 + x2) # Eq. 12 + else: + if p['U_in_x1x2']: + alphaE = (alpha_34-alpha0)*(1-A1-A2) + (x1 + x2)/U + alpha0# Eq. 12 + else: + alphaE = (alpha_34-alpha0)*(1-A1-A2) + x1 + x2 + alpha0# Eq. 12 + +# alphaE = u['alphaE'](t) # <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< HACK HACK TODO TODO TODO TODO TODO + + fs_aE = F_st(alphaE) + Cl_sep_e = Cl_fs(alphaE) + Cd_e = Cd(alphaE) + Cm_e = Cm(alphaE) + x4 = np.clip(x4,0,1) + DeltaCdfpp = (np.sqrt(fs_aE)-np.sqrt(x4))/2 - (fs_aE-x4) /4 + + #ast_x4 = (fCm(x4) - fCm(alpha0))/Cl(x4) + #ast_faE = (fCm(fs_aE) - fCm(alpha0))/Cl(fs_aE) + #DeltaCmfpp = (fa_st(x4) - fa_st(fs_aE)) + DeltaCmfpp = 0 # <<<<<<<< 1)] = 1 # Storing self.fs = fs self.cl_fs = cl_fs @@ -1534,6 +1535,7 @@ def cl_linear_slope(alpha, cl, window=None, method="max", nInterp=721, inputInRa - alpha: angle of attack in radians - Cl : lift coefficient - window: [alpha_min, alpha_max]: region when linear slope is sought + - method: 'max', 'optim', 'leastsquare', 'leastsquare_constraint' OUTPUTS: - Cl_alpha, alpha0: lift slope (1/rad) and angle of attack (rad) of zero lift diff --git a/pyFAST/airfoils/__init__.py b/pyFAST/airfoils/__init__.py index e69de29..ed2833a 100644 --- a/pyFAST/airfoils/__init__.py +++ b/pyFAST/airfoils/__init__.py @@ -0,0 +1,2 @@ +from .Polar import * +from .DynamicStall import * diff --git a/pyFAST/airfoils/examples/correction3D.py b/pyFAST/airfoils/examples/correction3D.py index 6a638e8..6528f27 100644 --- a/pyFAST/airfoils/examples/correction3D.py +++ b/pyFAST/airfoils/examples/correction3D.py @@ -11,27 +11,33 @@ def main_correction3D(test=False): polarFile_in = os.path.join(MyDir,'../data/DU21_A17.csv') - r_over_R = 0.2 - chord_over_r = 3./5. - tsr = 10 + r_over_R = 0.2 # spanwise location [-] + chord_over_r = 3./5. # chord divided by local radius [-] + tsr = 10 # tip speed ratio [-] polar = Polar(polarFile_in, compute_params=True, verbose=False) - #ADpol = polar.toAeroDyn(polarFile_AD) + #ADpol = polar.toAeroDyn(polarFile_AD) # Optional, write to AeroDyn format polar3D= polar.correction3D(r_over_R, chord_over_r, tsr) + # --- Plot + fig,ax = plt.subplots(1, 1, sharey=False, figsize=(6.4,4.8)) # (6.4,4.8) + fig.subplots_adjust(left=0.12, right=0.95, top=0.95, bottom=0.11, hspace=0.20, wspace=0.20) + ax.plot(polar.alpha, polar.cl ,'k-' , label= r'2D polar') + ax.plot(polar3D.alpha, polar3D.cl , '-' , label= r'3D corrected') + ax.plot(polar.alpha, polar.cl_inv ,'k--' , label= r'inviscid') + ax.tick_params(direction='in', top=True, right=True) + ax.set_xlabel(r'Angle of attack, $\alpha$ [deg]') + ax.set_ylabel(r'Lift coefficient, $C_l$ [-]') + ax.set_title(r'Airfoils - 3D correction') + ax.set_xlim([-50,50]) + ax.set_ylim([-1.5,2]) + ax.legend() + return polar, polar3D +polar,polar3D = main_correction3D() if __name__ == '__main__': - polar,polar3D = main_correction3D() - - import matplotlib.pyplot as plt - plt.plot(polar.alpha, polar.cl , label= 'cl') - plt.plot(polar3D.alpha, polar3D.cl , label= 'cl 3d') - plt.plot(polar.alpha, polar.cl_inv , label= 'cl inv') - plt.ylim([-1.5,2]) - plt.legend() plt.show() - if __name__ == '__test__': - polar,polar3D = main_correction3D() + pass diff --git a/pyFAST/case_generation/case_gen.py b/pyFAST/case_generation/case_gen.py index 90c4a91..1aef819 100644 --- a/pyFAST/case_generation/case_gen.py +++ b/pyFAST/case_generation/case_gen.py @@ -11,10 +11,7 @@ import pyFAST.input_output.fast_input_file as fi import pyFAST.case_generation.runner as runner import pyFAST.input_output.postpro as postpro -from pyFAST.input_output.fast_wind_file import FASTWndFile -from pyFAST.input_output.rosco_performance_file import ROSCOPerformanceFile -from pyFAST.input_output.csv_file import CSVFile - +from pyFAST.input_output.fast_wind_file import FASTWndFile from pyFAST.input_output.rosco_performance_file import ROSCOPerformanceFile from pyFAST.input_output.csv_file import CSVFile # --------------------------------------------------------------------------------} # --- Template replace # --------------------------------------------------------------------------------{ @@ -150,11 +147,7 @@ def replaceRecurse(templatename_or_newname, FileKey, ParamKey, ParamValue, Files ext = os.path.splitext(templatefilename)[-1] newfilename_full = os.path.join(wd,strID+ext) newfilename = strID+ext - if dryRun: - newfilename = os.path.join(workDir, newfilename) - obj=type('DummyClass', (object,), {'filename':newfilename}) - return newfilename, {'Root':obj} - else: + if dryRun: newfilename = os.path.join(workDir, newfilename) obj=type('DummyClass', (object,), {'filename':newfilename}) return newfilename, {'Root':obj} else: newfilename, newfilename_full = rebaseFileName(templatefilename, workDir, strID) #print('--------------------------------------------------------------') #print('TemplateFile :', templatefilename) @@ -172,8 +165,12 @@ def replaceRecurse(templatename_or_newname, FileKey, ParamKey, ParamValue, Files Key = NewFileKey_or_Key #print('Setting', FileKey, '|',Key, 'to',ParamValue) if Key=='OutList': - OutList=f[Key] - f[Key] = addToOutlist(OutList, ParamValue) + if len(ParamValue)>0: + if len(ParamValue[0])==0: + f[Key] = ParamValue # We replace + else: + OutList=f[Key] + f[Key] = addToOutlist(OutList, ParamValue) # we insert else: f[Key] = ParamValue else: @@ -229,14 +226,21 @@ def replaceRecurse(templatename_or_newname, FileKey, ParamKey, ParamValue, Files removeFASTOuputs(wd) if os.path.exists(wd) and removeAllowed: shutil.rmtree(wd, ignore_errors=False, onerror=handleRemoveReadonlyWin) - copyTree(templateDir, wd) - if removeAllowed: - removeFASTOuputs(wd) + templateDir = os.path.normpath(templateDir) + wd = os.path.normpath(wd) + # NOTE: need some special handling if path are the sames + if templateDir!=wd: + copyTree(templateDir, wd) + if removeAllowed: + removeFASTOuputs(wd) TemplateFiles=[] files=[] + nTot=len(PARAMS) for ip,(wd,p) in enumerate(zip(workDirS,PARAMS)): + if np.mod(ip+1,1000)==0: + print('File {:d}/{:d}'.format(ip,nTot)) if '__index__' not in p.keys(): p['__index__']=ip @@ -248,16 +252,12 @@ def replaceRecurse(templatename_or_newname, FileKey, ParamKey, ParamValue, Files if k =='__index__' or k=='__name__': continue new_mainFile, Files = replaceRecurse(main_file_base, '', k, v, Files, strID, wd, TemplateFiles) - if dryRun: - break - + if dryRun: break # --- Writting files for k,f in Files.items(): if k=='Root': files.append(f.filename) - if not dryRun: - f.write() - + if not dryRun: f.write() # --- Remove extra files at the end if removeRefSubFiles: TemplateFiles, nCounts = np.unique(TemplateFiles, return_counts=True) diff --git a/pyFAST/case_generation/runner.py b/pyFAST/case_generation/runner.py index 72312a0..c584c2c 100644 --- a/pyFAST/case_generation/runner.py +++ b/pyFAST/case_generation/runner.py @@ -195,8 +195,40 @@ def run_fast(input_file, fastExe=None, wait=True, showOutputs=False, showCommand return run_cmd(input_file, fastExe, wait=wait, showOutputs=showOutputs, showCommand=showCommand) -def writeBatch(batchfile, fastfiles, fastExe=None, nBatches=1, pause=False, flags='', flags_after=''): - """ Write batch file, everything is written relative to the batch file""" +def writeBatch(batchfile, fastfiles, fastExe=None, nBatches=1, pause=False, flags='', flags_after='', + run_if_ext_missing=None, + discard_if_ext_present=None, + dispatch=False, + stdOutToFile=False, + echo=True): + """ Write one or several batch file, all paths are written relative to the batch file directory. + The batch file will consist of lines of the form: + [CONDITION] EXE [FLAGS] FILENAME [FLAGS_AFTER] + + INPUTS: + - batchfile: path of the batch file to be written. + If several files are requested (using nBatches) _i is inserted before the extension + - nBatches: split into nBatches files. + - pause: insert a pause statement at the end so that batch file is not closed after execution + - flags: flags (string) to be placed between the executable and the filename + - flags_after: flags (string) to be placed after the filename + - run_if_ext_missing: add a line in the batch file so that the command is only run if + the file `f.EXT` is missing, where .EXT is specified in run_if_ext_missing + If None, the command is always run + - discard_if_ext_present: similar to run_if_ext_missing, but this time, the lines are not written to the batch file + The test for existing outputs is done before writing the batch file + - dispatch: if True, the input files are dispatched (the first nBatches files are dispathced on the nBatches) + - stdOutToFile: if True, the output of the command is redirected to filename.stdout + + example: + writeBatch('dir/MyBatch.bat', ['dir/c1.fst','dir/c2.fst'], 'op.exe', flags='-v', run_if_ext_missing='.outb') + + will generate a file with the following content: + if not exist c1.outb (../of.exe c1.fst) else (echo "Skipping c1.fst") + if not exist c2.outb (../of.exe c2.fst) else (echo "Skipping c2.fst") + + + """ if fastExe is None: fastExe=FAST_EXE fastExe_abs = os.path.abspath(fastExe) @@ -207,20 +239,54 @@ def writeBatch(batchfile, fastfiles, fastExe=None, nBatches=1, pause=False, flag flags=' '+flags if len(flags_after)>0: flags_after=' '+flags_after + + # Remove commandlines if outputs are already present + if discard_if_ext_present: + outfiles = [os.path.splitext(f)[0] + discard_if_ext_present for f in fastfiles] + nIn=len(fastfiles) + fastfiles =[f for f,o in zip(fastfiles,outfiles) if not os.path.exists(o)] + nMiss=len(fastfiles) + if nIn>nMiss: + print('[INFO] WriteBatch: discarding simulations, only {}/{} needed'.format(nMiss, nIn)) + + def writeb(batchfile, fastfiles): with open(batchfile,'w') as f: + if not echo: + if os.name == 'nt': + f.write('@echo off\n') for ff in fastfiles: ff_abs = os.path.abspath(ff) ff_rel = os.path.relpath(ff_abs, batchdir) - l = fastExe_rel + flags + ' '+ ff_rel + flags_after - f.write("{:s}\n".format(l)) + cmd = fastExe_rel + flags + ' '+ ff_rel + flags_after + if stdOutToFile: + stdout = os.path.splitext(ff_rel)[0]+'.stdout' + cmd += ' > ' +stdout + if run_if_ext_missing is not None: + # TODO might be windows only + ff_out = os.path.splitext(ff_rel)[0] + run_if_ext_missing + if os.name == 'nt': + cmd = 'if not exist {} ({}) else (echo Skipping {})'.format(ff_out, cmd, ff_rel) + else: + cmd = 'if [[ ! -f {} ]] ; then {}; else echo Skipping {} ; fi'.format(ff_out, cmd, ff_rel) + f.write("{:s}\n".format(cmd)) if pause: - f.write("pause\n") # windows only.. + f.write("pause\n") # might be windows only.. + + if nBatches==1: writeb(batchfile, fastfiles) return batchfile else: + + if dispatch: + # TODO this can probably be done with a one liner + fastfiles2=[] + for i in range(nBatches): + fastfiles2+=fastfiles[i::nBatches] + fastfiles = fastfiles2 + splits = np.array_split(fastfiles,nBatches) base, ext = os.path.splitext(batchfile) batchfiles=[] diff --git a/pyFAST/input_output/__init__.py b/pyFAST/input_output/__init__.py index df960d4..f5bde70 100644 --- a/pyFAST/input_output/__init__.py +++ b/pyFAST/input_output/__init__.py @@ -1,3 +1,4 @@ +# --- Making main readers available from .csv_file import CSVFile from .excel_file import ExcelFile from .fast_input_deck import FASTInputDeck @@ -32,3 +33,275 @@ # from .cactus_file import CactusFile # from .rosco_performance_file import ROSCOPerformanceFile from .raawmat_file import RAAWMatFile + + +# --- Generic reader / fileformat detection +from .file import File, WrongFormatError, BrokenFormatError, FileNotFoundError, EmptyFileError, OptionalImportError +from .file_formats import FileFormat, isRightFormat +import sys +import os +import numpy as np + +class FormatNotDetectedError(Exception): + pass + +class UserFormatImportError(Exception): + pass + + +_FORMATS=None + +def fileFormats(userpath=None, ignoreErrors=False, verbose=False): + """ return list of fileformats supported by the library + If userpath is provided, + + OUTPUTS: + if ignoreErrors is True: + formats, errors + else: + formats + + """ + global _FORMATS + errors=[] + if _FORMATS is not None: + if ignoreErrors: + return _FORMATS, errors + else: + return _FORMATS + # --- Library formats + from .fast_input_file import FASTInputFile + from .fast_output_file import FASTOutputFile + from .csv_file import CSVFile + from .fast_wind_file import FASTWndFile + from .fast_linearization_file import FASTLinearizationFile + from .fast_summary_file import FASTSummaryFile + from .bmodes_out_file import BModesOutFile + from .hawc2_pc_file import HAWC2PCFile + from .hawc2_ae_file import HAWC2AEFile + from .hawc2_dat_file import HAWC2DatFile + from .hawc2_htc_file import HAWC2HTCFile + from .hawc2_st_file import HAWC2StFile + from .hawcstab2_pwr_file import HAWCStab2PwrFile + from .hawcstab2_ind_file import HAWCStab2IndFile + from .hawcstab2_cmb_file import HAWCStab2CmbFile + from .mannbox_file import MannBoxFile + from .flex_blade_file import FLEXBladeFile + from .flex_profile_file import FLEXProfileFile + from .flex_out_file import FLEXOutFile + from .flex_doc_file import FLEXDocFile + from .flex_wavekin_file import FLEXWaveKinFile + from .excel_file import ExcelFile + from .turbsim_ts_file import TurbSimTSFile + from .turbsim_file import TurbSimFile + from .netcdf_file import NetCDFFile + from .tdms_file import TDMSFile + from .tecplot_file import TecplotFile + from .vtk_file import VTKFile + from .bladed_out_file import BladedFile + from .parquet_file import ParquetFile + from .pickle_file import PickleFile + from .cactus_file import CactusFile + from .raawmat_file import RAAWMatFile + from .rosco_discon_file import ROSCODISCONFile + from .rosco_performance_file import ROSCOPerformanceFile + priorities = [] + formats = [] + def addFormat(priority, fmt): + priorities.append(priority) + formats.append(fmt) + addFormat(0, FileFormat(CSVFile)) + addFormat(0, FileFormat(ExcelFile)) + addFormat(10, FileFormat(TecplotFile)) + addFormat(10, FileFormat(BladedFile)) + addFormat(20, FileFormat(FASTInputFile)) + addFormat(20, FileFormat(FASTOutputFile)) + addFormat(20, FileFormat(FASTWndFile)) + addFormat(20, FileFormat(FASTLinearizationFile)) + addFormat(20, FileFormat(FASTSummaryFile)) + addFormat(20, FileFormat(TurbSimTSFile)) + addFormat(20, FileFormat(TurbSimFile)) + addFormat(30, FileFormat(HAWC2DatFile)) + addFormat(30, FileFormat(HAWC2HTCFile)) + addFormat(30, FileFormat(HAWC2StFile)) + addFormat(30, FileFormat(HAWC2PCFile)) + addFormat(30, FileFormat(HAWC2AEFile)) + addFormat(30, FileFormat(HAWCStab2PwrFile)) + addFormat(30, FileFormat(HAWCStab2IndFile)) + addFormat(30, FileFormat(HAWCStab2CmbFile)) + addFormat(30, FileFormat(MannBoxFile)) + addFormat(40, FileFormat(FLEXBladeFile)) + addFormat(40, FileFormat(FLEXProfileFile)) + addFormat(40, FileFormat(FLEXOutFile)) + addFormat(40, FileFormat(FLEXWaveKinFile)) + addFormat(40, FileFormat(FLEXDocFile)) + addFormat(50, FileFormat(BModesOutFile)) + addFormat(50, FileFormat(ROSCODISCONFile)) + addFormat(50, FileFormat(ROSCOPerformanceFile)) + addFormat(60, FileFormat(NetCDFFile)) + addFormat(60, FileFormat(VTKFile)) + addFormat(60, FileFormat(TDMSFile)) + addFormat(60, FileFormat(ParquetFile)) + addFormat(60, FileFormat(PickleFile)) + addFormat(70, FileFormat(CactusFile)) + addFormat(70, FileFormat(RAAWMatFile)) + + # --- User defined formats from user path + UserClasses, UserPaths, UserModules, UserModuleNames, errors = userFileClasses(userpath, ignoreErrors, verbose=verbose) + for cls, f in zip(UserClasses, UserPaths): + try: + ff = FileFormat(cls) + except Exception as e: + s='Error registering a user fileformat.\n\nThe module location was: {}\n\nThe class name was: {}\n\nMake sure the class has `defaultExtensions` and `formatName` as static methods.\n\nThe exception was:\n{}'.format(f, cls.__name__, e) + if ignoreErrors: + errors.append(s) + continue + else: + raise UserFormatImportError(s) + # Use class.priority + try: + priority = cls.priority() + except: + priority=2 + addFormat(priority, ff) + + # --- Sort fileformats by priorities + formats = np.asarray(formats)[np.argsort(priorities, kind='stable')] + + _FORMATS=formats + if ignoreErrors: + return formats, errors + else: + return formats + + + +def userFileClasses(userpath=None, ignoreErrors=False, verbose=True): + """ return list of user file class in UserData folder""" + if userpath is None: + dataDir = defaultUserDataDir() + userpath = os.path.join(dataDir, 'weio') + errors = [] + UserClasses = [] + UserPaths = [] + UserModules = [] + UserModuleNames = [] + if os.path.exists(userpath): + if verbose: + print('>>> Looking for user modules in folder:',userpath) + import glob + from importlib.machinery import SourceFileLoader + import inspect + pyfiles = glob.glob(os.path.join(userpath,'*.py')) + # Loop through files, look for classes of the form ClassNameFile, + for f in pyfiles: + if f in ['__init__.py']: + continue + mod_name = os.path.basename(os.path.splitext(f)[0]) + try: + if verbose: + print('>>> Trying to load user module:',f) + module = SourceFileLoader(mod_name,f).load_module() + except Exception as e: + s='Error importing a user module.\n\nThe module location was: {}\n\nTry importing this module to debug it.\n\nThe Exception was:\n{}'.format(f, e) + if ignoreErrors: + errors.append(s) + continue + else: + raise UserFormatImportError(s) + found=False + for name, obj in inspect.getmembers(module): + if inspect.isclass(obj): + classname = obj.__name__.lower() + if classname!='file' and classname.find('file')>=0 and classname.find('error')<0: + if verbose: + print(' Found File class with name:',obj.__name__) + UserClasses.append(obj) + UserPaths.append(f) + UserModules.append(module) + UserModuleNames.append(mod_name) + found=True # allowing only one class per file for now.. + break + if not found: + s='Error finding a class named "*File" in the user module.\n\nThe module location was: {}\n\nNo class containing the string "File" in its name was found.'.format(f) + if ignoreErrors: + errors.append(s) + else: + raise UserFormatImportError(s) + return UserClasses, UserPaths, UserModules, UserModuleNames, errors + + +def defaultUserDataDir(): + """ + Returns a parent directory path + where persistent application data can be stored. + # linux: ~/.local/share + # macOS: ~/Library/Application Support + # windows: C:/Users//AppData/Roaming + """ + home = os.path.expanduser('~') + ptfm = sys.platform + if ptfm == "win32": + return os.path.join(home , 'AppData','Roaming') + elif ptfm.startswith("linux"): + return os.path.join(home, '.local', 'share') + elif ptfm == "darwin": + return os.path.join(home, 'Library','Application Support') + else: + print('>>>>>>>>>>>>>>>>> Unknown Platform', sys.platform) + return './UserData' + + + +def detectFormat(filename, **kwargs): + """ Detect the file formats by looping through the known list. + The method may simply try to open the file, if that's the case + the read file is returned. """ + import os + import re + global _FORMATS + if _FORMATS is None: + formats=fileFormats() + else: + formats=_FORMATS + ext = os.path.splitext(filename.lower())[1] + detected = False + i = 0 + while not detected and i0: + extPatMatch = [re.match(pat, ext) is not None for pat in extPatterns] + extMatch = any(extPatMatch) + else: + extMatch = False + if extMatch: # we have a match on the extension + valid, F = isRightFormat(myformat, filename, **kwargs) + if valid: + #print('File detected as :',myformat) + detected=True + return myformat,F + + i += 1 + + if not detected: + raise FormatNotDetectedError('The file format could not be detected for the file: '+filename) + +def read(filename, fileformat=None, **kwargs): + F = None + if not os.path.exists(filename): + raise FileNotFoundError('weio cannot read the following file because it does not exist:\n Inp. path: {}\n Abs. path: {}'.format(filename, os.path.abspath(filename))) + # Detecting format if necessary + if fileformat is None: + fileformat,F = detectFormat(filename, **kwargs) + # Reading the file with the appropriate class if necessary + if not isinstance(F, fileformat.constructor): + F=fileformat.constructor(filename=filename) + return F + + + diff --git a/pyFAST/input_output/bladed_out_file.py b/pyFAST/input_output/bladed_out_file.py new file mode 100644 index 0000000..f6ac45c --- /dev/null +++ b/pyFAST/input_output/bladed_out_file.py @@ -0,0 +1,398 @@ +import os +import numpy as np +import re +import pandas as pd +import glob +import shlex +try: + from .file import File, WrongFormatError, BrokenFormatError +except: + File = dict + class WrongFormatError(Exception): pass + class BrokenFormatError(Exception): pass + + +# --------------------------------------------------------------------------------} +# --- Helper functions +# --------------------------------------------------------------------------------{ +def read_bladed_sensor_file(sensorfile): + """ + Extract relevant informations from a bladed sensor file + """ + with open(sensorfile, 'r') as fid: + sensorLines = fid.readlines() + + dat=dict() # relevant info in sensor file + + ## read sensor file line by line (just read up to line 20) + #while i < 17: + for i, t_line in enumerate(sensorLines): + if i>30: + break + t_line = t_line.replace('\t',' ') + + if t_line.startswith('NDIMENS'): + # check what is matrix dimension of the file. For blade & tower, + # the matrix is 3-dimensional. + temp = t_line[7:].strip().split() + dat['NDIMENS'] = int(temp[-1]); + + elif t_line.startswith('DIMENS'): + # check what is the size of the matrix + # for example, it can be 11x52500 or 12x4x52500 + temp = t_line[6:].strip().split() + dat['nSensors'] = int(temp[0]) + dat['nMajor'] = int(temp[dat['NDIMENS']-1]) + if dat['NDIMENS'] == 2: + dat['nSections'] = 1 + dat['SectionList'] = [] + + elif t_line.startswith('FORMAT'): + # precision: n/a, R*4, R*8, I*4 + temp = t_line[7:].strip() + dat['Precision'] = np.float32 + if temp[-1] == '8': + dat['Precision'] = np.float64 + + elif t_line.startswith('GENLAB'): + # category of the file you are reading: + dat['category'] = t_line[6:].strip().replace('\'','') + + elif t_line.startswith('AXIVAL'): + # Section on the 3rd dimension you are reading + # sometimes, the info is written on "AXITICK" + temp = t_line[7:].split() + dat['SectionList'] = np.array(temp, dtype=float) + dat['nSections'] = len(dat['SectionList']) + + elif t_line.startswith('AXITICK'): + # Section on the 3rd dimension you are reading + # sometimes, the info is written on "AXIVAL" + # Check next line, we concatenate if doesnt start with AXISLAB (Might need more cases) + try: + # Combine the strings into one string + combined_string = ''.join(sensorLines) + + # Search everything betwee AXITICK and AXISLAB with a regex pattern + t_line = re.search(r'(?<=AXITICK).+?(?=AXISLAB)', combined_string, flags=re.DOTALL) + t_line=t_line.group(0) + # Replace consecutive whitespace characters with a single space + t_line = re.sub('\s+', ' ', t_line) + except: + pass + + temp = t_line.strip() + temp = temp.strip('\'').split('\' \'') + dat['SectionList'] = np.array(temp, dtype=str) + dat['nSections'] = len(dat['SectionList']) + + elif t_line.startswith('VARIAB'): + # channel names, NOTE: either quoted, non-quoted, and a mix of both + # Check next line, we concatenate if doesnt start with AXISLAB (Might need more cases) + try: + nextLine=sensorLines[i+1].strip() + if not nextLine.startswith('VARUNIT'): + t_line = t_line.strip()+' '+nextLine + except: + pass + dat['ChannelName'] = shlex.split(t_line[6:]) + + elif t_line.startswith('VARUNIT'): + # channel units: + # Check next line, we concatenate if doesnt start with AXISLAB (Might need more cases) + try: + nextLine=sensorLines[i+1].strip() + if not nextLine.startswith('AXISLAB'): + t_line = t_line.strip()+' '+nextLine + except: + pass + def repUnits(s): + s = s.replace('TT','s^2').replace('T','s').replace('A','rad') + s = s.replace('P','W').replace('L','m').replace('F','N').replace('M','kg') + return s + dat['ChannelUnit']=[repUnits(s) for s in shlex.split(t_line[7:].strip())] + + elif t_line.startswith('MIN '): + dat['MIN'] = float(t_line[3:].strip()) # Start time? + + elif t_line.startswith('STEP'): + dat['STEP'] = float(t_line[4:].strip()) # DT? + + + NeededKeys=['ChannelName','nSensors','nMajor','nSections'] + if not all(key in dat.keys() for key in NeededKeys): + raise BrokenFormatError('Broken or unsupported format. Some necessary keys where not found in the bladed sensor file: {}'.format(sensorfile)) + + if len(dat['ChannelName']) != dat['nSensors']: + raise BrokenFormatError('Broken or unsupported format. Wrong number of channels while reading bladed sensor file: {}'.format(sensorfile)) + # if number of channel names are not matching with Sensor number then create dummy ones: + #dat['ChannelName'] = ['Channel' + str(ss) for ss in range(dat['nSensors'])] + + + return dat + +def OrgData(data, **info): + """ Flatten 3D field into 2D table""" + # since some of the matrices are 3 dimensional, we want to make all + # to 2d matrix, so I am organizing them here: + if info['NDIMENS'] == 3: + SName = [] + SUnit = [] + dataOut = np.zeros( (info['nMajor'],len(info['SectionList'])*len(info['ChannelName'])) ) + + col_vec = -1 + for isec,sec in enumerate(info['SectionList']): + for ichan,(chan,unit) in enumerate(zip(info['ChannelName'], info['ChannelUnit'])): + try: + SName.append(str(np.around(float(sec),2)) + 'm-' + chan) + except ValueError: + SName.append(str(sec) + '-' + chan) + SUnit.append(unit) + col_vec +=1 + dataOut[:,col_vec] = data[:,isec,ichan] + + data = dataOut + info['ChannelName'] = SName + info['ChannelUnit'] = SUnit + else: + pass # Nothing to do for 2D + + return data, info + + + +def read_bladed_output(sensorFilename, readTimeFilesOnly=False): + """ + read a bladed sensor file and data file, reorganize a 3D file into 2D table + """ + # --- Read sensor file and extract relevant informations + sensorInfo = read_bladed_sensor_file(sensorFilename) + nSensors = sensorInfo['nSensors'] + nMajor = sensorInfo['nMajor'] + nSections = sensorInfo['nSections'] + hasTime = 'MIN' and 'STEP' in sensorInfo.keys() + # --- Return if caller only wants time series + if readTimeFilesOnly and not hasTime: + return [], {} + + # --- Read data file + dataFilename = sensorFilename.replace('%','$') + + if isBinary(dataFilename): # it is binary + + with open(os.path.join(dataFilename), 'rb') as fid_2: + data = np.fromfile(fid_2, sensorInfo['Precision']) + + try: + if sensorInfo['NDIMENS'] == 3: + data = np.reshape(data,(nMajor, nSections, nSensors), order='C') + + elif sensorInfo['NDIMENS'] == 2: + data = np.reshape(data,(nMajor,nSensors), order='C') + except: + print('>>> Failed to reshape binary file {}'.format(dataFilename)) + raise + + + else: + #print('it is ascii', NDIMENS) + if sensorInfo['NDIMENS'] == 2: + try: + # Data is stored as time, signal, we reshape to signal, time + data = np.loadtxt(dataFilename) + except ValueError as e: + # Most likely this was a binary file... + data = np.empty((nMajor, nSensors)) * np.nan + print('>>> Value error while reading 2d ascii file: {}'.format(dataFilename)) + raise e + except: + data = np.empty((nMajor, nSensors)) * np.nan + print('>>> Failed to read 2d ascii file: {}'.format(dataFilename)) + raise + + + elif sensorInfo['NDIMENS'] == 3: + try: + # Data is stored as sections, time, signal, we reshape to signal, section, time + data = np.loadtxt(dataFilename).reshape((nMajor, nSections, nSensors),order='C') + except: + data = np.empty((nMajor, nSections, nSensors)) * np.nan + print('>>> Failed to read 3d ascii file: {}'.format(dataFilename)) + + return OrgData(data, **sensorInfo) + + +class BladedFile(File): + r""" + Read a Bladed out put file (current version is only binary files) + + Main methods: + read: it finds all % and $ files based on selected .$PJ file and calls "DataValue" to read data from all those files + toDataFrame: create Pandas dataframe output + + Main data stored: + self.dataSets: dictionary of datasets, for each "length" of data + + example: + filename = r'h:\004_Loads\Sim\Bladed\003\Ramp_up\Bladed_out_ascii.$04' + f = BladedFile(filename) + print(f.dataSets.keys()) + df = f.toDataFrame() + + """ + @staticmethod + def defaultExtensions(): + return ['.%*', '.$*'] + + @staticmethod + def formatName(): + return 'Bladed output file' + + def __init__(self, filename=None, **kwargs): + self.filename = filename + if filename: + self.read(**kwargs) + + def read(self, filename=None, **kwargs): + """ read self, or read filename if provided """ + if filename: + self.filename = filename + if not self.filename: + raise Exception('No filename provided') + if not os.path.isfile(self.filename): + raise OSError(2,'File not found:',self.filename) + if os.stat(self.filename).st_size == 0: + raise EmptyFileError('File is empty:',self.filename) + # Calling children function + self._read(**kwargs) + + def _read(self): + """ + Read a bladed output file, data are in *.$II and sensors in *%II. + - If the file is a *$PJ file, all output files are read + - Otherwise only the current file is read + """ + + basename, ext = os.path.splitext(self.filename) + if ext.lower()=='.$pj': + readTimeFilesOnly=True + searchPattern = basename + '.%[0-9][0-9]*' # find all files in the folder + else: + readTimeFilesOnly=False + searchPattern = basename + ext.replace('$','%') # sensor file name + + # Look for files matching pattern + files = glob.glob(searchPattern) + + # We'll store the data in "dataSets",dictionaries + dataSets={} + + if len(files)==0: + e= FileNotFoundError(searchPattern) + e.filename=(searchPattern) + raise e + elif len(files)==1: + readTimeFilesOnly=False + + files.sort() + + for i,filename in enumerate(files): + + dataFilename = filename.replace('%','$') + try: + # Call "Read_bladed_file" function to Read and store data: + data, info = read_bladed_output(filename, readTimeFilesOnly=readTimeFilesOnly) + except FileNotFoundError as e: + print('>>> Missing datafile: {}'.format(e.filename)) + if len(files)==1: + raise e + continue + except ValueError as e: + print('>>> ValueError while reading: {}'.format(dataFilename)) + if len(files)==1: + raise e + continue + except: + raise + print('>>> Misc error while reading: {}'.format(dataFilename)) + if len(files)==1: + raise + continue + if len(data)==0: + print('>>> Skipping file since no time present {}'.format(filename)) + continue + + # we use number of data as key, but we'll use "name" later + key = info['nMajor'] + + if key in dataSets.keys(): + # dataset with this length are already present, we concatenate + dset = dataSets[key] + dset['data'] = np.column_stack((dset['data'], data)) + dset['sensors'] += info['ChannelName'] + dset['units'] += info['ChannelUnit'] + dset['name'] = 'Misc_'+str(key) + + else: + # We add a new dataset for this length + dataSets[key] = {} + dset = dataSets[key] + # We force a time vector when possible + if 'MIN' and 'STEP' in info.keys(): + time = np.arange(info['nMajor'])*info['STEP'] + info['MIN'] + data = np.column_stack((time, data)) + info['ChannelName'].insert(0, 'Time') + info['ChannelUnit'].insert(0, 's') + + dset['data'] = data + dset['sensors'] = info['ChannelName'] + dset['units'] = info['ChannelUnit'] + dset['name'] = info['category'] + + # Check if we have "many" misc, if only one, replace by "Misc" + keyMisc = [k for k,v in dataSets.items() if v['name'].startswith('Misc_')] + if len(keyMisc)==1: + #dataSets[keyMisc[0]]['name']='Misc' + # We keep only one dataset for simplicity + self.dataSets= {'Misc': dataSets[keyMisc[0]]} + else: + # Instead of using nMajor as key, we use the "name" + self.dataSets= {v['name']: v for (k, v) in dataSets.items()} + + + def toDataFrame(self): + dfs={} + for k,dset in self.dataSets.items(): + BL_ChannelUnit = [ name+' ['+unit+']' for name,unit in zip(dset['sensors'],dset['units'])] + df = pd.DataFrame(data=dset['data'], columns=BL_ChannelUnit) + # remove duplicate columns + df = df.loc[:,~df.columns.duplicated()] + df.columns.name = k # hack for pyDatView when one dataframe is returned + dfs[k] = df + if len(dfs)==1: + return dfs[next(iter(dfs))] + else: + return dfs + + +def isBinary(filename): + with open(filename, 'r') as f: + try: + # first try to read as string + l = f.readline() + # then look for weird characters + for c in l: + code = ord(c) + if code<10 or (code>14 and code<31): + return True + return False + except UnicodeDecodeError: + return True + +if __name__ == '__main__': + pass + #filename = r'E:\Work_Google Drive\Bladed_Sims\Bladed_out_binary.$41' + #Output = BladedFile(filename) + #df = Output.toDataFrame() + + diff --git a/pyFAST/input_output/cactus_element_file.py b/pyFAST/input_output/cactus_element_file.py new file mode 100644 index 0000000..08e7fbf --- /dev/null +++ b/pyFAST/input_output/cactus_element_file.py @@ -0,0 +1,107 @@ +import numpy as np +import pandas as pd +import os + +try: + from .file import File, WrongFormatError, BrokenFormatError +except: + EmptyFileError = type('EmptyFileError', (Exception,),{}) + WrongFormatError = type('WrongFormatError', (Exception,),{}) + BrokenFormatError = type('BrokenFormatError', (Exception,),{}) + File=dict + +class CactusElementFile(File): + + @staticmethod + def defaultExtensions(): + return ['.in'] + + @staticmethod + def formatName(): + return 'CACTUS file' + + def __init__(self,filename=None,**kwargs): + self.filename = filename + if filename: + self.read(**kwargs) + + def read(self, filename=None, **kwargs): + """ read self, or read filename if provided """ + if filename: + self.filename = filename + if not self.filename: + raise Exception('No filename provided') + if not os.path.isfile(self.filename): + raise OSError(2,'File not found:',self.filename) + if os.stat(self.filename).st_size == 0: + raise EmptyFileError('File is empty:',self.filename) + # Calling children function + self._read(**kwargs) + + def write(self, filename=None): + """ write self, or to filename if provided """ + if filename: + self.filename = filename + if not self.filename: + raise Exception('No filename provided') + # Calling children function + self._write() + + def _read(self): + """ """ + import f90nml + from .csv_file import CSVFile + + filepath = self.filename + basepath = '_'.join(filepath.split('_')[:-1]) + basename = os.path.basename(basepath) + parentdir = os.path.dirname(basepath) + mainfile = parentdir[:-6]+basename+'.in' + print(basename) + print(basepath) + print(parentdir) + print(mainfile) + print(os.path.dirname(basepath)) + + + + +# elemfile= +# if len(df.columns)!=len(cols): +# print('column for rename:',cols) +# print('columns in file :',df.columns) +# print(len(df.columns)) +# print(len(cols)) +# raise Exception('Problem with number of columns') +# df.columns=cols +# +# # --- Read elem +# elemfile = f.replace('TimeData','ElementData') +# dsfile = f.replace('TimeData','DSData') +# dfElem = weio.read(elemfile).toDataFrame() + + #with open(self.filename, 'r', errors="surrogateescape") as f: + # for i, line in enumerate(f): + # data.append(line) + + def _write(self): + """ """ + with open(self.filename,'w') as f: + f.write(self.toString) + + def toDataFrame(self): + #cols=['Alpha_[deg]','Cl_[-]','Cd_[-]','Cm_[-]'] + #dfs[name] = pd.DataFrame(data=..., columns=cols) + #df=pd.DataFrame(data=,columns=) + return + + + def toString(self): + s='' + return s + + def __repr__(self): + s ='Class XXXX (attributes: data)\n' + return s + + diff --git a/pyFAST/input_output/cactus_file.py b/pyFAST/input_output/cactus_file.py new file mode 100644 index 0000000..265d0d8 --- /dev/null +++ b/pyFAST/input_output/cactus_file.py @@ -0,0 +1,386 @@ +import numpy as np +import pandas as pd +import os + +try: + from .file import File, WrongFormatError, BrokenFormatError, EmptyFileError +except: + File=dict + EmptyFileError = type('EmptyFileError', (Exception,),{}) + WrongFormatError = type('WrongFormatError', (Exception,),{}) + BrokenFormatError = type('BrokenFormatError', (Exception,),{}) + +class CactusFile(File): + + @staticmethod + def defaultExtensions(): + return ['.in'] + + @staticmethod + def formatName(): + return 'CACTUS file' + + def __init__(self,filename=None,**kwargs): + self.filename = filename + if filename: + self.read(**kwargs) + + def read(self, filename=None, **kwargs): + """ read self, or read filename if provided """ + if filename: + self.filename = filename + if not self.filename: + raise Exception('No filename provided') + if not os.path.isfile(self.filename): + raise OSError(2,'File not found:',self.filename) + if os.stat(self.filename).st_size == 0: + raise EmptyFileError('File is empty:',self.filename) + # Calling children function + self._read(**kwargs) + + def write(self, filename=None): + """ write self, or to filename if provided """ + if filename: + self.filename = filename + if not self.filename: + raise Exception('No filename provided') + # Calling children function + self._write() + + def _read(self): + """ """ + import f90nml + from .csv_file import CSVFile + + filepath = self.filename + basepath = os.path.splitext(filepath)[0] + basename = os.path.basename(basepath) + parentdir = os.path.dirname(basepath) + + # --- Read main input file + nml = f90nml.read(filepath) + for k in ['configinputs','caseinputs']: + self[k] = nml[k] + + # --- Try to read geometry file + arfoilfile = os.path.join(parentdir, nml['caseinputs']['afdpath']) + geomfile = os.path.join(parentdir, nml['caseinputs']['geomfilepath']) + if os.path.exists(geomfile): + with open(geomfile, 'r', errors="surrogateescape") as fid: + geom=dict() + nMax=10 + for i, line in enumerate(fid): + # remove comment + line = line.strip().split('!')[0] + sp = line.strip().split(':') + key = sp[0].strip().lower() + if len(key)>0: + strvalue = sp[1] + try: + value = np.asarray(strvalue.split()).astype(float) + if len(value)==1: + value = value[0] + except: + value = strvalue + geom[key]=value + if i==nMax: + break + self['geom']=geom + self['geom']['file']=geomfile + else: + print('[FAIL] Geom file not found (quantites will be pooorly scaled):',geomfile) + self['geom']={'nblade':1, 'refr':3.28084, 'refar':2, 'file':None} + + # --- Try to read element time data file + timefile = os.path.join(parentdir, 'output', basename+'_TimeData.csv') + if os.path.exists(timefile): + df = CSVFile(timefile).toDataFrame() + nBlades = list(df.columns).count('Blade Fx Coeff. (-)') + self['geom']['nblade'] = list(df.columns).count('Blade Fx Coeff. (-)') + cols=list(df.columns[:8]) + bldCols=['Blade{:d} Fx Coeff. (-)', 'Blade{:d} Fy Coeff. (-)', 'Blade{:d} Fz Coeff. (-)', 'Blade{:d} Torque Coeff. (-)'] + for ib in range(self['geom']['nblade']): + cols+=[b.format(ib+1) for b in bldCols] + df.columns=cols + self['dfTime']=df + + else: + self['dfTime']=None + print('TimeData file not found:',timefile) + + # --- Try to read element data file + elemfile = os.path.join(parentdir, 'output', basename+'_ElementData.csv') + if os.path.exists(elemfile): + dfElem = CSVFile(elemfile).toDataFrame() + self['dfElem'] = dfElem + else: + self['dfElem'] = None + print('ElementData file not found:',elemfile) + + + # --- Read DS file + dsfile = os.path.join(parentdir, 'output', basename+'_DSData.csv') + try: + dfDS =CSVFile(dsfile).toDataFrame() + self['dfDS'] = dfDS + except (FileNotFoundError, EmptyFileError): + self['dfDS'] = None + print('DSData file not found or empty:',dsfile) + + + @property + def omega(self): + return self['caseinputs']['rpm']*2*np.pi/60 + + @property + def TSR(self): + return self['caseinputs']['ut'] + + @property + def RPM(self): + return self['caseinputs']['rpm'] + + @property + def dt(self): + nRot = self['configinputs']['nr'] + nPerRot = self['configinputs']['nti'] + T = 2*np.pi/(self.omega) + return T/nPerRot + + @property + def R(self): + if self['geom']['file'] is not None: + R = self['geom']['refr']/3.28084 # feet to m + else: + R=1 + return R + + @property + def A(self): + # NOTE: Turbine reference area (for force/torque/power normalization) divided by reference radius squared. + if self['geom']['refar'] is not None: + #A = self['geom']['refar']/(3.28084**2) # feet^2 to m^2 + A = self['geom']['refar']*self['geom']['refr']**2 + A /=(3.28084**2) # feet^2 to m^2 + else: + A = (2*self.R)**2 # D^2 + return A + + @property + def U(self): + return self.omega*self.R/self.TSR + + + @property + def time(self): + nRot = self['configinputs']['nr'] + nPerRot = self['configinputs']['nti'] + timeSteps = np.arange(0,nRot*nPerRot) + T = 2*np.pi/(self.omega) + return timeSteps*self.dt + + + def timeDataToOpenFAST(self, df): + """ Convert to similar labels as OpenFAST""" + if df is None: + return None + nRot = self['configinputs']['nr'] + nPerRot = self['configinputs']['nti'] + TSR = self.TSR + CTExcrM = self['caseinputs']['ctexcrm'] + rho = self['caseinputs']['rho']*1.2/0.0023280000 + + time = self.time + if df.shape[0]>> Inconsistent shape, ',iB,ie) + else: + # TODO x/y/R + uix_g=dfSec['IndU (-)'].values*U + uiy_g=dfSec['IndV (-)'].values*U + uiz_g=dfSec['IndW (-)'].values*U + + uix=-(np.cos(psi)*uiz_g + np.sin(psi)*uix_g) + uiy= (np.cos(psi)*uix_g - np.sin(psi)*uiz_g) + + df.insert(c , 'AB{:d}N{:03d}Alpha_[deg]'.format(iB,ie) , alphaSign*dfSec['AOA25 (deg)'].values); c+=1 + df.insert(c , 'AB{:d}N{:03d}Alpha50_[deg]'.format(iB,ie) , alphaSign*dfSec['AOA50 (deg)'].values); c+=1 + df.insert(c , 'AB{:d}N{:03d}Alpha75_[deg]'.format(iB,ie) , alphaSign*dfSec['AOA75 (deg)'].values); c+=1 + df.insert(c , 'AB{:d}N{:03d}Cl_[-]' .format(iB,ie) , alphaSign*dfSec['CL (-)'].values); c+=1 + df.insert(c , 'AB{:d}N{:03d}Cd_[-]' .format(iB,ie) , dfSec['CD (-)'].values); c+=1 + df.insert(c , 'AB{:d}N{:03d}Cm_[-]' .format(iB,ie) , alphaSign*dfSec['CM25 (-)'].values); c+=1 + + #BladeElemOutData(BladeElemOutRow,24)=CN ! Element normal force coefficient (per span) based on local chord and flow velocity + #BladeElemOutData(BladeElemOutRow,25)=CT ! Element tangential force coefficient (per span) based on local chord and flow velocity + df.insert(c , 'AB{:d}N{:03d}Cn_[-]' .format(iB,ie) , alphaSign*dfSec['CN (-)'].values); c+=1 + df.insert(c , 'AB{:d}N{:03d}Ct_[-]' .format(iB,ie) , alphaSign*dfSec['CT (-)'].values); c+=1 + # CT=-CL5*sin(alpha25)+CD5*cos(alpha25) + # CT=-CL5*sin(alpha50)+CD5*cos(alpha50) + Cl= dfSec['CL (-)'].values; Cd= dfSec['CD (-)'].values; alpha= dfSec['AOA25 (deg)'].values*np.pi/180 + df.insert(c , 'AB{:d}N{:03d}Ct2_[-]' .format(iB,ie) , alphaSign*(-Cl*np.sin(alpha) + Cd*np.cos(alpha))); c+=1 + + df.insert(c , 'AB{:d}N{:03d}Cxg_[-]' .format(iB,ie) , dfSec['Fx (-)'].values); c+=1 # TODO, this is likely coefficients related to global coords + df.insert(c , 'AB{:d}N{:03d}Cyg_[-]' .format(iB,ie) , - dfSec['Fz (-)'].values); c+=1 # TODO + df.insert(c , 'AB{:d}N{:03d}Czg_[-]' .format(iB,ie) , dfSec['Fy (-)'].values); c+=1 # TODO + df.insert(c , 'AB{:d}N{:03d}ClC_[-]' .format(iB,ie) , alphaSign*dfSec['CLCirc (-)'].values); c+=1 + df.insert(c , 'AB{:d}N{:03d}Re_[-]' .format(iB,ie) , dfSec['Re (-)'].values/1e6); c+=1 + df.insert(c , 'AB{:d}N{:03d}Gam_[m^2/s]'.format(iB,ie) , alphaSign*dfSec['GB (?)'].values*U*R); c+=1 # TODO + df.insert(c , 'AB{:d}N{:03d}Vrel_[m/s]' .format(iB,ie) , dfSec['Ur (-)'].values*U); c+=1 + df.insert(c , 'AB{:d}N{:03d}Vindx_[m/s]'.format(iB,ie) , uix ); c+=1 # TODO + df.insert(c , 'AB{:d}N{:03d}Vindy_[m/s]'.format(iB,ie) , uiy ); c+=1 # TODO + + if dfDS is not None: + dfSecDS = dfBld_DS[dfBld_DS['Element']==ie] + df.insert(c , 'AB{:d}N{:03d}alpha_34_[deg]'.format(iB,ie) , alphaSign*dfSecDS['alpha (deg)'].values); c+=1 + df.insert(c , 'AB{:d}N{:03d}alphaE_[deg]'.format(iB,ie) , alphaSign*dfSecDS['alrefL (deg)'].values); c+=1 + df.insert(c , 'AB{:d}N{:03d}alphaED_[deg]'.format(iB,ie) , alphaSign*dfSecDS['alrefD (deg)'].values); c+=1 + df.insert(c , 'AB{:d}N{:03d}adotnorm_[-]'.format(iB,ie) , alphaSign*dfSecDS['adotnorm (-)'].values); c+=1 + #df.insert(c , 'AB{:d}N{:03d}AlphaDot_[-]'.format(iB,ie) , alphaSign*dfSec['AdotNorm (-)'].values); c+=1 # TODO + try: + df.insert(c , 'AB{:d}N{:03d}activeL_[-]'.format(iB,ie) , dfSecDS['DynamicFlagL'].values); c+=1 + df.insert(c , 'AB{:d}N{:03d}activeD_[-]'.format(iB,ie) , dfSecDS['DynamicFlagD'].values); c+=1 + df.insert(c , 'AB{:d}N{:03d}alphaLagD_[deg]'.format(iB,ie), alphaSign*dfSecDS['alphaLagD (deg)'].values); c+=1 + df.insert(c , 'AB{:d}N{:03d}delP_[-]'.format(iB,ie) , dfSecDS['delN'].values); c+=1 # NOTE SWAPPING N AND P!!!! + df.insert(c , 'AB{:d}N{:03d}delN_[-]'.format(iB,ie) , dfSecDS['delP'].values); c+=1 # NOTE SWAPPING N AND P!!!! + df.insert(c , 'AB{:d}N{:03d}transA_[-]'.format(iB,ie) , dfSecDS['transA'].values); c+=1 + df.insert(c , 'AB{:d}N{:03d}gammaL_[-]'.format(iB,ie) , dfSecDS['gammaL'].values); c+=1 + df.insert(c , 'AB{:d}N{:03d}gammaD_[-]'.format(iB,ie) , dfSecDS['gammaD'].values); c+=1 + df.insert(c , 'AB{:d}N{:03d}dalphaL_[deg]'.format(iB,ie) , alphaSign*dfSecDS['dalphaL (deg)'].values); c+=1 + df.insert(c , 'AB{:d}N{:03d}dalphaD_[deg]'.format(iB,ie) , alphaSign*(dfSecDS['alpha (deg)'].values -dfSecDS['alrefD (deg)'].values)) + df.insert(c , 'AB{:d}N{:03d}Tu_[s]'.format(iB,ie) , dfSecDS['Tu'].values); c+=1 # TODO TODO WRONG + df.insert(c , 'AB{:d}N{:03d}alphaDot_[rad/s]'.format(iB,ie),alphaSign*dfSecDS['alphadot'].values); c+=1 + df.insert(c , 'AB{:d}N{:03d}alphaDot2_[rad/s]'.format(iB,ie),np.concatenate(([0],np.diff(-dfSecDS['alpha (deg)'].values))))*np.pi/180; c+=1 # TODO TODO WRONG + except: + pass + return df,c + + + def _write(self): + """ """ + with open(self.filename,'w') as f: + f.write(self.toString) + + def toDataFrame(self, format='OpenFAST', alphaSign=-1): + # --- + df,c = self.timeDataToOpenFAST(df = self['dfTime']) + df,c = self.elemDataToOpenFAST(dfElem=self['dfElem'], df=df, c=c, dfDS=self['dfDS'], alphaSign=alphaSign) + return df + + + def toString(self): + s='' + return s + + def __repr__(self): + s ='Class XXXX (attributes: data)\n' + return s + + diff --git a/pyFAST/input_output/converters.py b/pyFAST/input_output/converters.py new file mode 100644 index 0000000..990d1e4 --- /dev/null +++ b/pyFAST/input_output/converters.py @@ -0,0 +1,64 @@ +import os + + +# -------------------------------------------------------------------------------- +# --- Writing pandas DataFrame to different formats +# -------------------------------------------------------------------------------- +# The + +def writeFileDataFrames(fileObject, writer, extension='.conv', filename=None, **kwargs): + """ + From a fileObejct, extract dataframes and write them to disk. + + - fileObject: object inheriting from weio.File with at least + - the attributes .filename + - the method .toDataFrame() + - writer: function with the interface: writer ( dataframe, filename, **kwargs ) + """ + if filename is None: + base, _ = os.path.splitext(fileObject.filename) + filename = base + extension + else: + base, ext = os.path.splitext(filename) + if len(ext)!=0: + extension = ext + if filename == fileObject.filename: + raise Exception('Not overwritting {}. Specify a filename or an extension.'.format(filename)) + + dfs = fileObject.toDataFrame() + if isinstance(dfs, dict): + for name,df in dfs.items(): + filename = base + name + extension + if filename == fileObject.filename: + raise Exception('Not overwritting {}. Specify a filename or an extension.'.format(filename)) + writeDataFrame(df=df, writer=writer, filename=filename, **kwargs) + else: + writeDataFrame(df=dfs, writer=writer, filename=filename, **kwargs) + + +def writeDataFrame(df, writer, filename, **kwargs): + """ + Write a dataframe to disk based on a "writer" function. + - df: pandas dataframe + - writer: function with the interface: writer ( dataframe, filename, **kwargs ) + - filename: filename + """ + writer(df, filename, **kwargs) + +# --- Low level writers +def dataFrameToCSV(df, filename, sep=',', index=False, **kwargs): + base, ext = os.path.splitext(filename) + if len(ext)==0: + filename = base='.csv' + df.to_csv(filename, sep=sep, index=index, **kwargs) + +def dataFrameToOUTB(df, filename, **kwargs): + from .fast_output_file import writeDataFrame as writeDataFrameToOUTB + base, ext = os.path.splitext(filename) + if len(ext)==0: + filename = base='.outb' + writeDataFrameToOUTB(df, filename, binary=True) + +def dataFrameToParquet(df, filename, **kwargs): + df.to_parquet(path=filename, **kwargs) + diff --git a/pyFAST/input_output/csv_file.py b/pyFAST/input_output/csv_file.py index 18cdcbf..5621f03 100644 --- a/pyFAST/input_output/csv_file.py +++ b/pyFAST/input_output/csv_file.py @@ -244,8 +244,6 @@ def strIsFloat(s): self.colNames=['C{}'.format(i) for i in range(len(self.data.columns))] self.data.columns = self.colNames; self.data.rename(columns=lambda x: x.strip(),inplace=True) - #import pdb - #pdb.set_trace() def _write(self): # --- Safety diff --git a/pyFAST/input_output/fast_input_deck.py b/pyFAST/input_output/fast_input_deck.py index 56119ad..9228652 100644 --- a/pyFAST/input_output/fast_input_deck.py +++ b/pyFAST/input_output/fast_input_deck.py @@ -28,6 +28,8 @@ def __init__(self, fullFstPath='', readlist=['all'], verbose=False): if type(verbose) is not bool: raise Exception('`verbose` arguments needs to be a boolean') + # Main Data + self.inputFilesRead = {} self.filename = fullFstPath self.verbose = verbose self.readlist = readlist @@ -38,7 +40,6 @@ def __init__(self, fullFstPath='', readlist=['all'], verbose=False): else: self.readlist = ['Fst']+self.readlist - self.inputfiles = {} # --- Harmonization with AeroElasticSE self.FAST_ver = 'OPENFAST' @@ -74,6 +75,19 @@ def __init__(self, fullFstPath='', readlist=['all'], verbose=False): if len(fullFstPath)>0: self.read() + @property + def ED(self): + ED = self.fst_vt['ElastoDyn'] + if ED is None: + if 'ED' not in self.readlist: + self.readlist.append('ED') + if self.verbose: + print('>>> Reading ED', self.ED_path) + self.fst_vt['ElastoDyn'] = self._read(self.fst_vt['Fst']['EDFile'],'ED') + return self.fst_vt['ElastoDyn'] + else: + return ED + def readAD(self, filename=None, readlist=None, verbose=False, key='AeroDyn15'): """ @@ -145,57 +159,37 @@ def inputFiles(self): files=[] files+=[self.ED_path, self.ED_twr_path, self.ED_bld_path] files+=[self.BD_path, self.BD_bld_path] + files+=[self.SD_path] return [f for f in files if f not in self.unusedNames] - - @property - def ED_relpath(self): + def _relpath(self, k1, k2=None, k3=None): try: - return self.fst_vt['Fst']['EDFile'].replace('"','') - except: - return 'none' - - @property - def ED_twr_relpath(self): - try: - return os.path.join(os.path.dirname(self.fst_vt['Fst']['EDFile']).replace('"',''), self.fst_vt['ElastoDyn']['TwrFile'].replace('"','')) - except: - return 'none' - - @property - def ED_bld_relpath(self): - try: - if 'BldFile(1)' in self.fst_vt['ElastoDyn'].keys(): - return os.path.join(os.path.dirname(self.fst_vt['Fst']['EDFile'].replace('"','')), self.fst_vt['ElastoDyn']['BldFile(1)'].replace('"','')) + if k2 is None: + return self.fst_vt['Fst'][k1].replace('"','') else: - return os.path.join(os.path.dirname(self.fst_vt['Fst']['EDFile'].replace('"','')), self.fst_vt['ElastoDyn']['BldFile1'].replace('"','')) - except: - return 'none' - - @property - def BD_relpath(self): - try: - return self.fst_vt['Fst']['BDBldFile(1)'].replace('"','') + parent = os.path.dirname(self.fst_vt['Fst'][k1]).replace('"','') + if type(k3)==list: + for k in k3: + if k in self.fst_vt[k2].keys(): + child = self.fst_vt[k2][k].replace('"','') + else: + child = self.fst_vt[k2][k3].replace('"','') + return os.path.join(parent, child) except: return 'none' @property - def BD_bld_relpath(self): - try: - return os.path.join(os.path.dirname(self.fst_vt['Fst']['BDBldFile(1)'].replace('"','')), self.fst_vt['BeamDyn']['BldFile'].replace('"','')) - except: - return 'none' - + def ED_path(self): return self._fullpath(self._relpath('EDFile')) @property - def ED_path(self): return self._fullpath(self.ED_relpath) + def SD_path(self): return self._fullpath(self._relpath('SubFile')) @property - def BD_path(self): return self._fullpath(self.BD_relpath) + def BD_path(self): return self._fullpath(self._relpath('BDBldFile(1)')) @property - def BD_bld_path(self): return self._fullpath(self.BD_bld_relpath) + def BD_bld_path(self): return self._fullpath(self._relpath('BDBldFile(1)','BeamDyn','BldFile')) @property - def ED_twr_path(self): return self._fullpath(self.ED_twr_relpath) + def ED_twr_path(self): return self._fullpath(self._relpath('EDFile','ElastoDyn','TwrFile')) @property - def ED_bld_path(self): return self._fullpath(self.ED_bld_relpath) + def ED_bld_path(self): return self._fullpath(self._relpath('EDFile','ElastoDyn',['BldFile(1)','BldFile1'])) @@ -209,10 +203,15 @@ def _fullpath(self, relfilepath): def read(self, filename=None): + """ + Read all OpenFAST inputs files, based on the requested list of modules `readlist` + """ if filename is not None: self.filename = filename # Read main file (.fst, or .drv) and store into key "Fst" + if self.verbose: + print('Reading:', self.FAST_InputFile) self.fst_vt['Fst'] = self._read(self.FAST_InputFile, 'Fst') if self.fst_vt['Fst'] is None: raise Exception('Error reading main file {}'.format(self.filename)) @@ -255,8 +254,8 @@ def read(self, filename=None): if 'EDFile' in self.fst_vt['Fst'].keys(): self.fst_vt['ElastoDyn'] = self._read(self.fst_vt['Fst']['EDFile'],'ED') if self.fst_vt['ElastoDyn'] is not None: - twr_file = self.ED_twr_relpath - bld_file = self.ED_bld_relpath + twr_file = self.ED_twr_path + bld_file = self.ED_bld_path self.fst_vt['ElastoDynTower'] = self._read(twr_file,'EDtwr') self.fst_vt['ElastoDynBlade'] = self._read(bld_file,'EDbld') @@ -280,7 +279,7 @@ def read(self, filename=None): # SubDyn if self.fst_vt['Fst']['CompSub'] == 1: - self.fst_vt['SubDyn'] = self._read(self.fst_vt['Fst']['SubFile'],'HD') + self.fst_vt['SubDyn'] = self._read(self.fst_vt['Fst']['SubFile'], 'SD') # Mooring if self.fst_vt['Fst']['CompMooring']==1: @@ -298,7 +297,7 @@ def read(self, filename=None): # --- Backward compatibility self.fst = self.fst_vt['Fst'] - self.ED = self.fst_vt['ElastoDyn'] + self._ED = self.fst_vt['ElastoDyn'] if not hasattr(self,'AD'): self.AD = None if self.AD is not None: @@ -307,6 +306,7 @@ def read(self, filename=None): self.IW = self.fst_vt['InflowWind'] self.BD = self.fst_vt['BeamDyn'] self.BDbld = self.fst_vt['BeamDynBlade'] + self.SD = self.fst_vt['SubDyn'] @ property def unusedNames(self): @@ -330,12 +330,15 @@ def _read(self, relfilepath, shortkey): return None # Attempt reading - fullpath =os.path.join(self.FAST_directory, relfilepath) + if relfilepath.startswith(self.FAST_directory): + fullpath = relfilepath + else: + fullpath = os.path.join(self.FAST_directory, relfilepath) try: data = FASTInputFile(fullpath) if self.verbose: print('>>> Read: ',fullpath) - self.inputfiles[shortkey] = fullpath + self.inputFilesRead[shortkey] = fullpath return data except FileNotFoundError: print('[WARN] File not found '+fullpath) @@ -460,10 +463,12 @@ def write(self, filename=None, prefix='', suffix='', directory=None): def __repr__(self): s=''+'\n' s+='filename : '+self.filename+'\n' + s+='readlist : {}'.format(self.readlist)+'\n' s+='version : '+self.version+'\n' s+='AD version : '+self.ADversion+'\n' s+='fst_vt : dict{'+','.join([k for k,v in self.fst_vt.items() if v is not None])+'}\n' s+='inputFiles : {}\n'.format(self.inputFiles) + s+='inputFilesRead : {}\n'.format(self.inputFilesRead) s+='\n' return s diff --git a/pyFAST/input_output/fast_input_file.py b/pyFAST/input_output/fast_input_file.py index d666976..b02e4fe 100644 --- a/pyFAST/input_output/fast_input_file.py +++ b/pyFAST/input_output/fast_input_file.py @@ -558,11 +558,16 @@ def _read(self): break elif labelRaw=='re': - nAirfoilTab = self['NumTabs'] - iTab +=1 - if nAirfoilTab>1: - labOffset ='_'+str(iTab) - d['label']=labelRaw+labOffset + try: + nAirfoilTab = self['NumTabs'] + iTab +=1 + if nAirfoilTab>1: + labOffset ='_'+str(iTab) + d['label']=labelRaw+labOffset + except: + # Unsteady driver input file... + pass + #print('label>',d['label'],'<',type(d['label'])); #print('value>',d['value'],'<',type(d['value'])); @@ -870,30 +875,16 @@ def _toDataFrame(self): if self.getIDSafe('TwFAM1Sh(2)')>0: # Hack for tower files, we add the modes + # NOTE: we provide interpolated shape function just in case the resolution of the input file is low.. x=Val[:,0] Modes=np.zeros((x.shape[0],4)) - Modes[:,0] = x**2 * self['TwFAM1Sh(2)'] \ - + x**3 * self['TwFAM1Sh(3)'] \ - + x**4 * self['TwFAM1Sh(4)'] \ - + x**5 * self['TwFAM1Sh(5)'] \ - + x**6 * self['TwFAM1Sh(6)'] - Modes[:,1] = x**2 * self['TwFAM2Sh(2)'] \ - + x**3 * self['TwFAM2Sh(3)'] \ - + x**4 * self['TwFAM2Sh(4)'] \ - + x**5 * self['TwFAM2Sh(5)'] \ - + x**6 * self['TwFAM2Sh(6)'] - Modes[:,2] = x**2 * self['TwSSM1Sh(2)'] \ - + x**3 * self['TwSSM1Sh(3)'] \ - + x**4 * self['TwSSM1Sh(4)'] \ - + x**5 * self['TwSSM1Sh(5)'] \ - + x**6 * self['TwSSM1Sh(6)'] - Modes[:,3] = x**2 * self['TwSSM2Sh(2)'] \ - + x**3 * self['TwSSM2Sh(3)'] \ - + x**4 * self['TwSSM2Sh(4)'] \ - + x**5 * self['TwSSM2Sh(5)'] \ - + x**6 * self['TwSSM2Sh(6)'] + Modes[:,0] = x**2 * self['TwFAM1Sh(2)'] + x**3 * self['TwFAM1Sh(3)'] + x**4 * self['TwFAM1Sh(4)'] + x**5 * self['TwFAM1Sh(5)'] + x**6 * self['TwFAM1Sh(6)'] + Modes[:,1] = x**2 * self['TwFAM2Sh(2)'] + x**3 * self['TwFAM2Sh(3)'] + x**4 * self['TwFAM2Sh(4)'] + x**5 * self['TwFAM2Sh(5)'] + x**6 * self['TwFAM2Sh(6)'] + Modes[:,2] = x**2 * self['TwSSM1Sh(2)'] + x**3 * self['TwSSM1Sh(3)'] + x**4 * self['TwSSM1Sh(4)'] + x**5 * self['TwSSM1Sh(5)'] + x**6 * self['TwSSM1Sh(6)'] + Modes[:,3] = x**2 * self['TwSSM2Sh(2)'] + x**3 * self['TwSSM2Sh(3)'] + x**4 * self['TwSSM2Sh(4)'] + x**5 * self['TwSSM2Sh(5)'] + x**6 * self['TwSSM2Sh(6)'] Val = np.hstack((Val,Modes)) - Cols = Cols + ['ShapeForeAft1_[-]','ShapeForeAft2_[-]','ShapeSideSide1_[-]','ShapeSideSide2_[-]'] + ShapeCols = [c+'_[-]' for c in ['ShapeForeAft1','ShapeForeAft2','ShapeSideSide1','ShapeSideSide2']] + Cols = Cols + ShapeCols name=d['label'] @@ -1548,21 +1539,9 @@ def _toDataFrame(self): # We add the shape functions for EDBladeFile x=df['BlFract_[-]'].values Modes=np.zeros((x.shape[0],3)) - Modes[:,0] = x**2 * self['BldFl1Sh(2)'] \ - + x**3 * self['BldFl1Sh(3)'] \ - + x**4 * self['BldFl1Sh(4)'] \ - + x**5 * self['BldFl1Sh(5)'] \ - + x**6 * self['BldFl1Sh(6)'] - Modes[:,1] = x**2 * self['BldFl2Sh(2)'] \ - + x**3 * self['BldFl2Sh(3)'] \ - + x**4 * self['BldFl2Sh(4)'] \ - + x**5 * self['BldFl2Sh(5)'] \ - + x**6 * self['BldFl2Sh(6)'] - Modes[:,2] = x**2 * self['BldEdgSh(2)'] \ - + x**3 * self['BldEdgSh(3)'] \ - + x**4 * self['BldEdgSh(4)'] \ - + x**5 * self['BldEdgSh(5)'] \ - + x**6 * self['BldEdgSh(6)'] + Modes[:,0] = x**2 * self['BldFl1Sh(2)'] + x**3 * self['BldFl1Sh(3)'] + x**4 * self['BldFl1Sh(4)'] + x**5 * self['BldFl1Sh(5)'] + x**6 * self['BldFl1Sh(6)'] + Modes[:,1] = x**2 * self['BldFl2Sh(2)'] + x**3 * self['BldFl2Sh(3)'] + x**4 * self['BldFl2Sh(4)'] + x**5 * self['BldFl2Sh(5)'] + x**6 * self['BldFl2Sh(6)'] + Modes[:,2] = x**2 * self['BldEdgSh(2)'] + x**3 * self['BldEdgSh(3)'] + x**4 * self['BldEdgSh(4)'] + x**5 * self['BldEdgSh(5)'] + x**6 * self['BldEdgSh(6)'] df[['ShapeFlap1_[-]','ShapeFlap2_[-]','ShapeEdge1_[-]']]=Modes return df @@ -1642,29 +1621,15 @@ def _writeSanityChecks(self): def _toDataFrame(self): df = FASTInputFileBase._toDataFrame(self) # We add the shape functions for EDBladeFile - x=df['BlFract_[-]'].values - Modes=np.zeros((x.shape[0],3)) - Modes[:,0] = x**2 * self['TwFAM1Sh(2)'] \ - + x**3 * self['TwFAM1Sh(3)'] \ - + x**4 * self['TwFAM1Sh(4)'] \ - + x**5 * self['TwFAM1Sh(5)'] \ - + x**6 * self['TwFAM1Sh(6)'] - Modes[:,1] = x**2 * self['TwFAM2Sh(2)'] \ - + x**3 * self['TwFAM2Sh(3)'] \ - + x**4 * self['TwFAM2Sh(4)'] \ - + x**5 * self['TwFAM2Sh(5)'] \ - + x**6 * self['TwFAM2Sh(6)'] - Modes[:,2] = x**2 * self['TwSSM1Sh(2)'] \ - + x**3 * self['TwSSM1Sh(3)'] \ - + x**4 * self['TwSSM1Sh(4)'] \ - + x**5 * self['TwSSM1Sh(5)'] \ - + x**6 * self['TwSSM1Sh(6)'] - Modes[:,3] = x**2 * self['TwSSM2Sh(2)'] \ - + x**3 * self['TwSSM2Sh(3)'] \ - + x**4 * self['TwSSM2Sh(4)'] \ - + x**5 * self['TwSSM2Sh(5)'] \ - + x**6 * self['TwSSM2Sh(6)'] - df[['ShapeForeAft1_[-]','ShapeForeAft2_[-]','ShapeSideSide1_[-]','ShapeSideSide2_[-]']]=Modes + # NOTE: we provide interpolated shape function just in case the resolution of the input file is low.. + x = df['HtFract_[-]'].values + Modes=np.zeros((x.shape[0],4)) + Modes[:,0] = x**2 * self['TwFAM1Sh(2)'] + x**3 * self['TwFAM1Sh(3)'] + x**4 * self['TwFAM1Sh(4)'] + x**5 * self['TwFAM1Sh(5)'] + x**6 * self['TwFAM1Sh(6)'] + Modes[:,1] = x**2 * self['TwFAM2Sh(2)'] + x**3 * self['TwFAM2Sh(3)'] + x**4 * self['TwFAM2Sh(4)'] + x**5 * self['TwFAM2Sh(5)'] + x**6 * self['TwFAM2Sh(6)'] + Modes[:,2] = x**2 * self['TwSSM1Sh(2)'] + x**3 * self['TwSSM1Sh(3)'] + x**4 * self['TwSSM1Sh(4)'] + x**5 * self['TwSSM1Sh(5)'] + x**6 * self['TwSSM1Sh(6)'] + Modes[:,3] = x**2 * self['TwSSM2Sh(2)'] + x**3 * self['TwSSM2Sh(3)'] + x**4 * self['TwSSM2Sh(4)'] + x**5 * self['TwSSM2Sh(5)'] + x**6 * self['TwSSM2Sh(6)'] + ShapeCols = [c+'_[-]' for c in ['ShapeForeAft1','ShapeForeAft2','ShapeSideSide1','ShapeSideSide2']] + df[ShapeCols]=Modes return df @property diff --git a/pyFAST/input_output/fast_output_file.py b/pyFAST/input_output/fast_output_file.py index baa2dc2..f83ad6d 100644 --- a/pyFAST/input_output/fast_output_file.py +++ b/pyFAST/input_output/fast_output_file.py @@ -9,11 +9,21 @@ - data, info = def load_binary_output(filename, use_buffer=True) - def writeDataFrame(df, filename, binary=True) - def writeBinary(fileName, channels, chanNames, chanUnits, fileID=2, descStr='') + +NOTE: + - load_binary and writeBinary are not "fully reversible" for now. + Some small numerical errors are introduced in the conversion. + Some of the error is likely due to the fact that Python converts to "int" and "float" (double). + Maybe all the operations should be done in single. I tried but failed. + I simply wonder if the operation is perfectly reversible. + + """ from itertools import takewhile import numpy as np import pandas as pd import struct +import ctypes import os import re try: @@ -30,6 +40,13 @@ class EmptyFileError(Exception): pass print('CSVFile not available') + +FileFmtID_WithTime = 1 # File identifiers used in FAST +FileFmtID_WithoutTime = 2 +FileFmtID_NoCompressWithoutTime = 3 +FileFmtID_ChanLen_In = 4 # Channel length included in file + + # --------------------------------------------------------------------------------} # --- OUT FILE # --------------------------------------------------------------------------------{ @@ -108,7 +125,12 @@ def readline(iLine): self.info['attribute_units']=readline(3).replace('sec','s').split() self.info['attribute_names']=self.data.columns.values else: - self.data, self.info = load_output(self.filename) + if isBinary(self.filename): + self.data, self.info = load_binary_output(self.filename) + self['binary']=True + else: + self.data, self.info = load_ascii_output(self.filename) + self['binary']=False except MemoryError as e: raise BrokenReaderError('FAST Out File {}: Memory error encountered\n{}'.format(self.filename,e)) except Exception as e: @@ -120,13 +142,13 @@ def readline(iLine): self.info['attribute_units'] = [re.sub(r'[()\[\]]','',u) for u in self.info['attribute_units']] - def _write(self): - if self['binary']: - channels = self.data - chanNames = self.info['attribute_names'] - chanUnits = self.info['attribute_units'] - descStr = self.info['description'] - writeBinary(self.filename, channels, chanNames, chanUnits, fileID=2, descStr=descStr) + def _write(self, binary=None, fileID=4): + if binary is None: + binary = self['binary'] + + if binary: + # NOTE: user provide a filename, we allow overwrite + self.toOUTB(filename=self.filename, fileID=fileID, noOverWrite=False) else: # ascii output with open(self.filename,'w') as f: @@ -155,9 +177,6 @@ def toDataFrame(self): raise BrokenFormatError('Inconstistent number of columns between headers ({}) and data ({}) for file {}'.format(len(cols), self.data.shape[1], self.filename)) df = pd.DataFrame(data=self.data,columns=cols) - # Remove duplicate columns if they are present - df = df.loc[:,~df.columns.duplicated()].copy() - return df def writeDataFrame(self, df, filename, binary=True): @@ -170,38 +189,59 @@ def __repr__(self): s+='and keys: {}\n'.format(self.keys()) return s + # -------------------------------------------------------------------------------- + # --- Converters + # -------------------------------------------------------------------------------- + def toOUTB(self, filename=None, extension='.outb', fileID=4, noOverWrite=True, **kwargs): + #NOTE: we override the File class here + if filename is None: + base, _ = os.path.splitext(self.filename) + filename = base + extension + else: + base, ext = os.path.splitext(filename) + if len(ext)!=0: + extension = ext + if (filename==self.filename) and noOverWrite: + raise Exception('Not overwritting {}. Specify a filename or an extension.'.format(filename)) + + # NOTE: fileID=2 will chop the channels name of long channels use fileID4 instead + channels = self.data + chanNames = self.info['attribute_names'] + chanUnits = self.info['attribute_units'] + descStr = self.info['description'] + if isinstance(descStr, list): + descStr=(''.join(descStr[:2])).replace('\n','') + writeBinary(filename, channels, chanNames, chanUnits, fileID=fileID, descStr=descStr) + + # -------------------------------------------------------------------------------- # --- Helper low level functions # -------------------------------------------------------------------------------- -def load_output(filename): - """Load a FAST binary or ascii output file - - Parameters - ---------- - filename : str - filename - - Returns - ------- - data : ndarray - data values - info : dict - info containing: - - name: filename - - description: description of dataset - - attribute_names: list of attribute names - - attribute_units: list of attribute units - """ - - assert os.path.isfile(filename), "File, %s, does not exists" % filename +def isBinary(filename): with open(filename, 'r') as f: try: - f.readline() + # first try to read as string + l = f.readline() + # then look for weird characters + for c in l: + code = ord(c) + if code<10 or (code>14 and code<31): + return True + return False except UnicodeDecodeError: - return load_binary_output(filename) - return load_ascii_output(filename) + return True + + + + + +def load_ascii_output(filename, method='numpy'): + + + if method in ['forLoop','pandas']: + from .file import numberOfLines + nLines = numberOfLines(filename, method=2) -def load_ascii_output(filename): with open(filename) as f: info = {} info['name'] = os.path.splitext(os.path.basename(filename))[0] @@ -220,12 +260,42 @@ def load_ascii_output(filename): info['description'] = header info['attribute_names'] = l.split() info['attribute_units'] = [unit[1:-1] for unit in f.readline().split()] - # --- - # Data, up to end of file or empty line (potential comment line at the end) -# data = np.array([l.strip().split() for l in takewhile(lambda x: len(x.strip())>0, f.readlines())]).astype(np.float) - # --- - data = np.loadtxt(f, comments=('This')) # Adding "This" for the Hydro Out files.. - return data, info + + nHeader = len(header)+1 + nCols = len(info['attribute_names']) + + if method=='numpy': + # The most efficient, and will remove empty lines and the lines that starts with "This" + # ("This" is found at the end of some Hydro Out files..) + data = np.loadtxt(f, comments=('This')) + + elif method =='pandas': + # Could probably be made more efficient, but + f.close() + nRows = nLines-nHeader + sep=r'\s+' + cols= ['C{}'.format(i) for i in range(nCols)] + df = pd.read_csv(filename, sep=sep, header=0, skiprows=nHeader, names=cols, dtype=float, na_filter=False, nrows=nRows, engine='pyarrow'); print(df) + data=df.values + + elif method == 'forLoop': + # The most inefficient + nRows = nLines-nHeader + sep=r'\s+' + data = np.zeros((nRows, nCols)) + for i in range(nRows): + l = f.readline().strip() + sp = np.array(l.split()).astype(np.float) + data[i,:] = sp[:nCols] + + elif method == 'listCompr': + # --- Method 4 - List comprehension + # Data, up to end of file or empty line (potential comment line at the end) + data = np.array([l.strip().split() for l in takewhile(lambda x: len(x.strip())>0, f.readlines())]).astype(np.float) + else: + raise NotImplementedError() + + return data, info def load_binary_output(filename, use_buffer=True): @@ -240,8 +310,15 @@ def load_binary_output(filename, use_buffer=True): % % Edited for FAST v7.02.00b-bjj 22-Oct-2012 """ - def fread(fid, n, type): - fmt, nbytes = {'uint8': ('B', 1), 'int16':('h', 2), 'int32':('i', 4), 'float32':('f', 4), 'float64':('d', 8)}[type] + StructDict = { + 'uint8': ('B', 1, np.uint8), + 'int16':('h', 2, np.int16), + 'int32':('i', 4, np.int32), + 'float32':('f', 4, np.float32), + 'float64':('d', 8, np.float64)} + def fread(fid, n, dtype): + fmt, nbytes, npdtype = StructDict[dtype] + #return np.array(struct.unpack(fmt * n, fid.read(nbytes * n)), dtype=npdtype) return struct.unpack(fmt * n, fid.read(nbytes * n)) def freadRowOrderTableBuffered(fid, n, type_in, nCols, nOff=0, type_out='float64'): @@ -282,12 +359,6 @@ def freadRowOrderTableBuffered(fid, n, type_in, nCols, nOff=0, type_out='float64 raise Exception('Read only %d of %d values in file:' % (nIntRead, n, filename)) return data - - FileFmtID_WithTime = 1 # File identifiers used in FAST - FileFmtID_WithoutTime = 2 - FileFmtID_NoCompressWithoutTime = 3 - FileFmtID_ChanLen_In = 4 - with open(filename, 'rb') as fid: #---------------------------- # get the header information @@ -307,11 +378,11 @@ def freadRowOrderTableBuffered(fid, n, type_in, nCols, nOff=0, type_out='float64 NT = fread(fid, 1, 'int32')[0] #; % The number of time steps, INT(4) if FileID == FileFmtID_WithTime: - TimeScl = fread(fid, 1, 'float64') #; % The time slopes for scaling, REAL(8) - TimeOff = fread(fid, 1, 'float64') #; % The time offsets for scaling, REAL(8) + TimeScl = fread(fid, 1, 'float64')[0] # The time slopes for scaling, REAL(8) + TimeOff = fread(fid, 1, 'float64')[0] # The time offsets for scaling, REAL(8) else: - TimeOut1 = fread(fid, 1, 'float64') #; % The first time in the time series, REAL(8) - TimeIncr = fread(fid, 1, 'float64') #; % The time increment, REAL(8) + TimeOut1 = fread(fid, 1, 'float64')[0] # The first time in the time series, REAL(8) + TimeIncr = fread(fid, 1, 'float64')[0] # The time increment, REAL(8) if FileID == FileFmtID_NoCompressWithoutTime: ColScl = np.ones ((NumOutChans, 1)) # The channel slopes for scaling, REAL(4) @@ -395,40 +466,7 @@ def freadRowOrderTableBuffered(fid, n, type_in, nCols, nOff=0, type_out='float64 return data, info -def writeDataFrame(df, filename, binary=True): - channels = df.values - # attempt to extract units from channel names - chanNames=[] - chanUnits=[] - for c in df.columns: - c = c.strip() - name = c - units = '' - if c[-1]==']': - chars=['[',']'] - elif c[-1]==')': - chars=['(',')'] - else: - chars=[] - if len(chars)>0: - op,cl = chars - iu=c.rfind(op) - if iu>1: - name = c[:iu] - unit = c[iu+1:].replace(cl,'') - if name[-1]=='_': - name=name[:-1] - - chanNames.append(name) - chanUnits.append(unit) - - if binary: - writeBinary(filename, channels, chanNames, chanUnits) - else: - NotImplementedError() - - -def writeBinary(fileName, channels, chanNames, chanUnits, fileID=2, descStr=''): +def writeBinary(fileName, channels, chanNames, chanUnits, fileID=4, descStr=''): """ Write an OpenFAST binary file. @@ -463,7 +501,7 @@ def writeBinary(fileName, channels, chanNames, chanUnits, fileID=2, descStr=''): time = channels[:,iTime] timeStart = time[0] - timeIncr = time[1]-time[0] + timeIncr = (time[-1]-time[0])/(nT-1) dataWithoutTime = channels[:,1:] # Compute data range, scaling and offsets to convert to int16 @@ -482,8 +520,8 @@ def writeBinary(fileName, channels, chanNames, chanUnits, fileID=2, descStr=''): ColOff = np.single(int16Min - np.single(mins)*ColScl) #Just available for fileID - if fileID != 2: - print("current version just works with FileID = 2") + if fileID not in [2,4]: + print("current version just works with fileID = 2 or 4") else: with open(fileName,'wb') as fid: @@ -497,6 +535,14 @@ def writeBinary(fileName, channels, chanNames, chanUnits, fileID=2, descStr=''): # Write header informations fid.write(struct.pack('@h',fileID)) + if fileID == FileFmtID_ChanLen_In: + maxChanLen = np.max([len(s) for s in chanNames]) + maxUnitLen = np.max([len(s) for s in chanUnits]) + nChar = max(maxChanLen, maxUnitLen) + fid.write(struct.pack('@h',nChar)) + else: + nChar = 10 + fid.write(struct.pack('@i',nChannels)) fid.write(struct.pack('@i',nT)) fid.write(struct.pack('@d',timeStart)) @@ -509,27 +555,86 @@ def writeBinary(fileName, channels, chanNames, chanUnits, fileID=2, descStr=''): # Write channel names for chan in chanNames: - ordchan = [ord(char) for char in chan]+ [32]*(10-len(chan)) - fid.write(struct.pack('@10B', *ordchan)) + chan = chan[:nChar] + ordchan = [ord(char) for char in chan] + [32]*(nChar-len(chan)) + fid.write(struct.pack('@'+str(nChar)+'B', *ordchan)) # Write channel units for unit in chanUnits: - ordunit = [ord(char) for char in unit]+ [32]*(10-len(unit)) - fid.write(struct.pack('@10B', *ordunit)) - - # Pack data - packedData=np.zeros((nT, nChannels), dtype=np.int16) + unit = unit[:nChar] + ordunit = [ord(char) for char in unit] + [32]*(nChar-len(unit)) + fid.write(struct.pack('@'+str(nChar)+'B', *ordunit)) + + # --- Pack and write data + # Method 1 + #packedData=np.zeros((nT, nChannels), dtype=np.int16) + #for iChan in range(nChannels): + # packedData[:,iChan] = np.clip( ColScl[iChan]*dataWithoutTime[:,iChan]+ColOff[iChan], int16Min, int16Max) + #packedData = packedData.ravel() + ## NOTE: the *packedData converts to a tuple before passing to struct.pack + ## which is inefficient + #fid.write(struct.pack('@{}h'.format(packedData.size), *packedData)) + + # --- Method 2 + #packedData=np.zeros((nT, nChannels), dtype=np.int16) + #for iChan in range(nChannels): + # packedData[:,iChan] = np.clip( ColScl[iChan]*dataWithoutTime[:,iChan]+ColOff[iChan], int16Min, int16Max) + #packedData = packedData.ravel() + ## Here we use slice assignment + #buf = (ctypes.c_int16 * len(packedData))() + #buf[:] = packedData + #fid.write(buf) + + # --- Method 3 use packedData as slice directly + packedData = (ctypes.c_int16 * (nT*nChannels))() for iChan in range(nChannels): - packedData[:,iChan] = np.clip( ColScl[iChan]*dataWithoutTime[:,iChan]+ColOff[iChan], int16Min, int16Max) + packedData[iChan::nChannels] = np.clip( ColScl[iChan]*dataWithoutTime[:,iChan]+ColOff[iChan], int16Min, int16Max).astype(np.int16) + fid.write(packedData) + - # Write data - fid.write(struct.pack('@{}h'.format(packedData.size), *packedData.flatten())) fid.close() +def writeDataFrame(df, filename, binary=True): + """ write a DataFrame to OpenFAST output format""" + channels = df.values + # attempt to extract units from channel names + chanNames=[] + chanUnits=[] + for c in df.columns: + c = c.strip() + name = c + unit = '' + if c[-1]==']': + chars=['[',']'] + elif c[-1]==')': + chars=['(',')'] + else: + chars=[] + if len(chars)>0: + op,cl = chars + iu=c.rfind(op) + if iu>1: + name = c[:iu] + unit = c[iu+1:].replace(cl,'') + if name[-1]=='_': + name=name[:-1] + + chanNames.append(name) + chanUnits.append(unit) + + if binary: + writeBinary(filename, channels, chanNames, chanUnits, fileID=FileFmtID_ChanLen_In) + else: + NotImplementedError() + + if __name__ == "__main__": B=FASTOutputFile('tests/example_files/FASTOutBin.outb') df=B.toDataFrame() B.writeDataFrame(df, 'tests/example_files/FASTOutBin_OUT.outb') + B.toOUTB(extension='.dat.outb') + B.toParquet() + B.toCSV() diff --git a/pyFAST/input_output/file.py b/pyFAST/input_output/file.py index 9d0d936..09b7e8c 100644 --- a/pyFAST/input_output/file.py +++ b/pyFAST/input_output/file.py @@ -1,4 +1,5 @@ import os +from collections import OrderedDict class WrongFormatError(Exception): pass @@ -12,12 +13,15 @@ class BrokenFormatError(Exception): class BrokenReaderError(Exception): pass +class OptionalImportError(Exception): + pass + try: #Python3 FileNotFoundError=FileNotFoundError except NameError: # Python2 FileNotFoundError = IOError -class File(dict): +class File(OrderedDict): def __init__(self,filename=None,**kwargs): if filename: self.read(filename, **kwargs) @@ -36,13 +40,13 @@ def read(self, filename=None, **kwargs): # Calling children function self._read(**kwargs) - def write(self, filename=None): + def write(self, filename=None, **kwargs): if filename: self.filename = filename if not self.filename: raise Exception('No filename provided') # Calling children function - self._write() + self._write(**kwargs) def toDataFrame(self): return self._toDataFrame() @@ -73,8 +77,32 @@ def encoding(self): # --------------------------------------------------------------------------------} - # --- Helper methods + # --- Conversions # --------------------------------------------------------------------------------{ + def toCSV(self, filename=None, extension='.csv', **kwargs): + """ By default, writes dataframes to CSV + Keywords arguments are the same as pandas DataFrame to_csv: + - sep: separator + - index: write index or not + - etc. + """ + from .converters import writeFileDataFrames + from .converters import dataFrameToCSV + writeFileDataFrames(self, dataFrameToCSV, filename=filename, extension=extension, **kwargs) + + def toOUTB(self, filename=None, extension='.outb', **kwargs): + """ write to OUTB""" + from .converters import writeFileDataFrames + from .converters import dataFrameToOUTB + writeFileDataFrames(self, dataFrameToOUTB, filename=filename, extension=extension, **kwargs) + + + def toParquet(self, filename=None, extension='.parquet', **kwargs): + """ write to OUTB""" + from .converters import writeFileDataFrames + from .converters import dataFrameToParquet + writeFileDataFrames(self, dataFrameToParquet, filename=filename, extension=extension, **kwargs) + # -------------------------------------------------------------------------------- # --- Sub class methods @@ -168,6 +196,23 @@ def isBinary(filename): except UnicodeDecodeError: return True + +def numberOfLines(filename, method=1): + + if method==1: + return sum(1 for i in open(filename, 'rb')) + + elif method==2: + def blocks(files, size=65536): + while True: + b = files.read(size) + if not b: break + yield b + with open(filename, "r",encoding="utf-8",errors='ignore') as f: + return sum(bl.count("\n") for bl in blocks(f)) + else: + raise NotImplementedError() + def ascii_comp(file1,file2,bDelete=False): """ Compares two ascii files line by line. Comparison is done ignoring multiple white spaces for now""" diff --git a/pyFAST/input_output/file_formats.py b/pyFAST/input_output/file_formats.py new file mode 100644 index 0000000..daa5400 --- /dev/null +++ b/pyFAST/input_output/file_formats.py @@ -0,0 +1,29 @@ +from .file import WrongFormatError + +def isRightFormat(fileformat, filename, **kwargs): + """ Tries to open a file, return true and the file if it succeeds """ + #raise NotImplementedError("Method must be implemented in the subclass") + try: + F=fileformat.constructor(filename=filename, **kwargs) + return True,F + except MemoryError: + raise + except WrongFormatError: + return False,None + except: + raise + +class FileFormat(): + def __init__(self,fileclass=None): + self.constructor = fileclass + if fileclass is None: + self.extensions = [] + self.name = '' + else: + self.extensions = fileclass.defaultExtensions() + self.name = fileclass.formatName() + + + def __repr__(self): + return 'FileFormat object: {} ({})'.format(self.name,self.extensions[0]) + diff --git a/pyFAST/input_output/flex_blade_file.py b/pyFAST/input_output/flex_blade_file.py new file mode 100644 index 0000000..5ce5a0e --- /dev/null +++ b/pyFAST/input_output/flex_blade_file.py @@ -0,0 +1,139 @@ +import numpy as np +import pandas as pd +import os + +try: + from .file import File, WrongFormatError, BrokenFormatError +except: + File = dict + class WrongFormatError(Exception): pass + class BrokenFormatError(Exception): pass + +class FLEXBladeFile(File): + + @staticmethod + def defaultExtensions(): + return ['.bld','.bla','.00X'] #'.001 etc..' + + @staticmethod + def formatName(): + return 'FLEX blade file' + + def _read(self): + headers_all = ['r_[m]','EIFlap_[Nm2]','EIEdge_[Nm2]','GKt_[Nm2]','Mass_[kg/m]','Jxx_[kg.m]','PreBendFlap_[m]','PreBendEdge_[m]'\ + ,'Str.Twist_[deg]','PhiOut_[deg]','Ycog_[m]','Yshc_[m]','CalcOutput_[0/1]'\ + ,'Chord_[m]','AeroTwist_[deg]','RelThickness_[%]','AeroCenter_[m]','AeroTorsion_[0/1]','ProfileSet_[#]'] + with open(self.filename, 'r', errors="surrogateescape") as f: + try: + firstline = f.readline().strip() + nSections = int(f.readline().strip().split()[0]) + except: + raise WrongFormatError('Unable to read first two lines of blade file') + try: + self.version=int(firstline[1:4]) + except: + self.version=0 + # --- Different handling depending on version + if self.version==0: + # Version 0 struct has no GKt + # Version 0 aero has no profile set, no TorsionAero + nColsStruct = 8 + nColsAero = 5 + struct_headers = ['r_[m]','EIFlap_[Nm2]','EIEdge_[Nm2]','Mass_[kg/m]','Str.Twist_[deg]','CalcOutput_[0/1]','PreBendFlap_[m]','PreBendEdge_[m]'] + aero_headers = ['X_BladeRoot_[m]','Chord_[m]','AeroTwist_[deg]','RelThickness_[%]','AeroCenter_[m]'] + elif self.version==1: + # Version 1 struct has GKt + # Version 1 aero has no profile set + nColsStruct = 8 + nColsAero = 6 + struct_headers = ['r_[m]','EIFlap_[Nm2]','EIEdge_[Nm2]','Mass_[kg/m]','Str.Twist_[deg]','CalcOutput_[0/1]','PreBendFlap_[m]','PreBendEdge_[m]'] + aero_headers = ['X_BladeRoot_[m]','Chord_[m]','AeroTwist_[deg]','RelThickness_[%]','AeroCenter_[m]','AeroTorsion_[0/1]'] + elif self.version==2: + nColsStruct = 9 + nColsAero = 7 + struct_headers = ['r_[m]','EIFlap_[Nm2]','EIEdge_[Nm2]','Mass_[kg/m]','Str.Twist_[deg]','CalcOutput_[0/1]','PreBendFlap_[m]','PreBendEdge_[m]','GKt_[Nm2]'] + aero_headers = ['X_BladeRoot_[m]','Chord_[m]','AeroTwist_[deg]','RelThickness_[%]','AeroCenter_[m]','AeroTorsion_[0/1]','ProfileSet_[#]'] + elif self.version==3: + nColsStruct = 13 + nColsAero = 7 + struct_headers = ['r_[m]','EIFlap_[Nm2]','EIEdge_[Nm2]','GKt_[Nm2]','Mass_[kg/m]','Jxx_[kg.m]','PreBendFlap_[m]','PreBendEdge_[m]','Str.Twist_[deg]','PhiOut_[deg]','Ycog_[m]','Yshc_[m]','CalcOutput_[0/1]'] + aero_headers = ['X_BladeRoot_[m]','Chord_[m]','AeroTwist_[deg]','RelThickness_[%]','AeroCenter_[m]','AeroTorsion_[0/1]','ProfileSet_[#]'] + else: + raise BrokenFormatError('Blade format not implemented') + + struct = np.zeros((nSections,nColsStruct)) + aero = np.zeros((nSections,nColsAero)) + + # --- Structural data + try: + for iSec in range(nSections): + vals=f.readline().split() + #if len(vals)>=nColsStruct: + struct[iSec,:]=np.array(vals[0:nColsStruct]).astype(float) + #elif self.version==1: + # # version 1 has either 8 or 9 columns + # nColsStruct=nColsStruct-1 + # struct_headers=struct_headers[0:-1] + # struct =struct[:,:-1] + # struct[iSec,:]=np.array(vals[0:nColsStruct]).astype(float) + except: + raise WrongFormatError('Unable to read structural data') + try: + self.BetaC = float(f.readline().strip().split()[0]) + if self.version==3: + f.readline() + self.FlapDamping = [float(v) for v in f.readline().strip().split(';')[0].split()] + self.EdgeDamping = [float(v) for v in f.readline().strip().split(';')[0].split()] + self.TorsDamping = [float(v) for v in f.readline().strip().split(';')[0].split()] + f.readline() + f.readline() + else: + Damping = [float(v) for v in f.readline().strip().split()[0:4]] + self.FlapDamping = Damping[0:2] + self.EdgeDamping = Damping[2:4] + self.TorsDamping = [] + except: + raise + raise WrongFormatError('Unable to read damping data') + + # --- Aero + try: + for iSec in range(nSections): + vals=f.readline().split()[0:nColsAero] + aero[iSec,:]=np.array(vals).astype(float) + except: + raise WrongFormatError('Unable to read aerodynamic data') + + self.ProfileFile=f.readline().strip() + + # --- Concatenating aero and structural data + self._cols = struct_headers+aero_headers[1:] + data = np.column_stack((struct,aero[:,1:])) + dataMiss=pd.DataFrame(data=data, columns=self._cols) + self._nColsStruct=nColsStruct # to remember where to split + # --- Making sure all columns are present, irrespectively of version + self.data=pd.DataFrame(data=[], columns=headers_all) + for c in self._cols: + self.data[c]=dataMiss[c] + +# def toString(self): +# s='' +# if len(self.ProfileSets)>0: +# prefix='PROFILE SET ' +# else: +# prefix='' +# for pset in self.ProfileSets: +# s+=pset.toString(prefix) +# return s +# +# def _write(self): +# with open(self.filename,'w') as f: +# f.write(self.toString) +# + def __repr__(self): + s ='Class FLEXBladeFile (attributes: data, BetaC, FlapDamping, EdgeDamping, ProfileFile)\n' + return s + + def _toDataFrame(self): + return self.data + diff --git a/pyFAST/input_output/flex_doc_file.py b/pyFAST/input_output/flex_doc_file.py new file mode 100644 index 0000000..46eef7b --- /dev/null +++ b/pyFAST/input_output/flex_doc_file.py @@ -0,0 +1,208 @@ +import numpy as np +import pandas as pd +import os +import re +try: + from .file import File, WrongFormatError, BrokenFormatError +except: + File = dict + class WrongFormatError(Exception): pass + class BrokenFormatError(Exception): pass + +class FLEXDocFile(File): + + @staticmethod + def defaultExtensions(): + return ['.out','doc'] + + @staticmethod + def formatName(): + return 'FLEX WaveKin file' + + def _read(self): + with open(self.filename, 'r', errors="surrogateescape") as f: + line1=f.readline().strip() + if line1.find('#Program')!=0: + raise WrongFormatError() + lines=f.read().splitlines()+[line1] + + ArraysHeader=[ + '#Blade_ShapeFunction_DOF1_Shape', + '#Blade_ShapeFunction_DOF2_Shape', + '#Blade_ShapeFunction_DOF3_Shape', + '#Blade_ShapeFunction_DOF4_Shape', + '#Blade_ShapeFunction_DOF5_Shape', + '#Blade_ShapeFunction_DOF6_Shape', + '#Blade_ShapeFunction_DOF7_Shape', + '#Blade_ShapeFunction_DOF8_Shape', + '#Blade_ShapeFunction_DOF9_Shape', + '#Blade_ShapeFunction_DOF10_Shape', + '#Tower_SectionData', + '#Tower_ShapeFunction_DOF1_Shape', + '#Tower_ShapeFunction_DOF2_Shape', + '#Tower_ShapeFunction_DOF3_Shape', + '#Tower_ShapeFunction_DOF4_Shape', + '#Tower_ShapeFunction_DOF5_Shape', + '#Tower_ShapeFunction_DOF6_Shape', + '#Tower_ShapeFunction_DOF7_Shape', + '#Tower_ShapeFunction_DOF8_Shape', + '#Foundation_SectionData', + '#Foundation_ShapeFunction_DOF1_Shape', + '#Foundation_ShapeFunction_DOF2_Shape', + '#Global_Mode1_Shape', + ] + + ArraysNoHeader=[ + '#Blade_IsolatedMassMatrix' + '#Blade_IsolatedStiffnessMatrix', + '#Blade_IsolatedDampingMatrix', + '#Foundation_IsolatedMassMatrix', + '#Foundation_IsolatedStiffnessMatrix', + '#Foundation_IsolatedStiffnessMatrixCorrection', + '#Foundation_IsolatedDampingMatrix', + '#Tower_MassMatrix', + '#Tower_IsolatedMassMatrix', + '#Tower_IsolatedStiffnessMatrix', + '#Tower_IsolatedStiffnessMatrixCorrection', + '#Tower_IsolatedDampingMatrix', + '#EVA_MassMatrix', + '#EVA_StiffnessMatrix', + '#EVA_DampingMatrix', + '#EVA_Eigenvectors', + '#EVA_Eigenfrequencies', + '#EVA_Eigenvalues', + '#EVA_Damping', + '#EVA_LogDec', + ] + + numeric_const_pattern = r'[-+]? (?: (?: \d* \. \d+ ) | (?: \d+ \.? ) )(?: [Ee] [+-]? \d+ ) ?' + rx = re.compile(numeric_const_pattern, re.VERBOSE) + + i=0 + while i0 and lines[i][0]!='#': + array_lines.append(np.array([float(v) if v not in ['Fnd','Twr','COG'] else ['Fnd','Twr','COG'].index(v) for v in lines[i].split()])) + i=i+1 + # --- Process array + M = np.array(array_lines) + # --- Process header + cols=header.split() + try: + ii=int(cols[0]) + header=' '.join(cols[1:]) + except: + pass + if header.find('[')<=0: + cols=header.split() + else: + header=header.replace('rough','rough_[-]') + header=header.replace('n ','n_[-] ') + spcol=header.split(']') + cols= [v.strip().replace(' ','_').replace('[','_[').replace('__','_').replace('__','_')+']' for v in spcol[:-1]] + + if len(cols)!=M.shape[1]: + cols=['C{}'.format(j) for j in range(M.shape[1])] + # --- Store + keys = sp[0].split('_') + keys[0]=keys[0][1:] + if keys[0] not in self.keys(): + self[keys[0]] = dict() + subkey = '_'.join(keys[1:]) + df = pd.DataFrame(data = M, columns = cols) + self[keys[0]][subkey] = df + continue + # --- Array with no header + elif sp[0] in ArraysNoHeader: + array_lines=[] + i=i+1 + header=lines[i] + i=i+1 + while i0 and lines[i][0]!='#': + array_lines.append(np.array([float(v) for v in lines[i].split()])) + i=i+1 + # --- Process array + M = np.array(array_lines) + # --- Store + keys = sp[0].split('_') + keys[0]=keys[0][1:] + if keys[0] not in self.keys(): + self[keys[0]] = dict() + subkey = '_'.join(keys[1:]) + self[keys[0]][subkey] = M + continue + else: + # --- Regular + keys = sp[0].split('_') + key=keys[0][1:] + subkey = '_'.join(keys[1:]) + values= ' '.join(sp[1:]) + try: + dat= np.array(rx.findall(values)).astype(float) + if len(dat)==1: + dat=dat[0] + except: + dat = values + + if len(key.strip())>0: + if len(subkey)==0: + if key not in self.keys(): + self[key] = dat + else: + print('>>> line',i,l) + print(self.keys()) + raise Exception('Duplicate singleton key:',key) + else: + if key not in self.keys(): + self[key] = dict() + self[key][subkey]=dat + i+=1 + + # --- Some adjustements + try: + df=self['Global']['Mode1_Shape'] + df=df.drop('C0',axis=1) + df.columns=['H_[m]','U_FA_[-]','U_SS_[]'] + self['Global']['Mode1_Shape']=df + except: + pass + +# def _write(self): +# with open(self.filename,'w') as f: +# f.write(self.toString) + + def __repr__(self): + s='<{} object> with keys:\n'.format(type(self).__name__) + for k in self.keys(): + if type(self[k]) is dict: + s+='{:15s}: dict with keys {}\n'.format(k, list(self[k].keys())) + else: + s+='{:15s} : {}\n'.format(k,self[k]) + return s + + def _toDataFrame(self): + dfs={} + for k,v in self.items(): + if type(v) is pd.DataFrame: + dfs[k]=v + #if type(v) is np.ndarray: + # if len(v.shape)>1: + # dfs[k]=v + if type(self[k]) is dict: + for k2,v2 in self[k].items(): + if type(v2) is pd.DataFrame: + dfs[k+'_'+k2]=v2 + return dfs + diff --git a/pyFAST/input_output/flex_out_file.py b/pyFAST/input_output/flex_out_file.py new file mode 100644 index 0000000..ea8eb38 --- /dev/null +++ b/pyFAST/input_output/flex_out_file.py @@ -0,0 +1,205 @@ +import numpy as np +import pandas as pd +import os +try: + from .file import File, WrongFormatError, BrokenFormatError +except: + File = dict + class WrongFormatError(Exception): pass + class BrokenFormatError(Exception): pass + +# --------------------------------------------------------------------------------} +# --- OUT FILE +# --------------------------------------------------------------------------------{ +class FLEXOutFile(File): + + @staticmethod + def defaultExtensions(): + return ['.res','.int'] + + @staticmethod + def formatName(): + return 'FLEX output file' + + def _read(self): + # --- First read the binary file + dtype=np.float32; # Flex internal data is stored in single precision + try: + self.data,self.tmin,self.dt,self.Version,self.DateID,self.title=read_flex_res(self.filename, dtype=dtype) + except WrongFormatError as e: + raise WrongFormatError('FLEX File {}: '.format(self.filename)+'\n'+e.args[0]) + self.nt = np.size(self.data,0) + self.nSensors = np.size(self.data,1) + self.time = np.arange(self.tmin, self.tmin + self.nt * self.dt, self.dt).reshape(self.nt,1).astype(dtype) + + # --- Then the sensor file + parentdir = os.path.dirname(self.filename) + basename = os.path.splitext(os.path.basename(self.filename))[0] + #print(parentdir) + #print(basename) + PossibleFiles=[] + PossibleFiles+=[os.path.join(parentdir, basename+'.Sensor')] + PossibleFiles+=[os.path.join(parentdir, 'Sensor_'+basename)] + PossibleFiles+=[os.path.join(parentdir, 'sensor')] + # We try allow for other files + Found =False + for sf in PossibleFiles: + if os.path.isfile(sf): + self.sensors=read_flex_sensor(sf) + if len(self.sensors['ID'])!=self.nSensors: + Found = False + else: + Found = True + break + if not Found: + # we are being nice and create some fake sensors info + self.sensors=read_flex_sensor_fake(self.nSensors) + + if len(self.sensors['ID'])!=self.nSensors: + raise BrokenFormatError('Inconsistent number of sensors: {} (sensor file) {} (out file), for file: {}'.format(len(self.sensors['ID']),self.nSensors,self.filename)) + + #def _write(self): # TODO + # pass + + def __repr__(self): + return 'Flex Out File: {}\nVersion:{} - DateID:{} - Title:{}\nSize:{}x{} - tmin:{} - dt:{}]\nSensors:{}'.format(self.filename,self.Version,self.DateID,self.title,self.nt,self.nSensors,self.tmin,self.dt,self.sensors['Name']) + + def _toDataFrame(self): + # Appending time to form the dataframe + names = ['Time'] + self.sensors['Name'] + units = ['s'] + self.sensors['Unit'] + units = [u.replace('(','').replace(')','').replace('[','').replace(']','') for u in units] + data = np.concatenate((self.time, self.data), axis=1) + cols=[n+'_['+u+']' for n,u in zip(names,units)] + return pd.DataFrame(data=data,columns=cols) + +# --------------------------------------------------------------------------------} +# --- Helper Functions +# --------------------------------------------------------------------------------{ +def read_flex_res(filename, dtype=np.float32): + # Read flex file + with open(filename,'rb') as fid: + #_ = struct.unpack('i', fid.read(4)) # Dummy + _ = np.fromfile(fid, 'int32', 1) # Dummy + # --- Trying to get DateID + fid.seek(4) # + DateID=np.fromfile(fid, 'int32', 6) + if DateID[0]<32 and DateID[1]<13 and DateID[3]<25 and DateID[4]<61: + # OK, DateID was present + title = fid.read(40).strip() + else: + fid.seek(4) # + DateID = np.fromfile(fid, 'int32', 1) + title = fid.read(60).strip() + _ = np.fromfile(fid, 'int32', 2) # Dummy + # FILE POSITION <<< fid.seek(4 * 19) + nSensors = np.fromfile(fid, 'int32', 1)[0] + IDs = np.fromfile(fid, 'int32', nSensors) + _ = np.fromfile(fid, 'int32', 1) # Dummy + # FILE POSITION <<< fid.seek(4*nSensors+4*21) + Version = np.fromfile(fid, 'int32', 1)[0] + # FILE POSITION <<< fid.seek(4*(nSensors)+4*22) + if Version == 12: + raise NotImplementedError('Flex out file with version 12, TODO. Implement it!') + # TODO + #fseek(o.fid,4*(21+o.nSensors),-1);% seek to the data from beginning of file + #RL=o.nSensors+5; % calculate the length of each row + #A = fread(o.fid,[RL,inf],'single'); % read whole file + #t=A(2,:);% time vector contained in row 2 + #o.SensorData=A(5:end,:); + # save relevant information + #o.tmin = t(1) ; + #o.dt = t(2)-t(1); + #o.t = t ; + #o.nt = length(t); + elif Version in [0,2,3]: + tmin = np.fromfile(fid, 'f', 1)[0] # Dummy + dt = np.fromfile(fid, 'f', 1)[0] # Dummy + scale_factors = np.fromfile(fid, 'f', nSensors).astype(dtype) + # --- Reading Time series + # FILE POSITION <<< fid.seek(8*nSensors + 48*2) + data = np.fromfile(fid, 'int16').astype(dtype) #data = np.fromstring(fid.read(), 'int16').astype(dtype) + nt = int(len(data) / nSensors) + try: + if Version ==3: + data = data.reshape(nSensors, nt).transpose() + else: + data = data.reshape(nt, nSensors) + except ValueError: + raise WrongFormatError("Flat data length {} is not compatible with {}x{} (nt x nSensors)".format(len(data),nt,nSensors)) + for i in range(nSensors): + data[:, i] *= scale_factors[i] + + return (data,tmin,dt,Version,DateID,title) + + +def read_flex_sensor(sensor_file): + with open(sensor_file, encoding="utf-8") as fid: + sensor_info_lines = fid.readlines()[2:] + sensor_info = [] + d=dict({ 'ID':[],'Gain':[],'Offset':[],'Unit':[],'Name':[],'Description':[]}); + for line in sensor_info_lines: + line = line.strip().split() + d['ID'] .append(int(line[0])) + d['Gain'] .append(float(line[1])) + d['Offset'] .append(float(line[2])) + d['Unit'] .append(line[5]) + d['Name'] .append(line[6]) + d['Description'] .append(' '.join(line[7:])) + return d + +def read_flex_sensor_fake(nSensors): + d=dict({ 'ID':[],'Gain':[],'Offset':[],'Unit':[],'Name':[],'Description':[]}); + for i in range(nSensors): + d['ID'] .append(i+1) + d['Gain'] .append(1.0) + d['Offset'] .append(0.0) + d['Unit'] .append('(NA)') + d['Name'] .append('S{:04d}'.format(i+1)) + d['Description'] .append('NA') + return d + + + + + + +# def write_flex_file(filename,data,tmin,dt): +# ds = dataset +# # Write int data file +# f = open(filename, 'wb') +# f.write(struct.pack('ii', 0, 0)) # 2x empty int +# title = ("%-60s" % str(ds.name)).encode() +# f.write(struct.pack('60s', title)) # title +# f.write(struct.pack('ii', 0, 0)) # 2x empty int +# ns = len(sensors) +# f.write(struct.pack('i', ns)) +# f.write(struct.pack('i' * ns, *range(1, ns + 1))) # sensor number +# f.write(struct.pack('ii', 0, 0)) # 2x empty int +# time = ds.basis_attribute() +# f.write(struct.pack('ff', time[0], time[1] - time[0])) # start time and time step +# +# scale_factors = np.max(np.abs(data), 0) / 32000 +# f.write(struct.pack('f' * len(scale_factors), *scale_factors)) +# # avoid dividing by zero +# not0 = np.where(scale_factors != 0) +# data[:, not0] /= scale_factors[not0] +# #flatten and round +# data = np.round(data.flatten()).astype(np.int16) +# f.write(struct.pack('h' * len(data), *data.tolist())) +# f.close() +# +# # write sensor file +# f = open(os.path.join(os.path.dirname(filename), 'sensor'), 'w') +# f.write("Sensor list for %s\n" % filename) +# f.write(" No forst offset korr. c Volt Unit Navn Beskrivelse------------\n") +# sensorlineformat = "%3s %.3f %.3f 1.00 0.00 %7s %-8s %s\n" +# +# if isinstance(ds, FLEX4Dataset): +# gains = np.r_[ds.gains[1:], np.ones(ds.shape[1] - len(ds.gains))] +# offsets = np.r_[ds.offsets[1:], np.zeros(ds.shape[1] - len(ds.offsets))] +# sensorlines = [sensorlineformat % ((nr + 1), gain, offset, att.unit[:7], att.name.replace(" ", "_")[:8], att.description[:512]) for nr, att, gain, offset in zip(range(ns), sensors, gains, offsets)] +# else: +# sensorlines = [sensorlineformat % ((nr + 1), 1, 0, att.unit[:7], att.name.replace(" ", "_")[:8], att.description[:512]) for nr, att in enumerate(sensors)] +# f.writelines(sensorlines) +# f.close() diff --git a/pyFAST/input_output/flex_profile_file.py b/pyFAST/input_output/flex_profile_file.py new file mode 100644 index 0000000..90c4fb8 --- /dev/null +++ b/pyFAST/input_output/flex_profile_file.py @@ -0,0 +1,144 @@ +import numpy as np +import pandas as pd +import os +try: + from .file import File, WrongFormatError, BrokenFormatError +except: + File = dict + class WrongFormatError(Exception): pass + class BrokenFormatError(Exception): pass + +class ProfileSet(): + def __init__(self,header,thickness,polars,polar_headers): + self.header = header + self.polars = polars + self.thickness = thickness + self.polar_headers = polar_headers + + def toString(self,PREFIX=''): + s =PREFIX+self.header+'\n' + s+=' '.join([str(t) for t in self.thickness])+'\n' + s+=str(self.polars[0].shape[0])+'\n' + for ph,t,polar in zip(self.polar_headers,self.thickness,self.polars): + s+=ph+'\n' + s+='\n'.join([' '.join(['{:15.7e}'.format(v) for v in line]) for line in polar]) +# s+=ph+'\n' + return s + + def __repr__(self): + s ='Class ProfileSet (attributes: header, polars, thickness, polar_headers)\n' + s+=' header : '+self.header+'\n' + s+=' thickness : '+str(self.thickness)+'\n' + s+=' Number of polars: '+str(len(self.thickness))+'\n' + s+=' Alpha values : '+str(self.polars[0].shape[0])+'\n' + for ip,(ph,t) in enumerate(zip(self.polar_headers,self.thickness)): + s+= ' Polar: {}, Thickness: {}, Header: {}\n'.format(ip+1,t,ph) + return s + +class FLEXProfileFile(File): + + @staticmethod + def defaultExtensions(): + return ['.pro','.00X'] #'.001 etc..' + + @staticmethod + def formatName(): + return 'FLEX profile file' + + def _read(self): + self.ProfileSets=[] + setNumber=1 + with open(self.filename, 'r', errors="surrogateescape") as f: + def read_header(allow_empty=False): + """ Reads the header of a profile set (4 first lines) + - The first line may start with "Profile set I:" to indicate a set number + - Second line is number of thicknesses + - Third is thicnkesses + - Fourth is number of alpha values + """ + header=[] + for i, line in enumerate(f): + header.append(line.strip()) + if i==3: + break + if len(header)<4: + if allow_empty: + return [],[],'',False + else: + raise WrongFormatError('A Flex profile file needs at leats 4 lines of headers') + try: + nThickness=int(header[1]) + except: + raise WrongFormatError('Number of thicknesses (integer) should be on line 2') + try: + thickness=np.array(header[2].split()).astype(float) + except: + raise WrongFormatError('Number of thicknesses (integer) should be on line 2') + if len(thickness)!=nThickness: + raise WrongFormatError('Number of thicknesses read ({}) different from the number reported ({})'.format(len(thickness),nThickness)) + try: + nAlpha=int(header[3]) + except: + raise WrongFormatError('Number of alpha values (integer) should be on line 4') + if header[0].lower().find('profile set')==0: + header[0]=header[0][11:] + bHasSets=True + else: + bHasSets=False + return nAlpha,thickness,header[0],bHasSets + + def read_polars(nAlpha,thickness): + polars=[] + polar_headers=[] + for it,t in enumerate(thickness): + polar_headers.append(f.readline().strip()) + polars.append(np.zeros((nAlpha,4))) + try: + for ia in range(nAlpha): + polars[it][ia,:]=np.array([f.readline().split()]).astype(float) + except: + raise BrokenFormatError('An error occured while reading set number {}, polar number {}, (thickness {}), value number {}.'.format(setNumber,it+1,t,ia+1)) + + return polars,polar_headers + + # Reading headers and polars + while True: + nAlpha,thickness,Header,bHasSets = read_header(allow_empty=setNumber>1) + if len(thickness)==0: + break + polars,polar_headers = read_polars(nAlpha,thickness) + PSet= ProfileSet(Header,thickness,polars,polar_headers) + self.ProfileSets.append(PSet) + setNumber=setNumber+1 + + def toString(self): + s='' + if len(self.ProfileSets)>0: + prefix='PROFILE SET ' + else: + prefix='' + for pset in self.ProfileSets: + s+=pset.toString(prefix) + return s + + def _write(self): + with open(self.filename,'w') as f: + f.write(self.toString) + + def __repr__(self): + s ='Class FlexProfileFile (attributes: ProfileSets)\n' + s+=' Number of profiles sets: '+str(len(self.ProfileSets))+'\n' + for ps in self.ProfileSets: + s+=ps.__repr__() + return s + + + def _toDataFrame(self): + cols=['Alpha_[deg]','Cl_[-]','Cd_[-]','Cm_[-]'] + dfs = {} + for iset,pset in enumerate(self.ProfileSets): + for ipol,(thickness,polar) in enumerate(zip(pset.thickness,pset.polars)): + name='pc_set_{}_t_{}'.format(iset+1,thickness) + dfs[name] = pd.DataFrame(data=polar, columns=cols) + return dfs + diff --git a/pyFAST/input_output/flex_wavekin_file.py b/pyFAST/input_output/flex_wavekin_file.py new file mode 100644 index 0000000..d6cdf72 --- /dev/null +++ b/pyFAST/input_output/flex_wavekin_file.py @@ -0,0 +1,101 @@ +import numpy as np +import pandas as pd +import os +import re +try: + from .file import File, WrongFormatError, BrokenFormatError +except: + File = dict + class WrongFormatError(Exception): pass + class BrokenFormatError(Exception): pass +from .csv_file import CSVFile + + +class FLEXWaveKinFile(File): + + @staticmethod + def defaultExtensions(): + return ['.wko'] #'.001 etc..' + + @staticmethod + def formatName(): + return 'FLEX WaveKin file' + + def _read(self): + numeric_const_pattern = r'[-+]? (?: (?: \d* \. \d+ ) | (?: \d+ \.? ) )(?: [Ee] [+-]? \d+ ) ?' + rx = re.compile(numeric_const_pattern, re.VERBOSE) + def extract_floats(s): + v=np.array(rx.findall(s)) + return v + + + try: + csv = CSVFile(self.filename, sep=' ', commentLines=list(np.arange(11)),detectColumnNames=False) + except: + raise WrongFormatError('Unable to parse Flex WaveKin file as CSV with 11 header lines') + + header = csv.header + self['header'] = csv.header[0:2] + self['data'] = csv.data + try: + self['MaxCrestHeight'] = float(extract_floats(header[2])[0]) + self['MaxLongiVel'] = float(extract_floats(header[3])[0]) + self['MaxLongiAcc'] = float(extract_floats(header[4])[0]) + dat = extract_floats(header[5]).astype(float) + self['WaterDepth'] = dat[0] + self['Hs'] = dat[1] + self['Tp'] = dat[2] + self['SpecType'] = dat[3] + except: + raise BrokenFormatError('Unable to parse floats from header lines 3-6') + + try: + nDisp = int(extract_floats(header[6])[0]) + nRelD = int(extract_floats(header[8])[0]) + except: + raise BrokenFormatError('Unable to parse int from header lines 7 and 9') + + try: + displ = extract_floats(header[7]).astype(float) + depth = extract_floats(header[9]).astype(float) + except: + raise BrokenFormatError('Unable to parse displacements or depths from header lines 8 and 10') + if len(displ)!=nDisp: + print(displ) + raise BrokenFormatError('Number of displacements ({}) does not match number provided ({})'.format(nDisp, len(displ))) + if len(depth)!=nRelD: + print(depth) + raise BrokenFormatError('Number of rel depth ({}) does not match number provided ({})'.format(nRelD, len(depth))) + + self['RelDepth'] = depth + self['Displacements'] = displ + + cols=['Time_[s]', 'WaveElev_[m]'] + for j,x in enumerate(displ): + for i,z in enumerate(depth): + cols+=['u_z={:.1f}_x={:.1f}_[m/s]'.format(z*self['WaterDepth']*-1,x)] + for i,z in enumerate(depth): + cols+=['a_z={:.1f}_x={:.1f}_[m/s^2]'.format(z*self['WaterDepth'],x)] + + if len(cols)!=len(self['data'].columns): + raise BrokenFormatError('Number of columns not valid') + self['data'].columns = cols + +# def _write(self): +# with open(self.filename,'w') as f: +# f.write(self.toString) + + def __repr__(self): + s='<{} object> with keys:\n'.format(type(self).__name__) + + for k in ['MaxCrestHeight','MaxLongiVel','MaxLongiAcc','WaterDepth','Hs','Tp','SpecType','RelDepth','Displacements']: + s += '{:15s}: {}\n'.format(k,self[k]) + if len(self['header'])>0: + s += 'header : '+ ' ,'.join(self['header'])+'\n' + if len(self['data'])>0: + s += 'data size : {}x{}'.format(self['data'].shape[0],self['data'].shape[1]) + return s + + def _toDataFrame(self): + return self['data'] + diff --git a/pyFAST/input_output/mannbox_input_file.py b/pyFAST/input_output/mannbox_input_file.py new file mode 100644 index 0000000..13f1cc2 --- /dev/null +++ b/pyFAST/input_output/mannbox_input_file.py @@ -0,0 +1,191 @@ +""" +Read/Write Mann Box input file + +""" +import os +import numpy as np +try: + from .file import File, EmptyFileError, BrokenFormatError +except: + EmptyFileError = type('EmptyFileError', (Exception,),{}) + BrokenFormatError = type('BrokenFormatError', (Exception,),{}) + File=dict + +class MannBoxInputFile(File): + def __init__(self, filename=None, **kwargs): + self.filename = None + # Default Init + self['fieldDim'] = None + self['nComp'] = None + self['nx'] = None + self['ny'] = None + self['nz'] = None + self['Lx'] = None + self['Ly'] = None + self['Lz'] = None + self['type'] = None + self['U'] = None + self['z'] = None + self['zNone'] = None + self['spectrum'] = None + # Basic + self['alpha_eps'] = None + self['L'] = None + self['Gamma'] = None + self['seed'] = -1 + self['file_u'] = None + self['file_v'] = None + self['file_w'] = None + for k,v in kwargs.items(): + print('>>> Setting',k,v) + + if filename: + self.read(filename=filename) + + def read(self, filename): + if filename: + self.filename = filename + if not self.filename: + raise Exception('No filename provided') + if not os.path.isfile(self.filename): + raise OSError(2,'File not found:',self.filename) + if os.stat(self.filename).st_size == 0: + raise EmptyFileError('File is empty:',self.filename) + + with open(self.filename) as f: + print('IO: Reading Mann input file: '+self.filename) + self['fieldDim'] = int(f.readline()) + self['nComp'] = int(f.readline()) + if(self['nComp']==3): + self['nx'] = int(f.readline()) + self['ny'] = int(f.readline()) + self['nz'] = int(f.readline()) + self['Lx'] = float(f.readline()) + self['Ly'] = float(f.readline()) + self['Lz'] = float(f.readline()) + else: + raise Exception('nComp=2') + self['type']=f.readline().strip() + if(self['type']=='basic'): + self['alpha_eps'] = float(f.readline()) + self['L'] = float(f.readline()) + self['Gamma'] = float(f.readline()) + else: + raise Exception('not basic') + self['seed']=int(f.readline()) + if(self['nComp']>=1): + self['file_u']=f.readline().strip() + if(self['nComp']>=2): + self['file_v']=f.readline().strip() + if(self['nComp']>=3): + self['file_w']=f.readline().strip() + + def write(self, filename): + """ Write mann box """ + if filename: + self.filename = filename + + self.defaultFileNames(self.filename) + + with open(self.filename, 'w') as f: + f.write(str(self['fieldDim'])+'\n') + f.write(str(self['nComp'])+'\n') + if(self['nComp']==3): + f.write(str(self['nx'])+'\n') + f.write(str(self['ny'])+'\n') + f.write(str(self['nz'])+'\n') + f.write(str(self['Lx'])+'\n') + f.write(str(self['Ly'])+'\n') + f.write(str(self['Lz'])+'\n') + else: + raise Exception('nComp=2') + + f.write(self['type']+'\n') + if(self['type']=='basic'): + f.write(str(self['alpha_eps'])+'\n'); + f.write(str(self['L'])+'\n'); + f.write(str(self['Gamma'])+'\n'); + else: + raise Exception('not basic') + + f.write(str(self['seed'])+'\n'); + if(self['nComp']>=1): + f.write(self['file_u']+'\n'); + if(self['nComp']>=2): + f.write(self['file_v']+'\n'); + if(self['nComp']>=3): + f.write(self['file_w']+'\n'); + + def defaultFileNames(self, inpfile): + base = os.path.splitext(os.path.basename(inpfile))[0] + nx,ny,nz = self['nx'], self['ny'], self['nz'] + self['file_u'] = base+'_{}x{}x{}.u'.format(nx,ny,nz) + self['file_v'] = base+'_{}x{}x{}.v'.format(nx,ny,nz) + self['file_w'] = base+'_{}x{}x{}.w'.format(nx,ny,nz) + +# def set3Disotropic(self,nx,ny,nz,Lx,Ly,Lz,alpha_eps,L): +# self.fieldDim=3; +# self.NComp=3; +# self.nx=nx; +# self.ny=ny; +# self.nz=nz; +# self.Lx=Lx; +# self.Ly=Ly; +# self.Lz=Lz; +# self.alpha_eps=alpha_eps; +# self.L=L; +# # Isotropic +# self.Gamma=0.0; +# self.type='basic'; +# +# def auto_file_names(self,folder=''): +# if folder is None: +# folder='' +# import os +# if(self.type=='basic' and self.Gamma==0): +# if self.fieldDim==3: +# filenameBase='Isotropic3D_'+str(self.nx)+'_'+str(self.ny)+'_'+str(self.nz)+'_h'+str(int(100*float(self.Lx)/float(self.nx-1))/100.) +# self.filename = os.path.join(folder,filenameBase+'.maninp') +# self.file_u = os.path.join(folder,filenameBase+'_u.dat') +# self.file_v = os.path.join(folder,filenameBase+'_v.dat') +# self.file_w = os.path.join(folder,filenameBase+'_w.dat') +# + + def __repr__(self): + s = '<{} object> with keys:\n'.format(type(self).__name__) + for k,v in self.items(): + s += '{:15s}: {}\n'.format(k,v) + return s + + + +# +# def get_outfile_u(self): +# return self.file_u +# def get_outfile_v(self): +# return self.file_v +# def get_outfile_w(self): +# return self.file_w +# +# if __name__ == "__main__": +# import sys +# if len(sys.argv)>1: +# print 'called with: ', int(sys.argv[1]) +# +# nx=1024; +# ny=256; +# nz=256; +# Lx=100; +# Ly=10; +# Lz=10; +# alpha_eps=0.01 +# L=10; +# +# inp=MannInputFile(); +# inp.set3Disotropic(nx,ny,nz,Lx,Ly,Lz,alpha_eps,L) +# inp.auto_file_names() +# inp.write(); +# # inp2=MannInputFile(inp.filename); +# # inp2.filename='out10.inp' +# # inp2.write() +# diff --git a/pyFAST/input_output/matlabmat_file.py b/pyFAST/input_output/matlabmat_file.py new file mode 100644 index 0000000..e1aff75 --- /dev/null +++ b/pyFAST/input_output/matlabmat_file.py @@ -0,0 +1,112 @@ +""" +Input/output class for the matlab .mat fileformat +""" +import numpy as np +import pandas as pd +import os +import scipy.io + +try: + from .file import File, WrongFormatError, BrokenFormatError +except: + File=dict + EmptyFileError = type('EmptyFileError', (Exception,),{}) + WrongFormatError = type('WrongFormatError', (Exception,),{}) + BrokenFormatError = type('BrokenFormatError', (Exception,),{}) + +class MatlabMatFile(File): + """ + Read/write a mat file. The object behaves as a dictionary. + + Main methods + ------------ + - read, write, toDataFrame, keys + + Examples + -------- + f = MatlabMatFile('file.mat') + print(f.keys()) + print(f.toDataFrame().columns) + + """ + + @staticmethod + def defaultExtensions(): + """ List of file extensions expected for this fileformat""" + return ['.mat'] + + @staticmethod + def formatName(): + """ Short string (~100 char) identifying the file format""" + return 'Matlab mat file' + + @staticmethod + def priority(): return 60 # Priority in weio.read fileformat list between 0=high and 100:low + + def __init__(self, filename=None, **kwargs): + """ Class constructor. If a `filename` is given, the file is read. """ + self.filename = filename + if filename: + self.read(**kwargs) + + def read(self, filename=None, **kwargs): + """ Reads the file self.filename, or `filename` if provided """ + + # --- Standard tests and exceptions (generic code) + if filename: + self.filename = filename + if not self.filename: + raise Exception('No filename provided') + if not os.path.isfile(self.filename): + raise OSError(2,'File not found:',self.filename) + if os.stat(self.filename).st_size == 0: + raise EmptyFileError('File is empty:',self.filename) + + mfile = scipy.io.loadmat(self.filename) + import pdb; pdb.set_trace() + + def write(self, filename=None): + """ Rewrite object to file, or write object to `filename` if provided """ + if filename: + self.filename = filename + if not self.filename: + raise Exception('No filename provided') + #with open(self.filename,'w') as f: + # f.write(self.toString) + raise NotImplementedError() + + def toDataFrame(self): + """ Returns object into one DataFrame, or a dictionary of DataFrames""" + # --- Example (returning one DataFrame): + # return pd.DataFrame(data=np.zeros((10,2)),columns=['Col1','Col2']) + # --- Example (returning dict of DataFrames): + #dfs={} + #cols=['Alpha_[deg]','Cl_[-]','Cd_[-]','Cm_[-]'] + #dfs['Polar1'] = pd.DataFrame(data=..., columns=cols) + #dfs['Polar1'] = pd.DataFrame(data=..., columns=cols) + # return dfs + raise NotImplementedError() + + # --- Optional functions + def __repr__(self): + """ String that is written to screen when the user calls `print()` on the object. + Provide short and relevant information to save time for the user. + """ + s='<{} object>:\n'.format(type(self).__name__) + s+='|Main attributes:\n' + s+='| - filename: {}\n'.format(self.filename) + # --- Example printing some relevant information for user + #s+='|Main keys:\n' + #s+='| - ID: {}\n'.format(self['ID']) + #s+='| - data : shape {}\n'.format(self['data'].shape) + s+='|Main methods:\n' + s+='| - read, write, toDataFrame, keys' + return s + + def toString(self): + """ """ + s='' + return s + + + diff --git a/pyFAST/input_output/netcdf_file.py b/pyFAST/input_output/netcdf_file.py new file mode 100644 index 0000000..7d69b45 --- /dev/null +++ b/pyFAST/input_output/netcdf_file.py @@ -0,0 +1,40 @@ +import pandas as pd + +try: + from .file import File, WrongFormatError, BrokenFormatError +except: + File = dict + class WrongFormatError(Exception): pass + class BrokenFormatError(Exception): pass + +class NetCDFFile(File): + + @staticmethod + def defaultExtensions(): + return ['.nc'] + + @staticmethod + def formatName(): + return 'NetCDF file (<=2D)' + + def _read(self): + try: + import xarray as xr + except: + raise Exception('Python module `xarray` not installed') + + self.data=xr.open_dataset(self.filename) + + def _write(self): + self.data.to_netcdf(self.filename) + + def _toDataFrame(self): + dfs={} + for k in self.data.keys(): + # Not pretty... + if len(self.data[k].shape)==2: + dfs[k]=pd.DataFrame(data=self.data[k].values) + elif len(self.data[k].shape)==1: + dfs[k]=pd.DataFrame(data=self.data[k].values) + return dfs + diff --git a/pyFAST/input_output/parquet_file.py b/pyFAST/input_output/parquet_file.py new file mode 100644 index 0000000..7cf8dd1 --- /dev/null +++ b/pyFAST/input_output/parquet_file.py @@ -0,0 +1,48 @@ +import pandas as pd + +from .file import File + + +class ParquetFile(File): + + @staticmethod + def defaultExtensions(): + return ['.parquet'] + + @staticmethod + def formatName(): + return 'Parquet file' + + def __init__(self,filename=None,**kwargs): + self.filename = filename + if filename: + self.read(**kwargs) + + + def _read(self): + """ use pandas read_parquet function to read parquet file""" + self.data=pd.read_parquet(self.filename) + + def _write(self): + """ use pandas DataFrame.to_parquet method to write parquet file """ + self.data.to_parquet(path=self.filename) + + def toDataFrame(self): + #already stored as a data frame in self.data + #just return self.data + return self.data + + def fromDataFrame(self, df): + #data already in dataframe + self.data = df + + def toString(self): + """ use pandas DataFrame.to_string method to convert to a string """ + s=self.data.to_string() + return s + + def __repr__(self): + s ='Class Parquet (attributes: data)\n' + return s + + diff --git a/pyFAST/input_output/pickle_file.py b/pyFAST/input_output/pickle_file.py new file mode 100644 index 0000000..65cbf86 --- /dev/null +++ b/pyFAST/input_output/pickle_file.py @@ -0,0 +1,168 @@ +""" +Input/output class for the pickle fileformats +""" +import numpy as np +import pandas as pd +import os +import pickle +import builtins + +try: + from .file import File, WrongFormatError, BrokenFormatError +except: + File=dict + EmptyFileError = type('EmptyFileError', (Exception,),{}) + WrongFormatError = type('WrongFormatError', (Exception,),{}) + BrokenFormatError = type('BrokenFormatError', (Exception,),{}) + +class PickleFile(File): + """ + Read/write a pickle file. The object behaves as a dictionary. + + Main methods + ------------ + - read, write, toDataFrame, keys + + Examples + -------- + f = PickleFile('file.pkl') + print(f.keys()) + print(f.toDataFrame().columns) + + """ + + @staticmethod + def defaultExtensions(): + """ List of file extensions expected for this fileformat""" + return ['.pkl'] + + @staticmethod + def formatName(): + """ Short string (~100 char) identifying the file format""" + return 'Pickle file' + + @staticmethod + def priority(): return 60 # Priority in weio.read fileformat list between 0=high and 100:low + + + def __init__(self, filename=None, data=None, **kwargs): + """ Class constructor. If a `filename` is given, the file is read. """ + self.filename = filename + if filename and not data: + self.read(**kwargs) + if data: + self._setData(data) + if filename: + self.write() + + def _setData(self, data): + if isinstance(data, dict): + for k,v in data.items(): + self[k] = v + else: + if hasattr(data, '__dict__'): + self.update(data.__dict__) + else: + self['data'] = data + + def read(self, filename=None, **kwargs): + """ Reads the file self.filename, or `filename` if provided """ + # --- Standard tests and exceptions (generic code) + if filename: + self.filename = filename + if not self.filename: + raise Exception('No filename provided') + if not os.path.isfile(self.filename): + raise OSError(2,'File not found:',self.filename) + if os.stat(self.filename).st_size == 0: + raise EmptyFileError('File is empty:',self.filename) + # Reads self.filename and stores data into self. Self is (or behaves like) a dictionary + # If pickle data is a dict we store its keys in self, otherwise with store the pickle in the "data" key + d = pickle.load(open(self.filename, 'rb')) + self._setData(d) + + def write(self, filename=None): + """ Rewrite object to file, or write object to `filename` if provided """ + if filename: + self.filename = filename + if not self.filename: + raise Exception('No filename provided') + with open(self.filename, 'wb') as fid: + pickle.dump(dict(self), fid) + + def toDataFrame(self): + """ Returns object into one DataFrame, or a dictionary of DataFrames""" + dfs={} + for k,v in self.items(): + if isinstance(v, pd.DataFrame): + dfs[k] = v + elif isinstance(v, np.ndarray): + if len(v.shape)==2: + dfs[k] = pd.DataFrame(data=v, columns=['C{}'.format(i) for i in range(v.shape[1])]) + elif len(v.shape)==1: + dfs[k] = pd.DataFrame(data=v, columns=[k]) + if len(dfs)==1: + dfs=dfs[list(dfs.keys())[0]] + return dfs + + # --- Optional functions + def __repr__(self): + """ String that is written to screen when the user calls `print()` on the object. + Provide short and relevant information to save time for the user. + """ + s='<{} object>:\n'.format(type(self).__name__) + s+='|Main attributes:\n' + s+='| - filename: {}\n'.format(self.filename) + # --- Example printing some relevant information for user + s+='|Main keys:\n' + for k,v in self.items(): + try: + s+='| - {}: type:{} shape:{}\n'.format(k,type(v),v.shape) + except: + try: + s+='| - {}: type:{} len:{}\n'.format(k,type(v), len(v)) + except: + s+='| - {}: type:{}\n'.format(k,type(v)) + s+='|Main methods:\n' + s+='| - read, write, toDataFrame, keys' + return s + + + # --- Functions speficic to filetype + def toGlobal(self, namespace=None, overwrite=True, verbose=False, force=False) : + #def toGlobal(self, **kwargs): + """ + NOTE: very dangerous, mostly works for global, but then might infect everything + + Inject variables (keys of read dict) into namespace (e.g. globals()). + By default, the namespace of the caller is used + To use the global namespace, use namespace=globals() + """ + import inspect + st = inspect.stack() + if len(st)>2: + if not force: + raise Exception('toGlobal is very dangerous, only use in isolated script. use `force=True` if you really know what you are doing') + else: + print('[WARN] toGlobal is very dangerous, only use in isolated script') + if namespace is None: + # Using parent local namespace + namespace = inspect.currentframe().f_back.f_globals + #namespace = inspect.currentframe().f_back.f_locals # could use f_globals + # Using global (difficult, overwriting won't work) It's best if the user sets namespace=globals() + # import builtins as _builtins + # namespace = _builtins # NOTE: globals() is for the package globals only, we need "builtins" + + gl_keys = list(namespace.keys()) + for k,v in self.items(): + if k in gl_keys: + if not overwrite: + print('[INFO] not overwritting variable {}, already present in global namespace'.format(k)) + continue + else: + print('[WARN] overwritting variable {}, already present in global namespace'.format(k)) + + if verbose: + print('[INFO] inserting in namespace: {}'.format(k)) + namespace[k] = v # OR do: builtins.__setattr__(k,v) + diff --git a/pyFAST/input_output/rosco_discon_file.py b/pyFAST/input_output/rosco_discon_file.py new file mode 100644 index 0000000..b4de969 --- /dev/null +++ b/pyFAST/input_output/rosco_discon_file.py @@ -0,0 +1,265 @@ +""" +Input/output class for the fileformat ROSCO DISCON file +""" +import numpy as np +import pandas as pd +import os +from collections import OrderedDict + +try: + from .file import File, WrongFormatError, BrokenFormatError +except: + File=OrderedDict + EmptyFileError = type('EmptyFileError', (Exception,),{}) + WrongFormatError = type('WrongFormatError', (Exception,),{}) + BrokenFormatError = type('BrokenFormatError', (Exception,),{}) + +class ROSCODISCONFile(File): + """ + Read/write a ROSCO DISCON file. The object behaves as a dictionary. + + Main methods + ------------ + - read, write, toDataFrame, keys + + Examples + -------- + f = ROSCODISCONFile('DISCON.IN') + print(f.keys()) + print(f.toDataFrame().columns) + + """ + + @staticmethod + def defaultExtensions(): + """ List of file extensions expected for this fileformat""" + return ['.in'] + + @staticmethod + def formatName(): + """ Short string (~100 char) identifying the file format""" + return 'ROSCO DISCON file' + + @staticmethod + def priority(): return 60 # Priority in weio.read fileformat list between 0=high and 100:low + + + def __init__(self, filename=None, **kwargs): + """ Class constructor. If a `filename` is given, the file is read. """ + self.filename = filename + if filename: + self.read(**kwargs) + + def read(self, filename=None, **kwargs): + """ Reads the file self.filename, or `filename` if provided """ + + # --- Standard tests and exceptions (generic code) + if filename: + self.filename = filename + if not self.filename: + raise Exception('No filename provided') + if not os.path.isfile(self.filename): + raise OSError(2,'File not found:',self.filename) + if os.stat(self.filename).st_size == 0: + raise EmptyFileError('File is empty:',self.filename) + # --- Calling (children) function to read + _, comments, lineKeys = read_DISCON(self.filename, self) + self.comments=comments + self.lineKeys=lineKeys + + def write(self, filename=None): + """ Rewrite object to file, or write object to `filename` if provided """ + if filename: + self.filename = filename + if not self.filename: + raise Exception('No filename provided') + with open(self.filename, 'w') as f: + f.write(self.toString()) + + + def toDataFrame(self): + """ Returns object into one DataFrame, or a dictionary of DataFrames""" + dfs={} + low_keys = [s.lower() for s in self.keys()] + if 'pc_gs_n' in low_keys: + M = np.column_stack([self['PC_GS_angles']*180/np.pi, self['PC_GS_KP'], self['PC_GS_KI'], self['PC_GS_KD'], self['PC_GS_TF']] ) + cols = ['Pitch_[deg]', 'KP_[-]', 'KI_[s]', 'KD_[1/s]', 'TF_[-]'] + dfs['PitchSchedule'] = pd.DataFrame(data=M, columns=cols) + if 'ps_bldpitchmin_n' in low_keys: + M = np.column_stack([self['PS_WindSpeeds'], self['PS_BldPitchMin']]) + cols = ['WindSpeed_[m/s]', 'Pitch_[deg]'] + dfs['PitchSaturation'] = pd.DataFrame(data=M, columns=cols) + if 'prc_n' in low_keys: + M = np.column_stack([self['PRC_WindSpeeds'], self['PRC_RotorSpeeds']*30/np.pi]) + cols = ['WindSpeed_[m/s]', 'RotorSpeed_[rpm]'] + dfs['PowerTracking'] = pd.DataFrame(data=M, columns=cols) + + return dfs + + # --- Optional functions + def __repr__(self): + """ String that is written to screen when the user calls `print()` on the object. + Provide short and relevant information to save time for the user. + """ + s='<{} object>:\n'.format(type(self).__name__) + s+='|Main attributes:\n' + s+='| - filename: {}\n'.format(self.filename) + # --- Example printing some relevant information for user + #s+='|Main keys:\n' + #s+='| - ID: {}\n'.format(self['ID']) + #s+='| - data : shape {}\n'.format(self['data'].shape) + s+='|Main methods:\n' + s+='| - read, write, toDataFrame, keys' + return s + + def toString(self): + """ """ + maxKeyLengh = np.max([len(k) for k in self.keys()]) + maxKeyLengh = max(maxKeyLengh, 18) + fmtKey = '{:' +str(maxKeyLengh)+'s}' + s='' + for l in self.lineKeys: + if len(l)==0: + s+='\n' + elif l.startswith('!'): + s+=l+'\n' + else: + param = l + comment = self.comments[param] + v = self[param] + sparam = '! '+fmtKey.format(param) + # NOTE: could to "param" specific outputs here + FMTs = {} + FMTs['{:<4.6f}']=['F_NotchBetaNumDen', 'F_FlCornerFreq', 'F_FlpCornerFreq', 'PC_GS_angles', 'PC_GS_KP', 'PC_GS_KI', 'PC_GS_KD', 'PC_GS_TF', 'IPC_Vramp', 'IPC_aziOffset'] + FMTs['{:<4.3e}']=['IPC_KP','IPC_KI'] + FMTs['{:<4.3f}']=['PRC_WindSpeeds', 'PRC_RotorSpeed','PS_WindSpeeds'] + FMTs['{:<4.4f}']=['WE_FOPoles_v'] + FMTs['{:<10.8f}']=['WE_FOPoles'] + FMTs['{:<10.3f}']=['PS_BldPitchMin'] + fmtFloat='{:<014.5f}' + for fmt,keys in FMTs.items(): + if param in keys: + fmtFloat=fmt + break + if type(v) is str: + sval='"{:15s}" '.format(v) + elif hasattr(v, '__len__'): + if isinstance(v[0], (np.floating, float)): + sval=' '.join([fmtFloat.format(vi) for vi in v] )+' ' + else: + sval=' '.join(['{}'.format(vi) for vi in v] )+' ' + elif type(v) is int: + sval='{:<14d} '.format(v) + elif isinstance(v, (np.floating, float)): + sval=fmtFloat.format(v) + ' ' + else: + sval='{} '.format(v) + s+='{}{}{}\n'.format(sval, sparam, comment) + return s + + + + + + +# Some useful constants +pi = np.pi +rad2deg = np.rad2deg(1) +deg2rad = np.deg2rad(1) +rpm2RadSec = 2.0*(np.pi)/60.0 +RadSec2rpm = 60/(2.0 * np.pi) + +def write_DISCON(turbine, controller, param_file='DISCON.IN', txt_filename='Cp_Ct_Cq.txt', rosco_vt = {}): + """ + Print the controller parameters to the DISCON.IN input file for the generic controller + + Parameters: + ----------- + turbine: class + Turbine class containing turbine operation information (ref speeds, etc...) + controller: class + Controller class containing controller operation information (gains, etc...) + param_file: str, optional + filename for parameter input file, should be DISCON.IN + txt_filename: str, optional + filename of rotor performance file + """ + + # Get ROSCO var tree if not provided + if not rosco_vt: + rosco_vt = DISCON_dict(turbine, controller, txt_filename) + + print('Writing new controller parameter file parameter file: %s.' % param_file) + # Should be obvious what's going on here... + file = open(param_file,'w') + + # Write Open loop input + if rosco_vt['OL_Mode'] and hasattr(controller, 'OpenLoop'): + write_ol_control(controller) + +def read_DISCON(DISCON_filename, DISCON_in = None): + ''' + Read the DISCON input file. + Adapted from ROSCO_Toolbox, https:github.com/NREL/ROSCO + + Parameters: + ---------- + DISCON_filename: string + Name of DISCON input file to read + + Returns: + -------- + DISCON_in: Dict + Dictionary containing input parameters from DISCON_in, organized by parameter name + ''' + + if DISCON_in is None: + DISCON_in = OrderedDict() + comments={} + lineKeys=[] + with open(DISCON_filename) as discon: + for line in discon: + line=line.strip() + # empty lines + if len(line)==0: + lineKeys.append('') + continue + # Pure comments + if line[0] == '!': + lineKeys.append(line) + continue + + if (line.split()[1] != '!'): # Array valued entries + sps = line.split() + array_length = sps.index('!') + param = sps[array_length+1] + values = np.array( [float(x) for x in sps[:array_length]] ) + else: # All other entries + param = line.split()[2] + value = line.split()[0] + # Remove printed quotations if string is in quotes + if (value[0] == '"') or (value[0] == "'"): + values = value[1:-1] + else: + if value.find('.')>0: + values = float(value) + else: + values = int(value) + DISCON_in[param] = values + lineKeys.append(param) + + sp = line.split('!') + comment = sp[1].strip() + comment = comment[len(param):].strip() + comments [param] = comment + + return DISCON_in, comments, lineKeys + + +if __name__ == '__main__': + filename = 'DISCON.in' + rd = ROSCODISCONFile(filename) + #print(rd.keys()) +# print(rd.toString()) + rd.write(filename+'_WEIO') + print(rd.toDataFrame()) diff --git a/pyFAST/input_output/tdms_file.py b/pyFAST/input_output/tdms_file.py new file mode 100644 index 0000000..13f3e9f --- /dev/null +++ b/pyFAST/input_output/tdms_file.py @@ -0,0 +1,225 @@ +import numpy as np +import pandas as pd +import os + +try: + from .file import File, WrongFormatError, BrokenFormatError, OptionalImportError +except: + File = dict + class WrongFormatError(Exception): pass + class BrokenFormatError(Exception): pass + class OptionalImportError(Exception): pass + +class TDMSFile(File): + + @staticmethod + def defaultExtensions(): + return ['.tdms'] + + @staticmethod + def formatName(): + return 'TDMS file' + + def __init__(self, filename=None, **kwargs): + """ Class constructor. If a `filename` is given, the file is read. """ + self.filename = filename + if filename: + self.read(**kwargs) + + def read(self, filename=None, **kwargs): + """ Reads the file self.filename, or `filename` if provided """ + + # --- Standard tests and exceptions (generic code) + if filename: + self.filename = filename + if not self.filename: + raise Exception('No filename provided') + if not os.path.isfile(self.filename): + raise OSError(2,'File not found:',self.filename) + if os.stat(self.filename).st_size == 0: + raise EmptyFileError('File is empty:',self.filename) + try: + from nptdms import TdmsFile + except: + raise OptionalImportError('Install the library nptdms to read this file') + + fh = TdmsFile(self.filename, read_metadata_only=False) + # --- OLD, using some kind of old version of tdms and probably specific to one file + # channels_address = list(fh.objects.keys()) + # channels_address = [ s.replace("'",'') for s in channels_address] + # channel_keys= [ s.split('/')[1:] for s in channels_address if len(s.split('/'))==3] + # # --- Setting up list of signals and times + # signals=[] + # times=[] + # for i,ck in enumerate(channel_keys): + # channel = fh.object(ck[0],ck[1]) + # signals.append(channel.data) + # times.append (channel.time_track()) + + # lenTimes = [len(time) for time in times] + # minTimes = [np.min(time) for time in times] + # maxTimes = [np.max(time) for time in times] + # if len(np.unique(lenTimes))>1: + # print(lenTimes) + # raise NotImplementedError('Different time length') + # # NOTE: could use fh.as_dataframe + # if len(np.unique(minTimes))>1: + # print(minTimes) + # raise NotImplementedError('Different time span') + # if len(np.unique(maxTimes))>1: + # print(maxTimes) + # raise NotImplementedError('Different time span') + # # --- Gathering into a data frame with time + # time =times[0] + # signals = [time]+signals + # M = np.column_stack(signals) + # colnames = ['Time_[s]'] + [ck[1] for ck in channel_keys] + # self['data'] = pd.DataFrame(data = M, columns=colnames) + # --- NEW + self['data'] = fh + + #for group in fh.groups(): + # for channel in group.channels(): + # #channel = group['channel name'] + # print('Group:',group.name , 'Chan:',channel.name) + # channel_data = channel[:] + # if len(channel_data)>0: + # print(' ', type(channel_data)) + # #print(' ', len(channel_data)) + # print(' ', channel_data) + # print(' ', channel_data[0]) + # try: + # print(channel.time_track()) + # except KeyError: + # print('>>> No time track') + + def write(self, filename=None, df=None): + """" + Write to TDMS file. + NOTE: for now only using a conversion from dataframe... + """ + if filename is None: + filename = self.filename + if df is None: + df = self.toDataFrame(split=False) + writeTDMSFromDataFrame(filename, df) + + + def groups(self): + return self['data'].groups() + + @property + def groupNames(self): + return [group.name for group in self['data'].groups()] + + def __repr__(self): + s ='Class TDMS (key: data)\n' + s +=' - data: TdmsFile\n' + s +=' * groupNames: {}\n'.format(self.groupNames) + #for group in fh.groups(): + # for channel in group.channels(): + # print(group.name) + # print(channel.name) + return s + + def toDataFrame(self, split=True): + """ Export to one (split=False) or several dataframes (split=True) + Splitting on the group + """ + + def cleanColumns(df): + # Cleanup columns + colnames = df.columns + colnames=[c.replace('\'','') for c in colnames] + colnames=[c[1:] if c.startswith('/') else c for c in colnames] + # If there is only one group, we remove the group key + groupNames = self.groupNames + if len(groupNames)==1: + nChar = len(groupNames[0]) + colnames=[c[nChar+1:] for c in colnames] # +1 for the "/" + df.columns = colnames + + fh = self['data'] + if split: + # --- One dataframe per group. We skip group that have empty data + dfs={} + for group in fh.groups(): + try: + df = group.as_dataframe(time_index=True) + df.insert(0,'Time_[s]', df.index.values) + df.index=np.arange(0,len(df)) + except KeyError: + df = group.as_dataframe(time_index=False) + if len(df)>0: + dfs[group.name] = df + if len(dfs)==1: + dfs=dfs[group.name] + return dfs + else: + # --- One dataframe with all data + try: + df = fh.as_dataframe(time_index=True) + cleanColumns(df) + df.insert(0,'Time_[s]', df.index.values) + df.index=np.arange(0,len(df)) + except KeyError: + df = fh.as_dataframe(time_index=False) + return df + +def writeTDMSFromDataFrame(filename, df, defaultGroupName='default'): + """ + Write a TDMS file from a pandas dataframe + + Example: + # --- Create a TDMS file - One group two channels with time track + time = np.linspace(0,1,20) + colA = np.random.normal(0,1,20) + colB = np.random.normal(0,1,20) + df = pd.DataFrame(data={'Time_[s]':time ,'ColA':colA,'ColB':colB}) + writeTDMSFromDataFrame('out12.tdms', df, defaultGroupName = 'myGroup') + + #--- Create a TDMS file - Two groups, two channels without time track but with timestamp + TS = np.arange('2010-02', '2010-02-21', dtype='datetime64[D]') + df = pd.DataFrame(data={'GroupA/ColTime':time,'GroupA/ColA':colA,'GroupB/ColTimestamp': TS,'GroupB/ColA':colB)}) + writeTDMSFromDataFrame('out22.tdms', df) + + """ + from nptdms import TdmsWriter, ChannelObject + + defaultGroupName = 'default' + + columns =df.columns + + # Check if first column is time + if columns[0].lower().find('time')==0: + t = df.iloc[:,0].values + n = len(t) + dt1 = (np.max(t)-np.min(t))/(n-1) + if n>1: + dt2 = t[1]-t[0] + timeProperties = {'wf_increment':dt1, 'wf_start_offset':t[0]} + columns = columns[1:] # We remove the time column + else: + timeProperties = {} + + with TdmsWriter(filename) as tdms_writer: + + channels=[] + for iCol, col in enumerate(columns): + sp = col.split('/') + if len(sp)==2: + groupName = sp[0] + channelName = sp[1] + else: + groupName = defaultGroupName + channelName = col + data_array = df[col].values + channels.append(ChannelObject(groupName, channelName, data_array, timeProperties)) + tdms_writer.write_segment(channels) + +if __name__ == '__main__': + pass +# f = TDMSFile('TDMS_.tdms') +# dfs = f.toDataFrame(split=True) +# print(f) + diff --git a/pyFAST/input_output/tecplot_file.py b/pyFAST/input_output/tecplot_file.py new file mode 100644 index 0000000..1c9aa2d --- /dev/null +++ b/pyFAST/input_output/tecplot_file.py @@ -0,0 +1,222 @@ +""" +Read/Write TecPto ascii files +sea read_tecplot documentation below + +Part of weio library: https://github.com/ebranlard/weio + +""" +import pandas as pd +import numpy as np +import os +import struct + +try: + from .file import File, EmptyFileError, WrongFormatError, BrokenFormatError +except: + EmptyFileError = type('EmptyFileError', (Exception,),{}) + WrongFormatError = type('WrongFormatError', (Exception,),{}) + BrokenFormatError = type('BrokenFormatError', (Exception,),{}) + File=dict + + + +Keywords=['title','variables','zone','text','geometry','datasetauxdata','customlabels','varauxdata'] +# --------------------------------------------------------------------------------} +# --- Helper functions +# --------------------------------------------------------------------------------{ +def is_number(s): + try: + float(s) + return True + except ValueError: + pass + + try: + import unicodedata + unicodedata.numeric(s) + return True + except (TypeError, ValueError): + pass + + return False + + +def _process_merged_line(line, section, dict_out): + n = len(section) + line = line[n:].strip() + if section=='title': + dict_out[section]=line + elif section=='variables': + line = line.replace('=','').strip() + line = line.replace(',',' ').strip() + line = line.replace(' ',' ').strip() + line = line.replace('[','_[').strip() + line = line.replace('(','_(').strip() + line = line.replace('__','_').strip() + if line.find('"')==0: + line = line.replace('" "',',') + line = line.replace('"','') + sp=line.split(',') + else: + sp=line.split() + dict_out[section]=sp + elif section=='datasetauxdata': + if section not in dict_out.keys(): + dict_out[section]={} # initialixe an empty directory + sp = line.split('=') + key = sp[0] + value = sp[1].replace('"','').strip() + if is_number(value): + value=float(value) + dict_out[section][key]=value + + elif section=='zone': + if section not in dict_out.keys(): + dict_out[section]={} # initialixe an empty directory + sp = line.split('=') + key = sp[0] + value = sp[1].replace('"','').strip() + if is_number(value): + value=float(value) + dict_out[section][key]=value + + else: + print('!!! Reading of section not implemented:') + print('Processing section {}:'.format(section),line) + dict_out[section]=line + +def read_tecplot(filename, dict_out={}): + """ Reads a tecplot file + Limited support: + - title optional + - variables mandatory + - Lines may be continued to next line, stopping when a predefined keyword is detected + For now, assumes that only one section of numerical data is present + """ + + merged_line='' + current_section='' + variables=[] + with open(filename, "r") as f: + dfs = [] # list of dataframes + iline=0 + while True: + line= f.readline().strip() + iline+=1 + if not line: + break + l=line.lower().strip() + # Comment + if l[0]=='#': + continue + new_section = [k for k in Keywords if l.find(k)==0 ] + + if len(new_section)==1: + # --- Start of a new section + # First, process the previous section + if len(merged_line)>0: + _process_merged_line(merged_line, current_section, dict_out) + # Then start the new section + current_section=new_section[0] + merged_line =line + elif len(current_section)==0: + raise WrongFormatError('No section detected') + else: + if current_section=='title' or current_section=='variables': + # OK + pass + else: + if 'variables' not in dict_out.keys(): + raise WrongFormatError('The `variables` section should be present') + sp = l.split() + if is_number(sp[0]): + if len(merged_line)>0: + _process_merged_line(merged_line, current_section, dict_out) + # --- Special case of numerical values outside of zone + f.close() + M = np.loadtxt(filename, skiprows = iline-1) + if M.shape[1]!=len(dict_out['variables']): + raise BrokenFormatError('Number of columns of data does not match number of variables') + dict_out['data']=M + break + else: + # --- Continuation of previous section + merged_line +=' '+line + return dict_out + + +class TecplotFile(File): + + @staticmethod + def defaultExtensions(): + return ['.dat'] + + @staticmethod + def formatName(): + return 'Tecplot ASCII file' + + def __init__(self,filename=None,**kwargs): + self.filename = None + if filename: + self.read(filename=filename,**kwargs) + + def read(self, filename=None): + """ read a tecplot ascii file + sea `read_tecplot` documentation above + """ + if filename: + self.filename = filename + if not self.filename: + raise Exception('No filename provided') + if not os.path.isfile(self.filename): + raise OSError(2,'File not found:',self.filename) + if os.stat(self.filename).st_size == 0: + raise EmptyFileError('File is empty:',self.filename) + + try: + read_tecplot(filename,self) + except BrokenFormatError: + raise + except WrongFormatError: + raise + except Exception as e: + raise WrongFormatError('Tecplot dat File {}: '.format(self.filename)+e.args[0]) + + def write(self, filename=None, precision=None): + """ Write tecplot ascii file """ + if filename: + self.filename = filename + if not self.filename: + raise Exception('No filename provided') + + with open(self.filename, mode='w') as f: + if 'title' in self.keys(): + f.write('TITLE = {}\n'.format(self['title'])) + f.write('VARIABLES = ' + ','.join(['"{}"'.format(col) for col in self['variables'] ]) + '\n') + for k in Keywords[2:]: + if k in self.keys(): + f.write('{} = {}\n'.format(k,self[k])) + # Data + if 'data' in self.keys(): + for row in self['data']: + srow = np.array2string(row, edgeitems=0, separator=' ', precision=precision) + f.write(srow[1:-1]+'\n') + + + def __repr__(self): + s='<{} object> with keys:\n'.format(type(self).__name__) + for k,v in self.items(): + s+=' - {}: {}\n'.format(k,v) + return s + + def toDataFrame(self): + return pd.DataFrame(data=self['data'],columns=self['variables']) + +if __name__=='__main__': + mb = MannBoxFile('mann_bin/mini-u.bin', N=(2,4,8)) + F1=mb['field'].ravel() + mb.write('mann_bin/mini-u-out.bin') + + mb2= MannBoxFile('mann_bin/mini-u-out.bin', N=(2,4,8)) + F2=mb2['field'].ravel() +# print(F1-F2) diff --git a/pyFAST/input_output/tests/example_files/Bladed_out_ascii.$04 b/pyFAST/input_output/tests/example_files/Bladed_out_ascii.$04 new file mode 100644 index 0000000..dce38fc --- /dev/null +++ b/pyFAST/input_output/tests/example_files/Bladed_out_ascii.$04 @@ -0,0 +1,200 @@ + 1.8998750e+002 0.0000000e+000 1.8180233e+006 9.1210831e+001 9.2043343e+001 -0.0000000e+000 1.9751873e+004 8.0003691e+000 1.8180233e+006 + 1.9003751e+002 0.0000000e+000 1.8181525e+006 9.1210831e+001 9.2047165e+001 -0.0000000e+000 1.9752469e+004 8.0003691e+000 1.8181525e+006 + 1.9008749e+002 0.0000000e+000 1.8182498e+006 9.1210831e+001 9.2049088e+001 -0.0000000e+000 1.9753117e+004 8.0003691e+000 1.8182498e+006 + 1.9013750e+002 0.0000000e+000 1.8183211e+006 9.1210831e+001 9.2049660e+001 -0.0000000e+000 1.9753770e+004 8.0003691e+000 1.8183211e+006 + 1.9018750e+002 0.0000000e+000 1.8183821e+006 9.1210831e+001 9.2049881e+001 -0.0000000e+000 1.9754379e+004 8.0003691e+000 1.8183821e+006 + 1.9023750e+002 0.0000000e+000 1.8184510e+006 9.1210831e+001 9.2050674e+001 -0.0000000e+000 1.9754953e+004 8.0003700e+000 1.8184510e+006 + 1.9028751e+002 0.0000000e+000 1.8185394e+006 9.1210831e+001 9.2052483e+001 -0.0000000e+000 1.9755527e+004 8.0003700e+000 1.8185394e+006 + 1.9033749e+002 0.0000000e+000 1.8186499e+006 9.1210831e+001 9.2055298e+001 -0.0000000e+000 1.9756127e+004 8.0003700e+000 1.8186499e+006 + 1.9038750e+002 0.0000000e+000 1.8187794e+006 9.1210831e+001 9.2058823e+001 -0.0000000e+000 1.9756783e+004 8.0003700e+000 1.8187794e+006 + 1.9043750e+002 0.0000000e+000 1.8189216e+006 9.1210831e+001 9.2062683e+001 -0.0000000e+000 1.9757510e+004 8.0003700e+000 1.8189216e+006 + 1.9048750e+002 0.0000000e+000 1.8190685e+006 9.1210831e+001 9.2066452e+001 -0.0000000e+000 1.9758303e+004 8.0003700e+000 1.8190685e+006 + 1.9053751e+002 0.0000000e+000 1.8192066e+006 9.1210831e+001 9.2069519e+001 -0.0000000e+000 1.9759150e+004 8.0003700e+000 1.8192066e+006 + 1.9058749e+002 0.0000000e+000 1.8193198e+006 9.1210831e+001 9.2071213e+001 -0.0000000e+000 1.9760016e+004 8.0003700e+000 1.8193198e+006 + 1.9063750e+002 0.0000000e+000 1.8193945e+006 9.1210831e+001 9.2071060e+001 -0.0000000e+000 1.9760854e+004 8.0003700e+000 1.8193945e+006 + 1.9068750e+002 0.0000000e+000 1.8194268e+006 9.1210831e+001 9.2069153e+001 -0.0000000e+000 1.9761602e+004 8.0003710e+000 1.8194268e+006 + 1.9073750e+002 0.0000000e+000 1.8194258e+006 9.1210831e+001 9.2066193e+001 -0.0000000e+000 1.9762213e+004 8.0003710e+000 1.8194258e+006 + 1.9078751e+002 0.0000000e+000 1.8194081e+006 9.1210831e+001 9.2063065e+001 -0.0000000e+000 1.9762676e+004 8.0003710e+000 1.8194081e+006 + 1.9083749e+002 0.0000000e+000 1.8193874e+006 9.1210831e+001 9.2060394e+001 -0.0000000e+000 1.9763010e+004 8.0003710e+000 1.8193874e+006 + 1.9088750e+002 0.0000000e+000 1.8193679e+006 9.1210831e+001 9.2058289e+001 -0.0000000e+000 1.9763238e+004 8.0003719e+000 1.8193679e+006 + 1.9093750e+002 0.0000000e+000 1.8193481e+006 9.1210831e+001 9.2056549e+001 -0.0000000e+000 1.9763391e+004 8.0003719e+000 1.8193481e+006 + 1.9098750e+002 0.0000000e+000 1.8193214e+006 9.1210831e+001 9.2054771e+001 -0.0000000e+000 1.9763475e+004 8.0003729e+000 1.8193214e+006 + 1.9103751e+002 0.0000000e+000 1.8192738e+006 9.1210831e+001 9.2052261e+001 -0.0000000e+000 1.9763488e+004 8.0003729e+000 1.8192738e+006 + 1.9108749e+002 0.0000000e+000 1.8191858e+006 9.1210831e+001 9.2048164e+001 -0.0000000e+000 1.9763398e+004 8.0003738e+000 1.8191858e+006 + 1.9113750e+002 0.0000000e+000 1.8190453e+006 9.1210831e+001 9.2042099e+001 -0.0000000e+000 1.9763154e+004 8.0003738e+000 1.8190453e+006 + 1.9118750e+002 0.0000000e+000 1.8188615e+006 9.1210831e+001 9.2034729e+001 -0.0000000e+000 1.9762719e+004 8.0003738e+000 1.8188615e+006 + 1.9123750e+002 0.0000000e+000 1.8186603e+006 9.1210831e+001 9.2027473e+001 -0.0000000e+000 1.9762066e+004 8.0003738e+000 1.8186603e+006 + 1.9128751e+002 0.0000000e+000 1.8184714e+006 9.1210831e+001 9.2021652e+001 -0.0000000e+000 1.9761248e+004 8.0003738e+000 1.8184714e+006 + 1.9133749e+002 0.0000000e+000 1.8183126e+006 9.1210831e+001 9.2017853e+001 -0.0000000e+000 1.9760330e+004 8.0003738e+000 1.8183126e+006 + 1.9138750e+002 0.0000000e+000 1.8181878e+006 9.1210831e+001 9.2015923e+001 -0.0000000e+000 1.9759385e+004 8.0003729e+000 1.8181878e+006 + 1.9143750e+002 0.0000000e+000 1.8180893e+006 9.1210831e+001 9.2015259e+001 -0.0000000e+000 1.9758461e+004 8.0003729e+000 1.8180893e+006 + 1.9148750e+002 0.0000000e+000 1.8180056e+006 9.1210831e+001 9.2015121e+001 -0.0000000e+000 1.9757590e+004 8.0003719e+000 1.8180056e+006 + 1.9153751e+002 0.0000000e+000 1.8179279e+006 9.1210831e+001 9.2014961e+001 -0.0000000e+000 1.9756787e+004 8.0003719e+000 1.8179279e+006 + 1.9158749e+002 0.0000000e+000 1.8178490e+006 9.1210831e+001 9.2014496e+001 -0.0000000e+000 1.9756033e+004 8.0003710e+000 1.8178490e+006 + 1.9163750e+002 0.0000000e+000 1.8177629e+006 9.1210831e+001 9.2013512e+001 -0.0000000e+000 1.9755313e+004 8.0003700e+000 1.8177629e+006 + 1.9168750e+002 0.0000000e+000 1.8176641e+006 9.1210831e+001 9.2011795e+001 -0.0000000e+000 1.9754607e+004 8.0003700e+000 1.8176641e+006 + 1.9173750e+002 0.0000000e+000 1.8175525e+006 9.1210831e+001 9.2009438e+001 -0.0000000e+000 1.9753896e+004 8.0003691e+000 1.8175525e+006 + 1.9178751e+002 0.0000000e+000 1.8174401e+006 9.1210831e+001 9.2007179e+001 -0.0000000e+000 1.9753160e+004 8.0003691e+000 1.8174401e+006 + 1.9183749e+002 0.0000000e+000 1.8173536e+006 9.1210831e+001 9.2006248e+001 -0.0000000e+000 1.9752420e+004 8.0003691e+000 1.8173536e+006 + 1.9188750e+002 0.0000000e+000 1.8173160e+006 9.1210831e+001 9.2007599e+001 -0.0000000e+000 1.9751729e+004 8.0003681e+000 1.8173160e+006 + 1.9193750e+002 0.0000000e+000 1.8173363e+006 9.1210831e+001 9.2011322e+001 -0.0000000e+000 1.9751166e+004 8.0003681e+000 1.8173363e+006 + 1.9198750e+002 0.0000000e+000 1.8174073e+006 9.1210831e+001 9.2016777e+001 -0.0000000e+000 1.9750789e+004 8.0003681e+000 1.8174073e+006 + 1.9203751e+002 0.0000000e+000 1.8175164e+006 9.1210831e+001 9.2023163e+001 -0.0000000e+000 1.9750629e+004 8.0003681e+000 1.8175164e+006 + 1.9208749e+002 0.0000000e+000 1.8176539e+006 9.1210831e+001 9.2029930e+001 -0.0000000e+000 1.9750695e+004 8.0003681e+000 1.8176539e+006 + 1.9213750e+002 0.0000000e+000 1.8178086e+006 9.1210831e+001 9.2036575e+001 -0.0000000e+000 1.9750977e+004 8.0003681e+000 1.8178086e+006 + 1.9218750e+002 0.0000000e+000 1.8179651e+006 9.1210831e+001 9.2042389e+001 -0.0000000e+000 1.9751447e+004 8.0003691e+000 1.8179651e+006 + 1.9223750e+002 0.0000000e+000 1.8181034e+006 9.1210831e+001 9.2046638e+001 -0.0000000e+000 1.9752051e+004 8.0003691e+000 1.8181034e+006 + 1.9228751e+002 0.0000000e+000 1.8182116e+006 9.1210831e+001 9.2049011e+001 -0.0000000e+000 1.9752723e+004 8.0003691e+000 1.8182116e+006 + 1.9233749e+002 0.0000000e+000 1.8182921e+006 9.1210831e+001 9.2049911e+001 -0.0000000e+000 1.9753402e+004 8.0003691e+000 1.8182921e+006 + 1.9238750e+002 0.0000000e+000 1.8183594e+006 9.1210831e+001 9.2050278e+001 -0.0000000e+000 1.9754051e+004 8.0003691e+000 1.8183594e+006 + 1.9243750e+002 0.0000000e+000 1.8184319e+006 9.1210831e+001 9.2051064e+001 -0.0000000e+000 1.9754666e+004 8.0003700e+000 1.8184319e+006 + 1.9248750e+002 0.0000000e+000 1.8185215e+006 9.1210831e+001 9.2052795e+001 -0.0000000e+000 1.9755268e+004 8.0003700e+000 1.8185215e+006 + 1.9253751e+002 0.0000000e+000 1.8186325e+006 9.1210831e+001 9.2055504e+001 -0.0000000e+000 1.9755896e+004 8.0003700e+000 1.8186325e+006 + 1.9258749e+002 0.0000000e+000 1.8187620e+006 9.1210831e+001 9.2058945e+001 -0.0000000e+000 1.9756570e+004 8.0003700e+000 1.8187620e+006 + 1.9263750e+002 0.0000000e+000 1.8189053e+006 9.1210831e+001 9.2062767e+001 -0.0000000e+000 1.9757314e+004 8.0003700e+000 1.8189053e+006 + 1.9268750e+002 0.0000000e+000 1.8190546e+006 9.1210831e+001 9.2066574e+001 -0.0000000e+000 1.9758127e+004 8.0003700e+000 1.8190546e+006 + 1.9273750e+002 0.0000000e+000 1.8191983e+006 9.1210831e+001 9.2069832e+001 -0.0000000e+000 1.9758994e+004 8.0003700e+000 1.8191983e+006 + 1.9278751e+002 0.0000000e+000 1.8193209e+006 9.1210831e+001 9.2071869e+001 -0.0000000e+000 1.9759891e+004 8.0003700e+000 1.8193209e+006 + 1.9283749e+002 0.0000000e+000 1.8194071e+006 9.1210831e+001 9.2072136e+001 -0.0000000e+000 1.9760766e+004 8.0003700e+000 1.8194071e+006 + 1.9288750e+002 0.0000000e+000 1.8194514e+006 9.1210831e+001 9.2070625e+001 -0.0000000e+000 1.9761559e+004 8.0003710e+000 1.8194514e+006 + 1.9293750e+002 0.0000000e+000 1.8194600e+006 9.1210831e+001 9.2067909e+001 -0.0000000e+000 1.9762221e+004 8.0003710e+000 1.8194600e+006 + 1.9298750e+002 0.0000000e+000 1.8194500e+006 9.1210831e+001 9.2064880e+001 -0.0000000e+000 1.9762748e+004 8.0003710e+000 1.8194500e+006 + 1.9303751e+002 0.0000000e+000 1.8194334e+006 9.1210831e+001 9.2062164e+001 -0.0000000e+000 1.9763135e+004 8.0003710e+000 1.8194334e+006 + 1.9308749e+002 0.0000000e+000 1.8194165e+006 9.1210831e+001 9.2059967e+001 -0.0000000e+000 1.9763412e+004 8.0003719e+000 1.8194165e+006 + 1.9313750e+002 0.0000000e+000 1.8193981e+006 9.1210831e+001 9.2058113e+001 -0.0000000e+000 1.9763600e+004 8.0003719e+000 1.8193981e+006 + 1.9318750e+002 0.0000000e+000 1.8193721e+006 9.1210831e+001 9.2056229e+001 -0.0000000e+000 1.9763717e+004 8.0003729e+000 1.8193721e+006 + 1.9323750e+002 0.0000000e+000 1.8193269e+006 9.1210831e+001 9.2053711e+001 -0.0000000e+000 1.9763756e+004 8.0003729e+000 1.8193269e+006 + 1.9328751e+002 0.0000000e+000 1.8192431e+006 9.1210831e+001 9.2049721e+001 -0.0000000e+000 1.9763691e+004 8.0003738e+000 1.8192431e+006 + 1.9333749e+002 0.0000000e+000 1.8191085e+006 9.1210831e+001 9.2043800e+001 -0.0000000e+000 1.9763480e+004 8.0003738e+000 1.8191085e+006 + 1.9338750e+002 0.0000000e+000 1.8189289e+006 9.1210831e+001 9.2036491e+001 -0.0000000e+000 1.9763076e+004 8.0003738e+000 1.8189289e+006 + 1.9343750e+002 0.0000000e+000 1.8187291e+006 9.1210831e+001 9.2029167e+001 -0.0000000e+000 1.9762455e+004 8.0003738e+000 1.8187291e+006 + 1.9348750e+002 0.0000000e+000 1.8185390e+006 9.1210831e+001 9.2023148e+001 -0.0000000e+000 1.9761664e+004 8.0003738e+000 1.8185390e+006 + 1.9353751e+002 0.0000000e+000 1.8183766e+006 9.1210831e+001 9.2019104e+001 -0.0000000e+000 1.9760756e+004 8.0003738e+000 1.8183766e+006 + 1.9358749e+002 0.0000000e+000 1.8182473e+006 9.1210831e+001 9.2016945e+001 -0.0000000e+000 1.9759813e+004 8.0003729e+000 1.8182473e+006 + 1.9363750e+002 0.0000000e+000 1.8181448e+006 9.1210831e+001 9.2016075e+001 -0.0000000e+000 1.9758889e+004 8.0003729e+000 1.8181448e+006 + 1.9368750e+002 0.0000000e+000 1.8180578e+006 9.1210831e+001 9.2015778e+001 -0.0000000e+000 1.9758014e+004 8.0003719e+000 1.8180578e+006 + 1.9373750e+002 0.0000000e+000 1.8179761e+006 9.1210831e+001 9.2015465e+001 -0.0000000e+000 1.9757199e+004 8.0003719e+000 1.8179761e+006 + 1.9378751e+002 0.0000000e+000 1.8178923e+006 9.1210831e+001 9.2014824e+001 -0.0000000e+000 1.9756432e+004 8.0003710e+000 1.8178923e+006 + 1.9383749e+002 0.0000000e+000 1.8178015e+006 9.1210831e+001 9.2013687e+001 -0.0000000e+000 1.9755691e+004 8.0003710e+000 1.8178015e+006 + 1.9388750e+002 0.0000000e+000 1.8176985e+006 9.1210831e+001 9.2011871e+001 -0.0000000e+000 1.9754961e+004 8.0003700e+000 1.8176985e+006 + 1.9393750e+002 0.0000000e+000 1.8175820e+006 9.1210831e+001 9.2009407e+001 -0.0000000e+000 1.9754225e+004 8.0003691e+000 1.8175820e+006 + 1.9398750e+002 0.0000000e+000 1.8174636e+006 9.1210831e+001 9.2006958e+001 -0.0000000e+000 1.9753459e+004 8.0003691e+000 1.8174636e+006 + 1.9403751e+002 0.0000000e+000 1.8173680e+006 9.1210831e+001 9.2005730e+001 -0.0000000e+000 1.9752684e+004 8.0003691e+000 1.8173680e+006 + 1.9408749e+002 0.0000000e+000 1.8173196e+006 9.1210831e+001 9.2006737e+001 -0.0000000e+000 1.9751947e+004 8.0003681e+000 1.8173196e+006 + 1.9413750e+002 0.0000000e+000 1.8173295e+006 9.1210831e+001 9.2010170e+001 -0.0000000e+000 1.9751336e+004 8.0003681e+000 1.8173295e+006 + 1.9418750e+002 0.0000000e+000 1.8173916e+006 9.1210831e+001 9.2015427e+001 -0.0000000e+000 1.9750902e+004 8.0003681e+000 1.8173916e+006 + 1.9423750e+002 0.0000000e+000 1.8174939e+006 9.1210831e+001 9.2021683e+001 -0.0000000e+000 1.9750695e+004 8.0003681e+000 1.8174939e+006 + 1.9428751e+002 0.0000000e+000 1.8176243e+006 9.1210831e+001 9.2028351e+001 -0.0000000e+000 1.9750707e+004 8.0003681e+000 1.8176243e+006 + 1.9433749e+002 0.0000000e+000 1.8177733e+006 9.1210831e+001 9.2034935e+001 -0.0000000e+000 1.9750936e+004 8.0003681e+000 1.8177733e+006 + 1.9438750e+002 0.0000000e+000 1.8179255e+006 9.1210831e+001 9.2040779e+001 -0.0000000e+000 1.9751355e+004 8.0003691e+000 1.8179255e+006 + 1.9443750e+002 0.0000000e+000 1.8180616e+006 9.1210831e+001 9.2045135e+001 -0.0000000e+000 1.9751914e+004 8.0003691e+000 1.8180616e+006 + 1.9448750e+002 0.0000000e+000 1.8181694e+006 9.1210831e+001 9.2047638e+001 -0.0000000e+000 1.9752555e+004 8.0003691e+000 1.8181694e+006 + 1.9453751e+002 0.0000000e+000 1.8182486e+006 9.1210831e+001 9.2048630e+001 -0.0000000e+000 1.9753203e+004 8.0003691e+000 1.8182486e+006 + 1.9458749e+002 0.0000000e+000 1.8183140e+006 9.1210831e+001 9.2049019e+001 -0.0000000e+000 1.9753826e+004 8.0003691e+000 1.8183140e+006 + 1.9463750e+002 0.0000000e+000 1.8183833e+006 9.1210831e+001 9.2049767e+001 -0.0000000e+000 1.9754416e+004 8.0003700e+000 1.8183833e+006 + 1.9468750e+002 0.0000000e+000 1.8184696e+006 9.1210831e+001 9.2051445e+001 -0.0000000e+000 1.9754992e+004 8.0003700e+000 1.8184696e+006 + 1.9473750e+002 0.0000000e+000 1.8185779e+006 9.1210831e+001 9.2054138e+001 -0.0000000e+000 1.9755594e+004 8.0003700e+000 1.8185779e+006 + 1.9478751e+002 0.0000000e+000 1.8187063e+006 9.1210831e+001 9.2057594e+001 -0.0000000e+000 1.9756252e+004 8.0003700e+000 1.8187063e+006 + 1.9483749e+002 0.0000000e+000 1.8188490e+006 9.1210831e+001 9.2061462e+001 -0.0000000e+000 1.9756982e+004 8.0003700e+000 1.8188490e+006 + 1.9488750e+002 0.0000000e+000 1.8189986e+006 9.1210831e+001 9.2065353e+001 -0.0000000e+000 1.9757779e+004 8.0003700e+000 1.8189986e+006 + 1.9493750e+002 0.0000000e+000 1.8191440e+006 9.1210831e+001 9.2068741e+001 -0.0000000e+000 1.9758637e+004 8.0003700e+000 1.8191440e+006 + 1.9498750e+002 0.0000000e+000 1.8192695e+006 9.1210831e+001 9.2070969e+001 -0.0000000e+000 1.9759525e+004 8.0003700e+000 1.8192695e+006 + 1.9503751e+002 0.0000000e+000 1.8193605e+006 9.1210831e+001 9.2071487e+001 -0.0000000e+000 1.9760398e+004 8.0003700e+000 1.8193605e+006 + 1.9508749e+002 0.0000000e+000 1.8194104e+006 9.1210831e+001 9.2070221e+001 -0.0000000e+000 1.9761205e+004 8.0003710e+000 1.8194104e+006 + 1.9513750e+002 0.0000000e+000 1.8194241e+006 9.1210831e+001 9.2067696e+001 -0.0000000e+000 1.9761879e+004 8.0003710e+000 1.8194241e+006 + 1.9518750e+002 0.0000000e+000 1.8194180e+006 9.1210831e+001 9.2064789e+001 -0.0000000e+000 1.9762420e+004 8.0003710e+000 1.8194180e+006 + 1.9523750e+002 0.0000000e+000 1.8194059e+006 9.1210831e+001 9.2062195e+001 -0.0000000e+000 1.9762834e+004 8.0003710e+000 1.8194059e+006 + 1.9528751e+002 0.0000000e+000 1.8193943e+006 9.1210831e+001 9.2060150e+001 -0.0000000e+000 1.9763135e+004 8.0003719e+000 1.8193943e+006 + 1.9533749e+002 0.0000000e+000 1.8193825e+006 9.1210831e+001 9.2058479e+001 -0.0000000e+000 1.9763357e+004 8.0003719e+000 1.8193825e+006 + 1.9538750e+002 0.0000000e+000 1.8193646e+006 9.1210831e+001 9.2056839e+001 -0.0000000e+000 1.9763506e+004 8.0003729e+000 1.8193646e+006 + 1.9543750e+002 0.0000000e+000 1.8193285e+006 9.1210831e+001 9.2054626e+001 -0.0000000e+000 1.9763580e+004 8.0003729e+000 1.8193285e+006 + 1.9548750e+002 0.0000000e+000 1.8192568e+006 9.1210831e+001 9.2051018e+001 -0.0000000e+000 1.9763566e+004 8.0003738e+000 1.8192568e+006 + 1.9553751e+002 0.0000000e+000 1.8191344e+006 9.1210831e+001 9.2045464e+001 -0.0000000e+000 1.9763410e+004 8.0003738e+000 1.8191344e+006 + 1.9558749e+002 0.0000000e+000 1.8189641e+006 9.1210831e+001 9.2038338e+001 -0.0000000e+000 1.9763068e+004 8.0003738e+000 1.8189641e+006 + 1.9563750e+002 0.0000000e+000 1.8187691e+006 9.1210831e+001 9.2030945e+001 -0.0000000e+000 1.9762516e+004 8.0003738e+000 1.8187691e+006 + 1.9568750e+002 0.0000000e+000 1.8185793e+006 9.1210831e+001 9.2024704e+001 -0.0000000e+000 1.9761771e+004 8.0003738e+000 1.8185793e+006 + 1.9573750e+002 0.0000000e+000 1.8184156e+006 9.1210831e+001 9.2020416e+001 -0.0000000e+000 1.9760904e+004 8.0003738e+000 1.8184156e+006 + 1.9578751e+002 0.0000000e+000 1.8182865e+006 9.1210831e+001 9.2018112e+001 -0.0000000e+000 1.9759992e+004 8.0003729e+000 1.8182865e+006 + 1.9583749e+002 0.0000000e+000 1.8181863e+006 9.1210831e+001 9.2017250e+001 -0.0000000e+000 1.9759092e+004 8.0003729e+000 1.8181863e+006 + 1.9588750e+002 0.0000000e+000 1.8181044e+006 9.1210831e+001 9.2017082e+001 -0.0000000e+000 1.9758244e+004 8.0003719e+000 1.8181044e+006 + 1.9593750e+002 0.0000000e+000 1.8180288e+006 9.1210831e+001 9.2016953e+001 -0.0000000e+000 1.9757455e+004 8.0003719e+000 1.8180288e+006 + 1.9598750e+002 0.0000000e+000 1.8179519e+006 9.1210831e+001 9.2016510e+001 -0.0000000e+000 1.9756721e+004 8.0003710e+000 1.8179519e+006 + 1.9603751e+002 0.0000000e+000 1.8178681e+006 9.1210831e+001 9.2015549e+001 -0.0000000e+000 1.9756020e+004 8.0003710e+000 1.8178681e+006 + 1.9608749e+002 0.0000000e+000 1.8177715e+006 9.1210831e+001 9.2013893e+001 -0.0000000e+000 1.9755326e+004 8.0003700e+000 1.8177715e+006 + 1.9613750e+002 0.0000000e+000 1.8176600e+006 9.1210831e+001 9.2011513e+001 -0.0000000e+000 1.9754621e+004 8.0003700e+000 1.8176600e+006 + 1.9618750e+002 0.0000000e+000 1.8175434e+006 9.1210831e+001 9.2008980e+001 -0.0000000e+000 1.9753896e+004 8.0003691e+000 1.8175434e+006 + 1.9623750e+002 0.0000000e+000 1.8174446e+006 9.1210831e+001 9.2007446e+001 -0.0000000e+000 1.9753150e+004 8.0003691e+000 1.8174446e+006 + 1.9628751e+002 0.0000000e+000 1.8173891e+006 9.1210831e+001 9.2008018e+001 -0.0000000e+000 1.9752430e+004 8.0003681e+000 1.8173891e+006 + 1.9633749e+002 0.0000000e+000 1.8173914e+006 9.1210831e+001 9.2011063e+001 -0.0000000e+000 1.9751814e+004 8.0003681e+000 1.8173914e+006 + 1.9638750e+002 0.0000000e+000 1.8174480e+006 9.1210831e+001 9.2016060e+001 -0.0000000e+000 1.9751379e+004 8.0003681e+000 1.8174480e+006 + 1.9643750e+002 0.0000000e+000 1.8175456e+006 9.1210831e+001 9.2022194e+001 -0.0000000e+000 1.9751146e+004 8.0003681e+000 1.8175456e+006 + 1.9648750e+002 0.0000000e+000 1.8176733e+006 9.1210831e+001 9.2028816e+001 -0.0000000e+000 1.9751137e+004 8.0003681e+000 1.8176733e+006 + 1.9653751e+002 0.0000000e+000 1.8178208e+006 9.1210831e+001 9.2035423e+001 -0.0000000e+000 1.9751346e+004 8.0003681e+000 1.8178208e+006 + 1.9658749e+002 0.0000000e+000 1.8179734e+006 9.1210831e+001 9.2041367e+001 -0.0000000e+000 1.9751748e+004 8.0003691e+000 1.8179734e+006 + 1.9663750e+002 0.0000000e+000 1.8181125e+006 9.1210831e+001 9.2045906e+001 -0.0000000e+000 1.9752301e+004 8.0003691e+000 1.8181125e+006 + 1.9668750e+002 0.0000000e+000 1.8182229e+006 9.1210831e+001 9.2048584e+001 -0.0000000e+000 1.9752934e+004 8.0003691e+000 1.8182229e+006 + 1.9673750e+002 0.0000000e+000 1.8183038e+006 9.1210831e+001 9.2049637e+001 -0.0000000e+000 1.9753586e+004 8.0003691e+000 1.8183038e+006 + 1.9678751e+002 0.0000000e+000 1.8183671e+006 9.1210831e+001 9.2049911e+001 -0.0000000e+000 1.9754211e+004 8.0003691e+000 1.8183671e+006 + 1.9683749e+002 0.0000000e+000 1.8184310e+006 9.1210831e+001 9.2050400e+001 -0.0000000e+000 1.9754795e+004 8.0003691e+000 1.8184310e+006 + 1.9688750e+002 0.0000000e+000 1.8185103e+006 9.1210831e+001 9.2051773e+001 -0.0000000e+000 1.9755361e+004 8.0003700e+000 1.8185103e+006 + 1.9693750e+002 0.0000000e+000 1.8186113e+006 9.1210831e+001 9.2054192e+001 -0.0000000e+000 1.9755943e+004 8.0003700e+000 1.8186113e+006 + 1.9698750e+002 0.0000000e+000 1.8187339e+006 9.1210831e+001 9.2057457e+001 -0.0000000e+000 1.9756580e+004 8.0003700e+000 1.8187339e+006 + 1.9703751e+002 0.0000000e+000 1.8188711e+006 9.1210831e+001 9.2061203e+001 -0.0000000e+000 1.9757275e+004 8.0003700e+000 1.8188711e+006 + 1.9708749e+002 0.0000000e+000 1.8190163e+006 9.1210831e+001 9.2065025e+001 -0.0000000e+000 1.9758037e+004 8.0003700e+000 1.8190163e+006 + 1.9713750e+002 0.0000000e+000 1.8191590e+006 9.1210831e+001 9.2068428e+001 -0.0000000e+000 1.9758865e+004 8.0003700e+000 1.8191590e+006 + 1.9718750e+002 0.0000000e+000 1.8192845e+006 9.1210831e+001 9.2070763e+001 -0.0000000e+000 1.9759730e+004 8.0003700e+000 1.8192845e+006 + 1.9723750e+002 0.0000000e+000 1.8193770e+006 9.1210831e+001 9.2071442e+001 -0.0000000e+000 1.9760586e+004 8.0003700e+000 1.8193770e+006 + 1.9728751e+002 0.0000000e+000 1.8194283e+006 9.1210831e+001 9.2070305e+001 -0.0000000e+000 1.9761379e+004 8.0003710e+000 1.8194283e+006 + 1.9733749e+002 0.0000000e+000 1.8194418e+006 9.1210831e+001 9.2067802e+001 -0.0000000e+000 1.9762047e+004 8.0003710e+000 1.8194418e+006 + 1.9738750e+002 0.0000000e+000 1.8194320e+006 9.1210831e+001 9.2064781e+001 -0.0000000e+000 1.9762574e+004 8.0003710e+000 1.8194320e+006 + 1.9743750e+002 0.0000000e+000 1.8194151e+006 9.1210831e+001 9.2061996e+001 -0.0000000e+000 1.9762975e+004 8.0003710e+000 1.8194151e+006 + 1.9748750e+002 0.0000000e+000 1.8193979e+006 9.1210831e+001 9.2059769e+001 -0.0000000e+000 1.9763252e+004 8.0003719e+000 1.8193979e+006 + 1.9753751e+002 0.0000000e+000 1.8193816e+006 9.1210831e+001 9.2058006e+001 -0.0000000e+000 1.9763445e+004 8.0003719e+000 1.8193816e+006 + 1.9758749e+002 0.0000000e+000 1.8193619e+006 9.1210831e+001 9.2056374e+001 -0.0000000e+000 1.9763574e+004 8.0003729e+000 1.8193619e+006 + 1.9763750e+002 0.0000000e+000 1.8193271e+006 9.1210831e+001 9.2054321e+001 -0.0000000e+000 1.9763629e+004 8.0003729e+000 1.8193271e+006 + 1.9768750e+002 0.0000000e+000 1.8192604e+006 9.1210831e+001 9.2051003e+001 -0.0000000e+000 1.9763607e+004 8.0003738e+000 1.8192604e+006 + 1.9773750e+002 0.0000000e+000 1.8191440e+006 9.1210831e+001 9.2045769e+001 -0.0000000e+000 1.9763449e+004 8.0003738e+000 1.8191440e+006 + 1.9778751e+002 0.0000000e+000 1.8189776e+006 9.1210831e+001 9.2038803e+001 -0.0000000e+000 1.9763117e+004 8.0003738e+000 1.8189776e+006 + 1.9783749e+002 0.0000000e+000 1.8187824e+006 9.1210831e+001 9.2031326e+001 -0.0000000e+000 1.9762578e+004 8.0003738e+000 1.8187824e+006 + 1.9788750e+002 0.0000000e+000 1.8185873e+006 9.1210831e+001 9.2024811e+001 -0.0000000e+000 1.9761838e+004 8.0003738e+000 1.8185873e+006 + 1.9793750e+002 0.0000000e+000 1.8184171e+006 9.1210831e+001 9.2020195e+001 -0.0000000e+000 1.9760967e+004 8.0003738e+000 1.8184171e+006 + 1.9798750e+002 0.0000000e+000 1.8182808e+006 9.1210831e+001 9.2017593e+001 -0.0000000e+000 1.9760039e+004 8.0003729e+000 1.8182808e+006 + 1.9803751e+002 0.0000000e+000 1.8181749e+006 9.1210831e+001 9.2016548e+001 -0.0000000e+000 1.9759115e+004 8.0003729e+000 1.8181749e+006 + 1.9808749e+002 0.0000000e+000 1.8180896e+006 9.1210831e+001 9.2016335e+001 -0.0000000e+000 1.9758240e+004 8.0003719e+000 1.8180896e+006 + 1.9813750e+002 0.0000000e+000 1.8180133e+006 9.1210831e+001 9.2016289e+001 -0.0000000e+000 1.9757428e+004 8.0003719e+000 1.8180133e+006 + 1.9818750e+002 0.0000000e+000 1.8179378e+006 9.1210831e+001 9.2016006e+001 -0.0000000e+000 1.9756674e+004 8.0003710e+000 1.8179378e+006 + 1.9823750e+002 0.0000000e+000 1.8178573e+006 9.1210831e+001 9.2015266e+001 -0.0000000e+000 1.9755961e+004 8.0003710e+000 1.8178573e+006 + 1.9828751e+002 0.0000000e+000 1.8177664e+006 9.1210831e+001 9.2013870e+001 -0.0000000e+000 1.9755275e+004 8.0003700e+000 1.8177664e+006 + 1.9833749e+002 0.0000000e+000 1.8176603e+006 9.1210831e+001 9.2011719e+001 -0.0000000e+000 1.9754584e+004 8.0003700e+000 1.8176603e+006 + 1.9838750e+002 0.0000000e+000 1.8175465e+006 9.1210831e+001 9.2009262e+001 -0.0000000e+000 1.9753871e+004 8.0003691e+000 1.8175465e+006 + 1.9843750e+002 0.0000000e+000 1.8174458e+006 9.1210831e+001 9.2007576e+001 -0.0000000e+000 1.9753139e+004 8.0003691e+000 1.8174458e+006 + 1.9848750e+002 0.0000000e+000 1.8173850e+006 9.1210831e+001 9.2007858e+001 -0.0000000e+000 1.9752420e+004 8.0003681e+000 1.8173850e+006 + 1.9853751e+002 0.0000000e+000 1.8173818e+006 9.1210831e+001 9.2010620e+001 -0.0000000e+000 1.9751805e+004 8.0003681e+000 1.8173818e+006 + 1.9858749e+002 0.0000000e+000 1.8174331e+006 9.1210831e+001 9.2015442e+001 -0.0000000e+000 1.9751346e+004 8.0003681e+000 1.8174331e+006 + 1.9863750e+002 0.0000000e+000 1.8175280e+006 9.1210831e+001 9.2021523e+001 -0.0000000e+000 1.9751098e+004 8.0003681e+000 1.8175280e+006 + 1.9868750e+002 0.0000000e+000 1.8176546e+006 9.1210831e+001 9.2028175e+001 -0.0000000e+000 1.9751070e+004 8.0003681e+000 1.8176546e+006 + 1.9873750e+002 0.0000000e+000 1.8178040e+006 9.1210831e+001 9.2034920e+001 -0.0000000e+000 1.9751271e+004 8.0003681e+000 1.8178040e+006 + 1.9878751e+002 0.0000000e+000 1.8179615e+006 9.1210831e+001 9.2041145e+001 -0.0000000e+000 1.9751666e+004 8.0003691e+000 1.8179615e+006 + 1.9883749e+002 0.0000000e+000 1.8181086e+006 9.1210831e+001 9.2046074e+001 -0.0000000e+000 1.9752223e+004 8.0003691e+000 1.8181086e+006 + 1.9888750e+002 0.0000000e+000 1.8182290e+006 9.1210831e+001 9.2049171e+001 -0.0000000e+000 1.9752875e+004 8.0003691e+000 1.8182290e+006 + 1.9893750e+002 0.0000000e+000 1.8183185e+006 9.1210831e+001 9.2050537e+001 -0.0000000e+000 1.9753557e+004 8.0003691e+000 1.8183185e+006 + 1.9898750e+002 0.0000000e+000 1.8183874e+006 9.1210831e+001 9.2050941e+001 -0.0000000e+000 1.9754215e+004 8.0003691e+000 1.8183874e+006 + 1.9903751e+002 0.0000000e+000 1.8184536e+006 9.1210831e+001 9.2051399e+001 -0.0000000e+000 1.9754832e+004 8.0003691e+000 1.8184536e+006 + 1.9908749e+002 0.0000000e+000 1.8185339e+006 9.1210831e+001 9.2052666e+001 -0.0000000e+000 1.9755430e+004 8.0003700e+000 1.8185339e+006 + 1.9913750e+002 0.0000000e+000 1.8186350e+006 9.1210831e+001 9.2054962e+001 -0.0000000e+000 1.9756037e+004 8.0003700e+000 1.8186350e+006 + 1.9918750e+002 0.0000000e+000 1.8187561e+006 9.1210831e+001 9.2058128e+001 -0.0000000e+000 1.9756678e+004 8.0003700e+000 1.8187561e+006 + 1.9923750e+002 0.0000000e+000 1.8188934e+006 9.1210831e+001 9.2061829e+001 -0.0000000e+000 1.9757383e+004 8.0003700e+000 1.8188934e+006 + 1.9928751e+002 0.0000000e+000 1.8190404e+006 9.1210831e+001 9.2065689e+001 -0.0000000e+000 1.9758158e+004 8.0003700e+000 1.8190404e+006 + 1.9933749e+002 0.0000000e+000 1.8191869e+006 9.1210831e+001 9.2069229e+001 -0.0000000e+000 1.9758998e+004 8.0003700e+000 1.8191869e+006 + 1.9938750e+002 0.0000000e+000 1.8193190e+006 9.1210831e+001 9.2071808e+001 -0.0000000e+000 1.9759881e+004 8.0003700e+000 1.8193190e+006 + 1.9943750e+002 0.0000000e+000 1.8194199e+006 9.1210831e+001 9.2072807e+001 -0.0000000e+000 1.9760760e+004 8.0003700e+000 1.8194199e+006 + 1.9948750e+002 0.0000000e+000 1.8194794e+006 9.1210831e+001 9.2071953e+001 -0.0000000e+000 1.9761582e+004 8.0003710e+000 1.8194794e+006 + 1.9953751e+002 0.0000000e+000 1.8194994e+006 9.1210831e+001 9.2069588e+001 -0.0000000e+000 1.9762295e+004 8.0003710e+000 1.8194994e+006 + 1.9958749e+002 0.0000000e+000 1.8194931e+006 9.1210831e+001 9.2066536e+001 -0.0000000e+000 1.9762865e+004 8.0003710e+000 1.8194931e+006 + 1.9963750e+002 0.0000000e+000 1.8194763e+006 9.1210831e+001 9.2063606e+001 -0.0000000e+000 1.9763295e+004 8.0003710e+000 1.8194763e+006 + 1.9968750e+002 0.0000000e+000 1.8194586e+006 9.1210831e+001 9.2061195e+001 -0.0000000e+000 1.9763609e+004 8.0003719e+000 1.8194586e+006 + 1.9973750e+002 0.0000000e+000 1.8194409e+006 9.1210831e+001 9.2059258e+001 -0.0000000e+000 1.9763822e+004 8.0003719e+000 1.8194409e+006 + 1.9978751e+002 0.0000000e+000 1.8194190e+006 9.1210831e+001 9.2057495e+001 -0.0000000e+000 1.9763953e+004 8.0003729e+000 1.8194190e+006 + 1.9983749e+002 0.0000000e+000 1.8193841e+006 9.1210831e+001 9.2055389e+001 -0.0000000e+000 1.9764021e+004 8.0003729e+000 1.8193841e+006 + 1.9988750e+002 0.0000000e+000 1.8193200e+006 9.1210831e+001 9.2052170e+001 -0.0000000e+000 1.9764006e+004 8.0003738e+000 1.8193200e+006 + 1.9993750e+002 0.0000000e+000 1.8192086e+006 9.1210831e+001 9.2047127e+001 -0.0000000e+000 1.9763861e+004 8.0003738e+000 1.8192086e+006 diff --git a/pyFAST/input_output/tests/example_files/Bladed_out_ascii.$41 b/pyFAST/input_output/tests/example_files/Bladed_out_ascii.$41 new file mode 100644 index 0000000..7a3191e --- /dev/null +++ b/pyFAST/input_output/tests/example_files/Bladed_out_ascii.$41 @@ -0,0 +1,800 @@ + -2.7539790e+006 5.2066395e+006 5.8901185e+006 1.1140432e+005 1.4636380e+005 1.4788936e+005 2.0807119e+005 4.1174444e+005 + -8.1026569e+005 2.4957093e+006 2.6239465e+006 9.0228570e+004 1.2245208e+005 6.2065863e+004 1.3728322e+005 2.8640097e+005 + -1.0471470e+005 6.4621475e+005 6.5464394e+005 4.1605594e+004 7.0481867e+004 1.7147863e+004 7.2537875e+004 1.1219445e+005 + 1.4410109e-008 5.8071645e-009 1.5536228e-008 -2.1895366e-007 -1.1670807e-009 7.9273569e-008 7.9282160e-008 -1.1795009e-011 + -2.8093123e+006 5.2177755e+006 5.9259950e+006 1.1278297e+005 1.4636638e+005 1.5078955e+005 2.1014425e+005 4.0421269e+005 + -8.2650669e+005 2.5040133e+006 2.6368913e+006 9.1369406e+004 1.2264096e+005 6.3267480e+004 1.3799847e+005 2.8303166e+005 + -1.0693295e+005 6.4920363e+005 6.5795138e+005 4.2152008e+004 7.0692891e+004 1.7445785e+004 7.2813734e+004 1.1116528e+005 + 7.0191528e-009 2.2906042e-009 7.3834525e-009 -3.2959292e-008 3.3155203e-009 1.2058507e-008 1.2506010e-008 -4.0785153e-012 + -2.8558188e+006 5.2286205e+006 5.9576985e+006 1.1406438e+005 1.4639395e+005 1.5328159e+005 2.1195856e+005 3.9656497e+005 + -8.3970044e+005 2.5117085e+006 2.6483535e+006 9.2433344e+004 1.2283001e+005 6.4268613e+004 1.3862780e+005 2.7960753e+005 + -1.0861304e+005 6.5185394e+005 6.6084069e+005 4.2667785e+004 7.0877250e+004 1.7675734e+004 7.3048039e+004 1.1011945e+005 + 1.2991153e-008 -1.6906245e-009 1.3100697e-008 -1.4731847e-007 4.6262443e-009 5.3238381e-008 5.3439006e-008 -2.8137492e-012 + -2.8938458e+006 5.2395495e+006 5.9855845e+006 1.1525708e+005 1.4643584e+005 1.5535908e+005 2.1349450e+005 3.8879013e+005 + -8.5020538e+005 2.5192230e+006 2.6588218e+006 9.3427594e+004 1.2302762e+005 6.5076570e+004 1.3917886e+005 2.7612269e+005 + -1.0988827e+005 6.5431469e+005 6.6347806e+005 4.3153207e+004 7.1050602e+004 1.7849842e+004 7.3258484e+004 1.0905501e+005 + -4.1570338e-009 2.6137075e-009 4.9104374e-009 8.8208566e-008 -5.2987730e-009 -3.1906893e-008 3.2343884e-008 -4.5332627e-012 + -2.9240260e+006 5.2508150e+006 6.0100735e+006 1.1636721e+005 1.4647689e+005 1.5704284e+005 2.1475086e+005 3.8088288e+005 + -8.5830431e+005 2.5269430e+006 2.6687315e+006 9.4356984e+004 1.2323811e+005 6.5706117e+004 1.3966003e+005 2.7257444e+005 + -1.1082500e+005 6.5674356e+005 6.6602875e+005 4.3606418e+004 7.1228086e+004 1.7975273e+004 7.3461219e+004 1.0797120e+005 + -7.4486495e-010 -1.9892736e-009 2.1241549e-009 3.0640723e-007 5.8601486e-008 -1.0657224e-007 1.2162145e-007 -1.9028334e-011 + -2.9470618e+006 5.2625840e+006 6.0315805e+006 1.1740167e+005 1.4651330e+005 1.5836606e+005 2.1574513e+005 3.7286266e+005 + -8.6417031e+005 2.5350985e+006 2.6783418e+006 9.5225688e+004 1.2346061e+005 6.6175016e+004 1.4007731e+005 2.6897084e+005 + -1.1142255e+005 6.5929244e+005 6.6864150e+005 4.4024883e+004 7.1419906e+004 1.8054166e+004 7.3666516e+004 1.0686985e+005 + 4.2688115e-009 -4.2554547e-009 6.0275736e-009 2.8723694e-008 9.1385886e-009 -9.7633190e-009 1.3372966e-008 9.9618092e-012 + -2.9635573e+006 5.2750625e+006 6.0505335e+006 1.1836488e+005 1.4655689e+005 1.5935298e+005 2.1650011e+005 3.6476888e+005 + -8.6797163e+005 2.5437955e+006 2.6878003e+006 9.6036516e+004 1.2369341e+005 6.6499508e+004 1.4043592e+005 2.6532875e+005 + -1.1168195e+005 6.6208531e+005 6.7143863e+005 4.4408836e+004 7.1631375e+004 1.8088959e+004 7.3880070e+004 1.0575482e+005 + 4.6302406e-009 -1.0771313e-009 4.7538764e-009 1.8116037e-007 -2.0623773e-008 -6.5384590e-008 6.8560084e-008 1.8843593e-011 + -2.9739258e+006 5.2886295e+006 6.0674410e+006 1.1926285e+005 1.4662719e+005 1.6000659e+005 2.1702913e+005 3.5663606e+005 + -8.6996800e+005 2.5531498e+006 2.6972983e+006 9.6794188e+004 1.2394257e+005 6.6690648e+004 1.4074588e+005 2.6166375e+005 + -1.1166615e+005 6.6518813e+005 6.7449581e+005 4.4763051e+004 7.1865695e+004 1.8087020e+004 7.4106805e+004 1.0462979e+005 + 1.1662071e-008 4.6299364e-009 1.2547519e-008 2.7310829e-008 -3.5194702e-008 -1.1469176e-008 3.7016335e-008 -8.9244168e-012 + -2.9783815e+006 5.3038325e+006 6.0828770e+006 1.2011208e+005 1.4674016e+005 1.6031534e+005 2.1733311e+005 3.4847856e+005 + -8.7045550e+005 2.5633695e+006 2.7071305e+006 9.7515688e+004 1.2422506e+005 6.6753063e+004 1.4102425e+005 2.5798398e+005 + -1.1148892e+005 6.6859869e+005 6.7783038e+005 4.5099164e+004 7.2126727e+004 1.8059006e+004 7.4353156e+004 1.0349752e+005 + 1.7537638e-009 -2.2744056e-009 2.8720391e-009 -5.9026526e-009 1.2015235e-008 2.6924454e-009 1.2313210e-008 -2.5437430e-012 + -2.9769070e+006 5.3211630e+006 6.0972740e+006 1.2092414e+005 1.4690172e+005 1.6026639e+005 2.1740614e+005 3.4029616e+005 + -8.6958713e+005 2.5746863e+006 2.7175708e+006 9.8213914e+004 1.2456055e+005 6.6685117e+004 1.4128777e+005 2.5429241e+005 + -1.1122984e+005 6.7226050e+005 6.8140025e+005 4.5426742e+004 7.2417828e+004 1.8011617e+004 7.4624125e+004 1.0236066e+005 + -2.9010327e-009 1.1441410e-008 1.1803468e-008 1.9757147e-007 -5.9924602e-008 -7.2211940e-008 9.3837748e-008 -1.6711965e-011 + -2.9692958e+006 5.3408760e+006 6.1107835e+006 1.2168980e+005 1.4711727e+005 1.5986119e+005 2.1725352e+005 3.3210234e+005 + -8.6721069e+005 2.5871728e+006 2.7286475e+006 9.8881406e+004 1.2496066e+005 6.6481297e+004 1.4154480e+005 2.5059811e+005 + -1.1084802e+005 6.7610613e+005 6.8513269e+005 4.5744566e+004 7.2738742e+004 1.7940699e+004 7.4918578e+004 1.0122395e+005 + 9.9090780e-009 -4.0759787e-009 1.0714636e-008 -2.8072881e-007 3.9073313e-009 9.7989378e-008 9.8067247e-008 6.0680350e-012 + -2.9550958e+006 5.3628545e+006 6.1231365e+006 1.2239624e+005 1.4739367e+005 1.5910523e+005 2.1688563e+005 3.2393200e+005 + -8.6291288e+005 2.6006683e+006 2.7400900e+006 9.9506375e+004 1.2542221e+005 6.6131602e+004 1.4178900e+005 2.4691841e+005 + -1.1020171e+005 6.8009019e+005 6.8896088e+005 4.6046055e+004 7.3084594e+004 1.7832840e+004 7.5228773e+004 1.0009359e+005 + 2.3677145e-009 3.3562300e-009 4.1073536e-009 -9.6511990e-008 -5.9000996e-008 3.0594123e-008 6.6461403e-008 -1.2931878e-012 + -2.9335890e+006 5.3866775e+006 6.1336965e+006 1.2302894e+005 1.4773430e+005 1.5798964e+005 2.1630106e+005 3.1582747e+005 + -8.5621244e+005 2.6148690e+006 2.7514795e+006 1.0007623e+005 1.2593021e+005 6.5620344e+004 1.4200158e+005 2.4327253e+005 + -1.0913813e+005 6.8419488e+005 6.9284469e+005 4.6325492e+004 7.3447922e+004 1.7673447e+004 7.5544344e+004 9.8975125e+004 + 4.0071972e-009 5.5601852e-009 6.8537065e-009 -1.3760601e-007 -7.4668378e-009 4.7040309e-008 4.7629239e-008 5.6843419e-013 + -2.9039308e+006 5.4118430e+006 6.1417310e+006 1.2357833e+005 1.4813409e+005 1.5648875e+005 2.1548188e+005 3.0781972e+005 + -8.4672138e+005 2.6294975e+006 2.7624618e+006 1.0058367e+005 1.2646817e+005 6.4928234e+004 1.4216144e+005 2.3967392e+005 + -1.0756305e+005 6.8840919e+005 6.9676181e+005 4.6579895e+004 7.3821992e+004 1.7452750e+004 7.5857008e+004 9.7871945e+004 + 4.8876085e-009 -1.8841284e-009 5.2381921e-009 2.6748818e-008 -8.7486995e-009 -9.5431147e-009 1.2946458e-008 6.4233063e-012 + -2.8652578e+006 5.4378820e+006 6.1465650e+006 1.2403780e+005 1.4857295e+005 1.5456675e+005 2.1439405e+005 2.9991350e+005 + -8.3417369e+005 2.6444120e+006 2.7728615e+006 1.0102379e+005 1.2702523e+005 6.4034223e+004 1.4225256e+005 2.3612438e+005 + -1.0544619e+005 6.9271156e+005 7.0069125e+005 4.6808801e+004 7.4203641e+004 1.7166246e+004 7.6163375e+004 9.6784383e+004 + 9.0584820e-009 4.0002250e-009 9.9024193e-009 5.5520459e-008 -1.8849263e-008 -1.9676847e-008 2.7248360e-008 2.3305802e-012 + -2.8169153e+006 5.4643680e+006 6.1477090e+006 1.2440137e+005 1.4902350e+005 1.5220289e+005 2.1301109e+005 2.9210222e+005 + -8.1831531e+005 2.6595525e+006 2.7825995e+006 1.0139077e+005 1.2759399e+005 6.2921832e+004 1.4226519e+005 2.3262055e+005 + -1.0274879e+005 6.9706600e+005 7.0459800e+005 4.7009910e+004 7.4592586e+004 1.6808455e+004 7.6462922e+004 9.5711680e+004 + 6.0927210e-009 9.6316857e-009 1.1396957e-008 -9.8884854e-008 -1.5040570e-008 3.2825817e-008 3.6107519e-008 -1.3514523e-011 + -2.7585800e+006 5.4908840e+006 6.1448815e+006 1.2465581e+005 1.4946489e+005 1.4940284e+005 2.1133141e+005 2.8438938e+005 + -7.9885250e+005 2.6748090e+006 2.7915530e+006 1.0167033e+005 1.2816579e+005 6.1583348e+004 1.4219345e+005 2.2916259e+005 + -9.9375195e+004 7.0143850e+005 7.0844294e+005 4.7176629e+004 7.4986828e+004 1.6370068e+004 7.6752875e+004 9.4653648e+004 + 8.4780383e-010 7.6529396e-009 7.6997573e-009 1.1463929e-007 -1.6250851e-008 -3.9067217e-008 4.2312379e-008 -7.9012352e-012 + -2.6902010e+006 5.5169820e+006 6.1379370e+006 1.2472601e+005 1.4988717e+005 1.4618327e+005 2.0936980e+005 2.7679784e+005 + -7.7556644e+005 2.6899680e+006 2.7995413e+006 1.0178959e+005 1.2872822e+005 6.0017891e+004 1.4203205e+005 2.2576005e+005 + -9.5235875e+004 7.0579994e+005 7.1219619e+005 4.7266625e+004 7.5381320e+004 1.5842762e+004 7.7028156e+004 9.3613875e+004 + -1.8381495e-009 1.0278107e-009 2.1059887e-009 3.2357352e-008 -3.3155061e-009 -1.0895896e-008 1.1389167e-008 5.0732751e-012 + -2.6120070e+006 5.5423220e+006 6.1269825e+006 1.2461052e+005 1.5029436e+005 1.4255505e+005 2.0714809e+005 2.6935209e+005 + -7.4850463e+005 2.7047413e+006 2.8064008e+006 1.0174961e+005 1.2927111e+005 5.8232234e+004 1.4178156e+005 2.2241875e+005 + -9.0313492e+004 7.1010081e+005 7.1582100e+005 4.7285211e+004 7.5763219e+004 1.5227601e+004 7.7278359e+004 9.2590922e+004 + 2.1068329e-009 4.3254884e-009 4.8112985e-009 -1.5030125e-007 8.0390379e-009 5.0032227e-008 5.0673957e-008 -3.2684966e-012 + -2.5243200e+006 5.5665165e+006 6.1121435e+006 1.2445129e+005 1.5068388e+005 1.3851072e+005 2.0467255e+005 2.6207038e+005 + -7.1800856e+005 2.7189013e+006 2.8121095e+006 1.0168798e+005 1.2978097e+005 5.6234211e+004 1.4144039e+005 2.1914053e+005 + -8.4725258e+004 7.1433581e+005 7.1934275e+005 4.7308543e+004 7.6129898e+004 1.4535749e+004 7.7505156e+004 9.1580242e+004 + 4.4021964e-010 5.6313487e-009 5.6485292e-009 1.6666181e-008 1.3183595e-008 -5.1760480e-009 1.4163285e-008 1.4352963e-012 + -2.4275670e+006 5.5893015e+006 6.0937160e+006 1.2420225e+005 1.5103756e+005 1.3404203e+005 2.0193963e+005 2.5495006e+005 + -6.8450988e+005 2.7324080e+006 2.8168435e+006 1.0155891e+005 1.3025482e+005 5.4032773e+004 1.4101723e+005 2.1592603e+005 + -7.8629539e+004 7.1847344e+005 7.2276319e+005 4.7314051e+004 7.6482289e+004 1.3781252e+004 7.7713984e+004 9.0584344e+004 + 7.9764095e-010 -6.7178947e-009 6.7650827e-009 4.0058374e-008 -6.9047559e-009 -1.3234427e-008 1.4927348e-008 -5.3859139e-012 + -2.3227158e+006 5.6105550e+006 6.0723420e+006 1.2379109e+005 1.5134133e+005 1.2919271e+005 1.9898481e+005 2.4798805e+005 + -6.4830063e+005 2.7452690e+006 2.8207793e+006 1.0128980e+005 1.3069459e+005 5.1650328e+004 1.4053055e+005 2.1277631e+005 + -7.2075273e+004 7.2244119e+005 7.2602763e+005 4.7261078e+004 7.6820281e+004 1.2969188e+004 7.7907352e+004 8.9606797e+004 + 8.6659036e-010 -1.8653976e-009 2.0568633e-009 9.1977057e-009 -2.7646720e-008 -3.3668428e-009 2.7850975e-008 -6.4517280e-012 + -2.2112778e+006 5.6302480e+006 6.0489210e+006 1.2327739e+005 1.5160350e+005 1.2404818e+005 1.9588664e+005 2.4120495e+005 + -6.0964663e+005 2.7573728e+006 2.8239640e+006 1.0093693e+005 1.3109911e+005 4.9123996e+004 1.4000052e+005 2.0969733e+005 + -6.5022023e+004 7.2619694e+005 7.2910206e+005 4.7176410e+004 7.7137086e+004 1.2101045e+004 7.8080508e+004 8.8646688e+004 + 1.2228465e-009 9.9183808e-009 9.9934789e-009 1.2457473e-008 -4.5347008e-008 -4.4008743e-009 4.5560057e-008 -7.6880724e-012 + -2.0944876e+006 5.6483590e+006 6.0241875e+006 1.2271556e+005 1.5183348e+005 1.1864909e+005 1.9269408e+005 2.3463058e+005 + -5.6908606e+005 2.7686203e+006 2.8265028e+006 1.0055424e+005 1.3146622e+005 4.6483082e+004 1.3944191e+005 2.0670106e+005 + -5.7586254e+004 7.2972938e+005 7.3199806e+005 4.7088641e+004 7.7431359e+004 1.1189806e+004 7.8235711e+004 8.7705555e+004 + 2.8429903e-010 6.8638943e-009 6.8697794e-009 -1.9542050e-008 -5.3267840e-008 6.1428267e-009 5.3620862e-008 -3.0127012e-012 + -1.9732024e+006 5.6648230e+006 5.9986455e+006 1.2209016e+005 1.5202734e+005 1.1298541e+005 1.8941492e+005 2.2827852e+005 + -5.2740025e+005 2.7790053e+006 2.8286078e+006 1.0012799e+005 1.3179489e+005 4.3746938e+004 1.3886572e+005 2.0379578e+005 + -5.0039703e+004 7.3302994e+005 7.3473588e+005 4.6994453e+004 7.7701797e+004 1.0261202e+004 7.8376406e+004 8.6787406e+004 + 2.1377278e-010 1.1228979e-008 1.1231013e-008 4.4440931e-009 -6.1776191e-008 -1.2453540e-009 6.1788739e-008 1.3727686e-011 + -1.8481578e+006 5.6795805e+006 5.9727150e+006 1.2155384e+005 1.5217530e+005 1.0705466e+005 1.8605919e+005 2.2214952e+005 + -4.8521088e+005 2.7885890e+006 2.8304873e+006 9.9804898e+004 1.3208380e+005 4.0932020e+004 1.3828073e+005 2.0098258e+005 + -4.2589219e+004 7.3613144e+005 7.3736244e+005 4.6976383e+004 7.7955734e+004 9.3346650e+003 7.8512625e+004 8.5891391e+004 + -2.6785829e-009 8.5059586e-009 8.9177430e-009 2.3864178e-008 -1.5282126e-008 -7.4678894e-009 1.7009196e-008 1.1084467e-012 + -1.7200655e+006 5.6927635e+006 5.9469475e+006 1.2094874e+005 1.5228080e+005 1.0089475e+005 1.8267236e+005 2.1626802e+005 + -4.4268716e+005 2.7974233e+006 2.8322340e+006 9.9432695e+004 1.3233759e+005 3.8054250e+004 1.3770027e+005 1.9828205e+005 + -3.5252578e+004 7.3903194e+005 7.3987225e+005 4.6940188e+004 7.8190445e+004 8.4133350e+003 7.8641781e+004 8.5031953e+004 + -3.2971790e-009 1.0294796e-009 3.4541594e-009 1.5102319e-008 6.8306434e-008 -5.7441483e-009 6.8547529e-008 7.4749096e-012 + -1.5894776e+006 5.7045780e+006 5.9218790e+006 1.2026538e+005 1.5236184e+005 9.4561031e+004 1.7932072e+005 2.1067219e+005 + -3.9970941e+005 2.8054840e+006 2.8338153e+006 9.9005133e+004 1.3256045e+005 3.5126305e+004 1.3713545e+005 1.9571348e+005 + -2.7918963e+004 7.4174088e+005 7.4226613e+005 4.6880547e+004 7.8405141e+004 7.4889946e+003 7.8761992e+004 8.4215664e+004 + -2.7379750e-009 -4.2857229e-009 5.0856590e-009 -1.3306052e-007 1.9913628e-008 4.1606484e-008 4.6126480e-008 8.9954710e-012 + -1.4566204e+006 5.7153280e+006 5.8980265e+006 1.1948437e+005 1.5244255e+005 8.8085258e+004 1.7606175e+005 2.0540466e+005 + -3.5613391e+005 2.8127783e+006 2.8352343e+006 9.8505531e+004 1.3275650e+005 3.2154877e+004 1.3659513e+005 1.9329720e+005 + -2.0496738e+004 7.4425113e+005 7.4453331e+005 4.6794137e+004 7.8599148e+004 6.5544419e+003 7.8871969e+004 8.3448203e+004 + -6.4086256e-009 2.9363330e-009 7.0492931e-009 1.3818320e-007 3.1175787e-008 -4.4226169e-008 5.4109922e-008 -1.7905677e-012 + -1.3212195e+006 5.7256025e+006 5.8760655e+006 1.1862022e+005 1.5254853e+005 8.1450375e+004 1.7293125e+005 2.0048844e+005 + -3.1195231e+005 2.8194435e+006 2.8366488e+006 9.7947961e+004 1.3294472e+005 2.9131635e+004 1.3609905e+005 1.9104459e+005 + -1.3012050e+004 7.4654888e+005 7.4666231e+005 4.6683848e+004 7.8773313e+004 5.6114873e+003 7.8972930e+004 8.2733133e+004 + 4.7522128e-009 1.1027534e-009 4.8784825e-009 -2.3382609e-007 2.7667753e-008 7.2380971e-008 7.7488771e-008 -1.2178702e-011 + -1.1827593e+006 5.7358945e+006 5.8565695e+006 1.1772162e+005 1.5268589e+005 7.4615602e+004 1.6994255e+005 1.9592145e+005 + -2.6718813e+005 2.8257658e+006 2.8383695e+006 9.7374344e+004 1.3313945e+005 2.6044924e+004 1.3566302e+005 1.8895519e+005 + -5.5363257e+003 7.4862369e+005 7.4864413e+005 4.6583449e+004 7.8936461e+004 4.6641563e+003 7.9074141e+004 8.2069586e+004 + -6.7634218e-009 3.7471004e-009 7.7320523e-009 -1.3231187e-007 -1.1631143e-007 4.5549257e-008 1.2491230e-007 9.8054898e-013 + -1.0405062e+006 5.7467260e+006 5.8401635e+006 1.1680978e+005 1.5285766e+005 6.7546328e+004 1.6711664e+005 1.9169675e+005 + -2.2168498e+005 2.8320295e+006 2.8406928e+006 9.6800016e+004 1.3336161e+005 2.2874111e+004 1.3530908e+005 1.8702913e+005 + 1.9165251e+003 7.5048619e+005 7.5048863e+005 4.6490461e+004 7.9095078e+004 3.7115530e+003 7.9182117e+004 8.1460258e+004 + -1.8672162e-008 5.0911595e-009 1.9353799e-008 4.1832752e-007 3.7069896e-008 -1.3169060e-007 1.3680859e-007 6.7785777e-012 + -8.9388494e+005 5.7582595e+006 5.8272275e+006 1.1576860e+005 1.5305959e+005 6.0249801e+004 1.6449097e+005 1.8782111e+005 + -1.7496597e+005 2.8384015e+006 2.8437890e+006 9.6113539e+004 1.3361492e+005 1.9605127e+004 1.3504558e+005 1.8527303e+005 + 9.4911699e+003 7.5217150e+005 7.5223138e+005 4.6346273e+004 7.9252547e+004 2.7392585e+003 7.9299867e+004 8.0910820e+004 + -2.4646134e-008 5.7123186e-009 2.5299457e-008 2.4472433e-007 -6.2410138e-008 -7.2866364e-008 9.5940258e-008 1.8474111e-013 + -7.4266856e+005 5.7701940e+006 5.8177915e+006 1.1456694e+005 1.5327989e+005 5.2773152e+004 1.6211025e+005 1.8430931e+005 + -1.2639058e+005 2.8448695e+006 2.8476758e+006 9.5285758e+004 1.3388314e+005 1.6234767e+004 1.3486388e+005 1.8369106e+005 + 1.7446309e+004 7.5375463e+005 7.5395650e+005 4.6140633e+004 7.9412016e+004 1.7238691e+003 7.9430727e+004 8.0420750e+004 + -1.1306346e-008 7.8836937e-010 1.1333798e-008 1.8136184e-007 -3.9344314e-008 -5.3912053e-008 6.6741926e-008 3.3679726e-012 + -5.8664231e+005 5.7822905e+006 5.8119730e+006 1.1322705e+005 1.5351870e+005 4.5145621e+004 1.6001913e+005 1.8117734e+005 + -7.5557055e+004 2.8512718e+006 2.8522728e+006 9.4340602e+004 1.3416044e+005 1.2754150e+004 1.3476531e+005 1.8228584e+005 + 2.5958828e+004 7.5533113e+005 7.5577706e+005 4.5866500e+004 7.9573000e+004 6.5021387e+002 7.9575656e+004 7.9987711e+004 + -1.0988002e-008 -1.1530641e-008 1.5927707e-008 3.3992114e-007 4.9353872e-008 -1.0768395e-007 1.1845521e-007 -5.2295945e-012 + -4.2568038e+005 5.7941495e+006 5.8097650e+006 1.1174642e+005 1.5376022e+005 3.7355734e+004 1.5823292e+005 1.7843052e+005 + -2.2479926e+004 2.8575715e+006 2.8576600e+006 9.3273672e+004 1.3442814e+005 9.1623936e+003 1.3474003e+005 1.8105500e+005 + 3.5006930e+004 7.5697300e+005 7.5778200e+005 4.5530734e+004 7.9737383e+004 -4.8011081e+002 7.9738828e+004 7.9607461e+004 + -2.7494991e-008 2.9752565e-009 2.7655499e-008 1.7764394e-007 2.5513870e-008 -5.6239593e-008 6.1756367e-008 -1.8047785e-012 + -2.5979608e+005 5.8056580e+006 5.8114680e+006 1.1015441e+005 1.5399361e+005 2.9371732e+004 1.5676967e+005 1.7605830e+005 + 3.2492109e+004 2.8638103e+006 2.8639948e+006 9.2109719e+004 1.3468298e+005 5.4591836e+003 1.3479358e+005 1.7999020e+005 + 4.4435699e+004 7.5871350e+005 7.6001363e+005 4.5150270e+004 7.9907367e+004 -1.6534775e+003 7.9924477e+004 7.9275453e+004 + -2.9039334e-008 2.3885987e-009 2.9137405e-008 4.7589967e-007 -1.0903415e-007 -1.3756019e-007 1.7553134e-007 1.7138291e-011 + -8.9490250e+004 5.8168800e+006 5.8175685e+006 1.0847368e+005 1.5421416e+005 2.1196980e+004 1.5566413e+005 1.7404570e+005 + 8.8997609e+004 2.8700578e+006 2.8714373e+006 9.0865984e+004 1.3492936e+005 1.6556659e+003 1.3493952e+005 1.7908294e+005 + 5.4130605e+004 7.6053538e+005 7.6245931e+005 4.4736652e+004 8.0084125e+004 -2.8588240e+003 8.0135141e+004 7.8988641e+004 + -3.9373496e-008 7.0926403e-009 4.0007219e-008 4.0329775e-007 1.2847067e-008 -1.2396625e-007 1.2463016e-007 -1.6271429e-011 + 8.4148656e+004 5.8279265e+006 5.8285340e+006 1.0670277e+005 1.5442588e+005 1.2890531e+004 1.5496295e+005 1.7238763e+005 + 1.4681231e+005 2.8763285e+006 2.8800728e+006 8.9540391e+004 1.3517308e+005 -2.2226748e+003 1.3519134e+005 1.7832975e+005 + 6.4092273e+004 7.6238569e+005 7.6507500e+005 4.4289488e+004 8.0266047e+004 -4.0939368e+003 8.0370383e+004 7.8745828e+004 + -4.6190296e-009 9.0298391e-009 1.0142654e-008 2.8418345e-008 -1.3708387e-007 3.3232084e-009 1.3712415e-007 -1.4210855e-014 + 2.5982634e+005 5.8389540e+006 5.8447320e+006 1.0485601e+005 1.5464511e+005 4.5248447e+003 1.5471130e+005 1.7109277e+005 + 2.0565230e+005 2.8825308e+006 2.8898575e+006 8.8140656e+004 1.3541897e+005 -6.1454429e+003 1.3555834e+005 1.7773309e+005 + 7.4332266e+004 7.6420675e+005 7.6781325e+005 4.3806902e+004 8.0448625e+004 -5.3566182e+003 8.0626758e+004 7.8546914e+004 + -1.4285209e-008 6.7180048e-009 1.5786030e-008 2.2309673e-007 6.5885644e-009 -6.8138526e-008 6.8456323e-008 1.4608759e-011 + 4.3634097e+005 5.8499975e+006 5.8662480e+006 1.0294448e+005 1.5488433e+005 -3.8602356e+003 1.5493242e+005 1.7017617e+005 + 2.6499713e+005 2.8885638e+006 2.9006938e+006 8.6681578e+004 1.3566786e+005 -1.0084825e+004 1.3604217e+005 1.7729814e+005 + 8.4730461e+004 7.6595131e+005 7.7062356e+005 4.3298914e+004 8.0627242e+004 -6.6337617e+003 8.0899688e+004 7.8392188e+004 + -1.8138719e-008 -5.3862301e-009 1.8921538e-008 2.8926530e-007 -5.9289292e-008 -8.1243613e-008 1.0057706e-007 7.6170181e-012 + 6.1280738e+005 5.8610370e+006 5.8929865e+006 1.0099920e+005 1.5514847e+005 -1.2268222e+004 1.5563277e+005 1.6964536e+005 + 3.2411053e+005 2.8943518e+006 2.9124423e+006 8.5197977e+004 1.3591970e+005 -1.4020963e+004 1.3664097e+005 1.7702806e+005 + 9.5041414e+004 7.6758600e+005 7.7344756e+005 4.2787301e+004 8.0798719e+004 -7.9020098e+003 8.1184203e+004 7.8282445e+004 + 1.0755017e-009 -2.7423637e-009 2.9457192e-009 -6.9885022e-007 -4.2977092e-008 2.1469194e-007 2.1895127e-007 -1.0913936e-011 + 7.8848400e+005 5.8719360e+006 5.9246385e+006 9.9047602e+004 1.5542952e+005 -2.0703334e+004 1.5680231e+005 1.6949625e+005 + 3.8236619e+005 2.8998525e+006 2.9249528e+006 8.3720820e+004 1.3617323e+005 -1.7937281e+004 1.3734955e+005 1.7692311e+005 + 1.0505390e+005 7.6908206e+005 7.7622394e+005 4.2290844e+004 8.0961711e+004 -9.1414502e+003 8.1476156e+004 7.8219453e+004 + -2.1605969e-008 -1.8101209e-009 2.1681661e-008 -6.5260957e-007 -4.0769976e-008 2.0020308e-007 2.0431216e-007 1.7863044e-011 + 9.6262594e+005 5.8824360e+006 5.9606795e+006 9.7105461e+004 1.5571259e+005 -2.9133703e+004 1.5841459e+005 1.6972058e+005 + 4.3950850e+005 2.9050033e+006 2.9380625e+006 8.2263859e+004 1.3642416e+005 -2.1816463e+004 1.3815755e+005 1.7698313e+005 + 1.1472539e+005 7.7041300e+005 7.7890831e+005 4.1814133e+004 8.1115000e+004 -1.0346840e+004 8.1772250e+004 7.8205727e+004 + 2.0387359e-008 -2.2311806e-009 2.0509086e-008 -6.7337686e-007 3.6964806e-008 1.9665285e-007 2.0009682e-007 -2.0321522e-012 + 1.1345435e+006 5.8921735e+006 6.0004080e+006 9.5164922e+004 1.5598441e+005 -3.7498027e+004 1.6042830e+005 1.7031831e+005 + 4.9561047e+005 2.9096710e+006 2.9515785e+006 8.0814422e+004 1.3666320e+005 -2.5642582e+004 1.3904809e+005 1.7721180e+005 + 1.2415364e+005 7.7156369e+005 7.8148875e+005 4.1342461e+004 8.1256313e+004 -1.1525435e+004 8.2069633e+004 7.8244750e+004 + -3.2804817e-008 3.0669796e-009 3.2947874e-008 6.4484783e-007 2.4098441e-008 -1.9515967e-007 1.9664189e-007 -9.6633812e-013 + 1.3037851e+006 5.9006790e+006 6.0430015e+006 9.3214750e+004 1.5623108e+005 -4.5744688e+004 1.6279044e+005 1.7129641e+005 + 5.5081275e+005 2.9136825e+006 2.9652893e+006 7.9361531e+004 1.3687692e+005 -2.9405684e+004 1.3999995e+005 1.7761372e+005 + 1.3344319e+005 7.7253463e+005 7.8397506e+005 4.0869203e+004 8.1382703e+004 -1.2685415e+004 8.2365430e+004 7.8338117e+004 + -2.8462452e-008 6.7588877e-009 2.9253952e-008 5.5130465e-007 -1.1168970e-007 -1.4880807e-007 1.8606029e-007 4.3343107e-012 + 1.4703428e+006 5.9076570e+006 6.0878830e+006 9.1266758e+004 1.5645109e+005 -5.3867824e+004 1.6546506e+005 1.7266466e+005 + 6.0511600e+005 2.9168373e+006 2.9789435e+006 7.7912070e+004 1.3705427e+005 -3.3106504e+004 1.4099614e+005 1.7819361e+005 + 1.4259688e+005 7.7333938e+005 7.8637631e+005 4.0395719e+004 8.1490672e+004 -1.3826977e+004 8.2655398e+004 7.8486891e+004 + 2.4472435e-008 1.9779200e-010 2.4473234e-008 -1.9042171e-006 2.3550584e-008 5.6118262e-007 5.6167653e-007 2.0889956e-012 + 1.6344551e+006 5.9127515e+006 6.1344990e+006 8.9311016e+004 1.5663223e+005 -6.1892207e+004 1.6841705e+005 1.7442948e+005 + 6.5845431e+005 2.9190893e+006 2.9924310e+006 7.6464078e+004 1.3718527e+005 -3.6751273e+004 1.4202272e+005 1.7895458e+005 + 1.5155020e+005 7.7397425e+005 7.8867206e+005 3.9928754e+004 8.1578633e+004 -1.4945316e+004 8.2936336e+004 7.8691953e+004 + 3.2573242e-009 1.6045778e-009 3.6310921e-009 -5.3566862e-007 -7.8229604e-008 1.6994706e-007 1.8708788e-007 -8.9386276e-012 + 1.7963986e+006 5.9160760e+006 6.1827990e+006 8.7365375e+004 1.5678055e+005 -6.9836523e+004 1.7163123e+005 1.7658350e+005 + 7.1083656e+005 2.9204023e+006 3.0056678e+006 7.5024063e+004 1.3727377e+005 -4.0346824e+004 1.4308023e+005 1.7989441e+005 + 1.6028364e+005 7.7441738e+005 7.9083069e+005 3.9470215e+004 8.1645125e+004 -1.6039358e+004 8.3205695e+004 7.8953594e+004 + -1.3976059e-008 1.2627899e-008 1.8835978e-008 -4.0154387e-007 -4.2548763e-008 1.2509206e-007 1.3213031e-007 -4.5616844e-012 + 1.9562043e+006 5.9178060e+006 6.2327490e+006 8.5422203e+004 1.5689867e+005 -7.7684250e+004 1.7507722e+005 1.7911338e+005 + 7.6244594e+005 2.9208350e+006 3.0187085e+006 7.3580922e+004 1.3732777e+005 -4.3894422e+004 1.4417223e+005 1.8100836e+005 + 1.6887108e+005 7.7462481e+005 7.9281844e+005 3.9011789e+004 8.1690109e+004 -1.7115988e+004 8.3463953e+004 7.9271445e+004 + -3.0832027e-008 -9.1869179e-010 3.0845708e-008 1.3367090e-006 -4.8370424e-008 -3.8660363e-007 3.8961784e-007 1.1098678e-011 + 2.1136953e+006 5.9181880e+006 6.2843185e+006 8.3463328e+004 1.5699388e+005 -8.5397844e+004 1.7871728e+005 1.8200778e+005 + 8.1349850e+005 2.9204583e+006 3.0316425e+006 7.2117211e+004 1.3735652e+005 -4.7390762e+004 1.4530209e+005 1.8229150e+005 + 1.7741891e+005 7.7455538e+005 7.9461531e+005 3.8541012e+004 8.1713641e+004 -1.8184586e+004 8.3712594e+004 7.9644234e+004 + -3.6654658e-008 -9.5239105e-010 3.6667029e-008 9.5723379e-008 5.4626526e-009 -2.9103361e-008 2.9611588e-008 2.4868996e-012 + 2.2686448e+006 5.9174470e+006 6.3374225e+006 8.1479695e+004 1.5707666e+005 -9.2951164e+004 1.8251847e+005 1.8526245e+005 + 8.6404163e+005 2.9193213e+006 3.0445040e+006 7.0625703e+004 1.3736642e+005 -5.0831664e+004 1.4646977e+005 1.8374041e+005 + 1.8596259e+005 7.7419381e+005 7.9621488e+005 3.8052199e+004 8.1715805e+004 -1.9248178e+004 8.3952156e+004 8.0070000e+004 + -7.0588847e-008 -1.2657582e-009 7.0600194e-008 -3.9516132e-007 -5.0662109e-008 1.2497365e-007 1.3485200e-007 8.1001872e-013 + 2.4208893e+006 5.9157900e+006 6.3919700e+006 7.9483336e+004 1.5715875e+005 -1.0034666e+005 1.8646266e+005 1.8887600e+005 + 9.1388338e+005 2.9174510e+006 3.0572380e+006 6.9119641e+004 1.3736308e+005 -5.4214816e+004 1.4767484e+005 1.8535213e+005 + 1.9442870e+005 7.7355706e+005 7.9761713e+005 3.7550379e+004 8.1696945e+004 -2.0300172e+004 8.4181281e+004 8.0546617e+004 + -2.4926457e-008 8.3571594e-009 2.6290119e-008 1.5852244e-009 -3.7729023e-008 6.1749148e-009 3.8230993e-008 -1.5106139e-011 + 2.5702413e+006 5.9132500e+006 6.4476870e+006 7.7483555e+004 1.5723941e+005 -1.0759855e+005 1.9053000e+005 1.9284300e+005 + 9.6273013e+005 2.9149228e+006 3.0697923e+006 6.7606734e+004 1.3734227e+005 -5.7535418e+004 1.4890675e+005 1.8712248e+005 + 2.0269916e+005 7.7267594e+005 7.9882106e+005 3.7045457e+004 8.1659289e+004 -2.1330049e+004 8.4399109e+004 8.1072047e+004 + -6.9540540e-008 9.8340216e-009 7.0232431e-008 9.9878980e-007 -4.6311015e-008 -2.8550960e-007 2.8924114e-007 1.6186164e-011 + 2.7163558e+006 5.9099060e+006 6.5042740e+006 7.5499508e+004 1.5731592e+005 -1.1470117e+005 1.9469119e+005 1.9714611e+005 + 1.0103699e+006 2.9118115e+006 3.0821248e+006 6.6101570e+004 1.3730719e+005 -6.0785996e+004 1.5016058e+005 1.8904320e+005 + 2.1071370e+005 7.7158756e+005 7.9984225e+005 3.6541539e+004 8.1605672e+004 -2.2331803e+004 8.4606117e+004 8.1644070e+004 + -2.1259616e-008 6.0397483e-009 2.2100901e-008 2.9481956e-007 -9.1578315e-008 -6.9623212e-008 1.1503904e-007 -1.8403057e-011 + 2.8586423e+006 5.9058030e+006 6.5612760e+006 7.3540383e+004 1.5738156e+005 -1.2161567e+005 1.9889527e+005 2.0176147e+005 + 1.0567313e+006 2.9082005e+006 3.0942383e+006 6.4609391e+004 1.3726145e+005 -6.3953629e+004 1.5142911e+005 1.9110358e+005 + 2.1849920e+005 7.7032438e+005 8.0071319e+005 3.6034410e+004 8.1539023e+004 -2.3306389e+004 8.4804484e+004 8.2259891e+004 + -1.4070102e-008 -5.9605423e-009 1.5280570e-008 -1.8539140e-007 -3.4100708e-008 6.1155490e-008 7.0020370e-008 2.5153213e-012 + 2.9963403e+006 5.9007875e+006 6.6179565e+006 7.1587570e+004 1.5742336e+005 -1.2828906e+005 2.0307683e+005 2.0667134e+005 + 1.1017645e+006 2.9041625e+006 3.1061300e+006 6.3111039e+004 1.3719616e+005 -6.7019188e+004 1.5269039e+005 1.9329538e+005 + 2.2610252e+005 7.6891431e+005 8.0146838e+005 3.5517363e+004 8.1462984e+004 -2.4256547e+004 8.4997633e+004 8.2916555e+004 + -4.4389893e-008 -1.3403322e-008 4.6369298e-008 1.0521391e-006 1.0590171e-008 -3.1203911e-007 3.1221876e-007 -5.0732751e-012 + 3.1288033e+006 5.8948165e+006 6.6737000e+006 6.9661375e+004 1.5744019e+005 -1.3468823e+005 2.0719153e+005 2.1186481e+005 + 1.1452684e+006 2.8996803e+006 3.1176570e+006 6.1627398e+004 1.3711131e+005 -6.9969266e+004 1.5393248e+005 1.9561227e+005 + 2.3349741e+005 7.6739919e+005 8.0213625e+005 3.4992469e+004 8.1377766e+004 -2.5178273e+004 8.5183836e+004 8.3611188e+004 + 3.2332395e-008 -2.8861891e-009 3.2460957e-008 -6.6451543e-007 5.7661964e-010 1.9578076e-007 1.9578161e-007 2.8606451e-011 + 3.2555393e+006 5.8876995e+006 6.7278185e+006 6.7583055e+004 1.5742939e+005 -1.4081055e+005 2.1121464e+005 2.1734094e+005 + 1.1868840e+006 2.8946980e+006 3.1285733e+006 5.9978688e+004 1.3699600e+005 -7.2791695e+004 1.5513392e+005 1.9805245e+005 + 2.4057556e+005 7.6581206e+005 8.0271088e+005 3.4470504e+004 8.1283492e+004 -2.6060947e+004 8.5359117e+004 8.4342008e+004 + -3.7214402e-008 5.1335958e-009 3.7566817e-008 -2.0516029e-007 -4.7831875e-008 7.0491780e-008 8.5187907e-008 9.4644292e-012 + 3.3761150e+006 5.8795275e+006 6.7798965e+006 6.5950969e+004 1.5739588e+005 -1.4665666e+005 2.1513167e+005 2.2308884e+005 + 1.2262600e+006 2.8892113e+006 3.1386710e+006 5.8761699e+004 1.3685463e+005 -7.5476961e+004 1.5628808e+005 2.0061078e+005 + 2.4721602e+005 7.6415738e+005 8.0315144e+005 3.3963922e+004 8.1178539e+004 -2.6892600e+004 8.5517055e+004 8.5107547e+004 + -8.2167475e-008 8.5553582e-009 8.2611670e-008 3.4369043e-007 7.3686550e-009 -1.0308638e-007 1.0334940e-007 1.2391865e-011 + 3.4900800e+006 5.8702550e+006 6.8293890e+006 6.4209316e+004 1.5734041e+005 -1.5221583e+005 2.1891930e+005 2.2909850e+005 + 1.2631825e+006 2.8831785e+006 3.1477528e+006 5.7415406e+004 1.3668472e+005 -7.8013930e+004 1.5738133e+005 2.0328384e+005 + 2.5337819e+005 7.6241556e+005 8.0341650e+005 3.3477359e+004 8.1061195e+004 -2.7668541e+004 8.5653172e+004 8.5907445e+004 + -1.6560584e-008 -6.9480270e-009 1.7959065e-008 -8.3740082e-007 -9.5230916e-009 2.4973616e-007 2.4991766e-007 -3.2542857e-012 + 3.5968543e+006 5.8600600e+006 6.8758755e+006 6.2566227e+004 1.5726780e+005 -1.5744978e+005 2.2253897e+005 2.3534984e+005 + 1.2975949e+006 2.8766378e+006 3.1557563e+006 5.6143113e+004 1.3649409e+005 -8.0388859e+004 1.5840772e+005 2.0606389e+005 + 2.5908842e+005 7.6053350e+005 8.0345381e+005 3.3015629e+004 8.0930148e+004 -2.8389801e+004 8.5765203e+004 8.6739977e+004 + 9.2718926e-009 1.2118219e-008 1.5258415e-008 1.1737892e-007 -8.0950144e-009 -3.2972295e-008 3.3951459e-008 2.6858515e-012 + 3.6959880e+006 5.8491795e+006 6.9190480e+006 6.1061133e+004 1.5718872e+005 -1.6232725e+005 2.2596111e+005 2.4183028e+005 + 1.3294446e+006 2.8696220e+006 3.1626183e+006 5.4981074e+004 1.3629077e+005 -8.2591617e+004 1.5936294e+005 2.0894586e+005 + 2.6436544e+005 7.5847263e+005 8.0322463e+005 3.2596014e+004 8.0786352e+004 -2.9057256e+004 8.5853125e+004 8.7603016e+004 + -5.4834061e-008 1.8935093e-009 5.4866742e-008 -3.3050591e-007 -1.7017655e-008 1.0190965e-007 1.0332075e-007 1.5958790e-011 + 3.7873118e+006 5.8377495e+006 6.9586675e+006 5.9589770e+004 1.5710756e+005 -1.6684209e+005 2.2917041e+005 2.4853206e+005 + 1.3586103e+006 2.8621728e+006 3.1682573e+006 5.3838984e+004 1.3608072e+005 -8.4620180e+004 1.6024523e+005 2.1192967e+005 + 2.6915213e+005 7.5619163e+005 8.0266344e+005 3.2174545e+004 8.0625539e+004 -2.9665850e+004 8.5910070e+004 8.8499805e+004 + 1.5003810e-008 7.3606765e-009 1.6712088e-008 -8.6941969e-007 -7.3838919e-008 2.7544723e-007 2.8517252e-007 1.2406076e-011 + 3.8709580e+006 5.8259310e+006 6.9946970e+006 5.8119613e+004 1.5704356e+005 -1.7101344e+005 2.3218156e+005 2.5545822e+005 + 1.3849684e+006 2.8542148e+006 3.1724878e+006 5.2684910e+004 1.3586559e+005 -8.6478109e+004 1.6105255e+005 2.1501558e+005 + 2.7338353e+005 7.5369719e+005 8.0174681e+005 3.1713813e+004 8.0449734e+004 -3.0211346e+004 8.5935352e+004 8.9429211e+004 + 4.7050072e-008 -5.2962390e-009 4.7347221e-008 -1.5889675e-007 1.6620447e-009 4.6966107e-008 4.6995506e-008 -5.2438054e-012 + 3.9469685e+006 5.8138925e+006 7.0270835e+006 5.6911508e+004 1.5700513e+005 -1.7484008e+005 2.3498864e+005 2.6259778e+005 + 1.4085869e+006 2.8457918e+006 3.1753185e+006 5.1770992e+004 1.3564778e+005 -8.8165281e+004 1.6178206e+005 2.1819716e+005 + 2.7709703e+005 7.5099756e+005 8.0048738e+005 3.1425770e+004 8.0260734e+004 -3.0695818e+004 8.5930313e+004 9.0387930e+004 + -2.8304935e-008 7.8169515e-009 2.9364504e-008 6.8259220e-007 1.5128876e-008 -2.0752506e-007 2.0807579e-007 3.3111291e-012 + 4.0150978e+006 5.8016160e+006 7.0554770e+006 5.5716141e+004 1.5697733e+005 -1.7828584e+005 2.3754520e+005 2.6992147e+005 + 1.4296834e+006 2.8370015e+006 3.1768810e+006 5.0842879e+004 1.3543163e+005 -8.9675664e+004 1.6242983e+005 2.2146558e+005 + 2.8040916e+005 7.4809725e+005 7.9892356e+005 3.1083707e+004 8.0055578e+004 -3.1128650e+004 8.5894633e+004 9.1376609e+004 + -3.5521246e-008 -1.7090809e-009 3.5562337e-008 -2.0968460e-007 -4.3051145e-008 7.3133407e-008 8.4863984e-008 1.1795009e-012 + 4.0751620e+006 5.7891265e+006 7.0796135e+006 5.4594375e+004 1.5695020e+005 -1.8133189e+005 2.3982206e+005 2.7740469e+005 + 1.4483018e+006 2.8279668e+006 3.1772588e+006 4.9958938e+004 1.3521827e+005 -9.1004945e+004 1.6299042e+005 2.2480972e+005 + 2.8335644e+005 7.4502313e+005 7.9708863e+005 3.0745004e+004 7.9839250e+004 -3.1512600e+004 8.5833266e+004 9.2391016e+004 + -5.4373043e-008 -3.5795047e-009 5.4490741e-008 1.3296670e-006 -4.9927806e-008 -3.8756082e-007 3.9076357e-007 -1.1596057e-011 + 4.1272490e+006 5.7763770e+006 7.0993460e+006 5.3550941e+004 1.5692595e+005 -1.8399956e+005 2.4182967e+005 2.8504738e+005 + 1.4642373e+006 2.8186950e+006 3.1763238e+006 4.9129777e+004 1.3500069e+005 -9.2156461e+004 1.6345641e+005 2.2822838e+005 + 2.8582741e+005 7.4185056e+005 7.9500913e+005 3.0414963e+004 7.9615180e+004 -3.1838900e+004 8.5745508e+004 9.3429938e+004 + -1.3088868e-007 1.5385883e-008 1.3178988e-007 6.9784852e-007 3.5368402e-009 -2.1129225e-007 2.1132185e-007 -1.3343993e-011 + 4.1715018e+006 5.7633175e+006 7.1145805e+006 5.2431895e+004 1.5690809e+005 -1.8631747e+005 2.4358644e+005 2.9285338e+005 + 1.4772686e+006 2.8091470e+006 3.1738983e+006 4.8216539e+004 1.3477456e+005 -9.3134906e+004 1.6382397e+005 2.3172728e+005 + 2.8770334e+005 7.3863313e+005 7.9268663e+005 3.0026699e+004 7.9381914e+004 -3.2097488e+004 8.5625563e+004 9.4499289e+004 + -2.5958103e-008 -3.5199150e-009 2.6195666e-008 1.0887020e-007 -7.4997217e-009 -3.1088632e-008 3.1980445e-008 1.7621460e-012 + 4.2076780e+006 5.7498640e+006 7.1249905e+006 5.1438391e+004 1.5689080e+005 -1.8826953e+005 2.4507170e+005 3.0079409e+005 + 1.4874059e+006 2.7993360e+006 3.1699620e+006 4.7407082e+004 1.3453550e+005 -9.3932422e+004 1.6408261e+005 2.3528669e+005 + 2.8902978e+005 7.3538013e+005 7.9014056e+005 2.9681209e+004 7.9140438e+004 -3.2290527e+004 8.5474484e+004 9.5586914e+004 + -1.2456147e-009 1.2673844e-008 1.2734908e-008 -1.4816821e-007 -8.7093383e-008 6.6650131e-008 1.0966995e-007 -4.1922021e-012 + 4.2356305e+006 5.7358915e+006 7.1302885e+006 5.0564754e+004 1.5685191e+005 -1.8982881e+005 2.4624681e+005 3.0882388e+005 + 1.4948230e+006 2.7893498e+006 3.1646435e+006 4.6687801e+004 1.3428536e+005 -9.4545523e+004 1.6422977e+005 2.3888911e+005 + 2.8989528e+005 7.3207394e+005 7.8738269e+005 2.9366572e+004 7.8894688e+004 -3.2425807e+004 8.5298328e+004 9.6689328e+004 + -1.0059883e-007 -2.0849189e-010 1.0059904e-007 1.5264395e-006 -5.2832490e-008 -4.5194076e-007 4.5501838e-007 6.5369932e-013 + 4.2553060e+006 5.7211595e+006 7.1301680e+006 4.9799168e+004 1.5676763e+005 -1.9097447e+005 2.4707758e+005 3.1689859e+005 + 1.4996814e+006 2.7792348e+006 3.1580358e+006 4.6043758e+004 1.3402113e+005 -9.4973352e+004 1.6426077e+005 2.4251633e+005 + 2.9036975e+005 7.2870419e+005 7.8442613e+005 2.9071469e+004 7.8647117e+004 -3.2509775e+004 8.5101438e+004 9.7801898e+004 + -3.7959687e-008 -2.6278411e-009 3.8050537e-008 -7.4332894e-007 -9.2168051e-009 2.2970696e-007 2.2989178e-007 1.3926638e-011 + 4.2667255e+006 5.7053185e+006 7.1242970e+006 4.9136641e+004 1.5662970e+005 -1.9170261e+005 2.4755353e+005 3.2500581e+005 + 1.5020099e+006 2.7688520e+006 3.1500120e+006 4.5472645e+004 1.3373100e+005 -9.5216750e+004 1.6416519e+005 2.4616223e+005 + 2.9046156e+005 7.2528713e+005 7.8128694e+005 2.8794514e+004 7.8395266e+004 -3.2543189e+004 8.4881547e+004 9.8922102e+004 + -3.2744452e-008 2.0485942e-009 3.2808472e-008 -1.5022533e-007 -6.3284659e-008 6.2132301e-008 8.8686924e-008 1.2732926e-011 + 4.2700365e+006 5.6879880e+006 7.1124130e+006 4.8522203e+004 1.5644395e+005 -1.9203038e+005 2.4769008e+005 3.3316125e+005 + 1.5017208e+006 2.7579278e+006 3.1402755e+006 4.4930387e+004 1.3339833e+005 -9.5279320e+004 1.6393066e+005 2.4983367e+005 + 2.9011850e+005 7.2183981e+005 7.7795981e+005 2.8517756e+004 7.8131031e+004 -3.2521381e+004 8.4629180e+004 1.0005235e+005 + 8.3581533e-009 -3.4139855e-009 9.0285113e-009 -3.3174427e-007 -5.8337363e-008 1.1709758e-007 1.3082467e-007 1.9468871e-012 + 4.2654115e+006 5.6689560e+006 7.0944200e+006 4.7910414e+004 1.5622130e+005 -1.9197775e+005 2.4750869e+005 3.4137144e+005 + 1.4987384e+006 2.7462060e+006 3.1285563e+006 4.4377637e+004 1.3301408e+005 -9.5164805e+004 1.6355148e+005 2.5353238e+005 + 2.8929825e+005 7.1834319e+005 7.7440975e+005 2.8232201e+004 7.7845367e+004 -3.2440053e+004 8.4334211e+004 1.0119280e+005 + 3.4378527e-008 -2.6722615e-009 3.4482227e-008 -1.7747884e-006 -4.8753279e-008 5.6212281e-007 5.6423306e-007 9.4644292e-012 + 4.2530135e+006 5.6482210e+006 7.0703980e+006 4.7450457e+004 1.5596245e+005 -1.9155400e+005 2.4701664e+005 3.4960331e+005 + 1.4931059e+006 2.7336245e+006 3.1148143e+006 4.3952191e+004 1.3258063e+005 -9.4876492e+004 1.6303119e+005 2.5724003e+005 + 2.8800713e+005 7.1474069e+005 7.7058575e+005 2.8005203e+004 7.7537555e+004 -3.2300193e+004 8.3996281e+004 1.0233496e+005 + -4.2654587e-008 3.5929499e-009 4.2805642e-008 1.4500404e-008 2.1786903e-008 -1.0050741e-008 2.3993470e-008 1.9397817e-011 + 4.2328945e+006 5.6259895e+006 7.0405365e+006 4.7121367e+004 1.5565700e+005 -1.9074720e+005 2.4619828e+005 3.5780413e+005 + 1.4850153e+006 2.7203300e+006 3.0992685e+006 4.3626242e+004 1.3211366e+005 -9.4415742e+004 1.6238334e+005 2.6093647e+005 + 2.8631606e+005 7.1092975e+005 7.6641894e+005 2.7812002e+004 7.7208273e+004 -3.2108785e+004 8.3618727e+004 1.0347563e+005 + 3.3617187e-008 5.2007465e-010 3.3621209e-008 -4.3370750e-007 2.8151931e-008 1.2849641e-007 1.3154413e-007 -4.9737992e-013 + 4.2049990e+006 5.6024190e+006 7.0049350e+006 4.6904676e+004 1.5529608e+005 -1.8953978e+005 2.4503509e+005 3.6593016e+005 + 1.4746238e+006 2.7064505e+006 3.0821080e+006 4.3376129e+004 1.3162488e+005 -9.3781195e+004 1.6161689e+005 2.6460391e+005 + 2.8429413e+005 7.0683688e+005 7.6186713e+005 2.7637563e+004 7.6859250e+004 -3.1872285e+004 8.3205695e+004 1.0461073e+005 + 4.7877410e-008 -7.9480849e-009 4.8532655e-008 -1.4244058e-006 -1.7717213e-008 4.5244346e-007 4.5279023e-007 2.9871217e-011 + 4.1693295e+006 5.5773805e+006 6.9635110e+006 4.6790250e+004 1.5488002e+005 -1.8793464e+005 2.4353080e+005 3.7397166e+005 + 1.4618843e+006 2.6919110e+006 3.0632485e+006 4.3194328e+004 1.3110930e+005 -9.2973336e+004 1.6072863e+005 2.6823706e+005 + 2.8191475e+005 7.0245831e+005 7.5691713e+005 2.7479279e+004 7.6488078e+004 -3.1588535e+004 8.2754227e+004 1.0573809e+005 + 8.0209883e-010 1.6410056e-009 1.8265437e-009 -7.2091785e-007 -1.6768738e-008 2.3228895e-007 2.3289343e-007 -9.9191766e-012 + 4.1259538e+006 5.5505305e+006 6.9160600e+006 4.6778574e+004 1.5441570e+005 -1.8595255e+005 2.4170759e+005 3.8194009e+005 + 1.4466025e+006 2.6764683e+006 3.0423908e+006 4.3090566e+004 1.3054965e+005 -9.1994063e+004 1.5970634e+005 2.7183897e+005 + 2.7908597e+005 6.9784581e+005 7.5158350e+005 2.7345199e+004 7.6089398e+004 -3.1249221e+004 8.2256375e+004 1.0685737e+005 + -7.4652199e-008 -3.5481289e-009 7.4736469e-008 -1.7449088e-006 5.8766716e-009 5.5391791e-007 5.5394912e-007 2.2708946e-011 + 4.0748835e+006 5.5215450e+006 6.8623710e+006 4.6870250e+004 1.5390608e+005 -1.8360730e+005 2.3958030e+005 3.8983825e+005 + 1.4286433e+006 2.6599203e+006 3.0193040e+006 4.3070836e+004 1.2993030e+005 -9.0843148e+004 1.5853819e+005 2.7540988e+005 + 2.7575294e+005 6.9305219e+005 7.4589613e+005 2.7240432e+004 7.5659102e+004 -3.0848961e+004 8.1706539e+004 1.0796823e+005 + -1.1165940e-007 3.1964316e-009 1.1170515e-007 1.5978018e-006 -4.0643101e-008 -5.0175339e-007 5.0339679e-007 8.9528385e-013 + 4.0159738e+006 5.4903720e+006 6.8023695e+006 4.7059840e+004 1.5334666e+005 -1.8088884e+005 2.3714125e+005 3.9764413e+005 + 1.4080380e+006 2.6422728e+006 2.9940233e+006 4.3127117e+004 1.2924962e+005 -8.9515984e+004 1.5722142e+005 2.7894069e+005 + 2.7195150e+005 6.8808650e+005 7.3987881e+005 2.7159580e+004 7.5197281e+004 -3.0390111e+004 8.1106039e+004 1.0906860e+005 + 4.7709207e-008 5.7036784e-009 4.8048939e-008 -6.6069339e-007 5.5049682e-008 1.9942796e-007 2.0688638e-007 5.5848659e-012 + 3.9489603e+006 5.4572960e+006 6.7361985e+006 4.7339277e+004 1.5273386e+005 -1.7777342e+005 2.3437369e+005 4.0532181e+005 + 1.3848744e+006 2.6237218e+006 2.9667815e+006 4.3245488e+004 1.2852029e+005 -8.8005398e+004 1.5576397e+005 2.8241684e+005 + 2.6774856e+005 6.8291913e+005 7.3353106e+005 2.7092703e+004 7.4707711e+004 -2.9877949e+004 8.0460758e+004 1.1015479e+005 + 9.0334140e-010 7.0833091e-009 7.1406787e-009 -1.5585729e-006 -4.4420787e-008 5.1783422e-007 5.1973598e-007 1.1297629e-011 + 3.8736985e+006 5.4228535e+006 6.6642990e+006 4.7702063e+004 1.5207897e+005 -1.7425570e+005 2.3128567e+005 4.1284703e+005 + 1.3590943e+006 2.6045023e+006 2.9377830e+006 4.3420160e+004 1.2776057e+005 -8.6308328e+004 1.5418136e+005 2.8582784e+005 + 2.6312688e+005 6.7752225e+005 7.2682331e+005 2.7037273e+004 7.4194328e+004 -2.9310895e+004 7.9774219e+004 1.1122348e+005 + -1.1869476e-008 7.6476558e-009 1.4119883e-008 5.8522926e-007 -2.0379034e-008 -1.8677294e-007 1.8788144e-007 -3.2827074e-012 + 3.7902300e+006 5.3876600e+006 6.5873155e+006 4.8144324e+004 1.5140931e+005 -1.7035420e+005 2.2791519e+005 4.2021694e+005 + 1.3304971e+006 2.5847795e+006 2.9071133e+006 4.3655777e+004 1.2698628e+005 -8.4426828e+004 1.5249067e+005 2.8917172e+005 + 2.5798527e+005 6.7189806e+005 7.1972450e+005 2.6999971e+004 7.3659469e+004 -2.8680393e+004 7.9046078e+004 1.1227354e+005 + -1.6360611e-008 -6.5093175e-009 1.7607976e-008 5.1746235e-007 1.7637333e-008 -1.7533149e-007 1.7621636e-007 -9.2370556e-013 + 3.6987363e+006 5.3523185e+006 6.5059940e+006 4.8661238e+004 1.5075667e+005 -1.6609292e+005 2.2430878e+005 4.2743775e+005 + 1.2989464e+006 2.5646795e+006 2.8748640e+006 4.3956785e+004 1.2621039e+005 -8.2365875e+004 1.5070900e+005 2.9245081e+005 + 2.5224188e+005 6.6607319e+005 7.1223550e+005 2.6987148e+004 7.3104813e+004 -2.7979660e+004 7.8276273e+004 1.1330564e+005 + -4.9938883e-008 -8.8077812e-010 4.9946649e-008 6.8548786e-007 -1.2354832e-008 -2.2552848e-007 2.2586664e-007 -4.9311666e-012 + 3.5994725e+006 5.3173655e+006 6.4211040e+006 4.9244840e+004 1.5014039e+005 -1.6148277e+005 2.2049677e+005 4.3450209e+005 + 1.2645489e+006 2.5443943e+006 2.8413073e+006 4.4316828e+004 1.2544618e+005 -8.0130914e+004 1.4885466e+005 2.9566256e+005 + 2.4592283e+005 6.6008163e+005 7.0440456e+005 2.6995930e+004 7.2534625e+004 -2.7211424e+004 7.7470859e+004 1.1431983e+005 + 3.7768579e-008 9.2190389e-009 3.8877452e-008 -1.4707348e-006 -1.6813431e-008 4.9875365e-007 4.9903696e-007 1.3073986e-011 + 3.4927733e+006 5.2831680e+006 6.3333505e+006 4.9963719e+004 1.4955569e+005 -1.5652411e+005 2.1648719e+005 4.4137559e+005 + 1.2276210e+006 2.5242400e+006 2.8069273e+006 4.4795012e+004 1.2470469e+005 -7.7728359e+004 1.4694542e+005 2.9879063e+005 + 2.3914222e+005 6.5397419e+005 6.9632694e+005 2.7057504e+004 7.1960047e+004 -2.6386656e+004 7.6645313e+004 1.1530982e+005 + -6.6865418e-008 -4.0683341e-009 6.6989074e-008 1.8128915e-007 -1.0428082e-008 -5.9084940e-008 5.9998122e-008 4.8316906e-013 + 3.3792310e+006 5.2500395e+006 6.2435660e+006 5.0746203e+004 1.4898998e+005 -1.5123252e+005 2.1229528e+005 4.4801941e+005 + 1.1885020e+006 2.5045615e+006 2.7722490e+006 4.5315480e+004 1.2399732e+005 -7.5171883e+004 1.4500395e+005 3.0182159e+005 + 2.3199275e+005 6.4782369e+005 6.8811056e+005 2.7124592e+004 7.1391047e+004 -2.5514936e+004 7.5813547e+004 1.1627444e+005 + -5.8783067e-010 -1.6216841e-009 1.7249361e-009 6.6215955e-007 -3.6357335e-009 -2.2605430e-007 2.2608353e-007 -8.1286089e-012 + 3.2595470e+006 5.2179975e+006 6.1524100e+006 5.1558051e+004 1.4842870e+005 -1.4563638e+005 2.0794478e+005 4.5440563e+005 + 1.1474420e+006 2.4855740e+006 2.7376453e+006 4.5846125e+004 1.2332195e+005 -7.2478180e+004 1.4304331e+005 3.0474334e+005 + 2.2451466e+005 6.4174619e+005 6.7988600e+005 2.7179797e+004 7.0837734e+004 -2.4601857e+004 7.4988242e+004 1.1720999e+005 + -2.1686837e-008 -2.9614426e-009 2.1888102e-008 -6.0268633e-008 1.9476005e-008 1.6399866e-008 2.5461155e-008 -1.3457679e-011 + 3.1343935e+006 5.1868200e+006 6.0603235e+006 5.2397789e+004 1.4786584e+005 -1.3976473e+005 2.0346619e+005 4.6053006e+005 + 1.1046381e+006 2.4672723e+006 2.7032680e+006 4.6393055e+004 1.2266459e+005 -6.9663180e+004 1.4106581e+005 3.0755184e+005 + 2.1673442e+005 6.3587869e+005 6.7180019e+005 2.7228803e+004 7.0304180e+004 -2.3650865e+004 7.4175742e+004 1.1811359e+005 + -3.9135159e-008 7.9460243e-009 3.9933695e-008 -3.9946053e-007 -3.7260179e-008 1.4775986e-007 1.5238535e-007 5.4996008e-012 + 3.0043583e+006 5.1562695e+006 5.9676865e+006 5.3258590e+004 1.4730322e+005 -1.3363675e+005 1.9888947e+005 4.6639900e+005 + 1.0603378e+006 2.4495538e+006 2.6692003e+006 4.6957406e+004 1.2201035e+005 -6.6740250e+004 1.3907116e+005 3.1024769e+005 + 2.0871477e+005 6.3033056e+005 6.6398681e+005 2.7275246e+004 6.9790734e+004 -2.2668107e+004 7.3379766e+004 1.1898382e+005 + -4.7648193e-008 -1.3322410e-009 4.7666816e-008 6.8546171e-007 1.3752839e-009 -2.4167173e-007 2.4167565e-007 -7.7875484e-012 + 2.8699228e+006 5.1263400e+006 5.8750165e+006 5.4134051e+004 1.4674355e+005 -1.2726236e+005 1.9424052e+005 4.7200609e+005 + 1.0148488e+006 2.4324073e+006 2.6356258e+006 4.7534234e+004 1.2135645e+005 -6.3719523e+004 1.3706773e+005 3.1282650e+005 + 2.0055803e+005 6.2514138e+005 6.5652519e+005 2.7318400e+004 6.9297820e+004 -2.1663100e+004 7.2604945e+004 1.1981820e+005 + -8.6369297e-008 5.4205449e-009 8.6539231e-008 -1.2128030e-008 -7.5884188e-008 2.0549876e-008 7.8617475e-008 3.2116532e-012 + 2.7314763e+006 5.0973515e+006 5.7830750e+006 5.5021359e+004 1.4619061e+005 -1.2065142e+005 1.8954805e+005 4.7732550e+005 + 9.6842563e+005 2.4159845e+006 2.6028503e+006 4.8116844e+004 1.2071575e+005 -6.0609191e+004 1.3507689e+005 3.1527572e+005 + 1.9234530e+005 6.2027775e+005 6.4941606e+005 2.7356447e+004 6.8827609e+004 -2.0643768e+004 7.1856836e+004 1.2061217e+005 + 6.0416113e-008 3.5432581e-009 6.0519923e-008 -2.0450938e-007 -2.4397863e-008 7.8331297e-008 8.2042959e-008 2.0619950e-011 + 2.5893005e+006 5.0698140e+006 5.6927580e+006 5.5923137e+004 1.4565217e+005 -1.1381971e+005 1.8484989e+005 4.8232650e+005 + 9.2115438e+005 2.4005098e+006 2.5711813e+006 4.8705418e+004 1.2010902e+005 -5.7415926e+004 1.3312688e+005 3.1758063e+005 + 1.8408402e+005 6.1567006e+005 6.4260144e+005 2.7392139e+004 6.8382820e+004 -1.9611766e+004 7.1139523e+004 1.2136055e+005 + 5.7699459e-009 -2.6806726e-009 6.3622543e-009 -5.0084344e-007 3.1141099e-009 1.8016655e-007 1.8019347e-007 -6.4659389e-012 + 2.4435568e+006 5.0441605e+006 5.6048660e+006 5.6841305e+004 1.4514122e+005 -1.0678640e+005 1.8019242e+005 4.8699250e+005 + 8.7295594e+005 2.3861235e+006 2.5407945e+006 4.9305129e+004 1.1955227e+005 -5.4144324e+004 1.3124158e+005 3.1973253e+005 + 1.7571556e+005 6.1126038e+005 6.3601506e+005 2.7433168e+004 6.7964438e+004 -1.8562803e+004 7.0453828e+004 1.2206002e+005 + -1.6954678e-008 2.5175639e-009 1.7140573e-008 -3.9308921e-007 5.5077436e-009 1.4191093e-007 1.4201778e-007 2.6375346e-011 + 2.2943353e+006 5.0205380e+006 5.5199435e+006 5.7776387e+004 1.4467013e+005 -9.9567938e+004 1.7562238e+005 4.9132422e+005 + 8.2370719e+005 2.3728113e+006 2.5117180e+006 4.9924418e+004 1.1904895e+005 -5.0797371e+004 1.2943348e+005 3.2173047e+005 + 1.6717045e+005 6.0702975e+005 6.2962775e+005 2.7488674e+004 6.7570914e+004 -1.7491234e+004 6.9798078e+004 1.2270980e+005 + 1.2339199e-009 1.0731323e-008 1.0802030e-008 -3.4705315e-007 -1.0520955e-007 1.4728434e-007 1.8100201e-007 -9.1091579e-012 + 2.1416865e+006 4.9987340e+006 5.4382130e+006 5.8725094e+004 1.4423791e+005 -9.2172102e+004 1.7117322e+005 4.9532519e+005 + 7.7338438e+005 2.3604600e+006 2.4839273e+006 5.0564297e+004 1.1859053e+005 -4.7375848e+004 1.2770351e+005 3.2357531e+005 + 1.5843675e+005 6.0299481e+005 6.2346206e+005 2.7562777e+004 6.7200398e+004 -1.6396057e+004 6.9171703e+004 1.2331015e+005 + -1.0337059e-008 1.6326496e-011 1.0337072e-008 3.0642212e-007 3.3594816e-008 -1.1930398e-007 1.2394374e-007 -7.8159701e-013 + 1.9856834e+006 4.9782445e+006 5.3596510e+006 5.9683266e+004 1.4382398e+005 -8.4601367e+004 1.6686141e+005 4.9898694e+005 + 7.2208556e+005 2.3489570e+006 2.4574390e+006 5.1218590e+004 1.1816127e+005 -4.3879922e+004 1.2604575e+005 3.2526350e+005 + 1.4956003e+005 5.9919213e+005 6.1757544e+005 2.7653174e+004 6.6852695e+004 -1.5281178e+004 6.8576945e+004 1.2386025e+005 + -2.9091686e-008 5.7340817e-009 2.9651405e-008 1.8060152e-007 1.2332293e-008 -6.9313899e-008 7.0402429e-008 1.7791990e-011 + 1.8264499e+006 4.9583840e+006 5.2840790e+006 6.0638957e+004 1.4339325e+005 -7.6860672e+004 1.6269353e+005 5.0228881e+005 + 6.6993519e+005 2.3382108e+006 2.4322918e+006 5.1870164e+004 1.1774148e+005 -4.0310688e+004 1.2445083e+005 3.2678666e+005 + 1.4059464e+005 5.9567013e+005 6.1203738e+005 2.7752721e+004 6.6529977e+004 -1.4151512e+004 6.8018406e+004 1.2435793e+005 + 2.6069257e-008 2.0441480e-009 2.6149277e-008 -3.6594497e-007 -1.4639923e-008 1.3940245e-007 1.4016908e-007 -5.8548721e-012 + 1.6641399e+006 4.9383275e+006 5.2111840e+006 6.1566160e+004 1.4290623e+005 -6.8962023e+004 1.5867563e+005 5.0521116e+005 + 6.1699081e+005 2.3280905e+006 2.4084608e+006 5.2490730e+004 1.1730646e+005 -3.6670059e+004 1.2290443e+005 3.2813722e+005 + 1.3155975e+005 5.9248438e+005 6.0691488e+005 2.7850516e+004 6.6233820e+004 -1.3008952e+004 6.7499273e+004 1.2480120e+005 + -2.4929577e-008 -1.1859864e-009 2.4957771e-008 -1.4474837e-007 1.0739910e-008 5.2735423e-008 5.3817942e-008 1.5859314e-011 + 1.4989420e+006 4.9172015e+006 5.1405930e+006 6.2406934e+004 1.4233409e+005 -6.0922980e+004 1.5482442e+005 5.0774956e+005 + 5.6323325e+005 2.3183303e+006 2.3857675e+006 5.3029574e+004 1.1682696e+005 -3.2961266e+004 1.2138774e+005 3.2931450e+005 + 1.2243842e+005 5.8968950e+005 6.0226644e+005 2.7926428e+004 6.5962109e+004 -1.1852149e+004 6.7018453e+004 1.2519055e+005 + -4.2432578e-008 6.4484889e-009 4.2919769e-008 1.2134848e-007 -7.2064793e-008 -3.4713011e-008 7.9989547e-008 -2.9132252e-012 + 1.3310275e+006 4.8941160e+006 5.0718840e+006 6.3060410e+004 1.4166009e+005 -5.2757813e+004 1.5116538e+005 5.0991444e+005 + 5.0865659e+005 2.3085053e+006 2.3638800e+006 5.3398535e+004 1.1627187e+005 -2.9186527e+004 1.1987911e+005 3.3032525e+005 + 1.1322891e+005 5.8731013e+005 5.9812538e+005 2.7942193e+004 6.5706906e+004 -1.0680646e+004 6.6569313e+004 1.2552979e+005 + -4.0682053e-008 4.5997357e-009 4.0941263e-008 2.5152221e-007 1.2598122e-008 -9.7649078e-008 9.8458393e-008 -2.1742608e-012 + 1.1605350e+006 4.8682140e+006 5.0046330e+006 6.3372004e+004 1.4087630e+005 -4.4473512e+004 1.4772956e+005 5.1171906e+005 + 4.5331241e+005 2.2980598e+006 2.3423430e+006 5.3457027e+004 1.1561357e+005 -2.5347229e+004 1.1835953e+005 3.3117838e+005 + 1.0396022e+005 5.8529544e+005 5.9445650e+005 2.7827160e+004 6.5453422e+004 -9.4965156e+003 6.6138750e+004 1.2582509e+005 + 1.8085162e-008 -4.0633363e-010 1.8089727e-008 1.8887301e-008 3.9258268e-008 -1.2735150e-008 4.1272212e-008 -1.5276669e-011 + 9.8760969e+005 4.8387090e+006 4.9384690e+006 6.3173535e+004 1.3997902e+005 -3.6079188e+004 1.4455392e+005 5.1316500e+005 + 3.9721556e+005 2.2863178e+006 2.3205665e+006 5.3048344e+004 1.1483463e+005 -2.1446875e+004 1.1682021e+005 3.3187719e+005 + 9.4632297e+004 5.8346956e+005 5.9109394e+005 2.7484922e+004 6.5178625e+004 -8.3000078e+003 6.5704977e+004 1.2608195e+005 + -1.8364661e-008 5.3127902e-010 1.8372344e-008 3.2235840e-007 3.4757363e-008 -1.2871995e-007 1.3333003e-007 2.8180125e-011 + 8.1240663e+005 4.8049720e+006 4.8731675e+006 6.2642316e+004 1.3897103e+005 -2.7596719e+004 1.4168461e+005 5.1422506e+005 + 3.4020244e+005 2.2725613e+006 2.2978843e+006 5.2328598e+004 1.1392886e+005 -1.7491648e+004 1.1526380e+005 3.3240188e+005 + 8.5136648e+004 5.8149281e+005 5.8769219e+005 2.6986869e+004 6.4855988e+004 -7.0835601e+003 6.5241672e+004 1.2629011e+005 + 1.4663311e-009 4.8673279e-009 5.0834053e-009 -1.2884315e-007 -5.9561472e-008 5.7288084e-008 8.2640753e-008 3.2684966e-013 + 6.3513931e+005 4.7671595e+006 4.8092840e+006 6.1953035e+004 1.3788136e+005 -1.9067016e+004 1.3919345e+005 5.1486494e+005 + 2.8194106e+005 2.2562495e+006 2.2737970e+006 5.1468832e+004 1.1292789e+005 -1.3492313e+004 1.1373105e+005 3.3272697e+005 + 7.5251664e+004 5.7877900e+005 5.8365050e+005 2.6424240e+004 6.4444723e+004 -5.8296489e+003 6.4707859e+004 1.2643399e+005 + -1.7922325e-008 7.7673148e-009 1.9533072e-008 3.4217550e-007 -5.2978876e-008 -1.2748653e-007 1.3805642e-007 -4.4053650e-013 + 4.5589291e+005 4.7270350e+006 4.7489680e+006 6.2041895e+004 1.3680088e+005 -1.0514382e+004 1.3720434e+005 5.1503394e+005 + 2.2222330e+005 2.2373308e+006 2.2483400e+006 5.1362223e+004 1.1190833e+005 -9.4503721e+003 1.1230665e+005 3.3280638e+005 + 6.4919984e+004 5.7476350e+005 5.7841825e+005 2.6343789e+004 6.3905578e+004 -4.5262729e+003 6.4065672e+004 1.2647440e+005 + -1.3520808e-008 -3.3628096e-009 1.3932723e-008 -3.3893451e-007 2.3731332e-008 1.3102053e-007 1.3315237e-007 2.7000624e-013 + 2.7507328e+005 4.6867850e+006 4.6948505e+006 6.4187887e+004 1.3579631e+005 -1.9355719e+003 1.3581011e+005 5.1472678e+005 + 1.6140692e+005 2.2166745e+006 2.2225433e+006 5.3206805e+004 1.1092953e+005 -5.3719185e+003 1.1105952e+005 3.3262988e+005 + 5.4328313e+004 5.6950594e+005 5.7209144e+005 2.7408621e+004 6.3261145e+004 -3.1847014e+003 6.3341254e+004 1.2639526e+005 + 7.2206414e-009 1.3913182e-009 7.3534636e-009 -3.0649286e-007 2.1517380e-008 1.1998875e-007 1.2190283e-007 1.9667823e-011 + 9.4433219e+004 4.6489140e+006 4.6498730e+006 6.7601734e+004 1.3490747e+005 6.6308018e+003 1.3507033e+005 5.1406563e+005 + 1.0047254e+005 2.1960708e+006 2.1983678e+006 5.6204773e+004 1.1003465e+005 -1.3024308e+003 1.1004236e+005 3.3230744e+005 + 4.3648238e+004 5.6371981e+005 5.6540713e+005 2.8977684e+004 6.2589977e+004 -1.8347250e+003 6.2616863e+004 1.2627910e+005 + 1.1963142e-008 6.0773964e-010 1.1978568e-008 -7.2182424e-008 1.0788668e-008 2.8137741e-008 3.0135158e-008 1.0160761e-011 + -8.3800250e+004 4.6173065e+006 4.6180670e+006 7.1262977e+004 1.3421600e+005 1.5121382e+004 1.3506514e+005 5.1311459e+005 + 4.0509652e+004 2.1778573e+006 2.1782340e+006 5.9343938e+004 1.0930309e+005 2.7031121e+003 1.0933652e+005 3.3189731e+005 + 3.3089988e+004 5.5823350e+005 5.5921338e+005 3.0401730e+004 6.1980336e+004 -5.0855951e+002 6.1982422e+004 1.2616863e+005 + -7.2360085e-010 -1.7635351e-009 1.9062147e-009 -6.1030136e-008 1.4392896e-008 2.3731687e-008 2.7755151e-008 3.1121772e-012 + -2.5779445e+005 4.5962085e+006 4.6034325e+006 7.4489516e+004 1.3382472e+005 2.3468830e+004 1.3586700e+005 5.1186166e+005 + -1.7724371e+004 2.1642385e+006 2.1643110e+006 6.1991012e+004 1.0884046e+005 6.6025015e+003 1.0904054e+005 3.3139113e+005 + 2.2826002e+004 5.5355481e+005 5.5402525e+005 3.1421029e+004 6.1496457e+004 7.7343042e+002 6.1501320e+004 1.2605702e+005 + -1.1528046e-008 8.1843723e-009 1.4137885e-008 -1.6382275e-007 -6.7812806e-008 7.2003225e-008 9.8909254e-008 7.4322770e-012 + -4.2647966e+005 4.5880585e+006 4.6078375e+006 7.7151039e+004 1.3377806e+005 3.1631107e+004 1.3746672e+005 5.1026584e+005 + -7.3765719e+004 2.1567145e+006 2.1579758e+006 6.4073789e+004 1.0870848e+005 1.0373180e+004 1.0920227e+005 3.3075084e+005 + 1.2995375e+004 5.5001400e+005 5.5016750e+005 3.2135262e+004 6.1180184e+004 1.9992084e+003 6.1212840e+004 1.2591616e+005 + -1.5151405e-008 -1.0757351e-009 1.5189546e-008 1.6858542e-007 -3.7946307e-008 -6.7502803e-008 7.7437399e-008 -1.1141310e-011 + -5.8930906e+005 4.5930660e+006 4.6307170e+006 7.9281336e+004 1.3405869e+005 3.9598895e+004 1.3978484e+005 5.0830363e+005 + -1.2726105e+005 2.1557825e+006 2.1595355e+006 6.5646469e+004 1.0890295e+005 1.4002102e+004 1.0979941e+005 3.2995250e+005 + 3.6977095e+003 5.4791525e+005 5.4792769e+005 3.2580852e+004 6.1052281e+004 3.1584578e+003 6.1133926e+004 1.2573148e+005 + -4.4208770e-009 6.1587935e-009 7.5812192e-009 6.3573147e-008 -5.3274917e-008 -2.3700444e-008 5.8308903e-008 1.9738877e-011 + -7.4618625e+005 4.6096385e+006 4.6696425e+006 8.1032477e+004 1.3460200e+005 4.7371227e+004 1.4269453e+005 5.0596563e+005 + -1.7801794e+005 2.1611535e+006 2.1684730e+006 6.6866805e+004 1.0936642e+005 1.7490967e+004 1.1075626e+005 3.2897828e+005 + -4.9514473e+003 5.4754681e+005 5.4756919e+005 3.2854910e+004 6.1115555e+004 4.2433618e+003 6.1262688e+004 1.2548838e+005 + 1.8274269e-009 1.0258830e-008 1.0420321e-008 1.6269145e-007 -9.0042661e-008 -6.3815754e-008 1.1036363e-007 2.4016344e-011 + -8.9732569e+005 4.6352865e+006 4.7213425e+006 8.2558531e+004 1.3531869e+005 5.4944828e+004 1.4604822e+005 5.0324641e+005 + -2.2611809e+005 2.1720990e+006 2.1838370e+006 6.7886828e+004 1.1001913e+005 2.0848537e+004 1.1197710e+005 3.2781653e+005 + -1.2929668e+004 5.4910569e+005 5.4925788e+005 3.3037164e+004 6.1360762e+004 5.2550444e+003 6.1585375e+004 1.2517648e+005 + 3.8062828e-009 3.8454937e-009 5.4106941e-009 1.9156985e-008 1.1087607e-008 -8.3310354e-009 1.3868713e-008 -6.9348971e-012 + -1.0432889e+006 4.6675770e+006 4.7827530e+006 8.3997195e+004 1.3612673e+005 6.2320207e+004 1.4971405e+005 5.0013344e+005 + -2.7193428e+005 2.1877623e+006 2.2045980e+006 6.8827969e+004 1.1078977e+005 2.4090631e+004 1.1337870e+005 3.2645666e+005 + -2.0321857e+004 5.5262281e+005 5.5299631e+005 3.3187012e+004 6.1771168e+004 6.2041299e+003 6.2081949e+004 1.2478851e+005 + -3.8558876e-009 -1.7225137e-009 4.2231414e-009 6.3087093e-008 5.3002054e-008 -2.7468722e-008 5.9697136e-008 1.0857093e-011 + -1.1848374e+006 4.7046485e+006 4.8515520e+006 8.5435906e+004 1.3696714e+005 6.9503469e+004 1.5359273e+005 4.9661100e+005 + -3.1602569e+005 2.2073118e+006 2.2298200e+006 6.9780500e+004 1.1163394e+005 2.7234406e+004 1.1490800e+005 3.2489038e+005 + -2.7300254e+004 5.5791969e+005 5.5858725e+005 3.3350797e+004 6.2322621e+004 7.1084141e+003 6.2726699e+004 1.2432063e+005 + 1.3746807e-009 1.0866037e-009 1.7522713e-009 -2.7745072e-008 -4.7642047e-008 1.2327270e-008 4.9211035e-008 3.6664005e-012 + -1.3226455e+006 4.7454900e+006 4.9263645e+006 8.6993570e+004 1.3782581e+005 7.6500539e+004 1.5763339e+005 4.9267841e+005 + -3.5892647e+005 2.2299338e+006 2.2586350e+006 7.0856688e+004 1.1253783e+005 3.0293518e+004 1.1654381e+005 3.2311828e+005 + -3.4051000e+004 5.6462263e+005 5.6564844e+005 3.3572621e+004 6.2983434e+004 7.9856294e+003 6.3487664e+004 1.2377420e+005 + -2.1874158e-010 6.0223506e-009 6.0263217e-009 -9.9298290e-008 -3.4221912e-008 4.0726754e-008 5.3195940e-008 9.2086339e-012 + -1.4573315e+006 4.7895070e+006 5.0063150e+006 8.8590555e+004 1.3871698e+005 8.3323742e+004 1.6181856e+005 4.8836016e+005 + -4.0094459e+005 2.2548290e+006 2.2901988e+006 7.1992727e+004 1.1350243e+005 3.3283965e+004 1.1828197e+005 3.2115534e+005 + -4.0640637e+004 5.7222931e+005 5.7367069e+005 3.3858660e+004 6.3717750e+004 8.8439805e+003 6.4328590e+004 1.2315598e+005 + -1.5706612e-009 1.5597950e-009 2.2135800e-009 -5.8160765e-010 1.6781570e-008 1.0648904e-010 1.6781907e-008 4.2632564e-013 + -1.5890878e+006 4.8362140e+006 5.0905960e+006 9.0251680e+004 1.3966905e+005 8.9974109e+004 1.6614084e+005 4.8369934e+005 + -4.4215678e+005 2.2811285e+006 2.3235858e+006 7.3208781e+004 1.1452948e+005 3.6211215e+004 1.2011767e+005 3.1902459e+005 + -4.7089039e+004 5.8022813e+005 5.8213581e+005 3.4220387e+004 6.4488180e+004 9.6851953e+003 6.5211414e+004 1.2247612e+005 + -6.8960526e-010 5.4174487e-009 5.4611635e-009 5.4492411e-009 1.0867183e-009 -2.1611086e-009 2.4189557e-009 -1.2875034e-011 + -1.7179148e+006 4.8848655e+006 5.1781410e+006 9.1985461e+004 1.4069519e+005 9.6448703e+004 1.7057986e+005 4.7873494e+005 + -4.8252303e+005 2.3079813e+006 2.3578818e+006 7.4507180e+004 1.1561110e+005 3.9076570e+004 1.2203649e+005 3.1674775e+005 + -5.3379844e+004 5.8818394e+005 5.9060119e+005 3.4659082e+004 6.5260711e+004 1.0507559e+004 6.6101203e+004 1.2174411e+005 + 7.4325990e-011 -5.1914837e-009 5.1920157e-009 -7.7699092e-010 1.5506942e-008 3.6067860e-010 1.5511135e-008 -6.0396133e-012 + -1.8435568e+006 4.9342740e+006 5.2674245e+006 9.3794508e+004 1.4177298e+005 1.0272732e+005 1.7507850e+005 4.7348188e+005 + -5.2205041e+005 2.3346430e+006 2.3922990e+006 7.5888805e+004 1.1672453e+005 4.1873508e+004 1.2400809e+005 3.1433619e+005 + -5.9552566e+004 5.9579375e+005 5.9876269e+005 3.5174246e+004 6.6010531e+004 1.1313232e+004 6.6972977e+004 1.2096643e+005 + 1.3044622e-009 9.2022123e-010 1.5963799e-009 -1.2452483e-008 -1.7902195e-008 4.6658712e-009 1.8500241e-008 9.1944230e-012 + -1.9655459e+006 4.9827740e+006 5.3564360e+006 9.5675219e+004 1.4284458e+005 1.0877906e+005 1.7954792e+005 4.6793953e+005 + -5.6075175e+005 2.3604570e+006 2.4261493e+006 7.7354844e+004 1.1782981e+005 4.4589199e+004 1.2598437e+005 3.1179441e+005 + -6.5675594e+004 6.0290738e+005 6.0647388e+005 3.5764473e+004 6.6721852e+004 1.2106246e+004 6.7811258e+004 1.2014769e+005 + 1.1929806e-009 6.2017902e-009 6.3154895e-009 -2.7280104e-008 -4.4941018e-008 9.8341815e-009 4.6004413e-008 2.2282620e-011 + -2.0832643e+006 5.0284040e+006 5.4428700e+006 9.7615375e+004 1.4383716e+005 1.1457790e+005 1.8389459e+005 4.6211334e+005 + -5.9846006e+005 2.3847800e+006 2.4587253e+006 7.8895648e+004 1.1887268e+005 4.7208141e+004 1.2790356e+005 3.0912913e+005 + -7.1742906e+004 6.0950838e+005 6.1371613e+005 3.6418746e+004 6.7383969e+004 1.2884231e+004 6.8604680e+004 1.1929320e+005 + 4.5066848e-009 -1.5671997e-009 4.7714068e-009 -2.6749262e-008 -2.0504999e-008 9.7753414e-009 2.2715904e-008 1.1297629e-011 + -2.1960210e+006 5.0692770e+006 5.5244980e+006 9.9592859e+004 1.4468677e+005 1.2011207e+005 1.8804566e+005 4.5602641e+005 + -6.3473919e+005 2.4069868e+006 2.4892728e+006 8.0491148e+004 1.1979542e+005 4.9715289e+004 1.2970178e+005 3.0635375e+005 + -7.7618586e+004 6.1566263e+005 6.2053619e+005 3.7118641e+004 6.7988273e+004 1.3634036e+004 6.9341852e+004 1.1840918e+005 + -3.6766603e-009 -2.1752369e-009 4.2719419e-009 -2.7310707e-008 -4.8902820e-009 1.0168058e-008 1.1282919e-008 1.7763568e-011 + -2.3030998e+006 5.1040720e+006 5.5996270e+006 1.0158479e+005 1.4535455e+005 1.2537488e+005 1.9195522e+005 4.4971106e+005 + -6.6903656e+005 2.4266073e+006 2.5171478e+006 8.2114992e+004 1.2055481e+005 5.2096289e+004 1.3132969e+005 3.0348413e+005 + -8.3106953e+004 6.2145038e+005 6.2698275e+005 3.7844590e+004 6.8528414e+004 1.4337706e+004 7.0012234e+004 1.1750094e+005 + 4.0280472e-009 4.3519028e-009 5.9299428e-009 -1.3694139e-008 -1.0889607e-008 4.8117954e-009 1.1905332e-008 -9.0381036e-012 + -2.4036660e+006 5.1323985e+006 5.6673735e+006 1.0356601e+005 1.4583005e+005 1.3033810e+005 1.9558738e+005 4.4318672e+005 + -7.0096181e+005 2.4435045e+006 2.5420585e+006 8.3740094e+004 1.2113745e+005 5.4332664e+004 1.3276414e+005 3.0052941e+005 + -8.8106094e+004 6.2690900e+005 6.3306994e+005 3.8579742e+004 6.9002828e+004 1.4984816e+004 7.0611156e+004 1.1657088e+005 + -9.6791086e-010 -3.1182879e-010 1.0169015e-009 8.5095699e-009 -6.7660721e-010 -3.1779877e-009 3.2492158e-009 3.4816594e-012 + -2.4968790e+006 5.1548235e+006 5.7277055e+006 1.0551141e+005 1.4612553e+005 1.3495648e+005 1.9891186e+005 4.3645284e+005 + -7.3036181e+005 2.4579190e+006 2.5641363e+006 8.5343656e+004 1.2156321e+005 5.6403426e+004 1.3401105e+005 2.9748963e+005 + -9.2637781e+004 6.3201113e+005 6.3876425e+005 3.9314164e+004 6.9415555e+004 1.5575002e+004 7.1141406e+004 1.1561907e+005 + 9.7281143e-009 5.2500226e-010 9.7422701e-009 -3.6569894e-008 -7.0742061e-009 1.3222660e-008 1.4996104e-008 7.9580786e-013 + -2.5820455e+006 5.1725715e+006 5.7812160e+006 1.0739450e+005 1.4627288e+005 1.3919230e+005 2.0191644e+005 4.2950216e+005 + -7.5714913e+005 2.4703173e+006 2.5837458e+006 8.6903656e+004 1.2187373e+005 5.8289871e+004 1.3509594e+005 2.9436178e+005 + -9.6749852e+004 6.3668444e+005 6.4399350e+005 4.0039586e+004 6.9773758e+004 1.6109956e+004 7.1609414e+004 1.1464547e+005 + 4.9168580e-009 2.7774192e-009 5.6470832e-009 -2.8190229e-008 1.1840484e-010 1.0334304e-008 1.0334983e-008 1.0530243e-011 + -2.6587100e+006 5.1870775e+006 5.8287660e+006 1.0918625e+005 1.4631948e+005 1.4303402e+005 2.0461702e+005 4.2234197e+005 + -7.8111494e+005 2.4811710e+006 2.6012210e+006 8.8393961e+004 1.2211430e+005 5.9980078e+004 1.3604967e+005 2.9114841e+005 + -1.0040671e+005 6.4086038e+005 6.4867831e+005 4.0743098e+004 7.0084172e+004 1.6584721e+004 7.2019750e+004 1.1365172e+005 + 7.4371936e-010 3.6650318e-009 3.7397294e-009 -1.0711551e-007 -7.6295848e-008 3.5864474e-008 8.4304908e-008 1.6314061e-011 + -2.7266255e+006 5.1996220e+006 5.8711630e+006 1.1075180e+005 1.4631917e+005 1.4648870e+005 2.0704647e+005 4.1500594e+005 + -8.0196125e+005 2.4908230e+006 2.6167425e+006 8.9689211e+004 1.2232202e+005 6.1468277e+004 1.3689786e+005 2.8786506e+005 + -1.0350608e+005 6.4450638e+005 6.5276488e+005 4.1353977e+004 7.0349633e+004 1.6989396e+004 7.2372023e+004 1.1264488e+005 + 1.2749884e-010 2.7838443e-010 3.0619249e-010 8.0451557e-008 -3.3626876e-008 -3.0700917e-008 4.5533646e-008 -1.9440449e-011 + -2.7856803e+006 5.2111115e+006 5.9089505e+006 1.1217122e+005 1.4631819e+005 1.4955661e+005 2.0922759e+005 4.0752813e+005 + -8.1954144e+005 2.4994955e+006 2.6304230e+006 9.0862563e+004 1.2251593e+005 6.2754152e+004 1.3765259e+005 2.8451994e+005 + -1.0597832e+005 6.4767031e+005 6.5628363e+005 4.1912258e+004 7.0575305e+004 1.7318197e+004 7.2669070e+004 1.1162252e+005 + -4.9149214e-009 -1.4785586e-009 5.1325029e-009 2.6770905e-009 2.0633365e-008 -4.3485215e-012 2.0633365e-008 2.0776270e-011 + -2.8359133e+006 5.2221505e+006 5.9424960e+006 1.1349227e+005 1.4633958e+005 1.5222516e+005 2.1115816e+005 3.9992978e+005 + -8.3398681e+005 2.5074513e+006 2.6425075e+006 9.1957836e+004 1.2270690e+005 6.3839734e+004 1.3832027e+005 2.8111859e+005 + -1.0787148e+005 6.5045513e+005 6.5933913e+005 4.2440387e+004 7.0770586e+004 1.7575510e+004 7.2920328e+004 1.1058342e+005 + -1.1146257e-008 -6.1451892e-009 1.2728016e-008 5.2406683e-009 2.5158755e-008 -6.7350214e-010 2.5167768e-008 -7.3896445e-012 + -2.8775920e+006 5.2331805e+006 5.9721615e+006 1.1472167e+005 1.4638119e+005 1.5448392e+005 2.1282089e+005 3.9220856e+005 + -8.4562369e+005 2.5150818e+006 2.6534348e+006 9.2980383e+004 1.2290436e+005 6.4730801e+004 1.3890845e+005 2.7765863e+005 + -1.0931026e+005 6.5299825e+005 6.6208413e+005 4.2937504e+004 7.0949313e+004 1.7772625e+004 7.3141445e+004 1.0952629e+005 + 1.6385633e-009 -1.0870504e-009 1.9663593e-009 -1.6573747e-008 -8.1881666e-009 5.5509091e-009 9.8923536e-009 1.6200374e-011 + -2.9112228e+006 5.2444830e+006 5.9983180e+006 1.1586763e+005 1.4642716e+005 1.5634008e+005 2.1420348e+005 3.8435353e+005 + -8.5478763e+005 2.5228005e+006 2.6636788e+006 9.3937305e+004 1.2311413e+005 6.5438711e+004 1.3942494e+005 2.7413475e+005 + -1.1040009e+005 6.5545413e+005 6.6468663e+005 4.3403695e+004 7.1127328e+004 1.7919678e+004 7.3349922e+004 1.0844959e+005 + 5.9995181e-010 7.5262694e-009 7.5501445e-009 6.7231419e-008 -3.5225881e-008 -2.5869689e-008 4.3704731e-008 9.1944230e-012 + -2.9374830e+006 5.2562430e+006 6.0213700e+006 1.1693734e+005 1.4646881e+005 1.5782231e+005 2.1531603e+005 3.7637484e+005 + -8.6169538e+005 2.5308908e+006 2.6735608e+006 9.4834094e+004 1.2333686e+005 6.5980023e+004 1.3987617e+005 2.7055103e+005 + -1.1116700e+005 6.5797700e+005 6.6730188e+005 4.3837121e+004 7.1316508e+004 1.8020844e+004 7.3558109e+004 1.0735430e+005 + 7.6134494e-009 4.8804623e-009 9.0434247e-009 1.4420436e-007 -1.6898341e-008 -5.2347914e-008 5.5007799e-008 -1.1453949e-011 + -2.9570160e+006 5.2686285e+006 6.0417210e+006 1.1793512e+005 1.4651270e+005 1.5896027e+005 2.1618125e+005 3.6830681e+005 + -8.6649606e+005 2.5394848e+006 2.6832440e+006 9.5672914e+004 1.2357016e+005 6.6371750e+004 1.4026686e+005 2.6692219e+005 + -1.1160065e+005 6.6070194e+005 6.7006100e+005 4.4236406e+004 7.1523539e+004 1.8077541e+004 7.3772719e+004 1.0624387e+005 + 1.4663188e-009 -4.3314010e-009 4.5728683e-009 7.9280525e-008 2.9500967e-008 -2.6678656e-008 3.9775088e-008 -1.2519763e-011 + -2.9703000e+006 5.2819380e+006 6.0598310e+006 1.1886682e+005 1.4657630e+005 1.5976663e+005 2.1681788e+005 3.6018816e+005 + -8.6938906e+005 2.5486778e+006 2.6928788e+006 9.6458930e+004 1.2381645e+005 6.6626844e+004 1.4060458e+005 2.6326528e+005 + -1.1172891e+005 6.6371581e+005 6.7305425e+005 4.4604246e+004 7.1751977e+004 1.8094299e+004 7.3998313e+004 1.0512230e+005 + 1.3300749e-009 8.6973042e-009 8.7984207e-009 1.1729819e-007 -6.4486017e-009 -4.1888498e-008 4.2381963e-008 -3.9648285e-012 + -2.9776083e+006 5.2966635e+006 6.0762485e+006 1.1973928e+005 1.4667763e+005 1.6023486e+005 2.1723153e+005 3.5204113e+005 + -8.7064644e+005 2.5586395e+006 2.7027135e+006 9.7197477e+004 1.2408887e+005 6.6752070e+004 1.4090381e+005 2.5959136e+005 + -1.1164297e+005 6.6703988e+005 6.7631825e+005 4.4948750e+004 7.2005203e+004 1.8080211e+004 7.4240438e+004 1.0399266e+005 + -3.1253240e-009 1.1092993e-010 3.1272920e-009 -7.1457706e-008 -6.4875707e-008 2.1875302e-008 6.8464487e-008 1.1127099e-011 + -2.9789733e+006 5.3133240e+006 6.0914440e+006 1.2057229e+005 1.4682472e+005 1.6034870e+005 2.1741481e+005 3.4386759e+005 + -8.7048825e+005 2.5696108e+006 2.7130515e+006 9.7909844e+004 1.2440683e+005 6.6747109e+004 1.4118156e+005 2.5590389e+005 + -1.1144613e+005 6.7063338e+005 6.7983038e+005 4.5281387e+004 7.2287141e+004 1.8044244e+004 7.4505203e+004 1.0285723e+005 + 4.6844097e-009 1.2326158e-009 4.8438662e-009 -1.6274483e-007 -4.9798729e-008 5.4599113e-008 7.3898420e-008 1.1951329e-011 + -2.9742495e+006 5.3322855e+006 6.1056880e+006 1.2136424e+005 1.4702336e+005 1.6010298e+005 2.1736797e+005 3.3567469e+005 + -8.6888631e+005 2.5817335e+006 2.7240248e+006 9.8595828e+004 1.2478610e+005 6.6607539e+004 1.4145011e+005 2.5220914e+005 + -1.1114854e+005 6.7443025e+005 6.8352769e+005 4.5605813e+004 7.2598922e+004 1.7986805e+004 7.4793906e+004 1.0171992e+005 + -1.1985347e-009 1.8815371e-008 1.8853505e-008 8.1898783e-008 -6.9199132e-008 -3.2049122e-008 7.6260520e-008 -9.9475983e-014 + -2.9630713e+006 5.3535605e+006 6.1188560e+006 1.2210195e+005 1.4727969e+005 1.5950222e+005 2.1709966e+005 3.2748978e+005 + -8.6551731e+005 2.5949295e+006 2.7354668e+006 9.9243938e+004 1.2522852e+005 6.6324875e+004 1.4170805e+005 2.4852166e+005 + -1.1064383e+005 6.7837663e+005 6.8734044e+005 4.5916410e+004 7.2937461e+004 1.7897699e+004 7.5101273e+004 1.0058653e+005 + -8.7384400e-010 -2.4936417e-009 2.6423195e-009 1.9432814e-008 -7.7480422e-010 -6.7852000e-009 6.8292940e-009 -8.6970431e-012 + -2.9448123e+006 5.3768155e+006 6.1304210e+006 1.2276976e+005 1.4759828e+005 1.5854420e+005 2.1661375e+005 3.1935378e+005 + -8.5990488e+005 2.6089335e+006 2.7469930e+006 9.9840555e+004 1.2572255e+005 6.5885445e+004 1.4194031e+005 2.4486048e+005 + -1.0977427e+005 6.8244656e+005 6.9121900e+005 4.6207176e+004 7.3295992e+004 1.7761959e+004 7.5417438e+004 9.9462977e+004 + 7.9647471e-009 1.1378641e-008 1.3889228e-008 -1.1840703e-007 -8.9944805e-008 3.6623284e-008 9.7115048e-008 6.3664629e-012 + -2.9186883e+006 5.4015665e+006 6.1396790e+006 1.2335656e+005 1.4797833e+005 1.5721223e+005 2.1590106e+005 3.1130466e+005 + -8.5160506e+005 2.6234423e+006 2.7582025e+006 1.0037646e+005 1.2625141e+005 6.5271758e+004 1.4212606e+005 2.4124228e+005 + -1.0841216e+005 6.8662881e+005 6.9513475e+005 4.6473086e+004 7.3667102e+004 1.7567090e+004 7.5732719e+004 9.8353680e+004 + -6.3768972e-009 -8.8694625e-009 1.0923928e-008 7.0914922e-008 1.5507879e-008 -2.3626242e-008 2.8261168e-008 -1.7479351e-012 + -2.8838170e+006 5.4273220e+006 6.1459110e+006 1.2385526e+005 1.4840422e+005 1.5547145e+005 2.1493066e+005 3.0335522e+005 + -8.4031388e+005 2.6382678e+006 2.7688598e+006 1.0084608e+005 1.2680202e+005 6.4462816e+004 1.4224700e+005 2.3767238e+005 + -1.0650903e+005 6.9090675e+005 6.9906819e+005 4.6713129e+004 7.4046484e+004 1.7307063e+004 7.6042203e+004 9.7259766e+004 + 8.1446565e-009 7.5320914e-009 1.1093594e-008 -1.6886463e-007 -3.6136498e-008 5.6021747e-008 6.6665457e-008 -9.2512664e-012 + -2.8394768e+006 5.4536620e+006 6.1485815e+006 1.2425984e+005 1.4885158e+005 1.5329461e+005 2.1367272e+005 2.9550234e+005 + -8.2579100e+005 2.6533308e+006 2.7788658e+006 1.0124430e+005 1.2736645e+005 6.3440684e+004 1.4229172e+005 2.3414897e+005 + -1.0403644e+005 6.9524938e+005 7.0299019e+005 4.6926176e+004 7.4432961e+004 1.6977428e+004 7.6344602e+004 9.6180766e+004 + 2.7300984e-010 4.3140016e-009 4.3226316e-009 -1.1180913e-007 2.4703866e-009 3.7981550e-008 3.8061803e-008 1.1326051e-011 + -2.7852098e+006 5.4801710e+006 6.1473300e+006 1.2456103e+005 1.4929584e+005 1.5067530e+005 2.1211386e+005 2.8774347e+005 + -8.0777563e+005 2.6685540e+006 2.7881323e+006 1.0156088e+005 1.2793714e+005 6.2193879e+004 1.4225327e+005 2.3067000e+005 + -1.0093087e+005 6.9962063e+005 7.0686356e+005 4.7107207e+004 7.4825555e+004 1.6571135e+004 7.6638547e+004 9.5116125e+004 + 3.4520864e-010 3.7134171e-009 3.7294283e-009 2.7257892e-007 -1.4861357e-008 -9.2313698e-008 9.3502294e-008 8.2422957e-012 + -2.7208935e+006 5.5064020e+006 6.1419645e+006 1.2474416e+005 1.4972272e+005 1.4762898e+005 2.1026461e+005 2.8009394e+005 + -7.8601488e+005 2.6837645e+006 2.7964998e+006 1.0178063e+005 1.2850181e+005 6.0719957e+004 1.4212539e+005 2.2724022e+005 + -9.7093656e+004 7.0399700e+005 7.1066094e+005 4.7247426e+004 7.5221453e+004 1.6078933e+004 7.6920734e+004 9.4066570e+004 + -4.0885240e-010 -2.7867575e-010 4.9479332e-010 -7.2056991e-008 -1.7621630e-008 2.3504896e-008 2.9376896e-008 2.6290081e-012 + -2.6466633e+006 5.5320345e+006 6.1325550e+006 1.2467912e+005 1.5013353e+005 1.4417094e+005 2.0814739e+005 2.7258150e+005 + -7.6044019e+005 2.6987038e+006 2.8037958e+006 1.0178080e+005 1.2905280e+005 5.9022715e+004 1.4190950e+005 2.2387169e+005 + -9.2473000e+004 7.0832481e+005 7.1433556e+005 4.7281133e+004 7.5608547e+004 1.5497876e+004 7.7180547e+004 9.3036906e+004 + 4.0854209e-009 1.2677326e-010 4.0873873e-009 -8.8398728e-009 1.0413572e-008 3.2499869e-009 1.0908937e-008 -3.6664005e-012 + -2.5628488e+006 5.5566645e+006 6.1192085e+006 1.2449566e+005 1.5052905e+005 1.4030283e+005 2.0577628e+005 2.6522747e+005 + -7.3129231e+005 2.7130975e+006 2.8099265e+006 1.0168760e+005 1.2957577e+005 5.7110777e+004 1.4160339e+005 2.2056625e+005 + -8.7136180e+004 7.1258219e+005 7.1789000e+005 4.7279219e+004 7.5980203e+004 1.4835416e+004 7.7414992e+004 9.2021844e+004 + 8.3335205e-009 -3.4756225e-009 9.0292591e-009 -8.1198962e-008 -1.9038751e-008 2.6321558e-008 3.2485357e-008 -6.6791017e-012 + -2.4698913e+006 5.5799505e+006 6.1021480e+006 1.2430649e+005 1.5089850e+005 1.3601688e+005 2.0315253e+005 2.5803772e+005 + -6.9900256e+005 2.7268175e+006 2.8149845e+006 1.0160937e+005 1.3006098e+005 5.4994707e+004 1.4121005e+005 2.1732223e+005 + -8.1230039e+004 7.1675600e+005 7.2134425e+005 4.7306094e+004 7.6336633e+004 1.4105302e+004 7.7628867e+004 9.1017406e+004 + 1.2585275e-009 1.3316391e-008 1.3375730e-008 -2.5666758e-009 -7.3394489e-008 -6.8200023e-010 7.3397658e-008 1.0800250e-011 + -2.3683905e+006 5.6017500e+006 6.0818485e+006 1.2395613e+005 1.5122063e+005 1.3132295e+005 2.0028330e+005 2.5100377e+005 + -6.6394788e+005 2.7399013e+006 2.8191995e+006 1.0139225e+005 1.3051283e+005 5.2686711e+004 1.4074617e+005 2.1414253e+005 + -7.4882570e+004 7.2078488e+005 7.2466425e+005 4.7273484e+004 7.6678805e+004 1.3318586e+004 7.7826883e+004 9.0031328e+004 + 3.2734238e-009 -7.2387518e-010 3.3525063e-009 -7.0920549e-008 -4.5501380e-008 2.2408663e-008 5.0720054e-008 2.1174174e-012 + -2.2597488e+006 5.6219950e+006 6.0591495e+006 1.2348430e+005 1.5149575e+005 1.2630058e+005 1.9723792e+005 2.4413530e+005 + -6.2636581e+005 2.7522818e+006 2.8226563e+006 1.0107252e+005 1.3093041e+005 5.0221426e+004 1.4023183e+005 2.1102867e+005 + -6.8053523e+004 7.2461488e+005 7.2780356e+005 4.7203684e+004 7.7004070e+004 1.2475218e+004 7.8008063e+004 8.9062078e+004 + 2.0320405e-009 -6.4096950e-009 6.7240897e-009 3.6549892e-009 -1.4320364e-008 -1.3339374e-009 1.4382358e-008 -1.1084467e-012 + -2.1453668e+006 5.6406250e+006 6.0348365e+006 1.2296604e+005 1.5173425e+005 1.2101633e+005 1.9408306e+005 2.3746355e+005 + -5.8664756e+005 2.7638280e+006 2.8254028e+006 1.0072409e+005 1.3130986e+005 4.7632500e+004 1.3968227e+005 2.0799200e+005 + -6.0780352e+004 7.2822163e+005 7.3075369e+005 4.7130262e+004 7.7306398e+004 1.1582598e+004 7.8169273e+004 8.8110398e+004 + -3.7305270e-011 2.0583979e-009 2.0587358e-009 -1.5388189e-009 -1.8486574e-008 4.0161297e-010 1.8490935e-008 -1.3869794e-011 + -2.0262050e+006 5.6576390e+006 6.0095245e+006 1.2232793e+005 1.5194092e+005 1.1547632e+005 1.9084241e+005 2.3101511e+005 + -5.4548794e+005 2.7744865e+006 2.8276018e+006 1.0027793e+005 1.3165155e+005 4.4941691e+004 1.3911106e+005 2.0504805e+005 + -5.3283160e+004 7.3159475e+005 7.3353250e+005 4.7015844e+004 7.7583211e+004 1.0662077e+004 7.8312414e+004 8.7183695e+004 + -5.1325699e-010 -1.0711894e-009 1.1878044e-009 3.1462740e-008 -3.7234742e-008 -1.0072796e-008 3.8573141e-008 -2.7995384e-012 + -1.9030343e+006 5.6729120e+006 5.9836000e+006 1.2177484e+005 1.5210442e+005 1.0966895e+005 1.8751808e+005 2.2478798e+005 + -5.0362175e+005 2.7843025e+006 2.8294833e+006 9.9932219e+004 1.3195120e+005 4.2167102e+004 1.3852503e+005 2.0219263e+005 + -4.5810883e+004 7.3476081e+005 7.3618750e+005 4.6979324e+004 7.7841320e+004 9.7370674e+003 7.8447953e+004 8.6276117e+004 + 2.5352582e-009 -7.9570208e-009 8.3511509e-009 1.3675010e-008 -1.5271212e-008 -4.2721524e-009 1.5857527e-008 1.3514523e-011 + -1.7765143e+006 5.6865490e+006 5.9575870e+006 1.2121048e+005 1.5222219e+005 1.0361130e+005 1.8413825e+005 2.1879672e+005 + -4.6140378e+005 2.7933573e+006 2.8312080e+006 9.9592297e+004 1.3221422e+005 3.9322563e+004 1.3793791e+005 1.9944178e+005 + -3.8472152e+004 7.3772800e+005 7.3873044e+005 4.6957910e+004 7.8081781e+004 8.8181445e+003 7.8578141e+004 8.5400172e+004 + 8.0130014e-010 -6.0004890e-010 1.0010697e-009 -1.1011966e-007 6.4065546e-008 3.4236351e-008 7.2639672e-008 1.8530955e-011 + -1.6472588e+006 5.6987540e+006 5.9320535e+006 1.2057316e+005 1.5230634e+005 9.7355016e+004 1.8076289e+005 2.1307150e+005 + -4.1878997e+005 2.8016728e+006 2.8328000e+006 9.9199898e+004 1.3244625e+005 3.6422125e+004 1.3736295e+005 1.9681383e+005 + -3.1186992e+004 7.4050088e+005 7.4115731e+005 4.6913461e+004 7.8302805e+004 7.9001782e+003 7.8700328e+004 8.4564797e+004 + -1.8692834e-009 -6.3543597e-009 6.6236026e-009 -7.2603591e-008 2.3948758e-008 2.2549514e-008 3.2894128e-008 9.0096819e-012 + -1.5156321e+006 5.7097635e+006 5.9074985e+006 1.1983847e+005 1.5237930e+005 9.0945992e+004 1.7745597e+005 2.0765758e+005 + -3.7560153e+005 2.8092305e+006 2.8342288e+006 9.8734586e+004 1.3264878e+005 3.3475707e+004 1.3680761e+005 1.9433055e+005 + -2.3836330e+004 7.4307931e+005 7.4346156e+005 4.6841621e+004 7.8504438e+004 6.9733936e+003 7.8813547e+004 8.3776398e+004 + 1.7611590e-009 -9.6365582e-011 1.7637934e-009 -7.5297123e-008 7.0792652e-009 2.3556240e-008 2.4597000e-008 1.0729195e-011 + -1.3815374e+006 5.7200980e+006 5.8845700e+006 1.1901630e+005 1.5247038e+005 8.4387914e+004 1.7426570e+005 2.0258877e+005 + -3.3175688e+005 2.8160958e+006 2.8355703e+006 9.8207875e+004 1.3283722e+005 3.0479545e+004 1.3628914e+005 1.9200780e+005 + -1.6391295e+004 7.4545588e+005 7.4563606e+005 4.6742844e+004 7.8685328e+004 6.0357090e+003 7.8916477e+004 8.3039438e+004 + -3.5522381e-009 -5.4605263e-009 6.5142722e-009 -9.9195887e-008 1.7900803e-008 3.0619546e-008 3.5468229e-008 -7.1054274e-013 + -1.2445448e+006 5.7302770e+006 5.8638695e+006 1.1809988e+005 1.5259363e+005 7.7646438e+004 1.7121269e+005 1.9787575e+005 + -2.8728025e+005 2.8225025e+006 2.8370848e+006 9.7609922e+004 1.3302519e+005 2.7424279e+004 1.3582266e+005 1.8985188e+005 + -8.9151563e+003 7.4760981e+005 7.4766300e+005 4.6622855e+004 7.8850211e+004 5.0909922e+003 7.9014391e+004 8.2356172e+004 + -4.6860968e-009 -3.1912757e-009 5.6695453e-009 1.9854288e-007 5.7990107e-008 -6.4045864e-008 8.6398643e-008 -8.1143980e-012 + -1.1039931e+006 5.7408325e+006 5.8460205e+006 1.1724145e+005 1.5275142e+005 7.0681258e+004 1.6831172e+005 1.9350359e+005 + -2.4211669e+005 2.8287520e+006 2.8390948e+006 9.7082367e+004 1.3323441e+005 2.4291121e+004 1.3543067e+005 1.8785494e+005 + -1.4528032e+003 7.4956250e+005 7.4956388e+005 4.6551098e+004 7.9011609e+004 4.1407183e+003 7.9120031e+004 8.1722234e+004 + 7.8855233e-009 6.2307404e-009 1.0050054e-008 -7.8918099e-009 2.7186076e-009 2.3504185e-009 3.5937855e-009 -1.4040324e-011 + -9.5917094e+005 5.7521375e+006 5.8315600e+006 1.1625158e+005 1.5294377e+005 6.3475898e+004 1.6559283e+005 1.8947817e+005 + -1.9592913e+005 2.8350900e+006 2.8418520e+006 9.6440102e+004 1.3347678e+005 2.1062246e+004 1.3512834e+005 1.8602723e+005 + 6.0560464e+003 7.5131644e+005 7.5134088e+005 4.6425668e+004 7.9169391e+004 3.1778074e+003 7.9233141e+004 8.1148266e+004 + -9.8359489e-009 9.2163761e-009 1.3479150e-008 9.6646033e-008 -3.3082344e-008 -2.8505276e-008 4.3669122e-008 8.9528385e-013 + -8.0974569e+005 5.7640700e+006 5.8206695e+006 1.1511630e+005 1.5316114e+005 5.6066426e+004 1.6310052e+005 1.8580753e+005 + -1.4812008e+005 2.8415738e+006 2.8454315e+006 9.5670234e+004 1.3374459e+005 1.7730744e+004 1.3491478e+005 1.8437056e+005 + 1.3841538e+004 7.5293744e+005 7.5306463e+005 4.6244043e+004 7.9328867e+004 2.1809492e+003 7.9358844e+004 8.0633516e+004 + 6.8482304e-009 1.0398574e-008 1.2451047e-008 -2.9968095e-007 -5.1830341e-008 9.5429350e-008 1.0859625e-007 -1.5489832e-012 + -6.5553938e+005 5.7763340e+006 5.8134125e+006 1.1383513e+005 1.5339939e+005 4.8495977e+004 1.6088267e+005 1.8250953e+005 + -9.8158820e+004 2.8480775e+006 2.8497685e+006 9.4774922e+004 1.3402648e+005 1.4290646e+004 1.3478620e+005 1.8288913e+005 + 2.2133992e+004 7.5451500e+005 7.5483956e+005 4.5995551e+004 7.9490500e+004 1.1300226e+003 7.9498531e+004 8.0176609e+004 + -1.5087707e-008 7.0043162e-009 1.6634283e-008 1.5431202e-007 -6.5155618e-008 -4.3844807e-008 7.8534207e-008 -1.1510792e-012 + -4.9644566e+005 5.7885045e+006 5.8097540e+006 1.1240505e+005 1.5364631e+005 4.0773258e+004 1.5896430e+005 1.7959603e+005 + -4.5880969e+004 2.8545043e+006 2.8548730e+006 9.3750719e+004 1.3430386e+005 1.0740671e+004 1.3473266e+005 1.8158334e+005 + 3.0998594e+004 7.5613413e+005 7.5676925e+005 4.5680617e+004 7.9655156e+004 1.9259068e+001 7.9655164e+004 7.9774047e+004 + 7.5462880e-009 -2.5165718e-009 7.9548474e-009 -5.2105696e-007 -3.3620736e-008 1.6236453e-007 1.6580891e-007 3.4106051e-012 + -3.3240859e+005 5.8003555e+006 5.8098725e+006 1.1085076e+005 1.5388964e+005 3.2870176e+004 1.5736095e+005 1.7706239e+005 + 8.4546387e+003 2.8608490e+006 2.8608615e+006 9.2619164e+004 1.3456861e+005 7.0790815e+003 1.3475467e+005 1.8044689e+005 + 4.0315602e+004 7.5784881e+005 7.5892044e+005 4.5313789e+004 7.9824719e+004 -1.1408463e+003 7.9832875e+004 7.9421148e+004 + -9.9122026e-009 -2.1479174e-009 1.0142253e-008 -2.0026310e-007 2.2898888e-008 5.9744551e-008 6.3982576e-008 -6.9775297e-012 + -1.6372258e+005 5.8118690e+006 5.8141745e+006 1.0920135e+005 1.5412033e+005 2.4770332e+004 1.5609819e+005 1.7489331e+005 + 6.4440176e+004 2.8671790e+006 2.8679030e+006 9.1403563e+004 1.3482203e+005 3.3120532e+003 1.3486270e+005 1.7947044e+005 + 4.9929602e+004 7.5966275e+005 7.6130181e+005 4.4910797e+004 8.0001078e+004 -2.3360852e+003 8.0035180e+004 7.9114055e+004 + -4.0186581e-008 8.7512406e-009 4.1128402e-008 2.9634771e-007 -3.5505664e-008 -8.7731486e-008 9.4643887e-008 2.7142733e-012 + 8.7348770e+003 5.8231305e+006 5.8231370e+006 1.0746359e+005 1.5433733e+005 1.6510428e+004 1.5521794e+005 1.7307805e+005 + 1.2178306e+005 2.8735415e+006 2.8761210e+006 9.0109109e+004 1.3506966e+005 -5.4009314e+002 1.3507073e+005 1.7864797e+005 + 5.9789352e+004 7.6153394e+005 7.6387744e+005 4.4476566e+004 8.0183898e+004 -3.5602024e+003 8.0262898e+004 7.8850898e+004 + -2.3491930e-008 3.3208938e-009 2.3725494e-008 2.1536468e-007 -7.4600919e-008 -5.9234644e-008 9.5257761e-008 2.9132252e-012 + 1.8372070e+005 5.8343070e+006 5.8371990e+006 1.0565055e+005 1.5455436e+005 8.1624155e+003 1.5476975e+005 1.7161991e+005 + 1.8023873e+005 2.8798720e+006 2.8855068e+006 8.8741039e+004 1.3531747e+005 -4.4481055e+003 1.3539056e+005 1.7797986e+005 + 6.9918563e+004 7.6340275e+005 7.6659788e+005 4.4008313e+004 8.0369422e+004 -4.8122031e+003 8.0513359e+004 7.8631375e+004 + -3.8553809e-009 -4.3557247e-010 3.8799080e-009 6.7376163e-007 -1.3370212e-008 -2.0301192e-007 2.0345172e-007 3.8511416e-012 + 3.5992913e+005 5.8454615e+006 5.8565320e+006 1.0376609e+005 1.5478564e+005 -2.1419301e+002 1.5478578e+005 1.7053400e+005 + 2.3941394e+005 2.8860680e+006 2.8959813e+006 8.7305742e+004 1.3556730e+005 -8.3813994e+003 1.3582614e+005 1.7747134e+005 + 8.0265750e+004 7.6521306e+005 7.6941125e+005 4.3509840e+004 8.0552648e+004 -6.0846943e+003 8.0782133e+004 7.8455875e+004 + -2.8478866e-009 -2.7112090e-010 2.8607630e-009 3.9415789e-007 2.4187344e-008 -1.2142260e-007 1.2380822e-007 -1.1425527e-011 + 5.3635650e+005 5.8566115e+006 5.8811205e+006 1.0183555e+005 1.5504113e+005 -8.6071406e+003 1.5527986e+005 1.6983280e+005 + 2.9864103e+005 2.8920325e+006 2.9074110e+006 8.5831742e+004 1.3581963e+005 -1.2317230e+004 1.3637700e+005 1.7712720e+005 + 9.0628352e+004 7.6692450e+005 7.7226075e+005 4.2998758e+004 8.0729492e+004 -7.3577769e+003 8.1064094e+004 7.8325125e+004 + -1.0462410e-008 -9.9576321e-009 1.4443562e-008 2.8619746e-007 -2.2203992e-008 -8.3810050e-008 8.6701448e-008 -2.7426950e-012 + 7.1224125e+005 5.8676825e+006 5.9107515e+006 9.9889727e+004 1.5531850e+005 -1.7026305e+004 1.5624894e+005 1.6951667e+005 + 3.5721384e+005 2.8977143e+006 2.9196490e+006 8.4354055e+004 1.3607420e+005 -1.6238743e+004 1.3703972e+005 1.7694880e+005 + 1.0076095e+005 7.6850438e+005 7.7508175e+005 4.2496941e+004 8.0897867e+004 -8.6084736e+003 8.1354602e+004 7.8240508e+004 + 7.8806037e-009 1.0936482e-008 1.3480006e-008 -9.5171015e-007 -1.4165678e-008 2.8709039e-007 2.8743966e-007 1.1155521e-011 + 8.8686800e+005 5.8784595e+006 5.9449830e+006 9.7946125e+004 1.5560348e+005 -2.5457568e+004 1.5767223e+005 1.6957755e+005 + 4.1471653e+005 2.9030703e+006 2.9325428e+006 8.2891953e+004 1.3632848e+005 -2.0129715e+004 1.3780661e+005 1.7693575e+005 + 1.1054431e+005 7.6992275e+005 7.7781819e+005 4.2014602e+004 8.1056789e+004 -9.8249951e+003 8.1650070e+004 7.8204375e+004 + -7.0574622e-009 -3.7919650e-009 8.0116642e-009 -2.6528991e-007 6.9816991e-008 7.1066232e-008 9.9623399e-008 3.4106051e-013 + 1.0595518e+006 5.8886280e+006 5.9831925e+006 9.6013203e+004 1.5588247e+005 -3.3850969e+004 1.5951563e+005 1.7001067e+005 + 4.7111891e+005 2.9079968e+006 2.9459120e+006 8.1447742e+004 1.3657531e+005 -2.3974047e+004 1.3866352e+005 1.7708961e+005 + 1.2003144e+005 7.7115888e+005 7.8044444e+005 4.1546453e+004 8.1204383e+004 -1.1010584e+004 8.1947453e+004 7.8219688e+004 + 8.6966105e-009 8.7398710e-009 1.2329492e-008 -3.3145980e-008 -7.9073743e-009 1.0873158e-008 1.3444409e-008 -2.2737368e-013 + 1.2297494e+006 5.8977450e+006 6.0245895e+006 9.4074711e+004 1.5614183e+005 -4.2146637e+004 1.6173005e+005 1.7082155e+005 + 5.2658331e+005 2.9123250e+006 2.9595485e+006 8.0003039e+004 1.3680233e+005 -2.7759465e+004 1.3959036e+005 1.7741555e+005 + 1.2934561e+005 7.7220813e+005 7.8296594e+005 4.1077621e+004 8.1337813e+004 -1.2174971e+004 8.2243969e+004 7.8289320e+004 + 1.1074166e-008 1.2298924e-008 1.6549945e-008 1.5878061e-007 -9.6438896e-008 -3.4548762e-008 1.0244060e-007 1.3244517e-011 + 1.3972698e+006 5.9053570e+006 6.0684105e+006 9.2123805e+004 1.5637078e+005 -5.0317000e+004 1.6426692e+005 1.7202025e+005 + 5.8120106e+005 2.9158740e+006 2.9732333e+006 7.8552898e+004 1.3699525e+005 -3.1482348e+004 1.4056613e+005 1.7791861e+005 + 1.3854277e+005 7.7308019e+005 7.8539613e+005 4.0606461e+004 8.1454039e+004 -1.3322716e+004 8.2536391e+004 7.8414391e+004 + -1.8387507e-008 2.4562272e-009 1.8550834e-008 -5.8942305e-008 -5.4846538e-008 2.5040961e-008 6.0292557e-008 5.4001248e-012 + 1.5623234e+006 5.9112730e+006 6.1142460e+006 9.0180016e+004 1.5657086e+005 -5.8379035e+004 1.6710041e+005 1.7361364e+005 + 6.3491631e+005 2.9184800e+006 2.9867448e+006 7.7109977e+004 1.3714617e+005 -3.5148477e+004 1.4157855e+005 1.7860197e+005 + 1.4757995e+005 7.7378644e+005 7.8773431e+005 4.0138086e+004 8.1549930e+004 -1.4450451e+004 8.2820328e+004 7.8595602e+004 + -7.0483495e-008 9.3424202e-010 7.0489691e-008 1.1197767e-007 -4.1227608e-008 -2.7221347e-008 4.9403617e-008 -1.9895197e-012 + 1.7251836e+006 5.9152725e+006 6.1617130e+006 8.8231203e+004 1.5673130e+005 -6.6357445e+004 1.7019991e+005 1.7560084e+005 + 6.8770144e+005 2.9201575e+006 3.0000423e+006 7.5668641e+004 1.3725042e+005 -3.8764742e+004 1.4261972e+005 1.7946583e+005 + 1.5640659e+005 7.7430969e+005 7.8994844e+005 3.9677355e+004 8.1624828e+004 -1.5554724e+004 8.3093695e+004 7.8833438e+004 + -2.4953675e-008 9.9046549e-009 2.6847497e-008 4.5062924e-007 -7.7161445e-008 -1.2161219e-007 1.4402573e-007 1.6243007e-011 + 1.8860090e+006 5.9175650e+006 6.2108455e+006 8.6288055e+004 1.5685870e+005 -7.4252375e+004 1.7354558e+005 1.7796923e+005 + 7.3966331e+005 2.9209198e+006 3.0131170e+006 7.4228500e+004 1.3731583e+005 -4.2335262e+004 1.4369381e+005 1.8050570e+005 + 1.6505653e+005 7.7461544e+005 7.9200550e+005 3.9220371e+004 8.1678086e+004 -1.6638992e+004 8.3355656e+004 7.9127672e+004 + -2.8709245e-008 1.0437747e-008 3.0547788e-008 1.8723102e-006 1.1385310e-009 -5.5270471e-007 5.5270590e-007 -5.5990768e-012 + 2.0446934e+006 5.9183880e+006 6.2616360e+006 8.4336164e+004 1.5695905e+005 -8.2032430e+004 1.7710297e+005 1.8070602e+005 + 7.9102069e+005 2.9208385e+006 3.0260555e+006 7.2773547e+004 1.3735181e+005 -4.5858195e+004 1.4480503e+005 1.8171652e+005 + 1.7363175e+005 7.7465906e+005 7.9387950e+005 3.8755430e+004 8.1709758e+004 -1.7712359e+004 8.3607484e+004 7.9477367e+004 + 2.5570076e-008 -4.2318362e-009 2.5917894e-008 2.8944675e-007 -6.1486247e-009 -8.4321400e-008 8.4545277e-008 -3.5953462e-012 + 2.2009940e+006 5.9179945e+006 6.3140345e+006 8.2360234e+004 1.5704263e+005 -8.9662711e+004 1.8083636e+005 1.8380434e+005 + 8.4190350e+005 2.9199735e+006 3.0389220e+006 7.1290805e+004 1.3736650e+005 -4.9328887e+004 1.4595511e+005 1.8309428e+005 + 1.8220630e+005 7.7441325e+005 7.9555956e+005 3.8273082e+004 8.1719844e+004 -1.8781203e+004 8.3850266e+004 7.9880797e+004 + -3.4810881e-008 5.4654947e-011 3.4810924e-008 -4.5144720e-008 2.4174483e-008 9.2423562e-009 2.5881011e-008 5.3432814e-012 + 2.3546998e+006 5.9166525e+006 6.3679970e+006 8.0369648e+004 1.5712409e+005 -9.7132789e+004 1.8472347e+005 1.8726209e+005 + 8.9220344e+005 2.9183510e+006 3.0516880e+006 6.9791563e+004 1.3736886e+005 -5.2744469e+004 1.4714681e+005 1.8463580e+005 + 1.9074906e+005 7.7388300e+005 7.9704463e+005 3.7774082e+004 8.1708281e+004 -1.9842549e+004 8.4083117e+004 8.0335805e+004 + -2.8014828e-008 3.5586822e-009 2.8239949e-008 1.7033000e-007 7.3167286e-009 -5.1406374e-008 5.1924463e-008 1.5276669e-011 + 2.5055925e+006 5.9143965e+006 6.4232455e+006 7.8362195e+004 1.5720355e+005 -1.0445320e+005 1.8874170e+005 1.9107711e+005 + 9.4163988e+005 2.9160585e+006 3.0643243e+006 6.8273219e+004 1.3735248e+005 -5.6098648e+004 1.4836698e+005 1.8633811e+005 + 1.9914608e+005 7.7309094e+005 7.9832875e+005 3.7267430e+004 8.1677211e+004 -2.0886350e+004 8.4305438e+004 8.0840305e+004 + -3.2399697e-008 3.9032688e-009 3.2633970e-008 -4.6906996e-007 2.0530806e-008 1.3436309e-007 1.3592260e-007 1.2036594e-011 + 2.6533963e+006 5.9113610e+006 6.4795605e+006 7.6365117e+004 1.5728245e+005 -1.1162958e+005 1.9287023e+005 1.9523556e+005 + 9.8992844e+005 2.9131605e+006 3.0767618e+006 6.6759344e+004 1.3732284e+005 -5.9386313e+004 1.4961383e+005 1.8819389e+005 + 2.0729872e+005 7.7207531e+005 7.9942044e+005 3.6760910e+004 8.1629070e+004 -2.1903369e+004 8.4516641e+004 8.1392102e+004 + -2.2805132e-008 6.1363101e-009 2.3616273e-008 4.1133166e-008 -7.2989636e-008 1.3862405e-009 7.3002795e-008 -1.1809220e-011 + 2.7976038e+006 5.9076040e+006 6.5365415e+006 7.4394539e+004 1.5735480e+005 -1.1863648e+005 1.9706634e+005 1.9971511e+005 + 1.0369303e+006 2.9097430e+006 3.0889850e+006 6.5261590e+004 1.3728359e+005 -6.2596434e+004 1.5088108e+005 1.9019308e+005 + 2.1519591e+005 7.7087275e+005 8.0034625e+005 3.6254188e+004 8.1566859e+004 -2.2891422e+004 8.4718180e+004 8.1988625e+004 + -3.9533401e-008 -6.3370402e-009 4.0038078e-008 -7.4479743e-007 -4.0712820e-009 2.2002715e-007 2.2006481e-007 2.2311042e-012 + 2.9374800e+006 5.9029855e+006 6.5934840e+006 7.2432234e+004 1.5740689e+005 -1.2542313e+005 2.0126572e+005 2.0449644e+005 + 1.0826008e+006 2.9058913e+006 3.1010045e+006 6.3759363e+004 1.3722648e+005 -6.5710344e+004 1.5214781e+005 1.9232706e+005 + 2.2288538e+005 7.6951238e+005 8.0114113e+005 3.5740340e+004 8.1494594e+004 -2.3853365e+004 8.4913789e+004 8.2627180e+004 + 6.4493277e-009 1.0770509e-008 1.2553792e-008 -4.5849018e-007 3.4013539e-009 1.3436782e-007 1.3441087e-007 4.0358827e-012 + 3.0723228e+006 5.8974770e+006 6.6497670e+006 7.0495602e+004 1.5743638e+005 -1.3194538e+005 2.0541616e+005 2.0956531e+005 + 1.1268108e+006 2.9016053e+006 3.1127183e+006 6.2270578e+004 1.3715266e+005 -6.8713359e+004 1.5340266e+005 1.9458838e+005 + 2.3037889e+005 7.6803594e+005 8.0184388e+005 3.5217656e+004 8.1412867e+004 -2.4788369e+004 8.5102984e+004 8.3304844e+004 + -4.6711499e-008 6.7774826e-009 4.7200619e-008 -7.9475291e-007 -6.6760208e-008 2.4763733e-007 2.5647842e-007 -7.5175421e-012 + 3.2015418e+006 5.8908865e+006 6.7046560e+006 6.8584672e+004 1.5743817e+005 -1.3818639e+005 2.0948092e+005 2.1491813e+005 + 1.1692649e+006 2.8968575e+006 3.1239340e+006 6.0795043e+004 1.3705095e+005 -7.1590742e+004 1.5462277e+005 1.9697420e+005 + 2.3760147e+005 7.6647719e+005 8.0245981e+005 3.4692785e+004 8.1322984e+004 -2.5688680e+004 8.5283852e+004 8.4019461e+004 + -7.6053848e-009 2.2144864e-009 7.9212263e-009 6.4883181e-007 -3.0335457e-008 -1.8512182e-007 1.8759086e-007 -3.8511416e-012 + 3.3247313e+006 5.8832005e+006 6.7576540e+006 6.6737148e+004 1.5741553e+005 -1.4415063e+005 2.1344567e+005 2.2054870e+005 + 1.2095865e+006 2.8916015e+006 3.1343993e+006 5.9369090e+004 1.3692172e+005 -7.4334250e+004 1.5579839e+005 1.9948080e+005 + 2.4442627e+005 7.6485956e+005 8.0296594e+005 3.4179281e+004 8.1222805e+004 -2.6541791e+004 8.5449461e+004 8.4769492e+004 + -2.3918792e-008 7.7386950e-009 2.5139531e-008 2.1198409e-007 -2.8163427e-008 -5.6646499e-008 6.3261396e-008 5.1301186e-012 + 3.4414593e+006 5.8744075e+006 6.8082530e+006 6.4963371e+004 1.5736905e+005 -1.4983403e+005 2.1729072e+005 2.2644609e+005 + 1.2474990e+006 2.8858223e+006 3.1439185e+006 5.7999645e+004 1.3676345e+005 -7.6932984e+004 1.5691695e+005 2.0210403e+005 + 2.5077434e+005 7.6316819e+005 8.0331406e+005 3.3685230e+004 8.1111508e+004 -2.7339740e+004 8.5595195e+004 8.5554133e+004 + -5.2850091e-008 3.3676875e-009 5.2957279e-008 6.8501606e-007 4.6857579e-008 -2.1258053e-007 2.1768352e-007 1.1567636e-011 + 3.5512058e+006 5.8646170e+006 6.8560040e+006 6.3268719e+004 1.5730209e+005 -1.5520831e+005 2.2098319e+005 2.3259328e+005 + 1.2829006e+006 2.8795298e+006 3.1523840e+006 5.6687469e+004 1.3658153e+005 -7.9374578e+004 1.5797100e+005 2.0483798e+005 + 2.5665131e+005 7.6136006e+005 8.0345444e+005 3.3210098e+004 8.0987609e+004 -2.8081803e+004 8.5718031e+004 8.6372688e+004 + 1.1287944e-009 1.1632666e-008 1.1687305e-008 4.7914305e-008 -1.1488149e-007 1.1077489e-008 1.1541432e-007 -7.1764816e-012 + 3.6534870e+006 5.8540325e+006 6.9005555e+006 6.1715305e+004 1.5722459e+005 -1.6023850e+005 2.2449042e+005 2.3897517e+005 + 1.3157634e+006 2.8727385e+006 3.1597248e+006 5.5487852e+004 1.3638309e+005 -8.1647813e+004 1.5895506e+005 2.0767570e+005 + 2.6208969e+005 7.5938844e+005 8.0334413e+005 3.2776977e+004 8.0849898e+004 -2.8769795e+004 8.5816125e+004 8.7222234e+004 + -2.5179272e-008 2.1974058e-009 2.5274975e-008 -7.1184701e-007 -1.2324617e-007 2.3866221e-007 2.6860616e-007 4.8316906e-012 + 3.7480118e+006 5.8428315e+006 6.9416330e+006 6.0226539e+004 1.5714308e+005 -1.6490569e+005 2.2778902e+005 2.4558311e+005 + 1.3460126e+006 2.8654860e+006 3.1658743e+006 5.4333711e+004 1.3617547e+005 -8.3747180e+004 1.5986666e+005 2.1061680e+005 + 2.6707538e+005 7.5721394e+005 8.0293350e+005 3.2355639e+004 8.0697773e+004 -2.9402285e+004 8.5887281e+004 8.8104984e+004 + -2.5102450e-008 -2.3967139e-010 2.5103594e-008 3.4692107e-007 -4.9519571e-009 -1.0192450e-007 1.0204472e-007 -4.5048409e-012 + 3.8348235e+006 5.8312050e+006 6.9791705e+006 5.8799574e+004 1.5707173e+005 -1.6922059e+005 2.3088339e+005 2.5241458e+005 + 1.3735215e+006 2.8577670e+006 3.1707088e+006 5.3227344e+004 1.3596302e+005 -8.5675102e+004 1.6070520e+005 2.1366016e+005 + 2.7153559e+005 7.5481881e+005 8.0217394e+005 3.1948500e+004 8.0529773e+004 -2.9973525e+004 8.5927039e+004 8.9020859e+004 + -8.4006196e-008 3.8461474e-009 8.4094196e-008 -4.1045138e-007 -4.8649227e-008 1.3345448e-007 1.4204522e-007 -5.4143356e-012 + 3.9140833e+006 5.8192805e+006 7.0131360e+006 5.7479301e+004 1.5702327e+005 -1.7319780e+005 2.3378148e+005 2.5946584e+005 + 1.3982678e+006 2.8495578e+006 3.1741350e+006 5.2211703e+004 1.3574611e+005 -8.7434805e+004 1.6146780e+005 2.1680220e+005 + 2.7545178e+005 7.5221081e+005 8.0105856e+005 3.1584516e+004 8.0347055e+004 -3.0482621e+004 8.5935094e+004 8.9967289e+004 + 9.0597307e-010 2.8867699e-009 3.0255956e-009 1.2014871e-007 -4.8108348e-008 -2.4556314e-008 5.4013199e-008 8.3844043e-013 + 3.9856723e+006 5.8071530e+006 7.0433380e+006 5.6252535e+004 1.5699381e+005 -1.7681398e+005 2.3645347e+005 2.6671500e+005 + 1.4204335e+006 2.8409295e+006 3.1762418e+006 5.1264871e+004 1.3553036e+005 -8.9022961e+004 1.6215291e+005 2.2003606e+005 + 2.7892256e+005 7.4939956e+005 7.9962331e+005 3.1241236e+004 8.0148453e+004 -3.0936590e+004 8.5911859e+004 9.0944273e+004 + 3.1358681e-008 6.3000725e-009 3.1985277e-008 -4.3287315e-007 -9.3635336e-008 1.5182724e-007 1.7837905e-007 7.8159701e-012 + 4.0493330e+006 5.7947970e+006 7.0694250e+006 5.5076172e+004 1.5696722e+005 -1.8003669e+005 2.3885542e+005 2.7413231e+005 + 1.4401731e+006 2.8320153e+006 3.1771700e+006 5.0340313e+004 1.3531769e+005 -9.0433125e+004 1.6275450e+005 2.2335038e+005 + 2.8203866e+005 7.4639319e+005 7.9790263e+005 3.0890502e+004 7.9936813e+004 -3.1342914e+004 8.5861938e+004 9.1949539e+004 + 4.7259256e-008 -5.4672018e-009 4.7574442e-008 -1.2350670e-006 2.2488450e-008 3.6528780e-007 3.6597936e-007 1.6200374e-011 diff --git a/pyFAST/input_output/tests/example_files/Bladed_out_ascii.%04 b/pyFAST/input_output/tests/example_files/Bladed_out_ascii.%04 new file mode 100644 index 0000000..d161002 --- /dev/null +++ b/pyFAST/input_output/tests/example_files/Bladed_out_ascii.%04 @@ -0,0 +1,39 @@ +FILE Steady_8ms_ASCII.$04 +ACCESS S +FORM F +RECL 4 +FORMAT R*4 +CONTENT 'POWPROD' +CONFIG 'STATIONARY' +NDIMENS 2 +DIMENS 9 200 +GENLAB 'Control variables' +VARIAB 'Time from start of simulation' 'Demanded power' 'Measured power' 'Demanded generator speed' 'Measured generator speed' 'Nominal pitch angle' 'Demanded generator torque' 'Nominal wind speed at hub position' 'Measured shaft power' +VARUNIT T P P A/T A/T A FL L/T P +AXISLAB 'Time' +AXIUNIT T +AXIMETH 2 +MIN 0.0000000E+000 +STEP 5.0000001E-002 +NVARS 0 +ULOADS 1.9993750E+002 0.0000000E+000 1.8192086E+006 9.1210831E+001 9.2047127E+001 -0.0000000E+000 1.9763861E+004 8.0003738E+000 1.8192086E+006 + 1.8998750E+002 0.0000000E+000 1.8180233E+006 9.1210831E+001 9.2043343E+001 -0.0000000E+000 1.9751873E+004 8.0003691E+000 1.8180233E+006 + 1.8998750E+002 0.0000000E+000 1.8180233E+006 9.1210831E+001 9.2043343E+001 -0.0000000E+000 1.9751873E+004 8.0003691E+000 1.8180233E+006 + 1.8998750E+002 0.0000000E+000 1.8180233E+006 9.1210831E+001 9.2043343E+001 -0.0000000E+000 1.9751873E+004 8.0003691E+000 1.8180233E+006 + 1.9953751E+002 0.0000000E+000 1.8194994E+006 9.1210831E+001 9.2069588E+001 -0.0000000E+000 1.9762295E+004 8.0003710E+000 1.8194994E+006 + 1.9188750E+002 0.0000000E+000 1.8173160E+006 9.1210831E+001 9.2007599E+001 -0.0000000E+000 1.9751729E+004 8.0003681E+000 1.8173160E+006 + 1.8998750E+002 0.0000000E+000 1.8180233E+006 9.1210831E+001 9.2043343E+001 -0.0000000E+000 1.9751873E+004 8.0003691E+000 1.8180233E+006 + 1.8998750E+002 0.0000000E+000 1.8180233E+006 9.1210831E+001 9.2043343E+001 -0.0000000E+000 1.9751873E+004 8.0003691E+000 1.8180233E+006 + 1.9943750E+002 0.0000000E+000 1.8194199E+006 9.1210831E+001 9.2072807E+001 -0.0000000E+000 1.9760760E+004 8.0003700E+000 1.8194199E+006 + 1.9403751E+002 0.0000000E+000 1.8173680E+006 9.1210831E+001 9.2005730E+001 -0.0000000E+000 1.9752684E+004 8.0003691E+000 1.8173680E+006 + 1.8998750E+002 0.0000000E+000 1.8180233E+006 9.1210831E+001 9.2043343E+001 -0.0000000E+000 1.9751873E+004 8.0003691E+000 1.8180233E+006 + 1.8998750E+002 0.0000000E+000 1.8180233E+006 9.1210831E+001 9.2043343E+001 -0.0000000E+000 1.9751873E+004 8.0003691E+000 1.8180233E+006 + 1.9983749E+002 0.0000000E+000 1.8193841E+006 9.1210831E+001 9.2055389E+001 -0.0000000E+000 1.9764021E+004 8.0003729E+000 1.8193841E+006 + 1.9203751E+002 0.0000000E+000 1.8175164E+006 9.1210831E+001 9.2023163E+001 -0.0000000E+000 1.9750629E+004 8.0003681E+000 1.8175164E+006 + 1.9108749E+002 0.0000000E+000 1.8191858E+006 9.1210831E+001 9.2048164E+001 -0.0000000E+000 1.9763398E+004 8.0003738E+000 1.8191858E+006 + 1.9188750E+002 0.0000000E+000 1.8173160E+006 9.1210831E+001 9.2007599E+001 -0.0000000E+000 1.9751729E+004 8.0003681E+000 1.8173160E+006 + 1.9953751E+002 0.0000000E+000 1.8194994E+006 9.1210831E+001 9.2069588E+001 -0.0000000E+000 1.9762295E+004 8.0003710E+000 1.8194994E+006 + 1.9188750E+002 0.0000000E+000 1.8173160E+006 9.1210831E+001 9.2007599E+001 -0.0000000E+000 1.9751729E+004 8.0003681E+000 1.8173160E+006 +MAXTIME 9.9500001E+000 0.0000000E+000 9.5500001E+000 0.0000000E+000 9.4500001E+000 0.0000000E+000 9.8500001E+000 1.1000000E+000 9.5500001E+000 +MINTIME 0.0000000E+000 0.0000000E+000 1.9000000E+000 0.0000000E+000 4.0500001E+000 0.0000000E+000 2.0500000E+000 1.9000000E+000 1.9000000E+000 +MEAN 1.9496250E+002 0.0000000E+000 1.8185299E+006 9.1210918E+001 9.2041807E+001 -0.0000000E+000 1.9757653E+004 8.0003668E+000 1.8185299E+006 diff --git a/pyFAST/input_output/tests/example_files/Bladed_out_ascii.%41 b/pyFAST/input_output/tests/example_files/Bladed_out_ascii.%41 new file mode 100644 index 0000000..845d7f3 --- /dev/null +++ b/pyFAST/input_output/tests/example_files/Bladed_out_ascii.%41 @@ -0,0 +1,98 @@ +FILE Steady_8ms_ASCII.$41 +ACCESS S +FORM F +RECL 4 +FORMAT R*4 +CONTENT 'POWPROD' +CONFIG 'STATIONARY' +NDIMENS 3 +DIMENS 8 4 200 +GENLAB 'Blade 1 Loads: Root axes' +VARIAB 'Blade 1 Mx (Root axes)' 'Blade 1 My (Root axes)' 'Blade 1 Mxy (Root axes)' 'Blade 1 Mz (Root axes)' 'Blade 1 Fx (Root axes)' 'Blade 1 Fy (Root axes)' 'Blade 1 Fxy (Root axes)' 'Blade 1 Fz (Root axes)' +VARUNIT FL FL FL FL F F F F +AXISLAB 'Distance along blade' +AXIUNIT L +AXIMETH 3 +AXIVAL 0.0000000 20.2000008 40.2000008 61.5000000 +AXISLAB 'Time' +AXIUNIT T +AXIMETH 2 +MIN 0.0000000E+000 +STEP 5.0000001E-002 +NVARS 0 +ULOADS 4.2700365E+006 5.6879880E+006 7.1124130E+006 4.8522203E+004 1.5644395E+005 -1.9203038E+005 2.4769008E+005 3.3316125E+005 + -2.9789733E+006 5.3133240E+006 6.0914440E+006 1.2057229E+005 1.4682472E+005 1.6034870E+005 2.1741481E+005 3.4386759E+005 + 2.0446934E+006 5.9183880E+006 6.2616360E+006 8.4336164E+004 1.5695905E+005 -8.2032430E+004 1.7710297E+005 1.8070602E+005 + -4.2647966E+005 4.5880585E+006 4.6078375E+006 7.7151039E+004 1.3377806E+005 3.1631107E+004 1.3746672E+005 5.1026584E+005 + 4.2356305E+006 5.7358915E+006 7.1302885E+006 5.0564754E+004 1.5685191E+005 -1.8982881E+005 2.4624681E+005 3.0882388E+005 + -2.5779445E+005 4.5962085E+006 4.6034325E+006 7.4489516E+004 1.3382472E+005 2.3468830E+004 1.3586700E+005 5.1186166E+005 + -2.7208935E+006 5.5064020E+006 6.1419645E+006 1.2474416E+005 1.4972272E+005 1.4762898E+005 2.1026461E+005 2.8009394E+005 + 4.1259538E+006 5.5505305E+006 6.9160600E+006 4.6778574E+004 1.5441570E+005 -1.8595255E+005 2.4170759E+005 3.8194009E+005 + 3.1288033E+006 5.8948165E+006 6.6737000E+006 6.9661375E+004 1.5744019E+005 -1.3468823E+005 2.0719153E+005 2.1186481E+005 + -4.2647966E+005 4.5880585E+006 4.6078375E+006 7.7151039E+004 1.3377806E+005 3.1631107E+004 1.3746672E+005 5.1026584E+005 + -2.9789733E+006 5.3133240E+006 6.0914440E+006 1.2057229E+005 1.4682472E+005 1.6034870E+005 2.1741481E+005 3.4386759E+005 + 4.2700365E+006 5.6879880E+006 7.1124130E+006 4.8522203E+004 1.5644395E+005 -1.9203038E+005 2.4769008E+005 3.3316125E+005 + 4.2700365E+006 5.6879880E+006 7.1124130E+006 4.8522203E+004 1.5644395E+005 -1.9203038E+005 2.4769008E+005 3.3316125E+005 + -8.3800250E+004 4.6173065E+006 4.6180670E+006 7.1262977E+004 1.3421600E+005 1.5121382E+004 1.3506514E+005 5.1311459E+005 + 4.5589291E+005 4.7270350E+006 4.7489680E+006 6.2041895E+004 1.3680088E+005 -1.0514382E+004 1.3720434E+005 5.1503394E+005 + 7.8848400E+005 5.8719360E+006 5.9246385E+006 9.9047602E+004 1.5542952E+005 -2.0703334E+004 1.5680231E+005 1.6949625E+005 +MAXTIME 3.7000001E+000 9.1000001E+000 3.5500001E+000 7.4500001E+000 2.8500000E+000 7.0500001E+000 3.7000001E+000 5.4000001E+000 +MINTIME 7.0500001E+000 5.6500001E+000 5.6000001E+000 4.0000001E+000 5.6500001E+000 3.7000001E+000 5.5500001E+000 2.1000000E+000 +MEAN 3.4850004E+005 5.5026298E+006 6.0608755E+006 9.0276320E+004 1.5060866E+005 -1.4724466E+003 1.9295544E+005 3.0861504E+005 +ULOADS 1.5020099E+006 2.7688520E+006 3.1500120E+006 4.5472645E+004 1.3373100E+005 -9.5216750E+004 1.6416519E+005 2.4616223E+005 + -8.7064644E+005 2.5586395E+006 2.7027135E+006 9.7197477E+004 1.2408887E+005 6.6752070E+004 1.4090381E+005 2.5959136E+005 + 7.3966331E+005 2.9209198E+006 3.0131170E+006 7.4228500E+004 1.3731583E+005 -4.2335262E+004 1.4369381E+005 1.8050570E+005 + -1.2726105E+005 2.1557825E+006 2.1595355E+006 6.5646469E+004 1.0890295E+005 1.4002102E+004 1.0979941E+005 3.2995250E+005 + 1.4483018E+006 2.8279668E+006 3.1772588E+006 4.9958938E+004 1.3521827E+005 -9.1004945E+004 1.6299042E+005 2.2480972E+005 + -7.3765719E+004 2.1567145E+006 2.1579758E+006 6.4073789E+004 1.0870848E+005 1.0373180E+004 1.0920227E+005 3.3075084E+005 + -7.7556644E+005 2.6899680E+006 2.7995413E+006 1.0178959E+005 1.2872822E+005 6.0017891E+004 1.4203205E+005 2.2576005E+005 + 1.4286433E+006 2.6599203E+006 3.0193040E+006 4.3070836E+004 1.2993030E+005 -9.0843148E+004 1.5853819E+005 2.7540988E+005 + 8.9220344E+005 2.9183510E+006 3.0516880E+006 6.9791563E+004 1.3736886E+005 -5.2744469E+004 1.4714681E+005 1.8463580E+005 + -7.3765719E+004 2.1567145E+006 2.1579758E+006 6.4073789E+004 1.0870848E+005 1.0373180E+004 1.0920227E+005 3.3075084E+005 + -8.7045550E+005 2.5633695E+006 2.7071305E+006 9.7515688E+004 1.2422506E+005 6.6753063E+004 1.4102425E+005 2.5798398E+005 + 1.5017208E+006 2.7579278E+006 3.1402755E+006 4.4930387E+004 1.3339833E+005 -9.5279320E+004 1.6393066E+005 2.4983367E+005 + 1.4996814E+006 2.7792348E+006 3.1580358E+006 4.6043758E+004 1.3402113E+005 -9.4973352E+004 1.6426077E+005 2.4251633E+005 + -1.7724371E+004 2.1642385E+006 2.1643110E+006 6.1991012E+004 1.0884046E+005 6.6025015E+003 1.0904054E+005 3.3139113E+005 + 2.2222330E+005 2.2373308E+006 2.2483400E+006 5.1362223E+004 1.1190833E+005 -9.4503721E+003 1.1230665E+005 3.3280638E+005 + 3.8236619E+005 2.8998525E+006 2.9249528E+006 8.3720820E+004 1.3617323E+005 -1.7937281E+004 1.3734955E+005 1.7692311E+005 +MAXTIME 3.6500001E+000 9.0500001E+000 3.3500000E+000 8.5000001E-001 9.2000001E+000 4.0000001E-001 3.6000001E+000 5.4000001E+000 +MINTIME 7.0000001E+000 5.7000001E+000 5.6500001E+000 4.0500001E+000 5.6500001E+000 3.7000001E+000 5.6000001E+000 2.1000000E+000 +MEAN 2.1848354E+005 2.6737498E+006 2.8073533E+006 7.5804670E+004 1.2867567E+005 -7.7045250E+003 1.4065615E+005 2.3958896E+005 +ULOADS 2.9046156E+005 7.2528713E+005 7.8128694E+005 2.8794514E+004 7.8395266E+004 -3.2543189E+004 8.4881547E+004 9.8922102E+004 + -1.1172891E+005 6.6371581E+005 6.7305425E+005 4.4604246E+004 7.1751977E+004 1.8094299E+004 7.3998313E+004 1.0512230E+005 + 1.7363175E+005 7.7465906E+005 7.9387950E+005 3.8755430E+004 8.1709758E+004 -1.7712359E+004 8.3607484E+004 7.9477367E+004 + -4.9514473E+003 5.4754681E+005 5.4756919E+005 3.2854910E+004 6.1115555E+004 4.2433618E+003 6.1262688E+004 1.2548838E+005 + 2.5665131E+005 7.6136006E+005 8.0345444E+005 3.3210098E+004 8.0987609E+004 -2.8081803E+004 8.5718031E+004 8.6372688E+004 + -4.9514473E+003 5.4754681E+005 5.4756919E+005 3.2854910E+004 6.1115555E+004 4.2433618E+003 6.1262688E+004 1.2548838E+005 + -7.8629539E+004 7.1847344E+005 7.2276319E+005 4.7314051E+004 7.6482289E+004 1.3781252E+004 7.7713984E+004 9.0584344E+004 + 6.4919984E+004 5.7476350E+005 5.7841825E+005 2.6343789E+004 6.3905578E+004 -4.5262729E+003 6.4065672E+004 1.2647440E+005 + 1.8220630E+005 7.7441325E+005 7.9555956E+005 3.8273082E+004 8.1719844E+004 -1.8781203E+004 8.3850266E+004 7.9880797E+004 + 3.6977095E+003 5.4791525E+005 5.4792769E+005 3.2580852E+004 6.1052281E+004 3.1584578E+003 6.1133926E+004 1.2573148E+005 + -1.1172891E+005 6.6371581E+005 6.7305425E+005 4.4604246E+004 7.1751977E+004 1.8094299E+004 7.3998313E+004 1.0512230E+005 + 2.9046156E+005 7.2528713E+005 7.8128694E+005 2.8794514E+004 7.8395266E+004 -3.2543189E+004 8.4881547E+004 9.8922102E+004 + 2.7338353E+005 7.5369719E+005 8.0174681E+005 3.1713813E+004 8.0449734E+004 -3.0211346E+004 8.5935352E+004 8.9429211E+004 + 3.6977095E+003 5.4791525E+005 5.4792769E+005 3.2580852E+004 6.1052281E+004 3.1584578E+003 6.1133926E+004 1.2573148E+005 + 6.4919984E+004 5.7476350E+005 5.7841825E+005 2.6343789E+004 6.3905578E+004 -4.5262729E+003 6.4065672E+004 1.2647440E+005 + 1.1054431E+005 7.6992275E+005 7.7781819E+005 4.2014602E+004 8.1056789E+004 -9.8249951E+003 8.1650070E+004 7.8204375E+004 +MAXTIME 3.6500001E+000 9.1000001E+000 9.6500001E+000 1.0000000E+000 9.1500001E+000 6.9500001E+000 3.2000000E+000 5.4000001E+000 +MINTIME 6.9500001E+000 5.7500001E+000 5.7500001E+000 5.4000001E+000 5.7000001E+000 3.6500001E+000 5.7000001E+000 8.7500001E+000 +MEAN 7.2587775E+004 7.0022096E+005 7.1772032E+005 3.8265158E+004 7.5225065E+004 -5.1883994E+003 7.7449200E+004 9.7516890E+004 +ULOADS 6.0416113E-008 3.5432581E-009 6.0519923E-008 -2.0450938E-007 -2.4397863E-008 7.8331297E-008 8.2042959E-008 2.0619950E-011 + -1.3088868E-007 1.5385883E-008 1.3178988E-007 6.9784852E-007 3.5368402E-009 -2.1129225E-007 2.1132185E-007 -1.3343993E-011 + -1.1985347E-009 1.8815371E-008 1.8853505E-008 8.1898783E-008 -6.9199132E-008 -3.2049122E-008 7.6260520E-008 -9.9475983E-014 + -4.4389893E-008 -1.3403322E-008 4.6369298E-008 1.0521391E-006 1.0590171E-008 -3.1203911E-007 3.1221876E-007 -5.0732751E-012 + -1.3088868E-007 1.5385883E-008 1.3178988E-007 6.9784852E-007 3.5368402E-009 -2.1129225E-007 2.1132185E-007 -1.3343993E-011 + 1.2749884E-010 2.7838443E-010 3.0619249E-010 8.0451557E-008 -3.3626876E-008 -3.0700917E-008 4.5533646E-008 -1.9440449E-011 + -2.8709245E-008 1.0437747E-008 3.0547788E-008 1.8723102E-006 1.1385310E-009 -5.5270471E-007 5.5270590E-007 -5.5990768E-012 + 2.4472435E-008 1.9779200E-010 2.4473234E-008 -1.9042171E-006 2.3550584E-008 5.6118262E-007 5.6167653E-007 2.0889956E-012 + -7.0574622E-009 -3.7919650E-009 8.0116642E-009 -2.6528991E-007 6.9816991E-008 7.1066232E-008 9.9623399E-008 3.4106051E-013 + -4.6190296E-009 9.0298391E-009 1.0142654E-008 2.8418345E-008 -1.3708387E-007 3.3232084E-009 1.3712415E-007 -1.4210855E-014 + 3.4378527E-008 -2.6722615E-009 3.4482227E-008 -1.7747884E-006 -4.8753279E-008 5.6212281E-007 5.6423306E-007 9.4644292E-012 + -2.8709245E-008 1.0437747E-008 3.0547788E-008 1.8723102E-006 1.1385310E-009 -5.5270471E-007 5.5270590E-007 -5.5990768E-012 + 3.4378527E-008 -2.6722615E-009 3.4482227E-008 -1.7747884E-006 -4.8753279E-008 5.6212281E-007 5.6423306E-007 9.4644292E-012 + -6.8960526E-010 5.4174487E-009 5.4611635E-009 5.4492411E-009 1.0867183E-009 -2.1611086E-009 2.4189557E-009 -1.2875034E-011 + 4.7877410E-008 -7.9480849E-009 4.8532655E-008 -1.4244058E-006 -1.7717213E-008 4.5244346E-007 4.5279023E-007 2.9871217E-011 + 1.2749884E-010 2.7838443E-010 3.0619249E-010 8.0451557E-008 -3.3626876E-008 -3.0700917E-008 4.5533646E-008 -1.9440449E-011 +MAXTIME 4.7000001E+000 7.1000001E+000 3.4000001E+000 9.0500001E+000 8.7500001E+000 3.7500001E+000 3.7500001E+000 3.9000001E+000 +MINTIME 3.4000001E+000 2.8000000E+000 6.6000001E+000 2.3000000E+000 1.9000000E+000 9.0500001E+000 6.0500001E+000 6.6000001E+000 +MEAN -9.8774774E-009 2.1970034E-009 2.0335806E-008 -2.5283964E-008 -1.8726793E-008 1.0430081E-008 1.1521881E-007 2.2351543E-012 diff --git a/pyFAST/input_output/tests/example_files/Bladed_out_binary.$04 b/pyFAST/input_output/tests/example_files/Bladed_out_binary.$04 new file mode 100644 index 0000000000000000000000000000000000000000..1de5350ba4ed9758f90746f360d084a70eefae24 GIT binary patch literal 21600 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 CL9#6X literal 0 HcmV?d00001 diff --git a/pyFAST/input_output/tests/example_files/Bladed_out_binary.$41 b/pyFAST/input_output/tests/example_files/Bladed_out_binary.$41 new file mode 100644 index 0000000000000000000000000000000000000000..485f293ce5ffdc54c6cc0e29edaabb2a42c45501 GIT binary patch literal 76800 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 ve;Kh!*IB=Lo;~vftY+$Rh#J+m z5H&XOZf z2T6CgLy1SYF^Kujk{o$FG_71G3{QeuTnvYO_ RZLRlk`zROa$P)h369Bf;nZp18 literal 0 HcmV?d00001 diff --git a/pyFAST/input_output/tests/example_files/Bladed_out_binary_case2.$06 b/pyFAST/input_output/tests/example_files/Bladed_out_binary_case2.$06 new file mode 100644 index 0000000000000000000000000000000000000000..ee265621fdb1980dadab440fb7210f7669400473 GIT binary patch literal 160 zcmXqEcXM03@1e_f7p`rJ3g z)z>WK$c39phr0`-9Aj68I599Z*hd4+P~mR3iz>6QDblL3mpEl^XWOS>ckjn(uWJ?? z?Gt8(*c{rsYHyoGgj0e^kL#m1hK7U;;37z8(iH>=-{y~fxkN3PaBZJCx` zcb2f7<$;r4nXxPF9o7ijWX_(uclX^;rwe|~t|^h4M-ILgI2Zm<00B%Oij45`{ ze6(!duc)=3W2R(x@E4ce;dLjy>i#da|4^u8V>e^|UW?PAPClm_To1L&9628*b9ldi zo#UsE2s7p?PO%e~Q?aRCUu*AnO3^O6kIRl<>$KP3lI8XX1pREBO*ZcJjtzHme$wpP zxl{ItOSJ!C!JT%F(|V2S+TCAz$}3oTnf(VA7n>~8 zZF{vZhB?hJZE{WNk~*@&CH`=xwyh(J5W&fUTM~95?swP3jGH3Yk~ydW1pp$dqe2htrj99Cfew!NbFG%5=N_=PEYUcWdoM z4#?SAwy@iMxO>8Df8G-N31TWXn{{^Vz5O=C>85q9>vBu6BSxYrhZ`?hIhML3!XwOP zj-Bl{1smq@I(z-!qIN6l{@Q+BaN6t8-?{b|{G@G8zFWCB^me$@#i%OR_Yb*`9C7kF zToP&Oc)2_n?hc-ni|ukPq-~@x)!E-~3QB3im4O5W)hnZZ@>H{9@W_-y|#Ki@&8iBrsrn((@^n~X%V)1l6X znu6J`%^=}&0?XpG=mi=mIl1{3_fY9~7+jS(fquqm$q3?*T$Y<~hke=%c8)p0=pm<> z+MNk~>E7l`?5w@p3^uErV5rDxa>~$P+~{qvwcfEASWWR3L4W47tz=D@D9BCtp2D!h zpgQIgBu0EX&opB27Rb$y*Z_CVVUWJ?3EUH(Do0%n=8JpMU;l~wU*W#)Fv$2Vf}58X sZIz$mu%Pct%|KI4f$~^quM8Æ|‘£Èo«£H»Æ‡I‘‰*Iì LH[2I(®™HÆÐ\¥È—i¥H \ No newline at end of file diff --git a/pyFAST/input_output/tests/example_files/Bladed_out_binary_case2.$25 b/pyFAST/input_output/tests/example_files/Bladed_out_binary_case2.$25 new file mode 100644 index 0000000000000000000000000000000000000000..705e757736e38b62db19b1d92a5a98f49b932e7b GIT binary patch literal 640 zcmV-`0)PDyJnTtQTdGUX{;ErmZn;P5ii=3BH(tgcw~I*2sT;~Jak@zYdrwOU$xus@ zOt?qU{f$U3bUDTq9F9m<#nH*B73WDo)Tv7_ajHvZNfb!R(1%D+Xh_Dq^@m91E*r`w zg}F)U22o2mPf|-wwi8JFo{UK0_AJJcxr|7P&(O)*5!gvSz^qFtN3BZIs4VT4IYne#n5*x~p+_p&z2U1IhL{m$!DGW%;XqHI449CR9vjLJS+hw{?NUo~9aKwi7Ntjh z-k3-&B*Vo<=9owwuh7Z9CCo|q$+1fYKe9{oNr6WnfR{*vD)YrrjF(8K^cu=any*Q= z!Bk7p;#Es^uYX4WWAqrliH6@QO(LJR8b{60b?rK3GeYSy@Ys0qRE}n~zAVE33tTp^r$l>(I&7 zRJ}; literal 0 HcmV?d00001 diff --git a/pyFAST/input_output/tests/example_files/Bladed_out_binary_case2.$31 b/pyFAST/input_output/tests/example_files/Bladed_out_binary_case2.$31 new file mode 100644 index 0000000000000000000000000000000000000000..f969429255b6e798c090d7f769043658fbd719cf GIT binary patch literal 120 zcmV-;0Eho!EUiDim*2sq$g@Ho-i<%rYxKeNIIcn#r&d1+fdj(jGN?jX%?UrEmb7llII a$C$sZ2q41P3VTAvUBJK8*B`=H*K0y>dOdgm literal 0 HcmV?d00001 diff --git a/pyFAST/input_output/tests/example_files/Bladed_out_binary_case2.$37 b/pyFAST/input_output/tests/example_files/Bladed_out_binary_case2.$37 new file mode 100644 index 0000000..0704a14 --- /dev/null +++ b/pyFAST/input_output/tests/example_files/Bladed_out_binary_case2.$37 @@ -0,0 +1,522 @@ + 1.00000001e-007 1.00000001e-007 1.00000001e-007 9.99999997e-007 3.90000000e+001 + 1.09999996e-006 9.99999997e-007 9.99999997e-007 6.06372214e-006 4.30000000e+001 + 7.16372188e-006 6.06372214e-006 6.06372214e-006 2.23819734e-005 3.10000000e+001 + 9.71269947e-006 2.23819734e-005 2.54897736e-006 1.25186389e-005 3.90000000e+001 + 9.99004078e-006 1.25186389e-005 2.77341599e-007 2.77341587e-006 4.50000000e+001 + 1.04492265e-005 2.77341587e-006 4.59185827e-007 4.15931822e-007 2.70000000e+001 + 1.08651584e-005 4.15931822e-007 4.15931822e-007 2.73320461e-006 4.30000000e+001 + 1.35983628e-005 2.73320461e-006 2.73320461e-006 1.22481479e-005 2.30000000e+001 + 2.58465116e-005 1.22481479e-005 1.22481479e-005 4.59457588e-005 2.30000000e+001 + 7.17922667e-005 4.59457588e-005 4.59457588e-005 1.31557274e-004 2.30000000e+001 + 2.03349555e-004 1.31557274e-004 1.31557274e-004 6.59651414e-004 4.30000000e+001 + 6.16022269e-004 6.59651414e-004 4.12672729e-004 1.29619334e-003 2.30000000e+001 + 1.91221561e-003 1.29619334e-003 1.29619334e-003 1.28277950e-003 4.80000000e+001 + 3.19499522e-003 1.28277950e-003 1.28277950e-003 1.51398242e-003 4.80000000e+001 + 4.70897742e-003 1.51398242e-003 1.51398242e-003 1.40180590e-003 4.80000000e+001 + 6.11078367e-003 1.40180590e-003 1.40180590e-003 1.49370986e-003 4.80000000e+001 + 7.60449329e-003 1.49370986e-003 1.49370986e-003 2.41702050e-003 4.80000000e+001 + 1.00215133e-002 2.41702050e-003 2.41702050e-003 4.11686441e-003 4.80000000e+001 + 1.41383782e-002 4.11686441e-003 4.11686441e-003 1.18834423e-002 4.80000000e+001 + 2.60218214e-002 1.18834423e-002 1.18834423e-002 1.57951973e-002 4.80000000e+001 + 4.18170169e-002 1.57951973e-002 1.57951973e-002 1.73934270e-002 4.10000000e+001 + 5.00000007e-002 8.18298198e-003 8.18298198e-003 2.27410011e-002 4.10000000e+001 + 7.27410018e-002 2.27410011e-002 2.27410011e-002 2.37777457e-002 9.00000000e+000 + 9.05157849e-002 2.37777457e-002 1.77747831e-002 1.97250992e-002 7.00000000e+000 + 1.00000001e-001 9.48421657e-003 9.48421657e-003 2.28166748e-002 1.00000000e+001 + 1.10935345e-001 2.28166748e-002 1.09353438e-002 1.14226975e-002 5.00000000e+000 + 1.22358046e-001 1.14226975e-002 1.14226975e-002 1.73314195e-002 1.10000000e+001 + 1.39689460e-001 1.73314195e-002 1.73314195e-002 2.22731773e-002 5.00000000e+000 + 1.50000006e-001 1.03105409e-002 1.03105409e-002 2.36999970e-002 5.00000000e+000 + 1.60120085e-001 2.36999970e-002 1.01200854e-002 1.07514914e-002 9.00000000e+000 + 1.70871586e-001 1.07514914e-002 1.07514914e-002 1.90458894e-002 9.00000000e+000 + 1.89917475e-001 1.90458894e-002 1.90458894e-002 2.06310935e-002 5.00000000e+000 + 2.00000003e-001 1.00825354e-002 1.00825354e-002 2.07613520e-002 1.10000000e+001 + 2.20761359e-001 2.07613520e-002 2.07613520e-002 2.20867749e-002 1.10000000e+001 + 2.42848128e-001 2.20867749e-002 2.20867749e-002 2.46330742e-002 9.00000000e+000 + 2.50000000e-001 7.15187239e-003 7.15187239e-003 2.47527175e-002 8.00000000e+000 + 2.74752706e-001 2.47527175e-002 2.47527175e-002 2.45418195e-002 5.00000000e+000 + 2.89711207e-001 2.45418195e-002 1.49584832e-002 1.37050040e-002 9.00000000e+000 + 3.00000012e-001 1.02888010e-002 1.02888010e-002 1.75118446e-002 9.00000000e+000 + 3.15365672e-001 1.75118446e-002 1.53656732e-002 1.55477561e-002 9.00000000e+000 + 3.30913424e-001 1.55477561e-002 1.55477561e-002 2.09076032e-002 5.00000000e+000 + 3.49999994e-001 1.90865714e-002 1.90865714e-002 1.82817988e-002 5.00000000e+000 + 3.68281811e-001 1.82817988e-002 1.82817988e-002 2.65394375e-002 8.00000000e+000 + 3.87463331e-001 2.65394375e-002 1.91815235e-002 2.10077241e-002 9.00000000e+000 + 4.00000006e-001 1.25366775e-002 1.25366775e-002 1.20385019e-002 2.90000000e+001 + 4.12038505e-001 1.20385019e-002 1.20385019e-002 2.06891522e-002 9.00000000e+000 + 4.27440852e-001 2.06891522e-002 1.54023534e-002 1.85789559e-002 2.90000000e+001 + 4.46019828e-001 1.85789559e-002 1.85789559e-002 2.34137960e-002 9.00000000e+000 + 4.50000018e-001 3.98018956e-003 3.98018956e-003 5.95660228e-003 2.90000000e+001 + 4.55956608e-001 5.95660228e-003 5.95660228e-003 8.34033452e-003 1.10000000e+001 + 4.64296937e-001 8.34033452e-003 8.34033452e-003 1.60274003e-002 3.70000000e+001 + 4.80324358e-001 1.60274003e-002 1.60274003e-002 2.28881817e-002 9.00000000e+000 + 5.00000000e-001 1.96756627e-002 1.96756627e-002 2.08391380e-002 7.00000000e+000 + 5.20839155e-001 2.08391380e-002 2.08391380e-002 2.20011361e-002 9.00000000e+000 + 5.42840302e-001 2.20011361e-002 2.20011361e-002 2.21460555e-002 5.00000000e+000 + 5.50000012e-001 7.15972669e-003 7.15972669e-003 7.09751016e-003 1.50000000e+001 + 5.57097495e-001 7.09751016e-003 7.09751016e-003 1.50501607e-002 1.50000000e+001 + 5.72147667e-001 1.50501607e-002 1.50501607e-002 1.86319258e-002 1.50000000e+001 + 5.90779603e-001 1.86319258e-002 1.86319258e-002 2.77909208e-002 1.10000000e+001 + 6.00000024e-001 9.22040362e-003 9.22040362e-003 1.09847970e-002 9.00000000e+000 + 6.10984802e-001 1.09847970e-002 1.09847970e-002 1.93563215e-002 7.00000000e+000 + 6.30341113e-001 1.93563215e-002 1.93563215e-002 2.88786851e-002 8.00000000e+000 + 6.50000036e-001 1.96588822e-002 1.96588822e-002 2.04996169e-002 3.70000000e+001 + 6.70499623e-001 2.04996169e-002 2.04996169e-002 2.05910653e-002 1.50000000e+001 + 6.91090703e-001 2.05910653e-002 2.05910653e-002 2.31486820e-002 3.60000000e+001 + 6.99999988e-001 8.90931860e-003 8.90931860e-003 9.29251499e-003 1.50000000e+001 + 7.09292531e-001 9.29251499e-003 9.29251499e-003 1.72127504e-002 3.50000000e+001 + 7.26505280e-001 1.72127504e-002 1.72127504e-002 2.48161796e-002 5.00000000e+000 + 7.50000000e-001 2.34947335e-002 2.34947335e-002 2.19880082e-002 1.50000000e+001 + 7.71988034e-001 2.19880082e-002 2.19880082e-002 2.50521209e-002 9.00000000e+000 + 7.97040164e-001 2.50521209e-002 2.50521209e-002 2.53270753e-002 1.10000000e+001 + 8.00000012e-001 2.95987120e-003 2.95987120e-003 5.28612360e-003 3.70000000e+001 + 8.05286109e-001 5.28612360e-003 5.28612360e-003 1.13487355e-002 5.00000000e+000 + 8.16634893e-001 1.13487355e-002 1.13487355e-002 1.96028911e-002 3.70000000e+001 + 8.33570540e-001 1.96028911e-002 1.69356633e-002 1.66237634e-002 5.00000000e+000 + 8.46137583e-001 1.64294783e-002 1.25670442e-002 1.32564399e-002 1.50000000e+001 + 8.50000024e-001 3.86243360e-003 3.86243360e-003 4.46102768e-003 1.50000000e+001 + 8.54461014e-001 4.46102768e-003 4.46102768e-003 9.75258183e-003 4.50000000e+001 + 8.64213645e-001 9.75258183e-003 9.75258183e-003 1.58354137e-002 1.50000000e+001 + 8.80049050e-001 1.58354137e-002 1.58354137e-002 1.75212231e-002 1.50000000e+001 + 8.95768821e-001 1.75212231e-002 1.57197770e-002 2.06265133e-002 1.50000000e+001 + 9.00000036e-001 4.23120055e-003 4.23120055e-003 2.06265133e-002 1.50000000e+001 + 9.20626521e-001 2.06265133e-002 2.06265133e-002 2.22139768e-002 5.00000000e+000 + 9.40020025e-001 2.22139768e-002 1.93934869e-002 2.28900816e-002 1.10000000e+001 + 9.49999988e-001 9.98000056e-003 9.98000056e-003 9.78896488e-003 1.50000000e+001 + 9.59788978e-001 9.78896488e-003 9.78896488e-003 1.76928565e-002 5.00000000e+000 + 9.77481842e-001 1.76928565e-002 1.76928565e-002 2.71279123e-002 1.50000000e+001 + 1.00000000e+000 2.25181784e-002 2.25181784e-002 2.03979798e-002 5.00000000e+000 + 1.02039802e+000 2.03979798e-002 2.03979798e-002 2.06671413e-002 5.00000000e+000 + 1.04106510e+000 2.06671413e-002 2.06671413e-002 2.36375257e-002 3.70000000e+001 + 1.05000007e+000 8.93487968e-003 8.93487968e-003 9.69749875e-003 1.10000000e+001 + 1.05969751e+000 9.69749875e-003 9.69749875e-003 1.75628942e-002 4.50000000e+001 + 1.07726038e+000 1.75628942e-002 1.75628942e-002 2.08862461e-002 1.50000000e+001 + 1.09814668e+000 2.08862461e-002 2.08862461e-002 2.42779832e-002 7.00000000e+000 + 1.10000002e+000 1.85336126e-003 1.85336126e-003 2.42779832e-002 7.00000000e+000 + 1.12427795e+000 2.42779832e-002 2.42779832e-002 2.43623611e-002 4.50000000e+001 + 1.14864039e+000 2.43623611e-002 2.43623611e-002 2.45558210e-002 1.50000000e+001 + 1.14999998e+000 1.35965587e-003 1.35965587e-003 2.45558210e-002 1.50000000e+001 + 1.17455590e+000 2.45558210e-002 2.45558210e-002 2.24102233e-002 1.50000000e+001 + 1.19696605e+000 2.24102233e-002 2.24102233e-002 2.10259221e-002 1.50000000e+001 + 1.20000005e+000 3.03395698e-003 3.03395698e-003 2.10259221e-002 1.10000000e+001 + 1.22102594e+000 2.10259221e-002 2.10259221e-002 2.21147053e-002 1.50000000e+001 + 1.24314070e+000 2.21147053e-002 2.21147053e-002 2.83976886e-002 3.70000000e+001 + 1.25000000e+000 6.85937377e-003 6.85937377e-003 7.46589294e-003 3.70000000e+001 + 1.25746596e+000 7.46589294e-003 7.46589294e-003 1.21333012e-002 3.70000000e+001 + 1.26959920e+000 1.21333012e-002 1.21333012e-002 1.41541725e-002 4.50000000e+001 + 1.28375340e+000 1.41541725e-002 1.41541725e-002 1.98099334e-002 4.50000000e+001 + 1.30000007e+000 1.62466336e-002 1.62466336e-002 2.18388550e-002 9.00000000e+000 + 1.32183886e+000 2.18388550e-002 2.18388550e-002 2.42168680e-002 1.10000000e+001 + 1.34605575e+000 2.42168680e-002 2.42168680e-002 2.94979978e-002 9.00000000e+000 + 1.35000002e+000 3.94427730e-003 3.94427730e-003 2.94979978e-002 7.00000000e+000 + 1.37031102e+000 2.94979978e-002 2.03109570e-002 2.52324305e-002 7.00000000e+000 + 1.39554346e+000 2.52324305e-002 2.52324305e-002 3.07907611e-002 5.00000000e+000 + 1.39999998e+000 4.45661275e-003 4.45661275e-003 3.07907611e-002 2.90000000e+001 + 1.42613304e+000 3.07907611e-002 2.61330642e-002 2.70701032e-002 1.10000000e+001 + 1.45000005e+000 2.38669366e-002 2.38669366e-002 2.49720532e-002 1.50000000e+001 + 1.47497213e+000 2.49720532e-002 2.49720532e-002 2.92371660e-002 7.00000000e+000 + 1.50000000e+000 2.50279475e-002 2.50279475e-002 2.61732060e-002 1.50000000e+001 + 1.52617323e+000 2.61732060e-002 2.61732060e-002 2.59126406e-002 5.00000000e+000 + 1.55000007e+000 2.38267947e-002 2.38267947e-002 2.32506692e-002 1.10000000e+001 + 1.56894910e+000 2.32506692e-002 1.89490467e-002 2.01085601e-002 1.50000000e+001 + 1.58905768e+000 2.01085601e-002 2.01085601e-002 2.45779436e-002 2.70000000e+001 + 1.60000002e+000 1.09423939e-002 1.09423939e-002 1.65136456e-002 2.70000000e+001 + 1.61651373e+000 1.65136456e-002 1.65136456e-002 2.38628164e-002 2.70000000e+001 + 1.64037645e+000 2.38628164e-002 2.38628164e-002 2.19553858e-002 9.00000000e+000 + 1.64999998e+000 9.62353777e-003 9.62353777e-003 1.12750111e-002 3.70000000e+001 + 1.66127503e+000 1.12750111e-002 1.12750111e-002 2.08534561e-002 1.10000000e+001 + 1.68212855e+000 2.08534561e-002 2.08534561e-002 2.87260041e-002 9.00000000e+000 + 1.70000005e+000 1.78715326e-002 1.78715326e-002 1.78794246e-002 1.50000000e+001 + 1.71787941e+000 1.78794246e-002 1.78794246e-002 2.39848346e-002 9.00000000e+000 + 1.73700452e+000 2.39848346e-002 1.91250909e-002 1.96920373e-002 9.00000000e+000 + 1.75000000e+000 1.29954852e-002 1.29954852e-002 1.49335349e-002 3.70000000e+001 + 1.76493359e+000 1.49335349e-002 1.49335349e-002 2.05503311e-002 3.70000000e+001 + 1.78548384e+000 2.05503311e-002 2.05503311e-002 2.62225065e-002 9.00000000e+000 + 1.80000007e+000 1.45161347e-002 1.45161347e-002 2.04397384e-002 2.90000000e+001 + 1.82043982e+000 2.04397384e-002 2.04397384e-002 3.13697234e-002 1.50000000e+001 + 1.84409559e+000 2.95602623e-002 2.36558281e-002 2.66021881e-002 7.00000000e+000 + 1.85000002e+000 5.90443425e-003 5.90443425e-003 2.66021881e-002 2.50000000e+001 + 1.87660217e+000 2.66021881e-002 2.66021881e-002 2.68413574e-002 1.50000000e+001 + 1.89999998e+000 2.33978126e-002 2.33978126e-002 2.66843345e-002 9.00000000e+000 + 1.92668438e+000 2.66843345e-002 2.66843345e-002 3.03133838e-002 1.10000000e+001 + 1.95000005e+000 2.33156662e-002 2.33156662e-002 2.92793140e-002 4.50000000e+001 + 1.97395909e+000 2.92793140e-002 2.39590034e-002 2.61395108e-002 4.50000000e+001 + 2.00000000e+000 2.60409974e-002 2.60409974e-002 3.12448069e-002 3.70000000e+001 + 2.03124475e+000 3.12448069e-002 3.12448069e-002 3.53926234e-002 4.50000000e+001 + 2.04999995e+000 1.87551938e-002 1.87551938e-002 3.25916968e-002 1.10000000e+001 + 2.07830954e+000 3.25916968e-002 2.83095818e-002 2.90316381e-002 9.00000000e+000 + 2.10000014e+000 2.16904189e-002 2.16904189e-002 2.21379008e-002 1.50000000e+001 + 2.12213802e+000 2.21379008e-002 2.21379008e-002 2.75327712e-002 2.90000000e+001 + 2.14967060e+000 2.75327712e-002 2.75327712e-002 3.05028465e-002 3.90000000e+001 + 2.15000010e+000 3.29328177e-004 3.29328177e-004 3.05028465e-002 3.90000000e+001 + 2.17312336e+000 3.05028465e-002 2.31233574e-002 2.25987192e-002 2.90000000e+001 + 2.19572210e+000 2.25987192e-002 2.25987192e-002 2.46350057e-002 1.50000000e+001 + 2.20000005e+000 4.27792408e-003 4.27792408e-003 4.88738017e-003 1.50000000e+001 + 2.20488739e+000 4.88738017e-003 4.88738017e-003 5.87147335e-003 2.90000000e+001 + 2.21075892e+000 5.87147335e-003 5.87147335e-003 8.96947272e-003 2.90000000e+001 + 2.21972847e+000 8.96947272e-003 8.96947272e-003 1.23321004e-002 2.90000000e+001 + 2.23206043e+000 1.23321004e-002 1.23321004e-002 1.96165442e-002 1.50000000e+001 + 2.25000000e+000 1.79395750e-002 1.79395750e-002 2.14174893e-002 4.50000000e+001 + 2.27141762e+000 2.14174893e-002 2.14174893e-002 3.00017800e-002 3.10000000e+001 + 2.29643345e+000 2.85825115e-002 2.50158738e-002 2.72340085e-002 2.90000000e+001 + 2.29999995e+000 3.56663764e-003 3.56663764e-003 5.31830220e-003 2.90000000e+001 + 2.30531836e+000 5.31830220e-003 5.31830220e-003 6.91342726e-003 2.90000000e+001 + 2.31223178e+000 6.91342726e-003 6.91342726e-003 1.19795650e-002 3.70000000e+001 + 2.32421136e+000 1.19795650e-002 1.19795650e-002 2.13524569e-002 4.50000000e+001 + 2.34556389e+000 2.13524569e-002 2.13524569e-002 2.20009014e-002 3.70000000e+001 + 2.35000014e+000 4.43624891e-003 4.43624891e-003 2.20009014e-002 3.70000000e+001 + 2.37200093e+000 2.20009014e-002 2.20009014e-002 2.27109753e-002 3.70000000e+001 + 2.39471197e+000 2.27109753e-002 2.27109753e-002 2.42642127e-002 5.00000000e+000 + 2.40000010e+000 5.28812269e-003 5.28812269e-003 2.42642127e-002 3.10000000e+001 + 2.42426419e+000 2.42642127e-002 2.42642127e-002 2.54806392e-002 3.10000000e+001 + 2.44974494e+000 2.54806392e-002 2.54806392e-002 3.31017040e-002 3.70000000e+001 + 2.45000005e+000 2.55148945e-004 2.55148945e-004 3.31017040e-002 3.70000000e+001 + 2.47592354e+000 3.31017040e-002 2.59234495e-002 2.80497558e-002 1.50000000e+001 + 2.50000000e+000 2.40765512e-002 2.40765512e-002 3.47426385e-002 9.00000000e+000 + 2.53474259e+000 3.47426385e-002 3.47426385e-002 3.53255756e-002 5.00000000e+000 + 2.54999995e+000 1.52573623e-002 1.52573623e-002 1.94439944e-002 3.70000000e+001 + 2.56944394e+000 1.94439944e-002 1.94439944e-002 2.22873185e-002 3.70000000e+001 + 2.59173131e+000 2.22873185e-002 2.22873185e-002 2.46922821e-002 3.70000000e+001 + 2.60000014e+000 8.26868694e-003 8.26868694e-003 9.62440204e-003 3.70000000e+001 + 2.60962439e+000 9.62440204e-003 9.62440204e-003 1.58533007e-002 1.50000000e+001 + 2.62547779e+000 1.58533007e-002 1.58533007e-002 1.66021064e-002 1.50000000e+001 + 2.64207983e+000 1.66021064e-002 1.66021064e-002 1.92888398e-002 3.70000000e+001 + 2.65000010e+000 7.92019255e-003 7.92019255e-003 1.06315371e-002 2.90000000e+001 + 2.66063166e+000 1.06315371e-002 1.06315371e-002 1.80985630e-002 1.50000000e+001 + 2.67873025e+000 1.80985630e-002 1.80985630e-002 1.96564235e-002 1.50000000e+001 + 2.69838667e+000 1.96564235e-002 1.96564235e-002 2.22592205e-002 5.00000000e+000 + 2.70000005e+000 1.61347771e-003 1.61347771e-003 2.22592205e-002 5.00000000e+000 + 2.72225928e+000 2.22592205e-002 2.22592205e-002 2.58834790e-002 5.00000000e+000 + 2.74814272e+000 2.58834790e-002 2.58834790e-002 2.81123035e-002 5.00000000e+000 + 2.75000000e+000 1.85730052e-003 1.85730052e-003 2.81123035e-002 5.00000000e+000 + 2.77811241e+000 2.81123035e-002 2.81123035e-002 2.95099486e-002 9.00000000e+000 + 2.79999995e+000 2.18876973e-002 2.18876973e-002 3.00153866e-002 9.00000000e+000 + 2.83001542e+000 3.00153866e-002 3.00153866e-002 2.89556794e-002 5.00000000e+000 + 2.85000014e+000 1.99846141e-002 1.99846141e-002 2.51792278e-002 4.50000000e+001 + 2.87257099e+000 2.51792278e-002 2.25708820e-002 2.34532077e-002 9.00000000e+000 + 2.89602423e+000 2.34532077e-002 2.34532077e-002 2.57035997e-002 2.70000000e+001 + 2.90000010e+000 3.97591107e-003 3.97591107e-003 5.02867904e-003 1.50000000e+001 + 2.90502882e+000 5.02867904e-003 5.02867904e-003 8.65203608e-003 3.50000000e+001 + 2.91368079e+000 8.65203608e-003 8.65203608e-003 1.04875769e-002 4.50000000e+001 + 2.92416835e+000 1.04875769e-002 1.04875769e-002 1.30167436e-002 3.70000000e+001 + 2.93718505e+000 1.30167436e-002 1.30167436e-002 1.28905205e-002 1.50000000e+001 + 2.94728851e+000 1.28149651e-002 1.01033812e-002 1.10027511e-002 2.90000000e+001 + 2.95000005e+000 2.71158316e-003 2.71158316e-003 3.20843747e-003 2.30000000e+001 + 2.95320845e+000 3.20843747e-003 3.20843747e-003 7.21747475e-003 3.50000000e+001 + 2.96042585e+000 7.21747475e-003 7.21747475e-003 1.05089294e-002 2.30000000e+001 + 2.97093487e+000 1.05089294e-002 1.05089294e-002 1.48290871e-002 2.90000000e+001 + 2.98576403e+000 1.48290871e-002 1.48290871e-002 1.87401511e-002 4.50000000e+001 + 3.00000000e+000 1.42360711e-002 1.42360711e-002 1.70830898e-002 3.70000000e+001 + 3.01708317e+000 1.70830898e-002 1.70830898e-002 1.60043724e-002 9.00000000e+000 + 3.03308749e+000 1.60043724e-002 1.60043724e-002 1.52448434e-002 9.00000000e+000 + 3.04833245e+000 1.52448434e-002 1.52448434e-002 1.82262547e-002 7.00000000e+000 + 3.04999995e+000 1.66769396e-003 1.66769396e-003 1.82262547e-002 7.00000000e+000 + 3.06822634e+000 1.82262547e-002 1.82262547e-002 2.18097903e-002 9.00000000e+000 + 3.09003615e+000 2.18097903e-002 2.18097903e-002 2.16706432e-002 9.00000000e+000 + 3.10000014e+000 9.96395387e-003 9.96395387e-003 1.26174204e-002 9.00000000e+000 + 3.11261749e+000 1.26174204e-002 1.26174204e-002 2.00801250e-002 3.50000000e+001 + 3.13269758e+000 2.00801250e-002 2.00801250e-002 2.26488467e-002 9.00000000e+000 + 3.15000010e+000 1.73024554e-002 1.73024554e-002 2.11464521e-002 9.00000000e+000 + 3.17114639e+000 2.11464521e-002 2.11464521e-002 1.92864425e-002 9.00000000e+000 + 3.18511677e+000 1.92864425e-002 1.39702354e-002 1.41943572e-002 9.00000000e+000 + 3.19931102e+000 1.41943572e-002 1.41943572e-002 1.52893998e-002 9.00000000e+000 + 3.20000005e+000 6.88956759e-004 6.88956759e-004 1.52893998e-002 9.00000000e+000 + 3.21528935e+000 1.52893998e-002 1.52893998e-002 2.47584730e-002 1.10000000e+001 + 3.24004793e+000 2.47584730e-002 2.47584730e-002 2.46770661e-002 9.00000000e+000 + 3.25000000e+000 9.95212886e-003 9.95212886e-003 2.34977268e-002 9.00000000e+000 + 3.27349782e+000 2.34977268e-002 2.34977268e-002 2.15547979e-002 9.00000000e+000 + 3.29505253e+000 2.15547979e-002 2.15547979e-002 1.95150375e-002 9.00000000e+000 + 3.29999995e+000 4.94747702e-003 4.94747702e-003 6.90035336e-003 3.70000000e+001 + 3.30690050e+000 6.90035336e-003 6.90035336e-003 1.11159710e-002 4.30000000e+001 + 3.31801629e+000 1.11159710e-002 1.11159710e-002 1.68087352e-002 3.70000000e+001 + 3.33482504e+000 1.68087352e-002 1.68087352e-002 2.81348154e-002 5.00000000e+000 + 3.35000014e+000 1.51749421e-002 1.51749421e-002 1.81033947e-002 1.50000000e+001 + 3.36810350e+000 1.81033947e-002 1.81033947e-002 2.62243375e-002 4.30000000e+001 + 3.39432788e+000 2.62243375e-002 2.62243375e-002 2.50973906e-002 7.00000000e+000 + 3.40000010e+000 5.67226904e-003 5.67226904e-003 5.73142199e-003 2.90000000e+001 + 3.40573144e+000 5.73142199e-003 5.73142199e-003 9.19183809e-003 3.30000000e+001 + 3.41492343e+000 9.19183809e-003 9.19183809e-003 1.40414666e-002 3.30000000e+001 + 3.42896485e+000 1.40414666e-002 1.40414666e-002 1.76469907e-002 3.30000000e+001 + 3.44661188e+000 1.76469907e-002 1.76469907e-002 1.82769950e-002 3.30000000e+001 + 3.45000005e+000 3.38828331e-003 3.38828331e-003 4.74619307e-003 4.50000000e+001 + 3.45474625e+000 4.74619307e-003 4.74619307e-003 9.10066068e-003 3.30000000e+001 + 3.46384692e+000 9.10066068e-003 9.10066068e-003 1.70701183e-002 7.00000000e+000 + 3.48091698e+000 1.70701183e-002 1.70701183e-002 1.87093522e-002 4.50000000e+001 + 3.49962640e+000 1.87093522e-002 1.87093522e-002 1.94901787e-002 5.00000000e+000 + 3.50000000e+000 3.73676419e-004 3.73676419e-004 1.94901787e-002 5.00000000e+000 + 3.51949024e+000 1.94901787e-002 1.94901787e-002 2.31601764e-002 5.00000000e+000 + 3.54265046e+000 2.31601764e-002 2.31601764e-002 2.53061559e-002 4.30000000e+001 + 3.54873896e+000 7.34964479e-003 6.08860468e-003 7.58182956e-003 4.50000000e+001 + 3.54999995e+000 1.26104010e-003 1.26104010e-003 7.58182956e-003 2.50000000e+001 + 3.55758190e+000 7.58182956e-003 7.58182956e-003 1.65637638e-002 4.30000000e+001 + 3.57414556e+000 1.65637638e-002 1.65637638e-002 1.59659516e-002 4.50000000e+001 + 3.59011149e+000 1.59659516e-002 1.59659516e-002 2.27339379e-002 4.50000000e+001 + 3.60000014e+000 9.88845620e-003 9.88845620e-003 2.71097496e-002 9.00000000e+000 + 3.62710977e+000 2.71097496e-002 2.71097496e-002 2.55956538e-002 7.00000000e+000 + 3.65000010e+000 2.28902511e-002 2.28902511e-002 2.35788487e-002 9.00000000e+000 + 3.66794086e+000 2.35788487e-002 1.79408807e-002 1.95247550e-002 4.50000000e+001 + 3.68746567e+000 1.95247550e-002 1.95247550e-002 2.20299698e-002 5.00000000e+000 + 3.70000005e+000 1.25343651e-002 1.25343651e-002 1.41593050e-002 5.00000000e+000 + 3.71415925e+000 1.41593050e-002 1.41593050e-002 1.96833517e-002 1.50000000e+001 + 3.73384261e+000 1.96833517e-002 1.96833517e-002 2.27563456e-002 5.00000000e+000 + 3.74824023e+000 1.61573440e-002 1.43974600e-002 1.33808963e-002 9.00000000e+000 + 3.75000000e+000 1.75988476e-003 1.75988476e-003 1.89383072e-003 2.90000000e+001 + 3.75189400e+000 1.89383072e-003 1.89383072e-003 2.85229762e-003 9.00000000e+000 + 3.75474620e+000 2.85229762e-003 2.85229762e-003 5.23521844e-003 9.00000000e+000 + 3.75998139e+000 5.23521844e-003 5.23521844e-003 5.06086135e-003 4.50000000e+001 + 3.76504230e+000 5.06086135e-003 5.06086135e-003 7.06311315e-003 4.50000000e+001 + 3.76762176e+000 7.06311315e-003 2.57944036e-003 2.46207626e-003 5.00000000e+000 + 3.77008367e+000 2.46207626e-003 2.46207626e-003 3.43325920e-003 5.00000000e+000 + 3.77351713e+000 3.43325920e-003 3.43325920e-003 5.36888279e-003 5.00000000e+000 + 3.77888584e+000 5.36888279e-003 5.36888279e-003 5.33847744e-003 5.00000000e+000 + 3.78422451e+000 5.33847744e-003 5.33847744e-003 5.51782642e-003 5.00000000e+000 + 3.78974223e+000 5.51782642e-003 5.51782642e-003 6.26468426e-003 5.00000000e+000 + 3.79600692e+000 6.26468426e-003 6.26468426e-003 6.34557568e-003 1.50000000e+001 + 3.79999995e+000 3.99314566e-003 3.99314566e-003 4.56693769e-003 5.00000000e+000 + 3.80456710e+000 4.56693769e-003 4.56693769e-003 6.30745152e-003 5.00000000e+000 + 3.81087446e+000 6.30745152e-003 6.30745152e-003 7.36514525e-003 5.00000000e+000 + 3.81823969e+000 7.36514525e-003 7.36514525e-003 1.26235457e-002 1.10000000e+001 + 3.83086324e+000 1.26235457e-002 1.26235457e-002 1.65244471e-002 5.00000000e+000 + 3.84738755e+000 1.65244471e-002 1.65244471e-002 2.62247454e-002 1.10000000e+001 + 3.85000014e+000 2.61247321e-003 2.61247321e-003 2.62247454e-002 1.10000000e+001 + 3.87021875e+000 2.62247454e-002 2.02186983e-002 2.23418772e-002 1.50000000e+001 + 3.89256072e+000 2.23418772e-002 2.23418772e-002 2.10942607e-002 5.00000000e+000 + 3.90000010e+000 7.43942522e-003 7.43942522e-003 8.51938035e-003 4.50000000e+001 + 3.90851951e+000 8.51938035e-003 8.51938035e-003 1.21158017e-002 3.70000000e+001 + 3.92063522e+000 1.21158017e-002 1.21158017e-002 1.61342379e-002 3.70000000e+001 + 3.93676949e+000 1.61342379e-002 1.61342379e-002 2.12970413e-002 3.70000000e+001 + 3.95000005e+000 1.32305808e-002 1.32305808e-002 1.45980427e-002 4.50000000e+001 + 3.96459818e+000 1.45980427e-002 1.45980427e-002 1.56944972e-002 4.50000000e+001 + 3.98029256e+000 1.56944972e-002 1.56944972e-002 1.78740006e-002 5.00000000e+000 + 3.99437237e+000 1.78740006e-002 1.40797906e-002 1.52956480e-002 4.50000000e+001 + 4.00000000e+000 5.62766893e-003 5.62766893e-003 7.86538329e-003 4.10000000e+001 + 4.00786543e+000 7.86538329e-003 7.86538329e-003 1.47584183e-002 3.50000000e+001 + 4.02262402e+000 1.47584183e-002 1.47584183e-002 2.05913633e-002 5.00000000e+000 + 4.04321527e+000 2.05913633e-002 2.05913633e-002 2.51277518e-002 4.10000000e+001 + 4.05000019e+000 6.78483676e-003 6.78483676e-003 9.39967204e-003 4.10000000e+001 + 4.05939960e+000 9.39967204e-003 9.39967204e-003 1.19817005e-002 4.10000000e+001 + 4.07138157e+000 1.19817005e-002 1.19817005e-002 1.30600445e-002 4.10000000e+001 + 4.08444166e+000 1.30600445e-002 1.30600445e-002 1.46792866e-002 1.50000000e+001 + 4.09912062e+000 1.46792866e-002 1.46792866e-002 2.09672768e-002 4.10000000e+001 + 4.09999990e+000 8.79297673e-004 8.79297673e-004 2.09672768e-002 4.10000000e+001 + 4.11318254e+000 2.09672768e-002 1.31824026e-002 1.72325633e-002 5.00000000e+000 + 4.13041496e+000 1.72325633e-002 1.72325633e-002 2.33208556e-002 2.90000000e+001 + 4.15000010e+000 1.95850357e-002 1.95850357e-002 2.08588485e-002 5.00000000e+000 + 4.17085886e+000 2.08588485e-002 2.08588485e-002 2.24987566e-002 8.00000000e+000 + 4.19335747e+000 2.24987566e-002 2.24987566e-002 2.08807085e-002 8.00000000e+000 + 4.20000029e+000 6.64239516e-003 6.64239516e-003 7.73999700e-003 2.90000000e+001 + 4.20773983e+000 7.73999700e-003 7.73999700e-003 1.21723739e-002 3.90000000e+001 + 4.21991253e+000 1.21723739e-002 1.21723739e-002 2.49400530e-002 4.50000000e+001 + 4.24485254e+000 2.49400530e-002 2.49400530e-002 2.29120702e-002 5.00000000e+000 + 4.25000000e+000 5.14757680e-003 5.14757680e-003 7.64521305e-003 2.90000000e+001 + 4.25764513e+000 7.64521305e-003 7.64521305e-003 1.75681841e-002 2.90000000e+001 + 4.27521324e+000 1.75681841e-002 1.75681841e-002 2.52531506e-002 5.00000000e+000 + 4.30000019e+000 2.47866027e-002 2.47866027e-002 2.26444360e-002 5.00000000e+000 + 4.32264471e+000 2.26444360e-002 2.26444360e-002 2.53014360e-002 1.50000000e+001 + 4.34245539e+000 2.53014360e-002 1.98107101e-002 2.07853224e-002 1.50000000e+001 + 4.34999990e+000 7.54485372e-003 7.54485372e-003 7.88425654e-003 1.50000000e+001 + 4.35788441e+000 7.88425654e-003 7.88425654e-003 1.59540512e-002 1.50000000e+001 + 4.37383842e+000 1.59540512e-002 1.59540512e-002 2.51675267e-002 5.00000000e+000 + 4.38732052e+000 2.51675267e-002 1.34822810e-002 1.34870671e-002 1.50000000e+001 + 4.40000010e+000 1.26794120e-002 1.26794120e-002 1.26350923e-002 3.10000000e+001 + 4.41263533e+000 1.26350923e-002 1.26350923e-002 1.95025206e-002 1.50000000e+001 + 4.43213749e+000 1.95025206e-002 1.95025206e-002 1.87052339e-002 1.50000000e+001 + 4.45000029e+000 1.78623889e-002 1.78623889e-002 2.10452750e-002 2.50000000e+001 + 4.47104549e+000 2.10452750e-002 2.10452750e-002 2.12833714e-002 5.00000000e+000 + 4.49232864e+000 2.12833714e-002 2.12833714e-002 2.03946661e-002 5.00000000e+000 + 4.50000000e+000 7.67135434e-003 7.67135434e-003 8.15666746e-003 2.50000000e+001 + 4.50815678e+000 8.15666746e-003 8.15666746e-003 1.61284227e-002 3.70000000e+001 + 4.52428532e+000 1.61284227e-002 1.61284227e-002 2.08633766e-002 5.00000000e+000 + 4.54514837e+000 2.08633766e-002 2.08633766e-002 2.67717298e-002 5.00000000e+000 + 4.55000019e+000 4.85153310e-003 4.85153310e-003 2.67717298e-002 5.00000000e+000 + 4.57677174e+000 2.67717298e-002 2.67717298e-002 2.61369050e-002 5.00000000e+000 + 4.59999990e+000 2.32282709e-002 2.32282709e-002 2.31254958e-002 5.00000000e+000 + 4.62312555e+000 2.31254958e-002 2.31254958e-002 2.87798122e-002 8.00000000e+000 + 4.65000010e+000 2.68745050e-002 2.68745050e-002 2.59787105e-002 7.00000000e+000 + 4.67597866e+000 2.59787105e-002 2.59787105e-002 2.54812296e-002 5.00000000e+000 + 4.69696379e+000 2.40212902e-002 2.09850315e-002 2.04545688e-002 9.00000000e+000 + 4.70000029e+000 3.03625921e-003 3.03625921e-003 2.04545688e-002 4.10000000e+001 + 4.72045469e+000 2.04545688e-002 2.04545688e-002 2.18011327e-002 9.00000000e+000 + 4.74225569e+000 2.18011327e-002 2.18011327e-002 2.51867883e-002 4.10000000e+001 + 4.74864149e+000 7.74429971e-003 6.38591684e-003 7.95372389e-003 4.10000000e+001 + 4.75000000e+000 1.35838275e-003 1.35838275e-003 7.95372389e-003 4.10000000e+001 + 4.75795364e+000 7.95372389e-003 7.95372389e-003 1.82486251e-002 2.70000000e+001 + 4.77620220e+000 1.82486251e-002 1.82486251e-002 2.10819021e-002 1.50000000e+001 + 4.79728413e+000 2.10819021e-002 2.10819021e-002 2.34708842e-002 1.50000000e+001 + 4.80000019e+000 2.71574920e-003 2.71574920e-003 2.34708842e-002 3.70000000e+001 + 4.82347107e+000 2.34708842e-002 2.34708842e-002 2.50374097e-002 9.00000000e+000 + 4.84850836e+000 2.50374097e-002 2.50374097e-002 2.26493292e-002 4.50000000e+001 + 4.84999990e+000 1.49170670e-003 1.49170670e-003 2.26493292e-002 4.50000000e+001 + 4.87264919e+000 2.26493292e-002 2.26493292e-002 3.10065839e-002 9.00000000e+000 + 4.89668846e+000 2.73506716e-002 2.40388904e-002 2.40270365e-002 9.00000000e+000 + 4.90000010e+000 3.31177982e-003 3.31177982e-003 2.40270365e-002 1.50000000e+001 + 4.92402697e+000 2.40270365e-002 2.40270365e-002 2.61237677e-002 5.00000000e+000 + 4.95000029e+000 2.59729642e-002 2.59729642e-002 2.71526221e-002 8.00000000e+000 + 4.97715282e+000 2.71526221e-002 2.71526221e-002 2.50165835e-002 9.00000000e+000 + 5.00000000e+000 2.28473786e-002 2.28473786e-002 2.52699368e-002 9.00000000e+000 + 5.02207565e+000 2.52699368e-002 2.20757332e-002 2.26081200e-002 9.00000000e+000 + 5.04468393e+000 2.26081200e-002 2.26081200e-002 2.32073534e-002 1.50000000e+001 + 5.05000019e+000 5.31614805e-003 5.31614805e-003 5.64638386e-003 1.50000000e+001 + 5.05564642e+000 5.64638386e-003 5.64638386e-003 9.74362995e-003 1.50000000e+001 + 5.06539011e+000 9.74362995e-003 9.74362995e-003 9.97530390e-003 1.50000000e+001 + 5.07336855e+000 9.97530390e-003 7.97838718e-003 1.23258466e-002 1.50000000e+001 + 5.08569431e+000 1.23258466e-002 1.23258466e-002 1.15560526e-002 1.50000000e+001 + 5.09725046e+000 1.15560526e-002 1.15560526e-002 1.28111178e-002 1.50000000e+001 + 5.09999990e+000 2.74969987e-003 2.74969987e-003 4.63366881e-003 3.30000000e+001 + 5.10463381e+000 4.63366881e-003 4.63366881e-003 7.03985197e-003 3.30000000e+001 + 5.11167383e+000 7.03985197e-003 7.03985197e-003 3.25842500e-002 3.30000000e+001 + 5.14425802e+000 3.25842500e-002 3.25842500e-002 3.33621688e-002 5.00000000e+000 + 5.15000010e+000 5.74222999e-003 5.74222999e-003 1.20297056e-002 5.00000000e+000 + 5.16202974e+000 1.20297056e-002 1.20297056e-002 1.98409054e-002 3.70000000e+001 + 5.18187046e+000 1.98409054e-002 1.98409054e-002 2.26549376e-002 5.00000000e+000 + 5.20000029e+000 1.81293897e-002 1.81293897e-002 2.67486479e-002 5.00000000e+000 + 5.22123289e+000 2.67486479e-002 2.12326590e-002 2.26808917e-002 9.00000000e+000 + 5.24391365e+000 2.26808917e-002 2.26808917e-002 2.55901143e-002 4.50000000e+001 + 5.25000000e+000 6.08645007e-003 6.08645007e-003 2.55901143e-002 9.00000000e+000 + 5.27559042e+000 2.55901143e-002 2.55901143e-002 2.32511554e-002 9.00000000e+000 + 5.29129648e+000 2.32511554e-002 1.57064665e-002 1.81108546e-002 3.70000000e+001 + 5.30000019e+000 8.70341901e-003 8.70341901e-003 1.21820997e-002 4.50000000e+001 + 5.31218195e+000 1.21820997e-002 1.21820997e-002 1.62567142e-002 4.50000000e+001 + 5.32843876e+000 1.62567142e-002 1.62567142e-002 1.71757936e-002 4.50000000e+001 + 5.33806133e+000 1.71757936e-002 9.62229259e-003 1.11503098e-002 4.50000000e+001 + 5.34921169e+000 1.11503098e-002 1.11503098e-002 1.51746217e-002 4.50000000e+001 + 5.34999990e+000 7.88583769e-004 7.88583769e-004 1.51746217e-002 4.50000000e+001 + 5.36517477e+000 1.51746217e-002 1.51746217e-002 2.02184599e-002 4.50000000e+001 + 5.38539314e+000 2.02184599e-002 2.02184599e-002 2.30938811e-002 4.50000000e+001 + 5.40000010e+000 1.46069191e-002 1.46069191e-002 3.11224572e-002 3.70000000e+001 + 5.43112230e+000 3.11224572e-002 3.11224572e-002 2.98052821e-002 5.00000000e+000 + 5.45000029e+000 1.88775435e-002 1.88775435e-002 2.50963196e-002 3.70000000e+001 + 5.47509623e+000 2.50963196e-002 2.50963196e-002 2.31296085e-002 5.00000000e+000 + 5.49822617e+000 2.31296085e-002 2.31296085e-002 2.39867568e-002 5.00000000e+000 + 5.50000000e+000 1.77407241e-003 1.77407241e-003 2.39867568e-002 5.00000000e+000 + 5.52398682e+000 2.39867568e-002 2.39867568e-002 2.59659290e-002 9.00000000e+000 + 5.54995298e+000 2.59659290e-002 2.59659290e-002 2.48278975e-002 1.50000000e+001 + 5.57478046e+000 2.48278975e-002 2.48278975e-002 2.84080207e-002 5.00000000e+000 + 5.59999990e+000 2.52194181e-002 2.52194181e-002 2.53473744e-002 9.00000000e+000 + 5.62141657e+000 2.53473744e-002 2.14163810e-002 2.23751701e-002 9.00000000e+000 + 5.64379168e+000 2.23751701e-002 2.23751701e-002 2.11976506e-002 5.00000000e+000 + 5.65000010e+000 6.20844960e-003 6.20844960e-003 6.54729689e-003 3.70000000e+001 + 5.65654755e+000 6.54729689e-003 6.54729689e-003 1.31446533e-002 3.70000000e+001 + 5.66969204e+000 1.31446533e-002 1.31446533e-002 2.15559267e-002 5.00000000e+000 + 5.68674755e+000 2.15559267e-002 1.70555972e-002 2.35677566e-002 3.70000000e+001 + 5.70000029e+000 1.32524548e-002 1.32524548e-002 2.34443005e-002 4.10000000e+001 + 5.72344446e+000 2.34443005e-002 2.34443005e-002 3.19801345e-002 1.10000000e+001 + 5.75000000e+000 2.65557002e-002 2.65557002e-002 3.36377919e-002 6.00000000e+000 + 5.77083731e+000 3.36377919e-002 2.08370425e-002 2.48640329e-002 5.00000000e+000 + 5.79124880e+000 2.48640329e-002 2.04117559e-002 2.20511220e-002 5.00000000e+000 + 5.80000019e+000 8.75120051e-003 8.75120051e-003 1.17994007e-002 1.50000000e+001 + 5.81179953e+000 1.17994007e-002 1.17994007e-002 2.69280896e-002 7.00000000e+000 + 5.83872747e+000 2.69280896e-002 2.69280896e-002 2.74773426e-002 5.00000000e+000 + 5.84999990e+000 1.12725114e-002 1.12725114e-002 1.97198540e-002 2.50000000e+001 + 5.86971998e+000 1.97198540e-002 1.97198540e-002 2.65872180e-002 2.90000000e+001 + 5.89630699e+000 2.65872180e-002 2.65872180e-002 2.46579777e-002 9.00000000e+000 + 5.90000010e+000 3.69292777e-003 3.69292777e-003 2.46579777e-002 1.00000000e+001 + 5.92465830e+000 2.46579777e-002 2.46579777e-002 2.87287906e-002 9.00000000e+000 + 5.95000029e+000 2.53420230e-002 2.53420230e-002 2.52731591e-002 5.00000000e+000 + 5.97527313e+000 2.52731591e-002 2.52731591e-002 2.47706510e-002 1.50000000e+001 + 6.00000000e+000 2.47268416e-002 2.47268416e-002 2.33987290e-002 9.00000000e+000 + 6.02339888e+000 2.33987290e-002 2.33987290e-002 2.21516918e-002 9.00000000e+000 + 6.04555035e+000 2.21516918e-002 2.21516918e-002 2.13362463e-002 1.50000000e+001 + 6.05000019e+000 4.44957893e-003 4.44957893e-003 5.12802647e-003 1.50000000e+001 + 6.05512810e+000 5.12802647e-003 5.12802647e-003 8.69598705e-003 3.70000000e+001 + 6.06382418e+000 8.69598705e-003 8.69598705e-003 1.82335768e-002 9.00000000e+000 + 6.08205748e+000 1.82335768e-002 1.82335768e-002 2.50912011e-002 7.00000000e+000 + 6.09999990e+000 1.79424100e-002 1.79424100e-002 2.48265732e-002 6.00000000e+000 + 6.12482643e+000 2.48265732e-002 2.48265732e-002 2.73955204e-002 5.00000000e+000 + 6.15000010e+000 2.51734275e-002 2.51734275e-002 2.45076679e-002 9.00000000e+000 + 6.17450762e+000 2.45076679e-002 2.45076679e-002 2.24253424e-002 9.00000000e+000 + 6.19448471e+000 2.24253424e-002 1.99770685e-002 2.24056672e-002 5.00000000e+000 + 6.20000029e+000 5.51526388e-003 5.51526388e-003 7.63834920e-003 2.50000000e+001 + 6.20763826e+000 7.63834920e-003 7.63834920e-003 1.44252935e-002 3.70000000e+001 + 6.22206354e+000 1.44252935e-002 1.44252935e-002 2.76072640e-002 4.50000000e+001 + 6.24531889e+000 2.76072640e-002 2.32553743e-002 2.37520467e-002 9.00000000e+000 + 6.25000000e+000 4.68098465e-003 4.68098465e-003 2.37520467e-002 5.00000000e+000 + 6.27375221e+000 2.37520467e-002 2.37520467e-002 2.28095632e-002 9.00000000e+000 + 6.29656172e+000 2.28095632e-002 2.28095632e-002 2.53989398e-002 5.00000000e+000 + 6.30000019e+000 3.43838939e-003 3.43838939e-003 4.19815117e-003 3.30000000e+001 + 6.30419827e+000 4.19815117e-003 4.19815117e-003 8.46917834e-003 2.90000000e+001 + 6.31266737e+000 8.46917834e-003 8.46917834e-003 1.60087515e-002 3.70000000e+001 + 6.32867622e+000 1.60087515e-002 1.60087515e-002 1.85615681e-002 5.00000000e+000 + 6.34723759e+000 1.85615681e-002 1.85615681e-002 2.28044242e-002 5.00000000e+000 + 6.34999990e+000 2.76235235e-003 2.76235235e-003 4.01682174e-003 3.70000000e+001 + 6.35401678e+000 4.01682174e-003 4.01682174e-003 6.47258433e-003 3.70000000e+001 + 6.36048937e+000 6.47258433e-003 6.47258433e-003 1.53739788e-002 2.90000000e+001 + 6.37586355e+000 1.53739788e-002 1.53739788e-002 2.28644162e-002 8.00000000e+000 + 6.39642715e+000 2.28644162e-002 2.05636118e-002 2.08726171e-002 5.00000000e+000 + 6.40000010e+000 3.57300392e-003 3.57300392e-003 1.36068435e-002 9.00000000e+000 + 6.41360712e+000 1.36068435e-002 1.36068435e-002 1.33091975e-002 9.00000000e+000 + 6.42691612e+000 1.33091975e-002 1.33091975e-002 1.36918277e-002 3.70000000e+001 + 6.44060802e+000 1.36918277e-002 1.36918277e-002 1.63213331e-002 5.00000000e+000 + 6.45000029e+000 9.39213205e-003 9.39213205e-003 8.52674153e-003 3.70000000e+001 + 6.45852661e+000 8.52674153e-003 8.52674153e-003 1.66469645e-002 4.10000000e+001 + 6.47517395e+000 1.66469645e-002 1.66469645e-002 1.96049549e-002 5.00000000e+000 + 6.49044037e+000 1.96049549e-002 1.52664566e-002 1.73625462e-002 5.00000000e+000 + 6.50000000e+000 9.55983810e-003 9.55983810e-003 1.07325893e-002 4.10000000e+001 + 6.51073265e+000 1.07325893e-002 1.07325893e-002 1.85120944e-002 4.10000000e+001 + 6.52924490e+000 1.85120944e-002 1.85120944e-002 2.11116876e-002 5.00000000e+000 + 6.54326582e+000 2.07553171e-002 1.40208416e-002 1.61828082e-002 5.00000000e+000 + 6.55000019e+000 6.73447549e-003 6.73447549e-003 7.82198925e-003 3.70000000e+001 + 6.55782223e+000 7.82198925e-003 7.82198925e-003 1.42915687e-002 4.50000000e+001 + 6.57211351e+000 1.42915687e-002 1.42915687e-002 1.87656786e-002 4.50000000e+001 + 6.59087944e+000 1.87656786e-002 1.87656786e-002 2.61717960e-002 3.70000000e+001 + 6.59999990e+000 9.12076421e-003 9.12076421e-003 1.23430882e-002 3.70000000e+001 + 6.61234331e+000 1.23430882e-002 1.23430882e-002 1.90122332e-002 5.00000000e+000 + 6.63135529e+000 1.90122332e-002 1.90122332e-002 2.34392006e-002 5.00000000e+000 + 6.65000010e+000 1.86446793e-002 1.86446793e-002 1.99231580e-002 5.00000000e+000 + 6.66992331e+000 1.99231580e-002 1.99231580e-002 2.17787717e-002 9.00000000e+000 + 6.69170189e+000 2.17787717e-002 2.17787717e-002 2.55952235e-002 8.00000000e+000 + 6.70000029e+000 8.29807110e-003 8.29807110e-003 1.16713606e-002 2.90000000e+001 + 6.71167135e+000 1.16713606e-002 1.16713606e-002 2.07148809e-002 3.10000000e+001 + 6.73238611e+000 2.07148809e-002 2.07148809e-002 2.00994052e-002 5.00000000e+000 + 6.75000000e+000 1.76137574e-002 1.76137574e-002 2.30700560e-002 9.00000000e+000 + 6.76900387e+000 2.30700560e-002 1.90035552e-002 1.97586548e-002 9.00000000e+000 + 6.78876209e+000 1.97586548e-002 1.97586548e-002 2.48674061e-002 3.70000000e+001 + 6.80000019e+000 1.12377899e-002 1.12377899e-002 1.86537690e-002 3.70000000e+001 + 6.81865406e+000 1.86537690e-002 1.86537690e-002 2.55279932e-002 8.00000000e+000 + 6.84418201e+000 2.55279932e-002 2.55279932e-002 2.32048370e-002 9.00000000e+000 + 6.84999990e+000 5.81823895e-003 5.81823895e-003 2.32048370e-002 1.50000000e+001 + 6.87320471e+000 2.32048370e-002 2.32048370e-002 2.26011463e-002 5.00000000e+000 + 6.89580631e+000 2.26011463e-002 2.26011463e-002 2.31083147e-002 9.00000000e+000 + 6.90000010e+000 4.19401797e-003 4.19401797e-003 5.46649517e-003 4.10000000e+001 + 6.90546656e+000 5.46649517e-003 5.46649517e-003 1.14733214e-002 1.50000000e+001 + 6.91693974e+000 1.14733214e-002 1.14733214e-002 1.33670010e-002 4.10000000e+001 + 6.93030691e+000 1.33670010e-002 1.33670010e-002 1.48211224e-002 1.50000000e+001 + 6.94308853e+000 1.48211224e-002 1.27815083e-002 1.89372841e-002 3.70000000e+001 + 6.95000029e+000 6.91167545e-003 6.91167545e-003 7.25058652e-003 1.50000000e+001 + 6.95725060e+000 7.25058652e-003 7.25058652e-003 1.33143375e-002 1.50000000e+001 + 6.97056484e+000 1.33143375e-002 1.33143375e-002 1.81747209e-002 1.50000000e+001 + 6.98873997e+000 1.81747209e-002 1.81747209e-002 2.32418850e-002 1.50000000e+001 + 6.99831295e+000 1.12602515e-002 9.57311876e-003 1.17313089e-002 1.50000000e+001 + 7.00000000e+000 1.68713229e-003 1.68713229e-003 1.17313089e-002 5.00000000e+000 + 7.01173115e+000 1.17313089e-002 1.17313089e-002 1.18237166e-002 1.50000000e+001 + 7.02355480e+000 1.18237166e-002 1.18237166e-002 1.94364451e-002 2.50000000e+001 + 7.04299164e+000 1.94364451e-002 1.94364451e-002 1.96044445e-002 1.50000000e+001 + 7.05000019e+000 7.00863358e-003 7.00863358e-003 6.94466382e-003 2.50000000e+001 + 7.05694485e+000 6.94466382e-003 6.94466382e-003 1.03408294e-002 2.50000000e+001 + 7.06728554e+000 1.03408294e-002 1.03408294e-002 1.31758517e-002 2.50000000e+001 + 7.08046150e+000 1.31758517e-002 1.31758517e-002 1.75320059e-002 2.90000000e+001 + 7.09799337e+000 1.75320059e-002 1.75320059e-002 2.92596333e-002 3.50000000e+001 + 7.09999990e+000 2.00665067e-003 2.00665067e-003 2.92596333e-002 3.50000000e+001 + 7.12255621e+000 2.92596333e-002 2.25562118e-002 2.41184123e-002 1.50000000e+001 + 7.13810682e+000 2.41184123e-002 1.55503871e-002 1.59810185e-002 4.50000000e+001 + 7.14741373e+000 1.18934009e-002 9.30706691e-003 1.04601616e-002 1.50000000e+001 + 7.15000010e+000 2.58633448e-003 2.58633448e-003 3.29716038e-003 1.50000000e+001 + 7.15329742e+000 3.29716038e-003 3.29716038e-003 9.02590528e-003 1.50000000e+001 + 7.16232300e+000 9.02590528e-003 9.02590528e-003 1.93039272e-002 1.50000000e+001 + 7.18162727e+000 1.93039272e-002 1.93039272e-002 2.62749083e-002 3.90000000e+001 + 7.20000029e+000 1.83730088e-002 1.83730088e-002 1.85478088e-002 3.90000000e+001 + 7.21854782e+000 1.85478088e-002 1.85478088e-002 1.99387781e-002 1.50000000e+001 + 7.23848677e+000 1.99387781e-002 1.99387781e-002 2.38984339e-002 1.50000000e+001 + 7.25000000e+000 1.15134139e-002 1.15134139e-002 1.35619324e-002 1.50000000e+001 + 7.26356220e+000 1.35619324e-002 1.35619324e-002 1.48574291e-002 5.00000000e+000 + 7.27841949e+000 1.48574291e-002 1.48574291e-002 2.10445672e-002 5.00000000e+000 + 7.29946423e+000 2.10445672e-002 2.10445672e-002 2.11236849e-002 1.50000000e+001 + 7.30000019e+000 5.36072650e-004 5.36072650e-004 2.11236849e-002 1.50000000e+001 + 7.32112360e+000 2.11236849e-002 2.11236849e-002 2.83719022e-002 1.50000000e+001 + 7.34949589e+000 2.83719022e-002 2.83719022e-002 3.06385253e-002 9.00000000e+000 + 7.34999990e+000 5.04413620e-004 5.04413620e-004 3.06385253e-002 9.00000000e+000 + 7.38063860e+000 3.06385253e-002 3.06385253e-002 3.11207101e-002 3.70000000e+001 + 7.40000010e+000 1.93614755e-002 1.93614755e-002 1.86129734e-002 4.50000000e+001 + 7.41861296e+000 1.86129734e-002 1.86129734e-002 2.44822372e-002 3.70000000e+001 + 7.44309521e+000 2.44822372e-002 2.44822372e-002 2.33007260e-002 3.70000000e+001 + 7.44999981e+000 6.90448750e-003 6.90448750e-003 2.33007260e-002 9.00000000e+000 diff --git a/pyFAST/input_output/tests/example_files/Bladed_out_binary_case2.$46 b/pyFAST/input_output/tests/example_files/Bladed_out_binary_case2.$46 new file mode 100644 index 0000000000000000000000000000000000000000..7b380db979a1eb2e8db2561aa74b0063678b3505 GIT binary patch literal 480 VcmZQzXxPVq0!H~L!7;?c2LQbRB}D)L literal 0 HcmV?d00001 diff --git a/pyFAST/input_output/tests/example_files/Bladed_out_binary_case2.$55 b/pyFAST/input_output/tests/example_files/Bladed_out_binary_case2.$55 new file mode 100644 index 0000000000000000000000000000000000000000..c9731d3e529bccdef5b94a0d095b022f16f27f5c GIT binary patch literal 200 zcmZQ%Kn09IE)$Xvhz|oWahMp4hRK6ykQfNF05M2CjOGHeL6{$iWq{ZYh@F5q1c)a9 c@f9G}0&08=#3z6nw=gp>d;@AW0E!a>0CnsJNdN!< literal 0 HcmV?d00001 diff --git a/pyFAST/input_output/tests/example_files/Bladed_out_binary_case2.$69 b/pyFAST/input_output/tests/example_files/Bladed_out_binary_case2.$69 new file mode 100644 index 0000000000000000000000000000000000000000..b425c09beb0808b1b7ba907adafa901e9a341baf GIT binary patch literal 240 zcmV70EEf?)5}TS9I;E9`3y)Yy_rZjteC}IB!tOg z4arGLKC?>+w5CV72%bpJ0;a|CJA}#Z9Lz}s#<5FrNP$NL*Oy4>#q`DG=Yz>bNXtnd z+^$Q?-=jyss*FhSy8*_v>x0SlmcvQN;;u_3bL&S?K#NF*F{Z_gc!bH-OT9_^Q?g59 q + + 0.2 + + + + + Position + false + + + cdecl + + Continuous + 0 + + + false + 0 + 0 + false + 0 + 0 + + + 6.2831853071795862 + 0.8 + + + 0.3 + + false + + 0 + 0 + + false + + 0 + 0 + + 0 + false + + false + 0 + 0 + 0 + 0 + 0 + + + + + 0 + 0 + + + + false + false + false + false + + + + 0 + 0 + + + Constant + 0 + + + + + + + diff --git a/pyFAST/input_output/tests/example_files/Bladed_out_binary_case2.%04 b/pyFAST/input_output/tests/example_files/Bladed_out_binary_case2.%04 new file mode 100644 index 0000000..5303072 --- /dev/null +++ b/pyFAST/input_output/tests/example_files/Bladed_out_binary_case2.%04 @@ -0,0 +1,39 @@ +FILE demo_a_44.$04 +ACCESS D +FORM U +RECL 4 +FORMAT R*4 +CONTENT POWPROD +CONFIG STATIONARY +NDIMENS 2 +DIMENS 9 10 +GENLAB 'Control variables' +VARIAB 'Time from start of simulation' 'Demanded power' 'Measured power' 'Demanded generator speed' 'Measured generator speed' 'Nominal pitch angle' 'Demanded generator torque' 'Nominal wind speed at hub position' 'Measured shaft power' +VARUNIT T P P A/T A/T A FL L/T P +AXISLAB 'Time' +AXIUNIT T +AXIMETH 2 +MIN 0.00000000E+00 +STEP 0.50000001E-01 +NVARS 0 +ULOADS 0.745000E+01 0.411234E+23 0.194046E+07 0.157079E+03 0.158929E+03 -0.349066E-01 0.128319E+05 0.130701E+02 0.204259E+07 + 0.700000E+01 0.411234E+23 0.191484E+07 0.157079E+03 0.159038E+03 -0.349066E-01 0.126798E+05 0.119510E+02 0.201562E+07 + 0.700000E+01 0.411234E+23 0.191484E+07 0.157079E+03 0.159038E+03 -0.349066E-01 0.126798E+05 0.119510E+02 0.201562E+07 + 0.000000E+00 0.990000E+21 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 + 0.745000E+01 0.411234E+23 0.194046E+07 0.157079E+03 0.158929E+03 -0.349066E-01 0.128319E+05 0.130701E+02 0.204259E+07 + 0.715000E+01 0.411234E+23 0.191410E+07 0.157079E+03 0.158926E+03 -0.349066E-01 0.126958E+05 0.125669E+02 0.201484E+07 + 0.700000E+01 0.411234E+23 0.191484E+07 0.157079E+03 0.159038E+03 -0.349066E-01 0.126798E+05 0.119510E+02 0.201562E+07 + 0.700000E+01 0.411234E+23 0.191484E+07 0.157079E+03 0.159038E+03 -0.349066E-01 0.126798E+05 0.119510E+02 0.201562E+07 + 0.700000E+01 0.411234E+23 0.191484E+07 0.157079E+03 0.159038E+03 -0.349066E-01 0.126798E+05 0.119510E+02 0.201562E+07 + 0.730000E+01 0.411234E+23 0.191817E+07 0.157079E+03 0.158835E+03 -0.349066E-01 0.127172E+05 0.126810E+02 0.201912E+07 + 0.700000E+01 0.411234E+23 0.191484E+07 0.157079E+03 0.159038E+03 -0.349066E-01 0.126798E+05 0.119510E+02 0.201562E+07 + 0.700000E+01 0.411234E+23 0.191484E+07 0.157079E+03 0.159038E+03 -0.349066E-01 0.126798E+05 0.119510E+02 0.201562E+07 + 0.745000E+01 0.411234E+23 0.194046E+07 0.157079E+03 0.158929E+03 -0.349066E-01 0.128319E+05 0.130701E+02 0.204259E+07 + 0.700000E+01 0.411234E+23 0.191484E+07 0.157079E+03 0.159038E+03 -0.349066E-01 0.126798E+05 0.119510E+02 0.201562E+07 + 0.745000E+01 0.411234E+23 0.194046E+07 0.157079E+03 0.158929E+03 -0.349066E-01 0.128319E+05 0.130701E+02 0.204259E+07 + 0.700000E+01 0.411234E+23 0.191484E+07 0.157079E+03 0.159038E+03 -0.349066E-01 0.126798E+05 0.119510E+02 0.201562E+07 + 0.745000E+01 0.411234E+23 0.194046E+07 0.157079E+03 0.158929E+03 -0.349066E-01 0.128319E+05 0.130701E+02 0.204259E+07 + 0.715000E+01 0.411234E+23 0.191410E+07 0.157079E+03 0.158926E+03 -0.349066E-01 0.126958E+05 0.125669E+02 0.201484E+07 +MAXTIME 0.450000E+00 0.000000E+00 0.450000E+00 0.000000E+00 0.000000E+00 0.000000E+00 0.450000E+00 0.450000E+00 0.450000E+00 +MINTIME 0.000000E+00 -0.500000E-01 0.150000E+00 0.000000E+00 0.300000E+00 0.000000E+00 0.000000E+00 0.000000E+00 0.150000E+00 +MEAN 0.722500E+01 0.411234E+23 0.192037E+07 0.157079E+03 0.158921E+03 -0.349066E-01 0.127260E+05 0.125565E+02 0.202144E+07 diff --git a/pyFAST/input_output/tests/example_files/Bladed_out_binary_case2.%06 b/pyFAST/input_output/tests/example_files/Bladed_out_binary_case2.%06 new file mode 100644 index 0000000..3f0f764 --- /dev/null +++ b/pyFAST/input_output/tests/example_files/Bladed_out_binary_case2.%06 @@ -0,0 +1,29 @@ +FILE demo_a_44.$06 +ACCESS D +FORM U +RECL 4 +FORMAT R*4 +CONTENT POWPROD +CONFIG STATIONARY +NDIMENS 2 +DIMENS 4 10 +GENLAB 'Generator variables' +VARIAB 'Generator torque' 'Electrical power' 'Generator power loss' 'Reactive power' +VARUNIT FL P P Q +AXISLAB 'Time' +AXIUNIT T +AXIMETH 2 +MIN 0.00000000E+00 +STEP 0.50000001E-01 +NVARS 0 +ULOADS 0.128522E+05 0.194046E+07 0.102130E+06 0.000000E+00 + 0.126738E+05 0.191484E+07 0.100781E+06 0.000000E+00 + 0.128522E+05 0.194046E+07 0.102130E+06 0.000000E+00 + 0.126778E+05 0.191410E+07 0.100742E+06 0.000000E+00 + 0.128522E+05 0.194046E+07 0.102130E+06 0.000000E+00 + 0.126778E+05 0.191410E+07 0.100742E+06 0.000000E+00 + 0.126738E+05 0.191484E+07 0.100781E+06 0.000000E+00 + 0.126738E+05 0.191484E+07 0.100781E+06 0.000000E+00 +MAXTIME 0.450000E+00 0.450000E+00 0.450000E+00 0.000000E+00 +MINTIME 0.000000E+00 0.150000E+00 0.150000E+00 0.000000E+00 +MEAN 0.127198E+05 0.192037E+07 0.101072E+06 0.000000E+00 diff --git a/pyFAST/input_output/tests/example_files/Bladed_out_binary_case2.%09 b/pyFAST/input_output/tests/example_files/Bladed_out_binary_case2.%09 new file mode 100644 index 0000000..7081588 --- /dev/null +++ b/pyFAST/input_output/tests/example_files/Bladed_out_binary_case2.%09 @@ -0,0 +1,59 @@ +FILE demo_a_44.$09 +ACCESS D +FORM U +RECL 4 +FORMAT R*4 +CONTENT POWPROD +CONFIG STATIONARY +NDIMENS 3 +DIMENS 17 1 10 +GENLAB 'Aerodynamic information: blade 1' +VARIAB ELRAD AXIALA1 TANGA1 TLOSS1 PHI1 ALPHA1 REYN1 CL1 CD1 CM1 WINDSP1 DFOUT1 DFIN1 DPMOM1 VP1 VT1 INFX1 +VARUNIT L N N N A A N N N N L/T F/L F/L FL/L L/T L/T N +AXISLAB 'Distance along blade' +AXIUNIT L +AXIMETH 3 +AXIVAL 26.407 +AXISLAB 'Time' +AXIUNIT T +AXIMETH 2 +MIN 0.00000000E+00 +STEP 0.50000001E-01 +NVARS 0 +ULOADS 0.276574E+02 0.253601E+00 0.109873E-01 0.982493E+00 0.187366E+00 0.164677E+00 0.676435E+07 0.143393E+01 0.152991E-01 -0.933515E-01 0.547070E+02 0.475387E+04 -0.848848E+03 -0.226851E+03 0.136527E+02 0.531654E+02 0.100000E+01 + 0.276574E+02 0.253601E+00 0.109873E-01 0.982493E+00 0.187366E+00 0.164677E+00 0.676435E+07 0.143393E+01 0.152991E-01 -0.933515E-01 0.547070E+02 0.475387E+04 -0.848848E+03 -0.226851E+03 0.136527E+02 0.531654E+02 0.100000E+01 + 0.276574E+02 0.319649E+00 0.947390E-02 0.995393E+00 0.136238E+00 0.113548E+00 0.671284E+07 0.118213E+01 0.899778E-02 -0.782873E-01 0.542904E+02 0.388817E+04 -0.502898E+03 -0.190484E+03 0.108378E+02 0.532825E+02 0.100000E+01 + 0.276574E+02 0.253601E+00 0.109873E-01 0.982493E+00 0.187366E+00 0.164677E+00 0.676435E+07 0.143393E+01 0.152991E-01 -0.933515E-01 0.547070E+02 0.475387E+04 -0.848848E+03 -0.226851E+03 0.136527E+02 0.531654E+02 0.100000E+01 + 0.276574E+02 0.263757E+00 0.112587E-01 0.985023E+00 0.179482E+00 0.156793E+00 0.668376E+07 0.138455E+01 0.129760E-01 -0.833697E-01 0.540552E+02 0.448655E+04 -0.770655E+03 -0.172653E+03 0.131070E+02 0.525947E+02 0.100000E+01 + 0.276574E+02 0.319649E+00 0.947390E-02 0.995393E+00 0.136238E+00 0.113548E+00 0.671284E+07 0.118213E+01 0.899778E-02 -0.782873E-01 0.542904E+02 0.388817E+04 -0.502898E+03 -0.190484E+03 0.108378E+02 0.532825E+02 0.100000E+01 + 0.276574E+02 0.319649E+00 0.947390E-02 0.995393E+00 0.136238E+00 0.113548E+00 0.671284E+07 0.118213E+01 0.899778E-02 -0.782873E-01 0.542904E+02 0.388817E+04 -0.502898E+03 -0.190484E+03 0.108378E+02 0.532825E+02 0.100000E+01 + 0.276574E+02 0.253601E+00 0.109873E-01 0.982493E+00 0.187366E+00 0.164677E+00 0.676435E+07 0.143393E+01 0.152991E-01 -0.933515E-01 0.547070E+02 0.475387E+04 -0.848848E+03 -0.226851E+03 0.136527E+02 0.531654E+02 0.100000E+01 + 0.276574E+02 0.253601E+00 0.109873E-01 0.982493E+00 0.187366E+00 0.164677E+00 0.676435E+07 0.143393E+01 0.152991E-01 -0.933515E-01 0.547070E+02 0.475387E+04 -0.848848E+03 -0.226851E+03 0.136527E+02 0.531654E+02 0.100000E+01 + 0.276574E+02 0.319649E+00 0.947390E-02 0.995393E+00 0.136238E+00 0.113548E+00 0.671284E+07 0.118213E+01 0.899778E-02 -0.782873E-01 0.542904E+02 0.388817E+04 -0.502898E+03 -0.190484E+03 0.108378E+02 0.532825E+02 0.100000E+01 + 0.276574E+02 0.253601E+00 0.109873E-01 0.982493E+00 0.187366E+00 0.164677E+00 0.676435E+07 0.143393E+01 0.152991E-01 -0.933515E-01 0.547070E+02 0.475387E+04 -0.848848E+03 -0.226851E+03 0.136527E+02 0.531654E+02 0.100000E+01 + 0.276574E+02 0.319649E+00 0.947390E-02 0.995393E+00 0.136238E+00 0.113548E+00 0.671284E+07 0.118213E+01 0.899778E-02 -0.782873E-01 0.542904E+02 0.388817E+04 -0.502898E+03 -0.190484E+03 0.108378E+02 0.532825E+02 0.100000E+01 + 0.276574E+02 0.253601E+00 0.109873E-01 0.982493E+00 0.187366E+00 0.164677E+00 0.676435E+07 0.143393E+01 0.152991E-01 -0.933515E-01 0.547070E+02 0.475387E+04 -0.848848E+03 -0.226851E+03 0.136527E+02 0.531654E+02 0.100000E+01 + 0.276574E+02 0.293917E+00 0.101556E-01 0.991046E+00 0.157636E+00 0.134947E+00 0.653945E+07 0.130386E+01 0.103582E-01 -0.926991E-01 0.528881E+02 0.405790E+04 -0.612012E+03 -0.236838E+03 0.117586E+02 0.517072E+02 0.100000E+01 + 0.276574E+02 0.253601E+00 0.109873E-01 0.982493E+00 0.187366E+00 0.164677E+00 0.676435E+07 0.143393E+01 0.152991E-01 -0.933515E-01 0.547070E+02 0.475387E+04 -0.848848E+03 -0.226851E+03 0.136527E+02 0.531654E+02 0.100000E+01 + 0.276574E+02 0.319649E+00 0.947390E-02 0.995393E+00 0.136238E+00 0.113548E+00 0.671284E+07 0.118213E+01 0.899778E-02 -0.782873E-01 0.542904E+02 0.388817E+04 -0.502898E+03 -0.190484E+03 0.108378E+02 0.532825E+02 0.100000E+01 + 0.276574E+02 0.253601E+00 0.109873E-01 0.982493E+00 0.187366E+00 0.164677E+00 0.676435E+07 0.143393E+01 0.152991E-01 -0.933515E-01 0.547070E+02 0.475387E+04 -0.848848E+03 -0.226851E+03 0.136527E+02 0.531654E+02 0.100000E+01 + 0.276574E+02 0.319649E+00 0.947390E-02 0.995393E+00 0.136238E+00 0.113548E+00 0.671284E+07 0.118213E+01 0.899778E-02 -0.782873E-01 0.542904E+02 0.388817E+04 -0.502898E+03 -0.190484E+03 0.108378E+02 0.532825E+02 0.100000E+01 + 0.276574E+02 0.280106E+00 0.109549E-01 0.988745E+00 0.166650E+00 0.143961E+00 0.661097E+07 0.132316E+01 0.901324E-02 -0.769566E-01 0.534665E+02 0.420185E+04 -0.677397E+03 -0.144921E+03 0.123199E+02 0.521545E+02 0.100000E+01 + 0.276574E+02 0.253601E+00 0.109873E-01 0.982493E+00 0.187366E+00 0.164677E+00 0.676435E+07 0.143393E+01 0.152991E-01 -0.933515E-01 0.547070E+02 0.475387E+04 -0.848848E+03 -0.226851E+03 0.136527E+02 0.531654E+02 0.100000E+01 + 0.276574E+02 0.253601E+00 0.109873E-01 0.982493E+00 0.187366E+00 0.164677E+00 0.676435E+07 0.143393E+01 0.152991E-01 -0.933515E-01 0.547070E+02 0.475387E+04 -0.848848E+03 -0.226851E+03 0.136527E+02 0.531654E+02 0.100000E+01 + 0.276574E+02 0.293917E+00 0.101556E-01 0.991046E+00 0.157636E+00 0.134947E+00 0.653945E+07 0.130386E+01 0.103582E-01 -0.926991E-01 0.528881E+02 0.405790E+04 -0.612012E+03 -0.236838E+03 0.117586E+02 0.517072E+02 0.100000E+01 + 0.276574E+02 0.253601E+00 0.109873E-01 0.982493E+00 0.187366E+00 0.164677E+00 0.676435E+07 0.143393E+01 0.152991E-01 -0.933515E-01 0.547070E+02 0.475387E+04 -0.848848E+03 -0.226851E+03 0.136527E+02 0.531654E+02 0.100000E+01 + 0.276574E+02 0.319649E+00 0.947390E-02 0.995393E+00 0.136238E+00 0.113548E+00 0.671284E+07 0.118213E+01 0.899778E-02 -0.782873E-01 0.542904E+02 0.388817E+04 -0.502898E+03 -0.190484E+03 0.108378E+02 0.532825E+02 0.100000E+01 + 0.276574E+02 0.319649E+00 0.947390E-02 0.995393E+00 0.136238E+00 0.113548E+00 0.671284E+07 0.118213E+01 0.899778E-02 -0.782873E-01 0.542904E+02 0.388817E+04 -0.502898E+03 -0.190484E+03 0.108378E+02 0.532825E+02 0.100000E+01 + 0.276574E+02 0.253601E+00 0.109873E-01 0.982493E+00 0.187366E+00 0.164677E+00 0.676435E+07 0.143393E+01 0.152991E-01 -0.933515E-01 0.547070E+02 0.475387E+04 -0.848848E+03 -0.226851E+03 0.136527E+02 0.531654E+02 0.100000E+01 + 0.276574E+02 0.280106E+00 0.109549E-01 0.988745E+00 0.166650E+00 0.143961E+00 0.661097E+07 0.132316E+01 0.901324E-02 -0.769566E-01 0.534665E+02 0.420185E+04 -0.677397E+03 -0.144921E+03 0.123199E+02 0.521545E+02 0.100000E+01 + 0.276574E+02 0.293917E+00 0.101556E-01 0.991046E+00 0.157636E+00 0.134947E+00 0.653945E+07 0.130386E+01 0.103582E-01 -0.926991E-01 0.528881E+02 0.405790E+04 -0.612012E+03 -0.236838E+03 0.117586E+02 0.517072E+02 0.100000E+01 + 0.276574E+02 0.253601E+00 0.109873E-01 0.982493E+00 0.187366E+00 0.164677E+00 0.676435E+07 0.143393E+01 0.152991E-01 -0.933515E-01 0.547070E+02 0.475387E+04 -0.848848E+03 -0.226851E+03 0.136527E+02 0.531654E+02 0.100000E+01 + 0.276574E+02 0.319649E+00 0.947390E-02 0.995393E+00 0.136238E+00 0.113548E+00 0.671284E+07 0.118213E+01 0.899778E-02 -0.782873E-01 0.542904E+02 0.388817E+04 -0.502898E+03 -0.190484E+03 0.108378E+02 0.532825E+02 0.100000E+01 + 0.276574E+02 0.319649E+00 0.947390E-02 0.995393E+00 0.136238E+00 0.113548E+00 0.671284E+07 0.118213E+01 0.899778E-02 -0.782873E-01 0.542904E+02 0.388817E+04 -0.502898E+03 -0.190484E+03 0.108378E+02 0.532825E+02 0.100000E+01 + 0.276574E+02 0.293917E+00 0.101556E-01 0.991046E+00 0.157636E+00 0.134947E+00 0.653945E+07 0.130386E+01 0.103582E-01 -0.926991E-01 0.528881E+02 0.405790E+04 -0.612012E+03 -0.236838E+03 0.117586E+02 0.517072E+02 0.100000E+01 + 0.276574E+02 0.253601E+00 0.109873E-01 0.982493E+00 0.187366E+00 0.164677E+00 0.676435E+07 0.143393E+01 0.152991E-01 -0.933515E-01 0.547070E+02 0.475387E+04 -0.848848E+03 -0.226851E+03 0.136527E+02 0.531654E+02 0.100000E+01 + 0.276574E+02 0.253601E+00 0.109873E-01 0.982493E+00 0.187366E+00 0.164677E+00 0.676435E+07 0.143393E+01 0.152991E-01 -0.933515E-01 0.547070E+02 0.475387E+04 -0.848848E+03 -0.226851E+03 0.136527E+02 0.531654E+02 0.100000E+01 +MAXTIME 0.000000E+00 0.450000E+00 0.500000E-01 0.450000E+00 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 0.100000E+00 0.000000E+00 0.000000E+00 0.450000E+00 0.100000E+00 0.000000E+00 0.450000E+00 0.000000E+00 +MINTIME 0.000000E+00 0.000000E+00 0.450000E+00 0.000000E+00 0.450000E+00 0.450000E+00 0.300000E+00 0.450000E+00 0.450000E+00 0.000000E+00 0.300000E+00 0.450000E+00 0.000000E+00 0.300000E+00 0.450000E+00 0.300000E+00 0.000000E+00 +MEAN 0.276574E+02 0.288380E+00 0.103229E-01 0.990101E+00 0.160178E+00 0.137489E+00 0.664723E+07 0.130496E+01 0.110595E-01 -0.850990E-01 0.537597E+02 0.419544E+04 -0.644742E+03 -0.199105E+03 0.120309E+02 0.525238E+02 0.100000E+01 diff --git a/pyFAST/input_output/tests/example_files/Bladed_out_binary_case2.%12 b/pyFAST/input_output/tests/example_files/Bladed_out_binary_case2.%12 new file mode 100644 index 0000000..c8aedbd --- /dev/null +++ b/pyFAST/input_output/tests/example_files/Bladed_out_binary_case2.%12 @@ -0,0 +1,47 @@ +FILE demo_a_44.$12 +ACCESS D +FORM U +RECL 4 +FORMAT R*4 +CONTENT POWPROD +CONFIG STATIONARY +NDIMENS 2 +DIMENS 13 10 +GENLAB 'Electrical variables' +VARIAB POW2 POW3 POW4 REAC2 REAC3 REAC4 PF2 PF3 PF4 VOLTS VOLTF TURBI FARMI +VARUNIT P P P Q Q Q N N N V V I I +AXISLAB 'Time' +AXIUNIT T +AXIMETH 2 +MIN 0.00000000E+00 +STEP 0.50000001E-01 +NVARS 0 +ULOADS 0.194046E+07 0.194046E+07 0.194015E+07 0.279992E-04 0.279992E-04 0.311090E+03 0.100000E+01 0.100000E+01 0.100000E+01 0.110018E+05 0.110018E+05 0.101832E+03 0.101832E+03 + 0.191410E+07 0.191410E+07 0.191380E+07 -0.245270E-04 -0.245270E-04 0.302695E+03 0.100000E+01 0.100000E+01 0.100000E+01 0.110017E+05 0.110017E+05 0.100448E+03 0.100448E+03 + 0.194046E+07 0.194046E+07 0.194015E+07 0.279992E-04 0.279992E-04 0.311090E+03 0.100000E+01 0.100000E+01 0.100000E+01 0.110018E+05 0.110018E+05 0.101832E+03 0.101832E+03 + 0.191410E+07 0.191410E+07 0.191380E+07 -0.245270E-04 -0.245270E-04 0.302695E+03 0.100000E+01 0.100000E+01 0.100000E+01 0.110017E+05 0.110017E+05 0.100448E+03 0.100448E+03 + 0.194046E+07 0.194046E+07 0.194015E+07 0.279992E-04 0.279992E-04 0.311090E+03 0.100000E+01 0.100000E+01 0.100000E+01 0.110018E+05 0.110018E+05 0.101832E+03 0.101832E+03 + 0.191410E+07 0.191410E+07 0.191380E+07 -0.245270E-04 -0.245270E-04 0.302695E+03 0.100000E+01 0.100000E+01 0.100000E+01 0.110017E+05 0.110017E+05 0.100448E+03 0.100448E+03 + 0.191534E+07 0.191534E+07 0.191503E+07 0.302640E-04 0.302640E-04 0.303087E+03 0.100000E+01 0.100000E+01 0.100000E+01 0.110017E+05 0.110017E+05 0.100513E+03 0.100513E+03 + 0.191410E+07 0.191410E+07 0.191380E+07 -0.245270E-04 -0.245270E-04 0.302695E+03 0.100000E+01 0.100000E+01 0.100000E+01 0.110017E+05 0.110017E+05 0.100448E+03 0.100448E+03 + 0.191534E+07 0.191534E+07 0.191503E+07 0.302640E-04 0.302640E-04 0.303087E+03 0.100000E+01 0.100000E+01 0.100000E+01 0.110017E+05 0.110017E+05 0.100513E+03 0.100513E+03 + 0.191410E+07 0.191410E+07 0.191380E+07 -0.245270E-04 -0.245270E-04 0.302695E+03 0.100000E+01 0.100000E+01 0.100000E+01 0.110017E+05 0.110017E+05 0.100448E+03 0.100448E+03 + 0.194046E+07 0.194046E+07 0.194015E+07 0.279992E-04 0.279992E-04 0.311090E+03 0.100000E+01 0.100000E+01 0.100000E+01 0.110018E+05 0.110018E+05 0.101832E+03 0.101832E+03 + 0.191410E+07 0.191410E+07 0.191380E+07 -0.245270E-04 -0.245270E-04 0.302695E+03 0.100000E+01 0.100000E+01 0.100000E+01 0.110017E+05 0.110017E+05 0.100448E+03 0.100448E+03 + 0.191484E+07 0.191484E+07 0.191453E+07 -0.857543E-05 -0.857543E-05 0.302929E+03 0.100000E+01 0.100000E+01 0.100000E+01 0.110017E+05 0.110017E+05 0.100487E+03 0.100487E+03 + 0.191484E+07 0.191484E+07 0.191453E+07 -0.857543E-05 -0.857543E-05 0.302929E+03 0.100000E+01 0.100000E+01 0.100000E+01 0.110017E+05 0.110017E+05 0.100487E+03 0.100487E+03 + 0.191484E+07 0.191484E+07 0.191453E+07 -0.857543E-05 -0.857543E-05 0.302929E+03 0.100000E+01 0.100000E+01 0.100000E+01 0.110017E+05 0.110017E+05 0.100487E+03 0.100487E+03 + 0.191484E+07 0.191484E+07 0.191453E+07 -0.857543E-05 -0.857543E-05 0.302929E+03 0.100000E+01 0.100000E+01 0.100000E+01 0.110017E+05 0.110017E+05 0.100487E+03 0.100487E+03 + 0.191484E+07 0.191484E+07 0.191453E+07 -0.857543E-05 -0.857543E-05 0.302929E+03 0.100000E+01 0.100000E+01 0.100000E+01 0.110017E+05 0.110017E+05 0.100487E+03 0.100487E+03 + 0.191484E+07 0.191484E+07 0.191453E+07 -0.857543E-05 -0.857543E-05 0.302929E+03 0.100000E+01 0.100000E+01 0.100000E+01 0.110017E+05 0.110017E+05 0.100487E+03 0.100487E+03 + 0.194046E+07 0.194046E+07 0.194015E+07 0.279992E-04 0.279992E-04 0.311090E+03 0.100000E+01 0.100000E+01 0.100000E+01 0.110018E+05 0.110018E+05 0.101832E+03 0.101832E+03 + 0.191484E+07 0.191484E+07 0.191453E+07 -0.857543E-05 -0.857543E-05 0.302929E+03 0.100000E+01 0.100000E+01 0.100000E+01 0.110017E+05 0.110017E+05 0.100487E+03 0.100487E+03 + 0.194046E+07 0.194046E+07 0.194015E+07 0.279992E-04 0.279992E-04 0.311090E+03 0.100000E+01 0.100000E+01 0.100000E+01 0.110018E+05 0.110018E+05 0.101832E+03 0.101832E+03 + 0.191410E+07 0.191410E+07 0.191380E+07 -0.245270E-04 -0.245270E-04 0.302695E+03 0.100000E+01 0.100000E+01 0.100000E+01 0.110017E+05 0.110017E+05 0.100448E+03 0.100448E+03 + 0.194046E+07 0.194046E+07 0.194015E+07 0.279992E-04 0.279992E-04 0.311090E+03 0.100000E+01 0.100000E+01 0.100000E+01 0.110018E+05 0.110018E+05 0.101832E+03 0.101832E+03 + 0.191410E+07 0.191410E+07 0.191380E+07 -0.245270E-04 -0.245270E-04 0.302695E+03 0.100000E+01 0.100000E+01 0.100000E+01 0.110017E+05 0.110017E+05 0.100448E+03 0.100448E+03 + 0.194046E+07 0.194046E+07 0.194015E+07 0.279992E-04 0.279992E-04 0.311090E+03 0.100000E+01 0.100000E+01 0.100000E+01 0.110018E+05 0.110018E+05 0.101832E+03 0.101832E+03 + 0.191410E+07 0.191410E+07 0.191380E+07 -0.245270E-04 -0.245270E-04 0.302695E+03 0.100000E+01 0.100000E+01 0.100000E+01 0.110017E+05 0.110017E+05 0.100448E+03 0.100448E+03 +MAXTIME 0.450000E+00 0.450000E+00 0.450000E+00 0.500000E-01 0.500000E-01 0.450000E+00 0.000000E+00 0.000000E+00 0.000000E+00 0.450000E+00 0.450000E+00 0.450000E+00 0.450000E+00 +MINTIME 0.150000E+00 0.150000E+00 0.150000E+00 0.150000E+00 0.150000E+00 0.150000E+00 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 0.150000E+00 0.150000E+00 0.150000E+00 +MEAN 0.192037E+07 0.192037E+07 0.192007E+07 0.581092E-05 0.581092E-05 0.304689E+03 0.100000E+01 0.100000E+01 0.100000E+01 0.110017E+05 0.110017E+05 0.100777E+03 0.100777E+03 diff --git a/pyFAST/input_output/tests/example_files/Bladed_out_binary_case2.%23 b/pyFAST/input_output/tests/example_files/Bladed_out_binary_case2.%23 new file mode 100644 index 0000000..c25f5bb --- /dev/null +++ b/pyFAST/input_output/tests/example_files/Bladed_out_binary_case2.%23 @@ -0,0 +1,37 @@ +FILE demo_a_44.$23 +ACCESS D +FORM U +RECL 4 +FORMAT R*4 +CONTENT POWPROD +CONFIG STATIONARY +NDIMENS 2 +DIMENS 8 10 +GENLAB 'Hub loads: fixed frame GL coordinates' +VARIAB 'Stationary hub Mx' 'Stationary hub My' 'Stationary hub Mz' 'Stationary hub Myz' 'Stationary hub Fx' 'Stationary hub Fy' 'Stationary hub Fz' 'Stationary hub Fyz' +VARUNIT FL FL FL FL F F F F +AXISLAB 'Time' +AXIUNIT T +AXIMETH 2 +MIN 0.00000000E+00 +STEP 0.50000001E-01 +NVARS 0 +ULOADS 0.111591E+07 0.726479E+06 0.178399E+06 0.748063E+06 0.315027E+06 -0.117953E+05 -0.334988E+06 0.335195E+06 + 0.109298E+07 0.439091E+06 0.242142E+06 0.501432E+06 0.324923E+06 -0.395683E+04 -0.324615E+06 0.324639E+06 + 0.111591E+07 0.726479E+06 0.178399E+06 0.748063E+06 0.315027E+06 -0.117953E+05 -0.334988E+06 0.335195E+06 + 0.109298E+07 0.439091E+06 0.242142E+06 0.501432E+06 0.324923E+06 -0.395683E+04 -0.324615E+06 0.324639E+06 + 0.109298E+07 0.439091E+06 0.242142E+06 0.501432E+06 0.324923E+06 -0.395683E+04 -0.324615E+06 0.324639E+06 + 0.109755E+07 0.576906E+06 0.939693E+05 0.584509E+06 0.316656E+06 -0.124688E+05 -0.324705E+06 0.324944E+06 + 0.111591E+07 0.726479E+06 0.178399E+06 0.748063E+06 0.315027E+06 -0.117953E+05 -0.334988E+06 0.335195E+06 + 0.109779E+07 0.461799E+06 0.165613E+06 0.490598E+06 0.334001E+06 -0.240930E+04 -0.334644E+06 0.334653E+06 + 0.109779E+07 0.461799E+06 0.165613E+06 0.490598E+06 0.334001E+06 -0.240930E+04 -0.334644E+06 0.334653E+06 + 0.111228E+07 0.698521E+06 0.209028E+06 0.729126E+06 0.314737E+06 -0.832075E+04 -0.338662E+06 0.338765E+06 + 0.109554E+07 0.450214E+06 0.224881E+06 0.503253E+06 0.329445E+06 -0.186323E+04 -0.326567E+06 0.326572E+06 + 0.110763E+07 0.702631E+06 0.142612E+06 0.716958E+06 0.316780E+06 -0.140612E+05 -0.327942E+06 0.328243E+06 + 0.110023E+07 0.642055E+06 0.105366E+06 0.650643E+06 0.316823E+06 -0.139339E+05 -0.323937E+06 0.324236E+06 + 0.111228E+07 0.698521E+06 0.209028E+06 0.729126E+06 0.314737E+06 -0.832075E+04 -0.338662E+06 0.338765E+06 + 0.111228E+07 0.698521E+06 0.209028E+06 0.729126E+06 0.314737E+06 -0.832075E+04 -0.338662E+06 0.338765E+06 + 0.110023E+07 0.642055E+06 0.105366E+06 0.650643E+06 0.316823E+06 -0.139339E+05 -0.323937E+06 0.324236E+06 +MAXTIME 0.400000E+00 0.400000E+00 0.100000E+00 0.400000E+00 0.000000E+00 0.500000E-01 0.300000E+00 0.450000E+00 +MINTIME 0.100000E+00 0.100000E+00 0.250000E+00 0.000000E+00 0.450000E+00 0.350000E+00 0.450000E+00 0.300000E+00 +MEAN 0.110094E+07 0.565960E+06 0.167636E+06 0.594012E+06 0.320398E+06 -0.873886E+04 -0.329187E+06 0.329333E+06 diff --git a/pyFAST/input_output/tests/example_files/Bladed_out_binary_case2.%25 b/pyFAST/input_output/tests/example_files/Bladed_out_binary_case2.%25 new file mode 100644 index 0000000..3809798 --- /dev/null +++ b/pyFAST/input_output/tests/example_files/Bladed_out_binary_case2.%25 @@ -0,0 +1,60 @@ +FILE demo_a_44.$25 +ACCESS D +FORM U +RECL 4 +FORMAT R*4 +CONTENT POWPROD +CONFIG STATIONARY +NDIMENS 3 +DIMENS 8 2 10 +GENLAB 'Tower loads GL coordinates' +VARIAB MXT MYT 'Tower Mxy' MZT FXT FYT 'Tower Fxy' FZT +VARUNIT FL FL FL FL F F F F +AXISLAB 'Tower station height' +AXIUNIT L +AXIMETH 3 +AXIVAL -15.000 15.000 +AXISLAB 'Time' +AXIUNIT T +AXIMETH 2 +MIN 0.00000000E+00 +STEP 0.50000001E-01 +NVARS 0 +ULOADS 0.193523E+07 0.223290E+08 0.224127E+08 0.949411E+05 0.285783E+06 -0.142219E+05 0.286137E+06 -0.255039E+07 + 0.155100E+07 0.233759E+08 0.234273E+08 0.145314E+06 0.303190E+06 -0.215841E+04 0.303198E+06 -0.254935E+07 + 0.158753E+07 0.238570E+08 0.239098E+08 0.161275E+06 0.323650E+06 -0.259231E+04 0.323661E+06 -0.255189E+07 + 0.189305E+07 0.222598E+08 0.223402E+08 0.151846E+06 0.278150E+06 -0.128261E+05 0.278446E+06 -0.254252E+07 + 0.158753E+07 0.238570E+08 0.239098E+08 0.161275E+06 0.323650E+06 -0.259231E+04 0.323661E+06 -0.255189E+07 + 0.189305E+07 0.222598E+08 0.223402E+08 0.151846E+06 0.278150E+06 -0.128261E+05 0.278446E+06 -0.254252E+07 + 0.177004E+07 0.226428E+08 0.227119E+08 0.177328E+06 0.291506E+06 -0.806033E+04 0.291617E+06 -0.253957E+07 + 0.167238E+07 0.233032E+08 0.233631E+08 0.661959E+05 0.310273E+06 -0.778131E+04 0.310371E+06 -0.253879E+07 + 0.158753E+07 0.238570E+08 0.239098E+08 0.161275E+06 0.323650E+06 -0.259231E+04 0.323661E+06 -0.255189E+07 + 0.189305E+07 0.222598E+08 0.223402E+08 0.151846E+06 0.278150E+06 -0.128261E+05 0.278446E+06 -0.254252E+07 + 0.155100E+07 0.233759E+08 0.234273E+08 0.145314E+06 0.303190E+06 -0.215841E+04 0.303198E+06 -0.254935E+07 + 0.193523E+07 0.223290E+08 0.224127E+08 0.949411E+05 0.285783E+06 -0.142219E+05 0.286137E+06 -0.255039E+07 + 0.158753E+07 0.238570E+08 0.239098E+08 0.161275E+06 0.323650E+06 -0.259231E+04 0.323661E+06 -0.255189E+07 + 0.189305E+07 0.222598E+08 0.223402E+08 0.151846E+06 0.278150E+06 -0.128261E+05 0.278446E+06 -0.254252E+07 + 0.167238E+07 0.233032E+08 0.233631E+08 0.661959E+05 0.310273E+06 -0.778131E+04 0.310371E+06 -0.253879E+07 + 0.158753E+07 0.238570E+08 0.239098E+08 0.161275E+06 0.323650E+06 -0.259231E+04 0.323661E+06 -0.255189E+07 +MAXTIME 0.000000E+00 0.450000E+00 0.450000E+00 0.100000E+00 0.450000E+00 0.400000E+00 0.450000E+00 0.250000E+00 +MINTIME 0.400000E+00 0.500000E-01 0.500000E-01 0.250000E+00 0.500000E-01 0.000000E+00 0.500000E-01 0.450000E+00 +MEAN 0.169947E+07 0.230326E+08 0.230957E+08 0.123108E+06 0.300157E+06 -0.712561E+04 0.300270E+06 -0.254371E+07 +ULOADS 0.152733E+07 0.135974E+08 0.136830E+08 0.943611E+05 0.290799E+06 -0.118690E+05 0.291041E+06 -0.171844E+07 + 0.142459E+07 0.140976E+08 0.141694E+08 0.832236E+05 0.295676E+06 -0.705929E+04 0.295760E+06 -0.170697E+07 + 0.143419E+07 0.141719E+08 0.142442E+08 0.119811E+06 0.294105E+06 -0.547746E+04 0.294156E+06 -0.171148E+07 + 0.152733E+07 0.135974E+08 0.136830E+08 0.943611E+05 0.290799E+06 -0.118690E+05 0.291041E+06 -0.171844E+07 + 0.143419E+07 0.141719E+08 0.142442E+08 0.119811E+06 0.294105E+06 -0.547746E+04 0.294156E+06 -0.171148E+07 + 0.152733E+07 0.135974E+08 0.136830E+08 0.943611E+05 0.290799E+06 -0.118690E+05 0.291041E+06 -0.171844E+07 + 0.151470E+07 0.137452E+08 0.138284E+08 0.176604E+06 0.295902E+06 -0.841797E+04 0.296021E+06 -0.170762E+07 + 0.143854E+07 0.139432E+08 0.140172E+08 0.654555E+05 0.306145E+06 -0.713270E+04 0.306228E+06 -0.170685E+07 + 0.147656E+07 0.141467E+08 0.142236E+08 0.160369E+06 0.314998E+06 -0.468302E+04 0.315032E+06 -0.171994E+07 + 0.151978E+07 0.136988E+08 0.137828E+08 0.151257E+06 0.287992E+06 -0.110697E+05 0.288205E+06 -0.171058E+07 + 0.145647E+07 0.141671E+08 0.142418E+08 0.144452E+06 0.303962E+06 -0.399357E+04 0.303989E+06 -0.171741E+07 + 0.152733E+07 0.135974E+08 0.136830E+08 0.943611E+05 0.290799E+06 -0.118690E+05 0.291041E+06 -0.171844E+07 + 0.147656E+07 0.141467E+08 0.142236E+08 0.160369E+06 0.314998E+06 -0.468302E+04 0.315032E+06 -0.171994E+07 + 0.151978E+07 0.136988E+08 0.137828E+08 0.151257E+06 0.287992E+06 -0.110697E+05 0.288205E+06 -0.171058E+07 + 0.143854E+07 0.139432E+08 0.140172E+08 0.654555E+05 0.306145E+06 -0.713270E+04 0.306228E+06 -0.170685E+07 + 0.147656E+07 0.141467E+08 0.142236E+08 0.160369E+06 0.314998E+06 -0.468302E+04 0.315032E+06 -0.171994E+07 +MAXTIME 0.000000E+00 0.350000E+00 0.350000E+00 0.100000E+00 0.450000E+00 0.400000E+00 0.450000E+00 0.250000E+00 +MINTIME 0.300000E+00 0.000000E+00 0.000000E+00 0.250000E+00 0.500000E-01 0.000000E+00 0.500000E-01 0.450000E+00 +MEAN 0.147594E+07 0.139156E+08 0.139937E+08 0.122358E+06 0.301069E+06 -0.723169E+04 0.301168E+06 -0.171176E+07 diff --git a/pyFAST/input_output/tests/example_files/Bladed_out_binary_case2.%31 b/pyFAST/input_output/tests/example_files/Bladed_out_binary_case2.%31 new file mode 100644 index 0000000..35e5c67 --- /dev/null +++ b/pyFAST/input_output/tests/example_files/Bladed_out_binary_case2.%31 @@ -0,0 +1,31 @@ +FILE demo_a_44.$31 +ACCESS D +FORM U +RECL 4 +FORMAT R*4 +CONTENT POWPROD +CONFIG STATIONARY +NDIMENS 3 +DIMENS 3 1 10 +GENLAB 'Blade 1 Absolute motion' +VARIAB 'Blade 1 x-position' 'Blade 1 y-position' 'Blade 1 z-position' +VARUNIT L L L +AXISLAB 'Distance along blade' +AXIUNIT L +AXIMETH 3 +AXIVAL 38.750 +AXISLAB 'Time' +AXIUNIT T +AXIMETH 2 +MIN 0.00000000E+00 +STEP 0.50000001E-01 +NVARS 0 +ULOADS 0.135292E+01 -0.279491E+02 0.898919E+02 + -0.150286E+01 -0.399608E+02 0.589603E+02 + 0.135292E+01 -0.279491E+02 0.898919E+02 + -0.119360E+01 -0.400085E+02 0.627606E+02 + 0.135292E+01 -0.279491E+02 0.898919E+02 + -0.150286E+01 -0.399608E+02 0.589603E+02 +MAXTIME 0.000000E+00 0.000000E+00 0.000000E+00 +MINTIME 0.450000E+00 0.400000E+00 0.450000E+00 +MEAN -0.180593E-01 -0.359549E+02 0.751973E+02 diff --git a/pyFAST/input_output/tests/example_files/Bladed_out_binary_case2.%37 b/pyFAST/input_output/tests/example_files/Bladed_out_binary_case2.%37 new file mode 100644 index 0000000..3c6663d --- /dev/null +++ b/pyFAST/input_output/tests/example_files/Bladed_out_binary_case2.%37 @@ -0,0 +1,31 @@ +FILE demo_a_44.$37 +ACCESS S +FORM F +RECL 4 +FORMAT R*4 +CONTENT 'POWPROD' +CONFIG 'STATIONARY' +NDIMENS 2 +DIMENS 5 522 +GENLAB 'Simulation step sizes' +VARIAB 'Simulation Time' 'Constrained Step Size' 'Actual Step Size' 'Requested Size of Next Step' 'State with largest error' +VARUNIT T T T T N +AXISLAB 'Step number' +AXIUNIT N +AXIMETH 2 +MIN 0.00000000E+000 +STEP 1.00000000E+000 +NVARS 0 +ULOADS 7.450000E+000 6.904487E-003 6.904487E-003 2.330073E-002 9.000000E+000 + 1.000000E-007 1.000000E-007 1.000000E-007 1.000000E-006 3.900000E+001 + 2.534743E+000 3.474264E-002 3.474264E-002 3.532558E-002 5.000000E+000 + 1.000000E-007 1.000000E-007 1.000000E-007 1.000000E-006 3.900000E+001 + 2.534743E+000 3.474264E-002 3.474264E-002 3.532558E-002 5.000000E+000 + 1.000000E-007 1.000000E-007 1.000000E-007 1.000000E-006 3.900000E+001 + 2.031245E+000 3.124481E-002 3.124481E-002 3.539262E-002 4.500000E+001 + 1.044923E-005 2.773416E-006 4.591858E-007 4.159318E-007 2.700000E+001 + 1.912216E-003 1.296193E-003 1.296193E-003 1.282779E-003 4.800000E+001 + 1.109353E-001 2.281667E-002 1.093534E-002 1.142270E-002 5.000000E+000 +MAXTIME 5.210000E+002 1.750000E+002 1.750000E+002 1.440000E+002 1.200000E+001 +MINTIME 0.000000E+000 0.000000E+000 0.000000E+000 5.000000E+000 2.500000E+001 +MEAN 3.647993E+000 1.482273E-002 1.427202E-002 1.861919E-002 2.054598E+001 \ No newline at end of file diff --git a/pyFAST/input_output/tests/example_files/Bladed_out_binary_case2.%46 b/pyFAST/input_output/tests/example_files/Bladed_out_binary_case2.%46 new file mode 100644 index 0000000..7f7f18a --- /dev/null +++ b/pyFAST/input_output/tests/example_files/Bladed_out_binary_case2.%46 @@ -0,0 +1,56 @@ +FILE demo_a_44.$46 +ACCESS D +FORM U +RECL 4 +FORMAT R*4 +CONTENT POWPROD +CONFIG STATIONARY +NDIMENS 3 +DIMENS 4 3 10 +GENLAB 'Water particle kinematics' +VARIAB 'Water particle velocity in X direction' 'Water particle velocity in Y direction' +'Water particle acceleration in X direction' 'Water particle acceleration in Y direction' +VARUNIT L/T L/T L/TT L/TT +AXISLAB 'Tower station height' +AXIUNIT L +AXIMETH 3 +AXITICK 'Node 1' 'Node 2' 'Node 3' +AXISLAB 'Time' +AXIUNIT T +AXIMETH 2 +MIN 0.00000000E+00 +STEP 0.50000001E-01 +NVARS 0 +ULOADS -0.250000E+00 0.000000E+00 0.000000E+00 0.000000E+00 + -0.250000E+00 0.000000E+00 0.000000E+00 0.000000E+00 + -0.250000E+00 0.000000E+00 0.000000E+00 0.000000E+00 + -0.250000E+00 0.000000E+00 0.000000E+00 0.000000E+00 + -0.250000E+00 0.000000E+00 0.000000E+00 0.000000E+00 + -0.250000E+00 0.000000E+00 0.000000E+00 0.000000E+00 + -0.250000E+00 0.000000E+00 0.000000E+00 0.000000E+00 + -0.250000E+00 0.000000E+00 0.000000E+00 0.000000E+00 +MAXTIME 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 +MINTIME 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 +MEAN -0.250000E+00 0.000000E+00 0.000000E+00 0.000000E+00 +ULOADS -0.250000E+00 0.000000E+00 0.000000E+00 0.000000E+00 + -0.250000E+00 0.000000E+00 0.000000E+00 0.000000E+00 + -0.250000E+00 0.000000E+00 0.000000E+00 0.000000E+00 + -0.250000E+00 0.000000E+00 0.000000E+00 0.000000E+00 + -0.250000E+00 0.000000E+00 0.000000E+00 0.000000E+00 + -0.250000E+00 0.000000E+00 0.000000E+00 0.000000E+00 + -0.250000E+00 0.000000E+00 0.000000E+00 0.000000E+00 + -0.250000E+00 0.000000E+00 0.000000E+00 0.000000E+00 +MAXTIME 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 +MINTIME 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 +MEAN -0.250000E+00 0.000000E+00 0.000000E+00 0.000000E+00 +ULOADS -0.250000E+00 0.000000E+00 0.000000E+00 0.000000E+00 + -0.250000E+00 0.000000E+00 0.000000E+00 0.000000E+00 + -0.250000E+00 0.000000E+00 0.000000E+00 0.000000E+00 + -0.250000E+00 0.000000E+00 0.000000E+00 0.000000E+00 + -0.250000E+00 0.000000E+00 0.000000E+00 0.000000E+00 + -0.250000E+00 0.000000E+00 0.000000E+00 0.000000E+00 + -0.250000E+00 0.000000E+00 0.000000E+00 0.000000E+00 + -0.250000E+00 0.000000E+00 0.000000E+00 0.000000E+00 +MAXTIME 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 +MINTIME 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 +MEAN -0.250000E+00 0.000000E+00 0.000000E+00 0.000000E+00 diff --git a/pyFAST/input_output/tests/example_files/Bladed_out_binary_case2.%55 b/pyFAST/input_output/tests/example_files/Bladed_out_binary_case2.%55 new file mode 100644 index 0000000..c16b84e --- /dev/null +++ b/pyFAST/input_output/tests/example_files/Bladed_out_binary_case2.%55 @@ -0,0 +1,17 @@ +FILE demo_a_44.$55 +ACCESS D +FORM F +RECL 4 +FORMAT I*4 +CONTENT 'POWPROD' +CONFIG 'TABLE' +NDIMENS 2 +DIMENS 1 50 +GENLAB 'Step size distribution' +VARIAB 'Step size histogram' +VARUNIT N +AXISLAB 'Bin maximum time step' +AXIUNIT T +AXIMETH 3 +AXIVAL 1e-007 1.38038e-007 1.90546e-007 2.63027e-007 3.63078e-007 5.01187e-007 6.91831e-007 9.54993e-007 1.31826e-006 1.8197e-006 2.51189e-006 3.46737e-006 4.7863e-006 6.60693e-006 9.12011e-006 1.25893e-005 1.7378e-005 2.39883e-005 3.31131e-005 4.57088e-005 6.30957e-005 8.70964e-005 0.000120226 0.000165959 0.000229087 0.000316228 0.000436516 0.00060256 0.000831764 0.00114815 0.00158489 0.00218776 0.00301995 0.00416869 0.0057544 0.00794328 0.0109648 0.0151356 0.020893 0.0288403 0.0549541 0.0758578 0.104713 0.144544 0.199526 0.275423 0.380189 0.524807 0.724436 1 +NVARS 0 diff --git a/pyFAST/input_output/tests/example_files/Bladed_out_binary_case2.%69 b/pyFAST/input_output/tests/example_files/Bladed_out_binary_case2.%69 new file mode 100644 index 0000000..e61c028 --- /dev/null +++ b/pyFAST/input_output/tests/example_files/Bladed_out_binary_case2.%69 @@ -0,0 +1,33 @@ +FILE demo_a_44.$69 +ACCESS D +FORM U +RECL 4 +FORMAT R*4 +CONTENT POWPROD +CONFIG STATIONARY +NDIMENS 2 +DIMENS 6 10 +GENLAB 'Foundation loads' +VARIAB 'Foundation Mx' 'Foundation My' 'Foundation Mz' 'Foundation Fx' 'Foundation Fy' 'Foundation Fz' +VARUNIT FL FL FL F F F +AXISLAB 'Time' +AXIUNIT T +AXIMETH 2 +MIN 0.00000000E+00 +STEP 0.50000001E-01 +NVARS 0 +ULOADS 0.193494E+07 0.223270E+08 0.949585E+05 0.286451E+06 -0.142810E+05 -0.109217E+07 + 0.155071E+07 0.233738E+08 0.145302E+06 0.303888E+06 -0.220653E+04 -0.109113E+07 + 0.158724E+07 0.238550E+08 0.161258E+06 0.324362E+06 -0.264157E+04 -0.109366E+07 + 0.189275E+07 0.222578E+08 0.151863E+06 0.278816E+06 -0.128840E+05 -0.108430E+07 + 0.176975E+07 0.226408E+08 0.177331E+06 0.292182E+06 -0.811495E+04 -0.108134E+07 + 0.167209E+07 0.233011E+08 0.661929E+05 0.310968E+06 -0.783274E+04 -0.108057E+07 + 0.158724E+07 0.238550E+08 0.161258E+06 0.324362E+06 -0.264157E+04 -0.109366E+07 + 0.189275E+07 0.222578E+08 0.151863E+06 0.278816E+06 -0.128840E+05 -0.108430E+07 + 0.155071E+07 0.233738E+08 0.145302E+06 0.303888E+06 -0.220653E+04 -0.109113E+07 + 0.193494E+07 0.223270E+08 0.949585E+05 0.286451E+06 -0.142810E+05 -0.109217E+07 + 0.167209E+07 0.233011E+08 0.661929E+05 0.310968E+06 -0.783274E+04 -0.108057E+07 + 0.158724E+07 0.238550E+08 0.161258E+06 0.324362E+06 -0.264157E+04 -0.109366E+07 +MAXTIME 0.000000E+00 0.450000E+00 0.100000E+00 0.450000E+00 0.400000E+00 0.250000E+00 +MINTIME 0.400000E+00 0.500000E-01 0.250000E+00 0.500000E-01 0.000000E+00 0.450000E+00 +MEAN 0.169918E+07 0.230306E+08 0.123107E+06 0.300845E+06 -0.717798E+04 -0.108548E+07 diff --git a/pyFAST/input_output/tests/example_files/Bladed_out_binary_case2_fail.$55 b/pyFAST/input_output/tests/example_files/Bladed_out_binary_case2_fail.$55 new file mode 100644 index 0000000000000000000000000000000000000000..f350cb9799f84e79890c4f681c27dc1669711b70 GIT binary patch literal 200 zcmZQ%Kn09IE)$Xvhz|oWahMp4hRK6yWHA^UL^A_1NIwXJ_?%G84WxyDSP6(Vf!Gv? Q-GR6sh^v8^4Ty;W0CF?}3jhEB literal 0 HcmV?d00001 diff --git a/pyFAST/input_output/tests/example_files/Bladed_out_binary_case2_fail.%55 b/pyFAST/input_output/tests/example_files/Bladed_out_binary_case2_fail.%55 new file mode 100644 index 0000000..c16b84e --- /dev/null +++ b/pyFAST/input_output/tests/example_files/Bladed_out_binary_case2_fail.%55 @@ -0,0 +1,17 @@ +FILE demo_a_44.$55 +ACCESS D +FORM F +RECL 4 +FORMAT I*4 +CONTENT 'POWPROD' +CONFIG 'TABLE' +NDIMENS 2 +DIMENS 1 50 +GENLAB 'Step size distribution' +VARIAB 'Step size histogram' +VARUNIT N +AXISLAB 'Bin maximum time step' +AXIUNIT T +AXIMETH 3 +AXIVAL 1e-007 1.38038e-007 1.90546e-007 2.63027e-007 3.63078e-007 5.01187e-007 6.91831e-007 9.54993e-007 1.31826e-006 1.8197e-006 2.51189e-006 3.46737e-006 4.7863e-006 6.60693e-006 9.12011e-006 1.25893e-005 1.7378e-005 2.39883e-005 3.31131e-005 4.57088e-005 6.30957e-005 8.70964e-005 0.000120226 0.000165959 0.000229087 0.000316228 0.000436516 0.00060256 0.000831764 0.00114815 0.00158489 0.00218776 0.00301995 0.00416869 0.0057544 0.00794328 0.0109648 0.0151356 0.020893 0.0288403 0.0549541 0.0758578 0.104713 0.144544 0.199526 0.275423 0.380189 0.524807 0.724436 1 +NVARS 0 diff --git a/pyFAST/input_output/tests/example_files/FASTIn_AD15_arf_multitabs.dat b/pyFAST/input_output/tests/example_files/FASTIn_AD15_arf_multitabs.dat new file mode 100644 index 0000000..8d6506f --- /dev/null +++ b/pyFAST/input_output/tests/example_files/FASTIn_AD15_arf_multitabs.dat @@ -0,0 +1,146 @@ +! ------------ AirfoilInfo v1.01.x Input File ---------------------------------- +! DU25 airfoil with an aspect ratio of 17. Original -180 to 180deg Cl, Cd, and Cm versus AOA data taken from Appendix A of DOWEC document 10046_009.pdf (numerical values obtained from Koert Lindenburg of ECN). +! Cl and Cd values corrected for rotational stall delay and Cd values corrected using the Viterna method for 0 to 90deg AOA by Jason Jonkman using AirfoilPrep_v2p0.xls. +! note that this file uses Marshall Buhl's new input file processing; start all comment lines with ! +! ------------------------------------------------------------------------------ +"DEFAULT" InterpOrd - Interpolation order to use for quasi-steady table lookup {1=linear; 3=cubic spline; "default"} [default=1] +default RelThickness - The non-dimensional thickness of the airfoil (thickness/chord) [only used if UAMod=7] [default=0.2] (-) +1 NonDimArea - The non-dimensional area of the airfoil (area/chord^2) (set to 1.0 if unsure or unneeded) +0 NumCoords - The number of coordinates in the airfoil shape file. Set to zero if coordinates not included. +"unused" BL_file - The file name including the boundary layer characteristics of the profile. Ignored if the aeroacoustic module is not called. +2 NumTabs - Number of airfoil tables in this file. Each table must have lines for Re and Ctrl. +! ------------------------------------------------------------------------------ +! data for table 1 +! ------------------------------------------------------------------------------ +0.05 Re - Reynolds number in millions +0 UserProp - User property (control) setting +True InclUAdata - Is unsteady aerodynamics data included in this table? If TRUE, then include 30 UA coefficients below this line +!........................................ +-2.1039 alpha0 - 0-lift angle of attack, depends on airfoil. +12.3238 alpha1 - Angle of attack at f=0.7, (approximately the stall angle) for AOA>alpha0. (deg) +-7.303 alpha2 - Angle of attack at f=0.7, (approximately the stall angle) for AOA1] +0 S2 - Constant in the f curve best-fit for AOA> alpha1; by definition it depends on the airfoil. [ignored if UAMod<>1] +0 S3 - Constant in the f curve best-fit for alpha2<=AOA< alpha0; by definition it depends on the airfoil. [ignored if UAMod<>1] +0 S4 - Constant in the f curve best-fit for AOA< alpha2; by definition it depends on the airfoil. [ignored if UAMod<>1] +1.2824 Cn1 - Critical value of C0n at leading edge separation. It should be extracted from airfoil data at a given Mach and Reynolds number. It can be calculated from the static value of Cn at either the break in the pitching moment or the loss of chord force at the onset of stall. It is close to the condition of maximum lift of the airfoil at low Mach numbers. +-0.48392 Cn2 - As Cn1 for negative AOAs. +"DEFAULT" St_sh - Strouhal's shedding frequency constant. [default = 0.19] +0.032848 Cd0 - 2D drag coefficient value at 0-lift. +0 Cm0 - 2D pitching moment coefficient about 1/4-chord location, at 0-lift, positive if nose up. [If the aerodynamics coefficients table does not include a column for Cm, this needs to be set to 0.0] +0 k0 - Constant in the curve best-fit; = ( [ignored if UAMod<>1] +0 k1 - Constant in the curve best-fit. [ignored if UAMod<>1] +0 k2 - Constant in the curve best-fit. [ignored if UAMod<>1] +0 k3 - Constant in the curve best-fit. [ignored if UAMod<>1] +0 k1_hat - Constant in the expression of Cc due to leading edge vortex effects. [ignored if UAMod<>1] +"DEFAULT" x_cp_bar - Constant in the expression of [ignored if UAMod<>1, default = 0.2] +"DEFAULT" UACutout - Angle of attack above which unsteady aerodynamics are disabled (deg). [Specifying the string "Default" sets UACutout to 45 degrees] +"DEFAULT" filtCutOff - Reduced frequency cut-off for low-pass filtering the AoA input to UA, as well as the 1st and 2nd derivatives (-) [default = 0.5] +!........................................ +! Table of aerodynamics coefficients +23 NumAlf - Number of data lines in the following table +! Alpha Cl Cd +! (deg) (-) (-) +-1.00000000e+01 -4.63300000e-01 1.92100000e-01 +-9.00000000e+00 -4.51700000e-01 1.64500000e-01 +-8.00000000e+00 -4.40200000e-01 1.38500000e-01 +-7.00000000e+00 -4.17800000e-01 1.15500000e-01 +-6.00000000e+00 -3.95500000e-01 9.38000000e-02 +-5.00000000e+00 -2.95400000e-01 7.01000000e-02 +-4.00000000e+00 -1.95300000e-01 4.67000000e-02 +-3.00000000e+00 -9.23000000e-02 3.93000000e-02 +-2.00000000e+00 1.07000000e-02 3.21000000e-02 +-1.00000000e+00 1.10000000e-01 2.80000000e-02 + 0.00000000e+00 2.01100000e-01 2.40000000e-02 + 1.00000000e+00 3.07500000e-01 2.94000000e-02 + 2.00000000e+00 4.36600000e-01 3.87000000e-02 + 3.00000000e+00 6.04500000e-01 3.30000000e-02 + 4.00000000e+00 7.30600000e-01 3.20000000e-02 + 5.00000000e+00 8.25900000e-01 3.14000000e-02 + 6.00000000e+00 9.27700000e-01 3.15000000e-02 + 7.00000000e+00 1.01760000e+00 3.59000000e-02 + 8.00000000e+00 1.10180000e+00 4.31000000e-02 + 9.00000000e+00 1.17740000e+00 4.63000000e-02 + 1.00000000e+01 1.24710000e+00 5.04000000e-02 + 1.10000000e+01 1.30040000e+00 6.35000000e-02 + 1.20000000e+01 1.21610000e+00 1.07300000e-01 +! ------------------------------------------------------------------------------ +! data for table 2 +! ------------------------------------------------------------------------------ +0.06 Re - Reynolds number in millions +0 UserProp - User property (control) setting +True InclUAdata - Is unsteady aerodynamics data included in this table? If TRUE, then include 30 UA coefficients below this line +!........................................ +-2.3715 alpha0 - 0-lift angle of attack, depends on airfoil. +11.3643 alpha1 - Angle of attack at f=0.7, (approximately the stall angle) for AOA>alpha0. (deg) +-6.5882 alpha2 - Angle of attack at f=0.7, (approximately the stall angle) for AOA1] +0 S2 - Constant in the f curve best-fit for AOA> alpha1; by definition it depends on the airfoil. [ignored if UAMod<>1] +0 S3 - Constant in the f curve best-fit for alpha2<=AOA< alpha0; by definition it depends on the airfoil. [ignored if UAMod<>1] +0 S4 - Constant in the f curve best-fit for AOA< alpha2; by definition it depends on the airfoil. [ignored if UAMod<>1] +1.3134 Cn1 - Critical value of C0n at leading edge separation. It should be extracted from airfoil data at a given Mach and Reynolds number. It can be calculated from the static value of Cn at either the break in the pitching moment or the loss of chord force at the onset of stall. It is close to the condition of maximum lift of the airfoil at low Mach numbers. +-0.44413 Cn2 - As Cn1 for negative AOAs. +"DEFAULT" St_sh - Strouhal's shedding frequency constant. [default = 0.19] +0.042829 Cd0 - 2D drag coefficient value at 0-lift. +0 Cm0 - 2D pitching moment coefficient about 1/4-chord location, at 0-lift, positive if nose up. [If the aerodynamics coefficients table does not include a column for Cm, this needs to be set to 0.0] +0 k0 - Constant in the curve best-fit; = ( [ignored if UAMod<>1] +0 k1 - Constant in the curve best-fit. [ignored if UAMod<>1] +0 k2 - Constant in the curve best-fit. [ignored if UAMod<>1] +0 k3 - Constant in the curve best-fit. [ignored if UAMod<>1] +0 k1_hat - Constant in the expression of Cc due to leading edge vortex effects. [ignored if UAMod<>1] +"DEFAULT" x_cp_bar - Constant in the expression of [ignored if UAMod<>1, default = 0.2] +"DEFAULT" UACutout - Angle of attack above which unsteady aerodynamics are disabled (deg). [Specifying the string "Default" sets UACutout to 45 degrees] +"DEFAULT" filtCutOff - Reduced frequency cut-off for low-pass filtering the AoA input to UA, as well as the 1st and 2nd derivatives (-) [default = 0.5] +!........................................ +! Table of aerodynamics coefficients +24 NumAlf - Number of data lines in the following table +! Alpha Cl Cd +! (deg) (-) (-) +-8.00000000e+00 -4.03100000e-01 1.47000000e-01 +-7.00000000e+00 -3.91200000e-01 1.20800000e-01 +-6.00000000e+00 -3.79100000e-01 9.55000000e-02 +-5.00000000e+00 -2.80300000e-01 7.56000000e-02 +-4.00000000e+00 -1.73600000e-01 5.28000000e-02 +-3.00000000e+00 -6.70000000e-02 4.66000000e-02 +-2.00000000e+00 3.96000000e-02 4.06000000e-02 +-1.00000000e+00 1.41300000e-01 4.01000000e-02 + 0.00000000e+00 2.43100000e-01 3.98000000e-02 + 1.00000000e+00 3.61600000e-01 4.11000000e-02 + 2.00000000e+00 5.31200000e-01 4.37000000e-02 + 3.00000000e+00 6.49600000e-01 4.01000000e-02 + 4.00000000e+00 7.59200000e-01 3.84000000e-02 + 5.00000000e+00 8.57200000e-01 3.66000000e-02 + 6.00000000e+00 9.47800000e-01 3.67000000e-02 + 7.00000000e+00 1.03400000e+00 3.90000000e-02 + 8.00000000e+00 1.11910000e+00 4.55000000e-02 + 9.00000000e+00 1.19660000e+00 4.97000000e-02 + 1.00000000e+01 1.27230000e+00 5.86000000e-02 + 1.10000000e+01 1.33120000e+00 7.79000000e-02 + 1.20000000e+01 1.20600000e+00 1.82900000e-01 + 1.30000000e+01 1.20490000e+00 1.96000000e-01 + 1.40000000e+01 1.04570000e+00 2.95400000e-01 + 1.50000000e+01 1.06950000e+00 3.19300000e-01 \ No newline at end of file diff --git a/pyFAST/input_output/tests/example_files/FASTIn_ExtPtfm_SubSef.dat b/pyFAST/input_output/tests/example_files/FASTIn_ExtPtfm_SubSef.dat index 65b8618..7dc9aea 100644 --- a/pyFAST/input_output/tests/example_files/FASTIn_ExtPtfm_SubSef.dat +++ b/pyFAST/input_output/tests/example_files/FASTIn_ExtPtfm_SubSef.dat @@ -1,31 +1,31 @@ !Comment !Comment Flex 5 Format !Dimension: 3 -!Time increment in simulation: 0.05 +!Time increment in simulation: 0.05 !Total simulation time in file: 0.2 -!Mass Matrix -!Dimension: 3 +!Mass Matrix +!Dimension: 3 9.62349663e+05 5.07244708e-11 -4.23103689e-11 5.02697235e-11 9.62255787e+05 -3.00720954e-10 -4.32198636e-11 -3.00720954e-10 9.38302373e+05 !Stiffness Matrix -!Dimension: 3 +!Dimension: 3 8.43408083e+07 1.70093408e+00 -2.98665579e-01 1.70093407e+00 8.43407974e+07 -3.47132372e-01 -2.98666294e-01 -3.47133325e-01 1.96653266e+09 !Damping Matrix -!Dimension: 3 +!Dimension: 3 1.54140226e+05 1.03756979e-03 -1.82186007e-04 1.03756979e-03 1.54130201e+05 -2.11750779e-04 -1.82186444e-04 -2.11751361e-04 1.29971117e+06 !Loading and Wave Elevation !Dimension: 1 time column - 3 force columns - 0.0000 0.00000000E+000 0.00000000E+000 0.00000000E+000 - 0.0500 9.65000686E+001 6.75167835E+001 6.75167835E+001 - 0.1000 3.98574430E+002 2.59332684E+002 2.59332684E+002 - 0.1500 9.20902919E+002 5.57660161E+002 5.57660161E+002 - 0.2000 1.66074968E+003 9.60831149E+002 9.60831149E+002 + 0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00 + 5.00000000e-02 9.65000686e+01 6.75167835e+01 6.75167835e+01 + 1.00000000e-01 3.98574430e+02 2.59332684e+02 2.59332684e+02 + 1.50000000e-01 9.20902919e+02 5.57660161e+02 5.57660161e+02 + 2.00000000e-01 1.66074968e+03 9.60831149e+02 9.60831149e+02 \ No newline at end of file diff --git a/pyFAST/input_output/tests/example_files/FASTIn_HD2.dat b/pyFAST/input_output/tests/example_files/FASTIn_HD2.dat new file mode 100644 index 0000000..b913a27 --- /dev/null +++ b/pyFAST/input_output/tests/example_files/FASTIn_HD2.dat @@ -0,0 +1,269 @@ +------- HydroDyn Input File ---------------------------------------------------- +NREL 5.0 MW offshore baseline floating platform HydroDyn input properties for the OC3 Hywind. +True Echo - Echo the input file data (flag) +---------------------- ENVIRONMENTAL CONDITIONS -------------------------------- + default WtrDens - Water density (kg/m^3) + default WtrDpth - Water depth (meters) + default MSL2SWL - Offset between still-water level and mean sea level (meters) [positive upward; unused when WaveMod = 6; must be zero if PotMod=1 or 2] +---------------------- FLOATING PLATFORM --------------------------------------- [unused with WaveMod=6] + 0 PotMod - Potential-flow model {0: none=no potential flow, 1: frequency-to-time-domain transforms based on WAMIT output, 2: fluid-impulse theory (FIT)} (switch) + 0 ExctnMod - Wave-excitation model {0: no wave-excitation calculation, 1: DFT, 2: state-space} (switch) [only used when PotMod=1; STATE-SPACE REQUIRES *.ssexctn INPUT FILE] + 1 ExctnDisp - Method of computing Wave Excitation {0: use undisplaced position, 1: use displaced position, 2: use low-pass filtered displaced position) [only used when PotMod=1 and ExctnMod>0 and SeaState's WaveMod>0]} (switch) + 10 ExctnCutOff - Cutoff (corner) frequency of the low-pass time-filtered displaced position (Hz) [>0.0] [used only when PotMod=1, ExctnMod>0, and ExctnDisp=2]) [only used when PotMod=1 and ExctnMod>0 and SeaState's WaveMod>0]} (switch) + 0 RdtnMod - Radiation memory-effect model {0: no memory-effect calculation, 1: convolution, 2: state-space} (switch) [only used when PotMod=1; STATE-SPACE REQUIRES *.ss INPUT FILE] + 0 RdtnTMax - Analysis time for wave radiation kernel calculations (sec) [only used when PotMod=1 and RdtnMod=1; determines RdtnDOmega=Pi/RdtnTMax in the cosine transform; MAKE SURE THIS IS LONG ENOUGH FOR THE RADIATION IMPULSE RESPONSE FUNCTIONS TO DECAY TO NEAR-ZERO FOR THE GIVEN PLATFORM!] + 0.01 RdtnDT - Time step for wave radiation kernel calculations (sec) [only used when PotMod=1 and ExctnMod>1 or RdtnMod>0; DT<=RdtnDT<=0.1 recommended; determines RdtnOmegaMax=Pi/RdtnDT in the cosine transform] + 1 NBody - Number of WAMIT bodies to be used (-) [>=1; only used when PotMod=1. If NBodyMod=1, the WAMIT data contains a vector of size 6*NBody x 1 and matrices of size 6*NBody x 6*NBody; if NBodyMod>1, there are NBody sets of WAMIT data each with a vector of size 6 x 1 and matrices of size 6 x 6] + 2 NBodyMod - Body coupling model {1: include coupling terms between each body and NBody in HydroDyn equals NBODY in WAMIT, 2: neglect coupling terms between each body and NBODY=1 with XBODY=0 in WAMIT, 3: Neglect coupling terms between each body and NBODY=1 with XBODY=/0 in WAMIT} (switch) [only used when PotMod=1] + "zeros" PotFile - Root name of potential-flow model data; WAMIT output files containing the linear, nondimensionalized, hydrostatic restoring matrix (.hst), frequency-dependent hydrodynamic added mass matrix and damping matrix (.1), and frequency- and direction-dependent wave excitation force vector per unit wave amplitude (.3) (quoted string) [1 to NBody if NBodyMod>1] [only used when PotMod=1 and ExctnMod>0 or RdtnMod>0] [MAKE SURE THE FREQUENCIES INHERENT IN THESE WAMIT FILES SPAN THE PHYSICALLY-SIGNIFICANT RANGE OF FREQUENCIES FOR THE GIVEN PLATFORM; THEY MUST CONTAIN THE ZERO- AND INFINITE-FREQUENCY LIMITS!] + 1 WAMITULEN - Characteristic body length scale used to redimensionalize WAMIT output (meters) [1 to NBody if NBodyMod>1] [only used when PotMod=1 and ExctnMod=1 or RdtnMod=1] + 0 PtfmRefxt - The xt offset of the body reference point(s) from (0,0,0) (meters) [1 to NBody] [only used when PotMod=1] + 0 PtfmRefyt - The yt offset of the body reference point(s) from (0,0,0) (meters) [1 to NBody] [only used when PotMod=1] + 0 PtfmRefzt - The zt offset of the body reference point(s) from (0,0,0) (meters) [1 to NBody] [only used when PotMod=1. If NBodyMod=2,PtfmRefzt=0.0] + 0 PtfmRefztRot - The rotation about zt of the body reference frame(s) from xt/yt (degrees) [1 to NBody] [only used when PotMod=1] + 0 PtfmVol0 - Displaced volume of water when the body is in its undisplaced position (m^3) [1 to NBody] [only used when PotMod=1; USE THE SAME VALUE COMPUTED BY WAMIT AS OUTPUT IN THE .OUT FILE!] + 0 PtfmCOBxt - The xt offset of the center of buoyancy (COB) from (0,0) (meters) [1 to NBody] [only used when PotMod=1] + 0 PtfmCOByt - The yt offset of the center of buoyancy (COB) from (0,0) (meters) [1 to NBody] [only used when PotMod=1] +---------------------- 2ND-ORDER FLOATING PLATFORM FORCES ---------------------- [unused with WaveMod=0 or 6 or PotMod=0 or 2] + 0 MnDrift - Mean-drift 2nd-order forces computed {0: None; [7, 8, 9, 10, 11, or 12]: WAMIT file to use} [Only one of MnDrift, NewmanApp, or DiffQTF can be non-zero. If NBody>1, MnDrift /=8] + 0 NewmanApp - Mean- and slow-drift 2nd-order forces computed with Newman's approximation {0: None; [7, 8, 9, 10, 11, or 12]: WAMIT file to use} [Only one of MnDrift, NewmanApp, or DiffQTF can be non-zero. If NBody>1, NewmanApp/=8. Used only when WaveDirMod=0] + 0 DiffQTF - Full difference-frequency 2nd-order forces computed with full QTF {0: None; [10, 11, or 12]: WAMIT file to use} [Only one of MnDrift, NewmanApp, or DiffQTF can be non-zero] + 0 SumQTF - Full summation -frequency 2nd-order forces computed with full QTF {0: None; [10, 11, or 12]: WAMIT file to use} +---------------------- PLATFORM ADDITIONAL STIFFNESS AND DAMPING -------------- [unused with PotMod=0 or 2] + 0 AddF0 - Additional preload (N, N-m) [If NBodyMod=1, one size 6*NBody x 1 vector; if NBodyMod>1, NBody size 6 x 1 vectors] + 0 + 0 + 0 + 0 + 0 + 0 0 0 0 0 0 AddCLin - Additional linear stiffness (N/m, N/rad, N-m/m, N-m/rad) [If NBodyMod=1, one size 6*NBody x 6*NBody matrix; if NBodyMod>1, NBody size 6 x 6 matrices] + 0 0 0 0 0 0 + 0 0 0 0 0 0 + 0 0 0 0 0 0 + 0 0 0 0 0 0 + 0 0 0 0 0 98340000 + 100000 0 0 0 0 0 AddBLin - Additional linear damping(N/(m/s), N/(rad/s), N-m/(m/s), N-m/(rad/s)) [If NBodyMod=1, one size 6*NBody x 6*NBody matrix; if NBodyMod>1, NBody size 6 x 6 matrices] + 0 100000 0 0 0 0 + 0 0 130000 0 0 0 + 0 0 0 0 0 0 + 0 0 0 0 0 0 + 0 0 0 0 0 13000000 + 0 0 0 0 0 0 AddBQuad - Additional quadratic drag(N/(m/s)^2, N/(rad/s)^2, N-m(m/s)^2, N-m/(rad/s)^2) [If NBodyMod=1, one size 6*NBody x 6*NBody matrix; if NBodyMod>1, NBody size 6 x 6 matrices] + 0 0 0 0 0 0 + 0 0 0 0 0 0 + 0 0 0 0 0 0 + 0 0 0 0 0 0 + 0 0 0 0 0 0 +---------------------- STRIP THEORY OPTIONS -------------------------------------- + 0 WaveDisp - Method of computing Wave Kinematics {0: use undisplaced position, 1: use displaced position) } (switch) + 0 AMMod - Method of computing distributed added-mass force. (0: Only and always on nodes below SWL at the undisplaced position. 2: Up to the instantaneous free surface) [overwrite to 0 when WaveMod = 0 or 6 or when WaveStMod = 0 in SeaState] +---------------------- AXIAL COEFFICIENTS -------------------------------------- + 2 NAxCoef - Number of axial coefficients (-) +AxCoefID AxCd AxCa AxCp AxFDMod AxVnCOff AxFDLoFSc + (-) (-) (-) (-) (-) (-) (-) + 1 0.00 0.00 0.00 0 0.00 1.00 + 2 0.60 0.00 1.00 0 0.00 1.00 +---------------------- MEMBER JOINTS ------------------------------------------- + 55 NJoints - Number of joints (-) [must be exactly 0 or at least 2] +JointID Jointxi Jointyi Jointzi JointAxID JointOvrlp [JointOvrlp= 0: do nothing at joint, 1: eliminate overlaps by calculating super member] + (-) (m) (m) (m) (-) (switch) + 101 0.0000000 0.000000 16.00000 1 0 + 102 0.0000000 0.000000 14.87000 1 0 + 103 0.0000000 0.000000 -14.03000 1 0 + 104 0.0000000 0.000000 -14.83000 2 0 + 211 2.7600000 0.000000 -13.97000 1 0 + 212 33.3000000 0.000000 -13.97000 1 0 + 213 36.0950000 0.000000 -13.97000 1 0 + 221 -1.3800000 -2.390000 -13.97000 1 0 + 222 -16.6500000 -28.838420 -13.97000 1 0 + 223 -18.0475000 -31.258960 -13.97000 1 0 + 231 -1.3800000 2.390000 -13.97000 1 0 + 232 -16.6500000 28.838420 -13.97000 1 0 + 233 -18.0475000 31.258960 -13.97000 1 0 + 311 3.1700000 0.000000 13.98000 1 0 + 312 4.2777890 0.000000 13.01372 1 0 + 313 30.9250100 0.000000 -10.22968 1 0 + 314 32.0328000 0.000000 -11.19596 1 0 + 321 -1.5850000 -2.745000 13.98000 1 0 + 322 -2.1388944 -3.704373 13.01372 1 0 + 323 -15.4625000 -26.781540 -10.22968 1 0 + 324 -16.0164000 -27.740910 -11.19596 1 0 + 331 -1.5850000 2.745000 13.98000 1 0 + 332 -2.1388944 3.704373 13.01372 1 0 + 333 -15.4625000 26.781540 -10.22968 1 0 + 334 -16.0164000 27.740910 -11.19596 1 0 + 411 32.3960000 -2.410000 -13.97000 1 0 + 412 27.7800800 -5.075000 -13.97000 1 0 + 413 -9.48585500 -26.590500 -13.97000 1 0 + 414 -14.10090000 -29.255000 -13.97000 1 0 + 421 -18.28512000 -26.850530 -13.97000 1 0 + 422 -18.28512000 -21.520530 -13.97000 1 0 + 423 -18.28512000 21.510470 -13.97000 1 0 + 424 -18.28512000 26.839470 -13.97000 1 0 + 431 -14.11088000 29.260530 -13.97000 1 0 + 432 -9.49496300 26.595530 -13.97000 1 0 + 433 27.77098000 5.080029 -13.97000 1 0 + 434 32.38603000 2.415529 -13.97000 1 0 + 511 20.50000000 33.492000 -61.46800 1 0 + 512 20.50000000 25.417000 -61.46800 1 0 + 513 20.50000000 18.721000 -61.46800 1 0 + 514 20.50000000 -18.719000 -61.46800 1 0 + 515 20.50000000 -25.419000 -61.46800 1 0 + 516 20.50000000 -33.489000 -61.46800 1 0 + 521 18.76795000 -34.489000 -61.46800 1 0 + 522 11.77479000 -30.451500 -61.46800 1 0 + 523 5.97588800 -27.103500 -61.46800 1 0 + 524 -26.44810000 -8.383500 -61.46800 1 0 + 525 -32.25047000 -5.033500 -61.46800 1 0 + 526 -39.23930000 -0.998500 -61.46800 1 0 + 531 -39.23930000 1.001500 -61.46800 1 0 + 532 -32.24614000 5.039000 -61.46800 1 0 + 533 -26.44724000 8.387000 -61.46800 1 0 + 534 5.97675400 27.107000 -61.46800 1 0 + 535 11.77912000 30.457000 -61.46800 1 0 + 536 18.76795000 34.492000 -61.46800 1 0 +---------------------- MEMBER CROSS-SECTION PROPERTIES ------------------------- + 12 NPropSets - Number of member property sets (-) +PropSetID PropD PropThck + (-) (m) (m) + 1 4.3 0.06000 + 2 3.462 0.02200 + 3 0.3462 0.00220 + 4 2.125 0.02500 + 5 2.16 0.02000 + 6 2.2 0.03000 + 7 4 0.02200 + 8 2 0.10000 + 9 4.1 0.02800 + 10 4.1 0.02800 + 11 4.1 0.02800 + 13 0.10762 0.05381 +---------------------- SIMPLE HYDRODYNAMIC COEFFICIENTS (model 1) -------------- + SimplCd SimplCdMG SimplCa SimplCaMG SimplCp SimplCpMG SimplAxCd SimplAxCdMG SimplAxCa SimplAxCaMG SimplAxCp SimplAxCpMG + (-) (-) (-) (-) (-) (-) (-) (-) (-) (-) (-) (-) + 0.60 0.60 0.00 0.00 1.00 1.00 0.60 0.60 0.00 0.00 1.00 1.00 +---------------------- DEPTH-BASED HYDRODYNAMIC COEFFICIENTS (model 2) --------- + 0 NCoefDpth - Number of depth-dependent coefficients (-) +Dpth DpthCd DpthCdMG DpthCa DpthCaMG DpthCp DpthCpMG DpthAxCd DpthAxCdMG DpthAxCa DpthAxCaMG DpthAxCp DpthAxCpMG +(m) (-) (-) (-) (-) (-) (-) (-) (-) (-) (-) (-) (-) +---------------------- MEMBER-BASED HYDRODYNAMIC COEFFICIENTS (model 3) -------- + 0 NCoefMembers - Number of member-based coefficients (-) +MemberID MemberCd1 MemberCd2 MemberCdMG1 MemberCdMG2 MemberCa1 MemberCa2 MemberCaMG1 MemberCaMG2 MemberCp1 MemberCp2 MemberCpMG1 MemberCpMG2 MemberAxCd1 MemberAxCd2 MemberAxCdMG1 MemberAxCdMG2 MemberAxCa1 MemberAxCa2 MemberAxCaMG1 MemberAxCaMG2 MemberAxCp1 MemberAxCp2 MemberAxCpMG1 MemberAxCpMG2 + (-) (-) (-) (-) (-) (-) (-) (-) (-) (-) (-) (-) (-) (-) (-) (-) (-) (-) (-) (-) (-) (-) (-) (-) (-) +-------------------- MEMBERS ------------------------------------------------- + 48 NMembers - Number of members (-) +MemberID MJointID1 MJointID2 MPropSetID1 MPropSetID2 MDivSize MCoefMod PropPot [MCoefMod=1: use simple coeff table, 2: use depth-based coeff table, 3: use member-based coeff table] [ PropPot/=0 if member is modeled with potential-flow theory] + (-) (-) (-) (-) (-) (m) (switch) (flag) R. Bergua comments: Dummy beams removed from the hydro. It is supposed that those beams are underneath other beams. + 1 101 102 1 1 5 1 FALSE + 2 102 103 1 1 1 1 FALSE + 3 103 104 1 1 5 1 FALSE + 4 211 212 2 2 5 1 FALSE + 5 212 213 2 3 5 1 FALSE + 6 221 222 2 2 5 1 FALSE + 7 222 223 2 3 5 1 FALSE + 8 231 232 2 2 5 1 FALSE + 9 232 233 2 3 5 1 FALSE + 10 311 312 4 5 5 1 FALSE + 11 312 313 5 5 1 1 FALSE + 12 313 314 5 4 5 1 FALSE + 13 321 322 4 5 5 1 FALSE + 14 322 323 5 5 1 1 FALSE + 15 323 324 5 4 5 1 FALSE + 16 331 332 4 5 5 1 FALSE + 17 332 333 5 5 1 1 FALSE + 18 333 334 5 4 5 1 FALSE + 19 411 412 6 7 5 1 FALSE + 20 412 413 7 7 5 1 FALSE + 21 413 414 7 6 5 1 FALSE + 22 421 422 6 7 5 1 FALSE + 23 422 423 7 7 5 1 FALSE + 24 423 424 7 6 5 1 FALSE + 25 431 432 6 7 5 1 FALSE + 26 432 433 7 7 5 1 FALSE + 27 433 434 7 6 5 1 FALSE + 28 511 512 8 9 5 1 FALSE + 29 512 513 10 10 5 1 FALSE + 30 513 514 11 11 5 1 FALSE + 31 514 515 10 10 5 1 FALSE + 32 515 516 9 8 5 1 FALSE + 33 521 522 8 9 5 1 FALSE + 34 522 523 10 10 5 1 FALSE + 35 523 524 11 11 5 1 FALSE + 36 524 525 10 10 5 1 FALSE + 37 525 526 9 8 5 1 FALSE + 38 531 532 8 9 5 1 FALSE + 39 532 533 10 10 5 1 FALSE + 40 533 534 11 11 5 1 FALSE + 41 534 535 10 10 5 1 FALSE + 42 535 536 9 8 5 1 FALSE + 61 213 511 13 13 5 1 FALSE + 62 213 516 13 13 5 1 FALSE + 63 223 521 13 13 5 1 FALSE + 64 223 526 13 13 5 1 FALSE + 65 233 531 13 13 5 1 FALSE + 66 233 536 13 13 5 1 FALSE +---------------------- FILLED MEMBERS ------------------------------------------ + 0 NFillGroups - Number of filled member groups (-) [If FillDens = DEFAULT, then FillDens = WtrDens; FillFSLoc is related to MSL2SWL] +FillNumM FillMList FillFSLoc FillDens +(-) (-) (m) (kg/m^3) +---------------------- MARINE GROWTH ------------------------------------------- + 0 NMGDepths - Number of marine-growth depths specified (-) +MGDpth MGThck MGDens +(m) (m) (kg/m^3) +---------------------- MEMBER OUTPUT LIST -------------------------------------- + 0 NMOutputs - Number of member outputs (-) [must be < 10] +MemberID NOutLoc NodeLocs [NOutLoc < 10; node locations are normalized distance from the start of the member, and must be >=0 and <= 1] [unused if NMOutputs=0] + (-) (-) (-) +---------------------- JOINT OUTPUT LIST --------------------------------------- + 0 NJOutputs - Number of joint outputs [Must be < 10] + 0 JOutLst - List of JointIDs which are to be output (-)[unused if NJOutputs=0] +---------------------- OUTPUT -------------------------------------------------- +False HDSum - Output a summary file [flag] +False OutAll - Output all user-specified member and joint loads (only at each member end, not interior locations) [flag] + 2 OutSwtch - Output requested channels to: [1=Hydrodyn.out, 2=GlueCode.out, 3=both files] +"ES11.4e2" OutFmt - Output format for numerical results (quoted string) [not checked for validity!] +"A11" OutSFmt - Output format for header strings (quoted string) [not checked for validity!] +---------------------- OUTPUT CHANNELS ----------------------------------------- +HydroFxi +HydroFyi +HydroFzi +HydroMxi +HydroMyi +HydroMzi +B1Surge +B1Sway +B1Heave +B1Roll +B1Pitch +B1Yaw +B1TVxi +B1TVyi +B1TVzi +B1RVxi +B1RVyi +B1RVzi +B1TAxi +B1TAyi +B1TAzi +B1RAxi +B1RAyi +B1RAzi +B1WvsFxi +B1WvsFyi +B1WvsFzi +B1WvsMxi +B1WvsMyi +B1WvsMzi +B1HDSFxi +B1HDSFyi +B1HDSFzi +B1HDSMxi +B1HDSMyi +B1HDSMzi +B1RdtFxi +B1RdtFyi +B1RdtFzi +B1RdtMxi +B1RdtMyi +B1RdtMzi +END of output channels and end of file. (the word "END" must appear in the first 3 columns of this line) diff --git a/pyFAST/input_output/tests/example_files/FASTIn_HD_SeaState.dat b/pyFAST/input_output/tests/example_files/FASTIn_HD_SeaState.dat new file mode 100644 index 0000000..d53f296 --- /dev/null +++ b/pyFAST/input_output/tests/example_files/FASTIn_HD_SeaState.dat @@ -0,0 +1,96 @@ +------- SeaState Input File ---------------------------------------------------- +Updated HydroDyn input file for the TCF project. Full scale model to verify OpenFAST against OrcaFlex for the TetraSpar project. +False Echo - Echo the input file data (flag) +---------------------- ENVIRONMENTAL CONDITIONS -------------------------------- + default WtrDens - Water density (kg/m^3) + default WtrDpth - Water depth (meters) + default MSL2SWL - Offset between still-water level and mean sea level (meters) [positive upward; unused when WaveMod = 6; must be zero if PotMod=1 or 2] +---------------------- SPATIAL DISCRETIZATION --------------------------------------------------- + 30 X_HalfWidth - Half-width of the domain in the X direction (m) [>0, NOTE: X[nX] = nX*dX, where nX = {-NX+1,-NX+2,…,NX-1} and dX = X_HalfWidth/(NX-1)] + 30 Y_HalfWidth - Half-width of the domain in the Y direction (m) [>0, NOTE: Y[nY] = nY*dY, where nY = {-NY+1,-NY+2,…,NY-1} and dY = Y_HalfWidth/(NY-1)] + default Z_Depth - Depth of the domain the Z direction (m) relative to SWL [0 < Z_Depth <= WtrDpth+MSL2SWL; "default": Z_Depth = WtrDpth+MSL2SWL; Z[nZ] = ( COS( nZ*dthetaZ ) – 1 )*Z_Depth, where nZ = {0,1,…NZ-1} and dthetaZ = pi/( 2*(NZ-1) )] + 3 NX - Number of nodes in half of the X-direction domain (-) [>=2] + 3 NY - Number of nodes in half of the Y-direction domain (-) [>=2] + 40 NZ - Number of nodes in the Z direction (-) [>=2] +---------------------- WAVES --------------------------------------------------- + 0 WaveMod - Incident wave kinematics model {0: none=still water, 1: regular (periodic), 1P#: regular with user-specified phase, 2: JONSWAP/Pierson-Moskowitz spectrum (irregular), 3: White noise spectrum (irregular), 4: user-defined spectrum from routine UserWaveSpctrm (irregular), 5: Externally generated wave-elevation time series, 6: Externally generated full wave-kinematics time series [option 6 is invalid for PotMod/=0]} (switch) + 0 WaveStMod - Model for stretching incident wave kinematics to instantaneous free surface {0: none=no stretching, 1: vertical stretching, 2: extrapolation stretching, 3: Wheeler stretching} (switch) [unused when WaveMod=0 or when PotMod/=0] + 20000 WaveTMax - Analysis time for incident wave calculations (sec) [unused when WaveMod=0; determines WaveDOmega=2Pi/WaveTMax in the IFFT] + 0.2 WaveDT - Time step for incident wave calculations (sec) [unused when WaveMod=0; 0.1<=WaveDT<=1.0 recommended; determines WaveOmegaMax=Pi/WaveDT in the IFFT] + 9.41 WaveHs - Significant wave height of incident waves (meters) [used only when WaveMod=1, 2, or 3] + 14.3 WaveTp - Peak-spectral period of incident waves (sec) [used only when WaveMod=1 or 2] +"DEFAULT" WavePkShp - Peak-shape parameter of incident wave spectrum (-) or DEFAULT (string) [used only when WaveMod=2; use 1.0 for Pierson-Moskowitz] + 0.15708 WvLowCOff - Low cut-off frequency or lower frequency limit of the wave spectrum beyond which the wave spectrum is zeroed (rad/s) [unused when WaveMod=0, 1, or 6] + 3.1416 WvHiCOff - High cut-off frequency or upper frequency limit of the wave spectrum beyond which the wave spectrum is zeroed (rad/s) [unused when WaveMod=0, 1, or 6] + 0 WaveDir - Incident wave propagation heading direction (degrees) [unused when WaveMod=0 or 6] + 0 WaveDirMod - Directional spreading function {0: none, 1: COS2S} (-) [only used when WaveMod=2,3, or 4] + 1 WaveDirSpread - Wave direction spreading coefficient ( > 0 ) (-) [only used when WaveMod=2,3, or 4 and WaveDirMod=1] + 1 WaveNDir - Number of wave directions (-) [only used when WaveMod=2,3, or 4 and WaveDirMod=1; odd number only] + 0 WaveDirRange - Range of wave directions (full range: WaveDir +/- 1/2*WaveDirRange) (degrees) [only used when WaveMod=2,3,or 4 and WaveDirMod=1] + 123456789 WaveSeed(1) - First random seed of incident waves [-2147483648 to 2147483647] (-) [unused when WaveMod=0, 5, or 6] + 1011121314 WaveSeed(2) - Second random seed of incident waves [-2147483648 to 2147483647] (-) [unused when WaveMod=0, 5, or 6] +FALSE WaveNDAmp - Flag for normally distributed amplitudes (flag) [only used when WaveMod=2, 3, or 4] +"" WvKinFile - Root name of externally generated wave data file(s) (quoted string) [used only when WaveMod=5 or 6] +---------------------- 2ND-ORDER WAVES ----------------------------------------- [unused with WaveMod=0 or 6] +FALSE WvDiffQTF - Full difference-frequency 2nd-order wave kinematics (flag) +FALSE WvSumQTF - Full summation-frequency 2nd-order wave kinematics (flag) + 0 WvLowCOffD - Low frequency cutoff used in the difference-frequencies (rad/s) [Only used with a difference-frequency method] + 0.1 WvHiCOffD - High frequency cutoff used in the difference-frequencies (rad/s) [Only used with a difference-frequency method] + 0 WvLowCOffS - Low frequency cutoff used in the summation-frequencies (rad/s) [Only used with a summation-frequency method] + 0.1 WvHiCOffS - High frequency cutoff used in the summation-frequencies (rad/s) [Only used with a summation-frequency method] +---------------------- CONSTRAINED WAVES --------------------------------------- + 0 ConstWaveMod - Constrained wave model: 0=none; 1=Constrained wave with specified crest elevation, alpha; 2=Constrained wave with guaranteed peak-to-trough crest height, HCrest (flag) + 1 CrestHmax - Crest height (2*alpha for ConstWaveMod=1 or HCrest for ConstWaveMod=2), must be larger than WaveHs (m) [unused when ConstWaveMod=0] + 60 CrestTime - Time at which the crest appears (s) [unused when ConstWaveMod=0] + 0 CrestXi - X-position of the crest (m) [unused when ConstWaveMod=0] + 0 CrestYi - Y-position of the crest (m) [unused when ConstWaveMod=0] +---------------------- CURRENT ------------------------------------------------- [unused with WaveMod=6] + 0 CurrMod - Current profile model {0: none=no current, 1: standard, 2: user-defined from routine UserCurrent} (switch) + 0 CurrSSV0 - Sub-surface current velocity at still water level (m/s) [used only when CurrMod=1] +"DEFAULT" CurrSSDir - Sub-surface current heading direction (degrees) or DEFAULT (string) [used only when CurrMod=1] + 0 CurrNSRef - Near-surface current reference depth (meters) [used only when CurrMod=1] + 0 CurrNSV0 - Near-surface current velocity at still water level (m/s) [used only when CurrMod=1] + 0 CurrNSDir - Near-surface current heading direction (degrees) [used only when CurrMod=1] + 0 CurrDIV - Depth-independent current velocity (m/s) [used only when CurrMod=1] + 0 CurrDIDir - Depth-independent current heading direction (degrees) [used only when CurrMod=1] +---------------------- MacCamy-Fuchs diffraction model ------------------------- + 0 MCFD - MacCamy-Fuchs member radius (ignored if radius <= 0) [must be 0 when WaveMod 0 or 6] +---------------------- OUTPUT -------------------------------------------------- +False SeaStSum - Output a summary file [flag] + 2 OutSwtch - Output requested channels to: [1=SeaState.out, 2=GlueCode.out, 3=both files] +"ES11.4e2" OutFmt - Output format for numerical results (quoted string) [not checked for validity!] +"A11" OutSFmt - Output format for header strings (quoted string) [not checked for validity!] + 1 NWaveElev - Number of points where the incident wave elevations can be computed (-) [maximum of 9 output locations] + 0 WaveElevxi - List of xi-coordinates for points where the incident wave elevations can be output (meters) [NWaveElev points, separated by commas or white space; usused if NWaveElev = 0] + 0 WaveElevyi - List of yi-coordinates for points where the incident wave elevations can be output (meters) [NWaveElev points, separated by commas or white space; usused if NWaveElev = 0] + 0 NWaveKin - Number of points where the wave kinematics can be output (-) [maximum of 9 output locations] + 0, WaveKinxi - List of xi-coordinates for points where the wave kinematics can be output (meters) [NWaveKin points, separated by commas or white space; usused if NWaveKin = 0] + 0, WaveKinyi - List of yi-coordinates for points where the wave kinematics can be output (meters) [NWaveKin points, separated by commas or white space; usused if NWaveKin = 0] + 0, WaveKinzi - List of zi-coordinates for points where the wave kinematics can be output (meters) [NWaveKin points, separated by commas or white space; usused if NWaveKin = 0] +---------------------- OUTPUT CHANNELS ----------------------------------------- +"Wave1Elev" - Wave elevation at the platform reference point (0, 0) +"HydroFxi" - Hydro force [N] in the X direction. +"HydroFyi" - Hydro force [N] in the Y direction. +"HydroFzi" - Hydro force [N] in the vertical direction (Z). +"HydroMxi" - Hydro moment [N] in the X direction. +"HydroMyi" - Hydro moment [N] in the Y direction. +"HydroMzi" - Hydro moment [N] in the vertical direction (Z). +"PRPSurge" +"PRPSway" +"PRPHeave" +"PRPRoll" +"PRPPitch" +"PRPYaw" +"PRPTVxi" +"PRPTVyi" +"PRPTVzi" +"PRPRVxi" +"PRPRVyi" +"PRPRVzi" +"PRPTAxi" +"PRPTAyi" +"PRPTAzi" +"PRPRAxi" +"PRPRAyi" +"PRPRAzi" +END of output channels and end of file. (the word "END" must appear in the first 3 columns of this line) diff --git a/pyFAST/input_output/tests/example_files/FASTIn_HD_driver.dvr b/pyFAST/input_output/tests/example_files/FASTIn_HD_driver.dvr new file mode 100644 index 0000000..a0e93c8 --- /dev/null +++ b/pyFAST/input_output/tests/example_files/FASTIn_HD_driver.dvr @@ -0,0 +1,27 @@ +------- HydroDyn Driver file ------------------------------------------------- +Compatible with HydroDyn v3.00 +FALSE Echo - Echo the input file data (flag) +---------------------- ENVIRONMENTAL CONDITIONS ------------------------------- +9.80665 Gravity - Gravity (m/s^2) +1025 WtrDens - Water density (kg/m^3) +220 WtrDpth - Water depth (m) +0 MSL2SWL - Offset between still-water level and mean sea level (m) [positive upward] +---------------------- HYDRODYN ----------------------------------------------- +"./HD_NoCa.dat" HDInputFile - Primary HydroDyn input file name (quoted string) +"./hd_driver_NoCa" OutRootName - The name which prefixes all HydroDyn generated files (quoted string) +True Linearize - Flag to enable linearization +1001 NSteps - Number of time steps in the simulations (-) +0.0125 TimeInterval - TimeInterval for the simulation (sec) +---------------------- PRP INPUTS (Platform Reference Point) ------------------ +1 PRPInputsMod - Model for the PRP (principal reference point) inputs {0: all inputs are zero for every timestep, 1: steadystate inputs, 2: read inputs from a file (InputsFile)} (switch) +16 PtfmRefzt - Vertical distance from the ground level [onshore] or MSL [offshore] to the platform reference point (meters) +"OpenFAST_DisplacementTimeseries.dat" PRPInputsFile - Filename for the PRP HydroDyn input InputsMod = 2 (quoted string) +---------------------- PRP STEADY STATE INPUTS ------------------------------- +0.0 0.0 0.0 0.0 0.0 0.0 uPRPInSteady - PRP Steady-state displacements and rotations at the platform reference point (m, rads) +0.0 0.0 0.0 0.0 0.0 0.0 uDotPRPInSteady - PRP Steady-state translational and rotational velocities at the platform reference point (m/s, rads/s) +0.0 0.0 0.0 0.0 0.0 0.0 uDotDotPRPInSteady - PRP Steady-state translational and rotational accelerations at the platform reference point (m/s^2, rads/s^2) +---------------------- Waves multipoint elevation output ---------------------- +FALSE WaveElevSeriesFlag - T/F flag to calculate the wave elevation field (for movies) +5.0 5.0 WaveElevDX WaveElevDY - WaveElevSeries spacing -- WaveElevDX WaveElevDY +3 3 WaveElevNX WaveElevNY - WaveElevSeries points -- WaveElevNX WaveElevNY +END of driver input file diff --git a/pyFAST/input_output/tests/example_files/FLEXBlade000.bla b/pyFAST/input_output/tests/example_files/FLEXBlade000.bla new file mode 100644 index 0000000..7fd8652 --- /dev/null +++ b/pyFAST/input_output/tests/example_files/FLEXBlade000.bla @@ -0,0 +1,32 @@ +Comment +13 r[m] EI_Flp[Nm2] EI_Edg[Nm2] Mass_[kg/m] PhiOut[deg] Out[0/1] PBF[m] PBE[m] +0.0 18110000000 18113600000 715.0 00.0 1 0 0 +3.2 15287400000 19788800000 779.4 13.3 0 0 0 +7.2 5528360000 8063160000 421.9 13.3 0 0 0 +11.2 3949460000 7271660000 439.0 13.2 0 0 0 +15.2 2388650000 4948490000 368.0 11.1 0 0 0 +22.2 1588710000 3995280000 339.1 9.1 0 0 0 +30.2 681300000 2734240000 277.3 6.5 1 0 0 +38.2 238630000 1584100000 210.9 4.4 0 0 0 +46.2 90880000 797810000 146.3 2.5 1 0 0 +54.2 39360000 395120000 95.0 1.0 0 0 0 +57.7 23840000 261710000 69.8 0.3 0 0 0 +59.7 10080000 101630000 51.7 0.1 0 0 0 +61.5 170000 5010000 10.9 0.0 0 0 0 +0.00 !BetaC +0.03 0.03 0.03 0.03 0.03 0.03 0.03 0.03 log decr. For mode 1-4 X C Beta t/c Yac/c +00.0 3.5 13.308 100 0.25000 +03.2 3.751109176 13.308 100 0.23284 +07.2 4.208520941 13.308 90 0.18382 +11.2 4.640859231 13.181 40 0.13481 +15.2 4.6248112 11.072 35 0.12500 +22.2 4.266988438 9.110 30 0.12500 +30.2 3.782642105 6.711 25 0.12500 +38.2 3.30106 4.401 21 0.12500 +46.2 2.82106 2.503 17 0.12500 +54.2 2.34106 0.954 17 0.12500 +57.7 2.0544545 0.319 17 0.12500 +59.7 1.579883655 0.140 17 0.12500 +61.5 0.877604473 0.000 17 0.12500 +data/ProfileFile.pro +61.5 Xtipbr diff --git a/pyFAST/input_output/tests/example_files/FLEXBlade001.bld b/pyFAST/input_output/tests/example_files/FLEXBlade001.bld new file mode 100644 index 0000000..808ab31 --- /dev/null +++ b/pyFAST/input_output/tests/example_files/FLEXBlade001.bld @@ -0,0 +1,32 @@ +#001 Comment +13 r[m] EI_Flp[Nm2] EI_Edg[Nm2] Mass_[kg/m] PhiOut[deg] Out[0/1] PBF[m] PBE[m] +0.0 18110000000 18113600000 715.0 00.0 1 0 0 +3.2 15287400000 19788800000 779.4 13.3 0 0 0 +7.2 5528360000 8063160000 421.9 13.3 0 0 0 +11.2 3949460000 7271660000 439.0 13.2 0 0 0 +15.2 2388650000 4948490000 368.0 11.1 0 0 0 +22.2 1588710000 3995280000 339.1 9.1 0 0 0 +30.2 681300000 2734240000 277.3 6.5 1 0 0 +38.2 238630000 1584100000 210.9 4.4 0 0 0 +46.2 90880000 797810000 146.3 2.5 1 0 0 +54.2 39360000 395120000 95.0 1.0 0 0 0 +57.7 23840000 261710000 69.8 0.3 0 0 0 +59.7 10080000 101630000 51.7 0.1 0 0 0 +61.5 170000 5010000 10.9 0.0 0 0 0 +0.00 !BetaC +0.03 0.03 0.03 0.03 0.03 0.03 0.03 0.03 log decr. For mode 1-4 X C Beta t/c Yac/c AeroTors +00.0 3.5 13.308 100 0.25000 0 +03.2 3.751109176 13.308 100 0.23284 1 +07.2 4.208520941 13.308 90 0.18382 1 +11.2 4.640859231 13.181 40 0.13481 1 +15.2 4.6248112 11.072 35 0.12500 1 +22.2 4.266988438 9.110 30 0.12500 1 +30.2 3.782642105 6.711 25 0.12500 1 +38.2 3.30106 4.401 21 0.12500 1 +46.2 2.82106 2.503 17 0.12500 1 +54.2 2.34106 0.954 17 0.12500 1 +57.7 2.0544545 0.319 17 0.12500 1 +59.7 1.579883655 0.140 17 0.12500 1 +61.5 0.877604473 0.000 17 0.12500 1 +data/ProfileFile.pro +61.5 Xtipbr diff --git a/pyFAST/input_output/tests/example_files/FLEXBlade002.bld b/pyFAST/input_output/tests/example_files/FLEXBlade002.bld new file mode 100644 index 0000000..31b012c --- /dev/null +++ b/pyFAST/input_output/tests/example_files/FLEXBlade002.bld @@ -0,0 +1,56 @@ +#002 Comment +25 r[m] EI_Flp[Nm2] EI_Edg[Nm2] Mass_[kg/m] PhiOut[deg] Out[0/1] PBF[m] PBE[m] GKt[Nm2] +0.0 18110000000 18113600000 715.0 00.0 1 0 0 1.00E+20 +1.2 19424900000 19558600000 814.5 13.3 0 0 0 1.00E+20 +3.2 15287400000 19788800000 779.4 13.3 0 0 0 1.00E+20 +5.2 7229720000 10220600000 474.2 13.3 0 0 0 1.00E+20 +7.2 5528360000 8063160000 421.9 13.3 0 0 0 1.00E+20 +9.2 4936840000 7009180000 420.9 13.3 0 0 0 1.00E+20 +11.2 3949460000 7271660000 439.0 13.2 0 0 0 1.00E+20 +13.2 2933740000 6244530000 401.7 12.2 0 0 0 1.00E+20 +15.2 2388650000 4948490000 368.0 11.1 0 0 0 1.00E+20 +18.2 2050050000 4501400000 357.4 10.2 0 0 0 1.00E+20 +22.2 1588710000 3995280000 339.1 9.1 0 0 0 1.00E+20 +26.2 1102380000 3447140000 310.4 7.9 0 0 0 1.00E+20 +30.2 681300000 2734240000 277.3 6.5 1 0 0 1.00E+20 +34.2 408900000 2334030000 254.5 5.5 0 0 0 1.00E+20 +38.2 238630000 1584100000 210.9 4.4 0 0 0 1.00E+20 +42.2 126010000 1183680000 173.9 3.3 0 0 0 1.00E+20 +46.2 90880000 797810000 146.3 2.5 1 0 0 1.00E+20 +50.2 61050000 518190000 113.0 1.7 0 0 0 1.00E+20 +54.2 39360000 395120000 95.0 1.0 0 0 0 1.00E+20 +56.2 30410000 304730000 76.8 0.6 0 0 0 1.00E+20 +57.7 23840000 261710000 69.8 0.3 0 0 0 1.00E+20 +58.7 16000000 137880000 58.9 0.2 0 0 0 1.00E+20 +59.7 10080000 101630000 51.7 0.1 0 0 0 1.00E+20 +60.7 4600000 64260000 43.9 0.1 0 0 0 1.00E+20 +61.5 170000 5010000 10.9 0.0 0 0 0 1.00E+20 +0.00 !BetaC +0.03 0.03 0.03 0.03 0.03 0.03 0.03 0.03 log decr. For mode 1-4 X C Beta t/c Yac/c AeroTors ProfileSet +00.0 3.5 13.308 100 0.25000 1 1 +01.2 3.522594595 13.308 100 0.24951 1 1 +03.2 3.751109176 13.308 100 0.23284 1 1 +05.2 3.979815059 13.308 100 0.20833 1 1 +07.2 4.208520941 13.308 90 0.18382 1 1 +09.2 4.437226824 13.308 40 0.15931 1 1 +11.2 4.640859231 13.181 40 0.13481 1 1 +13.2 4.6734155 12.192 35 0.12500 1 1 +15.2 4.6248112 11.072 35 0.12500 1 1 +18.2 4.4712112 10.232 35 0.12500 1 1 +22.2 4.266988438 9.110 30 0.12500 1 1 +26.2 4.035273684 7.932 25 0.12500 1 1 +30.2 3.782642105 6.711 25 0.12500 1 1 +34.2 3.54106 5.546 25 0.12500 1 1 +38.2 3.30106 4.401 21 0.12500 1 1 +42.2 3.06106 3.332 21 0.12500 1 1 +46.2 2.82106 2.503 17 0.12500 1 1 +50.2 2.58106 1.730 17 0.12500 1 1 +54.2 2.34106 0.954 17 0.12500 1 1 +56.2 2.2112045 0.574 17 0.12500 1 1 +57.7 2.0544545 0.319 17 0.12500 1 1 +58.7 1.845517824 0.216 17 0.12500 1 1 +59.7 1.579883655 0.140 17 0.12500 1 1 +60.7 1.2087202 0.062 17 0.12500 1 1 +61.5 0.877604473 0.000 17 0.12500 1 1 +data/ProfileFile.pro +61.5 Xtipbr diff --git a/pyFAST/input_output/tests/example_files/FLEXBlade003.bld b/pyFAST/input_output/tests/example_files/FLEXBlade003.bld new file mode 100644 index 0000000..48ca5a6 --- /dev/null +++ b/pyFAST/input_output/tests/example_files/FLEXBlade003.bld @@ -0,0 +1,21 @@ +#003 Comment +5 r[m] EI_Flp[Nm2] EI_Edg[Nm2] GKt[Nm2] Mass[kg/m] Jxx[kg.m] PBF[m] PBE[m] Str.Twist[°] PhiOut[°] Ycog[m] Yshc[m] Out[0/1] + 0.0000 1.8683e+12 1.8682e+12 7.0547e+11 8.8282e+03 1.3967e+05 0.0000 0.0000 0.00 0.00 0.0000 0.0000 1 + 25.0000 1.8683e+12 1.8682e+12 7.0547e+11 8.8282e+03 1.3967e+05 0.0000 0.0000 0.00 0.00 0.0000 0.0000 0 + 50.0000 1.8683e+12 1.8682e+12 7.0547e+11 8.8282e+03 1.3967e+05 0.0000 0.0000 0.00 0.00 0.0000 0.0000 0 + 75.0000 1.8683e+12 1.8682e+12 7.0547e+11 8.8282e+03 1.3967e+05 0.0000 0.0000 0.00 0.00 0.0000 0.0000 0 + 100.0000 1.8683e+12 1.8682e+12 7.0547e+11 8.8282e+03 1.3967e+05 0.0000 0.0000 0.00 0.00 0.0000 0.0000 0 +-90.00 ! If BetaC<0 use section pitch angles; if BetaC>0 Beta=constant (classic formulation) +#LOGD +1.0e-8 1.0e-8 1.0e-8 1.0e-8 1.0e-8 1.0e-8 1.0e-8 1.0e-8 1.0e-8 1.0e-8 ; Flapwise LOGD +1.0e-8 1.0e-8 1.0e-8 1.0e-8 1.0e-8 1.0e-8 1.0e-8 1.0e-8 1.0e-8 1.0e-8 ; Edgewise LOGD +1.0e-8 1.0e-8 1.0e-8 1.0e-8 1.0e-8 1.0e-8 1.0e-8 1.0e-8 1.0e-8 1.0e-8 ; Torsion LOGD +#AERO +; X[m] C[m] Beta[°] t/C[%] Yac/C[-] Pitching Aero.Pro.Set + 0.0000 2.000 0.00 100.00 0.250 1 1 + 25.0000 2.000 0.00 100.00 0.250 1 1 + 50.0000 2.000 0.00 100.00 0.250 1 1 + 75.0000 2.000 0.00 100.00 0.250 1 1 + 100.0000 2.000 0.00 100.00 0.250 1 1 +data/ProfileFile.pro +99999.00 diff --git a/pyFAST/input_output/tests/example_files/FLEXOutBinV0.int b/pyFAST/input_output/tests/example_files/FLEXOutBinV0.int new file mode 100644 index 0000000000000000000000000000000000000000..5df18a5d5b28c9b45ec0b48786925a5fb7d9a306 GIT binary patch literal 11352 zcmbW-hgTC>*gx=VuWJ|EqS(c*t84GQ(UK4#naNBNQmLsBL{~#c5J3eCB8sSp3J6$G z1O-v)HHy8j6}xL&f8Xr;d;fvgJ)EVj#$Ox!wZ&gM{I$oQ_1ORZtE{M)YyBHqcYW48PSvcC(85{1z05Om-i@AZ{Y?9s zFSEsgK}M;b1jpfo`If}M?}p(95^RUlp0bn&nhX!2EHrS^LQLQE`;8=c0h&2#nQk)a zMFAwZ8V#meRHkpb!GR>0?nFqEH7 ztzYg-NKXqBbkL9it?Ng~*QU@QgJC_?*q@NQO-F-P85|&LAR(8Vs)B3{cF?0igdA#G z5aeT+4k>L332QoODl)8sUJoWj)-=S_(?~;pc7!ZxiVKt&wa~kvgbZmSfw{&ANHd&} zZ;gHd?E{jn$3_rR*Ju+EV(fwI^cjU-YAi6eF`A(lqY2sA$Qm~pzCcoYLO6}>jl`fb zk>ERH2^ra#Y1p8@198U_^0~p;@Kfg$M1rqPAf&WmmfoOCg62*lB%xubu0?wvIzE{Y zQ9~!KLOW(X3AUX|h<(F5|A!hsDC!SF-q+vs6KKvrpQjOWw!YZ6!Jh<^V9}qrmU=J0 z7XQmQc4Y?cuinPLizW;jHw&YpzQ})+W+fDZQBqc?)-2X^f!@p^B)qP#meN$A^)>Sd zSzdQh8=%<+9iETTShq>ntZBqOkcEU4)-KV{)eeAM7ZDOr+s?30OX1!#785eLw#?8< zmx@07zJ!oxH4#RM?g>WLs%3;E*Q^dG)s2C6EGNXd=4-$reQSL3&I*k6nxlc|^|zqm zs|d-fb~DX1Tg#G<|QHH7r3&JUVwl;g@CttF(mO13`JxCr-P>p+Ni zRYXvr@hgsT90}=Hm0?l#4FwV-39_tv?|nE*=c(XMB$D z_19pg7Jf3N7%xKSpb3R5Ok)CeK(1Qc(S==sSpjP34fMx_7Xh;Z7eSkJgyfz-5Wov; z4fW6y^5=Pv0RO-eEeYNYoi6AV5EB>*4KWZhqu{#nXy6Jc1&GGUsRrZrz&^N#$Iy?Q{D9{HE>OSqgcx#? z0_O$9LCc_)Y?CS8SOs}Qiflp9Q$ttu**54w*1Yv|46C6m$Ro=T)Tp{5E=z}A9<{Krcrp4;7{nKr-xHC zowWI0B={o49Cp`?)3${|pyu@B8c(e|v=ri|x6-C)Pe25!KE%`B)V9W1^6`vT9XhJ* zqvJzzs3h&D)ljjH6X@)|tF&j_q{cb$&e@97z9iSO|&lqRwu z&lIDJUr&gO`F>^h55vE{;}sGb{P=Hv(LSlrQsjr0Z8E=UK8?@<JHHqSTAdoDCqh#Vew)Mv7{A2d)vh#)S{N8>#h{n_3FJ+ZaeC)c|IYH>r&x3$ve zi?<0q-@wN>+j`SymXAH2U%o5yO6*ggARk2h;O#u*xY+kT7kol73Ve}8?P7g>qkMd! zPt|X)~ zD%<~y@(R=nwZ_J%!~Tnuuc3TYC4WSH^G{J0V3aGD67qNCTFnn-B<4eZ)GzxYQ#9@> zK4#n%R64Ga?=`1YBM}+=PzCjfT&C@#{s8q~fczJcr1e)9BZd^BdJ2ztpc$pU05Nls zizCKqZmG^ft>&PjkI?zYsq&zdS?HJWLciszY-r9*d@sC%-*@G4sPRvXo^WU1Eag$i zYdYEs@9bNroC~#@Mu;Wsvd>3l4O-tZ75OhL*vCdCfksV1Z4>sV&rH>4Ty@?gLZ*kc zczdWKp~VwX$%P&94p!MhRpSY%32o!OQuP9N>oyLxeyGT+kLnC`-<}X{Xo32^G7OT9 zK|L61qs~-vp_iiw85F8f8I?mJpOL6WLn@U^lr8ANmf?h44_T@dD(^$S!w5+WIj7Jm zYoIrF==qSziWp@v^+RY< zZ>&(vt7Wg$g~$PEJrTXl6|w;J0;pdPWGC}Fc{}w*d@{NlA*;>R@_5w(=x0}4nb}D( zL{)?qe7hiGn(GwDlnbHzod_9b<|t<>i*YT^9SP}g9-=y@aDXng#}hNZP|a1`!d3s# z4jI;*r!JJsp@VI(rZfNLwLtz6_t(2MX1{rjcd={}WG47$w((ga>jr)J*QzLQxy%8Q z{KWG!|MZHM#X$AnQO}$GyiUq0p(Wo4>0*AYu9y9W7IOc=tTOY}Uu5f`kzdfC=5o~_ z`4(vBCsYt-O1(fH3w8X6{9t~omddw5ruT@n=FMIa@&xGBTS6SoeZ7y%cR)-FvYPpb zcbz;DYIuziVxHskS-u-u`3lj-e8T&a`~Xz=0yD=v)cd3S7&PrUY9jM?uMhHE=-4x4 zJac=m_ww`5@TY{lG@I1#o5jG zP0G)*8_>L3^mQ1kRLEvP<<*3&2wSOa_Dn>45mX^6hD}ng^Za7Xcol@?gxM%BN?oB% z<+%5-pNgr{e8kp%*HO2IKUSnkY@m}@(bwT^l@}$$q19!C>4fOq>JVI7`U7sBF1O91F#s zA!K({pgcp|4`*3%8u2q~k^Gi81!|Lzj2_)t-bFGRcY8S(BP6;`Hb;^Lg`OhhR`hO} zS~3&8v^ocs+!nEHkE96dm5m|4WwGa3$t>v3No1KV!=)BUHolj6f{-aOZ6u#0)(o`a zI3ZCnkJ#SQRJ6y*#7ZNkgqb280F6I}^;m40$Vs{l*V!QhJsA6eQAug&*%7SmW0#1{ z(s9uB!&u{OO^_r@|3Nm)O2>M7>qF^jX(_b(5PCOms;pKT4@IOAY^H3Izm$rgz=IeC zakcV7X$s_X0ONC;ogz+p7m}qClDW-SA(7gm1<8Jlkob#==~Bw76heaHyD2+J_d@c0 zxaxQc&-OOtn~WKkV5h8*Y{xwW?Iq+~!XrhYWG(JCdJpEq_H;$Iq%Ha-Wj7()w<{F~ zB^RL6yYP&5j8p88=rIQCld%7?w~#vyv|IwOrxYjA&dkYRT_&wgG7=q10cguLDD z;(5rc4O9|Kh-%L-PlMMi$QF9K=eTUSmk$&aLkPQ9An)gu2Hn|$)%D(vif8JZ&=N?H zoUb^e?v55tL}R=pOOz4nrO;rAzpqR=?!RLRQG_(@8>^~RHRF3-A=i{xm9Od$6cveM zDeqOiRMwj4H;BKVr_NQ*hPFo#a%2BlwX3onWCwYq`l-h$t=E|yj=C;&wECCA3t9n* z4%|~UDE31S!_e~wcB+ml>Y+`L=fQQV4T|=-vQg0UgY8rOX_GN!Z3k0H$ zcaTOh%OLBq$H$W-Lzw}P^?P0?>?LPJZ}7c-(1#P}#7-jYTm09a>Ev4RGdc-cfMcz* z?ywtaZ^(L`ky$>h4ZRo&f_h}9vY#lctoN6g-M}_bOQ3RSa85^YF0~Z;2Bqc95hqZ~ zph@Vnai@I58frPDfU-}eik+zyP&zatcd6Kyx`7qLbLf2TQ#OWDK~wOI7UxB>8PpFk z3El{m<&9&nQn64iv?jlbc}fj|M&r5H1-C+9^#%BF&k+C^cb?7c3{5I?V(i| zOCD$DGI~)KLxN91cg~Ds-ikb+VHks=vjZ3}rX92sdT_QQ^NcwuLXN}O6`$+Mq%j`Q z7UTTX9e05!jJfGnST0UyCO~$WxAlc5BsMG`x(Yd5jFX15e?eNz z_iGomp8v2%p%I7$ONt7mO>8!F11c!$B+X^dLfa5SrWNxf@oX{Vi1>1>c)!?>y$1C} z>=}CL73;v(Ko)Bxx-^3w$KHSrAZB&FJeFO~rb7ORXMbI`Wm#4UEk&&RaJf6Xo}B{y zjyUL9(wa?Vzu}I)K$enEOb&YqvLJr0DE*tMWTT)0#Mb=M&&)>F+7ky6cSl}n$4+6J zp{N?9t5B4$p z2Kp_75Zel0RwCYkE1PnRkid#6_JMc~#*14fuDaqR8zR04nUQZNR>rZT#7i*hvQJ>W zQt8KDWy_#P$kX>KommyT7Gt*`a`^PBDQs7^269AxkE-g$o?y6`0a2$Ay{jHF<>+mU z5Yz>et4}e1Gd-YAs2Mg@hcInfADnaDY0Q;sE;E#^K`8Cy|E^KEU3q<`wYLAG{Gh3i?)HbVXH;KHMZaAkM>Ylh-fykEG3K@$K z^=qezszg1}dM)aufpy(P8$}7wgv(f=)J+h*6P<&$l%RL(){3Oe1L$=rW@DXNWMMku zj-1M{-mH%jt!5`e`ByO?>W_*pusmq+b?occUlmOfuZJSau@r!O&m}=5%8+^F(qSE$q61jL@iOY$e!L2=0qo_)OzE##LO7 zJ4(2P`QA8^iDf;Y-goe}scE68D@*Xn1k~5#n*?+elMdP3M~%{CpgJ;Z(Za3=xXz|+ z!f??$YkQ9f+18XPXeUZU#60*IyP8d99%gzTBKz2!LyLfNqnP_W9&EJs`EbEwe!X!xb6FY5|Rcy6z zBlPWGj6usX@o}LigwRS(TNX;9g=?XKSmWhco`@@j%b<)lSO-`l#oZ}u1wXqT>MYAd zWSQyEwf2M@w$!l+)I`XwBSxLY$Tm@mc#5}nugl`hLwh8 zns~Zs9~9pW>sw2axI|>#TN}~?BiXV}!eMOD6UTaDeQUWXdB{-EpS`h1ZIMa+*}YJ4 zAMD#$E=fuIe3`=MHEGI`DvUxTOt$mmT?o+j}`RF01aqH1pX zLv~&q52=1fbDM&XGyP=JG&s*$G=-nu^(D+^YhV72Ew)Yr9{2Dh) zTZxxJPwkOc8aqk*h}T2(IMh9jTP35!IZ)$x>;X2|NM?zjL#rm@`86bq*NTT=HGh5* zVoyW1SR{UlW8@UuT_Dm(jrs1YIM7$a5I1RC`{*5?MybjV%$NR?m){+eI z0O;1Ah)eZ0lJnvxxXwj05i#qRieHMw(4kp`ysOi&10>enkq&b(mg@F1D3WHEr_Hbs*`jtCJzHXcDi)yJ&3YbBBjV0ZKg{wkIHt7 z%Ktm(Xeq|om6f9Lj5YT3@kDiT~p9@j=k6t zLg%oLp;QItd&whe1DlT)0+o2uC0D3bY$QgAg9^{#at76x6=Ez6RwJigj-l=|*7e^D zsNj+xmC3Zi_{44-S$>H_8JT(r?*vFq@h-}S>4+IH%?B&t;(Jt{XfD(OJt!JUb4C8p zAz#GSA~pSiPKWq@*lWI+O~=#CP+NaObQizSQ|Rv4G0TL$6^;=Nr}sk68bUS{dWdZ3 zG1zH)3$-qc7ImWYplB^(@`Vec@6=+vvl*&G&bjbL^oFX2PC=39?U)CY0PlJh>hU)4 zyq`!*t%0sW(FMhHduk4})_|R_f?jlza2!+)MV*sSbGgC`g09dxDEw?Y zp_AYTS{N6A?0dG0aFJj=ln9y6^c4Of_=zj~36V24!jXav5EF>ldir-^Kfy2D(FN#h z{xD%D!A7XB3HP2qM)=)>KuYLsUa~M*;0c|E)Oo$B1;Xo4t01gsb0et-!bv#ia)`pMQ>F;=8<4}q>h4>CgN^KQ-1NysB3o$BUh zeG3%_)gQekWcZMyZsbwgqk#)!Rb1;x}Si~<{|>)wxowsZ;PhglrA=ZSCyodA_$ zzT50+qM~Rq)DPObyNs@+r$Bh?K!)#*q*Lhapywe6`=>@jHuXd=VO7tFbUEkVs76NDHtTQ?p($pnvUEtQBW%AjcYlI zNSnX?jYmIW0*)0TCa>In(nBfi1=S#4-%jxLI4893^W8x#_e+@M(Us~6Jw=>vm+;n| zqHw<8m&gHo;5|EA~8pkT46H#6y)AyPcmS*bj9=7A3LX{8fUj&^;H_3^AVk zgC1Ha#~HIKM#%r^&WAQ3Gk@H|<@4QVLk`H=`?jp%XS-R~5B-qQS8iFt@8Z@9x`W*R zF?tT)lm7(kfcxCys$@Wv9Y2~UgFc~tsftW?oy|+e$}Md%@CBHz2I4 z1}W{Zmk@H*p|#^t=;>fQv5@-?ryRCGVq4SV`F*d63Ip$E}dAE1rY7 z#d)yPZRkm7{1(o9*;(W?79)h;33~$Ooz5vvM(An>)cR(X^F60hXl8rtqnLZT)H+2& zN84g=)O^O}vXe73pbdU;W~MoLPW_?CR@OIg?YM`X?xKaSzc6;qncO6&G)Vcc^%#d2 z<)nsg{;+;8nD^Cj7{0gayY-yuJh@{tbnYuMrMZUJ!wS40(by$TlzvnZ4 zk76F-dTL!O%#N;~@ZQP1&UNUzv(SbQ*84lc+qKS!Iri=y-WQu&^Ljc=M`Vz^#XBjp zkQ?b>-9@#$Aw+J@=McwiM3gnJ@#_?G4^Dui^|t^;FA3RdR=d1)9FLgw=L^ht^JL&a(C-h*ON56Y J6(Ac%{y#QJk9q(A literal 0 HcmV?d00001 diff --git a/pyFAST/input_output/tests/example_files/FLEXOutBinV3.res b/pyFAST/input_output/tests/example_files/FLEXOutBinV3.res new file mode 100644 index 0000000000000000000000000000000000000000..6eb3adf00c07212006a28cf7fe6c2b133b305dd7 GIT binary patch literal 186 zcmeZZU|`?`VjdtC0%BeumIPvbAa+SDPAGx} zftU%(hKT{W3}?@rvE{nH*fKcar@6_c*_I5oSRsR>FGw6nGVI^P(9F;bB!K{%C;;4s BB(VSh literal 0 HcmV?d00001 diff --git a/pyFAST/input_output/tests/example_files/FLEXProfile.pro b/pyFAST/input_output/tests/example_files/FLEXProfile.pro new file mode 100644 index 0000000..ebdd1b7 --- /dev/null +++ b/pyFAST/input_output/tests/example_files/FLEXProfile.pro @@ -0,0 +1,1016 @@ +PROFILE SET 1: Comment +2 +18.00 100.00 +251 +Profile 1, Set 1 - 18% AOA Cl Cd Cm 18% +-1.8000000E+02 -4.0000000E-01 5.0000000E-02 -6.6800000E-02 +-1.7800000E+02 -2.4810000E-01 5.6979005E-02 3.2800000E-03 +-1.7600000E+02 -8.1400000E-02 7.5713730E-02 7.6720000E-02 +-1.7400000E+02 1.0040000E-01 1.0290074E-01 1.5672000E-01 +-1.7200000E+02 2.9600000E-01 1.3523661E-01 2.3360000E-01 +-1.7000000E+02 4.7300000E-01 1.6941791E-01 2.7136000E-01 +-1.6800000E+02 5.2260000E-01 2.0214119E-01 2.6528000E-01 +-1.6600000E+02 5.4130000E-01 2.3010303E-01 2.5376000E-01 +-1.6400000E+02 4.9650000E-01 2.5000000E-01 2.4000000E-01 +-1.6200000E+02 4.6530000E-01 2.6016631E-01 2.3248000E-01 +-1.6000000E+02 4.5000000E-01 2.6548681E-01 2.2912000E-01 +-1.5800000E+02 4.6100000E-01 2.7248397E-01 2.3536000E-01 +-1.5600000E+02 4.9440000E-01 2.8768028E-01 2.5264000E-01 +-1.5400000E+02 5.5046361E-01 3.1759823E-01 2.7873657E-01 +-1.5200000E+02 6.1396608E-01 3.6045801E-01 3.0006065E-01 +-1.5000000E+02 6.7000628E-01 4.0586462E-01 3.2308783E-01 +-1.4800000E+02 7.0770287E-01 4.5295807E-01 3.3980436E-01 +-1.4600000E+02 7.4161481E-01 5.0087833E-01 3.5551030E-01 +-1.4400000E+02 7.6856222E-01 5.4876542E-01 3.6985848E-01 +-1.4200000E+02 7.8869353E-01 5.9575931E-01 3.8288806E-01 +-1.4000000E+02 8.0222023E-01 6.4100000E-01 3.9464363E-01 +-1.3800000E+02 8.0940261E-01 6.8590215E-01 4.0517341E-01 +-1.3600000E+02 8.1053705E-01 7.3120113E-01 4.1452780E-01 +-1.3400000E+02 8.0594512E-01 7.7531136E-01 4.2275813E-01 +-1.3200000E+02 7.9596425E-01 8.1857786E-01 4.2991567E-01 +-1.3000000E+02 7.8094007E-01 8.6060000E-01 4.3605090E-01 +-1.2800000E+02 7.6122019E-01 9.0132006E-01 4.4121290E-01 +-1.2600000E+02 7.3714923E-01 9.4068533E-01 4.4544903E-01 +-1.2400000E+02 7.0906508E-01 9.7821853E-01 4.4880461E-01 +-1.2200000E+02 6.7729608E-01 1.0142228E+00 4.5132287E-01 +-1.2000000E+02 6.4215894E-01 1.0482000E+00 4.5304487E-01 +-1.1800000E+02 6.0395751E-01 1.0802061E+00 4.5400954E-01 +-1.1600000E+02 5.6298196E-01 1.1102554E+00 4.5425378E-01 +-1.1400000E+02 5.1950851E-01 1.1378430E+00 4.5381259E-01 +-1.1200000E+02 4.7379944E-01 1.1634949E+00 4.5271919E-01 +-1.1000000E+02 4.2610350E-01 1.1866000E+00 4.5100519E-01 +-1.0800000E+02 3.7665646E-01 1.2073773E+00 4.4870078E-01 +-1.0600000E+02 3.2568191E-01 1.2259430E+00 4.4583483E-01 +-1.0400000E+02 2.7339216E-01 1.2416746E+00 4.4243506E-01 +-1.0200000E+02 2.1998931E-01 1.2555655E+00 4.3852819E-01 +-1.0000000E+02 1.6566636E-01 1.2665000E+00 4.3414003E-01 +-9.8000000E+01 1.1060839E-01 1.2752980E+00 4.2929558E-01 +-9.6000000E+01 5.4993857E-02 1.2819458E+00 4.2401913E-01 +-9.4000000E+01 -1.0041200E-03 1.2844808E+00 4.1833426E-01 +-9.2000000E+01 -5.7216440E-02 1.2857982E+00 4.1226392E-01 +-9.0000000E+01 -1.1347665E-01 1.2863000E+00 4.0583041E-01 +-8.8000000E+01 -1.6961958E-01 1.2837866E+00 3.9905539E-01 +-8.6000000E+01 -2.2547994E-01 1.2780933E+00 3.9195980E-01 +-8.4000000E+01 -2.8089095E-01 1.2713222E+00 3.8456387E-01 +-8.2000000E+01 -3.3568295E-01 1.2617782E+00 3.7688695E-01 +-8.0000000E+01 -3.8968202E-01 1.2504000E+00 3.6894750E-01 +-7.8000000E+01 -4.4270859E-01 1.2372024E+00 3.6076287E-01 +-7.6000000E+01 -4.9457615E-01 1.2217980E+00 3.5234924E-01 +-7.4000000E+01 -5.4508990E-01 1.2049106E+00 3.4372142E-01 +-7.2000000E+01 -5.9404553E-01 1.1859838E+00 3.3489264E-01 +-7.0000000E+01 -6.4122798E-01 1.1654000E+00 3.2587438E-01 +-6.8000000E+01 -6.8641039E-01 1.1431275E+00 3.1667613E-01 +-6.6000000E+01 -7.2935301E-01 1.1190173E+00 3.0730511E-01 +-6.4000000E+01 -7.6980240E-01 1.0933857E+00 2.9776607E-01 +-6.2000000E+01 -8.0749070E-01 1.0659453E+00 2.8806090E-01 +-6.0000000E+01 -8.4213514E-01 1.0368000E+00 2.7818840E-01 +-5.8000000E+01 -8.7343785E-01 1.0058711E+00 2.6814391E-01 +-5.6000000E+01 -9.0108599E-01 9.7301646E-01 2.5791893E-01 +-5.4000000E+01 -9.2475231E-01 9.3829725E-01 2.4750078E-01 +-5.2000000E+01 -9.4409624E-01 9.0149442E-01 2.3687219E-01 +-5.0000000E+01 -9.5876557E-01 8.6250000E-01 2.2601086E-01 +-4.8000000E+01 -9.6839892E-01 8.2116674E-01 2.1488911E-01 +-4.6000000E+01 -9.7262906E-01 7.7724007E-01 2.0347336E-01 +-4.4000000E+01 -9.7108730E-01 7.3039427E-01 1.9172381E-01 +-4.2000000E+01 -9.6340899E-01 6.8020982E-01 1.7959393E-01 +-4.0000000E+01 -9.5500000E-01 6.2730000E-01 1.6703018E-01 +-3.8000000E+01 -9.4600000E-01 5.7149033E-01 1.5397159E-01 +-3.6000000E+01 -9.3600000E-01 5.1292880E-01 1.4034954E-01 +-3.4000000E+01 -9.2400000E-01 4.5095130E-01 1.2608748E-01 +-3.2000000E+01 -9.1200000E-01 3.8269856E-01 1.1110090E-01 +-3.0000000E+01 -9.0000000E-01 3.2610000E-01 9.4958218E-02 +-2.9000000E+01 -8.8589127E-01 3.0767714E-01 8.6907123E-02 +-2.8000000E+01 -8.6233893E-01 2.9189846E-01 7.8384267E-02 +-2.7000000E+01 -8.3150083E-01 2.7827773E-01 6.9608788E-02 +-2.6000000E+01 -7.9553481E-01 2.6632871E-01 6.0799827E-02 +-2.5000000E+01 -7.5659873E-01 2.5556518E-01 5.2176522E-02 +-2.4000000E+01 -7.1685043E-01 2.4550091E-01 4.3958012E-02 +-2.3000000E+01 -6.7844777E-01 2.3564965E-01 3.6363437E-02 +-2.2000000E+01 -6.4354858E-01 2.2552517E-01 2.9611936E-02 +-2.1000000E+01 -6.1431072E-01 2.1464125E-01 2.3922648E-02 +-2.0000000E+01 -5.9289204E-01 2.0251165E-01 1.9514712E-02 +-1.9500000E+01 -5.8699378E-01 1.9551304E-01 1.8062448E-02 +-1.9000000E+01 -5.8109552E-01 1.8851442E-01 1.6610183E-02 +-1.8500000E+01 -5.7836086E-01 1.8084652E-01 1.5824749E-02 +-1.8000000E+01 -5.7562619E-01 1.7317861E-01 1.5039315E-02 +-1.7500000E+01 -5.7693243E-01 1.6577103E-01 1.4336321E-02 +-1.7000000E+01 -5.7823867E-01 1.5836345E-01 1.3633327E-02 +-1.6500000E+01 -5.9853289E-01 1.4940440E-01 1.0950506E-02 +-1.6000000E+01 -6.1882711E-01 1.4044535E-01 8.2676848E-03 +-1.5500000E+01 -6.7014193E-01 1.2253995E-01 -2.5428471E-03 +-1.5000000E+01 -7.2145675E-01 1.0463454E-01 -1.3353379E-02 +-1.4500000E+01 -8.0837459E-01 7.2768129E-02 -3.3394616E-02 +-1.4000000E+01 -8.9529242E-01 4.0901718E-02 -5.3435853E-02 +-1.3500000E+01 -8.8699667E-01 3.3187649E-02 -7.4705994E-02 +-1.3000000E+01 -8.7870091E-01 2.5473580E-02 -9.5976134E-02 +-1.2500000E+01 -8.2645570E-01 2.1687489E-02 -1.0698332E-01 +-1.2000000E+01 -7.7421048E-01 1.7901397E-02 -1.1799051E-01 +-1.1500000E+01 -7.1646130E-01 1.6084101E-02 -1.2186851E-01 +-1.1000000E+01 -6.5871211E-01 1.4266805E-02 -1.2574650E-01 +-1.0500000E+01 -5.9830006E-01 1.3334879E-02 -1.2769876E-01 +-1.0000000E+01 -5.3788801E-01 1.2402953E-02 -1.2965102E-01 +-9.5000000E+00 -4.7704887E-01 1.1743114E-02 -1.3119547E-01 +-9.0000000E+00 -4.1620973E-01 1.1083275E-02 -1.3273992E-01 +-8.5000000E+00 -3.5594463E-01 1.0575386E-02 -1.3399676E-01 +-8.0000000E+00 -2.9567953E-01 1.0067496E-02 -1.3525360E-01 +-7.5000000E+00 -2.3622463E-01 9.6788175E-03 -1.3633601E-01 +-7.0000000E+00 -1.7676972E-01 9.2901389E-03 -1.3741841E-01 +-6.5000000E+00 -1.1846463E-01 9.0002149E-03 -1.3846117E-01 +-6.0000000E+00 -6.0159535E-02 8.7102909E-03 -1.3950392E-01 +-5.5000000E+00 -6.4439750E-04 8.4819003E-03 -1.4044771E-01 +-5.0000000E+00 5.8870740E-02 8.2535096E-03 -1.4139150E-01 +-4.5000000E+00 1.1896666E-01 8.0224761E-03 -1.4230337E-01 +-4.0000000E+00 1.7906257E-01 7.7914425E-03 -1.4321523E-01 +-3.5000000E+00 2.3913166E-01 7.6165617E-03 -1.4411139E-01 +-3.0000000E+00 2.9920074E-01 7.4416808E-03 -1.4500754E-01 +-2.5000000E+00 3.5895569E-01 7.3343867E-03 -1.4590797E-01 +-2.0000000E+00 4.1871064E-01 7.2270926E-03 -1.4680839E-01 +-1.5000000E+00 4.7855629E-01 7.1228084E-03 -1.4766048E-01 +-1.0000000E+00 5.3840194E-01 7.0185241E-03 -1.4851256E-01 +-5.0000000E-01 5.9858880E-01 6.8795762E-03 -1.4927122E-01 +0.0000000E+00 6.5877566E-01 6.7406282E-03 -1.5002988E-01 +5.0000000E-01 7.1941267E-01 6.7403141E-03 -1.5074808E-01 +1.0000000E+00 7.8004967E-01 6.7400000E-03 -1.5146628E-01 +1.5000000E+00 8.3816844E-01 6.8947275E-03 -1.5160596E-01 +2.0000000E+00 8.9628720E-01 7.0494550E-03 -1.5174563E-01 +2.5000000E+00 9.5225775E-01 7.2094007E-03 -1.5192498E-01 +3.0000000E+00 1.0082283E+00 7.3693463E-03 -1.5210433E-01 +3.5000000E+00 1.0649419E+00 7.5459198E-03 -1.5197914E-01 +4.0000000E+00 1.1216555E+00 7.7224932E-03 -1.5185395E-01 +4.5000000E+00 1.1790484E+00 7.9887797E-03 -1.5166838E-01 +5.0000000E+00 1.2364412E+00 8.2550661E-03 -1.5148281E-01 +5.5000000E+00 1.2911315E+00 8.5451465E-03 -1.5105930E-01 +6.0000000E+00 1.3458217E+00 8.8352269E-03 -1.5063578E-01 +6.5000000E+00 1.3987349E+00 9.1548924E-03 -1.4999056E-01 +7.0000000E+00 1.4516480E+00 9.4745578E-03 -1.4934533E-01 +7.5000000E+00 1.5024387E+00 9.8680894E-03 -1.4845598E-01 +8.0000000E+00 1.5532294E+00 1.0261621E-02 -1.4756662E-01 +8.5000000E+00 1.6006183E+00 1.0795751E-02 -1.4619628E-01 +9.0000000E+00 1.6480072E+00 1.1329880E-02 -1.4482594E-01 +9.5000000E+00 1.6895051E+00 1.2095909E-02 -1.4312977E-01 +1.0000000E+01 1.7310029E+00 1.2861938E-02 -1.4143360E-01 +1.0500000E+01 1.7523811E+00 1.3709787E-02 -1.3896483E-01 +1.1000000E+01 1.7737592E+00 1.4557635E-02 -1.3649605E-01 +1.1500000E+01 1.7559954E+00 1.8864974E-02 -1.3390148E-01 +1.2000000E+01 1.7382316E+00 2.3172313E-02 -1.3130690E-01 +1.2500000E+01 1.6329848E+00 3.8673912E-02 -1.3237228E-01 +1.3000000E+01 1.5277380E+00 5.4175511E-02 -1.3343766E-01 +1.3500000E+01 1.4996226E+00 7.2910940E-02 -1.3087134E-01 +1.4000000E+01 1.4715072E+00 9.1646369E-02 -1.2830501E-01 +1.4500000E+01 1.4519308E+00 9.8651400E-02 -1.2714268E-01 +1.5000000E+01 1.4323544E+00 1.0565643E-01 -1.2598034E-01 +1.5500000E+01 1.4136528E+00 1.1355748E-01 -1.2547756E-01 +1.6000000E+01 1.3949511E+00 1.2145852E-01 -1.2497478E-01 +1.6500000E+01 1.3771778E+00 1.3149642E-01 -1.2630785E-01 +1.7000000E+01 1.3594044E+00 1.4153432E-01 -1.2764091E-01 +1.7500000E+01 1.3426130E+00 1.5135984E-01 -1.2942521E-01 +1.8000000E+01 1.3258215E+00 1.6118536E-01 -1.3120950E-01 +1.8500000E+01 1.3100655E+00 1.7163450E-01 -1.3281644E-01 +1.9000000E+01 1.2943094E+00 1.8208364E-01 -1.3442338E-01 +1.9500000E+01 1.2796424E+00 1.9292170E-01 -1.3578460E-01 +2.0000000E+01 1.2649754E+00 2.0375975E-01 -1.3714581E-01 +2.1000000E+01 1.2379265E+00 2.2594778E-01 -1.3958378E-01 +2.2000000E+01 1.2132700E+00 2.4860967E-01 -1.4182994E-01 +2.3000000E+01 1.1911130E+00 2.7170740E-01 -1.4397698E-01 +2.4000000E+01 1.1715627E+00 2.9520294E-01 -1.4611757E-01 +2.5000000E+01 1.1547261E+00 3.1905823E-01 -1.4834437E-01 +2.6000000E+01 1.1407104E+00 3.4323527E-01 -1.5075007E-01 +2.7000000E+01 1.1296228E+00 3.6769599E-01 -1.5342733E-01 +2.8000000E+01 1.1215705E+00 3.9240238E-01 -1.5646883E-01 +2.9000000E+01 1.1166605E+00 4.1731639E-01 -1.5996725E-01 +3.0000000E+01 1.1150000E+00 4.4240000E-01 -1.6401525E-01 +3.2000000E+01 1.1200000E+00 4.9415055E-01 -1.7804703E-01 +3.4000000E+01 1.1250000E+00 5.4628325E-01 -1.9127586E-01 +3.6000000E+01 1.1302000E+00 5.9675108E-01 -2.0375902E-01 +3.8000000E+01 1.1400000E+00 6.4593756E-01 -2.1555093E-01 +4.0000000E+01 1.1452397E+00 6.9380000E-01 -2.2670345E-01 +4.2000000E+01 1.1448578E+00 7.4031368E-01 -2.3726624E-01 +4.4000000E+01 1.1381346E+00 7.8551731E-01 -2.4686502E-01 +4.6000000E+01 1.1257306E+00 8.2929407E-01 -2.5570774E-01 +4.8000000E+01 1.1082083E+00 8.7180269E-01 -2.6428767E-01 +5.0000000E+01 1.0860505E+00 9.1270000E-01 -2.7267575E-01 +5.2000000E+01 1.0596750E+00 9.5199445E-01 -2.8092289E-01 +5.4000000E+01 1.0294483E+00 9.8969637E-01 -2.8906318E-01 +5.6000000E+01 9.9569550E-01 1.0254973E+00 -2.9711688E-01 +5.8000000E+01 9.5871014E-01 1.0596647E+00 -3.0509292E-01 +6.0000000E+01 9.1876081E-01 1.0919000E+00 -3.1299114E-01 +6.2000000E+01 8.7609702E-01 1.1222910E+00 -3.2080427E-01 +6.4000000E+01 8.3095338E-01 1.1508772E+00 -3.2851956E-01 +6.6000000E+01 7.8355266E-01 1.1773280E+00 -3.3612028E-01 +6.8000000E+01 7.3410779E-01 1.2020466E+00 -3.4358689E-01 +7.0000000E+01 6.8282291E-01 1.2246000E+00 -3.5089813E-01 +7.2000000E+01 6.2989366E-01 1.2451519E+00 -3.5803189E-01 +7.4000000E+01 5.7550684E-01 1.2638003E+00 -3.6496593E-01 +7.6000000E+01 5.1983937E-01 1.2801051E+00 -3.7167852E-01 +7.8000000E+01 4.6305689E-01 1.2947564E+00 -3.7814897E-01 +8.0000000E+01 4.0531195E-01 1.3070000E+00 -3.8435805E-01 +8.2000000E+01 3.4674178E-01 1.3173181E+00 -3.9028841E-01 +8.4000000E+01 2.8746589E-01 1.3257459E+00 -3.9592486E-01 +8.6000000E+01 2.2758350E-01 1.3311894E+00 -4.0125471E-01 +8.8000000E+01 1.6717078E-01 1.3354616E+00 -4.0626801E-01 +9.0000000E+01 1.0627813E-01 1.3373000E+00 -4.1095779E-01 +9.2000000E+01 4.3021145E-02 1.3363173E+00 -4.1532382E-01 +9.4000000E+01 -1.9038670E-02 1.3339005E+00 -4.1936326E-01 +9.6000000E+01 -8.0928289E-02 1.3302638E+00 -4.2307564E-01 +9.8000000E+01 -1.4244199E-01 1.3232023E+00 -4.2646197E-01 +1.0000000E+02 -2.0337687E-01 1.3142000E+00 -4.2952454E-01 +1.0200000E+02 -2.6353485E-01 1.3032495E+00 -4.3226653E-01 +1.0400000E+02 -3.2272425E-01 1.2897213E+00 -4.3469173E-01 +1.0600000E+02 -3.8076098E-01 1.2744930E+00 -4.3680411E-01 +1.0800000E+02 -4.3746916E-01 1.2567449E+00 -4.3860738E-01 +1.1000000E+02 -4.9268116E-01 1.2370000E+00 -4.4010442E-01 +1.1200000E+02 -5.4623685E-01 1.2151664E+00 -4.4129669E-01 +1.1400000E+02 -5.9798209E-01 1.1910326E+00 -4.4218347E-01 +1.1600000E+02 -6.4776620E-01 1.1650608E+00 -4.4276098E-01 +1.1800000E+02 -6.9543840E-01 1.1367906E+00 -4.4302140E-01 +1.2000000E+02 -7.4080000E-01 1.1066000E+00 -4.4296000E-01 +1.2200000E+02 -7.8380000E-01 1.0744324E+00 -4.4256000E-01 +1.2400000E+02 -8.2420000E-01 1.0402056E+00 -4.4176000E-01 +1.2600000E+02 -8.6170000E-01 1.0042975E+00 -4.4056000E-01 +1.2800000E+02 -8.9610000E-01 9.6640530E-01 -4.3880000E-01 +1.3000000E+02 -9.2710000E-01 9.2690000E-01 -4.3664000E-01 +1.3200000E+02 -9.5440000E-01 8.8578087E-01 -4.3376000E-01 +1.3400000E+02 -9.7740000E-01 8.4305973E-01 -4.3024000E-01 +1.3600000E+02 -9.8970000E-01 7.9909989E-01 -4.2472000E-01 +1.3800000E+02 -1.0021000E+00 7.5363717E-01 -4.1928000E-01 +1.4000000E+02 -1.0144000E+00 7.0750000E-01 -4.1376000E-01 +1.4200000E+02 -9.9520000E-01 6.5965806E-01 -4.0272000E-01 +1.4400000E+02 -9.7590000E-01 6.0912117E-01 -3.9168000E-01 +1.4600000E+02 -9.5670000E-01 5.5696009E-01 -3.8064000E-01 +1.4800000E+02 -9.3100000E-01 5.0424559E-01 -3.6680000E-01 +1.5000000E+02 -8.9100000E-01 4.5204845E-01 -3.4632000E-01 +1.5200000E+02 -8.5090000E-01 4.0143943E-01 -3.2584000E-01 +1.5400000E+02 -8.1090000E-01 3.5348931E-01 -3.0536000E-01 +1.5600000E+02 -7.5770000E-01 3.1504233E-01 -2.7920000E-01 +1.5800000E+02 -7.1650000E-01 2.9154894E-01 -2.5912000E-01 +1.6000000E+02 -7.0000000E-01 2.7693012E-01 -2.5112000E-01 +1.6200000E+02 -7.1300000E-01 2.6510681E-01 -2.5696000E-01 +1.6400000E+02 -7.4870000E-01 2.5000000E-01 -2.7312000E-01 +1.6600000E+02 -7.8613407E-01 2.2703171E-01 -2.9784000E-01 +1.6800000E+02 -8.2227926E-01 1.9762822E-01 -3.2912000E-01 +1.7000000E+02 -8.5000000E-01 1.6471690E-01 -3.6832000E-01 +1.7200000E+02 -8.5228250E-01 1.3122509E-01 -3.3616000E-01 +1.7400000E+02 -7.7260000E-01 1.0008014E-01 -2.7080000E-01 +1.7600000E+02 -6.2980000E-01 7.4209408E-02 -1.9872000E-01 +1.7800000E+02 -5.1490000E-01 5.6540244E-02 -1.3416000E-01 +1.8000000E+02 -4.0000000E-01 5.0000000E-02 -6.9520000E-02 +Profile 2, Set 1 - 100% AOA Cl Cd Cm 100% +-1.8000000E+02 -1.0000000E-03 6.0000000E-01 0.0000000E+00 +-1.7800000E+02 -9.8888889E-04 6.0000000E-01 0.0000000E+00 +-1.7600000E+02 -9.7777778E-04 6.0000000E-01 0.0000000E+00 +-1.7400000E+02 -9.6666667E-04 6.0000000E-01 0.0000000E+00 +-1.7200000E+02 -9.5555556E-04 6.0000000E-01 0.0000000E+00 +-1.7000000E+02 -9.4444444E-04 6.0000000E-01 0.0000000E+00 +-1.6800000E+02 -9.3333333E-04 6.0000000E-01 0.0000000E+00 +-1.6600000E+02 -9.2222222E-04 6.0000000E-01 0.0000000E+00 +-1.6400000E+02 -9.1111111E-04 6.0000000E-01 0.0000000E+00 +-1.6200000E+02 -9.0000000E-04 6.0000000E-01 0.0000000E+00 +-1.6000000E+02 -8.8888889E-04 6.0000000E-01 0.0000000E+00 +-1.5800000E+02 -8.7777778E-04 6.0000000E-01 0.0000000E+00 +-1.5600000E+02 -8.6666667E-04 6.0000000E-01 0.0000000E+00 +-1.5400000E+02 -8.5555556E-04 6.0000000E-01 0.0000000E+00 +-1.5200000E+02 -8.4444444E-04 6.0000000E-01 0.0000000E+00 +-1.5000000E+02 -8.3333333E-04 6.0000000E-01 0.0000000E+00 +-1.4800000E+02 -8.2222222E-04 6.0000000E-01 0.0000000E+00 +-1.4600000E+02 -8.1111111E-04 6.0000000E-01 0.0000000E+00 +-1.4400000E+02 -8.0000000E-04 6.0000000E-01 0.0000000E+00 +-1.4200000E+02 -7.8888889E-04 6.0000000E-01 0.0000000E+00 +-1.4000000E+02 -7.7777778E-04 6.0000000E-01 0.0000000E+00 +-1.3800000E+02 -7.6666667E-04 6.0000000E-01 0.0000000E+00 +-1.3600000E+02 -7.5555556E-04 6.0000000E-01 0.0000000E+00 +-1.3400000E+02 -7.4444444E-04 6.0000000E-01 0.0000000E+00 +-1.3200000E+02 -7.3333333E-04 6.0000000E-01 0.0000000E+00 +-1.3000000E+02 -7.2222222E-04 6.0000000E-01 0.0000000E+00 +-1.2800000E+02 -7.1111111E-04 6.0000000E-01 0.0000000E+00 +-1.2600000E+02 -7.0000000E-04 6.0000000E-01 0.0000000E+00 +-1.2400000E+02 -6.8888889E-04 6.0000000E-01 0.0000000E+00 +-1.2200000E+02 -6.7777778E-04 6.0000000E-01 0.0000000E+00 +-1.2000000E+02 -6.6666667E-04 6.0000000E-01 0.0000000E+00 +-1.1800000E+02 -6.5555556E-04 6.0000000E-01 0.0000000E+00 +-1.1600000E+02 -6.4444444E-04 6.0000000E-01 0.0000000E+00 +-1.1400000E+02 -6.3333333E-04 6.0000000E-01 0.0000000E+00 +-1.1200000E+02 -6.2222222E-04 6.0000000E-01 0.0000000E+00 +-1.1000000E+02 -6.1111111E-04 6.0000000E-01 0.0000000E+00 +-1.0800000E+02 -6.0000000E-04 6.0000000E-01 0.0000000E+00 +-1.0600000E+02 -5.8888889E-04 6.0000000E-01 0.0000000E+00 +-1.0400000E+02 -5.7777778E-04 6.0000000E-01 0.0000000E+00 +-1.0200000E+02 -5.6666667E-04 6.0000000E-01 0.0000000E+00 +-1.0000000E+02 -5.5555556E-04 6.0000000E-01 0.0000000E+00 +-9.8000000E+01 -5.4444444E-04 6.0000000E-01 0.0000000E+00 +-9.6000000E+01 -5.3333333E-04 6.0000000E-01 0.0000000E+00 +-9.4000000E+01 -5.2222222E-04 6.0000000E-01 0.0000000E+00 +-9.2000000E+01 -5.1111111E-04 6.0000000E-01 0.0000000E+00 +-9.0000000E+01 -5.0000000E-04 6.0000000E-01 0.0000000E+00 +-8.8000000E+01 -4.8888889E-04 6.0000000E-01 0.0000000E+00 +-8.6000000E+01 -4.7777778E-04 6.0000000E-01 0.0000000E+00 +-8.4000000E+01 -4.6666667E-04 6.0000000E-01 0.0000000E+00 +-8.2000000E+01 -4.5555556E-04 6.0000000E-01 0.0000000E+00 +-8.0000000E+01 -4.4444444E-04 6.0000000E-01 0.0000000E+00 +-7.8000000E+01 -4.3333333E-04 6.0000000E-01 0.0000000E+00 +-7.6000000E+01 -4.2222222E-04 6.0000000E-01 0.0000000E+00 +-7.4000000E+01 -4.1111111E-04 6.0000000E-01 0.0000000E+00 +-7.2000000E+01 -4.0000000E-04 6.0000000E-01 0.0000000E+00 +-7.0000000E+01 -3.8888889E-04 6.0000000E-01 0.0000000E+00 +-6.8000000E+01 -3.7777778E-04 6.0000000E-01 0.0000000E+00 +-6.6000000E+01 -3.6666667E-04 6.0000000E-01 0.0000000E+00 +-6.4000000E+01 -3.5555556E-04 6.0000000E-01 0.0000000E+00 +-6.2000000E+01 -3.4444444E-04 6.0000000E-01 0.0000000E+00 +-6.0000000E+01 -3.3333333E-04 6.0000000E-01 0.0000000E+00 +-5.8000000E+01 -3.2222222E-04 6.0000000E-01 0.0000000E+00 +-5.6000000E+01 -3.1111111E-04 6.0000000E-01 0.0000000E+00 +-5.4000000E+01 -3.0000000E-04 6.0000000E-01 0.0000000E+00 +-5.2000000E+01 -2.8888889E-04 6.0000000E-01 0.0000000E+00 +-5.0000000E+01 -2.7777778E-04 6.0000000E-01 0.0000000E+00 +-4.8000000E+01 -2.6666667E-04 6.0000000E-01 0.0000000E+00 +-4.6000000E+01 -2.5555556E-04 6.0000000E-01 0.0000000E+00 +-4.4000000E+01 -2.4444444E-04 6.0000000E-01 0.0000000E+00 +-4.2000000E+01 -2.3333333E-04 6.0000000E-01 0.0000000E+00 +-4.0000000E+01 -2.2222222E-04 6.0000000E-01 0.0000000E+00 +-3.8000000E+01 -2.1111111E-04 6.0000000E-01 0.0000000E+00 +-3.6000000E+01 -2.0000000E-04 6.0000000E-01 0.0000000E+00 +-3.4000000E+01 -1.8888889E-04 6.0000000E-01 0.0000000E+00 +-3.2000000E+01 -1.7777778E-04 6.0000000E-01 0.0000000E+00 +-3.0000000E+01 -1.6666667E-04 6.0000000E-01 0.0000000E+00 +-2.9000000E+01 -1.6111111E-04 6.0000000E-01 0.0000000E+00 +-2.8000000E+01 -1.5555556E-04 6.0000000E-01 0.0000000E+00 +-2.7000000E+01 -1.5000000E-04 6.0000000E-01 0.0000000E+00 +-2.6000000E+01 -1.4444444E-04 6.0000000E-01 0.0000000E+00 +-2.5000000E+01 -1.3888889E-04 6.0000000E-01 0.0000000E+00 +-2.4000000E+01 -1.3333333E-04 6.0000000E-01 0.0000000E+00 +-2.3000000E+01 -1.2777778E-04 6.0000000E-01 0.0000000E+00 +-2.2000000E+01 -1.2222222E-04 6.0000000E-01 0.0000000E+00 +-2.1000000E+01 -1.1666667E-04 6.0000000E-01 0.0000000E+00 +-2.0000000E+01 -1.1111111E-04 6.0000000E-01 0.0000000E+00 +-1.9500000E+01 -1.0833333E-04 6.0000000E-01 0.0000000E+00 +-1.9000000E+01 -1.0555556E-04 6.0000000E-01 0.0000000E+00 +-1.8500000E+01 -1.0277778E-04 6.0000000E-01 0.0000000E+00 +-1.8000000E+01 -1.0000000E-04 6.0000000E-01 0.0000000E+00 +-1.7500000E+01 -9.7222222E-05 6.0000000E-01 0.0000000E+00 +-1.7000000E+01 -9.4444444E-05 6.0000000E-01 0.0000000E+00 +-1.6500000E+01 -9.1666667E-05 6.0000000E-01 0.0000000E+00 +-1.6000000E+01 -8.8888889E-05 6.0000000E-01 0.0000000E+00 +-1.5500000E+01 -8.6111111E-05 6.0000000E-01 0.0000000E+00 +-1.5000000E+01 -8.3333333E-05 6.0000000E-01 0.0000000E+00 +-1.4500000E+01 -8.0555556E-05 6.0000000E-01 0.0000000E+00 +-1.4000000E+01 -7.7777778E-05 6.0000000E-01 0.0000000E+00 +-1.3500000E+01 -7.5000000E-05 6.0000000E-01 0.0000000E+00 +-1.3000000E+01 -7.2222222E-05 6.0000000E-01 0.0000000E+00 +-1.2500000E+01 -6.9444444E-05 6.0000000E-01 0.0000000E+00 +-1.2000000E+01 -6.6666667E-05 6.0000000E-01 0.0000000E+00 +-1.1500000E+01 -6.3888889E-05 6.0000000E-01 0.0000000E+00 +-1.1000000E+01 -6.1111111E-05 6.0000000E-01 0.0000000E+00 +-1.0500000E+01 -5.8333333E-05 6.0000000E-01 0.0000000E+00 +-1.0000000E+01 -5.5555556E-05 6.0000000E-01 0.0000000E+00 +-9.5000000E+00 -5.2777778E-05 6.0000000E-01 0.0000000E+00 +-9.0000000E+00 -5.0000000E-05 6.0000000E-01 0.0000000E+00 +-8.5000000E+00 -4.7222222E-05 6.0000000E-01 0.0000000E+00 +-8.0000000E+00 -4.4444444E-05 6.0000000E-01 0.0000000E+00 +-7.5000000E+00 -4.1666667E-05 6.0000000E-01 0.0000000E+00 +-7.0000000E+00 -3.8888889E-05 6.0000000E-01 0.0000000E+00 +-6.5000000E+00 -3.6111111E-05 6.0000000E-01 0.0000000E+00 +-6.0000000E+00 -3.3333333E-05 6.0000000E-01 0.0000000E+00 +-5.5000000E+00 -3.0555556E-05 6.0000000E-01 0.0000000E+00 +-5.0000000E+00 -2.7777778E-05 6.0000000E-01 0.0000000E+00 +-4.5000000E+00 -2.5000000E-05 6.0000000E-01 0.0000000E+00 +-4.0000000E+00 -2.2222222E-05 6.0000000E-01 0.0000000E+00 +-3.5000000E+00 -1.9444444E-05 6.0000000E-01 0.0000000E+00 +-3.0000000E+00 -1.6666667E-05 6.0000000E-01 0.0000000E+00 +-2.5000000E+00 -1.3888889E-05 6.0000000E-01 0.0000000E+00 +-2.0000000E+00 -1.1111111E-05 6.0000000E-01 0.0000000E+00 +-1.5000000E+00 -8.3333333E-06 6.0000000E-01 0.0000000E+00 +-1.0000000E+00 -5.5555556E-06 6.0000000E-01 0.0000000E+00 +-5.0000000E-01 -2.7777778E-06 6.0000000E-01 0.0000000E+00 +0.0000000E+00 1.0842022E-19 6.0000000E-01 0.0000000E+00 +5.0000000E-01 2.7777778E-06 6.0000000E-01 0.0000000E+00 +1.0000000E+00 5.5555556E-06 6.0000000E-01 0.0000000E+00 +1.5000000E+00 8.3333333E-06 6.0000000E-01 0.0000000E+00 +2.0000000E+00 1.1111111E-05 6.0000000E-01 0.0000000E+00 +2.5000000E+00 1.3888889E-05 6.0000000E-01 0.0000000E+00 +3.0000000E+00 1.6666667E-05 6.0000000E-01 0.0000000E+00 +3.5000000E+00 1.9444444E-05 6.0000000E-01 0.0000000E+00 +4.0000000E+00 2.2222222E-05 6.0000000E-01 0.0000000E+00 +4.5000000E+00 2.5000000E-05 6.0000000E-01 0.0000000E+00 +5.0000000E+00 2.7777778E-05 6.0000000E-01 0.0000000E+00 +5.5000000E+00 3.0555556E-05 6.0000000E-01 0.0000000E+00 +6.0000000E+00 3.3333333E-05 6.0000000E-01 0.0000000E+00 +6.5000000E+00 3.6111111E-05 6.0000000E-01 0.0000000E+00 +7.0000000E+00 3.8888889E-05 6.0000000E-01 0.0000000E+00 +7.5000000E+00 4.1666667E-05 6.0000000E-01 0.0000000E+00 +8.0000000E+00 4.4444444E-05 6.0000000E-01 0.0000000E+00 +8.5000000E+00 4.7222222E-05 6.0000000E-01 0.0000000E+00 +9.0000000E+00 5.0000000E-05 6.0000000E-01 0.0000000E+00 +9.5000000E+00 5.2777778E-05 6.0000000E-01 0.0000000E+00 +1.0000000E+01 5.5555556E-05 6.0000000E-01 0.0000000E+00 +1.0500000E+01 5.8333333E-05 6.0000000E-01 0.0000000E+00 +1.1000000E+01 6.1111111E-05 6.0000000E-01 0.0000000E+00 +1.1500000E+01 6.3888889E-05 6.0000000E-01 0.0000000E+00 +1.2000000E+01 6.6666667E-05 6.0000000E-01 0.0000000E+00 +1.2500000E+01 6.9444444E-05 6.0000000E-01 0.0000000E+00 +1.3000000E+01 7.2222222E-05 6.0000000E-01 0.0000000E+00 +1.3500000E+01 7.5000000E-05 6.0000000E-01 0.0000000E+00 +1.4000000E+01 7.7777778E-05 6.0000000E-01 0.0000000E+00 +1.4500000E+01 8.0555556E-05 6.0000000E-01 0.0000000E+00 +1.5000000E+01 8.3333333E-05 6.0000000E-01 0.0000000E+00 +1.5500000E+01 8.6111111E-05 6.0000000E-01 0.0000000E+00 +1.6000000E+01 8.8888889E-05 6.0000000E-01 0.0000000E+00 +1.6500000E+01 9.1666667E-05 6.0000000E-01 0.0000000E+00 +1.7000000E+01 9.4444444E-05 6.0000000E-01 0.0000000E+00 +1.7500000E+01 9.7222222E-05 6.0000000E-01 0.0000000E+00 +1.8000000E+01 1.0000000E-04 6.0000000E-01 0.0000000E+00 +1.8500000E+01 1.0277778E-04 6.0000000E-01 0.0000000E+00 +1.9000000E+01 1.0555556E-04 6.0000000E-01 0.0000000E+00 +1.9500000E+01 1.0833333E-04 6.0000000E-01 0.0000000E+00 +2.0000000E+01 1.1111111E-04 6.0000000E-01 0.0000000E+00 +2.1000000E+01 1.1666667E-04 6.0000000E-01 0.0000000E+00 +2.2000000E+01 1.2222222E-04 6.0000000E-01 0.0000000E+00 +2.3000000E+01 1.2777778E-04 6.0000000E-01 0.0000000E+00 +2.4000000E+01 1.3333333E-04 6.0000000E-01 0.0000000E+00 +2.5000000E+01 1.3888889E-04 6.0000000E-01 0.0000000E+00 +2.6000000E+01 1.4444444E-04 6.0000000E-01 0.0000000E+00 +2.7000000E+01 1.5000000E-04 6.0000000E-01 0.0000000E+00 +2.8000000E+01 1.5555556E-04 6.0000000E-01 0.0000000E+00 +2.9000000E+01 1.6111111E-04 6.0000000E-01 0.0000000E+00 +3.0000000E+01 1.6666667E-04 6.0000000E-01 0.0000000E+00 +3.2000000E+01 1.7777778E-04 6.0000000E-01 0.0000000E+00 +3.4000000E+01 1.8888889E-04 6.0000000E-01 0.0000000E+00 +3.6000000E+01 2.0000000E-04 6.0000000E-01 0.0000000E+00 +3.8000000E+01 2.1111111E-04 6.0000000E-01 0.0000000E+00 +4.0000000E+01 2.2222222E-04 6.0000000E-01 0.0000000E+00 +4.2000000E+01 2.3333333E-04 6.0000000E-01 0.0000000E+00 +4.4000000E+01 2.4444444E-04 6.0000000E-01 0.0000000E+00 +4.6000000E+01 2.5555556E-04 6.0000000E-01 0.0000000E+00 +4.8000000E+01 2.6666667E-04 6.0000000E-01 0.0000000E+00 +5.0000000E+01 2.7777778E-04 6.0000000E-01 0.0000000E+00 +5.2000000E+01 2.8888889E-04 6.0000000E-01 0.0000000E+00 +5.4000000E+01 3.0000000E-04 6.0000000E-01 0.0000000E+00 +5.6000000E+01 3.1111111E-04 6.0000000E-01 0.0000000E+00 +5.8000000E+01 3.2222222E-04 6.0000000E-01 0.0000000E+00 +6.0000000E+01 3.3333333E-04 6.0000000E-01 0.0000000E+00 +6.2000000E+01 3.4444444E-04 6.0000000E-01 0.0000000E+00 +6.4000000E+01 3.5555556E-04 6.0000000E-01 0.0000000E+00 +6.6000000E+01 3.6666667E-04 6.0000000E-01 0.0000000E+00 +6.8000000E+01 3.7777778E-04 6.0000000E-01 0.0000000E+00 +7.0000000E+01 3.8888889E-04 6.0000000E-01 0.0000000E+00 +7.2000000E+01 4.0000000E-04 6.0000000E-01 0.0000000E+00 +7.4000000E+01 4.1111111E-04 6.0000000E-01 0.0000000E+00 +7.6000000E+01 4.2222222E-04 6.0000000E-01 0.0000000E+00 +7.8000000E+01 4.3333333E-04 6.0000000E-01 0.0000000E+00 +8.0000000E+01 4.4444444E-04 6.0000000E-01 0.0000000E+00 +8.2000000E+01 4.5555556E-04 6.0000000E-01 0.0000000E+00 +8.4000000E+01 4.6666667E-04 6.0000000E-01 0.0000000E+00 +8.6000000E+01 4.7777778E-04 6.0000000E-01 0.0000000E+00 +8.8000000E+01 4.8888889E-04 6.0000000E-01 0.0000000E+00 +9.0000000E+01 5.0000000E-04 6.0000000E-01 0.0000000E+00 +9.2000000E+01 5.1111111E-04 6.0000000E-01 0.0000000E+00 +9.4000000E+01 5.2222222E-04 6.0000000E-01 0.0000000E+00 +9.6000000E+01 5.3333333E-04 6.0000000E-01 0.0000000E+00 +9.8000000E+01 5.4444444E-04 6.0000000E-01 0.0000000E+00 +1.0000000E+02 5.5555556E-04 6.0000000E-01 0.0000000E+00 +1.0200000E+02 5.6666667E-04 6.0000000E-01 0.0000000E+00 +1.0400000E+02 5.7777778E-04 6.0000000E-01 0.0000000E+00 +1.0600000E+02 5.8888889E-04 6.0000000E-01 0.0000000E+00 +1.0800000E+02 6.0000000E-04 6.0000000E-01 0.0000000E+00 +1.1000000E+02 6.1111111E-04 6.0000000E-01 0.0000000E+00 +1.1200000E+02 6.2222222E-04 6.0000000E-01 0.0000000E+00 +1.1400000E+02 6.3333333E-04 6.0000000E-01 0.0000000E+00 +1.1600000E+02 6.4444444E-04 6.0000000E-01 0.0000000E+00 +1.1800000E+02 6.5555556E-04 6.0000000E-01 0.0000000E+00 +1.2000000E+02 6.6666667E-04 6.0000000E-01 0.0000000E+00 +1.2200000E+02 6.7777778E-04 6.0000000E-01 0.0000000E+00 +1.2400000E+02 6.8888889E-04 6.0000000E-01 0.0000000E+00 +1.2600000E+02 7.0000000E-04 6.0000000E-01 0.0000000E+00 +1.2800000E+02 7.1111111E-04 6.0000000E-01 0.0000000E+00 +1.3000000E+02 7.2222222E-04 6.0000000E-01 0.0000000E+00 +1.3200000E+02 7.3333333E-04 6.0000000E-01 0.0000000E+00 +1.3400000E+02 7.4444444E-04 6.0000000E-01 0.0000000E+00 +1.3600000E+02 7.5555556E-04 6.0000000E-01 0.0000000E+00 +1.3800000E+02 7.6666667E-04 6.0000000E-01 0.0000000E+00 +1.4000000E+02 7.7777778E-04 6.0000000E-01 0.0000000E+00 +1.4200000E+02 7.8888889E-04 6.0000000E-01 0.0000000E+00 +1.4400000E+02 8.0000000E-04 6.0000000E-01 0.0000000E+00 +1.4600000E+02 8.1111111E-04 6.0000000E-01 0.0000000E+00 +1.4800000E+02 8.2222222E-04 6.0000000E-01 0.0000000E+00 +1.5000000E+02 8.3333333E-04 6.0000000E-01 0.0000000E+00 +1.5200000E+02 8.4444444E-04 6.0000000E-01 0.0000000E+00 +1.5400000E+02 8.5555556E-04 6.0000000E-01 0.0000000E+00 +1.5600000E+02 8.6666667E-04 6.0000000E-01 0.0000000E+00 +1.5800000E+02 8.7777778E-04 6.0000000E-01 0.0000000E+00 +1.6000000E+02 8.8888889E-04 6.0000000E-01 0.0000000E+00 +1.6200000E+02 9.0000000E-04 6.0000000E-01 0.0000000E+00 +1.6400000E+02 9.1111111E-04 6.0000000E-01 0.0000000E+00 +1.6600000E+02 9.2222222E-04 6.0000000E-01 0.0000000E+00 +1.6800000E+02 9.3333333E-04 6.0000000E-01 0.0000000E+00 +1.7000000E+02 9.4444444E-04 6.0000000E-01 0.0000000E+00 +1.7200000E+02 9.5555556E-04 6.0000000E-01 0.0000000E+00 +1.7400000E+02 9.6666667E-04 6.0000000E-01 0.0000000E+00 +1.7600000E+02 9.7777778E-04 6.0000000E-01 0.0000000E+00 +1.7800000E+02 9.8888889E-04 6.0000000E-01 0.0000000E+00 +1.8000000E+02 1.0000000E-03 6.0000000E-01 0.0000000E+00 +PROFILE SET 2: Comment 2 +2 +24.00 57.00 +251 +Profile 1, Set 2 - 24% AOA Cl Cd Cm 24% +-1.8000000E+02 -3.7080000E-01 5.0000000E-02 -6.6800000E-02 +-1.7800000E+02 -2.3007983E-01 5.6996066E-02 3.2800000E-03 +-1.7600000E+02 -7.5517601E-02 7.5772226E-02 7.6720000E-02 +-1.7400000E+02 9.3181440E-02 1.0301042E-01 1.5672000E-01 +-1.7200000E+02 2.7482692E-01 1.3539260E-01 2.3360000E-01 +-1.7000000E+02 4.3933974E-01 1.6960071E-01 2.7136000E-01 +-1.6800000E+02 4.8560200E-01 2.0231668E-01 2.6528000E-01 +-1.6600000E+02 5.0317695E-01 2.3022246E-01 2.5376000E-01 +-1.6400000E+02 4.6171454E-01 2.5000000E-01 2.4000000E-01 +-1.6200000E+02 4.3287137E-01 2.5997675E-01 2.3248000E-01 +-1.6000000E+02 4.1880299E-01 2.6506223E-01 2.2912000E-01 +-1.5800000E+02 4.2920973E-01 2.7181146E-01 2.3536000E-01 +-1.5600000E+02 4.6048810E-01 2.8677947E-01 2.5264000E-01 +-1.5400000E+02 5.1290839E-01 3.1652130E-01 2.7873657E-01 +-1.5200000E+02 5.7230396E-01 3.5940305E-01 3.0006065E-01 +-1.5000000E+02 6.2478753E-01 4.0494886E-01 3.2308783E-01 +-1.4800000E+02 6.6019994E-01 4.5225476E-01 3.3980436E-01 +-1.4600000E+02 6.9210804E-01 5.0041679E-01 3.5551030E-01 +-1.4400000E+02 7.1753888E-01 5.4853098E-01 3.6985848E-01 +-1.4200000E+02 7.3662342E-01 5.9569337E-01 3.8288806E-01 +-1.4000000E+02 7.4955176E-01 6.4100000E-01 3.9464363E-01 +-1.3800000E+02 7.5655990E-01 6.8590215E-01 4.0517341E-01 +-1.3600000E+02 7.5791802E-01 7.3120113E-01 4.1452780E-01 +-1.3400000E+02 7.5392024E-01 7.7531136E-01 4.2275813E-01 +-1.3200000E+02 7.4487604E-01 8.1857786E-01 4.2991567E-01 +-1.3000000E+02 7.3110303E-01 8.6060000E-01 4.3605090E-01 +-1.2800000E+02 7.1292122E-01 9.0132006E-01 4.4121290E-01 +-1.2600000E+02 6.9064833E-01 9.4068533E-01 4.4544903E-01 +-1.2400000E+02 6.6459625E-01 9.7821853E-01 4.4880461E-01 +-1.2200000E+02 6.3506842E-01 1.0142228E+00 4.5132287E-01 +-1.2000000E+02 6.0235788E-01 1.0482000E+00 4.5304487E-01 +-1.1800000E+02 5.6674603E-01 1.0802061E+00 4.5400954E-01 +-1.1600000E+02 5.2850190E-01 1.1102554E+00 4.5425378E-01 +-1.1400000E+02 4.8788183E-01 1.1378430E+00 4.5381259E-01 +-1.1200000E+02 4.4512948E-01 1.1634949E+00 4.5271919E-01 +-1.1000000E+02 4.0047618E-01 1.1866000E+00 4.5100519E-01 +-1.0800000E+02 3.5414141E-01 1.2073773E+00 4.4870078E-01 +-1.0600000E+02 3.0633355E-01 1.2259430E+00 4.4583483E-01 +-1.0400000E+02 2.5725070E-01 1.2416746E+00 4.4243506E-01 +-1.0200000E+02 2.0708164E-01 1.2555655E+00 4.3852819E-01 +-1.0000000E+02 1.5600689E-01 1.2665000E+00 4.3414003E-01 +-9.8000000E+01 1.0419980E-01 1.2752980E+00 4.2929558E-01 +-9.6000000E+01 5.1827745E-02 1.2819458E+00 4.2401913E-01 +-9.4000000E+01 -9.4667954E-04 1.2844808E+00 4.1833426E-01 +-9.2000000E+01 -5.3964404E-02 1.2857982E+00 4.1226392E-01 +-9.0000000E+01 -1.0706861E-01 1.2863000E+00 4.0583041E-01 +-8.8000000E+01 -1.6010345E-01 1.2837866E+00 3.9905539E-01 +-8.6000000E+01 -2.1291271E-01 1.2780933E+00 3.9195980E-01 +-8.4000000E+01 -2.6533855E-01 1.2713222E+00 3.8456387E-01 +-8.2000000E+01 -3.1722013E-01 1.2617782E+00 3.7688695E-01 +-8.0000000E+01 -3.6839234E-01 1.2504000E+00 3.6894750E-01 +-7.8000000E+01 -4.1868451E-01 1.2372024E+00 3.6076287E-01 +-7.6000000E+01 -4.6791909E-01 1.2217980E+00 3.5234924E-01 +-7.4000000E+01 -5.1591044E-01 1.2049106E+00 3.4372142E-01 +-7.2000000E+01 -5.6246361E-01 1.1859838E+00 3.3489264E-01 +-7.0000000E+01 -6.0737319E-01 1.1654000E+00 3.2587438E-01 +-6.8000000E+01 -6.5042225E-01 1.1431275E+00 3.1667613E-01 +-6.6000000E+01 -6.9138133E-01 1.1190173E+00 3.0730511E-01 +-6.4000000E+01 -7.3000761E-01 1.0933857E+00 2.9776607E-01 +-6.2000000E+01 -7.6604423E-01 1.0659453E+00 2.8806090E-01 +-6.0000000E+01 -7.9921980E-01 1.0368000E+00 2.7818840E-01 +-5.8000000E+01 -8.2924816E-01 1.0058711E+00 2.6814391E-01 +-5.6000000E+01 -8.5582850E-01 9.7301646E-01 2.5791893E-01 +-5.4000000E+01 -8.7864587E-01 9.3829725E-01 2.4750078E-01 +-5.2000000E+01 -8.9737214E-01 9.0149442E-01 2.3687219E-01 +-5.0000000E+01 -9.1166766E-01 8.6250000E-01 2.2601086E-01 +-4.8000000E+01 -9.2118350E-01 8.2116674E-01 2.1488911E-01 +-4.6000000E+01 -9.2556467E-01 7.7724007E-01 2.0347336E-01 +-4.4000000E+01 -9.2445423E-01 7.3039427E-01 1.9172381E-01 +-4.2000000E+01 -9.1749853E-01 6.8020982E-01 1.7959393E-01 +-4.0000000E+01 -9.0984107E-01 6.2730000E-01 1.6703018E-01 +-3.8000000E+01 -9.0161414E-01 5.7149033E-01 1.5397159E-01 +-3.6000000E+01 -8.9242716E-01 5.1292880E-01 1.4034954E-01 +-3.4000000E+01 -8.8132520E-01 4.5131152E-01 1.2608748E-01 +-3.2000000E+01 -8.7021442E-01 3.8431956E-01 1.1110090E-01 +-3.0000000E+01 -8.5909483E-01 3.2610000E-01 9.4958218E-02 +-2.9000000E+01 -8.5976973E-01 3.0497775E-01 8.7667452E-02 +-2.8000000E+01 -8.6159135E-01 2.8666880E-01 8.0668716E-02 +-2.7000000E+01 -8.6425508E-01 2.7054544E-01 7.3910297E-02 +-2.6000000E+01 -8.6745631E-01 2.5597998E-01 6.7340483E-02 +-2.5000000E+01 -8.7089041E-01 2.4234474E-01 6.0907558E-02 +-2.4000000E+01 -8.7425279E-01 2.2901202E-01 5.4559810E-02 +-2.3000000E+01 -8.7813915E-01 2.1690612E-01 4.8466134E-02 +-2.2000000E+01 -8.8189576E-01 2.0550890E-01 4.2368435E-02 +-2.1000000E+01 -8.8532825E-01 1.9464944E-01 3.6196697E-02 +-2.0000000E+01 -8.8824227E-01 1.8415686E-01 2.9880905E-02 +-1.9500000E+01 -8.8934285E-01 1.7900855E-01 2.6615975E-02 +-1.9000000E+01 -8.9044343E-01 1.7386023E-01 2.3351044E-02 +-1.8500000E+01 -8.9109040E-01 1.6872445E-01 1.9944072E-02 +-1.8000000E+01 -8.9173737E-01 1.6358867E-01 1.6537099E-02 +-1.7500000E+01 -8.9146773E-01 1.5837997E-01 1.2953077E-02 +-1.7000000E+01 -8.9119808E-01 1.5317127E-01 9.3690542E-03 +-1.6500000E+01 -8.8651715E-01 1.4780420E-01 5.5729745E-03 +-1.6000000E+01 -8.8183622E-01 1.4243713E-01 1.7768947E-03 +-1.5500000E+01 -8.7483575E-01 1.3682624E-01 -2.2662501E-03 +-1.5000000E+01 -8.6783528E-01 1.3121534E-01 -6.3093948E-03 +-1.4500000E+01 -8.5659133E-01 1.2527518E-01 -1.0634612E-02 +-1.4000000E+01 -8.4534737E-01 1.1933501E-01 -1.4959829E-02 +-1.3500000E+01 -8.2985818E-01 1.1298012E-01 -1.9602127E-02 +-1.3000000E+01 -8.1436899E-01 1.0662523E-01 -2.4244424E-02 +-1.2500000E+01 -7.9499698E-01 9.9770162E-02 -2.9238809E-02 +-1.2000000E+01 -7.7562497E-01 9.2915094E-02 -3.4233194E-02 +-1.1500000E+01 -7.5042427E-01 8.4917922E-02 -3.9863844E-02 +-1.1000000E+01 -7.2522356E-01 7.6920749E-02 -4.5494493E-02 +-1.0500000E+01 -7.0166226E-01 7.1444718E-02 -6.1634335E-02 +-1.0000000E+01 -6.7810095E-01 6.5968686E-02 -7.7774177E-02 +-9.5000000E+00 -6.3087528E-01 5.4937458E-02 -8.7344419E-02 +-9.0000000E+00 -5.8364960E-01 4.3906229E-02 -9.6914660E-02 +-8.5000000E+00 -5.3119389E-01 3.1801879E-02 -1.0447700E-01 +-8.0000000E+00 -4.7873818E-01 1.9697529E-02 -1.1203933E-01 +-7.5000000E+00 -4.0984329E-01 1.5668509E-02 -1.2340658E-01 +-7.0000000E+00 -3.4094839E-01 1.1639488E-02 -1.3477382E-01 +-6.5000000E+00 -2.7319015E-01 1.1511571E-02 -1.4005509E-01 +-6.0000000E+00 -2.0543190E-01 1.1383654E-02 -1.4533636E-01 +-5.5000000E+00 -1.4008407E-01 1.0887060E-02 -1.4802557E-01 +-5.0000000E+00 -7.4736249E-02 1.0390466E-02 -1.5071477E-01 +-4.5000000E+00 -1.0925257E-02 1.0187598E-02 -1.5226586E-01 +-4.0000000E+00 5.2885736E-02 9.9847291E-03 -1.5381694E-01 +-3.5000000E+00 1.1494406E-01 1.0023815E-02 -1.5475831E-01 +-3.0000000E+00 1.7700239E-01 1.0062900E-02 -1.5569968E-01 +-2.5000000E+00 2.3900932E-01 1.0124970E-02 -1.5639901E-01 +-2.0000000E+00 3.0101624E-01 1.0187039E-02 -1.5709834E-01 +-1.5000000E+00 3.6396668E-01 1.0281807E-02 -1.5767127E-01 +-1.0000000E+00 4.2691712E-01 1.0376574E-02 -1.5824419E-01 +-5.0000000E-01 4.8963800E-01 1.0501861E-02 -1.5873509E-01 +0.0000000E+00 5.5235887E-01 1.0627147E-02 -1.5922598E-01 +5.0000000E-01 6.1409321E-01 1.0787656E-02 -1.5966991E-01 +1.0000000E+00 6.7582754E-01 1.0948164E-02 -1.6011383E-01 +1.5000000E+00 7.3681057E-01 1.1136666E-02 -1.6045572E-01 +2.0000000E+00 7.9779359E-01 1.1325167E-02 -1.6079761E-01 +2.5000000E+00 8.5929688E-01 1.1550519E-02 -1.6108218E-01 +3.0000000E+00 9.2080016E-01 1.1775871E-02 -1.6136674E-01 +3.5000000E+00 9.8203113E-01 1.2036683E-02 -1.6155182E-01 +4.0000000E+00 1.0432621E+00 1.2297495E-02 -1.6173689E-01 +4.5000000E+00 1.1033698E+00 1.2604357E-02 -1.6167774E-01 +5.0000000E+00 1.1634774E+00 1.2911219E-02 -1.6161858E-01 +5.5000000E+00 1.2219564E+00 1.3275399E-02 -1.6140612E-01 +6.0000000E+00 1.2804353E+00 1.3639579E-02 -1.6119365E-01 +6.5000000E+00 1.3338964E+00 1.4111196E-02 -1.6076362E-01 +7.0000000E+00 1.3873575E+00 1.4582812E-02 -1.6033358E-01 +7.5000000E+00 1.4457454E+00 1.4886902E-02 -1.5977991E-01 +8.0000000E+00 1.5041332E+00 1.5190991E-02 -1.5922624E-01 +8.5000000E+00 1.5531272E+00 1.5840679E-02 -1.5842394E-01 +9.0000000E+00 1.6021211E+00 1.6490367E-02 -1.5762163E-01 +9.5000000E+00 1.6454078E+00 1.6931419E-02 -1.5669297E-01 +1.0000000E+01 1.6886945E+00 1.7372471E-02 -1.5576430E-01 +1.0500000E+01 1.7129639E+00 1.7904612E-02 -1.5427137E-01 +1.1000000E+01 1.7372332E+00 1.8436752E-02 -1.5277843E-01 +1.1500000E+01 1.7723415E+00 1.9408985E-02 -1.5063186E-01 +1.2000000E+01 1.8074498E+00 2.0381218E-02 -1.4848529E-01 +1.2500000E+01 1.8441227E+00 2.1553468E-02 -1.4612356E-01 +1.3000000E+01 1.8807955E+00 2.2725718E-02 -1.4376182E-01 +1.3500000E+01 1.8988876E+00 2.4025093E-02 -1.4349296E-01 +1.4000000E+01 1.9169797E+00 2.5324467E-02 -1.4322409E-01 +1.4500000E+01 1.8570856E+00 7.9539379E-02 -1.4675834E-01 +1.5000000E+01 1.7971915E+00 1.3375429E-01 -1.5029258E-01 +1.5500000E+01 1.7559874E+00 1.4584513E-01 -1.5227813E-01 +1.6000000E+01 1.7147832E+00 1.5793596E-01 -1.5426368E-01 +1.6500000E+01 1.6758508E+00 1.6932982E-01 -1.5495058E-01 +1.7000000E+01 1.6369184E+00 1.8072367E-01 -1.5563747E-01 +1.7500000E+01 1.6003422E+00 1.9152197E-01 -1.5615981E-01 +1.8000000E+01 1.5637660E+00 2.0232026E-01 -1.5668214E-01 +1.8500000E+01 1.5296305E+00 2.1262440E-01 -1.5706803E-01 +1.9000000E+01 1.4954950E+00 2.2292854E-01 -1.5745392E-01 +1.9500000E+01 1.4638847E+00 2.3283994E-01 -1.5773148E-01 +2.0000000E+01 1.4322743E+00 2.4275134E-01 -1.5800904E-01 +2.1000000E+01 1.3742728E+00 2.6199150E-01 -1.5840372E-01 +2.2000000E+01 1.3216595E+00 2.8085183E-01 -1.5869422E-01 +2.3000000E+01 1.2746033E+00 2.9953516E-01 -1.5893675E-01 +2.4000000E+01 1.2332732E+00 3.1824432E-01 -1.5918756E-01 +2.5000000E+01 1.1978381E+00 3.3718214E-01 -1.5950288E-01 +2.6000000E+01 1.1684669E+00 3.5655144E-01 -1.5993893E-01 +2.7000000E+01 1.1453285E+00 3.7655505E-01 -1.6055197E-01 +2.8000000E+01 1.1285920E+00 3.9739580E-01 -1.6139821E-01 +2.9000000E+01 1.1184261E+00 4.1927650E-01 -1.6253389E-01 +3.0000000E+01 1.1150000E+00 4.4240000E-01 -1.6401525E-01 +3.2000000E+01 1.1189099E+00 4.9324020E-01 -1.7804703E-01 +3.4000000E+01 1.1228100E+00 5.4608095E-01 -1.9127586E-01 +3.6000000E+01 1.1268998E+00 5.9675108E-01 -2.0375902E-01 +3.8000000E+01 1.1355616E+00 6.4593756E-01 -2.1555093E-01 +4.0000000E+01 1.1396662E+00 6.9380000E-01 -2.2670345E-01 +4.2000000E+01 1.1381718E+00 7.4031368E-01 -2.3726624E-01 +4.4000000E+01 1.1303801E+00 7.8551731E-01 -2.4686502E-01 +4.6000000E+01 1.1169649E+00 8.2929407E-01 -2.5570774E-01 +4.8000000E+01 1.0985004E+00 8.7180269E-01 -2.6428767E-01 +5.0000000E+01 1.0754796E+00 9.1270000E-01 -2.7267575E-01 +5.2000000E+01 1.0483295E+00 9.5199445E-01 -2.8092289E-01 +5.4000000E+01 1.0174243E+00 9.8969637E-01 -2.8906318E-01 +5.6000000E+01 9.8309664E-01 1.0254973E+00 -2.9711688E-01 +5.8000000E+01 9.4564611E-01 1.0596647E+00 -3.0509292E-01 +6.0000000E+01 9.0534690E-01 1.0919000E+00 -3.1299114E-01 +6.2000000E+01 8.6245327E-01 1.1222910E+00 -3.2080427E-01 +6.4000000E+01 8.1720387E-01 1.1508772E+00 -3.2851956E-01 +6.6000000E+01 7.6982482E-01 1.1773280E+00 -3.3612028E-01 +6.8000000E+01 7.2053169E-01 1.2020466E+00 -3.4358689E-01 +7.0000000E+01 6.6953062E-01 1.2246000E+00 -3.5089813E-01 +7.2000000E+01 6.1701864E-01 1.2451519E+00 -3.5803189E-01 +7.4000000E+01 5.6318332E-01 1.2638003E+00 -3.6496593E-01 +7.6000000E+01 5.0820189E-01 1.2801051E+00 -3.7167852E-01 +7.8000000E+01 4.5223988E-01 1.2947564E+00 -3.7814897E-01 +8.0000000E+01 3.9544936E-01 1.3070000E+00 -3.8435805E-01 +8.2000000E+01 3.3796690E-01 1.3173181E+00 -3.9028841E-01 +8.4000000E+01 2.7991129E-01 1.3257459E+00 -3.9592486E-01 +8.6000000E+01 2.2138109E-01 1.3311894E+00 -4.0125471E-01 +8.8000000E+01 1.6245211E-01 1.3354616E+00 -4.0626801E-01 +9.0000000E+01 1.0317481E-01 1.3373000E+00 -4.1095779E-01 +9.2000000E+01 4.1723054E-02 1.3363173E+00 -4.1532382E-01 +9.4000000E+01 -1.8445679E-02 1.3339005E+00 -4.1936326E-01 +9.6000000E+01 -7.8328872E-02 1.3302638E+00 -4.2307564E-01 +9.8000000E+01 -1.3772811E-01 1.3232023E+00 -4.2646197E-01 +1.0000000E+02 -1.9644850E-01 1.3142000E+00 -4.2952454E-01 +1.0200000E+02 -2.5430058E-01 1.3032495E+00 -4.3226653E-01 +1.0400000E+02 -3.1110187E-01 1.2897213E+00 -4.3469173E-01 +1.0600000E+02 -3.6667790E-01 1.2744930E+00 -4.3680411E-01 +1.0800000E+02 -4.2086284E-01 1.2567449E+00 -4.3860738E-01 +1.1000000E+02 -4.7349944E-01 1.2370000E+00 -4.4010442E-01 +1.1200000E+02 -5.2443836E-01 1.2151664E+00 -4.4129669E-01 +1.1400000E+02 -5.7353658E-01 1.1910326E+00 -4.4218347E-01 +1.1600000E+02 -6.2065503E-01 1.1650608E+00 -4.4276098E-01 +1.1800000E+02 -6.6565509E-01 1.1367906E+00 -4.4302140E-01 +1.2000000E+02 -7.0835296E-01 1.1066000E+00 -4.4296000E-01 +1.2200000E+02 -7.4870666E-01 1.0744324E+00 -4.4256000E-01 +1.2400000E+02 -7.8649560E-01 1.0402056E+00 -4.4176000E-01 +1.2600000E+02 -8.2144138E-01 1.0042975E+00 -4.4056000E-01 +1.2800000E+02 -8.5336200E-01 9.6640530E-01 -4.3880000E-01 +1.3000000E+02 -8.8198113E-01 9.2690000E-01 -4.3664000E-01 +1.3200000E+02 -9.0702358E-01 8.8578087E-01 -4.3376000E-01 +1.3400000E+02 -9.2793053E-01 8.4305973E-01 -4.3024000E-01 +1.3600000E+02 -9.3864468E-01 7.9909989E-01 -4.2472000E-01 +1.3800000E+02 -9.4942962E-01 7.5363717E-01 -4.1928000E-01 +1.4000000E+02 -9.6009579E-01 7.0750000E-01 -4.1376000E-01 +1.4200000E+02 -9.4095497E-01 6.5959283E-01 -4.0272000E-01 +1.4400000E+02 -9.2175707E-01 6.0888925E-01 -3.9168000E-01 +1.4600000E+02 -9.0269110E-01 5.5650350E-01 -3.8064000E-01 +1.4800000E+02 -8.7753577E-01 5.0354984E-01 -3.6680000E-01 +1.5000000E+02 -8.3896560E-01 4.5114253E-01 -3.4632000E-01 +1.5200000E+02 -8.0037923E-01 4.0039581E-01 -3.2584000E-01 +1.5400000E+02 -7.6196489E-01 3.5242395E-01 -3.0536000E-01 +1.5600000E+02 -7.1123784E-01 3.1404132E-01 -2.7920000E-01 +1.5800000E+02 -6.7186683E-01 2.9074532E-01 -2.5912000E-01 +1.6000000E+02 -6.5571333E-01 2.7639825E-01 -2.5112000E-01 +1.6200000E+02 -6.6719688E-01 2.6486238E-01 -2.5696000E-01 +1.6400000E+02 -6.9987478E-01 2.5000000E-01 -2.7312000E-01 +1.6600000E+02 -7.5236044E-01 2.2718741E-01 -2.9784000E-01 +1.6800000E+02 -8.1642199E-01 1.9785702E-01 -3.2912000E-01 +1.7000000E+02 -8.5000000E-01 1.6495522E-01 -3.6832000E-01 +1.7200000E+02 -8.1865860E-01 1.3142846E-01 -3.3616000E-01 +1.7400000E+02 -7.1845619E-01 1.0022313E-01 -2.7080000E-01 +1.7600000E+02 -5.8505061E-01 7.4285672E-02 -1.9872000E-01 +1.7800000E+02 -4.7781347E-01 5.6562488E-02 -1.3416000E-01 +1.8000000E+02 -3.7080000E-01 5.0000000E-02 -6.9520000E-02 +Profile 2, Set 2 - 57% AOA Cl Cd Cm 57% +-1.8000000E+02 -2.6086270E-01 2.4065434E-01 -6.6800000E-02 +-1.7800000E+02 -1.6200987E-01 2.3313291E-01 3.2800000E-03 +-1.7600000E+02 -5.3223218E-02 2.2711022E-01 7.6720000E-02 +-1.7400000E+02 6.5731224E-02 2.2241974E-01 1.5672000E-01 +-1.7200000E+02 1.9403956E-01 2.1889494E-01 2.3360000E-01 +-1.7000000E+02 3.1046992E-01 2.1636929E-01 2.7136000E-01 +-1.6800000E+02 3.4346850E-01 2.1467628E-01 2.6528000E-01 +-1.6600000E+02 3.5621641E-01 2.1364937E-01 2.5376000E-01 +-1.6400000E+02 3.2715444E-01 2.1312203E-01 2.4000000E-01 +-1.6200000E+02 3.0698954E-01 2.1292775E-01 2.3248000E-01 +-1.6000000E+02 2.9727562E-01 2.1290000E-01 2.2912000E-01 +-1.5800000E+02 3.0493217E-01 2.2091983E-01 2.3536000E-01 +-1.5600000E+02 3.2744292E-01 2.4331762E-01 2.5264000E-01 +-1.5400000E+02 3.6503951E-01 2.7760082E-01 2.7873657E-01 +-1.5200000E+02 4.0767028E-01 3.2127687E-01 3.0006065E-01 +-1.5000000E+02 4.4544722E-01 3.7185322E-01 3.2308783E-01 +-1.4800000E+02 4.7110785E-01 4.2683731E-01 3.3980436E-01 +-1.4600000E+02 4.9430964E-01 4.8373659E-01 3.5551030E-01 +-1.4400000E+02 5.1292081E-01 5.4005850E-01 3.6985848E-01 +-1.4200000E+02 5.2702288E-01 5.9331049E-01 3.8288806E-01 +-1.4000000E+02 5.3674007E-01 6.4100000E-01 3.9464363E-01 +-1.3800000E+02 5.4222998E-01 6.8590215E-01 4.0517341E-01 +-1.3600000E+02 5.4367533E-01 7.3120113E-01 4.1452780E-01 +-1.3400000E+02 5.4127674E-01 7.7531136E-01 4.2275813E-01 +-1.3200000E+02 5.3524658E-01 8.1857786E-01 4.2991567E-01 +-1.3000000E+02 5.2580391E-01 8.6060000E-01 4.3605090E-01 +-1.2800000E+02 5.1317025E-01 9.0132006E-01 4.4121290E-01 +-1.2600000E+02 4.9756633E-01 9.4068533E-01 4.4544903E-01 +-1.2400000E+02 4.7920946E-01 9.7821853E-01 4.4880461E-01 +-1.2200000E+02 4.5831163E-01 1.0142228E+00 4.5132287E-01 +-1.2000000E+02 4.3507808E-01 1.0482000E+00 4.5304487E-01 +-1.1800000E+02 4.0970638E-01 1.0802061E+00 4.5400954E-01 +-1.1600000E+02 3.8238587E-01 1.1102554E+00 4.5425378E-01 +-1.1400000E+02 3.5329732E-01 1.1378430E+00 4.5381259E-01 +-1.1200000E+02 3.2261301E-01 1.1634949E+00 4.5271919E-01 +-1.1000000E+02 2.9049685E-01 1.1866000E+00 4.5100519E-01 +-1.0800000E+02 2.5710473E-01 1.2073773E+00 4.4870078E-01 +-1.0600000E+02 2.2258502E-01 1.2259430E+00 4.4583483E-01 +-1.0400000E+02 1.8707913E-01 1.2416746E+00 4.4243506E-01 +-1.0200000E+02 1.5072219E-01 1.2555655E+00 4.3852819E-01 +-1.0000000E+02 1.1364376E-01 1.2665000E+00 4.3414003E-01 +-9.8000000E+01 7.5968638E-02 1.2752980E+00 4.2929558E-01 +-9.6000000E+01 3.7817674E-02 1.2819458E+00 4.2401913E-01 +-9.4000000E+01 -6.9135313E-04 1.2844808E+00 4.1833426E-01 +-9.2000000E+01 -3.9442841E-02 1.2857982E+00 4.1226392E-01 +-9.0000000E+01 -7.8322446E-02 1.2863000E+00 4.0583041E-01 +-8.8000000E+01 -1.1721615E-01 1.2837866E+00 3.9905539E-01 +-8.6000000E+01 -1.5600929E-01 1.2780933E+00 3.9195980E-01 +-8.4000000E+01 -1.9458561E-01 1.2713222E+00 3.8456387E-01 +-8.2000000E+01 -2.3282630E-01 1.2617782E+00 3.7688695E-01 +-8.0000000E+01 -2.7060901E-01 1.2504000E+00 3.6894750E-01 +-7.8000000E+01 -3.0780689E-01 1.2372024E+00 3.6076287E-01 +-7.6000000E+01 -3.4428762E-01 1.2217980E+00 3.5234924E-01 +-7.4000000E+01 -3.7991250E-01 1.2049106E+00 3.4372142E-01 +-7.2000000E+01 -4.1453552E-01 1.1859838E+00 3.3489264E-01 +-7.0000000E+01 -4.4800248E-01 1.1654000E+00 3.2587438E-01 +-6.8000000E+01 -4.8015018E-01 1.1431275E+00 3.1667613E-01 +-6.6000000E+01 -5.1080565E-01 1.1190173E+00 3.0730511E-01 +-6.4000000E+01 -5.3978549E-01 1.0933857E+00 2.9776607E-01 +-6.2000000E+01 -5.6689532E-01 1.0659453E+00 2.8806090E-01 +-6.0000000E+01 -5.9192939E-01 1.0368000E+00 2.7818840E-01 +-5.8000000E+01 -6.1467034E-01 1.0058711E+00 2.6814391E-01 +-5.6000000E+01 -6.3488929E-01 9.7301646E-01 2.5791893E-01 +-5.4000000E+01 -6.5234612E-01 9.3829725E-01 2.4750078E-01 +-5.2000000E+01 -6.6679017E-01 9.0149442E-01 2.3687219E-01 +-5.0000000E+01 -6.7796143E-01 8.6250000E-01 2.2601086E-01 +-4.8000000E+01 -6.8559221E-01 8.2116674E-01 2.1488911E-01 +-4.6000000E+01 -6.8940943E-01 7.7724007E-01 2.0347336E-01 +-4.4000000E+01 -6.8913774E-01 7.2973921E-01 1.9172381E-01 +-4.2000000E+01 -6.8450342E-01 6.7726207E-01 1.7959393E-01 +-4.0000000E+01 -6.7933634E-01 6.2730000E-01 1.6703018E-01 +-3.8000000E+01 -6.7373414E-01 5.8345422E-01 1.5397159E-01 +-3.6000000E+01 -6.6740367E-01 5.4261465E-01 1.4034954E-01 +-3.4000000E+01 -6.5962852E-01 5.0348652E-01 1.2608748E-01 +-3.2000000E+01 -6.5183308E-01 4.6609514E-01 1.1110090E-01 +-3.0000000E+01 -6.4401735E-01 4.3040914E-01 9.4958218E-02 +-2.9000000E+01 -6.4410974E-01 4.1357775E-01 8.9291383E-02 +-2.8000000E+01 -6.4441201E-01 3.9734701E-01 8.4362092E-02 +-2.7000000E+01 -6.4496180E-01 3.8173722E-01 7.9999491E-02 +-2.6000000E+01 -6.4579673E-01 3.6676863E-01 7.6032723E-02 +-2.5000000E+01 -6.4695443E-01 3.5246152E-01 7.2290931E-02 +-2.4000000E+01 -6.4847255E-01 3.3883616E-01 6.8603260E-02 +-2.3000000E+01 -6.5038870E-01 3.2591283E-01 6.4798855E-02 +-2.2000000E+01 -6.5274054E-01 3.1371178E-01 6.0706858E-02 +-2.1000000E+01 -6.5556568E-01 3.0225330E-01 5.6156414E-02 +-2.0000000E+01 -6.5890177E-01 2.9155766E-01 5.0976667E-02 +-1.9500000E+01 -6.6084410E-01 2.8728820E-01 4.7711826E-02 +-1.9000000E+01 -6.6278643E-01 2.8301874E-01 4.4446984E-02 +-1.8500000E+01 -6.6502187E-01 2.7923292E-01 4.0116082E-02 +-1.8000000E+01 -6.6725730E-01 2.7544710E-01 3.5785180E-02 +-1.7500000E+01 -6.6980466E-01 2.7195469E-01 3.1074294E-02 +-1.7000000E+01 -6.7235201E-01 2.6846228E-01 2.6363407E-02 +-1.6500000E+01 -6.7523011E-01 2.6507306E-01 2.1958612E-02 +-1.6000000E+01 -6.7810820E-01 2.6168384E-01 1.7553816E-02 +-1.5500000E+01 -6.8133585E-01 2.5822353E-01 1.4104836E-02 +-1.5000000E+01 -6.8456349E-01 2.5476321E-01 1.0655855E-02 +-1.4500000E+01 -6.8815951E-01 2.5154235E-01 7.5550427E-03 +-1.4000000E+01 -6.9175553E-01 2.4832149E-01 4.4542304E-03 +-1.3500000E+01 -6.9573874E-01 2.4530946E-01 1.4556958E-03 +-1.3000000E+01 -6.9972194E-01 2.4229742E-01 -1.5428388E-03 +-1.2500000E+01 -7.0411115E-01 2.3919694E-01 -4.1451931E-03 +-1.2000000E+01 -7.0850036E-01 2.3609645E-01 -6.7475473E-03 +-1.1500000E+01 -7.1331439E-01 2.3261022E-01 -8.6598187E-03 +-1.1000000E+01 -7.1812841E-01 2.2912399E-01 -1.0572090E-02 +-1.0500000E+01 -7.2338608E-01 2.2483721E-01 -1.1500280E-02 +-1.0000000E+01 -7.2864374E-01 2.2055043E-01 -1.2428470E-02 +-9.5000000E+00 -7.6640939E-01 2.1538238E-01 -1.3351477E-02 +-9.0000000E+00 -8.0417503E-01 2.1021432E-01 -1.4274484E-02 +-8.5000000E+00 -8.3959124E-01 2.0681938E-01 -2.0099807E-02 +-8.0000000E+00 -8.7500744E-01 2.0342444E-01 -2.5925130E-02 +-7.5000000E+00 -8.3440425E-01 1.9680217E-01 -2.8468869E-02 +-7.0000000E+00 -7.9380105E-01 1.9017990E-01 -3.1012608E-02 +-6.5000000E+00 -7.5711722E-01 1.8766607E-01 -3.9892345E-02 +-6.0000000E+00 -7.2043339E-01 1.8515224E-01 -4.8772081E-02 +-5.5000000E+00 -6.7854338E-01 1.7877714E-01 -5.5802547E-02 +-5.0000000E+00 -6.3665337E-01 1.7240203E-01 -6.2833012E-02 +-4.5000000E+00 -5.7684109E-01 1.6689625E-01 -6.7512806E-02 +-4.0000000E+00 -5.1702880E-01 1.6139047E-01 -7.2192600E-02 +-3.5000000E+00 -4.5467292E-01 1.5825650E-01 -7.4646484E-02 +-3.0000000E+00 -3.9231703E-01 1.5512252E-01 -7.7100367E-02 +-2.5000000E+00 -3.3024687E-01 1.5103837E-01 -7.9615935E-02 +-2.0000000E+00 -2.6817670E-01 1.4695421E-01 -8.2131502E-02 +-1.5000000E+00 -2.0610579E-01 1.3951342E-01 -8.5448967E-02 +-1.0000000E+00 -1.4403487E-01 1.3207262E-01 -8.8766431E-02 +-5.0000000E-01 -8.1964943E-02 1.2852293E-01 -9.2987757E-02 +0.0000000E+00 -1.9895016E-02 1.2497323E-01 -9.7209082E-02 +5.0000000E-01 4.2175067E-02 1.2465997E-01 -1.0301186E-01 +1.0000000E+00 1.0424515E-01 1.2434671E-01 -1.0881463E-01 +1.5000000E+00 1.6631560E-01 1.2284508E-01 -1.1466399E-01 +2.0000000E+00 2.2838605E-01 1.2134344E-01 -1.2051335E-01 +2.5000000E+00 2.9045610E-01 1.2083531E-01 -1.2521937E-01 +3.0000000E+00 3.5252615E-01 1.2032717E-01 -1.2992539E-01 +3.5000000E+00 4.1459660E-01 1.2053650E-01 -1.3359110E-01 +4.0000000E+00 4.7666705E-01 1.2074582E-01 -1.3725681E-01 +4.5000000E+00 5.3873710E-01 1.2082657E-01 -1.4050035E-01 +5.0000000E+00 6.0080715E-01 1.2090732E-01 -1.4374389E-01 +5.5000000E+00 6.6287760E-01 1.2173159E-01 -1.4683513E-01 +6.0000000E+00 7.2494805E-01 1.2255586E-01 -1.4992637E-01 +6.5000000E+00 7.8701810E-01 1.2277745E-01 -1.5253298E-01 +7.0000000E+00 8.4908815E-01 1.2299903E-01 -1.5513959E-01 +7.5000000E+00 9.1115857E-01 1.2287699E-01 -1.5696547E-01 +8.0000000E+00 9.7322898E-01 1.2275495E-01 -1.5879135E-01 +8.5000000E+00 1.0352994E+00 1.2252295E-01 -1.6016361E-01 +9.0000000E+00 1.0973698E+00 1.2229095E-01 -1.6153587E-01 +9.5000000E+00 1.1594419E+00 1.2223135E-01 -1.6320728E-01 +1.0000000E+01 1.2215139E+00 1.2217174E-01 -1.6487869E-01 +1.0500000E+01 1.2841391E+00 1.2218989E-01 -1.6636940E-01 +1.1000000E+01 1.3467642E+00 1.2220804E-01 -1.6786010E-01 +1.1500000E+01 1.4044194E+00 1.2595604E-01 -1.6848553E-01 +1.2000000E+01 1.4620746E+00 1.2970404E-01 -1.6911095E-01 +1.2500000E+01 1.4929721E+00 1.6198863E-01 -1.6778943E-01 +1.3000000E+01 1.5238696E+00 1.9427321E-01 -1.6646790E-01 +1.3500000E+01 1.5417361E+00 2.0633252E-01 -1.6655316E-01 +1.4000000E+01 1.5596026E+00 2.1839183E-01 -1.6663841E-01 +1.4500000E+01 1.5751592E+00 2.3010475E-01 -1.6695390E-01 +1.5000000E+01 1.5907157E+00 2.4181767E-01 -1.6726938E-01 +1.5500000E+01 1.6046489E+00 2.5275715E-01 -1.6993854E-01 +1.6000000E+01 1.6185821E+00 2.6369663E-01 -1.7260769E-01 +1.6500000E+01 1.6280830E+00 2.7402080E-01 -1.7372881E-01 +1.7000000E+01 1.6375838E+00 2.8434496E-01 -1.7484993E-01 +1.7500000E+01 1.6366015E+00 2.9476927E-01 -1.7690551E-01 +1.8000000E+01 1.6356192E+00 3.0519358E-01 -1.7896109E-01 +1.8500000E+01 1.6332945E+00 3.1565198E-01 -1.8215430E-01 +1.9000000E+01 1.6309697E+00 3.2611037E-01 -1.8534751E-01 +1.9500000E+01 1.6273795E+00 3.3659955E-01 -1.8736764E-01 +2.0000000E+01 1.6237893E+00 3.4708873E-01 -1.8938777E-01 +2.1000000E+01 1.6010999E+00 3.6625854E-01 -1.8867734E-01 +2.2000000E+01 1.5575610E+00 3.8473515E-01 -1.8674903E-01 +2.3000000E+01 1.4986420E+00 4.0260191E-01 -1.8390731E-01 +2.4000000E+01 1.4298123E+00 4.1994217E-01 -1.8045664E-01 +2.5000000E+01 1.3565415E+00 4.3683929E-01 -1.7670151E-01 +2.6000000E+01 1.2842989E+00 4.5337661E-01 -1.7294638E-01 +2.7000000E+01 1.2185540E+00 4.6963751E-01 -1.6949571E-01 +2.8000000E+01 1.1647762E+00 4.8570533E-01 -1.6665399E-01 +2.9000000E+01 1.1284351E+00 5.0166342E-01 -1.6472568E-01 +3.0000000E+01 1.1150000E+00 5.1759514E-01 -1.6401525E-01 +3.2000000E+01 1.1148055E+00 5.4971325E-01 -1.7804703E-01 +3.4000000E+01 1.1145647E+00 5.8258441E-01 -1.9127586E-01 +3.6000000E+01 1.1144747E+00 6.1748759E-01 -2.0375902E-01 +3.8000000E+01 1.1188511E+00 6.5429796E-01 -2.1555093E-01 +4.0000000E+01 1.1186821E+00 6.9380000E-01 -2.2670345E-01 +4.2000000E+01 1.1129993E+00 7.3818154E-01 -2.3726624E-01 +4.4000000E+01 1.1011847E+00 7.8504351E-01 -2.4686502E-01 +4.6000000E+01 1.0839623E+00 8.2929407E-01 -2.5570774E-01 +4.8000000E+01 1.0619503E+00 8.7180269E-01 -2.6428767E-01 +5.0000000E+01 1.0356804E+00 9.1270000E-01 -2.7267575E-01 +5.2000000E+01 1.0056136E+00 9.5199445E-01 -2.8092289E-01 +5.4000000E+01 9.7215441E-01 9.8969637E-01 -2.8906318E-01 +5.6000000E+01 9.3566220E-01 1.0254973E+00 -2.9711688E-01 +5.8000000E+01 8.9646038E-01 1.0596647E+00 -3.0509292E-01 +6.0000000E+01 8.5484386E-01 1.0919000E+00 -3.1299114E-01 +6.2000000E+01 8.1108487E-01 1.1222910E+00 -3.2080427E-01 +6.4000000E+01 7.6543730E-01 1.1508772E+00 -3.2851956E-01 +6.6000000E+01 7.1813982E-01 1.1773280E+00 -3.3612028E-01 +6.8000000E+01 6.6941800E-01 1.2020466E+00 -3.4358689E-01 +7.0000000E+01 6.1948548E-01 1.2246000E+00 -3.5089813E-01 +7.2000000E+01 5.6854447E-01 1.2451519E+00 -3.5803189E-01 +7.4000000E+01 5.1678556E-01 1.2638003E+00 -3.6496593E-01 +7.6000000E+01 4.6438710E-01 1.2801051E+00 -3.7167852E-01 +7.8000000E+01 4.1151410E-01 1.2947564E+00 -3.7814897E-01 +8.0000000E+01 3.5831694E-01 1.3070000E+00 -3.8435805E-01 +8.2000000E+01 3.0492969E-01 1.3173181E+00 -3.9028841E-01 +8.4000000E+01 2.5146838E-01 1.3257459E+00 -3.9592486E-01 +8.6000000E+01 1.9802917E-01 1.3311894E+00 -4.0125471E-01 +8.8000000E+01 1.4468642E-01 1.3354616E+00 -4.0626801E-01 +9.0000000E+01 9.1490875E-02 1.3373000E+00 -4.1095779E-01 +9.2000000E+01 3.6835771E-02 1.3363173E+00 -4.1532382E-01 +9.4000000E+01 -1.6213082E-02 1.3339005E+00 -4.1936326E-01 +9.6000000E+01 -6.8542131E-02 1.3302638E+00 -4.2307564E-01 +9.8000000E+01 -1.1998046E-01 1.3232023E+00 -4.2646197E-01 +1.0000000E+02 -1.7036334E-01 1.3142000E+00 -4.2952454E-01 +1.0200000E+02 -2.1953381E-01 1.3032495E+00 -4.3226653E-01 +1.0400000E+02 -2.6734390E-01 1.2897213E+00 -4.3469173E-01 +1.0600000E+02 -3.1365544E-01 1.2744930E+00 -4.3680411E-01 +1.0800000E+02 -3.5834040E-01 1.2567449E+00 -4.3860738E-01 +1.1000000E+02 -4.0128072E-01 1.2370000E+00 -4.4010442E-01 +1.1200000E+02 -4.4236756E-01 1.2151664E+00 -4.4129669E-01 +1.1400000E+02 -4.8149982E-01 1.1910326E+00 -4.4218347E-01 +1.1600000E+02 -5.1858210E-01 1.1650608E+00 -4.4276098E-01 +1.1800000E+02 -5.5352165E-01 1.1367906E+00 -4.4302140E-01 +1.2000000E+02 -5.8619063E-01 1.1066000E+00 -4.4296000E-01 +1.2200000E+02 -6.1658108E-01 1.0744324E+00 -4.4256000E-01 +1.2400000E+02 -6.4453942E-01 1.0402056E+00 -4.4176000E-01 +1.2600000E+02 -6.6986862E-01 1.0042975E+00 -4.4056000E-01 +1.2800000E+02 -6.9245447E-01 9.6640530E-01 -4.3880000E-01 +1.3000000E+02 -7.1210968E-01 9.2690000E-01 -4.3664000E-01 +1.3200000E+02 -7.2865251E-01 8.8578087E-01 -4.3376000E-01 +1.3400000E+02 -7.4167915E-01 8.4305973E-01 -4.3024000E-01 +1.3600000E+02 -7.4642260E-01 7.9909989E-01 -4.2472000E-01 +1.3800000E+02 -7.5112691E-01 7.5363717E-01 -4.1928000E-01 +1.4000000E+02 -7.5564172E-01 7.0750000E-01 -4.1376000E-01 +1.4200000E+02 -7.3672370E-01 6.5693499E-01 -4.0272000E-01 +1.4400000E+02 -7.1791022E-01 5.9943915E-01 -3.9168000E-01 +1.4600000E+02 -6.9934886E-01 5.3789862E-01 -3.8064000E-01 +1.4800000E+02 -6.7624424E-01 4.7519954E-01 -3.6680000E-01 +1.5000000E+02 -6.4305733E-01 4.1422808E-01 -3.4632000E-01 +1.5200000E+02 -6.1016974E-01 3.5787036E-01 -3.2584000E-01 +1.5400000E+02 -5.7772536E-01 3.0901255E-01 -3.0536000E-01 +1.5600000E+02 -5.3630890E-01 2.7054079E-01 -2.7920000E-01 +1.5800000E+02 -5.0382399E-01 2.4534122E-01 -2.5912000E-01 +1.6000000E+02 -4.8897509E-01 2.3630000E-01 -2.5112000E-01 +1.6200000E+02 -4.9474923E-01 2.3630435E-01 -2.5696000E-01 +1.6400000E+02 -5.1604898E-01 2.3633483E-01 -2.7312000E-01 +1.6600000E+02 -5.4927232E-01 2.3641757E-01 -2.9784000E-01 +1.6800000E+02 -5.9070228E-01 2.3657868E-01 -3.2912000E-01 +1.7000000E+02 -6.4238939E-01 2.3684429E-01 -3.6832000E-01 +1.7200000E+02 -6.0310098E-01 2.3724054E-01 -3.3616000E-01 +1.7400000E+02 -5.1460605E-01 2.3779354E-01 -2.7080000E-01 +1.7600000E+02 -4.1657023E-01 2.3852942E-01 -1.9872000E-01 +1.7800000E+02 -3.3818357E-01 2.3947431E-01 -1.3416000E-01 +1.8000000E+02 -2.6086270E-01 2.4065434E-01 -6.9520000E-02 diff --git a/pyFAST/input_output/tests/example_files/FLEXWaveKin.wko b/pyFAST/input_output/tests/example_files/FLEXWaveKin.wko new file mode 100644 index 0000000..91b1a97 --- /dev/null +++ b/pyFAST/input_output/tests/example_files/FLEXWaveKin.wko @@ -0,0 +1,24 @@ +Wave kinematics + 3 +CrestHeightMax 2.695 m +LongitudinalVelocityMax 2.064 m/s +LongitudinalAccelerationMax 1.034 m/s^2 + 20.00 5.39 12.54 11 Water Depth/Hs/Tp/spectype + 1 Number of displacements + 0.00 +12 Number of Relative Depths + 0.000 0.050 0.100 0.150 0.200 0.300 0.400 0.500 0.600 0.700 0.800 1.000 + t [eta u_1..u_nDepths a_1..a_nDepths](x,t) * nDisplacements + 0.000 -0.04 -0.03 -0.03 -0.03 -0.03 -0.02 -0.02 -0.02 -0.02 -0.02 -0.02 -0.02 -0.02 1.03 1.01 0.98 0.96 0.94 0.90 0.87 0.84 0.82 0.80 0.79 0.78 + 0.250 0.30 0.23 0.23 0.22 0.21 0.21 0.20 0.19 0.19 0.18 0.18 0.18 0.17 1.03 1.00 0.98 0.96 0.93 0.90 0.86 0.84 0.82 0.80 0.79 0.78 + 0.500 0.63 0.49 0.47 0.46 0.45 0.44 0.42 0.41 0.40 0.39 0.38 0.37 0.37 1.01 0.98 0.96 0.93 0.91 0.88 0.85 0.82 0.80 0.78 0.77 0.76 + 0.750 0.96 0.73 0.71 0.70 0.68 0.67 0.64 0.62 0.60 0.58 0.57 0.56 0.55 0.97 0.94 0.92 0.90 0.88 0.84 0.81 0.79 0.77 0.75 0.74 0.73 + 1.000 1.26 0.97 0.94 0.92 0.90 0.88 0.84 0.81 0.79 0.77 0.75 0.74 0.73 0.91 0.89 0.87 0.85 0.83 0.80 0.77 0.74 0.73 0.71 0.70 0.69 + 1.250 1.55 1.19 1.16 1.13 1.10 1.08 1.04 1.00 0.97 0.94 0.92 0.91 0.90 0.85 0.82 0.81 0.79 0.77 0.74 0.71 0.69 0.67 0.66 0.65 0.64 + 1.500 1.81 1.39 1.35 1.32 1.29 1.26 1.21 1.17 1.13 1.10 1.08 1.06 1.05 0.76 0.75 0.73 0.71 0.70 0.67 0.64 0.62 0.61 0.59 0.59 0.58 + 1.750 2.05 1.57 1.53 1.49 1.46 1.43 1.37 1.32 1.28 1.25 1.22 1.20 1.19 0.67 0.66 0.64 0.62 0.61 0.59 0.57 0.55 0.53 0.52 0.51 0.51 + 2.000 2.25 1.72 1.68 1.64 1.60 1.57 1.50 1.45 1.41 1.37 1.34 1.32 1.30 0.57 0.55 0.54 0.53 0.52 0.50 0.48 0.46 0.45 0.44 0.44 0.43 + 2.250 2.42 1.85 1.81 1.76 1.72 1.68 1.62 1.56 1.51 1.47 1.44 1.42 1.40 0.46 0.44 0.43 0.42 0.41 0.40 0.38 0.37 0.36 0.35 0.35 0.34 + 2.500 2.55 1.95 1.90 1.86 1.82 1.78 1.70 1.64 1.59 1.55 1.52 1.49 1.48 0.34 0.33 0.32 0.31 0.31 0.29 0.28 0.27 0.27 0.26 0.26 0.25 + 2.750 2.64 2.02 1.97 1.92 1.88 1.84 1.76 1.70 1.65 1.60 1.57 1.55 1.53 0.21 0.21 0.20 0.20 0.19 0.18 0.18 0.17 0.17 0.16 0.16 0.16 + 3.000 2.69 2.06 2.01 1.96 1.91 1.87 1.80 1.73 1.68 1.63 1.60 1.58 1.56 0.08 0.08 0.08 0.08 0.08 0.07 0.07 0.07 0.07 0.06 0.06 0.06 diff --git a/pyFAST/input_output/tests/example_files/ParquetFile_test.parquet b/pyFAST/input_output/tests/example_files/ParquetFile_test.parquet new file mode 100644 index 0000000000000000000000000000000000000000..549c998c47471d501f6954610d26a45fedc7b144 GIT binary patch literal 2951 zcmcguPjBKz6gPx=lPFTF)f$1s0a?3fRV5S~$S$i~1_)pZ1W1CxRTcRkU}759JLm(lQR@!!ycxK+;-+S}k6Lr+&m;mz`^Pww`UP%&wGMh5A&%r zCaAyj1%L3x{-$F;(vdYA8*urkP+BlP*iw7M2}=!lP96?Ke=TMThp&ERMIyS_FeR= zTlNxYaAL=@-P76qC~M1>4g`B@nsA#q^4NiEf}DpVI3~$a$4Bn&Yq+@fE{Jj6GrMl3 zYv`tSu}*VgT{!a*cE?U_Lmg@^f~hr{FBwB+psNl>X6cTs$&L)-x2$f1xMBCO?)h2a z;4OQ%0%jcr>y{~pjjTagWjw%R*v?^!Og&&y*OWQD=7C)nk_Vriye%zA>A?knOsmYHhe^`w$RhMH{lRl5N+H1qCP zYEC#5^3{ouE8l3v)(qRJwmA*+JA7uMiQKf!yS}CN(&w}zawe8l?AAodVSmy{60a}Y z{06_p0$?xPw3E%WTEv);p^51c*uRuc1?(Fzax=Y^N$Bk?u`5^^L$+E2El2o{)Nb~{ zMxt{HcuNyQ64L{PZ;lj8CH7jaX0dYCGrAA3t8uE@U*kxT>yiOBP!bsgPO5 zBwTtq9vmAaPaE(<&J(pkgYY2-j#@IiTFIP2Jckg^NUrlnp29`nMw}7>0?Yn zULZCl%QTf@v#VGI$FrGN_-jrPQ@yG|a^lrU?J2(-a%xVsd?(@72WM)<0@PTuzkJ71 zVuEwK$>({pId}CQzVm%Pg+eW%K8=l2EEB)kqCPpLolURSgu<8KOJeO`@OgEaESrG) z3Ve}TP>QCZT7|jp=6Wt(C zB!CZd0S|$Eb)k>?L`B8l6@4-fmil>eMYB3+k?{fJ2NoDR$MXZ`Q3=lzSdDul$2s6H zppzrMB7|LK7qxVa^F06sH0KT8<|lqo{n<|r!`bREuEZ& SkKjj;`$yGxD5?%W!T$l4HRPoL literal 0 HcmV?d00001 diff --git a/pyFAST/input_output/tests/example_files/RoscoDISCON_PowerTracking.in b/pyFAST/input_output/tests/example_files/RoscoDISCON_PowerTracking.in new file mode 100644 index 0000000..53e2fca --- /dev/null +++ b/pyFAST/input_output/tests/example_files/RoscoDISCON_PowerTracking.in @@ -0,0 +1,151 @@ +! Controller parameter input file for the Main_ED wind turbine +! - File written using ROSCO version 2.6.0 controller tuning logic on 01/21/23 + +!------- DEBUG ------------------------------------------------------------ +1 ! LoggingLevel - {0: write no debug files, 1: write standard output .dbg-file, 2: LoggingLevel 1 + ROSCO LocalVars (.dbg2) 3: LoggingLevel 2 + complete avrSWAP-array (.dbg3)} + +!------- CONTROLLER FLAGS ------------------------------------------------- +1 ! F_LPFType - {1: first-order low-pass filter, 2: second-order low-pass filter}, [rad/s] (currently filters generator speed and pitch control signals +0 ! F_NotchType - Notch on the measured generator speed and/or tower fore-aft motion (for floating) {0: disable, 1: generator speed, 2: tower-top fore-aft motion, 3: generator speed and tower-top fore-aft motion} +0 ! IPC_ControlMode - Turn Individual Pitch Control (IPC) for fatigue load reductions (pitch contribution) {0: off, 1: 1P reductions, 2: 1P+2P reductions} +3 ! VS_ControlMode - Generator torque control mode in above rated conditions {0: constant torque, 1: constant power, 2: TSR tracking PI control with constant torque, 3: TSR tracking PI control with constant power} +1 ! PC_ControlMode - Blade pitch control mode {0: No pitch, fix to fine pitch, 1: active PI blade pitch control} +0 ! Y_ControlMode - Yaw control mode {0: no yaw control, 1: yaw rate control, 2: yaw-by-IPC} +1 ! SS_Mode - Setpoint Smoother mode {0: no setpoint smoothing, 1: introduce setpoint smoothing} +1 ! PRC_Mode - Power reference tracking mode{0: use standard rotor speed set points, 1: use PRC rotor speed setpoints} +2 ! WE_Mode - Wind speed estimator mode {0: One-second low pass filtered hub height wind speed, 1: Immersion and Invariance Estimator, 2: Extended Kalman Filter} +1 ! PS_Mode - Pitch saturation mode {0: no pitch saturation, 1: implement pitch saturation} +0 ! SD_Mode - Shutdown mode {0: no shutdown procedure, 1: pitch to max pitch at shutdown} +0 ! Fl_Mode - Floating specific feedback mode {0: no nacelle velocity feedback, 1: feed back translational velocity, 2: feed back rotational veloicty} +0 ! TD_Mode - Tower damper mode {0: no tower damper, 1: feed back translational nacelle accelleration to pitch angle} +0 ! Flp_Mode - Flap control mode {0: no flap control, 1: steady state flap angle, 2: Proportional flap control, 2: Cyclic (1P) flap control} +0 ! OL_Mode - Open loop control mode {0: no open loop control, 1: open loop control vs. time} +0 ! PA_Mode - Pitch actuator mode {0 - not used, 1 - first order filter, 2 - second order filter} +0 ! Ext_Mode - External control mode {0 - not used, 1 - call external dynamic library} +0 ! ZMQ_Mode - Fuse ZeroMQ interface {0: unused, 1: Yaw Control} + +!------- FILTERS ---------------------------------------------------------- +0.61500 ! F_LPFCornerFreq >>> Tunedvalue was: 0.31500 - Corner frequency (-3dB point) in the low-pass filters, [rad/s] +0.00000 ! F_LPFDamping - Damping coefficient {used only when F_FilterType = 2} [-] +0.00000 ! F_NotchCornerFreq - Natural frequency of the notch filter, [rad/s] +0.000000 0.250000 ! F_NotchBetaNumDen - Two notch damping values (numerator and denominator, resp) - determines the width and depth of the notch, [-] +0.62830 ! F_SSCornerFreq - Corner frequency (-3dB point) in the first order low pass filter for the setpoint smoother, [rad/s]. +0.20944 ! F_WECornerFreq - Corner frequency (-3dB point) in the first order low pass filter for the wind speed estimate [rad/s]. +0.17952 ! F_YawErr - Low pass filter corner frequency for yaw controller [rad/s]. +0.000000 1.000000 ! F_FlCornerFreq - Natural frequency and damping in the second order low pass filter of the tower-top fore-aft motion for floating feedback control [rad/s, -]. +0.01042 ! F_FlHighPassFreq - Natural frequency of first-order high-pass filter for nacelle fore-aft motion [rad/s]. +3.780000 1.000000 ! F_FlpCornerFreq - Corner frequency and damping in the second order low pass filter of the blade root bending moment for flap control [rad/s, -]. + +!------- BLADE PITCH CONTROL ---------------------------------------------- +30 ! PC_GS_n - Amount of gain-scheduling table entries +0.150511 0.170047 0.187567 0.203638 0.218616 0.232742 0.246187 0.258963 0.271200 0.283059 0.294514 0.305550 0.316371 0.326840 0.337077 0.347108 0.356849 0.366502 0.375849 0.385148 0.394189 0.403154 0.411886 0.420555 0.429027 0.437457 0.445707 0.453937 0.461993 0.470003 ! PC_GS_angles - Gain-schedule table: pitch angles [rad]. +-1.802655 -1.644720 -1.508878 -1.390797 -1.287208 -1.195596 -1.113999 -1.040858 -0.974925 -0.915183 -0.860801 -0.811087 -0.765465 -0.723450 -0.684631 -0.648656 -0.615223 -0.584073 -0.554979 -0.527745 -0.502197 -0.478183 -0.455570 -0.434238 -0.414082 -0.395006 -0.376927 -0.359768 -0.343460 -0.327942 ! PC_GS_KP - Gain-schedule table: pitch controller kp gains [s]. +-0.747085 -0.690899 -0.642574 -0.600566 -0.563714 -0.531124 -0.502095 -0.476076 -0.452620 -0.431367 -0.412020 -0.394335 -0.378105 -0.363158 -0.349348 -0.336550 -0.324656 -0.313574 -0.303224 -0.293536 -0.284447 -0.275904 -0.267860 -0.260271 -0.253100 -0.246314 -0.239882 -0.233778 -0.227977 -0.222456 ! PC_GS_KI - Gain-schedule table: pitch controller ki gains [-]. +0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 ! PC_GS_KD - Gain-schedule table: pitch controller kd gains +0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 ! PC_GS_TF - Gain-schedule table: pitch controller tf gains (derivative filter) +1.570000000000 ! PC_MaxPit - Maximum physical pitch limit, [rad]. +0.036320000000 ! PC_MinPit - Minimum physical pitch limit, [rad]. +0.174500000000 ! PC_MaxRat - Maximum pitch rate (in absolute value) in pitch controller, [rad/s]. +-0.17450000000 ! PC_MinRat - Minimum pitch rate (in absolute value) in pitch controller, [rad/s]. +1.395100000000 ! PC_RefSpd - Desired (reference) HSS speed for pitch controller, [rad/s]. +0.036320000000 ! PC_FinePit - Record 5: Below-rated pitch angle set-point, [rad] +0.017450000000 ! PC_Switch - Angle above lowest minimum pitch angle for switch, [rad] + +!------- INDIVIDUAL PITCH CONTROL ----------------------------------------- +9.600000 12.000000 ! IPC_Vramp - Start and end wind speeds for cut-in ramp function. First entry: IPC inactive, second entry: IPC fully active. [m/s] +0.3 ! IPC_IntSat - Integrator saturation (maximum signal amplitude contribution to pitch from IPC), [rad] +0.000e+00 0.000e+00 ! IPC_KP - Proportional gain for the individual pitch controller: first parameter for 1P reductions, second for 2P reductions, [-] +0.000e+00 0.000e+00 ! IPC_KI - Integral gain for the individual pitch controller: first parameter for 1P reductions, second for 2P reductions, [-] +0.000000 0.000000 ! IPC_aziOffset - Phase offset added to the azimuth angle for the individual pitch controller, [rad]. +0.0 ! IPC_CornerFreqAct - Corner frequency of the first-order actuators model, to induce a phase lag in the IPC signal {0: Disable}, [rad/s] + +!------- VS TORQUE CONTROL ------------------------------------------------ +100.0000000000 ! VS_GenEff - Generator efficiency mechanical power -> electrical power, [should match the efficiency defined in the generator properties!], [%] +2580460.182070 ! VS_ArSatTq - Above rated generator torque PI control saturation, [Nm] +650000.0000000 ! VS_MaxRat >>> TunedValued:650000.0000000 - Maximum torque rate (in absolute value) in torque controller, [Nm/s]. +2838506.200270 ! VS_MaxTq - Maximum generator torque in Region 3 (HSS side), [Nm]. +0.000000000000 ! VS_MinTq - Minimum generator torque (HSS side), [Nm]. +0.680000000000 ! VS_MinOMSpd >>> TunedValue: - Minimum generator speed [rad/s] +1452489.929660 ! VS_Rgn2K - Generator torque constant in Region 2 (HSS side), [Nm/(rad/s)^2] +3600000.000000 ! VS_RtPwr - Wind turbine rated power [W] +2580460.182070 ! VS_RtTq - Rated torque, [Nm]. +1.395100000000 ! VS_RefSpd - Rated generator speed [rad/s] +1 ! VS_n - Number of generator PI torque controller gains +-6954512.69764 ! VS_KP - Proportional gain for generator PI torque controller [-]. (Only used in the transitional 2.5 region if VS_ControlMode =/ 2) +-781263.090000 ! VS_KI - Integral gain for generator PI torque controller [s]. (Only used in the transitional 2.5 region if VS_ControlMode =/ 2) +9.595 ! VS_TSRopt >>> TunedValue:8.82 - Power-maximizing region 2 tip-speed-ratio [rad]. + +!------- SETPOINT SMOOTHER --------------------------------------------- +1.00000 ! SS_VSGain - Variable speed torque controller setpoint smoother gain, [-]. +0.00100 ! SS_PCGain - Collective pitch controller setpoint smoother gain, [-]. + +!------- POWER REFERENCE TRACKING -------------------------------------- +45 ! PRC_n - Number of elements in PRC_WindSpeeds and PRC_RotorSpeeds array +3.0 3.5 4.0 4.5 5.0 5.5 6.0 6.5 7.0 7.5 8.0 8.5 9.0 9.5 10.0 10.5 11.0 11.5 12.0 12.5 13.0 13.5 14.0 14.5 15.0 15.5 16.0 16.5 17.0 17.5 18.0 18.5 19.0 19.5 20.0 20.5 21.0 21.5 22.0 22.5 23.0 23.5 24.0 24.5 25.0 ! PRC_WindSpeeds - Array of wind speeds used in rotor speed vs. wind speed lookup table [m/s]. +0.6910268144787133 0.6960401593650649 0.7165597438704334 0.738185676421178 0.7608053103653986 0.7776758882514259 0.9275392403488596 0.9519480597035652 1.0186573938021806 1.0857272198939956 1.1535923474943748 1.205354257912827 1.2447250825053235 1.265512130479433 1.2731046664774792 1.2768229123691115 1.3035008107787789 1.3644268867738227 1.3950661379469709 1.3949948761209916 1.394923614295013 1.3948523524690342 1.394781090643055 1.3947098288170765 1.394638566991098 1.394567305165119 1.39449604333914 1.3944247815131616 1.3943535196871826 1.3942822578612035 1.3942109960352247 1.3941397342092463 1.3940684723832677 1.3939972105572886 1.3939259487313096 1.3938546869053312 1.3937834250793522 1.3937121632533735 1.3936409014273947 1.275861750264016 1.2764425597166416 1.2770233691692672 1.2776041786218928 1.2781849880745186 1.2787657975271445 ! PRC_RotorSpeeds - Array of rotor speeds corresponding to PRC_WindSpeeds [rad/s]. + +!------- WIND SPEED ESTIMATOR --------------------------------------------- +65.085 ! WE_BladeRadius - Blade length (distance from hub center to blade tip), [m] +1 ! WE_CP_n - Amount of parameters in the Cp array +0.0 ! WE_CP - Parameters that define the parameterized CP(lambda) function +0.0 ! WE_Gamma - Adaption gain of the wind speed estimator algorithm [m/rad] +1.0 ! WE_GearboxRatio - Gearbox ratio [>=1], [-] +34722804.00000 ! WE_Jtot - Total drivetrain inertia, including blades, hub and casted generator inertia to LSS, [kg m^2] +1.225 ! WE_RhoAir - Air density, [kg m^-3] +"SWT-3p6-130_Cp_Ct_Cq.txt" ! PerfFileName - File containing rotor performance tables (Cp,Ct,Cq) (absolute path or relative to this file) +36 26 ! PerfTableSize - Size of rotor performance tables, first number refers to number of blade pitch angles, second number referse to number of tip-speed ratios +60 ! WE_FOPoles_N - Number of first-order system poles used in EKF +3.0000 3.3103 3.6207 3.9310 4.2414 4.5517 4.8621 5.1724 5.4828 5.7931 6.1034 6.4138 6.7241 7.0345 7.3448 7.6552 7.9655 8.2759 8.5862 8.8966 9.2069 9.5172 9.8276 10.1379 10.4483 10.7586 11.0690 11.3793 11.6897 12.0000 12.4333 12.8667 13.3000 13.7333 14.1667 14.6000 15.0333 15.4667 15.9000 16.3333 16.7667 17.2000 17.6333 18.0667 18.5000 18.9333 19.3667 19.8000 20.2333 20.6667 21.1000 21.5333 21.9667 22.4000 22.8333 23.2667 23.7000 24.1333 24.5667 25.0000 ! WE_FOPoles_v - Wind speeds corresponding to first-order system poles [m/s] +-0.01675237 -0.01848537 -0.02021837 -0.02195138 -0.02368438 -0.02541738 -0.02715039 -0.02888339 -0.03061639 -0.03234940 -0.03408240 -0.03581540 -0.03754841 -0.03928141 -0.04101441 -0.04274742 -0.04448042 -0.04621342 -0.04794643 -0.04967943 -0.05141243 -0.05314544 -0.05487844 -0.05661144 -0.05572739 -0.05017938 -0.04026019 -0.02871709 -0.00971339 -0.04067865 0.00818207 0.00211342 -0.00482802 -0.01262516 -0.02096731 -0.02974463 -0.03916053 -0.04874708 -0.05875276 -0.06931562 -0.07999290 -0.09081923 -0.10219998 -0.11405921 -0.12562480 -0.13746588 -0.14961465 -0.16249907 -0.17543461 -0.18782154 -0.20027790 -0.21343756 -0.22678626 -0.24101883 -0.25473108 -0.26767005 -0.28052026 -0.29438557 -0.30826452 -0.32314303 ! WE_FOPoles - First order system poles [1/s] + +!------- YAW CONTROL ------------------------------------------------------ +0.00000 ! Y_uSwitch - Wind speed to switch between Y_ErrThresh. If zero, only the second value of Y_ErrThresh is used [m/s] +4.000000 8.000000 ! Y_ErrThresh - Yaw error threshold/deadbands. Turbine begins to yaw when it passes this. If Y_uSwitch is zero, only the second value is used. [deg]. +0.00870 ! Y_Rate - Yaw rate [rad/s] +0.00000 ! Y_MErrSet - Integrator saturation (maximum signal amplitude contribution to pitch from yaw-by-IPC), [rad] +0.00000 ! Y_IPC_IntSat - Integrator saturation (maximum signal amplitude contribution to pitch from yaw-by-IPC), [rad] +0.00000 ! Y_IPC_KP - Yaw-by-IPC proportional controller gain Kp +0.00000 ! Y_IPC_KI - Yaw-by-IPC integral controller gain Ki + +!------- TOWER FORE-AFT DAMPING ------------------------------------------- +-1.00000 ! FA_KI - Integral gain for the fore-aft tower damper controller [rad s/m] +0.0 ! FA_HPFCornerFreq - Corner frequency (-3dB point) in the high-pass filter on the fore-aft acceleration signal [rad/s] +0.0 ! FA_IntSat - Integrator saturation (maximum signal amplitude contribution to pitch from FA damper), [rad] + +!------- MINIMUM PITCH SATURATION ------------------------------------------- +45 ! PS_BldPitchMin_N - Number of values in minimum blade pitch lookup table (should equal number of values in PS_WindSpeeds and PS_BldPitchMin) +3.0 3.5 4.0 4.5 5.0 5.5 6.0 6.5 7.0 7.5 8.0 8.5 9.0 9.5 10.0 10.5 11.0 11.5 12.0 12.5 13.0 13.5 14.0 14.5 15.0 15.5 16.0 16.5 17.0 17.5 18.0 18.5 19.0 19.5 20.0 20.5 21.0 21.5 22.0 22.5 23.0 23.5 24.0 24.5 25.0 ! PS_WindSpeeds - Wind speeds corresponding to minimum blade pitch angles [m/s] +0.03433429157815764 0.031810319257858856 0.0290411053542571 0.026159059732509628 0.023197536519278562 0.02023601330604749 0.016539572859770497 0.012812807649260424 0.009178197567017179 0.0055435874847739285 0.0019089774025306792 0.00035367821989023234 -0.0001847841217967057 -0.0006880495172071447 0.0011581050975086446 0.07313877536508506 0.10080437650314836 0.12350678083692652 0.143559308829277 0.16319259532494113 0.18282588182060522 0.19751809368049394 0.2121360790842221 0.2267540644879502 0.24148455945770236 0.2562873503246761 0.27109014119164987 0.2851632439462169 0.2986736364561117 0.3121840289660065 0.3256944214759013 0.337480946474891 0.3490998890057371 0.360718831536583 0.37233777406742913 0.3839567165982751 0.39557565912912107 0.40673217388403315 0.41782455324483364 0.42891693260563396 0.44000931196643445 0.45110169132723493 0.4621940706880354 0.47314377493350046 0.483632110622263 ! PS_BldPitchMin - Minimum blade pitch angles [rad] + +!------- SHUTDOWN ----------------------------------------------------------- +0.436300000000 ! SD_MaxPit - Maximum blade pitch angle to initiate shutdown, [rad] +0.418880000000 ! SD_CornerFreq - Cutoff Frequency for first order low-pass filter for blade pitch angle, [rad/s] + +!------- Floating ----------------------------------------------------------- +0.000000000000 ! Fl_Kp - Nacelle velocity proportional feedback gain [s] + +!------- FLAP ACTUATION ----------------------------------------------------- +0.000000000000 ! Flp_Angle - Initial or steady state flap angle [rad] +0.00000000e+00 ! Flp_Kp - Blade root bending moment proportional gain for flap control [s] +0.00000000e+00 ! Flp_Ki - Flap displacement integral gain for flap control [-] +0.174500000000 ! Flp_MaxPit - Maximum (and minimum) flap pitch angle [rad] + +!------- Open Loop Control ----------------------------------------------------- +"unused" ! OL_Filename - Input file with open loop timeseries (absolute path or relative to this file) +0 ! Ind_Breakpoint - The column in OL_Filename that contains the breakpoint (time if OL_Mode = 1) +0 ! Ind_BldPitch - The column in OL_Filename that contains the blade pitch input in rad +0 ! Ind_GenTq - The column in OL_Filename that contains the generator torque in Nm +0 ! Ind_YawRate - The column in OL_Filename that contains the generator torque in Nm + +!------- Pitch Actuator Model ----------------------------------------------------- +3.140000000000 ! PA_CornerFreq - Pitch actuator bandwidth/cut-off frequency [rad/s] +0.707000000000 ! PA_Damping - Pitch actuator damping ratio [-, unused if PA_Mode = 1] + +!------- External Controller Interface ----------------------------------------------------- +"unused" ! DLL_FileName - Name/location of the dynamic library in the Bladed-DLL format +"unused" ! DLL_InFile - Name of input file sent to the DLL (-) +"DISCON" ! DLL_ProcName - Name of procedure in DLL to be called (-) + +!------- ZeroMQ Interface --------------------------------------------------------- +"tcp://localhost:5555" ! ZMQ_CommAddress - Communication address for ZMQ server, (e.g. "tcp://localhost:5555") +2 ! ZMQ_UpdatePeriod - Call ZeroMQ every [x] seconds, [s] diff --git a/pyFAST/input_output/tests/example_files/RoscoPerformance_CpCtCq.txt b/pyFAST/input_output/tests/example_files/RoscoPerformance_CpCtCq.txt new file mode 100644 index 0000000..fecaae6 --- /dev/null +++ b/pyFAST/input_output/tests/example_files/RoscoPerformance_CpCtCq.txt @@ -0,0 +1,29 @@ +# ----- Rotor performance tables for the OpenFAST_5MW wind turbine ----- +# ------------ Written on Oct-30-19 using the ROSCO toolbox ------------ + +# Pitch angle vector - x axis (matrix columns) (deg) +-1.0 -0.5 0.0 0.5 1.0 +# TSR vector - y axis (matrix rows) (-) +3.0 3.5 4.0 +# Wind speed vector - z axis (m/s) +11.4 + +# Power coefficient + +0.089542 0.094392 0.099145 0.103788 0.108307 +0.141508 0.147502 0.153253 0.158739 0.163935 +0.201243 0.207940 0.214159 0.219868 0.225035 + + +# Thrust coefficient + +0.230643 0.231358 0.231931 0.232351 0.232612 +0.296203 0.296020 0.295605 0.294960 0.294088 +0.365790 0.364336 0.362614 0.360628 0.358382 + + +# Torque coefficient + +0.029876 0.031494 0.033080 0.034629 0.036137 +0.040469 0.042184 0.043828 0.045397 0.046883 +0.050359 0.052034 0.053591 0.055019 0.056312 diff --git a/pyFAST/input_output/tests/example_files/TDMS_1Grp2Chan_TimeTrack.tdms b/pyFAST/input_output/tests/example_files/TDMS_1Grp2Chan_TimeTrack.tdms new file mode 100644 index 0000000000000000000000000000000000000000..b09be010028a6c37af380da97c19b6bb4a224bc1 GIT binary patch literal 560 zcmWG>3C`tXU|`4)Vqg$q0y2Q$3WQ<+aRq@`Up*x?EwMDGL|tFqIX}lyT?8n|1;mU% z3}S--NHGr(m#4*N<|P-U=BDPAfYfNLU%033(<*y@h+J_=Vo^zaep*^_Dp(pQ3I?xq)HHfR5g^NSWw zz43DYj`CyQ!Z|Ue%jI7``N?WPg^m6wzsRDHmA4f`+oijC#8+Np4&V3 k$(}7={L9|AQsVo{mQD74V)e>PTe|j7C}NiN*!0aF08ixAWB>pF literal 0 HcmV?d00001 diff --git a/pyFAST/input_output/tests/example_files/TDMS_2Grp2Chan.tdms b/pyFAST/input_output/tests/example_files/TDMS_2Grp2Chan.tdms new file mode 100644 index 0000000000000000000000000000000000000000..cf9b2e919aefb324943c3908cd03d889680d9451 GIT binary patch literal 1019 zcmWG>3C`tXU|`4)Vqmz>3}gVoeh9_D0;Gk3SYO?}D8ID8QC(l%IX@>PGdERT1Sre} z#Ed`;V#9y{kb$b)5vN)Sgjy$r(ZwZ+xdmW7EJ~cf8<)5+#@upnA02RMt50;*C9V&mz9xNSj5325geGb^5M^N=o>;u3CK85Og zZf`c_I#B2(RR1e`sZ-fN9dDrKyt9Y6>w~?p&8N$E+`sO3{I_a9m-5s7H?{_9wV!-v z&)i_HvZM9V{#NGZtMR-a_J87Beq(j(tNrRV4vDK>F7NNFyS(7U~2_UqQ$Cw)8gVgJDgOiB(yYzO!fw=Usa^K5@I z!!+Bc55Deyb8EKU@kO8aXNp=E%XB`ofBNRI^!XQ0_e(u;VfMay4VDTR7*2H`hlM4G zZjkOp;WOm+q3|2_^`r0^1SX>J8xkj>@EJBwM&UQGOhw@{giS-?H!Poy!e{t71BKt< zH4BB$FlROjzv1m16h4FfJQRMzg!w3ZhDQrf_zlL3Q1}e3i&6Lu*Oq|!%yt}!PYb^7 zXI0~o|EKoQewDkJ=;4n~>`z!;kj~w4%l=5F*6pW>ANN1(X(%d~@YR0a+J%3eA6(y` zwVi)~(cc^PyBmJ2H=Xg&{!zs$?C&bK|6;#W zwm&c}rcRbwh*|qhJGS63gw0JGBzoC)C(sRyN?BklF KWft-5w+8^)7CplN literal 0 HcmV?d00001 diff --git a/pyFAST/input_output/tests/example_files/TecplotASCII_1.dat b/pyFAST/input_output/tests/example_files/TecplotASCII_1.dat new file mode 100644 index 0000000..3bc428d --- /dev/null +++ b/pyFAST/input_output/tests/example_files/TecplotASCII_1.dat @@ -0,0 +1,11 @@ +VARIABLES = "Alpha_[deg]","Cl_[-]","Cd_[-]" +-3.821594 -0.1068473 0.01848371 +-0.1351164 0.00076118 0.0133004 +7.051793 0.6396082 0.03828023 +18.14393 1.323551 0.1517825 +24.83678 1.788388 0.3538913 +29.01219 1.475058 0.2442263 +36.01752 1.786413 0.4773372 +37.48678 1.915456 0.7654659 +41.04442 1.728049 0.71557 +47.60305 1.444345 1.050172 diff --git a/pyFAST/input_output/tests/example_files/TecplotASCII_2.dat b/pyFAST/input_output/tests/example_files/TecplotASCII_2.dat new file mode 100644 index 0000000..b9b7faf --- /dev/null +++ b/pyFAST/input_output/tests/example_files/TecplotASCII_2.dat @@ -0,0 +1,28 @@ +TITLE = "accosurf-007 t= 15.5894" +VARIABLES = "CoordinateX" +"CoordinateY" +"CoordinateZ" +"u[m/s]" +"v[m/s]" +"w[m/s]" +DATASETAUXDATA NORM_m.s="0.28762452E+03 " +DATASETAUXDATA PATH="/lustre/cray/" +DATASETAUXDATA TIMESTAMP="01:35:32_19.02.2020" +DATASETAUXDATA TINF_K="0.28815000E+03 " +ZONE T="dom-7" + STRANDID=7, SOLUTIONTIME=15.5894049 + I=3, J=4, K=1, ZONETYPE=Ordered + DATAPACKING=POINT + DT=(SINGLE SINGLE SINGLE SINGLE SINGLE SINGLE ) + 0.000000000E+00 -1.000000000E+02 0.000000000E+00 6.422097965E-09 1.117181000E-11 -1.143066718E-11 + 0.000000000E+00 -9.600000000E+01 0.000000000E+00 6.424601295E-09 8.401054158E-12 -1.158505930E-11 + 0.000000000E+00 -9.200000000E+01 0.000000000E+00 6.426116972E-09 6.519663281E-12 -1.169956233E-11 + 0.000000000E+00 -8.800000000E+01 0.000000000E+00 6.426900789E-09 5.326627594E-12 -1.180718977E-11 + 0.000000000E+00 -8.400000000E+01 0.000000000E+00 6.427312460E-09 5.249155711E-12 -1.310005490E-11 + 0.000000000E+00 -8.000000000E+01 0.000000000E+00 1.353533626E+00 -8.091490017E-04 -2.368479269E-03 + 0.000000000E+00 -7.600000000E+01 0.000000000E+00 2.526548393E-08 1.098948450E-11 -4.482937632E-11 + 0.000000000E+00 -7.200000000E+01 0.000000000E+00 2.526406639E-08 9.429169251E-12 -4.497692149E-11 + 0.000000000E+00 -6.800000000E+01 0.000000000E+00 2.526252274E-08 7.517836180E-12 -4.499923351E-11 + 0.000000000E+00 -6.400000000E+01 0.000000000E+00 2.526164522E-08 5.834952747E-12 -4.466245082E-11 + 0.000000000E+00 -6.000000000E+01 0.000000000E+00 2.525990261E-08 4.354900121E-12 -4.505384260E-11 + 0.000000000E+00 -5.600000000E+01 0.000000000E+00 2.525895049E-08 3.207297926E-12 -4.506787304E-11 diff --git a/pyFAST/input_output/tests/example_files/TurbSimTS.txt b/pyFAST/input_output/tests/example_files/TurbSimTS.txt new file mode 100644 index 0000000..594d650 --- /dev/null +++ b/pyFAST/input_output/tests/example_files/TurbSimTS.txt @@ -0,0 +1,17 @@ +--------------TurbSim v2.00.* User Time Series Input File----------------------- +Comment +-------------------------------------------------------------------------------- + 3 nComp - Number of velocity components in the file + 1 nPoints - Number of time series points contained in this file (-) + 1 RefPtID - Index of the reference point (1-nPoints) + Pointyi Pointzi ! nPoints listed in order of increasing height + (m) (m) + 0.00 80.00 +--------Time Series------------------------------------------------------------- +Elapsed Time Point01u Point01v Point01w + (s) (m/s) (m/s) (m/s) +0.00 9.373997257 -0.863171203 -1.009559755 +0.01 9.291859275 -0.787680428 -1.030878636 +0.02 9.267038696 -0.870045789 -1.090877688 +0.03 9.404202705 -0.903012754 -1.009028963 +0.04 9.339542454 -0.751390609 -1.02042094 diff --git a/pyFAST/input_output/tests/test_csv.py b/pyFAST/input_output/tests/test_csv.py index 7f0f66d..7170a17 100644 --- a/pyFAST/input_output/tests/test_csv.py +++ b/pyFAST/input_output/tests/test_csv.py @@ -1,10 +1,7 @@ import unittest import os import numpy as np - -from .helpers_for_test import MyDir, reading_test - -import pyFAST +from pyFAST.input_output.tests.helpers_for_test import MyDir, reading_test from pyFAST.input_output import CSVFile class Test(unittest.TestCase): diff --git a/pyFAST/input_output/tests/test_fast_input.py b/pyFAST/input_output/tests/test_fast_input.py index d1beab2..731104f 100644 --- a/pyFAST/input_output/tests/test_fast_input.py +++ b/pyFAST/input_output/tests/test_fast_input.py @@ -1,12 +1,12 @@ import unittest import os import numpy as np - from pyFAST.input_output.tests.helpers_for_test import MyDir, reading_test - -import pyFAST -from pyFAST.input_output import FASTInputFile -from pyFAST.input_output.fast_wind_file import FASTWndFile +from pyFAST.input_output.fast_input_file import FASTInputFile +from pyFAST.input_output.fast_input_file import ExtPtfmFile +from pyFAST.input_output.fast_input_file import ADPolarFile +from pyFAST.input_output.fast_input_file import EDBladeFile +from pyFAST.input_output.fast_wind_file import FASTWndFile class Test(unittest.TestCase): @@ -18,16 +18,16 @@ def test_FASTIn(self): F=FASTInputFile(os.path.join(MyDir,'FASTIn_BD.dat')) F.test_ascii(bCompareWritesOnly=True,bDelete=True) self.assertEqual(F['PitchK'],2.0e+07) - self.assertEqual(F['MemberGeom'][-1,2],61.5) - self.assertEqual(F['MemberGeom'][-2,3],0.023000) + self.assertAlmostEqual(F['MemberGeom'][-1,2],61.5) + self.assertAlmostEqual(F['MemberGeom'][-2,3],0.023000) F=FASTInputFile(os.path.join(MyDir,'FASTIn_BD_bld.dat')) F.test_ascii(bCompareWritesOnly=False,bDelete=True) self.assertEqual(F['DampingCoeffs'][0][0],0.01) # TODO BeamDyn Blade properties are not really "user friendly" - self.assertEqual(F['BeamProperties']['span'][1],1.0) - self.assertEqual(F['BeamProperties']['K'][1][0,0],1.8e+08) # K11 @ section 2 - self.assertEqual(F['BeamProperties']['M'][1][0,0],1.2) # M11 @ section 2 + self.assertAlmostEqual(F['BeamProperties']['span'][1],1.0) + self.assertAlmostEqual(F['BeamProperties']['K'][1][0,0],1.8e+08) # K11 @ section 2 + self.assertAlmostEqual(F['BeamProperties']['M'][1][0,0],1.2) # M11 @ section 2 F=FASTInputFile(os.path.join(MyDir,'FASTIn_ED.dat')) F.test_ascii(bCompareWritesOnly=True,bDelete=True) @@ -36,6 +36,7 @@ def test_FASTIn(self): F=FASTInputFile(os.path.join(MyDir,'FASTIn_ED_bld.dat')) F.test_ascii(bCompareWritesOnly=True,bDelete=True) self.assertEqual(F['BldEdgSh(6)'],-0.6952) + F.comment = 'ElastoDyn file' F=FASTInputFile(os.path.join(MyDir,'FASTIn_ED_twr.dat')) F.test_ascii(bCompareWritesOnly=True,bDelete=True) @@ -51,11 +52,11 @@ def test_FASTIn(self): F=FASTInputFile(os.path.join(MyDir,'FASTIn_HD.dat')) #F.test_ascii(bCompareWritesOnly=True,bDelete=True) # TODO - self.assertEqual(F['RdtnDT'],0.0125) + self.assertAlmostEqual(F['RdtnDT'],0.0125) F=FASTInputFile(os.path.join(MyDir,'FASTIn_IF_NoHead.dat')) F.test_ascii(bCompareWritesOnly=True,bDelete=True) - self.assertEqual(F['Z0'],0.03) + self.assertAlmostEqual(F['Z0'],0.03) F=FASTInputFile(os.path.join(MyDir,'FASTIn_SbD.dat')) F.test_ascii(bCompareWritesOnly=True,bDelete=True) @@ -67,6 +68,81 @@ def test_FASTIn(self): F.test_ascii(bCompareWritesOnly=True,bDelete=True) self.assertEqual(F['PitManRat(1)'],2) + def test_FASTADBld(self): + F=FASTInputFile(os.path.join(MyDir,'FASTIn_AD15_bld.dat')) + F.test_ascii(bCompareWritesOnly=True,bDelete=True) + self.assertTrue('NumBlNds' in F.keys()) + df = F.toDataFrame() + self.assertEqual(df['BlChord_[m]'].values[-1], 1.419) + self.assertTrue('c2_Swp_Approx_[m]' in df.keys()) +# import matplotlib.pyplot as plt +# fig,axes = plt.subplots(1, 3, sharey=False, figsize=(10.4,4.8)) # (6.4,4.8) +# fig.subplots_adjust(left=0.12, right=0.95, top=0.95, bottom=0.11, hspace=0.20, wspace=0.20) +# ax= axes[0] +# ax.plot( df['BlSpn_[m]'], df['BlCrvAC_[m]'], 'k-' , label='Prebend AC') +# #ax.plot( df['BlSpn_[m]'], df['x'], '--' , label='x') +# ax.plot( df['BlSpn_[m]'], df['c2_Crv_Approx_[m]'], ':' , label='c2') +# ax.legend() +# ax= axes[1] +# ax.plot( df['BlSpn_[m]'], df['BlSwpAC_[m]'], 'k-' , label='Sweep AC') +# #ax.plot( df['BlSpn_[m]'], df['y'], '--' , label='y') +# ax.plot( df['BlSpn_[m]'], df['c2_Swp_Approx_[m]'], ':' , label='c2') +# ax.legend() +# ax= axes[2] +# ax.plot( df['BlSpn_[m]'], df['AC_Approx_[-]'], '-' , label='AC') +# ax.set_xlabel('') +# ax.set_ylabel('') +# ax.legend() +# plt.show() + + def test_FASTADPol(self): + #F=FASTInputFile(os.path.join(MyDir,'FASTIn_arf_coords.txt')) + #print(F.keys()) + F=FASTInputFile(os.path.join(MyDir,'FASTIn_AD15_arfl.dat')) + df = F.toDataFrame() + self.assertTrue('Cn_pot_[-]' in df.keys()) + # --- Test Dedicated code + F = ADPolarFile() + F.read(os.path.join(MyDir,'FASTIn_AD15_arfl.dat')) + + def test_FASTADPolMulti(self): + F=FASTInputFile(os.path.join(MyDir,'FASTIn_AD15_arf_multitabs.dat')) + F.test_ascii(bCompareWritesOnly=False,bDelete=True) + + dfs = F.toDataFrame() + self.assertTrue('AFCoeff_2' in dfs.keys()) + + df1 = dfs['AFCoeff_1'] + df2 = dfs['AFCoeff_2'] + self.assertTrue('Cn_pot_[-]' in df2.keys()) + + self.assertEqual(df1.shape[0],23) + self.assertEqual(df2.shape[0],24) + + F = ADPolarFile(numTabs=2) + F.write('_DUMMY') + + def test_FASTEDBld(self): + F=FASTInputFile(os.path.join(MyDir,'FASTIn_ED_bld.dat')) + F.test_ascii(bCompareWritesOnly=True, bDelete=True) + self.assertEqual(F['BldEdgSh(6)'],-0.6952) + df = F.toDataFrame() + self.assertAlmostEqual(df['ShapeFlap1_[-]'].values[40],0.8530735996) + # --- Test Dedicated code + F = EDBladeFile() + F.read(os.path.join(MyDir,'FASTIn_ED_bld.dat')) + + def test_FASTExt(self): + F=FASTInputFile(os.path.join(MyDir,'FASTIn_ExtPtfm_SubSef.dat')) + F.test_ascii(bCompareWritesOnly=False, bDelete=True) + self.assertEqual(F['StiffnessMatrix'][2,2],1.96653266e+09) + # --- Test Dedicated code + F = ExtPtfmFile() + F.read(os.path.join(MyDir,'FASTIn_ExtPtfm_SubSef.dat')) + F.test_ascii(bCompareWritesOnly=False, bDelete=True) + df=F.toDataFrame() + self.assertAlmostEqual(df['InpF_Fx_[N]'].values[-1], 1660.749680) + def test_FASTWnd(self): F=FASTWndFile(os.path.join(MyDir,'FASTWnd.wnd')) F.test_ascii(bCompareWritesOnly=True,bDelete=True) @@ -112,8 +188,10 @@ def test_FASTInAirfoil(self): self.assertEqual(F['AFCoeff'].shape, (30,4)) if __name__ == '__main__': - from welib.tools.clean_exceptions import * + #Test().test_FASTEDBld() + #Test().test_FASTADBld() + #Test().test_FASTADPol() + #Test().test_FASTADPolMulti() + #Test().test_FASTExt() #Test().test_FASTIn() - #Test().test_FASTInAirfoil() - #Test().test_FASTInMoorDyn() unittest.main() diff --git a/pyFAST/input_output/tests/test_fast_input_deck.py b/pyFAST/input_output/tests/test_fast_input_deck.py index 337ed1a..60e2439 100644 --- a/pyFAST/input_output/tests/test_fast_input_deck.py +++ b/pyFAST/input_output/tests/test_fast_input_deck.py @@ -1,8 +1,7 @@ import unittest import os import numpy as np -from .helpers_for_test import MyDir, reading_test - +from pyFAST.input_output.tests.helpers_for_test import MyDir, reading_test from pyFAST.input_output.fast_input_deck import FASTInputDeck class Test(unittest.TestCase): diff --git a/pyFAST/input_output/tests/test_fast_linearization.py b/pyFAST/input_output/tests/test_fast_linearization.py index 2282ef6..106cbf7 100644 --- a/pyFAST/input_output/tests/test_fast_linearization.py +++ b/pyFAST/input_output/tests/test_fast_linearization.py @@ -1,8 +1,7 @@ import unittest import os import numpy as np -from .helpers_for_test import MyDir, reading_test -import pyFAST +from pyFAST.input_output.tests.helpers_for_test import MyDir, reading_test from pyFAST.input_output import FASTLinearizationFile class Test(unittest.TestCase): diff --git a/pyFAST/input_output/tests/test_fast_output.py b/pyFAST/input_output/tests/test_fast_output.py index c786643..81b94ed 100644 --- a/pyFAST/input_output/tests/test_fast_output.py +++ b/pyFAST/input_output/tests/test_fast_output.py @@ -1,7 +1,6 @@ import unittest import os import numpy as np -import pyFAST from pyFAST.input_output.tests.helpers_for_test import MyDir, reading_test from pyFAST.input_output import FASTOutputFile diff --git a/pyFAST/input_output/tests/test_fast_summary.py b/pyFAST/input_output/tests/test_fast_summary.py index 67904c0..803fa30 100644 --- a/pyFAST/input_output/tests/test_fast_summary.py +++ b/pyFAST/input_output/tests/test_fast_summary.py @@ -1,9 +1,7 @@ import unittest import os import numpy as np - -from .helpers_for_test import MyDir, reading_test -import pyFAST +from pyFAST.input_output.tests.helpers_for_test import MyDir, reading_test from pyFAST.input_output import FASTSummaryFile class Test(unittest.TestCase): diff --git a/pyFAST/input_output/tests/test_hawc.py b/pyFAST/input_output/tests/test_hawc.py index 6e476c7..3c98772 100644 --- a/pyFAST/input_output/tests/test_hawc.py +++ b/pyFAST/input_output/tests/test_hawc.py @@ -1,12 +1,8 @@ import unittest import os import numpy as np -try: - from .helpers_for_test import MyDir, reading_test -except ImportError: - from helpers_for_test import MyDir, reading_test - -import pyFAST +import pyFAST.input_output as weio +from pyFAST.input_output.tests.helpers_for_test import MyDir, reading_test from pyFAST.input_output.hawc2_dat_file import HAWC2DatFile from pyFAST.input_output.hawc2_ae_file import HAWC2AEFile from pyFAST.input_output.hawc2_pc_file import HAWC2PCFile @@ -14,10 +10,11 @@ from pyFAST.input_output.hawcstab2_ind_file import HAWCStab2IndFile from pyFAST.input_output.hawcstab2_pwr_file import HAWCStab2PwrFile + class Test(unittest.TestCase): - #def test_001_read_all(self): - # reading_test('HAWC*.*', weio.read) + def test_001_read_all(self): + reading_test('HAWC*.*', weio.read) def DF(self,FN): """ Reads a file with weio and return a dataframe """ diff --git a/pyFAST/input_output/tests/test_mannbox.py b/pyFAST/input_output/tests/test_mannbox.py index a72895b..f8a6712 100644 --- a/pyFAST/input_output/tests/test_mannbox.py +++ b/pyFAST/input_output/tests/test_mannbox.py @@ -1,8 +1,9 @@ import unittest import os import numpy as np -from pyFAST.input_output.mannbox_file import MannBoxFile from pyFAST.input_output.tests.helpers_for_test import MyDir, reading_test +from pyFAST.input_output.mannbox_file import MannBoxFile + class Test(unittest.TestCase): diff --git a/pyFAST/input_output/tests/test_parquet.py b/pyFAST/input_output/tests/test_parquet.py new file mode 100644 index 0000000..42d4428 --- /dev/null +++ b/pyFAST/input_output/tests/test_parquet.py @@ -0,0 +1,26 @@ +import unittest +import os +from pyFAST.input_output.tests.helpers_for_test import MyDir, reading_test +from pyFAST.input_output.parquet_file import ParquetFile + +class Test(unittest.TestCase): + + def test_001_read_all(self, DEBUG=True): + reading_test('ParquetFile*.*', ParquetFile) + + def DF(self,FN): + """ Reads a file and return a dataframe """ + return ParquetFile(os.path.join(MyDir,FN)).toDataFrame() + + def test_ParquetFile(self): + df=self.DF('ParquetFile_test.parquet') + self.assertListEqual(list(df.columns),["Column1","Column 2","Column Str"]) + self.assertEqual(df.shape,(3,3)) + self.assertEqual(df.loc[0,"Column Str"],'abc') + self.assertEqual(df.loc[0, "Column1"], 1) + + + +if __name__ == '__main__': +# Test().test_000_debug() + unittest.main() diff --git a/pyFAST/input_output/tests/test_turbsim.py b/pyFAST/input_output/tests/test_turbsim.py index ced7744..897f3c3 100644 --- a/pyFAST/input_output/tests/test_turbsim.py +++ b/pyFAST/input_output/tests/test_turbsim.py @@ -1,11 +1,7 @@ import unittest import os import numpy as np -try: - from .helpers_for_test import MyDir, reading_test -except ImportError: - from helpers_for_test import MyDir, reading_test -import pyFAST +from pyFAST.input_output.tests.helpers_for_test import MyDir, reading_test from pyFAST.input_output import TurbSimFile class Test(unittest.TestCase): diff --git a/pyFAST/input_output/tests/test_vtk.py b/pyFAST/input_output/tests/test_vtk.py index 97c7ff5..7b52f71 100644 --- a/pyFAST/input_output/tests/test_vtk.py +++ b/pyFAST/input_output/tests/test_vtk.py @@ -1,11 +1,7 @@ import unittest import os import numpy as np -try: - from .helpers_for_test import MyDir, reading_test -except ImportError: - from helpers_for_test import MyDir, reading_test - +from pyFAST.input_output.tests.helpers_for_test import MyDir, reading_test from pyFAST.input_output.vtk_file import VTKFile diff --git a/pyFAST/input_output/turbsim_ts_file.py b/pyFAST/input_output/turbsim_ts_file.py new file mode 100644 index 0000000..85be93b --- /dev/null +++ b/pyFAST/input_output/turbsim_ts_file.py @@ -0,0 +1,102 @@ +from itertools import takewhile +import pandas as pd +import numpy as np +try: + from .file import File, WrongFormatError, BrokenFormatError +except: + File = dict + class WrongFormatError(Exception): pass + class BrokenFormatError(Exception): pass + + +class TurbSimTSFile(File): + + @staticmethod + def defaultExtensions(): + return ['.txt'] + + @staticmethod + def formatName(): + return 'TurbSim time series' + + def _read(self, *args, **kwargs): + self['header']=[] + nHeaderMax=10 + # Reading + iFirstData=-1 + with open(self.filename, 'r', errors="surrogateescape") as f: + for i, line in enumerate(f): + if i>nHeaderMax: + raise BrokenFormatError('`nComp` not found in file') + if line.lower().find('ncomp')>=0: + iFirstData=i + break + self['header'].append(line.strip()) + self['nComp'] = int(line.split()[0]) + line = f.readline().strip() + nPoints = int(line.split()[0]) + line = f.readline().strip() + self['ID'] = int(line.split()[0]) + f.readline() + f.readline() + self['Points']=np.zeros((nPoints,2)) + for i in np.arange(nPoints): + line = f.readline().strip() + self['Points'][i,:]= np.array(line.split()).astype(float) + f.readline() + f.readline() + f.readline() + lines=[] + # reading full data + self['data'] = np.array([l.strip().split() for l in takewhile(lambda x: len(x.strip())>0, f.readlines())]).astype(float) + + def columns(self): + Comp=['u','v','w'] + return ['Time']+['Point{}{}'.format(ip+1,Comp[ic]) for ic in np.arange(self['nComp']) for ip in np.arange(len(self['Points']))] + + def units(self): + nPoints = self['Points'].shape[0] + return ['(s)'] + ['(m/s)']*nPoints*self['nComp'] + + def toString(self): + + def toStringVLD(val,lab,descr): + val='{}'.format(val) + lab='{}'.format(lab) + if len(val)<13: + val='{:13s}'.format(val) + if len(lab)<13: + lab='{:13s}'.format(lab) + return val+' '+lab+' - '+descr.strip().strip('-')+'\n' + + s='\n'.join(self['header'])+'\n' + nPoints = self['Points'].shape[0] + s+=toStringVLD(self['nComp'],'nComp' ,'Number of velocity components in the file' ) + s+=toStringVLD(nPoints ,'nPoints','Number of time series points contained in this file(-)') + s+=toStringVLD(self['ID'] ,'RefPtID','Index of the reference point (1-nPoints)') + s+='{:^16s}{:^16s} {}\n'.format('Pointyi','Pointzi','! nPoints listed in order of increasing height') + s+='{:^16s}{:^16s}\n'.format('(m)','(m)') + for row in self['Points']: + s+=''.join(['{:16.8e}'.format(v) for v in row])+'\n' + + s+='--------Time Series-------------------------------------------------------------\n' + s+=''.join(['{:^16s}'.format(c) for c in self.columns()])+'\n' + s+=''.join(['{:^16s}'.format(c) for c in self.units()])+'\n' + s+='\n'.join(''.join('{:16.8e}'.format(x) for x in y) for y in self['data']) + return s + + def _write(self): + with open(self.filename,'w') as f: + f.write(self.toString()) + + + + def _toDataFrame(self): + Cols = ['{}_{}'.format(c.replace(' ','_'), u.replace('(','[').replace(')',']')) for c,u in zip(self.columns(),self.units())] + dfs={} + dfs['Points'] = pd.DataFrame(data = self['Points'],columns = ['PointYi','PointZi']) + dfs['TimeSeries'] = pd.DataFrame(data = self['data'] ,columns = Cols) + + return dfs + + diff --git a/pyFAST/postpro/examples/Example_RadialInterp.py b/pyFAST/postpro/examples/Example_RadialInterp.py index 6323011..6e1b832 100644 --- a/pyFAST/postpro/examples/Example_RadialInterp.py +++ b/pyFAST/postpro/examples/Example_RadialInterp.py @@ -9,7 +9,7 @@ import matplotlib.pyplot as plt import pyFAST.input_output as io -import pyFAST.postpro as postpro +import pyFAST.input_output.postpro as postpro def main(): # Get current directory so this script can be called from any location @@ -22,7 +22,19 @@ def main(): # --- Define output radial stations # Option 1 - Get all these locations automatically (recommended) fstFile = os.path.join(MyDir,'../../../data/NREL5MW/Main_Onshore.fst') # must correspond to the one used to generate outputs - r_AD, r_ED, r_BD, IR_AD, IR_ED, IR_BD, R, r_hub, fst = postpro.FASTRadialOutputs(fstFile, df.columns.values) + d = postpro.FASTSpanwiseOutputs(fstFile, df.columns.values) + r_AD = d['r_AD'] + r_ED_bld = d['r_ED_bld'] + r_ED_twr = d['r_ED_twr'] + r_BD = d['r_BD'] + IR_AD = d['IR_AD'] + IR_ED_bld = d['IR_ED_bld'] + IR_ED_twr = d['IR_ED_twr'] + IR_BD = d['IR_BD'] + TwrLen = d['TwrLen'] + R = d['R'] + r_hub = d['r_hub'] + fst = d['fst'] # Option 2 - Get ouputs locations for each module #r_ED_gag, IR_ED = ED_BldGag(fstFile) @@ -36,8 +48,8 @@ def main(): # NOTE: format need to be adjusted if you use AllOuts, or outputs at few nodes r = 60 # Radial location where outputs are to be interpolated Cl_interp = postpro.radialInterpTS(df, r, 'Cl_[-]', r_AD, bldFmt='AB{:d}', ndFmt='N{:03d}') - TDx_interp = postpro.radialInterpTS(df, r, 'TDx_[m]', r_ED, bldFmt='B{:d}' , ndFmt='N{:03d}') - #TDx_interp = postpro.radialInterpTS(df, r, 'TDx_[m]', r_ED, bldFmt='B{:d}' , ndFmt='N{d}') + TDx_interp = postpro.radialInterpTS(df, r, 'TDx_[m]', r_ED_bld, bldFmt='B{:d}' , ndFmt='N{:03d}') + #TDx_interp = postpro.radialInterpTS(df, r, 'TDx_[m]', r_ED_bld, bldFmt='B{:d}' , ndFmt='N{d}') # --- Plot fig,ax = plt.subplots(1, 1, sharey=False, figsize=(6.4,4.8)) # (6.4,4.8) diff --git a/pyFAST/postpro/examples/Example_RadialPostPro.py b/pyFAST/postpro/examples/Example_RadialPostPro.py index d16b4ad..32d1ba7 100644 --- a/pyFAST/postpro/examples/Example_RadialPostPro.py +++ b/pyFAST/postpro/examples/Example_RadialPostPro.py @@ -23,10 +23,11 @@ def main(): # Averaging here is done over 1 period (avgParam=1, avgMethod='periods') # To get the output radial stations, a .fst file is needed fstFile = os.path.join(scriptDir,'../../../data/NREL5MW/Main_Onshore.fst') - dfRad_ED, dfRad_AD, dfRad_BD = postpro.spanwisePostPro(FST_In=fstFile, avgMethod='periods', avgParam=1, df=df) + out = postpro.spanwisePostPro(FST_In=fstFile, avgMethod='periods', avgParam=1, df=df) + dfRad_ED=out['ED_bld']; dfRad_AD = out['AD']; dfRad_BD = out['BD'] # --- Step1&2 at once (when .outb and .fst are next to each other in same folder, with same name) - # dfRad_ED, dfRad_AD, dfRad_BD, df = postpro.spanwisePostPro(FST_In=fstFile, avgMethod='periods', avgParam=1, out_ext='.outb') + # out = postpro.spanwisePostPro(FST_In=fstFile, avgMethod='periods', avgParam=1, out_ext='.outb') # --- (Optional, compute time series average) # Averaging here is done over the last 100s (avgParam=100, avgMethod='constantwindow') diff --git a/pyFAST/postpro/examples/Example_Remap.py b/pyFAST/postpro/examples/Example_Remap.py index 881fe05..69a5047 100644 --- a/pyFAST/postpro/examples/Example_Remap.py +++ b/pyFAST/postpro/examples/Example_Remap.py @@ -7,7 +7,7 @@ import os # Local import pyFAST.input_output as io -import pyFAST.postpro as postpro +import pyFAST.input_output.postpro as postpro if __name__ == '__main__': ColumnMap={ diff --git a/pyFAST/postpro/postpro.py b/pyFAST/postpro/postpro.py index 4ca29c1..f398380 100644 --- a/pyFAST/postpro/postpro.py +++ b/pyFAST/postpro/postpro.py @@ -12,6 +12,24 @@ # --------------------------------------------------------------------------------} # --- Tools for IO # --------------------------------------------------------------------------------{ +def getEDClass(class_or_filename): + """ + Return ElastoDyn instance of FileCl + INPUT: either + - an instance of FileCl, as returned by reading the file, ED = weio.read(ED_filename) + - a filepath to a ElastoDyn input file + - a filepath to a main OpenFAST input file + """ + if hasattr(class_or_filename,'startswith'): # if string + ED = FASTInputFile(class_or_filename) + if 'EDFile' in ED.keys(): # User provided a .fst file... + parentDir=os.path.dirname(class_or_filename) + EDfilename = os.path.join(parentDir, ED['EDFile'].replace('"','')) + ED = FASTInputFile(EDfilename) + else: + ED = class_or_filename + return ED + def ED_BldStations(ED): """ Returns ElastoDyn Blade Station positions, useful to know where the outputs are. INPUTS: @@ -23,15 +41,14 @@ def ED_BldStations(ED): - bld_fract: fraction of the blade length were stations are defined - r_nodes: spanwise position from the rotor apex of the Blade stations """ - if hasattr(ED,'startswith'): # if string - ED = FASTInputFile(ED) + ED = getEDClass(ED) nBldNodes = ED['BldNodes'] bld_fract = np.arange(1./nBldNodes/2., 1, 1./nBldNodes) r_nodes = bld_fract*(ED['TipRad']-ED['HubRad']) + ED['HubRad'] return bld_fract, r_nodes -def ED_TwrStations(ED): +def ED_TwrStations(ED, addBase=True): """ Returns ElastoDyn Tower Station positions, useful to know where the outputs are. INPUTS: - ED: either: @@ -42,12 +59,13 @@ def ED_TwrStations(ED): - r_fract: fraction of the towet length were stations are defined - h_nodes: height from the *ground* of the stations (not from the Tower base) """ - if hasattr(ED,'startswith'): # if string - ED = FASTInputFile(ED) + ED = getEDClass(ED) nTwrNodes = ED['TwrNodes'] twr_fract = np.arange(1./nTwrNodes/2., 1, 1./nTwrNodes) - h_nodes = twr_fract*(ED['TowerHt']-ED['TowerBsHt']) + ED['TowerBsHt'] + h_nodes = twr_fract*(ED['TowerHt']-ED['TowerBsHt']) + if addBase: + h_nodes += ED['TowerBsHt'] return twr_fract, h_nodes def ED_BldGag(ED): @@ -59,8 +77,7 @@ def ED_BldGag(ED): OUTPUTS: - r_gag: The radial positions of the gages, given from the rotor apex """ - if hasattr(ED,'startswith'): # if string - ED = FASTInputFile(ED) + ED = getEDClass(ED) _,r_nodes= ED_BldStations(ED) # if ED.hasNodal: @@ -75,27 +92,28 @@ def ED_BldGag(ED): r_gag = r_nodes[ Inodes[:nOuts] -1] return r_gag, Inodes -def ED_TwrGag(ED): +def ED_TwrGag(ED, addBase=True): """ Returns the heights of ElastoDyn blade gages INPUTS: - ED: either: - a filename of a ElastoDyn input file - an instance of FileCl, as returned by reading the file, ED = weio.read(ED_filename) + - addBase: if True, TowerBsHt is added to h_gag OUTPUTS: - h_gag: The heights of the gages, given from the ground height (tower base + TowerBsHt) """ - if hasattr(ED,'startswith'): # if string - ED = FASTInputFile(ED) - _,h_nodes= ED_TwrStations(ED) + ED = getEDClass(ED) + + _,h_nodes= ED_TwrStations(ED, addBase=addBase) nOuts = ED['NTwGages'] if nOuts<=0: - return np.array([]) + return np.array([]), None if type(ED['TwrGagNd']) is list: Inodes = np.asarray(ED['TwrGagNd']) else: Inodes = np.array([ED['TwrGagNd']]) h_gag = h_nodes[ Inodes[:nOuts] -1] - return h_gag + return h_gag, Inodes def AD14_BldGag(AD): @@ -107,7 +125,7 @@ def AD14_BldGag(AD): OUTPUTS: - r_gag: The radial positions of the gages, given from the blade root """ - if hasattr(ED,'startswith'): # if string + if hasattr(AD,'startswith'): # if string AD = FASTInputFile(AD) Nodes=AD['BldAeroNodes'] @@ -235,6 +253,18 @@ def BD_BldGag(BD): return r_gag, Inodes, r_nodes # +def SD_MembersNodes(SD): + sd = SubDyn(SD) + return sd.pointsMN + +def SD_MembersJoints(SD): + sd = SubDyn(SD) + return sd.pointsMJ + +def SD_MembersGages(SD): + sd = SubDyn(SD) + return sd.pointsMNout + # # 1, 7, 14, 21, 30, 36, 43, 52, 58 BldGagNd List of blade nodes that have strain gages [1 to BldNodes] (-) [unused if NBlGages=0] @@ -297,7 +327,7 @@ def _HarmonizeSpanwiseData(Name, Columns, vr, R, IR=None) : return dfRad, nrMax, ValidRow -def insert_radial_columns(df, vr=None, R=None, IR=None): +def insert_spanwise_columns(df, vr=None, R=None, IR=None, sspan='r', sspan_bar='r/R'): """ Add some columns to the radial data df: dataframe @@ -317,14 +347,14 @@ def insert_radial_columns(df, vr=None, R=None, IR=None): if (nrMax)<=len(vr_bar): vr_bar=vr_bar[:nrMax] elif (nrMax)>len(vr_bar): - raise Exception('Inconsitent length between radial stations ({:d}) and max index present in output chanels ({:d})'.format(len(vr_bar),nrMax)) - df.insert(0, 'r/R_[-]', vr_bar) + raise Exception('Inconsistent length between radial stations ({:d}) and max index present in output chanels ({:d})'.format(len(vr_bar),nrMax)) + df.insert(0, sspan_bar+'_[-]', vr_bar) if IR is not None: df['Node_[#]']=IR[:nrMax] df['i_[#]']=ids+1 if vr is not None: - df['r_[m]'] = vr[:nrMax] + df[sspan+'_[m]'] = vr[:nrMax] return df def find_matching_columns(Cols, PatternMap): @@ -569,58 +599,130 @@ def spanwiseColED(Cols): EDSpanMap[r'^Spn(\d)MLz'+sB+r'_\[kN-m\]' ]=SB+'MLz_[kN-m]' return find_matching_columns(Cols, EDSpanMap) +def spanwiseColEDTwr(Cols): + """ Return column info, available columns and indices that contain ED spanwise data""" + EDSpanMap=dict() + # All Outs + EDSpanMap[r'^TwHt(\d*)ALxt_\[m/s^2\]'] = 'ALxt_[m/s^2]' + EDSpanMap[r'^TwHt(\d*)ALyt_\[m/s^2\]'] = 'ALyt_[m/s^2]' + EDSpanMap[r'^TwHt(\d*)ALzt_\[m/s^2\]'] = 'ALzt_[m/s^2]' + EDSpanMap[r'^TwHt(\d*)TDxt_\[m\]' ] = 'TDxt_[m]' + EDSpanMap[r'^TwHt(\d*)TDyt_\[m\]' ] = 'TDyt_[m]' + EDSpanMap[r'^TwHt(\d*)TDzt_\[m\]' ] = 'TDzt_[m]' + EDSpanMap[r'^TwHt(\d*)RDxt_\[deg\]' ] = 'RDxt_[deg]' + EDSpanMap[r'^TwHt(\d*)RDyt_\[deg\]' ] = 'RDyt_[deg]' + EDSpanMap[r'^TwHt(\d*)RDzt_\[deg\]' ] = 'RDzt_[deg]' + EDSpanMap[r'^TwHt(\d*)TPxi_\[m\]' ] = 'TPxi_[m]' + EDSpanMap[r'^TwHt(\d*)TPyi_\[m\]' ] = 'TPyi_[m]' + EDSpanMap[r'^TwHt(\d*)TPzi_\[m\]' ] = 'TPzi_[m]' + EDSpanMap[r'^TwHt(\d*)RPxi_\[deg\]' ] = 'RPxi_[deg]' + EDSpanMap[r'^TwHt(\d*)RPyi_\[deg\]' ] = 'RPyi_[deg]' + EDSpanMap[r'^TwHt(\d*)RPzi_\[deg\]' ] = 'RPzi_[deg]' + EDSpanMap[r'^TwHt(\d*)FLxt_\[kN\]' ] = 'FLxt_[kN]' + EDSpanMap[r'^TwHt(\d*)FLyt_\[kN\]' ] = 'FLyt_[kN]' + EDSpanMap[r'^TwHt(\d*)FLzt_\[kN\]' ] = 'FLzt_[kN]' + EDSpanMap[r'^TwHt(\d*)MLxt_\[kN-m\]' ] = 'MLxt_[kN-m]' + EDSpanMap[r'^TwHt(\d*)MLyt_\[kN-m\]' ] = 'MLyt_[kN-m]' + EDSpanMap[r'^TwHt(\d*)MLzt_\[kN-m\]' ] = 'MLzt_[kN-m]' + return find_matching_columns(Cols, EDSpanMap) + + + def spanwiseColAD(Cols): """ Return column info, available columns and indices that contain AD spanwise data""" ADSpanMap=dict() for sB in ['B1','B2','B3']: - ADSpanMap['^[A]*'+sB+r'N(\d*)Alpha_\[deg\]']=sB+'Alpha_[deg]' - ADSpanMap['^[A]*'+sB+r'N(\d*)AOA_\[deg\]' ]=sB+'Alpha_[deg]' # DBGOuts - ADSpanMap['^[A]*'+sB+r'N(\d*)AxInd_\[-\]' ]=sB+'AxInd_[-]' - ADSpanMap['^[A]*'+sB+r'N(\d*)TnInd_\[-\]' ]=sB+'TnInd_[-]' - ADSpanMap['^[A]*'+sB+r'N(\d*)AIn_\[deg\]' ]=sB+'AxInd_[-]' # DBGOuts NOTE BUG Unit - ADSpanMap['^[A]*'+sB+r'N(\d*)ApI_\[deg\]' ]=sB+'TnInd_[-]' # DBGOuts NOTE BUG Unit - ADSpanMap['^[A]*'+sB+r'N(\d*)AIn_\[-\]' ]=sB+'AxInd_[-]' # DBGOuts - ADSpanMap['^[A]*'+sB+r'N(\d*)ApI_\[-\]' ]=sB+'TnInd_[-]' # DBGOuts - ADSpanMap['^[A]*'+sB+r'N(\d*)Uin_\[m/s\]' ]=sB+'Uin_[m/s]' # DBGOuts - ADSpanMap['^[A]*'+sB+r'N(\d*)Uit_\[m/s\]' ]=sB+'Uit_[m/s]' # DBGOuts - ADSpanMap['^[A]*'+sB+r'N(\d*)Uir_\[m/s\]' ]=sB+'Uir_[m/s]' # DBGOuts - ADSpanMap['^[A]*'+sB+r'N(\d*)Cl_\[-\]' ]=sB+'Cl_[-]' - ADSpanMap['^[A]*'+sB+r'N(\d*)Cd_\[-\]' ]=sB+'Cd_[-]' - ADSpanMap['^[A]*'+sB+r'N(\d*)Cm_\[-\]' ]=sB+'Cm_[-]' - ADSpanMap['^[A]*'+sB+r'N(\d*)Cx_\[-\]' ]=sB+'Cx_[-]' - ADSpanMap['^[A]*'+sB+r'N(\d*)Cy_\[-\]' ]=sB+'Cy_[-]' - ADSpanMap['^[A]*'+sB+r'N(\d*)Cn_\[-\]' ]=sB+'Cn_[-]' - ADSpanMap['^[A]*'+sB+r'N(\d*)Ct_\[-\]' ]=sB+'Ct_[-]' - ADSpanMap['^[A]*'+sB+r'N(\d*)Re_\[-\]' ]=sB+'Re_[-]' - ADSpanMap['^[A]*'+sB+r'N(\d*)Vrel_\[m/s\]' ]=sB+'Vrel_[m/s]' - ADSpanMap['^[A]*'+sB+r'N(\d*)Theta_\[deg\]']=sB+'Theta_[deg]' - ADSpanMap['^[A]*'+sB+r'N(\d*)Phi_\[deg\]' ]=sB+'Phi_[deg]' - ADSpanMap['^[A]*'+sB+r'N(\d*)Twst_\[deg\]' ]=sB+'Twst_[deg]' #DBGOuts - ADSpanMap['^[A]*'+sB+r'N(\d*)Curve_\[deg\]']=sB+'Curve_[deg]' - ADSpanMap['^[A]*'+sB+r'N(\d*)Vindx_\[m/s\]']=sB+'Vindx_[m/s]' - ADSpanMap['^[A]*'+sB+r'N(\d*)Vindy_\[m/s\]']=sB+'Vindy_[m/s]' - ADSpanMap['^[A]*'+sB+r'N(\d*)Fx_\[N/m\]' ]=sB+'Fx_[N/m]' - ADSpanMap['^[A]*'+sB+r'N(\d*)Fy_\[N/m\]' ]=sB+'Fy_[N/m]' - ADSpanMap['^[A]*'+sB+r'N(\d*)Fl_\[N/m\]' ]=sB+'Fl_[N/m]' - ADSpanMap['^[A]*'+sB+r'N(\d*)Fd_\[N/m\]' ]=sB+'Fd_[N/m]' - ADSpanMap['^[A]*'+sB+r'N(\d*)Fn_\[N/m\]' ]=sB+'Fn_[N/m]' - ADSpanMap['^[A]*'+sB+r'N(\d*)Ft_\[N/m\]' ]=sB+'Ft_[N/m]' - ADSpanMap['^[A]*'+sB+r'N(\d*)VUndx_\[m/s\]']=sB+'VUndx_[m/s]' - ADSpanMap['^[A]*'+sB+r'N(\d*)VUndy_\[m/s\]']=sB+'VUndy_[m/s]' - ADSpanMap['^[A]*'+sB+r'N(\d*)VUndz_\[m/s\]']=sB+'VUndz_[m/s]' - ADSpanMap['^[A]*'+sB+r'N(\d*)VDisx_\[m/s\]']=sB+'VDisx_[m/s]' - ADSpanMap['^[A]*'+sB+r'N(\d*)VDisy_\[m/s\]']=sB+'VDisy_[m/s]' - ADSpanMap['^[A]*'+sB+r'N(\d*)VDisz_\[m/s\]']=sB+'VDisz_[m/s]' - ADSpanMap['^[A]*'+sB+r'N(\d*)STVx_\[m/s\]' ]=sB+'STVx_[m/s]' - ADSpanMap['^[A]*'+sB+r'N(\d*)STVy_\[m/s\]' ]=sB+'STVy_[m/s]' - ADSpanMap['^[A]*'+sB+r'N(\d*)STVz_\[m/s\]' ]=sB+'STVz_[m/s]' - ADSpanMap['^[A]*'+sB+r'N(\d*)Vx_\[m/s\]' ]=sB+'Vx_[m/s]' - ADSpanMap['^[A]*'+sB+r'N(\d*)Vy_\[m/s\]' ]=sB+'Vy_[m/s]' - ADSpanMap['^[A]*'+sB+r'N(\d*)Vz_\[m/s\]' ]=sB+'Vz_[m/s]' - ADSpanMap['^[A]*'+sB+r'N(\d*)DynP_\[Pa\]' ]=sB+'DynP_[Pa]' - ADSpanMap['^[A]*'+sB+r'N(\d*)M_\[-\]' ]=sB+'M_[-]' - ADSpanMap['^[A]*'+sB+r'N(\d*)Mm_\[N-m/m\]' ]=sB+'Mm_[N-m/m]' - ADSpanMap['^[A]*'+sB+r'N(\d*)Gam_\[' ]=sB+'Gam_[m^2/s]' #DBGOuts + ADSpanMap['^[A]*'+sB+r'N(\d*)Alpha_\[deg\]'] =sB+'Alpha_[deg]' + ADSpanMap['^[A]*'+sB+r'N(\d*)AxInd_\[-\]' ] =sB+'AxInd_[-]' + ADSpanMap['^[A]*'+sB+r'N(\d*)TnInd_\[-\]' ] =sB+'TnInd_[-]' + ADSpanMap['^[A]*'+sB+r'N(\d*)AxInd_qs_\[-\]' ]=sB+'AxInd_qs_[-]' + ADSpanMap['^[A]*'+sB+r'N(\d*)TnInd_qs_\[-\]' ]=sB+'TnInd_qs_[-]' + ADSpanMap['^[A]*'+sB+r'N(\d*)BEM_k_\[-\]' ]=sB+'BEM_k_[-]' + ADSpanMap['^[A]*'+sB+r'N(\d*)BEM_kp_\[-\]' ]=sB+'BEM_kp_[-]' + ADSpanMap['^[A]*'+sB+r'N(\d*)BEM_F_\[-\]' ]=sB+'BEM_F_[-]' + ADSpanMap['^[A]*'+sB+r'N(\d*)BEM_CT_qs_\[-\]' ]=sB+'BEM_CT_qs_[-]' + ADSpanMap['^[A]*'+sB+r'N(\d*)Cl_\[-\]' ] =sB+'Cl_[-]' + ADSpanMap['^[A]*'+sB+r'N(\d*)Cd_\[-\]' ] =sB+'Cd_[-]' + ADSpanMap['^[A]*'+sB+r'N(\d*)Cm_\[-\]' ] =sB+'Cm_[-]' + ADSpanMap['^[A]*'+sB+r'N(\d*)Cx_\[-\]' ] =sB+'Cx_[-]' + ADSpanMap['^[A]*'+sB+r'N(\d*)Cy_\[-\]' ] =sB+'Cy_[-]' + ADSpanMap['^[A]*'+sB+r'N(\d*)Cn_\[-\]' ] =sB+'Cn_[-]' + ADSpanMap['^[A]*'+sB+r'N(\d*)Ct_\[-\]' ] =sB+'Ct_[-]' + ADSpanMap['^[A]*'+sB+r'N(\d*)Re_\[-\]' ] =sB+'Re_[-]' + ADSpanMap['^[A]*'+sB+r'N(\d*)Vrel_\[m/s\]' ] =sB+'Vrel_[m/s]' + ADSpanMap['^[A]*'+sB+r'N(\d*)Theta_\[deg\]'] =sB+'Theta_[deg]' + ADSpanMap['^[A]*'+sB+r'N(\d*)Phi_\[deg\]' ] =sB+'Phi_[deg]' + ADSpanMap['^[A]*'+sB+r'N(\d*)Curve_\[deg\]'] =sB+'Curve_[deg]' + ADSpanMap['^[A]*'+sB+r'N(\d*)Vindx_\[m/s\]'] =sB+'Vindx_[m/s]' + ADSpanMap['^[A]*'+sB+r'N(\d*)Vindy_\[m/s\]'] =sB+'Vindy_[m/s]' + ADSpanMap['^[A]*'+sB+r'N(\d*)Vindxi_\[m/s\]'] =sB+'Vindxi_[m/s]' + ADSpanMap['^[A]*'+sB+r'N(\d*)Vindyi_\[m/s\]'] =sB+'Vindyi_[m/s]' + ADSpanMap['^[A]*'+sB+r'N(\d*)Vindzi_\[m/s\]'] =sB+'Vindzi_[m/s]' + ADSpanMap['^[A]*'+sB+r'N(\d*)Vindxh_\[m/s\]'] =sB+'Vindxh_[m/s]' + ADSpanMap['^[A]*'+sB+r'N(\d*)Vindyh_\[m/s\]'] =sB+'Vindyh_[m/s]' + ADSpanMap['^[A]*'+sB+r'N(\d*)Vindzh_\[m/s\]'] =sB+'Vindzh_[m/s]' + ADSpanMap['^[A]*'+sB+r'N(\d*)Vindxp_\[m/s\]'] =sB+'Vindxp_[m/s]' + ADSpanMap['^[A]*'+sB+r'N(\d*)Vindyp_\[m/s\]'] =sB+'Vindyp_[m/s]' + ADSpanMap['^[A]*'+sB+r'N(\d*)Vindzp_\[m/s\]'] =sB+'Vindzp_[m/s]' + ADSpanMap['^[A]*'+sB+r'N(\d*)Fx_\[N/m\]' ] =sB+'Fx_[N/m]' + ADSpanMap['^[A]*'+sB+r'N(\d*)Fy_\[N/m\]' ] =sB+'Fy_[N/m]' + ADSpanMap['^[A]*'+sB+r'N(\d*)Fxi_\[N/m\]' ] =sB+'Fxi_[N/m]' + ADSpanMap['^[A]*'+sB+r'N(\d*)Fyi_\[N/m\]' ] =sB+'Fyi_[N/m]' + ADSpanMap['^[A]*'+sB+r'N(\d*)Fzi_\[N/m\]' ] =sB+'Fzi_[N/m]' + ADSpanMap['^[A]*'+sB+r'N(\d*)Mxi_\[N-m/m\]' ] =sB+'Mxi_[N-m/m]' + ADSpanMap['^[A]*'+sB+r'N(\d*)Myi_\[N-m/m\]' ] =sB+'Myi_[N-m/m]' + ADSpanMap['^[A]*'+sB+r'N(\d*)Mzi_\[N-m/m\]' ] =sB+'Mzi_[N-m/m]' + ADSpanMap['^[A]*'+sB+r'N(\d*)Fl_\[N/m\]' ] =sB+'Fl_[N/m]' + ADSpanMap['^[A]*'+sB+r'N(\d*)Fd_\[N/m\]' ] =sB+'Fd_[N/m]' + ADSpanMap['^[A]*'+sB+r'N(\d*)Fn_\[N/m\]' ] =sB+'Fn_[N/m]' + ADSpanMap['^[A]*'+sB+r'N(\d*)Ft_\[N/m\]' ] =sB+'Ft_[N/m]' + ADSpanMap['^[A]*'+sB+r'N(\d*)VUndx_\[m/s\]'] =sB+'VUndx_[m/s]' + ADSpanMap['^[A]*'+sB+r'N(\d*)VUndy_\[m/s\]'] =sB+'VUndy_[m/s]' + ADSpanMap['^[A]*'+sB+r'N(\d*)VUndz_\[m/s\]'] =sB+'VUndz_[m/s]' + ADSpanMap['^[A]*'+sB+r'N(\d*)VUndxi_\[m/s\]'] =sB+'VUndxi_[m/s]' + ADSpanMap['^[A]*'+sB+r'N(\d*)VUndyi_\[m/s\]'] =sB+'VUndyi_[m/s]' + ADSpanMap['^[A]*'+sB+r'N(\d*)VUndzi_\[m/s\]'] =sB+'VUndzi_[m/s]' + ADSpanMap['^[A]*'+sB+r'N(\d*)VDisx_\[m/s\]'] =sB+'VDisx_[m/s]' + ADSpanMap['^[A]*'+sB+r'N(\d*)VDisy_\[m/s\]'] =sB+'VDisy_[m/s]' + ADSpanMap['^[A]*'+sB+r'N(\d*)VDisz_\[m/s\]'] =sB+'VDisz_[m/s]' + ADSpanMap['^[A]*'+sB+r'N(\d*)VDisxi_\[m/s\]'] =sB+'VDisxi_[m/s]' + ADSpanMap['^[A]*'+sB+r'N(\d*)VDisyi_\[m/s\]'] =sB+'VDisyi_[m/s]' + ADSpanMap['^[A]*'+sB+r'N(\d*)VDiszi_\[m/s\]'] =sB+'VDiszi_[m/s]' + ADSpanMap['^[A]*'+sB+r'N(\d*)VDisxh_\[m/s\]'] =sB+'VDisxh_[m/s]' + ADSpanMap['^[A]*'+sB+r'N(\d*)VDisyh_\[m/s\]'] =sB+'VDisyh_[m/s]' + ADSpanMap['^[A]*'+sB+r'N(\d*)VDiszh_\[m/s\]'] =sB+'VDiszh_[m/s]' + ADSpanMap['^[A]*'+sB+r'N(\d*)VDisxp_\[m/s\]'] =sB+'VDisxp_[m/s]' + ADSpanMap['^[A]*'+sB+r'N(\d*)VDisyp_\[m/s\]'] =sB+'VDisyp_[m/s]' + ADSpanMap['^[A]*'+sB+r'N(\d*)VDiszp_\[m/s\]'] =sB+'VDiszp_[m/s]' + ADSpanMap['^[A]*'+sB+r'N(\d*)STVx_\[m/s\]' ] =sB+'STVx_[m/s]' + ADSpanMap['^[A]*'+sB+r'N(\d*)STVy_\[m/s\]' ] =sB+'STVy_[m/s]' + ADSpanMap['^[A]*'+sB+r'N(\d*)STVz_\[m/s\]' ] =sB+'STVz_[m/s]' + ADSpanMap['^[A]*'+sB+r'N(\d*)STVxi_\[m/s\]' ] =sB+'STVxi_[m/s]' + ADSpanMap['^[A]*'+sB+r'N(\d*)STVyi_\[m/s\]' ] =sB+'STVyi_[m/s]' + ADSpanMap['^[A]*'+sB+r'N(\d*)STVzi_\[m/s\]' ] =sB+'STVzi_[m/s]' + ADSpanMap['^[A]*'+sB+r'N(\d*)STVxh_\[m/s\]' ] =sB+'STVxh_[m/s]' + ADSpanMap['^[A]*'+sB+r'N(\d*)STVyh_\[m/s\]' ] =sB+'STVyh_[m/s]' + ADSpanMap['^[A]*'+sB+r'N(\d*)STVzh_\[m/s\]' ] =sB+'STVzh_[m/s]' + ADSpanMap['^[A]*'+sB+r'N(\d*)STVxp_\[m/s\]' ] =sB+'STVxp_[m/s]' + ADSpanMap['^[A]*'+sB+r'N(\d*)STVyp_\[m/s\]' ] =sB+'STVyp_[m/s]' + ADSpanMap['^[A]*'+sB+r'N(\d*)STVzp_\[m/s\]' ] =sB+'STVzp_[m/s]' + ADSpanMap['^[A]*'+sB+r'N(\d*)Vx_\[m/s\]' ] =sB+'Vx_[m/s]' + ADSpanMap['^[A]*'+sB+r'N(\d*)Vy_\[m/s\]' ] =sB+'Vy_[m/s]' + ADSpanMap['^[A]*'+sB+r'N(\d*)Vz_\[m/s\]' ] =sB+'Vz_[m/s]' + ADSpanMap['^[A]*'+sB+r'N(\d*)DynP_\[Pa\]' ] =sB+'DynP_[Pa]' + ADSpanMap['^[A]*'+sB+r'N(\d*)M_\[-\]' ] =sB+'M_[-]' + ADSpanMap['^[A]*'+sB+r'N(\d*)Mm_\[N-m/m\]' ] =sB+'Mm_[N-m/m]' + ADSpanMap['^[A]*'+sB+r'N(\d*)Gam_\[' ] =sB+'Gam_[m^2/s]' #DBGOuts + # DEPRECIATED + ADSpanMap['^[A]*'+sB+r'N(\d*)AOA_\[deg\]' ] =sB+'Alpha_[deg]' # DBGOuts + ADSpanMap['^[A]*'+sB+r'N(\d*)AIn_\[deg\]' ] =sB+'AxInd_[-]' # DBGOuts NOTE BUG Unit + ADSpanMap['^[A]*'+sB+r'N(\d*)ApI_\[deg\]' ] =sB+'TnInd_[-]' # DBGOuts NOTE BUG Unit + ADSpanMap['^[A]*'+sB+r'N(\d*)AIn_\[-\]' ] =sB+'AxInd_[-]' # DBGOuts + ADSpanMap['^[A]*'+sB+r'N(\d*)ApI_\[-\]' ] =sB+'TnInd_[-]' # DBGOuts + ADSpanMap['^[A]*'+sB+r'N(\d*)Uin_\[m/s\]' ] =sB+'Uin_[m/s]' # DBGOuts + ADSpanMap['^[A]*'+sB+r'N(\d*)Uit_\[m/s\]' ] =sB+'Uit_[m/s]' # DBGOuts + ADSpanMap['^[A]*'+sB+r'N(\d*)Uir_\[m/s\]' ] =sB+'Uir_[m/s]' # DBGOuts + ADSpanMap['^[A]*'+sB+r'N(\d*)Twst_\[deg\]' ] =sB+'Twst_[deg]' #DBGOuts # --- AD 14 ADSpanMap[r'^Alpha(\d*)_\[deg\]' ]='Alpha_[deg]' ADSpanMap[r'^DynPres(\d*)_\[Pa\]' ]='DynPres_[Pa]' @@ -650,6 +752,25 @@ def insert_extra_columns_AD(dfRad, tsAvg, vr=None, rho=None, R=None, nB=None, ch if vr is not None: chord =chord[0:len(dfRad)] for sB in ['B1','B2','B3']: + for coord in ['i','p','h']: + for comp in ['x','y','z']: + s=comp+coord + try: + dfRad[sB+'Vflw{}_[m/s]'.format(s)] = dfRad[sB+'VDis{}_[m/s]'.format(s)] - dfRad[sB+'STV{}_[m/s]'.format(s)] + except: + pass + for coord in ['i','p','h']: + for comp in ['x','y','z']: + s=comp+coord + try: + dfRad[sB+'Vrel{}_[m/s]'.format(s)] = dfRad[sB+'VDis{}_[m/s]'.format(s)] - dfRad[sB+'STV{}_[m/s]'.format(s)] + dfRad[sB+'Vind{}_[m/s]'.format(s)] + except: + pass + try: + s='p' + dfRad[sB+'phi_{}_[def]'.format(s)] = np.arctan2(dfRad[sB+'Vrelx{}_[m/s]'.format(s)], dfRad[sB+'Vrely{}_[m/s]'.format(s)])*180/np.pi + except: + pass try: vr_bar=vr/R Fx = dfRad[sB+'Fx_[N/m]'] @@ -677,7 +798,8 @@ def insert_extra_columns_AD(dfRad, tsAvg, vr=None, rho=None, R=None, nB=None, ch def spanwisePostPro(FST_In=None,avgMethod='constantwindow',avgParam=5,out_ext='.outb',df=None): """ - Postprocess FAST radial data. Average the time series, return a dataframe nr x nColumns + Postprocess FAST radial data. + if avgMethod is not None: Average the time series, return a dataframe nr x nColumns INPUTS: - FST_IN: Fast .fst input file @@ -687,20 +809,39 @@ def spanwisePostPro(FST_In=None,avgMethod='constantwindow',avgParam=5,out_ext='. """ # --- Opens Fast output and performs averaging if df is None: - df = FASTOutputFile(FST_In.replace('.fst',out_ext).replace('.dvr',out_ext)).toDataFrame() + filename =FST_In.replace('.fst',out_ext).replace('.dvr',out_ext) + df = FASTOutputFile(filename).toDataFrame() returnDF=True else: + filename='' returnDF=False # NOTE: spanwise script doest not support duplicate columns df = df.loc[:,~df.columns.duplicated()] - dfAvg = averageDF(df,avgMethod=avgMethod ,avgParam=avgParam) # NOTE: average 5 last seconds - + if avgMethod is not None: + dfAvg = averageDF(df,avgMethod=avgMethod ,avgParam=avgParam, filename=filename) # NOTE: average 5 last seconds + else: + dfAvg=df + # --- The script assume '_' between units and colnames + Cols= dfAvg.columns # --- Extract info (e.g. radial positions) from Fast input file # We don't have a .fst input file, so we'll rely on some default values for "r" rho = 1.225 chord = None # --- Extract radial positions of output channels - r_AD, r_ED, r_BD, IR_AD, IR_ED, IR_BD, R, r_hub, fst = FASTRadialOutputs(FST_In, OutputCols=df.columns.values) + d = FASTSpanwiseOutputs(FST_In, OutputCols=Cols) + r_AD = d['r_AD'] + r_ED_bld = d['r_ED_bld'] + r_ED_twr = d['r_ED_twr'] + r_BD = d['r_BD'] + IR_AD = d['IR_AD'] + IR_ED_bld = d['IR_ED_bld'] + IR_ED_twr = d['IR_ED_twr'] + IR_BD = d['IR_BD'] + TwrLen = d['TwrLen'] + R = d['R'] + r_hub = d['r_hub'] + fst = d['fst'] + if R is None: R=1 try: @@ -720,29 +861,47 @@ def spanwisePostPro(FST_In=None,avgMethod='constantwindow',avgParam=5,out_ext='. #print('I_AD:', IR_AD) #print('I_ED:', IR_ED) #print('I_BD:', IR_BD) + out = {} + if returnDF: + out['df'] = df + out['dfAvg'] = dfAvg # --- Extract radial data and export to csv if needed - dfRad_AD = None - dfRad_ED = None - dfRad_BD = None - Cols=dfAvg.columns.values # --- AD ColsInfoAD, nrMaxAD = spanwiseColAD(Cols) dfRad_AD = extract_spanwise_data(ColsInfoAD, nrMaxAD, df=None, ts=dfAvg.iloc[0]) dfRad_AD = insert_extra_columns_AD(dfRad_AD, dfAvg.iloc[0], vr=r_AD, rho=rho, R=R, nB=3, chord=chord) - dfRad_AD = insert_radial_columns(dfRad_AD, r_AD, R=R, IR=IR_AD) - # --- ED + dfRad_AD = insert_spanwise_columns(dfRad_AD, r_AD, R=R, IR=IR_AD) + out['AD'] = dfRad_AD + # --- ED Bld ColsInfoED, nrMaxED = spanwiseColED(Cols) dfRad_ED = extract_spanwise_data(ColsInfoED, nrMaxED, df=None, ts=dfAvg.iloc[0]) - dfRad_ED = insert_radial_columns(dfRad_ED, r_ED, R=R, IR=IR_ED) + dfRad_ED = insert_spanwise_columns(dfRad_ED, r_ED_bld, R=R, IR=IR_ED_bld) + out['ED_bld'] = dfRad_ED + # --- ED Twr + ColsInfoED, nrMaxEDt = spanwiseColEDTwr(Cols) + dfRad_EDt = extract_spanwise_data(ColsInfoED, nrMaxEDt, df=None, ts=dfAvg.iloc[0]) + dfRad_EDt2 = insert_spanwise_columns(dfRad_EDt, r_ED_twr, R=TwrLen, IR=IR_ED_twr, sspan='H',sspan_bar='H/L') + # TODO we could insert TwrBs and TwrTp quantities here... + out['ED_twr'] = dfRad_EDt # --- BD ColsInfoBD, nrMaxBD = spanwiseColBD(Cols) dfRad_BD = extract_spanwise_data(ColsInfoBD, nrMaxBD, df=None, ts=dfAvg.iloc[0]) - dfRad_BD = insert_radial_columns(dfRad_BD, r_BD, R=R, IR=IR_BD) - if returnDF: - return dfRad_ED , dfRad_AD, dfRad_BD, df - else: - return dfRad_ED , dfRad_AD, dfRad_BD + dfRad_BD = insert_spanwise_columns(dfRad_BD, r_BD, R=R, IR=IR_BD) + out['BD'] = dfRad_BD + # --- SubDyn + try: + # NOTE: fst might be None + sd = SubDyn(fst.SD) + #MN = sd.pointsMN + MNout, MJout = sd.memberPostPro(dfAvg) + out['SD_MembersOut'] = MNout + out['SD_JointsOut'] = MJout + except: + out['SD_MembersOut'] = None + out['SD_JointsOut'] = None + # Combine all into a dictionary + return out def spanwisePostProRows(df, FST_In=None): @@ -756,7 +915,19 @@ def spanwisePostProRows(df, FST_In=None): rho = 1.225 chord = None # --- Extract radial positions of output channels - r_AD, r_ED, r_BD, IR_AD, IR_ED, IR_BD, R, r_hub, fst = FASTRadialOutputs(FST_In, OutputCols=df.columns.values) + d = FASTSpanwiseOutputs(FST_In, OutputCols=df.columns.values) + r_AD = d['r_AD'] + r_ED_bld = d['r_ED_bld'] + r_ED_twr = d['r_ED_twr'] + r_BD = d['r_BD'] + IR_AD = d['IR_AD'] + IR_ED_bld = d['IR_ED_bld'] + IR_ED_twr = d['IR_ED_twr'] + IR_BD = d['IR_BD'] + TwrLen = d['TwrLen'] + R = d['R'] + r_hub = d['r_hub'] + fst = d['fst'] #print('r_AD:', r_AD) #print('r_ED:', r_ED) #print('r_BD:', r_BD) @@ -795,21 +966,21 @@ def spanwisePostProRows(df, FST_In=None): if r_AD is not None: dfRad_AD = extract_spanwise_data(ColsInfoAD, nrMaxAD, df=None, ts=df.iloc[i]) dfRad_AD = insert_extra_columns_AD(dfRad_AD, df.iloc[i], vr=r_AD, rho=rho, R=R, nB=3, chord=chord) - dfRad_AD = insert_radial_columns(dfRad_AD, r_AD, R=R, IR=IR_AD) + dfRad_AD = insert_spanwise_columns(dfRad_AD, r_AD, R=R, IR=IR_AD) if i==0: M_AD = np.zeros((len(v), len(dfRad_AD), len(dfRad_AD.columns))) Col_AD=dfRad_AD.columns.values M_AD[i, :, : ] = dfRad_AD.values if r_ED is not None and len(r_ED)>0: dfRad_ED = extract_spanwise_data(ColsInfoED, nrMaxED, df=None, ts=df.iloc[i]) - dfRad_ED = insert_radial_columns(dfRad_ED, r_ED, R=R, IR=IR_ED) + dfRad_ED = insert_spanwise_columns(dfRad_ED, r_ED, R=R, IR=IR_ED) if i==0: M_ED = np.zeros((len(v), len(dfRad_ED), len(dfRad_ED.columns))) Col_ED=dfRad_ED.columns.values M_ED[i, :, : ] = dfRad_ED.values if r_BD is not None and len(r_BD)>0: dfRad_BD = extract_spanwise_data(ColsInfoBD, nrMaxBD, df=None, ts=df.iloc[i]) - dfRad_BD = insert_radial_columns(dfRad_BD, r_BD, R=R, IR=IR_BD) + dfRad_BD = insert_spanwise_columns(dfRad_BD, r_BD, R=R, IR=IR_BD) if i==0: M_BD = np.zeros((len(v), len(dfRad_BD), len(dfRad_BD.columns))) Col_BD=dfRad_BD.columns.values @@ -817,24 +988,28 @@ def spanwisePostProRows(df, FST_In=None): return M_AD, Col_AD, M_ED, Col_ED, M_BD, Col_BD -def FASTRadialOutputs(FST_In, OutputCols=None): - """ Returns radial positions where FAST has outputs +def FASTSpanwiseOutputs(FST_In, OutputCols=None, verbose=False): + """ Returns spanwise positions where OpenFAST has outputs INPUTS: - FST_In: fast input file (.fst) + - FST_In: fast input file (.fst) OUTPUTS: - r_AD: radial positions of FAST Outputs from the rotor center + dictionary with fields: + - r_AD: radial positions of FAST Outputs from the rotor center """ R = None + TwrLen = None r_hub =0 r_AD = None - r_ED = None + r_ED_bld = None + r_ED_twr = None r_BD = None - IR_ED = None + IR_ED_bld = None + IR_ED_twr = None IR_AD = None IR_BD = None fst=None if FST_In is not None: - fst = FASTInputDeck(FST_In, readlist=['AD','ADbld','ED','BD','BDbld']) + fst = FASTInputDeck(FST_In, readlist=['AD','ADbld','ED','BD','BDbld','SD']) # NOTE: all this below should be in FASTInputDeck if fst.version == 'F7': # --- FAST7 @@ -859,16 +1034,21 @@ def FASTRadialOutputs(FST_In, OutputCols=None): r_hub = fst.fst['BldHubRad_bl(1_1)'] elif not hasattr(fst,'ED'): - print('[WARN] The Elastodyn file couldn''t be found or read, from main file: '+FST_In) + if verbose: + print('[WARN] The Elastodyn file couldn''t be found or read, from main file: '+FST_In) #raise Exception('The Elastodyn file couldn''t be found or read, from main file: '+FST_In) else: R = fst.ED['TipRad'] r_hub = fst.ED['HubRad'] if fst.ED.hasNodal: - _, r_ED = ED_BldStations(fst.ED) - IR_ED =None + _, r_ED_bld = ED_BldStations(fst.ED) + IR_ED_bld =None else: - r_ED, IR_ED = ED_BldGag(fst.ED) + r_ED_bld, IR_ED_bld = ED_BldGag(fst.ED) + + # No nodal output for elastodyn tower yet + TwrLen = fst.ED['TowerHt'] -fst.ED['TowerBsHt'] + r_ED_twr, IR_ED_twr = ED_TwrGag(fst.ED) # --- BeamDyn if fst.BD is not None: @@ -882,7 +1062,8 @@ def FASTRadialOutputs(FST_In, OutputCols=None): # --- AeroDyn if fst.AD is None: - print('[WARN] The AeroDyn file couldn''t be found or read, from main file: '+FST_In) + if verbose: + print('[WARN] The AeroDyn file couldn''t be found or read, from main file: '+FST_In) #raise Exception('The AeroDyn file couldn''t be found or read, from main file: '+FST_In) else: if fst.ADversion == 'AD15': @@ -907,9 +1088,71 @@ def FASTRadialOutputs(FST_In, OutputCols=None): else: raise Exception('AeroDyn version unknown') - return r_AD, r_ED, r_BD, IR_AD, IR_ED, IR_BD, R, r_hub, fst + # Put everything into a dictionary for convenience + outs = {'r_AD':r_AD, 'IR_AD':IR_AD, 'r_ED_bld':r_ED_bld, 'IR_ED_bld':IR_ED_bld, 'r_ED_twr':r_ED_twr, 'IR_ED_twr':IR_ED_twr, 'r_BD':r_BD, 'IR_BD':IR_BD} + outs['R'] = R + outs['TwrLen']= TwrLen + outs['r_hub'] = r_hub + outs['fst'] = fst + return outs # r_AD, r_ED, r_BD, IR_AD, IR_ED, IR_BD, R, r_hub, fst + +def spanwiseConcat(df): + """ + Perform time-concatenation of all the spanwise data (AeroDyn only for now) + + For instance if df is: + + Time B1N001Alpha B1N002Alpha B1N003Alpha + t a1 a2 a3 + + with t, a1, a2, a3, arrays or length nt + + The concatenated dataframe will be: + Time i Alpha + t 1 a1 + t 2 a2 + t 3 a3 + + INPUTS: + - df: a dataframe, typically returned by FASTOutputFile (nt x (nc*nr + nother) ) + + OUTPUTS: + - dfCat: the time-concatenated dataframe (nt*nr x (2 + nc) ) + + """ + Cols = df.columns + ColsInfoAD, nrMaxAD = spanwiseColAD(Cols) + nChan = len(ColsInfoAD) + if nChan==0: + raise WELIBException('Cannot perform spanwise concatenation, no AeroDyn spanwise data was detected in the dataframe (e.g. columns of the form "AB1N001Cl_[-]"). ') + imin = np.min( [np.min(ColsInfoAD[i]['Idx']) for i in range(nChan)] ) + imax = np.max( [np.max(ColsInfoAD[i]['Idx']) for i in range(nChan)] ) + if 'Time_[s]' not in df.columns: + raise WELIBException('Cannot perform spanwise concatenation, the column `Time_[s]` is not present in the dataframe.') + time = df['Time_[s]'] + nt = len(time) + # We add two channels one for time, one for ispan + data = np.zeros((nt*nrMaxAD, nChan+2))*np.nan + # Loop on Channels and radial positions.. + for ic in range(nChan): + for ir in range(nrMaxAD): + data[ir*nt:(ir+1)*nt, 0] = time + data[ir*nt:(ir+1)*nt, 1] = ir+1 + IdxAvailableForThisChannel = ColsInfoAD[ic]['Idx'] + chanName = ColsInfoAD[ic]['name'] + colName = ColsInfoAD[ic]['cols'][ir] + #print('Channel {}: colName {}'.format(chanName, colName)) + if ir+1 in IdxAvailableForThisChannel: + data[ir*nt:(ir+1)*nt, ic+2] = df[colName].values + #else: + # raise Exception('Channel {}: Index missing {}'.format(chanName, ic+1)) + columns = ['Time_[s]'] + ['i_[-]'] + [ColsInfoAD[i]['name'] for i in range(nChan)] + dfCat = pd.DataFrame(data=data, columns=columns) + + return dfCat + def addToOutlist(OutList, Signals): if not isinstance(Signals,list): @@ -927,9 +1170,13 @@ def addToOutlist(OutList, Signals): # --- Generic df # --------------------------------------------------------------------------------{ def remap_df(df, ColMap, bColKeepNewOnly=False, inPlace=False, dataDict=None, verbose=False): - """ Add/rename columns of a dataframe, potentially perform operations between columns + """ + NOTE: see welib.tools.pandalib - dataDict: dicitonary of data to be made available as "variable" in the column mapping + Add/rename columns of a dataframe, potentially perform operations between columns + + dataDict: dictionary of data to be made available as "variable" in the column mapping + 'key' (new) : value (old) Example: @@ -937,6 +1184,7 @@ def remap_df(df, ColMap, bColKeepNewOnly=False, inPlace=False, dataDict=None, ve 'WS_[m/s]' : '{Wind1VelX_[m/s]}' , # create a new column from existing one 'RtTSR_[-]' : '{RtTSR_[-]} * 2 + {RtAeroCt_[-]}' , # change value of column 'RotSpeed_[rad/s]' : '{RotSpeed_[rpm]} * 2*np.pi/60 ', # new column [rpm] -> [rad/s] + 'q_p' : ['Q_P_[rad]', '{PtfmSurge_[deg]}*np.pi/180'] # List of possible matches } # Read df = weio.read('FASTOutBin.outb').toDataFrame() @@ -958,33 +1206,47 @@ def remap_df(df, ColMap, bColKeepNewOnly=False, inPlace=False, dataDict=None, ve # Loop for expressions for k0,v in ColMap.items(): k=k0.strip() - v=v.strip() - if v.find('{')>=0: - search_results = re.finditer(r'\{.*?\}', v) - expr=v - if verbose: - print('Attempt to insert column {:15s} with expr {}'.format(k,v)) - # For more advanced operations, we use an eval - bFail=False - for item in search_results: - col=item.group(0)[1:-1] - if col not in df.columns: - ColMapMiss.append(col) - bFail=True - expr=expr.replace(item.group(0),'df[\''+col+'\']') - #print(k0, '=', expr) - if not bFail: - df[k]=eval(expr) - ColNew.append(k) - else: - print('[WARN] Column not present in dataframe, cannot evaluate: ',expr) + if type(v) is not list: + values = [v] else: - #print(k0,'=',v) - if v not in df.columns: - ColMapMiss.append(v) - print('[WARN] Column not present in dataframe: ',v) + values = v + Found = False + for v in values: + v=v.strip() + if Found: + break # We avoid replacing twice + if v.find('{')>=0: + # --- This is an advanced substitution using formulae + search_results = re.finditer(r'\{.*?\}', v) + expr=v + if verbose: + print('Attempt to insert column {:15s} with expr {}'.format(k,v)) + # For more advanced operations, we use an eval + bFail=False + for item in search_results: + col=item.group(0)[1:-1] + if col not in df.columns: + ColMapMiss.append(col) + bFail=True + expr=expr.replace(item.group(0),'df[\''+col+'\']') + #print(k0, '=', expr) + if not bFail: + df[k]=eval(expr) + ColNew.append(k) + else: + print('[WARN] Column not present in dataframe, cannot evaluate: ',expr) else: - RenameMap[k]=v + #print(k0,'=',v) + if v not in df.columns: + ColMapMiss.append(v) + if verbose: + print('[WARN] Column not present in dataframe: ',v) + else: + if k in RenameMap.keys(): + print('[WARN] Not renaming {} with {} as the key is already present'.format(k,v)) + else: + RenameMap[k]=v + Found=True # Applying renaming only now so that expressions may be applied in any order for k,v in RenameMap.items(): @@ -1061,9 +1323,9 @@ def _zero_crossings(y,x=None,direction=None): raise Exception('Direction should be either `up` or `down`') return xzc, iBef, sign -def find_matching_pattern(List, pattern, sort=False, integers=True): +def find_matching_pattern(List, pattern, sort=False, integers=True, n=1): r""" Return elements of a list of strings that match a pattern - and return the first matching group + and return the n first matching group Example: @@ -1106,7 +1368,7 @@ def extractSpanTS(df, pattern): NOTE: time is not inserted in the output dataframe - To find "r" use FASTRadialOutputs, it is different for AeroDyn/ElastoDyn/BeamDyn/ + To find "r" use FASTSpanwiseOutputs, it is different for AeroDyn/ElastoDyn/BeamDyn/ There is no guarantee that the number of columns matching pattern will exactly corresponds to the number of radial stations. That's the responsability of the OpenFAST user. @@ -1275,7 +1537,7 @@ def azimuthal_average_DF(df, psiBin=None, colPsi='Azimuth_[deg]', tStart=None, c return dfPsi -def averageDF(df,avgMethod='periods',avgParam=None,ColMap=None,ColKeep=None,ColSort=None,stats=['mean']): +def averageDF(df,avgMethod='periods',avgParam=None,ColMap=None,ColKeep=None,ColSort=None,stats=['mean'], filename=''): """ See average PostPro for documentation, same interface, just does it for one dataframe """ @@ -1284,8 +1546,17 @@ def renameCol(x): if x==v: return k return x + # Sanity + if len(filename)>0: + filename=' (File: {})'.format(filename) + + sTAllowed = ['Time_[s]','Time [s]'] + sT = [s for s in sTAllowed if s in df.columns] + if len(sT)==0: + raise WELIBException('The dataframe must contain one of the following column: {}'.format(','.join(sTAllowed))) + # Before doing the colomn map we store the time - time = df['Time_[s]'].values + time = df[sT[0]].values timenoNA = time[~np.isnan(time)] # Column mapping if ColMap is not None: @@ -1302,15 +1573,17 @@ def renameCol(x): tStart =tEnd-avgParam elif avgMethod.lower()=='periods': # --- Using azimuth to find periods - if 'Azimuth_[deg]' not in df.columns: - raise Exception('The sensor `Azimuth_[deg]` does not appear to be in the output file. You cannot use the averaging method by `periods`, use `constantwindow` instead.') + sAAllowed = ['Azimuth_[deg]','Azimuth [deg]'] + sA = [s for s in sAAllowed if s in df.columns] + if len(sA)==0: + raise WELIBException('The dataframe must contain one of the following columns: {}.\nYou cannot use the averaging method by `periods`, use `constantwindow` instead.\n{}'.format(','.join(sAAllowed),filename)) # NOTE: potentially we could average over each period and then average - psi=df['Azimuth_[deg]'].values + psi=df[sA[0]].values _,iBef = _zero_crossings(psi-psi[-2],direction='up') if len(iBef)==0: _,iBef = _zero_crossings(psi-180,direction='up') if len(iBef)==0: - print('[WARN] Not able to find a zero crossing!') + print('[WARN] Not able to find a zero crossing!{}'.format(filename)) tEnd = time[-1] iBef=[0] else: @@ -1321,7 +1594,7 @@ def renameCol(x): else: avgParam=int(avgParam) if len(iBef)-1=tStart) & (time<=tEnd) & (~np.isnan(time)))[0] iEnd = IWindow[-1] iStart = IWindow[0] @@ -1372,10 +1645,15 @@ def renameCol(x): -def averagePostPro(outFiles,avgMethod='periods',avgParam=None,ColMap=None,ColKeep=None,ColSort=None,stats=['mean']): +def averagePostPro(outFiles_or_DFs,avgMethod='periods',avgParam=None, + ColMap=None,ColKeep=None,ColSort=None,stats=['mean'], + skipIfWrongCol=False): """ Opens a list of FAST output files, perform average of its signals and return a panda dataframe For now, the scripts only computes the mean within a time window which may be a constant or a time that is a function of the rotational speed (see `avgMethod`). The script only computes the mean for now. Other stats will be added + INPUTS: + + outFiles_or_DFs: list of fst filenames or dataframes `ColMap` : dictionary where the key is the new column name, and v the old column name. Default: None, output is not sorted @@ -1396,32 +1674,52 @@ def averagePostPro(outFiles,avgMethod='periods',avgParam=None,ColMap=None,ColKee Default: None, full simulation length is used """ result=None - if len(outFiles)==0: - raise Exception('No outFiles provided') + if len(outFiles_or_DFs)==0: + raise Exception('No outFiles or DFs provided') invalidFiles =[] # Loop trough files and populate result - for i,f in enumerate(outFiles): - try: - df=FASTOutputFile(f).toDataFrame() # For pyFAST - except: - invalidFiles.append(f) - continue - postpro=averageDF(df, avgMethod=avgMethod, avgParam=avgParam, ColMap=ColMap, ColKeep=ColKeep,ColSort=ColSort,stats=stats) + for i,f in enumerate(outFiles_or_DFs): + if isinstance(f, pd.DataFrame): + df = f + else: + try: + df=weio.read(f).toDataFrame() + #df=FASTOutputFile(f).toDataFrame()A # For pyFAST + except: + invalidFiles.append(f) + continue + postpro=averageDF(df, avgMethod=avgMethod, avgParam=avgParam, ColMap=ColMap, ColKeep=ColKeep,ColSort=ColSort,stats=stats, filename=f) MeanValues=postpro # todo if result is None: # We create a dataframe here, now that we know the colums columns = MeanValues.columns - result = pd.DataFrame(np.nan, index=np.arange(len(outFiles)), columns=columns) - result.iloc[i,:] = MeanValues.copy().values + result = pd.DataFrame(np.nan, index=np.arange(len(outFiles_or_DFs)), columns=columns) + if MeanValues.shape[1]!=result.shape[1]: + columns_ref = result.columns + columns_loc = MeanValues.columns + if skipIfWrongCol: + print('[WARN] File {} has {} columns and not {}. Skipping.'.format(f, MeanValues.shape[1], result.shape[1])) + else: + try: + MeanValues=MeanValues[columns_ref] + result.iloc[i,:] = MeanValues.copy().values + print('[WARN] File {} has more columns than other files. Truncating.'.format(f, MeanValues.shape[1], result.shape[1])) + except: + print('[WARN] File {} is missing some columns compared to other files. Skipping.'.format(f)) + else: + result.iloc[i,:] = MeanValues.copy().values - if len(invalidFiles)==len(outFiles): + if len(invalidFiles)==len(outFiles_or_DFs): raise Exception('None of the files can be read (or exist)!. For instance, cannot find: {}'.format(invalidFiles[0])) elif len(invalidFiles)>0: print('[WARN] There were {} missing/invalid files: \n {}'.format(len(invalidFiles),'\n'.join(invalidFiles))) if ColSort is not None: + if not ColSort in result.keys(): + print('[INFO] Columns present: ', result.keys()) + raise Exception('[FAIL] Cannot sort results with column `{}`, column not present in dataframe (see above)'.format(ColSort)) # Sorting result.sort_values([ColSort],inplace=True,ascending=True) result.reset_index(drop=True,inplace=True) @@ -1482,4 +1780,8 @@ def integrateMomentTS(r, F): return M if __name__ == '__main__': - main() + + import welib.weio as weio + df = weio.read('ad_driver_yaw.6.outb').toDataFrame() + dfCat = spanwiseConcat(df) + print(dfCat) diff --git a/pyFAST/tools/pandalib.py b/pyFAST/tools/pandalib.py index 4b63ca5..fb648f4 100644 --- a/pyFAST/tools/pandalib.py +++ b/pyFAST/tools/pandalib.py @@ -1,5 +1,7 @@ import pandas as pd import numpy as np +import re + def pd_interp1(x_new, xLabel, df): """ Interpolate a panda dataframe based on a set of new value @@ -22,3 +24,107 @@ def pd_interp1(x_new, xLabel, df): def create_dummy_dataframe(size): return pd.DataFrame(data={'col1': np.linspace(0,1,size), 'col2': np.random.normal(0,1,size)}) + + + +def remap_df(df, ColMap, bColKeepNewOnly=False, inPlace=False, dataDict=None, verbose=False): + """ + NOTE: see welib.fast.postpro + + Add/rename columns of a dataframe, potentially perform operations between columns + + dataDict: dictionary of data to be made available as "variable" in the column mapping + 'key' (new) : value (old) + + Example: + + ColumnMap={ + 'WS_[m/s]' : '{Wind1VelX_[m/s]}' , # create a new column from existing one + 'RtTSR_[-]' : '{RtTSR_[-]} * 2 + {RtAeroCt_[-]}' , # change value of column + 'RotSpeed_[rad/s]' : '{RotSpeed_[rpm]} * 2*np.pi/60 ', # new column [rpm] -> [rad/s] + 'q_p' : ['Q_P_[rad]', '{PtfmSurge_[deg]}*np.pi/180'] # List of possible matches + } + # Read + df = weio.read('FASTOutBin.outb').toDataFrame() + # Change columns based on formulae, potentially adding new columns + df = fastlib.remap_df(df, ColumnMap, inplace=True) + + """ + # Insert dataDict into namespace + if dataDict is not None: + for k,v in dataDict.items(): + exec('{:s} = dataDict["{:s}"]'.format(k,k)) + + + if not inPlace: + df=df.copy() + ColMapMiss=[] + ColNew=[] + RenameMap=dict() + # Loop for expressions + for k0,v in ColMap.items(): + k=k0.strip() + if type(v) is not list: + values = [v] + else: + values = v + Found = False + for v in values: + v=v.strip() + if Found: + break # We avoid replacing twice + if v.find('{')>=0: + # --- This is an advanced substitution using formulae + search_results = re.finditer(r'\{.*?\}', v) + expr=v + if verbose: + print('Attempt to insert column {:15s} with expr {}'.format(k,v)) + # For more advanced operations, we use an eval + bFail=False + for item in search_results: + col=item.group(0)[1:-1] + if col not in df.columns: + ColMapMiss.append(col) + bFail=True + expr=expr.replace(item.group(0),'df[\''+col+'\']') + #print(k0, '=', expr) + if not bFail: + df[k]=eval(expr) + ColNew.append(k) + else: + print('[WARN] Column not present in dataframe, cannot evaluate: ',expr) + else: + #print(k0,'=',v) + if v not in df.columns: + ColMapMiss.append(v) + if verbose: + print('[WARN] Column not present in dataframe: ',v) + else: + if k in RenameMap.keys(): + print('[WARN] Not renaming {} with {} as the key is already present'.format(k,v)) + else: + RenameMap[k]=v + Found=True + + # Applying renaming only now so that expressions may be applied in any order + for k,v in RenameMap.items(): + if verbose: + print('Renaming column {:15s} > {}'.format(v,k)) + k=k.strip() + iCol = list(df.columns).index(v) + df.columns.values[iCol]=k + ColNew.append(k) + df.columns = df.columns.values # Hack to ensure columns are updated + + if len(ColMapMiss)>0: + print('[FAIL] The following columns were not found in the dataframe:',ColMapMiss) + #print('Available columns are:',df.columns.values) + + if bColKeepNewOnly: + ColNew = [c for c,_ in ColMap.items() if c in ColNew]# Making sure we respec order from user + ColKeepSafe = [c for c in ColNew if c in df.columns.values] + ColKeepMiss = [c for c in ColNew if c not in df.columns.values] + if len(ColKeepMiss)>0: + print('[WARN] Signals missing and omitted for ColKeep:\n '+'\n '.join(ColKeepMiss)) + df=df[ColKeepSafe] + return df diff --git a/pyFAST/tools/signal_analysis.py b/pyFAST/tools/signal_analysis.py index f8577b4..1cce7de 100644 --- a/pyFAST/tools/signal_analysis.py +++ b/pyFAST/tools/signal_analysis.py @@ -355,11 +355,12 @@ def applyFilterDF(df_old, x_col, options): # --------------------------------------------------------------------------------} # --- # --------------------------------------------------------------------------------{ -def zero_crossings(y,x=None,direction=None): +def zero_crossings(y, x=None, direction=None, bouncingZero=False): """ Find zero-crossing points in a discrete vector, using linear interpolation. direction: 'up' or 'down', to select only up-crossings or down-crossings + bouncingZero: also returns zeros that are exactly zero and do not change sign returns: x values xzc such that y(yzc)==0 @@ -386,7 +387,8 @@ def zero_crossings(y,x=None,direction=None): # Selecting points that are exactly 0 and where neighbor change sign iZero = np.where(y == 0.0)[0] iZero = iZero[np.where((iZero > 0) & (iZero < x.size-1))] - iZero = iZero[np.where(y[iZero-1]*y[iZero+1] < 0.0)] + if not bouncingZero: + iZero = iZero[np.where(y[iZero-1]*y[iZero+1] < 0.0)] # we only accept zeros that change signs # Concatenate xzc = np.concatenate((xzc, x[iZero])) @@ -405,26 +407,47 @@ def zero_crossings(y,x=None,direction=None): I= np.where(sign==-1)[0] return xzc[I],iBef[I] elif direction is not None: - raise Exception('Direction should be either `up` or `down`') + raise Exception('Direction should be either `up` or `down` or `None`') return xzc, iBef, sign # --------------------------------------------------------------------------------} # --- Correlation # --------------------------------------------------------------------------------{ -def correlation(x, nMax=80, dt=1, method='manual'): +def correlation(x, nMax=80, dt=1, method='numpy'): """ Compute auto correlation of a signal """ + + def acf(x, nMax=20): + return np.array([1]+[np.corrcoef(x[:-i], x[i:])[0,1] for i in range(1, nMax)]) + + nvec = np.arange(0,nMax) - sigma2 = np.var(x) - R = np.zeros(nMax) - R[0] =1 - for i,nDelay in enumerate(nvec[1:]): - R[i+1] = np.mean( x[0:-nDelay] * x[nDelay:] ) / sigma2 + if method=='manual': + sigma2 = np.var(x) + R = np.zeros(nMax) + R[0] =1 + for i,nDelay in enumerate(nvec[1:]): + R[i+1] = np.mean( x[0:-nDelay] * x[nDelay:] ) / sigma2 + #R[i+1] = np.corrcoef(x[:-nDelay], x[nDelay:])[0,1] + + elif method=='numpy': + R= acf(x, nMax=nMax) + else: + raise NotImplementedError() tau = nvec*dt return R, tau +# Auto-correlation comes in two versions: statistical and convolution. They both do the same, except for a little detail: The statistical version is normalized to be on the interval [-1,1]. Here is an example of how you do the statistical one: +# +# +# def autocorr(x): +# result = numpy.correlate(x, x, mode='full') +# return result[result.size/2:] + + + def correlated_signal(coeff, n=1000, seed=None): @@ -521,7 +544,6 @@ def amplitude(x, t=None, T = None, mask=None, debug=False): return A # split signals into subsets - import pdb; pdb.set_trace() else: return (np.max(x)-np.min(x))/2 diff --git a/pyFAST/tools/tictoc.py b/pyFAST/tools/tictoc.py new file mode 100644 index 0000000..2c1bebe --- /dev/null +++ b/pyFAST/tools/tictoc.py @@ -0,0 +1,75 @@ +import numpy as np +import time + +def pretty_time(t): + # fPrettyTime: returns a 6-characters string corresponding to the input time in seconds. + # fPrettyTime(612)=='10m12s' + # AUTHOR: E. Branlard + if(t<0): + s='------'; + elif (t<1) : + c=np.floor(t*100); + s='{:2d}.{:02d}s'.format(0,int(c)) + elif(t<60) : + s=np.floor(t); + c=np.floor((t-s)*100); + s='{:2d}.{:02d}s'.format(int(s),int(c)) + elif(t<3600) : + m=np.floor(t/60); + s=np.mod( np.floor(t), 60); + s='{:2d}m{:02d}s'.format(int(m),int(s)) + elif(t<86400) : + h=np.floor(t/3600); + m=np.floor(( np.mod( np.floor(t) , 3600))/60); + s='{:2d}h{:02d}m'.format(int(h),int(m)) + elif(t<8553600) : #below 3month + d=np.floor(t/86400); + h=np.floor( np.mod(np.floor(t), 86400)/3600); + s='{:2d}d{:02d}h'.format(int(d),int(h)) + elif(t<31536000): + m=t/(3600*24*30.5); + s='{:4.1f}mo'.format(m) + #s='+3mon.'; + else: + y=t/(3600*24*365.25); + s='{:.1f}y'.format(y) + return s + + +class Timer(object): + """ Time a set of commands, as a context manager + usage: + + with Timer('A name'): + cmd1 + cmd2 + """ + def __init__(self, name=None, writeBefore=False, silent=False, nChar=40): + self.name = name + self.writeBefore = writeBefore + self.silent=silent + self.nChar=nChar + self.sFmt='{:'+str(nChar+1)+'s}' + + def ref_str(self): + s='[TIME] ' + if self.name: + s+=self.sFmt.format(self.name[:self.nChar]) + return s + + def __enter__(self): + if self.silent: + return + self.tstart = time.time() + if self.writeBefore: + s=self.ref_str() + print(s,end='') + + def __exit__(self, type, value, traceback): + if self.silent: + return + if self.writeBefore: + print('Elapsed: {:6s}'.format(pretty_time(time.time() - self.tstart))) + else: + s=self.ref_str() + print(s+'Elapsed: {:6s}'.format(pretty_time(time.time() - self.tstart))) diff --git a/setup.py b/setup.py index e0c83f3..659eab1 100644 --- a/setup.py +++ b/setup.py @@ -31,15 +31,15 @@ packages=["pyFAST"], python_requires=">=3.6", install_requires=[ + "matplotlib", + "openpyxl", "numpy>=1.15.2", "pandas", - "matplotlib", - "chardet", + "pyarrow", # for parquet files "scipy", - "sympy", - "openpyxl", + "chardet", + "xarray", # for netcdf files "pytest", - "xarray" ], test_suite="pytest", tests_require=["pytest"], diff --git a/subdyn.py b/subdyn.py new file mode 100644 index 0000000..bdc9507 --- /dev/null +++ b/subdyn.py @@ -0,0 +1,908 @@ +""" +Tools for SubDyn + +- Setup a FEM model, compute Guyan and CB modes +- Get a dataframe with properties +- More todo + +""" + + +import numpy as np +import pandas as pd +import matplotlib.pyplot as plt +import copy +import re +# Local +from pyFAST.input_output.fast_input_file import FASTInputFile +from pyFAST.tools.tictoc import Timer + +idGuyanDamp_None = 0 +idGuyanDamp_Rayleigh = 1 +idGuyanDamp_66 = 2 + +class SubDyn: + + def __init__(self, sdFilename_or_data=None): + """ + Initialize a SubDyn object either with: + - sdFilename: a subdyn input file name + - sdData: an instance of FASTInputFile + """ + + self._graph=None + self.File=None + + # Read SubDyn file + if sdFilename_or_data is not None: + if hasattr(sdFilename_or_data,'startswith'): # if string + self.File = FASTInputFile(sdFilename_or_data) + else: + self.File = sdFilename_or_data + + self.M_tip=None + + # Internal + self._graph=None + self._mgraph=None # Member graph + self._FEM=None + + def __repr__(self): + s='<{} object>:\n'.format(type(self).__name__) + s+='|properties:\n' + s+='|- File: (input file data)\n' + s+='|* graph: (Nodes/Elements/Members)\n' + s+='|* pointsMJ, pointsMN, pointsMNout\n' + s+='|methods:\n' + s+='|- memberPostPro\n' + s+='|- setTopMass\n' + s+='|- beamDataFrame, beamFEM, beamModes\n' + s+='|- toYAMSData\n' + return s + + # --------------------------------------------------------------------------------} + # --- Functions for general FEM model (jacket, flexible floaters) + # --------------------------------------------------------------------------------{ + def init(self, TP=(0,0,0), gravity = 9.81): + """ + Initialize SubDyn FEM model + + TP: position of transition point + gravity: position of transition point + """ + import welib.FEM.fem_beam as femb + import welib.FEM.fem_model as femm + BC = 'clamped-free' # TODO Boundary condition: free-free or clamped-free + element = 'frame3d' # Type of element used in FEM + + FEMMod = self.File['FEMMod'] + if FEMMod==1: + mainElementType='frame3d' + elif FEMMod==2: + mainElementType='frame3dlin' + elif FEMMod==3: + mainElementType='timoshenko' + else: + raise NotImplementedError() + # Get graph + graph = self.graph + #print('>>> graph\n',graph) + #graph.toJSON('_GRAPH.json') + # Convert to FEM model + with Timer('From graph'): + FEM = femm.FEMModel.from_graph(self.graph, mainElementType=mainElementType, refPoint=TP, gravity=gravity) + #model.toJSON('_MODEL.json') + with Timer('Assembly'): + FEM.assembly() + with Timer('Internal constraints'): + FEM.applyInternalConstraints() + FEM.partition() + with Timer('BC'): + FEM.applyFixedBC() + with Timer('EIG'): + Q, freq = FEM.eig(normQ='byMax') + with Timer('CB'): + FEM.CraigBampton(nModesCB = self.File['Nmodes']) + + with Timer('Modes'): + FEM.setModes(nModesFEM=30, nModesCB=self.File['Nmodes']) +# FEM.nodesDisp(Q) + + # --- SubDyn partition/notations + FEM.MBB = FEM.MM_CB[np.ix_(FEM.DOF_Leader_CB , FEM.DOF_Leader_CB)] + FEM.KBB = FEM.KK_CB[np.ix_(FEM.DOF_Leader_CB , FEM.DOF_Leader_CB)] + FEM.MBM = FEM.MM_CB[np.ix_(FEM.DOF_Leader_CB , FEM.DOF_Follower_CB)] + FEM.KMM = FEM.KK_CB[np.ix_(FEM.DOF_Follower_CB, FEM.DOF_Follower_CB)] + zeta =self.File['JDampings']/100 + if not hasattr(zeta,'__len__'): + zeta = [zeta]*FEM.nModesCB + FEM.CMM = 2*np.array(zeta) * FEM.f_CB * 2 * np.pi + + # --- Matrices wrt TP point + TI=FEM.T_refPoint + MBBt = TI.T.dot(FEM.MBB).dot(TI) + KBBt = TI.T.dot(FEM.KBB).dot(TI) + MBBt[np.abs(MBBt)<1e-4] =0 + KBBt[np.abs(KBBt)<1e-4] =0 + FEM.MBBt = MBBt + FEM.KBBt = KBBt + + # --- Set Damping + dampMod = self.File['GuyanDampMod'] + alpha_Rayleigh, beta_Rayleigh = None, None + # 6x6 Guyan Damping matrix + CC_CB_G = None + if dampMod == idGuyanDamp_None: + FEM.CBBt = np.zeros((6,6)) + elif dampMod == idGuyanDamp_Rayleigh: + # Rayleigh Damping + alpha_Rayleigh, beta_Rayleigh = self.File['RayleighDamp'] + FEM.CBBt = alpha_Rayleigh * FEM.MBBt + beta_Rayleigh * FEM.KBBt + elif dampMod == idGuyanDamp_66: + FEM.CBBt = self.File['GuyanDampMatrix'] + else: + raise Exception() + + # --- Compute rigid body equivalent + FEM.rigidBodyEquivalent() + self._FEM = FEM + + return FEM + + + def setTopMass(self): + # TODO + # Add an optional top mass and ineria + if TopMass: + # NOTE: you can use welib.yams.windturbine to compute RNA mass and inertia + Mtop = 50000 # Top mass [kg] + M_tip= rigidBodyMassMatrixAtP(m=Mtop, J_G=None, Ref2COG=None) + else: + M_tip=None + + + def getGraph(self, nDiv=1): + # See welib.weio.fast_input_file_graph.py to see how SubDyn files are converted to graph + # See welib.fem.graph.py for Graph interface + _graph = self.File.toGraph().divideElements(nDiv, + excludeDataKey='Type', excludeDataList=['Cable','Rigid'], method='insert', + keysNotToCopy=['IBC','RBC','addedMassMatrix'] # Very important + ) + + if len(_graph.Members)==0: + raise Exception('Problem in graph subdivisions, no members found.') + # Sanitization + #idBC_Fixed = 0 # Fixed BC + #idBC_Internal = 10 # Free/Internal BC + #idBC_Leader = 20 # Leader DOF + MapIBC={0:0, 1:20} # 1 in the input file is leader + MapRBC={0:10, 1:0} # 1 in the input file is fixed + for n in _graph.Nodes: + #print(n) + if 'IBC' in n.data.keys(): + IBC = n.data['IBC'].copy() + n.data['IBC'] = [MapIBC[i] for i in IBC[:6]] + if 'RBC' in n.data.keys(): + RBC = n.data['RBC'].copy() + n.data['RBC'] = [MapRBC[i] for i in RBC[:6]] + if any(RBC[:6])==0: + print('RBC ',RBC) + print('n.data[RBC]',n.data['RBC'] ) + print('n ',n ) + raise NotImplementedError('SSI') + + return _graph + + @property + def graph(self): + if self._graph is None: + self._graph = self.getGraph(nDiv = self.File['NDiv']) + return copy.deepcopy(self._graph) + + + @property + def pointsMJ(self): + """ return a dataframe with the coordinates of all members and joints + The index corresponds to the SubDyn outputs "M_J_XXX" + """ + Joints=[] + labels =[] + graph = self.graph + for ie,M in enumerate(graph.Members): # loop on members + Nodes = M.getNodes(graph) + for iN,N in enumerate([Nodes[0], Nodes[-1]]): + s='M{}J{}'.format(ie+1, iN+1) + Joints.append([N.x,N.y,N.z]) + labels.append(s) + df =pd.DataFrame(data=np.asarray(Joints), index=labels, columns=['x','y','z']) + return df + + @property + def pointsMN(self): + """ return a dataframe with the coordinates of all members and nodes + The index would correspond to the SubDyn outputs "M_N_XXX *prior* to the user selection" + """ + Nodes=[] + labels =[] + graph = self.graph + for im,M in enumerate(graph.Members): # loop on members + nodes = M.getNodes(graph) + for iN,N in enumerate(nodes): # Loop on member nodes + s='M{}N{}'.format(im+1, iN+1) + Nodes.append([N.x,N.y,N.z]) + labels.append(s) + df =pd.DataFrame(data=np.asarray(Nodes), index=labels, columns=['x','y','z']) + return df + + @property + def pointsMNout(self): + """ return a dataframe with the coordinates of members and nodes requested by user + The index corresponds to the SubDyn outputs "M_N_XXX selected by the user" + """ + Nodes=[] + labels =[] + graph = self.graph + for im, out in enumerate(self.File['MemberOuts']): + mID = out[0] # Member ID + iNodes = np.array(out[2:])-1 # Node positions within member (-1 for python indexing) + nodes = graph.getMemberNodes(mID) + nodes = np.array(nodes)[iNodes] + for iN,N in enumerate(nodes): # Loop on selected nodes + s='M{}N{}'.format(im+1, iN+1) + Nodes.append([N.x,N.y,N.z]) + labels.append(s) + df =pd.DataFrame(data=np.asarray(Nodes), index=labels, columns=['x','y','z']) + return df + + + def memberPostPro(self, dfAvg): + """ + Convert a dataframe of SubDyn/OpenFAST outputs (time-averaged) + with columns such as: M_N_* and M_J_* + into a dataframe that is organized by main channel name and nodal coordinates. + The scripts taken into account with member ID and node the user requested as outputs channels. + Discretization (nDIV) is also taken into account. + + For instance: + dfAvg with columns = ['M1N1MKye_[N*m]', 'M1N2MKye_[N*m]', 'M1N1TDxss_[m]'] + returns: + MNout with columns ['x', 'y', 'z', 'MKye_[Nm]', 'TDxss_[m]'] + and index ['M1N1', 'M1N2'] + with x,y,z the undisplaced nodal positions (accounting for discretization) + + INPUTS: + - dfAvg: a dataframe of time-averaged SubDyn/OpenFAST outputs, for instance obtained as: + df = FASTInputFile(filename).toDataFrame() + dfAvg = postpro.averageDF(df, avgMethod=avgMethod ,avgParam=avgParam) + OUTPUTS + - MNout: dataframe of members outputs (requested by the user) + - MJout: dataframe of joints outputs + """ + import welib.fast.postpro as postpro # Import done here to avoid circular dependency + + # --- Get Points where output are requested + MJ = self.pointsMJ + MNo= self.pointsMNout + MJ.columns = ['x_[m]','y_[m]', 'z_[m]'] + MNo.columns = ['x_[m]','y_[m]', 'z_[m]'] + + # --- Requested Member Outputs + Map={} + Map['^'+r'M(\d*)N(\d*)TDxss_\[m\]'] = 'TDxss_[m]' + Map['^'+r'M(\d*)N(\d*)TDyss_\[m\]'] = 'TDyss_[m]' + Map['^'+r'M(\d*)N(\d*)TDzss_\[m\]'] = 'TDzss_[m]' + Map['^'+r'M(\d*)N(\d*)RDxe_\[rad\]'] = 'RDxe_[deg]' # NOTE rescale needed + Map['^'+r'M(\d*)N(\d*)RDye_\[rad\]'] = 'RDye_[deg]' # NOTE rescale needed + Map['^'+r'M(\d*)N(\d*)RDze_\[rad\]'] = 'RDze_[deg]' # NOTE rescale needed + Map['^'+r'M(\d*)N(\d*)FKxe_\[N\]'] = 'FKxe_[N]' + Map['^'+r'M(\d*)N(\d*)FKye_\[N\]'] = 'FKye_[N]' + Map['^'+r'M(\d*)N(\d*)FKze_\[N\]'] = 'FKze_[N]' + Map['^'+r'M(\d*)N(\d*)MKxe_\[N\*m\]'] = 'MKxe_[Nm]' + Map['^'+r'M(\d*)N(\d*)MKye_\[N\*m\]'] = 'MKye_[Nm]' + Map['^'+r'M(\d*)N(\d*)MKze_\[N\*m\]'] = 'MKze_[Nm]' + ColsInfo, _ = postpro.find_matching_columns(dfAvg.columns, Map) + nCols = len(ColsInfo) + if nCols>0: + newCols=[c['name'] for c in ColsInfo ] + ValuesM = pd.DataFrame(index=MNo.index, columns=newCols) + for ic,c in enumerate(ColsInfo): + Idx, cols, colname = c['Idx'], c['cols'], c['name'] + labels = [re.match(r'(^M\d*N\d*)', s)[0] for s in cols] + ValuesM.loc[labels,colname] = dfAvg[cols].values.flatten() + if 'deg' in colname and 'rad' in cols[0]: + ValuesM[colname] *= 180/np.pi + # We remove lines that are all NaN + Values = ValuesM.dropna(axis = 0, how = 'all') + MNo2 = MNo.loc[Values.index] + MNout = pd.concat((MNo2, Values), axis=1) + else: + MNout = None + + # --- Joint Outputs + Map={} + Map['^'+r'M(\d*)J(\d*)FKxe_\[N\]'] ='FKxe_[N]' + Map['^'+r'M(\d*)J(\d*)FKye_\[N\]'] ='FKye_[N]' + Map['^'+r'M(\d*)J(\d*)FKze_\[N\]'] ='FKze_[N]' + Map['^'+r'M(\d*)J(\d*)MKxe_\[N\*m\]']='MKxe_[Nm]' + Map['^'+r'M(\d*)J(\d*)MKye_\[N\*m\]']='MKye_[Nm]' + Map['^'+r'M(\d*)J(\d*)MKze_\[N\*m\]']='MKze_[Nm]' + Map['^'+r'M(\d*)J(\d*)FMxe_\[N\]'] ='FMxe_[N]' + Map['^'+r'M(\d*)J(\d*)FMye_\[N\]'] ='FMye_[N]' + Map['^'+r'M(\d*)J(\d*)FMze_\[N\]'] ='FMze_[N]' + Map['^'+r'M(\d*)J(\d*)MMxe_\[N\*m\]']='MMxe_[Nm]' + Map['^'+r'M(\d*)J(\d*)MMye_\[N\*m\]']='MMye_[Nm]' + Map['^'+r'M(\d*)J(\d*)MMze_\[N\*m\]']='MMze_[Nm]' + ColsInfo, _ = postpro.find_matching_columns(dfAvg.columns, Map) + nCols = len(ColsInfo) + if nCols>0: + newCols=[c['name'] for c in ColsInfo ] + ValuesJ = pd.DataFrame(index=MJ.index, columns=newCols) + for ic,c in enumerate(ColsInfo): + Idx, cols, colname = c['Idx'], c['cols'], c['name'] + labels = [re.match(r'(^M\d*J\d*)', s)[0] for s in cols] + ValuesJ.loc[labels,colname] = dfAvg[cols].values.flatten() + # We remove lines that are all NaN + Values = ValuesJ.dropna(axis = 0, how = 'all') + MJ2 = MJ.loc[Values.index] + MJout = pd.concat((MJ2, Values), axis=1) + else: + MJout = None + return MNout, MJout + + + + + # --------------------------------------------------------------------------------} + # --- Functions for beam-like structure (Spar, Monopile) + # --------------------------------------------------------------------------------{ + def beamDataFrame(self, equispacing=False): + """ """ + # --- Parameters + UseSubDynModel=True + TopMass = False + + + # Convert to "welib.fem.Graph" class to easily handle the model (overkill for a monopile) + locgraph = self.graph.sortNodesBy('z') + # Add nodal properties from propsets (NOTE: Not done anymore by SubDyn because a same node can have different diameters...) + for e in locgraph.Elements: + locgraph.setElementNodalProp(e, propset=e.propset, propIDs=e.propIDs) + df = locgraph.nodalDataFrame() + + if equispacing: + from welib.tools.pandalib import pd_interp1 + # Interpolate dataframe to equispaced values + xOld = df['z'] # NOTE: FEM uses "x" as main axis + nSpan = len(xOld) + x = np.linspace(np.min(xOld),np.max(xOld), nSpan) + df = pd_interp1(x, 'z', df) + + x = df['z'] # NOTE: FEM uses "x" as main axis + D = df['D'] # Diameter [m] + t = df['t'] # thickness [m] + # Derive section properties for a hollow cylinder based on diameter and thickness + A = np.pi*( (D/2)**2 - (D/2-t)**2) # Area for annulus [m^2] + I = np.pi/64*(D**4-(D-2*t)**4) # Second moment of area for annulus (m^4) + Kt = I # Torsion constant, same as I for annulus [m^4] + Ip = 2*I # Polar second moment of area [m^4] + df['A'] = A + df['I'] = I + df['Kt'] = Kt + df['Ip'] = Ip + df['m'] = df['rho'].values*A + + return df + + def beamFEM(self, df=None): + """ return FEM model for beam-like structures, like Spar/Monopile""" + import welib.FEM.fem_beam as femb + + BC = 'clamped-free' # TODO Boundary condition: free-free or clamped-free + element = 'frame3d' # Type of element used in FEM + + if df is None: + df = self.beamDataFrame() + x = df['z'] # NOTE: FEM uses "x" as main axis + E = df['E'] # Young modules [N/m^2] + G = df['G'] # Shear modules [N/m^2] + rho = df['rho'] # material density [kg/m^3] + Ip = df['Ip'] + I = df['I'] + A = df['A'] + Kt = df['Kt'] + + # --- Compute FEM model and mode shapes + with Timer('Setting up FEM model'): + FEM=femb.cbeam(x,m=rho*A,EIx=E*Ip,EIy=E*I,EIz=E*I,EA=E*A,A=A,E=E,G=G,Kt=Kt, + element=element, BC=BC, M_tip=self.M_tip) + return FEM + + def beamModes(self, nCB=8, FEM = None): + """ Returns mode shapes for beam-like structures, like Spar/Monopile """ + import welib.FEM.fem_beam as femb + element = 'frame3d' # Type of element used in FEM + if FEM is None: + FEM = self.beamFEM() + # --- Perform Craig-Bampton reduction, fixing the top node of the beam + with Timer('FEM eigenvalue analysis'): + Q_G,_Q_CB, df_G, df_CB, Modes_G, Modes_CB, CB = femb.CB_topNode(FEM, nCB=nCB, element=element, main_axis='x') + # df_CB.to_csv('_CB.csv',index=False) + # df_G.to_csv('_Guyan.csv',index=False) + return Q_G,_Q_CB, df_G, df_CB, Modes_G, Modes_CB, CB + + def beamModesPlot(self): + """ """ + # TODO + nModesPlot=8 + # --- Show frequencies to screen + print('Mode Frequency Label ') + for i in np.arange(8): + print('{:4d} {:10.3f} {:s}'.format(i+1,FEM['freq'][i],FEM['modeNames'][i])) + + # --- Plot mode components for first few modes + print(x.shape) + #Q=FEM['Q'] ; modeNames = FEM['modeNames'] + #Q=Q_CB ;modeNames = names_CB + Modes=Modes_CB + nModesPlot=min(len(Modes),nModesPlot) + + fig,axes = plt.subplots(1, nModesPlot, sharey=False, figsize=(12.4,2.5)) + fig.subplots_adjust(left=0.04, right=0.98, top=0.91, bottom=0.11, hspace=0.40, wspace=0.30) + for i in np.arange(nModesPlot): + key= list(Modes.keys())[i] + + axes[i].plot(x, Modes[key]['comp'][:,0] ,'-' , label='ux') + axes[i].plot(x, Modes[key]['comp'][:,1] ,'-' , label='uy') + axes[i].plot(x, Modes[key]['comp'][:,2] ,'-' , label='uz') + axes[i].plot(x, Modes[key]['comp'][:,3] ,':' , label='vx') + axes[i].plot(x, Modes[key]['comp'][:,4] ,':' , label='vy') + axes[i].plot(x, Modes[key]['comp'][:,5] ,':' , label='vz') + axes[i].set_xlabel('') + axes[i].set_ylabel('') + axes[i].set_title(Modes[key]['label']) + if i==0: + axes[i].legend() + + + + + # --------------------------------------------------------------------------------} + # --- IO/Converters + # --------------------------------------------------------------------------------{ + def toYAML(self, filename): + if self._FEM is None: + raise Exception('Call `initFEM()` before calling `toYAML`') + subdyntoYAMLSum(self._FEM, filename, more = self.File['OutAll']) + + + def toYAMSData(self, shapes=[0,4], main_axis='z'): + """ + Convert to Data needed to setup a Beam Model in YAMS (see bodies.py in yams) + """ + from welib.mesh.gradient import gradient_regular + + # --- Perform Craig-Bampton reduction, fixing the top node of the beam + # Get beam data frame + df = self.beamDataFrame(equispacing=True) + if np.any(df['y']!=0): + raise NotImplementedError('FASTBeamBody for substructure only support monopile, structure not fully vertical in file: {}'.format(self.File.filename)) + if np.any(df['x']!=0): + raise NotImplementedError('FASTBeamBody for substructure only support monopile, structure not fully vertical in file: {}'.format(self.File.filename)) + + FEM = self.beamFEM(df) + Q_G,_Q_CB, df_G, df_CB, Modes_G, Modes_CB, CB = self.beamModes(nCB=0, FEM=FEM) + + x = df['z'].values + nSpan = len(x) + + # TODO TODO finda way to use these matrices instead of the ones computed with flexibility + #print('CB MM\n',CB['MM']) + #print('CB KK\n',CB['KK']) + + # --- Setup shape functions + if main_axis=='x': + raise NotImplementedError('') + else: + pass + # we need to swap the CB modes + nShapes=len(shapes) + PhiU = np.zeros((nShapes,3,nSpan)) # Shape + PhiV = np.zeros((nShapes,3,nSpan)) # Shape + PhiK = np.zeros((nShapes,3,nSpan)) # Shape + dx=np.unique(np.around(np.diff(x),4)) + if len(dx)>1: + print(x) + print(dx) + raise NotImplementedError() + for iShape, idShape in enumerate(shapes): + if idShape==0: + # shape 0 "ux" (uz in FEM) + PhiU[iShape][0,:] = df_G['G3_uz'].values + PhiV[iShape][0,:] =-df_G['G3_ty'].values + PhiK[iShape][0,:] = gradient_regular(PhiV[iShape][0,:],dx=dx[0],order=4) + elif idShape==1: + # shape 1, "uy" + PhiU[iShape][1,:] = df_G['G2_uy'].values + PhiV[iShape][1,:] = df_G['G2_tz'].values + PhiK[iShape][1,:] = gradient_regular(PhiV[iShape][1,:],dx=dx[0],order=4) + elif idShape==4: + # shape 4, "vy" (vz in FEM) + PhiU[iShape][0,:] = df_G['G6_uy'].values + PhiV[iShape][0,:] = df_G['G6_tz'].values + PhiK[iShape][0,:] = gradient_regular(PhiV[iShape][0,:],dx=dx[0],order=4) + else: + raise NotImplementedError() + + # --- Dictionary structure for YAMS + p=dict() + p['s_span']=x-np.min(x) + p['s_P0']=np.zeros((3,nSpan)) + if main_axis=='z': + p['s_P0'][2,:]=x-np.min(x) + p['r_O'] = (df['x'].values[0], df['y'].values[0], df['z'].values[0]) + p['R_b2g'] = np.eye(3) + p['m'] = df['m'].values + p['EI'] = np.zeros((3,nSpan)) + if main_axis=='z': + p['EI'][0,:]=df['E'].values*df['I'].values + p['EI'][1,:]=df['E'].values*df['I'].values + p['jxxG'] = df['rho']*df['Ip'] # TODO verify + p['s_min'] = p['s_span'][0] + p['s_max'] = p['s_span'][-1] + p['PhiU'] = PhiU + p['PhiV'] = PhiV + p['PhiK'] = PhiK + + # --- Damping + damp_zeta = None + RayleighCoeff = None + DampMat = None + if self.File['GuyanDampMod']==1: + # Rayleigh Damping + RayleighCoeff=self.File['RayleighDamp'] + #if RayleighCoeff[0]==0: + # damp_zeta=omega*RayleighCoeff[1]/2. + elif self.File['GuyanDampMod']==2: + # Full matrix + DampMat = self.File['GuyanDampMatrix'] + DampMat=DampMat[np.ix_(shapes,shapes)] + + return p, damp_zeta, RayleighCoeff, DampMat + + +# --------------------------------------------------------------------------------} +# --- Export of summary file and Misc FEM variables used by SubDyn +# --------------------------------------------------------------------------------{ +def yaml_array(var, M, Fmt='{:15.6e}', comment=''): + M = np.atleast_2d(M) + if len(comment)>0: + s='{}: # {} x {} {}\n'.format(var, M.shape[0], M.shape[1], comment) + else: + s='{}: # {} x {}\n'.format(var, M.shape[0], M.shape[1]) + + if M.shape[0]==1: + if M.shape[1]==0: + s+= ' - [ ]\n' + else: + for l in M: + s+= ' - [' + ','.join([Fmt.format(le) for le in l]) + ',]\n' + else: + for l in M: + s+= ' - [' + ','.join([Fmt.format(le) for le in l]) + ']\n' + s = s.replace('e+','E+').replace('e-','E-') + return s + + + +def subdynPartitionVars(model): + from welib.FEM.fem_elements import idDOF_Leader, idDOF_Fixed, idDOF_Internal + # --- Count nodes per types + nNodes = len(model.Nodes) + nNodes_I = len(model.interfaceNodes) + nNodes_C = len(model.reactionNodes) + nNodes_L = len(model.internalNodes) + + # --- Partition Nodes: Nodes_L = IAll - NodesR + Nodes_I = [n.ID for n in model.interfaceNodes] + Nodes_C = [n.ID for n in model.reactionNodes] + Nodes_R = Nodes_I + Nodes_C + Nodes_L = [n.ID for n in model.Nodes if n.ID not in Nodes_R] + + # --- Count DOFs - NOTE: we count node by node + nDOF___ = sum([len(n.data['DOFs_c']) for n in model.Nodes]) + # Interface DOFs + nDOFI__ = sum([len(n.data['DOFs_c']) for n in model.interfaceNodes]) + nDOFI_B = sum([sum(np.array(n.data['IBC'])==idDOF_Leader) for n in model.interfaceNodes]) + nDOFI_F = sum([sum(np.array(n.data['IBC'])==idDOF_Fixed ) for n in model.interfaceNodes]) + if nDOFI__!=nDOFI_B+nDOFI_F: raise Exception('Wrong distribution of interface DOFs') + # DOFs of reaction nodes + nDOFC__ = sum([len(n.data['DOFs_c']) for n in model.reactionNodes]) + nDOFC_B = sum([sum(np.array(n.data['RBC'])==idDOF_Leader) for n in model.reactionNodes]) + nDOFC_F = sum([sum(np.array(n.data['RBC'])==idDOF_Fixed) for n in model.reactionNodes]) + nDOFC_L = sum([sum(np.array(n.data['RBC'])==idDOF_Internal) for n in model.reactionNodes]) + if nDOFC__!=nDOFC_B+nDOFC_F+nDOFC_L: raise Exception('Wrong distribution of reaction DOFs') + # DOFs of reaction + interface nodes + nDOFR__ = nDOFI__ + nDOFC__ # Total number, used to be called "nDOFR" + # DOFs of internal nodes + nDOFL_L = sum([len(n.data['DOFs_c']) for n in model.internalNodes]) + if nDOFL_L!=nDOF___-nDOFR__: raise Exception('Wrong distribution of internal DOF') + # Total number of DOFs in each category: + nDOF__B = nDOFC_B + nDOFI_B + nDOF__F = nDOFC_F + nDOFI_F + nDOF__L = nDOFC_L + nDOFL_L + + # --- Distibutes the I, L, C nodal DOFs into B, F, L sub-categories + # NOTE: order is importatn for compatibility with SubDyn + IDI__ = [] + IDI_B = [] + IDI_F = [] + for n in model.interfaceNodes: + IDI__ += n.data['DOFs_c'] # NOTE: respects order + IDI_B += [dof for i,dof in enumerate(n.data['DOFs_c']) if n.data['IBC'][i]==idDOF_Leader] + IDI_F += [dof for i,dof in enumerate(n.data['DOFs_c']) if n.data['IBC'][i]==idDOF_Fixed ] + IDI__ = IDI_B+IDI_F + IDC__ = [] + IDC_B = [] + IDC_L = [] + IDC_F = [] + for n in model.reactionNodes: + IDC__ += n.data['DOFs_c'] # NOTE: respects order + IDC_B += [dof for i,dof in enumerate(n.data['DOFs_c']) if n.data['RBC'][i]==idDOF_Leader ] + IDC_L += [dof for i,dof in enumerate(n.data['DOFs_c']) if n.data['RBC'][i]==idDOF_Internal] + IDC_F += [dof for i,dof in enumerate(n.data['DOFs_c']) if n.data['RBC'][i]==idDOF_Fixed ] + IDR__=IDC__+IDI__ + IDL_L = [] + for n in model.internalNodes: + IDL_L += n.data['DOFs_c'] + + # Storing variables similar to SubDyn + SD_Vars={} + SD_Vars['nDOF___']=nDOF___; + SD_Vars['nDOFI__']=nDOFI__; SD_Vars['nDOFI_B']=nDOFI_B; SD_Vars['nDOFI_F']=nDOFI_F; + SD_Vars['nDOFC__']=nDOFC__; SD_Vars['nDOFC_B']=nDOFC_B; SD_Vars['nDOFC_F']=nDOFC_F; SD_Vars['nDOFC_L']=nDOFC_L; + SD_Vars['nDOFR__']=nDOFR__; SD_Vars['nDOFL_L']=nDOFL_L; + SD_Vars['nDOF__B']=nDOF__B; SD_Vars['nDOF__F']=nDOF__F; SD_Vars['nDOF__L']=nDOF__L; + SD_Vars['IDC__']=IDC__; + SD_Vars['IDC_B']=IDC_B; + SD_Vars['IDC_F']=IDC_F; + SD_Vars['IDC_L']=IDC_L; + SD_Vars['IDI__']=IDI__; + SD_Vars['IDR__']=IDR__; + SD_Vars['IDI_B']=IDI_B; + SD_Vars['IDI_F']=IDI_F; + SD_Vars['IDL_L']=IDL_L; + SD_Vars['ID__B']=model.DOFc_Leader + SD_Vars['ID__F']=model.DOFc_Fixed + SD_Vars['ID__L']=model.DOFc_Follower + return SD_Vars + +def subdyntoYAMLSum(model, filename, more=False): + """ + Write a YAML summary file, similar to SubDyn + """ + # --- Helper functions + def nodeID(nodeID): + if hasattr(nodeID,'__len__'): + return [model.Nodes.index(model.getNode(n))+1 for n in nodeID] + else: + return model.Nodes.index(model.getNode(nodeID))+1 + + def elemID(elemID): + #e=model.getElement(elemID) + for ie,e in enumerate(model.Elements): + if e.ID==elemID: + return ie+1 + def elemType(elemType): + from welib.FEM.fem_elements import idMemberBeam, idMemberCable, idMemberRigid + return {'SubDynBeam3d':idMemberBeam, 'SubDynFrame3d':idMemberBeam, 'Beam':idMemberBeam, 'Frame3d':idMemberBeam, + 'SubDynTimoshenko3d':idMemberBeam, + 'SubDynCable3d':idMemberCable, 'Cable':idMemberCable, + 'Rigid':idMemberRigid, + 'SubDynRigid3d':idMemberRigid}[elemType] + + def propID(propID, propset): + prop = model.NodePropertySets[propset] + for ip, p in enumerate(prop): + if p.ID == propID: + return ip+1 + + SD_Vars = subdynPartitionVars(model) + + # --- Helper functions + s='' + s += '#____________________________________________________________________________________________________\n' + s += '# RIGID BODY EQUIVALENT DATA\n' + s += '#____________________________________________________________________________________________________\n' + s0 = 'Mass: {:15.6e} # Total Mass\n'.format(model.M_O[0,0]) + s += s0.replace('e+','E+').replace('e-','E-') + s0 = 'CM_point: [{:15.6e},{:15.6e},{:15.6e},] # Center of mass coordinates (Xcm,Ycm,Zcm)\n'.format(model.center_of_mass[0],model.center_of_mass[1],model.center_of_mass[2]) + s += s0.replace('e+','E+').replace('e-','E-') + s0 = 'TP_point: [{:15.6e},{:15.6e},{:15.6e},] # Transition piece reference point\n'.format(model.refPoint[0],model.refPoint[1],model.refPoint[2]) + s += s0.replace('e+','E+').replace('e-','E-') + s += yaml_array('MRB', model.M_O, comment = 'Rigid Body Equivalent Mass Matrix w.r.t. (0,0,0).') + s += yaml_array('M_P' , model.M_ref,comment = 'Rigid Body Equivalent Mass Matrix w.r.t. TP Ref point') + s += yaml_array('M_G' , model.M_G, comment = 'Rigid Body Equivalent Mass Matrix w.r.t. CM (Xcm,Ycm,Zcm).') + s += '#____________________________________________________________________________________________________\n' + s += '# GUYAN MATRICES at the TP reference point\n' + s += '#____________________________________________________________________________________________________\n' + s += yaml_array('KBBt' , model.KBBt, comment = '') + s += yaml_array('MBBt' , model.MBBt, comment = '') + s += yaml_array('CBBt' , model.CBBt, comment = '(user Guyan Damping + potential joint damping from CB-reduction)') + s += '#____________________________________________________________________________________________________\n' + s += '# SYSTEM FREQUENCIES\n' + s += '#____________________________________________________________________________________________________\n' + s += '#Eigenfrequencies [Hz] for full system, with reaction constraints (+ Soil K/M + SoilDyn K0) \n' + s += yaml_array('Full_frequencies', model.freq) + s += '#Frequencies of Guyan modes [Hz]\n' + s += yaml_array('GY_frequencies', model.f_G) + s += '#Frequencies of Craig-Bampton modes [Hz]\n' + s += yaml_array('CB_frequencies', model.f_CB) + s += '#____________________________________________________________________________________________________\n' + s += '# Internal FEM representation\n' + s += '#____________________________________________________________________________________________________\n' + s += 'nNodes_I: {:7d} # Number of Nodes: "interface" (I)\n'.format(len(model.interfaceNodes)) + s += 'nNodes_C: {:7d} # Number of Nodes: "reactions" (C)\n'.format(len(model.reactionNodes)) + s += 'nNodes_L: {:7d} # Number of Nodes: "internal" (L)\n'.format(len(model.internalNodes)) + s += 'nNodes : {:7d} # Number of Nodes: total (I+C+L)\n'.format(len(model.Nodes)) + if more: + s += 'nDOFI__ : {:7d} # Number of DOFs: "interface" (I__)\n'.format(len(SD_Vars['IDI__'])) + s += 'nDOFI_B : {:7d} # Number of DOFs: "interface" retained (I_B)\n'.format(len(SD_Vars['IDI_B'])) + s += 'nDOFI_F : {:7d} # Number of DOFs: "interface" fixed (I_F)\n'.format(len(SD_Vars['IDI_F'])) + s += 'nDOFC__ : {:7d} # Number of DOFs: "reactions" (C__)\n'.format(len(SD_Vars['IDC__'])) + s += 'nDOFC_B : {:7d} # Number of DOFs: "reactions" retained (C_B)\n'.format(len(SD_Vars['IDC_B'])) + s += 'nDOFC_L : {:7d} # Number of DOFs: "reactions" internal (C_L)\n'.format(len(SD_Vars['IDC_L'])) + s += 'nDOFC_F : {:7d} # Number of DOFs: "reactions" fixed (C_F)\n'.format(len(SD_Vars['IDC_F'])) + s += 'nDOFR__ : {:7d} # Number of DOFs: "intf+react" (__R)\n'.format(len(SD_Vars['IDR__'])) + s += 'nDOFL_L : {:7d} # Number of DOFs: "internal" internal (L_L)\n'.format(len(SD_Vars['IDL_L'])) + s += 'nDOF__B : {:7d} # Number of DOFs: retained (__B)\n'.format(SD_Vars['nDOF__B']) + s += 'nDOF__L : {:7d} # Number of DOFs: internal (__L)\n'.format(SD_Vars['nDOF__L']) + s += 'nDOF__F : {:7d} # Number of DOFs: fixed (__F)\n'.format(SD_Vars['nDOF__F']) + s += 'nDOF_red: {:7d} # Number of DOFs: total\n' .format(SD_Vars['nDOF___']) + s += yaml_array('Nodes_I', nodeID([n.ID for n in model.interfaceNodes]), Fmt='{:7d}', comment='"interface" nodes"'); + s += yaml_array('Nodes_C', nodeID([n.ID for n in model.reactionNodes ]), Fmt='{:7d}', comment='"reaction" nodes"'); + s += yaml_array('Nodes_L', nodeID([n.ID for n in model.internalNodes ]), Fmt='{:7d}', comment='"internal" nodes"'); + if more: + s += yaml_array('DOF_I__', np.array(SD_Vars['IDI__'])+1, Fmt='{:7d}', comment = '"interface" DOFs"') + s += yaml_array('DOF_I_B', np.array(SD_Vars['IDI_B'])+1, Fmt='{:7d}', comment = '"interface" retained DOFs') + s += yaml_array('DOF_I_F', np.array(SD_Vars['IDI_F'])+1, Fmt='{:7d}', comment = '"interface" fixed DOFs') + s += yaml_array('DOF_C__', np.array(SD_Vars['IDC__'])+1, Fmt='{:7d}', comment = '"reaction" DOFs"') + s += yaml_array('DOF_C_B', np.array(SD_Vars['IDC_B'])+1, Fmt='{:7d}', comment = '"reaction" retained DOFs') + s += yaml_array('DOF_C_L', np.array(SD_Vars['IDC_L'])+1, Fmt='{:7d}', comment = '"reaction" internal DOFs') + s += yaml_array('DOF_C_F', np.array(SD_Vars['IDC_F'])+1, Fmt='{:7d}', comment = '"reaction" fixed DOFs') + s += yaml_array('DOF_L_L', np.array(SD_Vars['IDL_L'])+1, Fmt='{:7d}', comment = '"internal" internal DOFs') + s += yaml_array('DOF_R_' , np.array(SD_Vars['IDR__'])+1, Fmt='{:7d}', comment = '"interface&reaction" DOFs') + s += yaml_array('DOF___B', np.array(model.DOFc_Leader )+1, Fmt='{:7d}', comment='all retained DOFs'); + s += yaml_array('DOF___F', np.array(model.DOFc_Fixed )+1, Fmt='{:7d}', comment='all fixed DOFs'); + s += yaml_array('DOF___L', np.array(model.DOFc_Follower)+1, Fmt='{:7d}', comment='all internal DOFs'); + s += '\n' + s += '#Index map from DOF to nodes\n' + s += '# Node No., DOF/Node, NodalDOF\n' + s += 'DOF2Nodes: # {} x 3 (nDOFRed x 3, for each constrained DOF, col1: node index, col2: number of DOF, col3: DOF starting from 1)\n'.format(model.nDOFc) + DOFc2Nodes = model.DOFc2Nodes + for l in DOFc2Nodes: + s +=' - [{:7d},{:7d},{:7d}] # {}\n'.format(l[1]+1, l[2], l[3], l[0]+1 ) + s += '# Node_[#] X_[m] Y_[m] Z_[m] JType_[-] JDirX_[-] JDirY_[-] JDirZ_[-] JStff_[Nm/rad]\n' + s += 'Nodes: # {} x 9\n'.format(len(model.Nodes)) + for n in model.Nodes: + s += ' - [{:7d}.,{:15.3f},{:15.3f},{:15.3f},{:14d}., 0.000000E+00, 0.000000E+00, 0.000000E+00, 0.000000E+00]\n'.format(nodeID(n.ID), n.x, n.y, n.z, int(n.data['Type']) ) + s += '# Elem_[#] Node_1 Node_2 Prop_1 Prop_2 Type Length_[m] Area_[m^2] Dens._[kg/m^3] E_[N/m2] G_[N/m2] shear_[-] Ixx_[m^4] Iyy_[m^4] Jzz_[m^4] T0_[N]\n' + s += 'Elements: # {} x 16\n'.format(len(model.Elements)) + for e in model.Elements: + I = e.inertias + s0=' - [{:7d}.,{:7d}.,{:7d}.,{:7d}.,{:7d}.,{:7d}.,{:15.3f},{:15.3f},{:15.3f},{:15.6e},{:15.6e},{:15.6e},{:15.6e},{:15.6e},{:15.6e},{:15.6e}]\n'.format( + elemID(e.ID), nodeID(e.nodeIDs[0]), nodeID(e.nodeIDs[1]), propID(e.propIDs[0], e.propset), propID(e.propIDs[1], e.propset), elemType(e.data['Type']), + e.length, e.area, e.rho, e.E, e.G, e.kappa, I[0], I[1], I[2], e.T0) + s += s0.replace('e+','E+').replace('e-','E-') + s += '#____________________________________________________________________________________________________\n' + s += '#User inputs\n' + s += '\n' + s += '#Number of properties (NProps):{:6d}\n'.format(len(model.NodePropertySets['Beam'])) + s += '#Prop No YoungE ShearG MatDens XsecD XsecT\n' + for ip,p in enumerate(model.NodePropertySets['Beam']): + s0='#{:8d}{:15.6e}{:15.6e}{:15.6e}{:15.6e}{:15.6e}\n'.format(p.ID, p['E'],p['G'],p['rho'],p['D'],p['t']) + s += s0.replace('e+','E+').replace('e-','E-') + s +='\n' + s += '#No. of Reaction DOFs:{:6d}\n'.format(len(SD_Vars['IDC__']) ) + s += '#React. DOF_ID BC\n' + s += '\n'.join(['#{:10d}{:10s}'.format(idof+1,' Fixed' ) for idof in SD_Vars['IDC_F']]) + s += '\n'.join(['#{:10d}{:10s}'.format(idof+1,' Free' ) for idof in SD_Vars['IDC_L']]) + s += '\n'.join(['#{:10d}{:10s}'.format(idof+1,' Leader') for idof in SD_Vars['IDC_B']]) + s += '\n\n' + s += '#No. of Interface DOFs:{:6d}\n'.format(len(SD_Vars['IDI__'])) + s += '#Interf. DOF_ID BC\n' + s += '\n'.join(['#{:10d}{:10s}'.format(idof+1,' Fixed' ) for idof in SD_Vars['IDI_F']]) + s += '\n'.join(['#{:10d}{:10s}'.format(idof+1,' Leader') for idof in SD_Vars['IDI_B']]) + s += '\n\n' + CM = [] + from welib.yams.utils import identifyRigidBodyMM + for n in model.Nodes: + if 'addedMassMatrix' in n.data: + mass, J_G, ref2COG = identifyRigidBodyMM(n.data['addedMassMatrix']) + CM.append( (n.ID, mass, J_G, ref2COG) ) + s += '#Number of concentrated masses (NCMass):{:6d}\n'.format(len(CM)) + s += '#JointCMas Mass JXX JYY JZZ JXY JXZ JYZ MCGX MCGY MCGZ\n' + for cm in CM: + s0 = '# {:9.0f}.{:15.6e}{:15.6e}{:15.6e}{:15.6e}{:15.6e}{:15.6e}{:15.6e}{:15.6e}{:15.6e}{:15.6e}\n'.format( nodeID(cm[0]), cm[1], cm[2][0,0], cm[2][1,1], cm[2][2,2], cm[2][0,1], cm[2][0,2], cm[2][1,2],cm[3][0],cm[3][1],cm[3][2] ) + s += s0.replace('e+','E+').replace('e-','E-') + s += '\n' + #s += '#Number of members 18\n' + #s += '#Number of nodes per member: 2\n' + #s += '#Member I Joint1_ID Joint2_ID Prop_I Prop_J Mass Length Node IDs...\n' + #s += '# 77 61 60 11 11 1.045888E+04 2.700000E+00 19 18\n' + #s += '#____________________________________________________________________________________________________\n' + #s += '#Direction Cosine Matrices for all Members: GLOBAL-2-LOCAL. No. of 3x3 matrices= 18\n' + #s += '#Member I DC(1,1) DC(1,2) DC(1,3) DC(2,1) DC(2,2) DC(2,3) DC(3,1) DC(3,2) DC(3,3)\n' + #s += '# 77 1.000E+00 0.000E+00 0.000E+00 0.000E+00 -1.000E+00 0.000E+00 0.000E+00 0.000E+00 -1.000E+00\n' + s += '#____________________________________________________________________________________________________\n' + s += '#FEM Eigenvectors ({} x {}) [m or rad], full system with reaction constraints (+ Soil K/M + SoilDyn K0)\n'.format(*model.Q.shape) + s += yaml_array('Full_Modes', model.Q) + s += '#____________________________________________________________________________________________________\n' + s += '#CB Matrices (PhiM,PhiR) (reaction constraints applied)\n' + s += yaml_array('PhiM', model.Phi_CB[:,:model.nModesCB] ,comment='(CB modes)') + s += yaml_array('PhiR', model.Phi_G, comment='(Guyan modes)') + s += '\n' + if more: + s += '#____________________________________________________________________________________________________\n' + s += '# ADDITIONAL DEBUGGING INFORMATION\n' + s += '#____________________________________________________________________________________________________\n' + s += '' + e = model.Elements[0] + rho=e.rho + A = e.area + L = e.length + t= rho*A*L + s0 = '{:15.6e}{:15.6e}{:15.6e}{:15.6e}{:15.6e}{:15.6e}{:15.6e}{:15.6e}{:15.6e}{:15.6e}{:15.6e}\n'.format(model.gravity,e.area, e.length, e.inertias[0], e.inertias[1], e.inertias[2], e.kappa, e.E, e.G, e.rho, t) + s0 = s0.replace('e+','E+').replace('e-','E-') + s += s0 + s += yaml_array('KeLocal' +str(), model.Elements[0].Ke(local=True)) + + for ie,e in enumerate(model.Elements): + s += yaml_array('DC' +str(ie+1), e.DCM.transpose()) + s += yaml_array('Ke' +str(ie+1), e.Ke()) + s += yaml_array('Me' +str(ie+1), e.Me()) + s += yaml_array('FGe'+str(ie+1), e.Fe_g(model.gravity)) + s += yaml_array('FCe'+str(ie+1), e.Fe_o()) + + s += yaml_array('KeLocal' +str(ie+1), e.Ke(local=True)) + s += yaml_array('MeLocal' +str(ie+1), e.Me(local=True)) + s += yaml_array('FGeLocal'+str(ie+1), e.Fe_g(model.gravity, local=True)) + s += yaml_array('FCeLocal'+str(ie+1), e.Fe_o(local=True)) + + s += '#____________________________________________________________________________________________________\n' + e = model.Elements[0] + s += yaml_array('Ke', e.Ke(local=True), comment='First element stiffness matrix'); # TODO not in local + s += yaml_array('Me', e.Me(local=True), comment='First element mass matrix'); + s += yaml_array('FGe', e.Fe_g(model.gravity,local=True), comment='First element gravity vector'); + s += yaml_array('FCe', e.Fe_o(local=True), comment='First element cable pretension'); + s += '#____________________________________________________________________________________________________\n' + s += '#FULL FEM K and M matrices. TOTAL FEM TDOFs: {}\n'.format(model.nDOF); # NOTE: wrong in SubDyn, should be nDOFc + s += yaml_array('K', model.KK, comment='Stiffness matrix'); + s += yaml_array('M', model.MM, comment='Mass matrix'); + s += '#____________________________________________________________________________________________________\n' + s += '#Gravity and cable loads applied at each node of the system (before DOF elimination with T matrix)\n' + s += yaml_array('FG', model.FF_init, comment=' '); + s += '#____________________________________________________________________________________________________\n' + s += '#Additional CB Matrices (MBB,MBM,KBB) (constraint applied)\n' + s += yaml_array('MBB' , model.MBB, comment=''); + s += yaml_array('MBM' , model.MBM[:,:model.nModesCB], comment=''); + s += yaml_array('CMMdiag', model.CMM, comment='(2 Zeta OmegaM)'); + s += yaml_array('KBB' , model.KBB, comment=''); + s += yaml_array('KMM' , np.diag(model.KMM), comment='(diagonal components, OmegaL^2)'); + s += yaml_array('KMMdiag', np.diag(model.KMM)[:model.nModesCB], comment='(diagonal components, OmegaL^2)'); + s += yaml_array('PhiL' , model.Phi_CB, comment=''); + s += 'PhiLOm2-1: # 18 x 18 \n' + s += 'KLL^-1: # 18 x 18 \n' + s += '#____________________________________________________________________________________________________\n' + s += yaml_array('T_red', model.T_c, Fmt = '{:9.2e}', comment='(Constraint elimination matrix)'); + s += 'AA: # 16 x 16 (State matrix dXdx)\n' + s += 'BB: # 16 x 48 (State matrix dXdu)\n' + s += 'CC: # 6 x 16 (State matrix dYdx)\n' + s += 'DD: # 6 x 48 (State matrix dYdu)\n' + s += '#____________________________________________________________________________________________________\n' + s += yaml_array('TI', model.T_refPoint, Fmt = '{:9.2e}',comment='(TP refpoint Transformation Matrix TI)'); + if filename is not None: + with open(filename, 'w') as f: + f.write(s) + + + From c3030fed8d1da9bc87d7889d5a85dcbe9ca7bd5d Mon Sep 17 00:00:00 2001 From: Emmanuel Branlard Date: Tue, 8 Aug 2023 17:18:33 -0600 Subject: [PATCH 099/124] Fatigue: small fix for constant channels --- pyFAST/tools/fatigue.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyFAST/tools/fatigue.py b/pyFAST/tools/fatigue.py index 58ca008..c1e414e 100644 --- a/pyFAST/tools/fatigue.py +++ b/pyFAST/tools/fatigue.py @@ -131,7 +131,7 @@ def find_range_count(signal, bins, method='rainflow_windap', meanBin=True, binSt ranges = fatpack.find_rainflow_ranges(signal) except IndexError: # Currently fails for constant signal - return np.nan + return np.nan, np.nan, np.nan # --- Legacy fatpack # if (not binStartAt0) and (not meanBin): # N, S = fatpack.find_range_count(ranges, bins) @@ -148,6 +148,7 @@ def find_range_count(signal, bins, method='rainflow_windap', meanBin=True, binSt b = np.isnan(S) S[b] = 0 N[b] = 0 + return N, S, S_bin_edges def create_bins(x, bins, binStartAt0=False): From 63cc25792a278cec1ae570007d323e04c703458e Mon Sep 17 00:00:00 2001 From: Emmanuel Branlard Date: Tue, 8 Aug 2023 17:20:58 -0600 Subject: [PATCH 100/124] Lin: using FASTLinearizationFile reader, adding options for state selection and TXT selection --- .../input_output/fast_linearization_file.py | 598 ++++++++++++------ .../tests/test_fast_linearization.py | 33 + pyFAST/linearization/campbell.py | 38 +- pyFAST/linearization/campbell_data.py | 51 +- .../ex1a_OneLinFile_SimpleEigenAnalysis.py | 3 +- pyFAST/linearization/linearization.py | 14 +- pyFAST/linearization/linfile.py | 130 ++-- pyFAST/linearization/mbc.py | 23 +- pyFAST/linearization/tools.py | 37 +- 9 files changed, 645 insertions(+), 282 deletions(-) diff --git a/pyFAST/input_output/fast_linearization_file.py b/pyFAST/input_output/fast_linearization_file.py index 997ad5b..dd18069 100644 --- a/pyFAST/input_output/fast_linearization_file.py +++ b/pyFAST/input_output/fast_linearization_file.py @@ -7,13 +7,19 @@ File = dict class BrokenFormatError(Exception): pass + + +_lin_vec = ['x','xd','xdot','u','y','z','header'] +_lin_mat = ['A','B','C','D','dUdu','dUdy', 'StateRotation', 'M'] +_lin_dict = ['x_info','xdot_info','u_info','y_info'] + class FASTLinearizationFile(File): """ Read/write an OpenFAST linearization file. The object behaves like a dictionary. Main keys --------- - - 'x', 'xdot' 'u', 'y', 'A', 'B', 'C', 'D' + - 'x', 'xdot', 'xd', 'u', 'y', 'z', 'A', 'B', 'C', 'D' Main methods ------------ @@ -47,8 +53,16 @@ def __init__(self, filename=None, **kwargs): if filename: self.read(**kwargs) - def read(self, filename=None, **kwargs): - """ Reads the file self.filename, or `filename` if provided """ + def read(self, filename=None, starSub=None, removeStatesPattern=None): + """ Reads the file self.filename, or `filename` if provided + + - starSub: if None, raise an error if `****` are present + otherwise replace *** with `starSub` (e.g. 0) + - removeStatesPattern: if None, do nothing + otherwise search for states matching a pattern and remove them + e.g: 'tower|Drivetrain' or '^AD' + see removeStates in this file. + """ # --- Standard tests and exceptions (generic code) if filename: @@ -59,17 +73,23 @@ def read(self, filename=None, **kwargs): raise OSError(2,'File not found:',self.filename) if os.stat(self.filename).st_size == 0: raise EmptyFileError('File is empty:',self.filename) - # --- Calling (children) function to read - self._read(**kwargs) - def _read(self, *args, **kwargs): + # --- Main Data self['header']=[] - def extractVal(lines, key): + # --- StarValues replacement `*****` -> inf + starPattern = re.compile(r"[\*]+") + starSubStr = ' inf ' + + def extractVal(lines, key, NA=None, missing=None, dtype=float): for l in lines: if l.find(key)>=0: - return l.split(key)[1].split()[0] - return None + #l = starPattern.sub(starSubStr, l) + try: + return dtype(l.split(key)[1].split()[0]) + except: + return NA + return missing def readToMarker(fid, marker, nMax): lines=[] @@ -80,15 +100,17 @@ def readToMarker(fid, marker, nMax): break lines.append(line.strip()) return lines, line - - def readOP(fid, n, name=''): + + def readOP(fid, n, name='', defaultDerivOrder=1): OP=[] Var = {'RotatingFrame': [], 'DerivativeOrder': [], 'Description': []} colNames=fid.readline().strip() dummy= fid.readline().strip() bHasDeriv= colNames.find('Derivative Order')>=0 for i, line in enumerate(fid): - sp=line.strip().split() + line = line.strip() + line = starPattern.sub(starSubStr, line) + sp = line.split() if sp[1].find(',')>=0: # Most likely this OP has three values (e.g. orientation angles) # For now we discard the two other values @@ -102,16 +124,27 @@ def readOP(fid, n, name=''): Var['DerivativeOrder'].append(int(sp[iRot+1])) Var['Description'].append(' '.join(sp[iRot+2:]).strip()) else: - Var['DerivativeOrder'].append(-1) + Var['DerivativeOrder'].append(defaultDerivOrder) Var['Description'].append(' '.join(sp[iRot+1:]).strip()) if i>=n-1: break - OP=np.asarray(OP) + OP = np.asarray(OP) + nInf = sum(np.isinf(OP)) + if nInf>0: + sErr = 'Some ill-formated/infinite values (e.g. `*******`) were found in the vector `{}`\n\tin linflile: {}'.format(name, self.filename) + if starSub is None: + raise Exception(sErr) + else: + print('[WARN] '+sErr) + OP[np.isinf(OP)] = starSub + + Var['RotatingFrame'] = np.asarray(Var['RotatingFrame']) + Var['DerivativeOrder'] = np.asarray(Var['DerivativeOrder']) + Var['Description'] = np.asarray(Var['Description']) return OP, Var def readMat(fid, n, m, name=''): - pattern = re.compile(r"[\*]+") - vals=[pattern.sub(' inf ', fid.readline().strip() ).split() for i in np.arange(n)] + vals=[starPattern.sub(starSubStr, fid.readline().strip() ).split() for i in np.arange(n)] vals = np.array(vals) try: vals = np.array(vals).astype(float) # This could potentially fail @@ -125,7 +158,12 @@ def readMat(fid, n, m, name=''): nNaN = sum(np.isnan(vals.ravel())) nInf = sum(np.isinf(vals.ravel())) if nInf>0: - raise Exception('Some ill-formated/infinite values (e.g. `*******`) were found in the matrix `{}`\n\tin linflile: {}'.format(name, self.filename)) + sErr = 'Some ill-formated/infinite values (e.g. `*******`) were found in the matrix `{}`\n\tin linflile: {}'.format(name, self.filename) + if starSub is None: + raise Exception(sErr) + else: + print('[WARN] '+sErr) + vals[np.isinf(vals)] = starSub if nNaN>0: raise Exception('Some NaN values were found in the matrix `{}`\n\tin linfile: `{}`.'.format(name, self.filename)) return vals @@ -136,35 +174,31 @@ def readMat(fid, n, m, name=''): # --- Reader header self['header'], lastLine=readToMarker(f, 'Jacobians included', 30) self['header'].append(lastLine) - nx = int(extractVal(self['header'],'Number of continuous states:')) - nxd = int(extractVal(self['header'],'Number of discrete states:' )) - nz = int(extractVal(self['header'],'Number of constraint states:')) - nu = int(extractVal(self['header'],'Number of inputs:' )) - ny = int(extractVal(self['header'],'Number of outputs:' )) - bJac = extractVal(self['header'],'Jacobians included in this file?') - try: - self['Azimuth'] = float(extractVal(self['header'],'Azimuth:')) - except: - self['Azimuth'] = None - try: - self['RotSpeed'] = float(extractVal(self['header'],'Rotor Speed:')) # rad/s - except: - self['RotSpeed'] = None - try: - self['WindSpeed'] = float(extractVal(self['header'],'Wind Speed:')) - except: - self['WindSpeed'] = None + nx = extractVal(self['header'],'Number of continuous states:' , dtype=int, NA=np.nan, missing=None) + nxd = extractVal(self['header'],'Number of discrete states:' , dtype=int, NA=np.nan, missing=None) + nz = extractVal(self['header'],'Number of constraint states:' , dtype=int, NA=np.nan, missing=None) + nu = extractVal(self['header'],'Number of inputs:' , dtype=int, NA=np.nan, missing=None) + ny = extractVal(self['header'],'Number of outputs:' , dtype=int, NA=np.nan, missing=None) + bJac = extractVal(self['header'],'Jacobians included in this file?', dtype=bool, NA=False, missing=None) + self['Azimuth'] = extractVal(self['header'], 'Azimuth:' , dtype=float, NA=np.nan, missing=None) + self['RotSpeed'] = extractVal(self['header'], 'Rotor Speed:', dtype=float, NA=np.nan, missing=None) # rad/s + self['WindSpeed'] = extractVal(self['header'], 'Wind Speed:' , dtype=float, NA=np.nan, missing=None) + self['t'] = extractVal(self['header'],'Simulation time:' , dtype=float, NA=np.nan, missing=None) for i, line in enumerate(f): line = line.strip() if line.find('Order of continuous states:')>=0: - self['x'], self['x_info'] = readOP(f, nx, 'x') + self['x'], self['x_info'] = readOP(f, nx, 'x', defaultDerivOrder=1) elif line.find('Order of continuous state derivatives:')>=0: - self['xdot'], self['xdot_info'] = readOP(f, nx, 'xdot') + self['xdot'], self['xdot_info'] = readOP(f, nx, 'xdot', defaultDerivOrder=2) + elif line.find('Order of discrete states:')>=0: + self['xd'], self['xd_info'] = readOP(f, nxd, 'xd', defaultDerivOrder=2) elif line.find('Order of inputs')>=0: - self['u'], self['u_info'] = readOP(f, nu, 'u') + self['u'], self['u_info'] = readOP(f, nu, 'u', defaultDerivOrder=0) elif line.find('Order of outputs')>=0: - self['y'], self['y_info'] = readOP(f, ny, 'y') + self['y'], self['y_info'] = readOP(f, ny, 'y', defaultDerivOrder=0) + elif line.find('Order of constraint states:')>=0: + self['z'], self['z_info'] = readOP(f, nz, 'z', defaultDerivOrder=0) elif line.find('A:')>=0: self['A'] = readMat(f, nx, nx, 'A') elif line.find('B:')>=0: @@ -185,6 +219,9 @@ def readMat(fid, n, m, name=''): self['EDDOF'] = line[5:].split() self['M'] = readMat(f, 24, 24,'M') + if removeStatesPattern is not None: + self.removeStates(pattern=removeStatesPattern) + def toString(self): s='' return s @@ -193,176 +230,113 @@ def _write(self): with open(self.filename,'w') as f: f.write(self.toString()) - def short_descr(self,slist): - def shortname(s): - s=s.strip() - s = s.replace('(m/s)' , '_[m/s]' ); - s = s.replace('(kW)' , '_[kW]' ); - s = s.replace('(deg)' , '_[deg]' ); - s = s.replace('(N)' , '_[N]' ); - s = s.replace('(kN-m)' , '_[kNm]' ); - s = s.replace('(N-m)' , '_[Nm]' ); - s = s.replace('(kN)' , '_[kN]' ); - s = s.replace('(rpm)' , '_[rpm]' ); - s = s.replace('(rad)' , '_[rad]' ); - s = s.replace('(rad/s)' , '_[rad/s]' ); - s = s.replace('(rad/s^2)', '_[rad/s^2]' ); - s = s.replace('(m/s^2)' , '_[m/s^2]'); - s = s.replace('(deg/s^2)','_[deg/s^2]'); - s = s.replace('(m)' , '_[m]' ); - s = s.replace(', m/s/s','_[m/s^2]'); - s = s.replace(', m/s^2','_[m/s^2]'); - s = s.replace(', m/s','_[m/s]'); - s = s.replace(', m','_[m]'); - s = s.replace(', rad/s/s','_[rad/s^2]'); - s = s.replace(', rad/s^2','_[rad/s^2]'); - s = s.replace(', rad/s','_[rad/s]'); - s = s.replace(', rad','_[rad]'); - s = s.replace(', -','_[-]'); - s = s.replace(', Nm/m','_[Nm/m]'); - s = s.replace(', Nm','_[Nm]'); - s = s.replace(', N/m','_[N/m]'); - s = s.replace(', N','_[N]'); - s = s.replace('(1)','1') - s = s.replace('(2)','2') - s = s.replace('(3)','3') - s= re.sub(r'\([^)]*\)','', s) # remove parenthesis - s = s.replace('ED ',''); - s = s.replace('BD_','BD_B'); - s = s.replace('IfW ',''); - s = s.replace('Extended input: ','') - s = s.replace('1st tower ','qt1'); - s = s.replace('2nd tower ','qt2'); - nd = s.count('First time derivative of ') - if nd>=0: - s = s.replace('First time derivative of ' ,''); - if nd==1: - s = 'd_'+s.strip() - elif nd==2: - s = 'dd_'+s.strip() - s = s.replace('Variable speed generator DOF ','psi_rot'); # NOTE: internally in FAST this is the azimuth of the rotor - s = s.replace('fore-aft bending mode DOF ' ,'FA' ); - s = s.replace('side-to-side bending mode DOF','SS' ); - s = s.replace('bending-mode DOF of blade ' ,'' ); - s = s.replace(' rotational-flexibility DOF, rad','-ROT' ); - s = s.replace('rotational displacement in ','rot' ); - s = s.replace('Drivetrain','DT' ); - s = s.replace('translational displacement in ','trans' ); - s = s.replace('finite element node ','N' ); - s = s.replace('-component position of node ','posN') - s = s.replace('-component inflow on tower node','TwrN') - s = s.replace('-component inflow on blade 1, node','Bld1N') - s = s.replace('-component inflow on blade 2, node','Bld2N') - s = s.replace('-component inflow on blade 3, node','Bld3N') - s = s.replace('-component inflow velocity at node','N') - s = s.replace('X translation displacement, node','TxN') - s = s.replace('Y translation displacement, node','TyN') - s = s.replace('Z translation displacement, node','TzN') - s = s.replace('X translation velocity, node','TVxN') - s = s.replace('Y translation velocity, node','TVyN') - s = s.replace('Z translation velocity, node','TVzN') - s = s.replace('X translation acceleration, node','TAxN') - s = s.replace('Y translation acceleration, node','TAyN') - s = s.replace('Z translation acceleration, node','TAzN') - s = s.replace('X orientation angle, node' ,'RxN') - s = s.replace('Y orientation angle, node' ,'RyN') - s = s.replace('Z orientation angle, node' ,'RzN') - s = s.replace('X rotation velocity, node' ,'RVxN') - s = s.replace('Y rotation velocity, node' ,'RVyN') - s = s.replace('Z rotation velocity, node' ,'RVzN') - s = s.replace('X rotation acceleration, node' ,'RAxN') - s = s.replace('Y rotation acceleration, node' ,'RAyN') - s = s.replace('Z rotation acceleration, node' ,'RAzN') - s = s.replace('X force, node','FxN') - s = s.replace('Y force, node','FyN') - s = s.replace('Z force, node','FzN') - s = s.replace('X moment, node','MxN') - s = s.replace('Y moment, node','MyN') - s = s.replace('Z moment, node','MzN') - s = s.replace('FX', 'Fx') - s = s.replace('FY', 'Fy') - s = s.replace('FZ', 'Fz') - s = s.replace('MX', 'Mx') - s = s.replace('MY', 'My') - s = s.replace('MZ', 'Mz') - s = s.replace('FKX', 'FKx') - s = s.replace('FKY', 'FKy') - s = s.replace('FKZ', 'FKz') - s = s.replace('MKX', 'MKx') - s = s.replace('MKY', 'MKy') - s = s.replace('MKZ', 'MKz') - s = s.replace('Nodes motion','') - s = s.replace('cosine','cos' ); - s = s.replace('sine','sin' ); - s = s.replace('collective','coll.'); - s = s.replace('Blade','Bld'); - s = s.replace('rotZ','TORS-R'); - s = s.replace('transX','FLAP-D'); - s = s.replace('transY','EDGE-D'); - s = s.replace('rotX','EDGE-R'); - s = s.replace('rotY','FLAP-R'); - s = s.replace('flapwise','FLAP'); - s = s.replace('edgewise','EDGE'); - s = s.replace('horizontal surge translation DOF','Surge'); - s = s.replace('horizontal sway translation DOF','Sway'); - s = s.replace('vertical heave translation DOF','Heave'); - s = s.replace('roll tilt rotation DOF','Roll'); - s = s.replace('pitch tilt rotation DOF','Pitch'); - s = s.replace('yaw rotation DOF','Yaw'); - s = s.replace('vertical power-law shear exponent','alpha') - s = s.replace('horizontal wind speed ','WS') - s = s.replace('propagation direction','WD') - s = s.replace(' pitch command','pitch') - s = s.replace('HSS_','HSS') - s = s.replace('Bld','B') - s = s.replace('tower','Twr') - s = s.replace('Tower','Twr') - s = s.replace('Nacelle','Nac') - s = s.replace('Platform','Ptfm') - s = s.replace('SrvD','SvD') - s = s.replace('Generator torque','Qgen') - s = s.replace('coll. blade-pitch command','PitchColl') - s = s.replace('wave elevation at platform ref point','WaveElevRefPoint') - s = s.replace('1)','1'); - s = s.replace('2)','2'); - s = s.replace('3)','3'); - s = s.replace(',',''); - s = s.replace(' ',''); - s=s.strip() - return s - return [shortname(s) for s in slist] - - def xdescr(self): - if 'x_info' in self.keys(): - return self.short_descr(self['x_info']['Description']) + @property + def nx(self): + if 'x' in self.keys(): + return len(self['x']) + return 0 + + @property + def nxd(self): + if 'xd' in self.keys(): + return len(self['xd']) + return 0 + + @property + def nu(self): + if 'u' in self.keys(): + return len(self['u']) + return 0 + + @property + def ny(self): + if 'y' in self.keys(): + return len(self['y']) + return 0 + + @property + def nz(self): + if 'z' in self.keys(): + return len(self['z']) + return 0 + + @property + def u_descr(self): + if self.nu>0: + return self['u_info']['Description'] + else: + return [] + + @property + def x_descr(self): + if self.nx>0: + return self['x_info']['Description'] else: return [] - def xdotdescr(self): - if 'xdot_info' in self.keys(): - return self.short_descr(self['xdot_info']['Description']) + @property + def xd_descr(self): # Discrete states not derivative! + if self.nxd>0: + return self['xd_info']['Description'] else: return [] - def ydescr(self): - if 'y_info' in self.keys(): - return self.short_descr(self['y_info']['Description']) + @property + def xdot_descr(self): + if self.nx>0: + return self['xdot_info']['Description'] else: return [] - def udescr(self): - if 'u_info' in self.keys(): - return self.short_descr(self['u_info']['Description']) + + @property + def y_descr(self): + if self.ny>0: + return self['y_info']['Description'] else: return [] + @property + def z_descr(self): + if self.nz>0: + return self['z_info']['Description'] + else: + return [] + + def __repr__(self): + s='<{} object> with attributes:\n'.format(type(self).__name__) + s+=' - filename: {}\n'.format(self.filename) + s+=' * nx : {}\n'.format(self.nx) + s+=' * nxd : {}\n'.format(self.nxd) + s+=' * nu : {}\n'.format(self.nu) + s+=' * ny : {}\n'.format(self.ny) + s+=' * nz : {}\n'.format(self.nz) + s+='keys:\n' + for k,v in self.items(): + if k in _lin_vec: + s+=' - {:15s}: shape: ({}) \n'.format(k,len(v)) + elif k in _lin_mat: + s+=' - {:15s}: shape: ({} x {})\n'.format(k,v.shape[0], v.shape[1]) + elif k in _lin_dict: + s+=' - {:15s}: dict with keys: {} \n'.format(k,list(v.keys())) + else: + s+=' - {:15s}: {}\n'.format(k,v) + s+='methods:\n' + s+=' - toDataFrame: convert A,B,C,D to dataframes\n' + s+=' - removeStates: remove states\n' + s+=' - eva: eigenvalue analysis\n' + + return s + def toDataFrame(self): import pandas as pd dfs={} - xdescr_short = self.xdescr() - xdotdescr_short = self.xdotdescr() - ydescr_short = self.ydescr() - udescr_short = self.udescr() + xdescr_short = short_descr(self.x_descr) + xddescr_short = short_descr(self.xd_descr) + xdotdescr_short = short_descr(self.xdot_descr) + udescr_short = short_descr(self.u_descr) + ydescr_short = short_descr(self.y_descr) + zdescr_short = short_descr(self.z_descr) if 'A' in self.keys(): dfs['A'] = pd.DataFrame(data = self['A'], index=xdescr_short, columns=xdescr_short) @@ -374,12 +348,16 @@ def toDataFrame(self): dfs['D'] = pd.DataFrame(data = self['D'], index=ydescr_short, columns=udescr_short) if 'x' in self.keys(): dfs['x'] = pd.DataFrame(data = np.asarray(self['x']).reshape((1,-1)), columns=xdescr_short) + if 'xd' in self.keys(): + dfs['xd'] = pd.DataFrame(data = np.asarray(self['xd']).reshape((1,-1))) if 'xdot' in self.keys(): dfs['xdot'] = pd.DataFrame(data = np.asarray(self['xdot']).reshape((1,-1)), columns=xdotdescr_short) if 'u' in self.keys(): dfs['u'] = pd.DataFrame(data = np.asarray(self['u']).reshape((1,-1)), columns=udescr_short) if 'y' in self.keys(): dfs['y'] = pd.DataFrame(data = np.asarray(self['y']).reshape((1,-1)), columns=ydescr_short) + if 'z' in self.keys(): + dfs['z'] = pd.DataFrame(data = np.asarray(self['z']).reshape((1,-1)), columns=zdescr_short) if 'M' in self.keys(): dfs['M'] = pd.DataFrame(data = self['M'], index=self['EDDOF'], columns=self['EDDOF']) if 'dUdu' in self.keys(): @@ -389,4 +367,236 @@ def toDataFrame(self): return dfs + def removeStates(self, pattern=None, Irm=None, verbose=True): + """ + remove states based on pattern or index + + - pattern: e.g: 'tower|Drivetrain' or '^AD' + """ + if self.nx==0: + return + desc = self['x_info']['Description'] + Iall = set(range(len(desc))) + sInfo='' + if pattern is not None: + Irm = [i for i, s in enumerate(desc) if re.search(pattern, s)] + sInfo=' with pattern `{}`'.format(pattern) + if verbose: + print('[INFO] removing {}/{} states{}'.format(len(Irm), len(Iall), sInfo)) + Ikeep = list(Iall.difference(Irm)) + Ikeep.sort() # safety + if len(Ikeep)==0: + raise Exception('All states have been removed{}!'.format(sInfo)) + # Remove states and info in vectors + self['x'] = self['x'][Ikeep] + self['xdot'] = self['xdot'][Ikeep] + for k in self['x_info'].keys(): + self['x_info'][k] = self['x_info'][k][Ikeep] + self['xdot_info'][k] = self['xdot_info'][k][Ikeep] + # Remove states in matrices + if 'A' in self.keys(): + self['A'] = self['A'][np.ix_(Ikeep,Ikeep)] + if 'B' in self.keys(): + self['B'] = self['B'][Ikeep,:] + if 'C' in self.keys(): + self['C'] = self['C'][:, Ikeep] + + + def eva(self, normQ=None, sort=True, discardIm=True): + """ Perform eigenvalue analysis of A matrix and return frequencies and damping """ + # --- Excerpt from welib.tools.eva.eigA + A = self['A'] + n,m = A.shape + if m!=n: + raise Exception('Matrix needs to be square') + # Basic EVA + D,Q = np.linalg.eig(A) + Lambda = np.diag(D) + v = np.diag(Lambda) + + # Selecting eigenvalues with positive imaginary part (frequency) + if discardIm: + Ipos = np.imag(v)>0 + Q = Q[:,Ipos] + v = v[Ipos] + + # Frequencies and damping based on compled eigenvalues + omega_0 = np.abs(v) # natural cylic frequency [rad/s] + freq_d = np.imag(v)/(2*np.pi) # damped frequency [Hz] + zeta = - np.real(v)/omega_0 # damping ratio + freq_0 = omega_0/(2*np.pi) # natural frequency [Hz] + # Sorting + if sort: + I = np.argsort(freq_0) + freq_d = freq_d[I] + freq_0 = freq_0[I] + zeta = zeta[I] + Q = Q[:,I] + + # Normalize Q + if normQ=='byMax': + for j in range(Q.shape[1]): + q_j = Q[:,j] + scale = np.max(np.abs(q_j)) + Q[:,j]= Q[:,j]/scale + return freq_d, zeta, Q, freq_0 + + + + + +def short_descr(slist): + """ Shorten and "unify" the description from lin file """ + def shortname(s): + s=s.strip() + s = s.replace('(m/s)' , '_[m/s]' ); + s = s.replace('(kW)' , '_[kW]' ); + s = s.replace('(deg)' , '_[deg]' ); + s = s.replace('(N)' , '_[N]' ); + s = s.replace('(kN-m)' , '_[kNm]' ); + s = s.replace('(N-m)' , '_[Nm]' ); + s = s.replace('(kN)' , '_[kN]' ); + s = s.replace('(rpm)' , '_[rpm]' ); + s = s.replace('(rad)' , '_[rad]' ); + s = s.replace('(rad/s)' , '_[rad/s]' ); + s = s.replace('(rad/s^2)', '_[rad/s^2]' ); + s = s.replace('(m/s^2)' , '_[m/s^2]'); + s = s.replace('(deg/s^2)','_[deg/s^2]'); + s = s.replace('(m)' , '_[m]' ); + s = s.replace(', m/s/s','_[m/s^2]'); + s = s.replace(', m/s^2','_[m/s^2]'); + s = s.replace(', m/s','_[m/s]'); + s = s.replace(', m','_[m]'); + s = s.replace(', rad/s/s','_[rad/s^2]'); + s = s.replace(', rad/s^2','_[rad/s^2]'); + s = s.replace(', rad/s','_[rad/s]'); + s = s.replace(', rad','_[rad]'); + s = s.replace(', -','_[-]'); + s = s.replace(', Nm/m','_[Nm/m]'); + s = s.replace(', Nm','_[Nm]'); + s = s.replace(', N/m','_[N/m]'); + s = s.replace(', N','_[N]'); + s = s.replace('(1)','1') + s = s.replace('(2)','2') + s = s.replace('(3)','3') + s= re.sub(r'\([^)]*\)','', s) # remove parenthesis + s = s.replace('ED ',''); + s = s.replace('BD_','BD_B'); + s = s.replace('IfW ',''); + s = s.replace('Extended input: ','') + s = s.replace('1st tower ','qt1'); + s = s.replace('2nd tower ','qt2'); + nd = s.count('First time derivative of ') + if nd>=0: + s = s.replace('First time derivative of ' ,''); + if nd==1: + s = 'd_'+s.strip() + elif nd==2: + s = 'dd_'+s.strip() + s = s.replace('Variable speed generator DOF ','psi_rot'); # NOTE: internally in FAST this is the azimuth of the rotor + s = s.replace('fore-aft bending mode DOF ' ,'FA' ); + s = s.replace('side-to-side bending mode DOF','SS' ); + s = s.replace('bending-mode DOF of blade ' ,'' ); + s = s.replace(' rotational-flexibility DOF, rad','-ROT' ); + s = s.replace('rotational displacement in ','rot' ); + s = s.replace('Drivetrain','DT' ); + s = s.replace('translational displacement in ','trans' ); + s = s.replace('finite element node ','N' ); + s = s.replace('-component position of node ','posN') + s = s.replace('-component inflow on tower node','TwrN') + s = s.replace('-component inflow on blade 1, node','Bld1N') + s = s.replace('-component inflow on blade 2, node','Bld2N') + s = s.replace('-component inflow on blade 3, node','Bld3N') + s = s.replace('-component inflow velocity at node','N') + s = s.replace('X translation displacement, node','TxN') + s = s.replace('Y translation displacement, node','TyN') + s = s.replace('Z translation displacement, node','TzN') + s = s.replace('X translation velocity, node','TVxN') + s = s.replace('Y translation velocity, node','TVyN') + s = s.replace('Z translation velocity, node','TVzN') + s = s.replace('X translation acceleration, node','TAxN') + s = s.replace('Y translation acceleration, node','TAyN') + s = s.replace('Z translation acceleration, node','TAzN') + s = s.replace('X orientation angle, node' ,'RxN') + s = s.replace('Y orientation angle, node' ,'RyN') + s = s.replace('Z orientation angle, node' ,'RzN') + s = s.replace('X rotation velocity, node' ,'RVxN') + s = s.replace('Y rotation velocity, node' ,'RVyN') + s = s.replace('Z rotation velocity, node' ,'RVzN') + s = s.replace('X rotation acceleration, node' ,'RAxN') + s = s.replace('Y rotation acceleration, node' ,'RAyN') + s = s.replace('Z rotation acceleration, node' ,'RAzN') + s = s.replace('X force, node','FxN') + s = s.replace('Y force, node','FyN') + s = s.replace('Z force, node','FzN') + s = s.replace('X moment, node','MxN') + s = s.replace('Y moment, node','MyN') + s = s.replace('Z moment, node','MzN') + s = s.replace('FX', 'Fx') + s = s.replace('FY', 'Fy') + s = s.replace('FZ', 'Fz') + s = s.replace('MX', 'Mx') + s = s.replace('MY', 'My') + s = s.replace('MZ', 'Mz') + s = s.replace('FKX', 'FKx') + s = s.replace('FKY', 'FKy') + s = s.replace('FKZ', 'FKz') + s = s.replace('MKX', 'MKx') + s = s.replace('MKY', 'MKy') + s = s.replace('MKZ', 'MKz') + s = s.replace('Nodes motion','') + s = s.replace('cosine','cos' ); + s = s.replace('sine','sin' ); + s = s.replace('collective','coll.'); + s = s.replace('Blade','Bld'); + s = s.replace('rotZ','TORS-R'); + s = s.replace('transX','FLAP-D'); + s = s.replace('transY','EDGE-D'); + s = s.replace('rotX','EDGE-R'); + s = s.replace('rotY','FLAP-R'); + s = s.replace('flapwise','FLAP'); + s = s.replace('edgewise','EDGE'); + s = s.replace('horizontal surge translation DOF','Surge'); + s = s.replace('horizontal sway translation DOF','Sway'); + s = s.replace('vertical heave translation DOF','Heave'); + s = s.replace('roll tilt rotation DOF','Roll'); + s = s.replace('pitch tilt rotation DOF','Pitch'); + s = s.replace('yaw rotation DOF','Yaw'); + s = s.replace('vertical power-law shear exponent','alpha') + s = s.replace('horizontal wind speed ','WS') + s = s.replace('propagation direction','WD') + s = s.replace(' pitch command','pitch') + s = s.replace('HSS_','HSS') + s = s.replace('Bld','B') + s = s.replace('tower','Twr') + s = s.replace('Tower','Twr') + s = s.replace('Nacelle','Nac') + s = s.replace('Platform','Ptfm') + s = s.replace('SrvD','SvD') + s = s.replace('Generator torque','Qgen') + s = s.replace('coll. blade-pitch command','PitchColl') + s = s.replace('wave elevation at platform ref point','WaveElevRefPoint') + s = s.replace('1)','1'); + s = s.replace('2)','2'); + s = s.replace('3)','3'); + s = s.replace(',',''); + s = s.replace(' ',''); + s=s.strip() + return s + return [shortname(s) for s in slist] + + + +if __name__ == '__main__': + f = FASTLinearizationFile('../../data/example_files/StandstillSemi_ForID_EDHD.1.lin') + print(f) + _, zeta1, _, freq1 = f.eva() + f.removeStates(pattern=r'^AD') + print(f) + dfs = f.toDataFrame() + _, zeta2, _, freq2 = f.eva() + print('f',freq1) + print('f',freq2) + print('d',zeta1) + print('d',zeta2) diff --git a/pyFAST/input_output/tests/test_fast_linearization.py b/pyFAST/input_output/tests/test_fast_linearization.py index 106cbf7..4c607ac 100644 --- a/pyFAST/input_output/tests/test_fast_linearization.py +++ b/pyFAST/input_output/tests/test_fast_linearization.py @@ -10,16 +10,49 @@ def test_001_read_all(self, DEBUG=True): reading_test('FASTLin*.*', FASTLinearizationFile) def test_FASTLin(self): + + # --- Test basic read F=FASTLinearizationFile(os.path.join(MyDir,'FASTLin.lin')) self.assertAlmostEqual(F['A'][3,1], 3.91159454E-04 ) self.assertAlmostEqual(F['u'][7] ,4.00176055E+04) + # Test properties + np.testing.assert_almost_equal(F.nx , 4) + np.testing.assert_almost_equal(F.nu , 9) + np.testing.assert_almost_equal(F.ny , 16) + np.testing.assert_almost_equal(F.nz , 0 ) + + # Test keys + np.testing.assert_almost_equal(F['Azimuth'] , 5.8684 , 4) + np.testing.assert_almost_equal(F['RotSpeed'] , 1.2367 , 4) + self.assertEqual(F['WindSpeed'], None ) # NOTE: might become NaN in the future + + # --- Test methods + dfs = F.toDataFrame() # Make sure this runs + + # Test EVA + fd, zeta, Q, f0 = F.eva() + np.testing.assert_almost_equal(f0 , [0.394858], 4) + np.testing.assert_almost_equal(zeta, [0.06078], 4) + + # Test state removal + F.removeStates(pattern='generator') + fd, zeta, Q, f0 = F.eva() + dfs = F.toDataFrame() # Make sure this runs + np.testing.assert_almost_equal(F.nx , 2) + np.testing.assert_almost_equal(f0 , [0.394258], 4) + np.testing.assert_almost_equal(zeta, [0.0603 ], 4) + + + # --- Test lin file with M (only for EB's special branch...) F=FASTLinearizationFile(os.path.join(MyDir,'FASTLin_EDM.lin')) dfs=F.toDataFrame() M=dfs['M'] self.assertAlmostEqual(M['7_TwFADOF1']['7_TwFADOF1'],0.436753E+06) self.assertAlmostEqual(M['13_GeAz']['13_GeAz'] , 0.437026E+08) + + if __name__ == '__main__': # Test().test_000_debug() unittest.main() diff --git a/pyFAST/linearization/campbell.py b/pyFAST/linearization/campbell.py index 59c9c31..44512a7 100644 --- a/pyFAST/linearization/campbell.py +++ b/pyFAST/linearization/campbell.py @@ -28,10 +28,11 @@ from pyFAST.linearization.campbell_data import IdentifyModes -def postproCampbell(fstFiles, BladeLen=None, TowerLen=None, verbose=True, nFreqOut=15, - WS_legacy=None, removeTwrAzimuth=False, - writeModes=None, - **kwargs +def postproCampbell(fstFiles, BladeLen=None, TowerLen=None, verbose=True, + WS_legacy=None, + nFreqOut=500, freqRange=None, posDampRange=None, # Options for TXT output + removeTwrAzimuth=False, starSub=None, removeStatesPattern=None, # Options for A matrix selection + writeModes=None, **kwargs # Options for .viz files ): """ Postprocess linearization files to extract Campbell diagram (linearization at different Operating points) @@ -42,6 +43,23 @@ def postproCampbell(fstFiles, BladeLen=None, TowerLen=None, verbose=True, nFreqO INPUTS: - fstFiles: list of fst files + + INPUTS (related to A matrices): + - removeTwrAzimuth: if False do nothing + otherwise discard lin files where azimuth in [60, 180, 300]+/-4deg (close to tower). + - starSub: if None, raise an error if `****` are present + otherwise replace *** with `starSub` (e.g. 0) + see FASTLinearizationFile. + - removeStatesPattern: remove states matching a giving description pattern. + e.g: 'tower|Drivetrain' or '^AD' + see FASTLinearizationFile. + + INPUTS (related to Campbell_Summary.txt output): + - nFreqOut: maximum number of frequencies to write to Campbell_Summary.txt file + - freqRange: range in which frequencies are "accepted", if None: [-np.inf, np.inf] + - posDampRange: range in which damping are "accepted' , if None: [1e-5, 0.96] + + INPUTS (related to .viz files): - writeModes: if True, a binary file and a .viz file is written to disk for OpenFAST VTK visualization. if None, the binary file is written only if a checkpoint file is present. For instance, if the main file is : 'main.fst', @@ -50,6 +68,13 @@ def postproCampbell(fstFiles, BladeLen=None, TowerLen=None, verbose=True, nFreqO the checkpoint file is expected to be : 'main.ModeShapeVTK.chkp' - **kwargs: list of key/values to be passed to writeVizFile (see function below) VTKLinModes=15, VTKLinScale=10, VTKLinTim=1, VTKLinTimes1=True, VTKLinPhase=0, VTKModes=None + + OUTPUTS: + - OP: dataframe of operating points + - Freq: dataframe of frequencies for each OP and identified mode + - Damp: dataframe of dampings for each OP and identified mode + - UnMapped: + - ModeData: all mode data all mode data """ if len(fstFiles)==0: raise Exception('postproCampbell requires a list of at least one .fst') @@ -60,7 +85,8 @@ def postproCampbell(fstFiles, BladeLen=None, TowerLen=None, verbose=True, nFreqO CD = pickle.load(open(fstFiles[0],'rb')) else: # --- Attemps to extract Blade Length and TowerLen from first file... - CD, MBC = getCampbellDataOPs(fstFiles, writeModes=writeModes, BladeLen=BladeLen, TowerLen=TowerLen, removeTwrAzimuth=removeTwrAzimuth, verbose=verbose, **kwargs) + CD, MBC = getCampbellDataOPs(fstFiles, writeModes=writeModes, BladeLen=BladeLen, TowerLen=TowerLen, + removeTwrAzimuth=removeTwrAzimuth, starSub=starSub, removeStatesPattern=removeStatesPattern, verbose=verbose, **kwargs) # --- Identify modes modeID_table,modesDesc=IdentifyModes(CD) @@ -71,7 +97,7 @@ def postproCampbell(fstFiles, BladeLen=None, TowerLen=None, verbose=True, nFreqO modeID_file = campbellData2CSV(baseName, CD, modeID_table, modesDesc) # Write summary txt file to help manual identification step.. txtFileName = baseName+'_Summary.txt' - campbellData2TXT(CD, nFreqOut=nFreqOut, txtFileName=txtFileName) + campbellData2TXT(CD, txtFileName=txtFileName, nFreqOut=nFreqOut, freqRange=freqRange, posDampRange=posDampRange) # --- Return nice dataframes (assuming the identification is correct) # TODO, for now we reread the files... diff --git a/pyFAST/linearization/campbell_data.py b/pyFAST/linearization/campbell_data.py index bb869d2..ca975cb 100644 --- a/pyFAST/linearization/campbell_data.py +++ b/pyFAST/linearization/campbell_data.py @@ -201,49 +201,68 @@ def printCampbellDataOP(CDDOP, nModesMax=15, nCharMaxDesc=50) : # --- Manipulation of multiple operating points # --------------------------------------------------------------------------------{ -def campbellData2TXT(CD, nFreqOut=15, txtFileName=None, skipHighDamp=True, skipNonEDBD=True): +def campbellData2TXT(CD, txtFileName=None, nFreqOut=500, freqRange=None, posDampRange=None, skipNonEDBD=True): """ Write frequencies, damping, and mode contents for each operating points to a string Write to file if filename provided + INPUTS: + - nFreqOut: maximum number of frequencies to write to Campbell_Summary.txt file + - freqRange: range in which frequencies are "accepted", if None: [-np.inf, np.inf] + - posDampRange: range in which damping are "accepted' , if None: [1e-5, 0.96] """ if not isinstance(CD, list): CD=[CD] + if freqRange is None: + freqRange =[-np.inf, np.inf] + if posDampRange is None: + posDampRange =[1e-5, 0.96] + + def mode2txtline(cd, im): + m = cd['Modes'][im] + Desc = cd['ShortModeDescr'][im] + zeta = m['DampingRatio'] + return '{:03d} ; {:8.3f} ; {:7.4f} ; {:s}\n'.format(im+1,m['NaturalFreq_Hz'],m['DampingRatio'],Desc) + txt='' for iOP,cd in enumerate(CD): WS = cd['WindSpeed'] RPM = cd['RotSpeed_rpm'] nFreqOut_loc = np.min([len(cd['Modes']),nFreqOut]) - txt+='------------------------------------------------------------------------\n' - txt+='--- OP {:d} - WS {:.1f} - RPM {:.2f} \n'.format(iOP+1, WS, RPM) - txt+='------------------------------------------------------------------------\n' + txt+='# -----------------------------------------------------------------------\n' + txt+='# --- OP {:d} - WS {:.1f} - RPM {:.2f} \n'.format(iOP+1, WS, RPM) + txt+='# -----------------------------------------------------------------------\n' + txt+='# --- "Selected" modes\n' + txt+='# ID; Freq [Hz]; Zeta [-]; Mode content\n' skippedDamp=[] + skippedFreq=[] skippedEDBD=[] for im in np.arange(nFreqOut_loc): m = cd['Modes'][im] Desc = cd['ShortModeDescr'][im] zeta = m['DampingRatio'] + freq = m['NaturalFreq_Hz'] hasED = Desc.find('ED')>=0 hasBD = Desc.find('BD')>=0 hasAD = Desc.find('AD')>=0 - if skipHighDamp and (zeta>0.96 or abs(zeta)<1e-5): + if (freq>freqRange[1] or freqposDampRange[1] or abs(zeta)0: - txt+='---- Skipped (No ED/BD)\n' + txt+='# --- Skipped (No ED/BD)\n' for im in skippedEDBD: - m = cd['Modes'][im] - Desc = cd['ShortModeDescr'][im] - zeta = m['DampingRatio'] - txt+='{:02d} ; {:8.3f} ; {:7.4f} ; {:s}\n'.format(im+1,m['NaturalFreq_Hz'],m['DampingRatio'],Desc) + txt+=mode2txtline(cd, im) + if len(skippedFreq)>0: + txt+='# --- Skipped (Frequency outside of `freqRange`={})\n'.format(freqRange) + for im in skippedFreq: + txt+=mode2txtline(cd, im) if len(skippedDamp)>0: - txt+='---- Skipped (High Damping)\n' + txt+='# --- Skipped (Damping outside of `posDampRange`={})\n'.format(posDampRange) for im in skippedDamp: - m = cd['Modes'][im] - Desc = cd['ShortModeDescr'][im] - zeta = m['DampingRatio'] - txt+='{:02d} ; {:8.3f} ; {:7.4f} ; {:s}\n'.format(im+1,m['NaturalFreq_Hz'],m['DampingRatio'],Desc) + txt+=mode2txtline(cd, im) if txtFileName is not None: with open(txtFileName, 'w') as f: f.write(txt) diff --git a/pyFAST/linearization/examples/ex1a_OneLinFile_SimpleEigenAnalysis.py b/pyFAST/linearization/examples/ex1a_OneLinFile_SimpleEigenAnalysis.py index 9c5f5b6..cf3213c 100644 --- a/pyFAST/linearization/examples/ex1a_OneLinFile_SimpleEigenAnalysis.py +++ b/pyFAST/linearization/examples/ex1a_OneLinFile_SimpleEigenAnalysis.py @@ -17,7 +17,8 @@ print('Keys available:',lin.keys()) # --- Perform eigenvalue analysis -fd, zeta, Q, f0 = eigA(lin['A']) +#fd, zeta, Q, f0 = eigA(lin['A']) +fd, zeta, Q, f0 = lin.eva() print('Nat. freq. [Hz], Damping ratio [%]') print(np.column_stack((np.around(f0,4),np.around(zeta*100,4)))) diff --git a/pyFAST/linearization/linearization.py b/pyFAST/linearization/linearization.py index acd4072..9d0dc90 100644 --- a/pyFAST/linearization/linearization.py +++ b/pyFAST/linearization/linearization.py @@ -43,7 +43,9 @@ def campbell(templateFstFile, operatingPointsFile, workDir, toolboxDir, fastExe, nPerPeriod=36, baseDict=None, tStart=5400, trim=True, viz=False, trimGainPitch = 0.001, trimGainGenTorque = 300, maxTrq= None, - generateInputs=True, runFast=True, runMBC=True, prefix='',sortedSuffix=None, ylim=None, removeTwrAzimuth=False): + generateInputs=True, runFast=True, runMBC=True, prefix='',sortedSuffix=None, ylim=None, + removeTwrAzimuth=False, starSub=None, removeStatesPattern=None # Options for A matrices + ): """ Wrapper function to perform a Campbell diagram study see: writeLinearizationFiles, postproLinearization and postproMBC for more description of inputs. @@ -60,6 +62,14 @@ def campbell(templateFstFile, operatingPointsFile, workDir, toolboxDir, fastExe, - prefix: strings such that the output files will looked like: [folder prefix ] - sortedSuffix use a separate file where IDs have been sorted - runFast Logical to specify whether to run the simulations or not + - removeTwrAzimuth: if False do nothing + otherwise discard lin files where azimuth in [60, 180, 300]+/-4deg (close to tower). + - starSub: if None, raise an error if `****` are present + otherwise replace *** with `starSub` (e.g. 0) + see FASTLinearizationFile. + - removeStatesPattern: remove states matching a giving description pattern. + e.g: 'tower|Drivetrain' or '^AD' + see FASTLinearizationFile. """ Cases=pd.read_csv(caseFile); Cases.rename(columns=lambda x: x.strip(), inplace=True) @@ -80,7 +90,7 @@ def campbell(templateFstFile, operatingPointsFile, workDir, toolboxDir, fastExe, # --- Postprocess linearization outputs (MBC + modes ID) if runMBC: - OP, Freq, Damp, _, _, modeID_file = postproCampbell(FSTfilenames, removeTwrAzimuth=removeTwrAzimuth, suffix=suffix) + OP, Freq, Damp, _, _, modeID_file = postproCampbell(FSTfilenames, removeTwrAzimuth=removeTwrAzimuth, starSub=starSub, removeStatesPattern=removeStatesPattern, suffix=suffix) # --- Plot Campbell fig, axes = plotCampbell(OP, Freq, Damp, sx='WS_[m/s]', UnMapped=UnMapped, ylim=ylim) diff --git a/pyFAST/linearization/linfile.py b/pyFAST/linearization/linfile.py index c20c157..6b57fe4 100644 --- a/pyFAST/linearization/linfile.py +++ b/pyFAST/linearization/linfile.py @@ -12,6 +12,7 @@ from itertools import islice import numpy as np import re +from pyFAST.input_output.fast_linearization_file import FASTLinearizationFile def _isbool(str): flag=0 @@ -245,7 +246,65 @@ def readFASTMatrix(f): return name,tmp -def ReadFASTLinear(filename): +def ReadFASTLinear(filename, starSub=None, removeStatesPattern=None): + """ + Read one lin file. + + INPUTS: + - filename: linfile name, string. + - starSub: if None, raise an error if `****` are present + otherwise replace *** with `starSub` (e.g. 0) + see FASTLinearizationFile. + - removeStatesPattern: remove states matching a giving description pattern. + e.g: 'tower|Drivetrain' or '^AD' + see FASTLinearizationFile. + OUTPUTS: + - data: a dictionary with fields matching the MATLAB implementation. + """ + + # --- Read lin file + f = FASTLinearizationFile(filename, starSub=starSub, removeStatesPattern=removeStatesPattern) + + # --- Legacy structure. TODO, just use "f" + data={} + data['n_x'] = f.nx + data['n_u'] = f.nu + data['n_y'] = f.ny + data['n_z'] = f.nz + KeyMap={'t':'t'} + KeyMap.update({'Azimuth':'Azimuth', 'WindSpeed':'WindSpeed', 'RotSpeed':'RotSpeed'}) + KeyMap.update({'x_op':'x', 'xdot_op':'xdot', 'z_op':'z', 'y_op':'y'}) + for knew,kold in KeyMap.items(): + if kold in f.keys(): + data[knew] = f[kold] + data['WindSpeed'] = data['WindSpeed'] if data['WindSpeed'] is not None else np.nan + data['RotSpeed'] = data['RotSpeed'] if data['RotSpeed'] is not None else np.nan + data['Azimuth'] = np.mod(data['Azimuth'],2.0*np.pi) + if data['n_x']>0: + data['x_rotFrame'] = f['x_info']['RotatingFrame'] + data['x_DerivOrder'] = f['x_info']['DerivativeOrder'] + data['x_desc'] = f['x_info']['Description'] + data['xdot_desc'] = f['xdot_info']['Description'] + data['n_x2'] = np.sum(data['x_DerivOrder']== 2) + else: + data['n_x2'] = 0; + if data['n_z']>0: + data['z_desc'] = f['z_info']['Description'] + if data['n_u']>0: + data['u_rotFrame'] = f['u_info']['RotatingFrame'] + data['u_desc'] = f['u_info']['Description'] + if data['n_y']>0: + data['y_rotFrame'] = f['y_info']['RotatingFrame'] + data['y_desc'] = f['y_info']['Description'] + for k in ['A','B','C','D']: + if k in f.keys(): + data[k] = f[k] + return data, None + +def ReadFASTLinearLegacy(filename): + """ + Legacy reader, TODO remove in future release + """ def extractVal(lines, key): for l in lines: @@ -294,25 +353,6 @@ def readToMarker(fid, marker, nMax): except: data['WindSpeed'] = np.nan - # --- Old method for reading - # header = [f.readline() for _ in range(17)] - # info['fast_version'] = header[1].strip() - # info['modules'] = header[2].strip() - # info['description'] = header[4].strip() - # ln=7; - # data = np.array([line.split() for line in f.readlines()]).astype(np.float) - # data['t']=np.float(header[ln].split(':')[1].strip().split(' ')[0]) - # data['RotSpeed']=np.float(header[ln+1].split(':')[1].strip().split(' ')[0]) - # data['Azimuth']=np.float(header[ln+2].split(':')[1].strip().split(' ')[0]) - # data['WindSpeed']=np.float(header[ln+3].split(':')[1].strip().split(' ')[0]) - # data['n_x']=np.float(header[ln+4].split(':')[1].strip().split(' ')[0]) - # data['n_xd']=np.float(header[ln+5].split(':')[1].strip().split(' ')[0]) - # data['n_z']=np.float(header[ln+6].split(':')[1].strip().split(' ')[0]) - # data['n_u']=np.float(header[ln+7].split(':')[1].strip().split(' ')[0]) - # data['n_y']=np.float(header[ln+8].split(':')[1].strip().split(' ')[0]) - # if header[ln+9].split('?')[1].strip()=='Yes': - # SetOfMatrices=2 - data['Azimuth']=np.mod(data['Azimuth'],2.0*np.pi) try: # skip next three lines @@ -388,18 +428,34 @@ def readToMarker(fid, marker, nMax): raise -def get_Mats(FileNames, verbose=True, removeTwrAzimuth=False): +def get_Mats(FileNames, verbose=True, removeTwrAzimuth=False, starSub=None, removeStatesPattern=None): + """ + Extra main data from a list of lin files. + INPUTS: + - FileNames : list of lin files + - starSub: if None, raise an error if `****` are present + otherwise replace *** with `starSub` (e.g. 0) + see FASTLinearizationFile. + - removeStatesPattern: remove states matching a giving description pattern. + e.g: 'tower|Drivetrain' or '^AD' + see FASTLinearizationFile. + - removeTwrAzimuth: if False do nothing + otherwise discard lin files where azimuth in [60, 180, 300]+/-4deg (close to tower). + + """ NAzimStep = len(FileNames) data = [None]*NAzimStep # --- Read all files for iFile, filename in enumerate(FileNames): - data[iFile],_= ReadFASTLinear(FileNames[iFile]); + #data[iFile], _ = ReadFASTLinearLegacy(FileNames[iFile]); + data[iFile],_= ReadFASTLinear(FileNames[iFile], starSub=starSub, removeStatesPattern=removeStatesPattern); Azimuth = np.array([d['Azimuth'] for d in data])*180/np.pi # --- Sort by azimuth, not required, but filenames are not sorted, nor are the lin times azimuth ISort = np.argsort(Azimuth) Azimuth = Azimuth[ISort] data = [data[i] for i in ISort] FileNames = [FileNames[i] for i in ISort] + # --- Remove some azimuth if removeTwrAzimuth: IDiscard = [i for i,a in enumerate(Azimuth) if np.any(np.abs(np.array([60,180,300])-a)<4) ] n=len(FileNames[0]); sfmt='{:'+str(n+2)+'s}' @@ -572,34 +628,8 @@ def get_Mats(FileNames, verbose=True, removeTwrAzimuth=False): else: matData['RotTripletIndicesOutput'] = []; matData['n_RotTripletOutputs'] = 0; - - return matData, data -#matData,FAST_linData=get_Mats(FileNames) - -#print("\n".join(matData['DescStates'])) - -# print(info) -# print(data['t']) -# print(data['RotSpeed']) -# print(data['Azimuth']) -# print(data['WindSpeed']) -# print(data['n_x']) -# print(data['n_xd']) -# print(data['n_z']) -# print(data['n_u']) -# print(data['n_y']) -# print(data['n_x2']) -# print(data['x_op']) -# print(data['x_desc']) -# print(data['x_rotFrame']) -# print(data['xdot_op']) -# print(data['xdot_desc']) -# print(data['u_op']) -# print(data['u_desc']) -# print(data['u_rotFrame']) -# print(data['y_op']) -# print(data['y_desc']) -# print(data['y_rotFrame']) +if __name__ == '__main__': + madData,dat = get_Mats(['file.lin']) diff --git a/pyFAST/linearization/mbc.py b/pyFAST/linearization/mbc.py index c33d1ea..9bfee75 100644 --- a/pyFAST/linearization/mbc.py +++ b/pyFAST/linearization/mbc.py @@ -4,7 +4,7 @@ import numpy as np import scipy.linalg as scp import os -from pyFAST.linearization.linfile import get_Mats # TODO use pyFAST +from pyFAST.linearization.linfile import get_Mats # --------------------------------------------------------------------------------} @@ -120,20 +120,31 @@ def eiganalysis(A, ndof2=None, ndof1=None): # --------------------------------------------------------------------------------} # --- Main function # --------------------------------------------------------------------------------{ -def fx_mbc3(FileNames, verbose=True, removeTwrAzimuth=False): +def fx_mbc3(FileNames, verbose=True, starSub=None, removeStatesPattern=None, removeTwrAzimuth=False): """ - FileNames: list of lin files for a given operating point + Perform MBC2 funciton based on a list of lin files. + NOTE: variable names and data structure match MATLAB implementation. - NOTE: unlike the matlab function, fx_mbc3 does not write the modes for VTK visualization - Instead use the wrapper function def getCDDOP from pyFAST.linearization.tools + INPUTS: + - FileNames: list of lin files for a given operating point + - starSub: if None, raise an error if `****` are present + otherwise replace *** with `starSub` (e.g. 0) + see FASTLinearizationFile. + - removeStatesPattern: remove states matching a giving description pattern. + e.g: 'tower|Drivetrain' or '^AD' + see FASTLinearizationFile. + - removeTwrAzimuth: if False do nothing + otherwise discard lin files where azimuth in [60, 180, 300]+/-4deg (close to tower). + NOTE: unlike the matlab function, fx_mbc3 does not write the modes for VTK visualization + Instead use the wrapper function def getCDDOP from pyFAST.linearization.tools Original contribution by: Srinivasa B. Ramisett, ramisettisrinivas@yahoo.com, http://ramisetti.github.io """ MBC={} - matData, _ = get_Mats(FileNames, verbose=verbose, removeTwrAzimuth=removeTwrAzimuth) + matData, _ = get_Mats(FileNames, verbose=verbose, starSub=starSub, removeStatesPattern=removeStatesPattern, removeTwrAzimuth=removeTwrAzimuth) # print('matData[Omega] ', matData['Omega']) # print('matData[OmegaDot] ', matData['OmegaDot']) diff --git a/pyFAST/linearization/tools.py b/pyFAST/linearization/tools.py index 0858474..0d3693b 100644 --- a/pyFAST/linearization/tools.py +++ b/pyFAST/linearization/tools.py @@ -11,7 +11,9 @@ from pyFAST.linearization.campbell_data import campbell_diagram_data_oneOP # -def getCampbellDataOP(fstFile_or_linFiles, writeModes=None, BladeLen=None, TowerLen=None, removeTwrAzimuth=False, verbose=False, writeViz=False, **kwargs): +def getCampbellDataOP(fstFile_or_linFiles, writeModes=None, BladeLen=None, TowerLen=None, + removeTwrAzimuth=False, starSub=None, removeStatesPattern=None, verbose=False, + writeViz=False, **kwargs): """ Return Campbell Data at one operating point from a .fst file or a list of lin files INPUTS: @@ -30,6 +32,14 @@ def getCampbellDataOP(fstFile_or_linFiles, writeModes=None, BladeLen=None, Tower - TowerLen: tower length needed to scale the Campbell diagram data. if None: the length is inferred by reading the .fst file (and ED file) + - starSub: if None, raise an error if `****` are present + otherwise replace *** with `starSub` (e.g. 0) + see FASTLinearizationFile. + - removeStatesPattern: remove states matching a giving description pattern. + e.g: 'tower|Drivetrain' or '^AD' + see FASTLinearizationFile. + - removeTwrAzimuth: if False do nothing + otherwise discard lin files where azimuth in [60, 180, 300]+/-4deg (close to tower). - verbose: if True, more info is written to stdout - **kwargs: list of key/values to be passed to writeVizFile (see function below) @@ -43,7 +53,7 @@ def getCampbellDataOP(fstFile_or_linFiles, writeModes=None, BladeLen=None, Tower fstFile, linFiles = getFST_and_LinFiles(fstFile_or_linFiles, verbose=verbose) # --- Open lin files for given OP/fst file, perform MBC - MBCOP, matData = getMBCOP(fstFile=fstFile, linFiles=linFiles, verbose=verbose, removeTwrAzimuth=removeTwrAzimuth) + MBCOP, matData = getMBCOP(fstFile=fstFile, linFiles=linFiles, verbose=verbose, removeTwrAzimuth=removeTwrAzimuth, starSub=starSub, removeStatesPattern=removeStatesPattern) if MBCOP is None: return None, None @@ -78,10 +88,10 @@ def getCampbellDataOP(fstFile_or_linFiles, writeModes=None, BladeLen=None, Tower return CDDOP, MBCOP -def getCampbellDataOPs(fstFiles, writeModes=None, BladeLen=None, TowerLen=None, removeTwrAzimuth=False, verbose=False, **kwargs): +def getCampbellDataOPs(fstFiles, BladeLen=None, TowerLen=None, verbose=False, **kwargs): """ Return Campbell Data at several operating points from a list of .fst files - see getCDDOP for inputs + see getCampbellDataOP for input arguments """ # --- Estimate blade length and tower length for scaling if BladeLen is None and TowerLen is None: @@ -91,7 +101,7 @@ def getCampbellDataOPs(fstFiles, writeModes=None, BladeLen=None, TowerLen=None, MBC = [] CDD = [] for i_lin, fstFile in enumerate(fstFiles): - CDDOP, MBCOP = getCampbellDataOP(fstFile, writeModes=writeModes, BladeLen=BladeLen, TowerLen=TowerLen, removeTwrAzimuth=removeTwrAzimuth, verbose=verbose, **kwargs) + CDDOP, MBCOP = getCampbellDataOP(fstFile, BladeLen=BladeLen, TowerLen=TowerLen, verbose=verbose, **kwargs) if MBCOP is not None: CDD.append(CDDOP) MBC.append(MBCOP) @@ -100,9 +110,22 @@ def getCampbellDataOPs(fstFiles, writeModes=None, BladeLen=None, TowerLen=None, raise Exception('No linearization file found') return CDD, MBC -def getMBCOP(fstFile, linFiles=None, verbose=False, removeTwrAzimuth=False): +def getMBCOP(fstFile, linFiles=None, verbose=False, removeTwrAzimuth=False, starSub=None, removeStatesPattern=None): """ Run necessary MBC for an OpenFAST file (one operating point) + + INPUTS: + - fstFile: main openfast `.fst` filename + - linFiles: list of linfiles, inferred from fstfile if None provided + - starSub: if None, raise an error if `****` are present + otherwise replace *** with `starSub` (e.g. 0) + see FASTLinearizationFile. + - removeStatesPattern: remove states matching a giving description pattern. + e.g. r'^AD' : remove the AeroDyn states + see FASTLinearizationFile. + - removeTwrAzimuth: if False do nothing + otherwise discard lin files where azimuth in [60, 180, 300]+/-4deg (close to tower). + """ # --- Find available lin files @@ -111,7 +134,7 @@ def getMBCOP(fstFile, linFiles=None, verbose=False, removeTwrAzimuth=False): # --- run MBC3 and campbell post_pro on lin files, generate postMBC file if needed if len(linFiles)>0: - MBC, matData = fx_mbc3(linFiles, verbose=False, removeTwrAzimuth=removeTwrAzimuth) + MBC, matData = fx_mbc3(linFiles, verbose=False, removeTwrAzimuth=removeTwrAzimuth, starSub=starSub, removeStatesPattern=removeStatesPattern) else: return None, None From 3f4fce7c8b9e508667057f87b66cd02eb65d792e Mon Sep 17 00:00:00 2001 From: Emmanuel Branlard Date: Tue, 22 Aug 2023 17:59:16 -0600 Subject: [PATCH 101/124] IO: Lin: speed up of reader for baseline case with floats --- .../input_output/fast_linearization_file.py | 295 +++++++++--------- 1 file changed, 154 insertions(+), 141 deletions(-) diff --git a/pyFAST/input_output/fast_linearization_file.py b/pyFAST/input_output/fast_linearization_file.py index dd18069..e8a13f9 100644 --- a/pyFAST/input_output/fast_linearization_file.py +++ b/pyFAST/input_output/fast_linearization_file.py @@ -7,6 +7,8 @@ File = dict class BrokenFormatError(Exception): pass +class SlowReaderNeededError(Exception): + pass _lin_vec = ['x','xd','xdot','u','y','z','header'] @@ -80,144 +82,61 @@ def read(self, filename=None, starSub=None, removeStatesPattern=None): # --- StarValues replacement `*****` -> inf starPattern = re.compile(r"[\*]+") starSubStr = ' inf ' - - def extractVal(lines, key, NA=None, missing=None, dtype=float): - for l in lines: - if l.find(key)>=0: - #l = starPattern.sub(starSubStr, l) - try: - return dtype(l.split(key)[1].split()[0]) - except: - return NA - return missing - - def readToMarker(fid, marker, nMax): - lines=[] - for i, line in enumerate(fid): - if i>nMax: - raise BrokenFormatError('`{}` not found in file'.format(marker)) - if line.find(marker)>=0: - break - lines.append(line.strip()) - return lines, line - - def readOP(fid, n, name='', defaultDerivOrder=1): - OP=[] - Var = {'RotatingFrame': [], 'DerivativeOrder': [], 'Description': []} - colNames=fid.readline().strip() - dummy= fid.readline().strip() - bHasDeriv= colNames.find('Derivative Order')>=0 - for i, line in enumerate(fid): - line = line.strip() - line = starPattern.sub(starSubStr, line) - sp = line.split() - if sp[1].find(',')>=0: - # Most likely this OP has three values (e.g. orientation angles) - # For now we discard the two other values - OP.append(float(sp[1][:-1])) - iRot=4 - else: - OP.append(float(sp[1])) - iRot=2 - Var['RotatingFrame'].append(sp[iRot]) - if bHasDeriv: - Var['DerivativeOrder'].append(int(sp[iRot+1])) - Var['Description'].append(' '.join(sp[iRot+2:]).strip()) - else: - Var['DerivativeOrder'].append(defaultDerivOrder) - Var['Description'].append(' '.join(sp[iRot+1:]).strip()) - if i>=n-1: - break - OP = np.asarray(OP) - nInf = sum(np.isinf(OP)) - if nInf>0: - sErr = 'Some ill-formated/infinite values (e.g. `*******`) were found in the vector `{}`\n\tin linflile: {}'.format(name, self.filename) - if starSub is None: - raise Exception(sErr) - else: - print('[WARN] '+sErr) - OP[np.isinf(OP)] = starSub - - Var['RotatingFrame'] = np.asarray(Var['RotatingFrame']) - Var['DerivativeOrder'] = np.asarray(Var['DerivativeOrder']) - Var['Description'] = np.asarray(Var['Description']) - return OP, Var - - def readMat(fid, n, m, name=''): - vals=[starPattern.sub(starSubStr, fid.readline().strip() ).split() for i in np.arange(n)] - vals = np.array(vals) - try: - vals = np.array(vals).astype(float) # This could potentially fail - except: - raise Exception('Failed to convert into an array of float the matrix `{}`\n\tin linfile: {}'.format(name, self.filename)) - if vals.shape[0]!=n or vals.shape[1]!=m: - shape1 = vals.shape - shape2 = (n,m) - raise Exception('Shape of matrix `{}` has wrong dimension ({} instead of {})\n\tin linfile: {}'.format(name, shape1, shape2, name, self.filename)) - - nNaN = sum(np.isnan(vals.ravel())) - nInf = sum(np.isinf(vals.ravel())) - if nInf>0: - sErr = 'Some ill-formated/infinite values (e.g. `*******`) were found in the matrix `{}`\n\tin linflile: {}'.format(name, self.filename) - if starSub is None: - raise Exception(sErr) - else: - print('[WARN] '+sErr) - vals[np.isinf(vals)] = starSub - if nNaN>0: - raise Exception('Some NaN values were found in the matrix `{}`\n\tin linfile: `{}`.'.format(name, self.filename)) - return vals - - - # Reading - with open(self.filename, 'r', errors="surrogateescape") as f: - # --- Reader header - self['header'], lastLine=readToMarker(f, 'Jacobians included', 30) - self['header'].append(lastLine) - nx = extractVal(self['header'],'Number of continuous states:' , dtype=int, NA=np.nan, missing=None) - nxd = extractVal(self['header'],'Number of discrete states:' , dtype=int, NA=np.nan, missing=None) - nz = extractVal(self['header'],'Number of constraint states:' , dtype=int, NA=np.nan, missing=None) - nu = extractVal(self['header'],'Number of inputs:' , dtype=int, NA=np.nan, missing=None) - ny = extractVal(self['header'],'Number of outputs:' , dtype=int, NA=np.nan, missing=None) - bJac = extractVal(self['header'],'Jacobians included in this file?', dtype=bool, NA=False, missing=None) - self['Azimuth'] = extractVal(self['header'], 'Azimuth:' , dtype=float, NA=np.nan, missing=None) - self['RotSpeed'] = extractVal(self['header'], 'Rotor Speed:', dtype=float, NA=np.nan, missing=None) # rad/s - self['WindSpeed'] = extractVal(self['header'], 'Wind Speed:' , dtype=float, NA=np.nan, missing=None) - self['t'] = extractVal(self['header'],'Simulation time:' , dtype=float, NA=np.nan, missing=None) - - for i, line in enumerate(f): - line = line.strip() - if line.find('Order of continuous states:')>=0: - self['x'], self['x_info'] = readOP(f, nx, 'x', defaultDerivOrder=1) - elif line.find('Order of continuous state derivatives:')>=0: - self['xdot'], self['xdot_info'] = readOP(f, nx, 'xdot', defaultDerivOrder=2) - elif line.find('Order of discrete states:')>=0: - self['xd'], self['xd_info'] = readOP(f, nxd, 'xd', defaultDerivOrder=2) - elif line.find('Order of inputs')>=0: - self['u'], self['u_info'] = readOP(f, nu, 'u', defaultDerivOrder=0) - elif line.find('Order of outputs')>=0: - self['y'], self['y_info'] = readOP(f, ny, 'y', defaultDerivOrder=0) - elif line.find('Order of constraint states:')>=0: - self['z'], self['z_info'] = readOP(f, nz, 'z', defaultDerivOrder=0) - elif line.find('A:')>=0: - self['A'] = readMat(f, nx, nx, 'A') - elif line.find('B:')>=0: - self['B'] = readMat(f, nx, nu, 'B') - elif line.find('C:')>=0: - self['C'] = readMat(f, ny, nx, 'C') - elif line.find('D:')>=0: - self['D'] = readMat(f, ny, nu, 'D') - elif line.find('dUdu:')>=0: - self['dUdu'] = readMat(f, nu, nu,'dUdu') - elif line.find('dUdy:')>=0: - self['dUdy'] = readMat(f, nu, ny,'dUdy') - elif line.find('StateRotation:')>=0: - pass - # TODO - #StateRotation: - elif line.find('ED M:')>=0: - self['EDDOF'] = line[5:].split() - self['M'] = readMat(f, 24, 24,'M') + starSubFn = lambda si: starPattern.sub(starSubStr, si) + + # Reading function, with slow or fast reader. See sub functions at end of this file + def doRead(slowReader=False): + with open(self.filename, 'r', errors="surrogateescape") as f: + # --- Reader header + self['header'], lastLine=readToMarker(f, 'Jacobians included', 30) + self['header'].append(lastLine) + nx = extractVal(self['header'],'Number of continuous states:' , dtype=int, NA=np.nan, missing=None) + nxd = extractVal(self['header'],'Number of discrete states:' , dtype=int, NA=np.nan, missing=None) + nz = extractVal(self['header'],'Number of constraint states:' , dtype=int, NA=np.nan, missing=None) + nu = extractVal(self['header'],'Number of inputs:' , dtype=int, NA=np.nan, missing=None) + ny = extractVal(self['header'],'Number of outputs:' , dtype=int, NA=np.nan, missing=None) + bJac = extractVal(self['header'],'Jacobians included in this file?', dtype=bool, NA=False, missing=None) + self['Azimuth'] = extractVal(self['header'], 'Azimuth:' , dtype=float, NA=np.nan, missing=None) + self['RotSpeed'] = extractVal(self['header'], 'Rotor Speed:', dtype=float, NA=np.nan, missing=None) # rad/s + self['WindSpeed'] = extractVal(self['header'], 'Wind Speed:' , dtype=float, NA=np.nan, missing=None) + self['t'] = extractVal(self['header'],'Simulation time:' , dtype=float, NA=np.nan, missing=None) + for i, line in enumerate(f): + line = line.strip() + if line.find('Order of continuous states:')>=0: + self['x'], self['x_info'] = readOP(f, nx, 'x', defaultDerivOrder=1, starSubFn=starSubFn, starSub=starSub) + elif line.find('Order of continuous state derivatives:')>=0: + self['xdot'], self['xdot_info'] = readOP(f, nx, 'xdot', defaultDerivOrder=2, starSubFn=starSubFn, starSub=starSub) + elif line.find('Order of discrete states:')>=0: + self['xd'], self['xd_info'] = readOP(f, nxd, 'xd', defaultDerivOrder=2, starSubFn=starSubFn, starSub=starSub) + elif line.find('Order of inputs')>=0: + self['u'], self['u_info'] = readOP(f, nu, 'u', defaultDerivOrder=0, starSubFn=starSubFn, starSub=starSub) + elif line.find('Order of outputs')>=0: + self['y'], self['y_info'] = readOP(f, ny, 'y', defaultDerivOrder=0, starSubFn=starSubFn, starSub=starSub) + elif line.find('Order of constraint states:')>=0: + self['z'], self['z_info'] = readOP(f, nz, 'z', defaultDerivOrder=0, starSubFn=starSubFn, starSub=starSub) + elif line.find('A:')>=0: + self['A'] = readMat(f, nx, nx, 'A', slowReader=slowReader, filename=self.filename, starSubFn=starSubFn, starSub=starSub) + elif line.find('B:')>=0: + self['B'] = readMat(f, nx, nu, 'B', slowReader=slowReader, filename=self.filename, starSubFn=starSubFn, starSub=starSub) + elif line.find('C:')>=0: + self['C'] = readMat(f, ny, nx, 'C', slowReader=slowReader, filename=self.filename, starSubFn=starSubFn, starSub=starSub) + elif line.find('D:')>=0: + self['D'] = readMat(f, ny, nu, 'D', slowReader=slowReader, filename=self.filename, starSubFn=starSubFn, starSub=starSub) + elif line.find('dUdu:')>=0: + self['dUdu'] = readMat(f, nu, nu,'dUdu', slowReader=slowReader, filename=self.filename, starSubFn=starSubFn, starSub=starSub) + elif line.find('dUdy:')>=0: + self['dUdy'] = readMat(f, nu, ny,'dUdy', slowReader=slowReader, filename=self.filename, starSubFn=starSubFn, starSub=starSub) + elif line.find('StateRotation:')>=0: + pass + # TODO + #StateRotation: + elif line.find('ED M:')>=0: + self['EDDOF'] = line[5:].split() + self['M'] = readMat(f, 24, 24,'M', slowReader=slowReader, filename=self.filename, starSubFn=starSubFn, starSub=starSub) + try: + doRead(slowReader=False) + except SlowReaderNeededError: + doRead(slowReader=True) if removeStatesPattern is not None: self.removeStates(pattern=removeStatesPattern) @@ -442,9 +361,6 @@ def eva(self, normQ=None, sort=True, discardIm=True): return freq_d, zeta, Q, freq_0 - - - def short_descr(slist): """ Shorten and "unify" the description from lin file """ def shortname(s): @@ -587,6 +503,103 @@ def shortname(s): +def extractVal(lines, key, NA=None, missing=None, dtype=float): + for l in lines: + if l.find(key)>=0: + #l = starPattern.sub(starSubStr, l) + try: + return dtype(l.split(key)[1].split()[0]) + except: + return NA + return missing + +def readToMarker(fid, marker, nMax): + lines=[] + for i, line in enumerate(fid): + if i>nMax: + raise BrokenFormatError('`{}` not found in file'.format(marker)) + if line.find(marker)>=0: + break + lines.append(line.strip()) + return lines, line + +def readOP(fid, n, name='', defaultDerivOrder=1, filename='', starSubFn=None, starSub=None): + OP=[] + Var = {'RotatingFrame': [], 'DerivativeOrder': [], 'Description': []} + colNames=fid.readline().strip() + dummy= fid.readline().strip() + bHasDeriv= colNames.find('Derivative Order')>=0 + for i, line in enumerate(fid): + line = line.strip() + line = starSubFn(line) + sp = line.split() + if sp[1].find(',')>=0: + # Most likely this OP has three values (e.g. orientation angles) + # For now we discard the two other values + OP.append(float(sp[1][:-1])) + iRot=4 + else: + OP.append(float(sp[1])) + iRot=2 + Var['RotatingFrame'].append(sp[iRot]) + if bHasDeriv: + Var['DerivativeOrder'].append(int(sp[iRot+1])) + Var['Description'].append(' '.join(sp[iRot+2:]).strip()) + else: + Var['DerivativeOrder'].append(defaultDerivOrder) + Var['Description'].append(' '.join(sp[iRot+1:]).strip()) + if i>=n-1: + break + OP = np.asarray(OP) + nInf = np.sum(np.isinf(OP)) + if nInf>0: + sErr = 'Some ill-formated/infinite values (e.g. `*******`) were found in the vector `{}`\n\tin linflile: {}'.format(name, filename) + if starSub is None: + raise Exception(sErr) + else: + print('[WARN] '+sErr) + OP[np.isinf(OP)] = starSub + + Var['RotatingFrame'] = np.asarray(Var['RotatingFrame']) + Var['DerivativeOrder'] = np.asarray(Var['DerivativeOrder']) + Var['Description'] = np.asarray(Var['Description']) + return OP, Var + + + +def readMat(fid, n, m, name='', slowReader=False, filename='', starSubFn=None, starSub=None): + if not slowReader: + try: + return np.array([fid.readline().strip().split() for i in np.arange(n)],dtype=float) + except: + print('[INFO] Failed to read some value in matrix {}, trying slower reader'.format(name)) + raise SlowReaderNeededError() + else: + #vals = vals.ravel() + #vals = np.array(list(map(starSubFn, vals))).reshape(n,m) + vals=np.array([starSubFn( fid.readline().strip() ).split() for i in np.arange(n)], dtype=str) + try: + vals = vals.astype(float) # This could potentially fail + except: + raise Exception('Failed to convert into an array of float the matrix `{}`\n\tin linfile: {}'.format(name, filename)) + if vals.shape[0]!=n or vals.shape[1]!=m: + shape1 = vals.shape + shape2 = (n,m) + raise Exception('Shape of matrix `{}` has wrong dimension ({} instead of {})\n\tin linfile: {}'.format(name, shape1, shape2, name, filename)) + + nNaN = np.sum(np.isnan(vals.ravel())) + nInf = np.sum(np.isinf(vals.ravel())) + if nInf>0: + sErr = 'Some ill-formated/infinite values (e.g. `*******`) were found in the matrix `{}`\n\tin linflile: {}'.format(name, filename) + if starSub is None: + raise Exception(sErr) + else: + print('[WARN] '+sErr) + vals[np.isinf(vals)] = starSub + if nNaN>0: + raise Exception('Some NaN values were found in the matrix `{}`\n\tin linfile: `{}`.'.format(name, filename)) + return vals + if __name__ == '__main__': f = FASTLinearizationFile('../../data/example_files/StandstillSemi_ForID_EDHD.1.lin') print(f) From 69ec49688afb4a3973899dcc890fd174be7459c3 Mon Sep 17 00:00:00 2001 From: pibo Date: Fri, 15 Sep 2023 15:27:40 -0600 Subject: [PATCH 102/124] fcl non negative, try except around 3d corrections --- pyFAST/airfoils/Polar.py | 13 ++++++++----- pyFAST/converters/hawc2ToOpenfast.py | 15 +++++++++------ 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/pyFAST/airfoils/Polar.py b/pyFAST/airfoils/Polar.py index fe19339..e627807 100644 --- a/pyFAST/airfoils/Polar.py +++ b/pyFAST/airfoils/Polar.py @@ -394,8 +394,11 @@ def correction3D( / cl_slope * (1.6 * chord_over_r / 0.1267 * (a - chord_over_r ** expon) / (b + chord_over_r ** expon) - 1) ) + # Force fcl to stay non-negative + if fcl < 0.: + fcl = 0. else: - fcl=1.0 + fcl=0.0 elif lift_method == "Snel": # Snel correction fcl = 3.0 * chord_over_r ** 2.0 @@ -730,7 +733,7 @@ def unsteadyParams(self, window_offset=None, nMin=720): else: window = [-20, 20] try: - alpha0cn = _find_alpha0(alpha, cn, window, direction='up') + alpha0cn = _find_alpha0(alpha, cn, window, direction='up', value_if_constant = 0.) except NoCrossingException: print("[WARN] Polar: Cn unsteady, cannot find zero crossing with up direction, trying down direction") alpha0cn = _find_alpha0(alpha, cn, window, direction='down') @@ -1265,17 +1268,17 @@ def _alpha_window_in_bounds(alpha, window): return window -def _find_alpha0(alpha, coeff, window, direction='up'): +def _find_alpha0(alpha, coeff, window, direction='up', value_if_constant = np.nan): """Finds the point where coeff(alpha)==0 using interpolation. The search is narrowed to a window that can be specified by the user. The default window is yet enough for cases that make physical sense. The angle alpha0 is found by looking at a zero up crossing in this window, and interpolation is used to find the exact location. """ # Constant case or only one value - if np.all(coeff == coeff[0]) or len(coeff) == 1: + if np.max(abs((coeff - coeff[0])<1e-8)) or len(coeff) == 1: if coeff[0] == 0: return 0 else: - return np.nan + return value_if_constant # Ensuring window is within our alpha values window = _alpha_window_in_bounds(alpha, window) diff --git a/pyFAST/converters/hawc2ToOpenfast.py b/pyFAST/converters/hawc2ToOpenfast.py index 11c4b8d..c87df17 100644 --- a/pyFAST/converters/hawc2ToOpenfast.py +++ b/pyFAST/converters/hawc2ToOpenfast.py @@ -192,12 +192,15 @@ def AE_PC_C2_toAD(ae_filename, pc_filename, blade_c2def, r_AD=None, ADbldFilenam P = Polar(Re=Re, alpha=vAlpha, cl=M[:,1], cd=M[:,2], cm=M[:,3], radians=False) # Apply 3D correction if r>0 and correction3D: - P = P.correction3D( - r_over_R = r/r_max, - chord_over_r = c/r, - tsr=tsr, - lift_method="DuSelig", drag_method="None", blending_method="linear_25_45", - max_cl_corr=0.25) + try: + P = P.correction3D( + r_over_R = r/r_max, + chord_over_r = c/r, + tsr=tsr, + lift_method="DuSelig", drag_method="None", blending_method="linear_25_45", + max_cl_corr=0.25) + except: + print('3D correction not applied at station ', ir) # Store polars M = np.column_stack((P.alpha, P.cl, P.cd, P.cm)) From 41d829c435dd0c6be3030fb85b574f930f5ee12e Mon Sep 17 00:00:00 2001 From: Regis Thedin Date: Tue, 26 Sep 2023 14:36:56 -0600 Subject: [PATCH 103/124] FF: automatic seeds up to 30 seeds --- pyFAST/fastfarm/FASTFarmCaseCreation.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/pyFAST/fastfarm/FASTFarmCaseCreation.py b/pyFAST/fastfarm/FASTFarmCaseCreation.py index a93ebee..b3847a7 100644 --- a/pyFAST/fastfarm/FASTFarmCaseCreation.py +++ b/pyFAST/fastfarm/FASTFarmCaseCreation.py @@ -390,11 +390,17 @@ def _checkInputs(self): # Check on seed parameters if not isinstance(self.nSeeds,int): raise ValueError(f'An integer number of seeds should be requested. Got {self.nSeeds}.') + if self.nSeeds > 30: + raise ValueError(f'Number of seeds requested is larger than 30. For the case of {self.nSeeds} seeds, '\ + f'pass seedValues as a {self.nSeeds}-sized array of scalars.') if self.seedValues is None: - self.seedValues = [2318573, 122299, 123456, 389432, -432443, 9849898] + self.seedValues = [2318573, 122299, 123456, 389432, -432443, 9849898, 432425, 894832, 849324, 678095, + 1235456, 435342, 897023, 423800, -898881, 2988900, 798911, 482391, 892111, 899190, + 7693202, 587924, 890090, 435646, -454899, -785138, -78564, -17944, -99021, 389432] + self.seedValues = self.seedValues[:self.nSeeds] if len(self.seedValues) != self.nSeeds: - raise ValueError(f'Number of seeds is {self.nSeeds} but {len(self.seedValues)} seed values were given. '\ - f'Adjust the seedValues array accordingly') + raise ValueError(f'The array seedValues has been passed, but its length does not correspond '\ + f'to the number of seeds requested.') # Check LES parameters if self.LESpath is None: @@ -1596,7 +1602,7 @@ def TS_high_create_symlink(self): try: os.symlink(src, dst) except FileExistsError: - if self.verbose>1: print(f'File {dst} already exists. Skipping symlink.') + if self.verbose>1: print(f' File {dst} already exists. Skipping symlink.') os.chdir(notepath) From dbad0873612fce6ff68a3628cd6ba5dc1a71908d Mon Sep 17 00:00:00 2001 From: Regis Thedin Date: Wed, 27 Sep 2023 11:05:16 -0600 Subject: [PATCH 104/124] FF: Fix hardcoded filenames for inflowwind --- pyFAST/fastfarm/FASTFarmCaseCreation.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pyFAST/fastfarm/FASTFarmCaseCreation.py b/pyFAST/fastfarm/FASTFarmCaseCreation.py index b3847a7..0aff879 100644 --- a/pyFAST/fastfarm/FASTFarmCaseCreation.py +++ b/pyFAST/fastfarm/FASTFarmCaseCreation.py @@ -610,9 +610,9 @@ def copyTurbineFilesForEachCase(self, writeFiles=True): self.InflowWindFile['PropagationDir'] = 0 self.InflowWindFile['Filename_BTS'] = '"./TurbSim"' if writeFiles: - self.InflowWindFile.write( os.path.join(currPath,f'InflowWind.dat')) + self.InflowWindFile.write( os.path.join(currPath,self.IWfilename)) for seed in range(self.nSeeds): - self.InflowWindFile.write( os.path.join(currPath,f'Seed_{seed}','InflowWind.dat')) + self.InflowWindFile.write( os.path.join(currPath,f'Seed_{seed}',self.IWfilename)) for t in range(self.nTurbines): @@ -731,11 +731,11 @@ def _were_all_turbine_files_copied(self): if not _: return False _ = checkIfExists(os.path.join(currPath,self.controllerInputfilename)) if not _: return False - _ = checkIfExists( os.path.join(currPath,f'InflowWind.dat')) + _ = checkIfExists( os.path.join(currPath,self.IWfilename)) if not _: return False for seed in range(self.nSeeds): - _ = checkIfExists(os.path.join(currPath,f'Seed_{seed}','InflowWind.dat')) + _ = checkIfExists(os.path.join(currPath,f'Seed_{seed}',self.IWfilename)) if not _: return False for t in range(self.nTurbines): From 3a27861d2504ddc0428778fb008a909d4f7ef419 Mon Sep 17 00:00:00 2001 From: Regis Thedin Date: Wed, 27 Sep 2023 11:12:17 -0600 Subject: [PATCH 105/124] FF: misc small improvements --- pyFAST/fastfarm/FASTFarmCaseCreation.py | 22 ++++++++++++++----- .../examples/Ex3_FFarmCompleteSetup.py | 8 ++++--- 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/pyFAST/fastfarm/FASTFarmCaseCreation.py b/pyFAST/fastfarm/FASTFarmCaseCreation.py index 0aff879..7569374 100644 --- a/pyFAST/fastfarm/FASTFarmCaseCreation.py +++ b/pyFAST/fastfarm/FASTFarmCaseCreation.py @@ -1252,6 +1252,9 @@ def TS_low_slurm_prepare(self, slurmfilepath): shutil.copy2(slurmfilepath, os.path.join(self.path, self.slurmfilename_low)) + # Determine memory-per-cpu + memory_per_cpu = int(150000/self.nSeeds) + # Change job name (for convenience only) _ = subprocess.call(f"sed -i 's|#SBATCH --job-name=lowBox|#SBATCH --job-name=lowBox_{os.path.basename(self.path)}|g' {self.slurmfilename_low}", cwd=self.path, shell=True) # Change the path inside the script to the desired one @@ -1259,6 +1262,8 @@ def TS_low_slurm_prepare(self, slurmfilepath): _ = subprocess.call(sed_command, cwd=self.path, shell=True) # Change number of nodes values _ = subprocess.call(f"sed -i 's|#SBATCH --nodes=2|#SBATCH --nodes={int(np.ceil(self.nConditions*self.nSeeds/6))}|g' {self.slurmfilename_low}", cwd=self.path, shell=True) + # Change memory per cpu + _ = subprocess.call(f"sed -i 's|--mem-per-cpu=25000M|--mem-per-cpu={memory_per_cpu}M|g' {self.slurmfilename_low}", cwd=self.path, shell=True) # Assemble list of conditions and write it listtoprint = "' '".join(self.condDirList) sed_command = f"""sed -i "s|^condList.*|condList=('{listtoprint}')|g" {self.slurmfilename_low}""" @@ -1267,8 +1272,8 @@ def TS_low_slurm_prepare(self, slurmfilepath): _ = subprocess.call(f"sed -i 's|nSeeds=6|nSeeds={self.nSeeds}|g' {self.slurmfilename_low}", cwd=self.path, shell=True) - if self.nSeeds != 6: - print(f'--- WARNING: The memory-per-cpu on the low-res boxes SLURM script is configured for 6 seeds, not {self.nSeeds}.') + if self.nSeeds > 6: + print(f'--- WARNING: The memory-per-cpu on the low-res boxes SLURM script might be too low given {self.nSeeds} seeds.') def TS_low_slurm_submit(self, qos='normal', A=None, t=None): @@ -2144,7 +2149,7 @@ def FF_check_output(self): - def plot(self, figsize=(15,7), fontsize=14, saveFig=True, returnFig=False): + def plot(self, figsize=(15,7), fontsize=14, saveFig=True, returnFig=False, figFormat='png'): import matplotlib.pyplot as plt fig, ax = plt.subplots(figsize=figsize) @@ -2192,8 +2197,8 @@ def plot(self, figsize=(15,7), fontsize=14, saveFig=True, returnFig=False): _, ind = np.unique(allyaw_currwdir, axis=0, return_index=True) yaw_currwdir = allyaw_currwdir[np.sort(ind)].values # duplicates removed, same order as original array for yaw in yaw_currwdir: - ax.plot([dst.x.values-(dst.D.values/2)*sind(yaw-inflow.values), dst.x.values+(dst.D.values/2)*sind(yaw-inflow.values)], - [dst.y.values-(dst.D.values/2)*cosd(yaw-inflow.values), dst.y.values+(dst.D.values/2)*cosd(yaw-inflow.values)], c=color, alpha=alphas[j]) + ax.plot([dst.x.values-(dst.D.values/2)*sind(yaw), dst.x.values+(dst.D.values/2)*sind(yaw)], + [dst.y.values-(dst.D.values/2)*cosd(yaw), dst.y.values+(dst.D.values/2)*cosd(yaw)], c=color, alpha=alphas[j]) # plot convex hull of farm (or line) for given inflow turbs = self.wts_rot_ds.sel(inflow_deg=inflow)[['x','y']].to_array().transpose() @@ -2218,7 +2223,12 @@ def plot(self, figsize=(15,7), fontsize=14, saveFig=True, returnFig=False): ax.set_aspect('equal') if saveFig: - fig.savefig(os.path.join(self.path,'farm.png'), bbox_inches='tight', facecolor='white', transparent=False) + if figFormat == 'png': + fig.savefig(os.path.join(self.path,'farm.png'), bbox_inches='tight', facecolor='white', transparent=False) + elif figFormat == 'pdf': + fig.savefig(os.path.join(self.path,'farm.pdf'), bbox_inches='tight', facecolor='white', transparent=False) + else: + raise ValueError (f'Figure format not recognized. Options are png and pdf.') if returnFig: return fig, ax diff --git a/pyFAST/fastfarm/examples/Ex3_FFarmCompleteSetup.py b/pyFAST/fastfarm/examples/Ex3_FFarmCompleteSetup.py index d44e8ab..318fb72 100644 --- a/pyFAST/fastfarm/examples/Ex3_FFarmCompleteSetup.py +++ b/pyFAST/fastfarm/examples/Ex3_FFarmCompleteSetup.py @@ -122,9 +122,11 @@ def main(): # Initial setup - case = FFCaseCreation(path, wts, tmax, zbot, vhub, shear, - TIvalue, inflow_deg, dt_high_les, ds_high_les, extent_high, - dt_low_les, ds_low_les, extent_low, ffbin, mod_wake, LESpath=LESpath, + case = FFCaseCreation(path, wts, tmax, zbot, vhub, shear, TIvalue, inflow_deg, + dt_high_les, ds_high_les, extent_high, + dt_low_les, ds_low_les, extent_low, + ffbin, mod_wake, yaw_init, + nSeeds=nSeeds, LESpath=LESpath, verbose=1) case.setTemplateFilename(templatePath, EDfilename, SEDfilename, HDfilename, SrvDfilename, ADfilename, From e419fb9bb5a0cc2482dbbd86aee2d55c9060f23a Mon Sep 17 00:00:00 2001 From: Regis Thedin Date: Wed, 4 Oct 2023 15:55:35 -0600 Subject: [PATCH 106/124] FF: Remove misleading comment --- pyFAST/fastfarm/FASTFarmCaseCreation.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pyFAST/fastfarm/FASTFarmCaseCreation.py b/pyFAST/fastfarm/FASTFarmCaseCreation.py index 7569374..3f7d52f 100644 --- a/pyFAST/fastfarm/FASTFarmCaseCreation.py +++ b/pyFAST/fastfarm/FASTFarmCaseCreation.py @@ -1489,7 +1489,6 @@ def TS_high_setup(self, writeFiles=True): # Create and write new Low.inp files creating the proper box with proper resolution currentTS = TSCaseCreation(D_, HubHt_, Vhub_, tivalue_, shear_, x=xloc_, y=yloc_, zbot=self.zbot, cmax=self.cmax, fmax=self.fmax, Cmeander=self.Cmeander, boxType=boxType, high_ext=self.extent_high) - # !!!!!!!!!!! I have to give the resolution on the call above!!!! currentTS.writeTSFile(self.turbsimHighfilepath, currentTSHighFile, tmax=self.tmax, turb=t, verbose=self.verbose) # Modify some values and save file (some have already been set in the call above) From b7a4bf242f894c8188d17bdbe0dba73d4f155b94 Mon Sep 17 00:00:00 2001 From: pibo Date: Thu, 5 Oct 2023 14:27:11 -0600 Subject: [PATCH 107/124] fix check if cl is flat --- pyFAST/airfoils/Polar.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyFAST/airfoils/Polar.py b/pyFAST/airfoils/Polar.py index e627807..e098e58 100644 --- a/pyFAST/airfoils/Polar.py +++ b/pyFAST/airfoils/Polar.py @@ -1274,7 +1274,7 @@ def _find_alpha0(alpha, coeff, window, direction='up', value_if_constant = np.na The angle alpha0 is found by looking at a zero up crossing in this window, and interpolation is used to find the exact location. """ # Constant case or only one value - if np.max(abs((coeff - coeff[0])<1e-8)) or len(coeff) == 1: + if np.all(abs((coeff - coeff[0])<1e-8)) or len(coeff) == 1: if coeff[0] == 0: return 0 else: From 12a25ee04fc4b5c3ea9e9b639c9cfc8b81fe58d5 Mon Sep 17 00:00:00 2001 From: AbhineetGupta Date: Fri, 13 Oct 2023 10:07:00 -0600 Subject: [PATCH 108/124] Fix viterna extension bug where polar data is repeated if alpha_low = -alpha_high --- pyFAST/airfoils/Polar.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyFAST/airfoils/Polar.py b/pyFAST/airfoils/Polar.py index e098e58..ce29a38 100644 --- a/pyFAST/airfoils/Polar.py +++ b/pyFAST/airfoils/Polar.py @@ -539,6 +539,8 @@ def extrapolate(self, cdmax, AR=None, cdmin=0.001, nalpha=15): # -90 <-> -alpha_high alpha5 = np.linspace(-np.pi / 2, alpha5max, nalpha) alpha5 = alpha5[1:] + if alpha_low == -alpha_high: + alpha5 = alpha5[:-1] cl5, cd5 = self.__Viterna(-alpha5, -cl_adj) # -180+alpha_high <-> -90 From f23947ea031b41dc5592ab596e2bca62248158d1 Mon Sep 17 00:00:00 2001 From: Regis Thedin Date: Mon, 16 Oct 2023 09:49:19 -0600 Subject: [PATCH 109/124] FF: Add partition option to slurm methods; add warnings --- pyFAST/fastfarm/FASTFarmCaseCreation.py | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/pyFAST/fastfarm/FASTFarmCaseCreation.py b/pyFAST/fastfarm/FASTFarmCaseCreation.py index 3f7d52f..54f9c81 100644 --- a/pyFAST/fastfarm/FASTFarmCaseCreation.py +++ b/pyFAST/fastfarm/FASTFarmCaseCreation.py @@ -1191,6 +1191,7 @@ def _setRotorParameters(self): def TS_low_setup(self, writeFiles=True, runOnce=False): # Loops on all conditions/seeds creating Low-res TurbSim box (following python-toolbox/pyFAST/fastfarm/examples/Ex1_TurbSimInputSetup.py) + boxType='lowres' for cond in range(self.nConditions): for seed in range(self.nSeeds): @@ -1276,7 +1277,7 @@ def TS_low_slurm_prepare(self, slurmfilepath): print(f'--- WARNING: The memory-per-cpu on the low-res boxes SLURM script might be too low given {self.nSeeds} seeds.') - def TS_low_slurm_submit(self, qos='normal', A=None, t=None): + def TS_low_slurm_submit(self, qos='normal', A=None, t=None, p=None): # --------------------------------- # ----- Run turbSim Low boxes ----- # --------------------------------- @@ -1286,6 +1287,8 @@ def TS_low_slurm_submit(self, qos='normal', A=None, t=None): options += f'-A {A} ' if t is not None: options += f'-t {t} ' + if p is not None: + options += f'-p {p} ' sub_command = f"sbatch {options}{self.slurmfilename_low}" print(f'Calling: {sub_command}') @@ -1451,6 +1454,10 @@ def TS_high_setup(self, writeFiles=True): #todo: Check if the low-res boxes were created successfully + if self.ds_high_les != self.cmax: + print(f'WARNING: The requested ds_high = {self.ds_high_les} m is not actually used. The TurbSim ') + print(f' boxes use the default max chord value ({self.cmax} m here) for the spatial resolution.') + # Create symbolic links for the low-res boxes self.TS_low_createSymlinks() @@ -1546,7 +1553,7 @@ def TS_high_slurm_prepare(self, slurmfilepath): - def TS_high_slurm_submit(self, qos='normal', A=None, t=None): + def TS_high_slurm_submit(self, qos='normal', A=None, t=None, p=None): # ---------------------------------- # ----- Run turbSim High boxes ----- # ---------------------------------- @@ -1556,6 +1563,8 @@ def TS_high_slurm_submit(self, qos='normal', A=None, t=None): options += f'-A {A} ' if t is not None: options += f'-t {t} ' + if p is not None: + otions += f'-p {p} ' sub_command = f"sbatch {options}{self.slurmfilename_high}" print(f'Calling: {sub_command}') @@ -1861,8 +1870,6 @@ def _FF_setup_TS(self): highbts = TurbSimFile(os.path.join(seedPath,'TurbSim', f'HighT1.bts')) # Get dictionary with all the D{X,Y,Z,t}, L{X,Y,Z,t}, N{X,Y,Z,t}, {X,Y,Z}0 - dt_low_desired = self.Cmeander*D_/(10*Vhub_) # will be made multiple of dT_High inside _getBoxesParamsForFF - #d = self._getBoxesParamsForFF(lowbts, highbts, dt_low_desired, D_, HubHt_, xWT, yt) d = self._getBoxesParamsForFF(lowbts, highbts, self.dt_low_les, D_, HubHt_, xWT, yt) # Write the file @@ -1888,7 +1895,6 @@ def _FF_setup_TS(self): self.dr = round(self.D/10) ff_file['dr'] = self.dr ff_file['NumRadii'] = int(np.ceil(3*D_/(2*self.dr) + 1)) - #ff_file['NumPlanes'] = int(np.ceil( 20*D_/(dt_low_desired*Vhub_*(1-1/6)) ) ) ff_file['NumPlanes'] = int(np.ceil( 20*D_/(self.dt_low_les*Vhub_*(1-1/6)) ) ) # Vizualization outputs @@ -2090,7 +2096,7 @@ def FF_slurm_prepare(self, slurmfilepath): - def FF_slurm_submit(self, qos='normal', A=None, t=None, delay=4): + def FF_slurm_submit(self, qos='normal', A=None, t=None, p=None, delay=4): # ---------------------------------- # ---------- Run FAST.Farm --------- @@ -2110,6 +2116,8 @@ def FF_slurm_submit(self, qos='normal', A=None, t=None, delay=4): options += f'-A {A} ' if t is not None: options += f'-t {t} ' + if p is not None: + otions += f'-p {p} ' sub_command = f"sbatch {options}{fname}" print(f'Calling: {sub_command}') From 6462749de96f60158b1663c4fda82756e669b1f7 Mon Sep 17 00:00:00 2001 From: pibo Date: Tue, 17 Oct 2023 12:58:01 -0600 Subject: [PATCH 110/124] BD now works even when BD not defined in mid chord --- pyFAST/converters/beamDynToHawc2.py | 53 +++++++++++++++++++++++++++-- 1 file changed, 50 insertions(+), 3 deletions(-) diff --git a/pyFAST/converters/beamDynToHawc2.py b/pyFAST/converters/beamDynToHawc2.py index 6575113..ad19ac5 100644 --- a/pyFAST/converters/beamDynToHawc2.py +++ b/pyFAST/converters/beamDynToHawc2.py @@ -9,6 +9,7 @@ from pyFAST.input_output.csv_file import CSVFile from pyFAST.input_output.fast_input_file import FASTInputFile from pyFAST.converters.beam import ComputeStiffnessProps, TransformCrossSectionMatrix +from pyFAST.input_output.fast_input_deck import FASTInputDeck from .beam import * from .hawc2 import dfstructure2stfile @@ -58,7 +59,7 @@ def arc_length(points): # --------------------------------------------------------------------------------} # ---beamDynToHawc2 # --------------------------------------------------------------------------------{ -def beamDynToHawc2(BD_mainfile, BD_bladefile, H2_htcfile=None, H2_stfile=None, bodyname=None, A=None, E=None, G=None, theta_p_in=None, FPM=False, verbose=False): +def beamDynToHawc2(BD_mainfile, BD_bladefile, H2_htcfile=None, H2_stfile=None, bodyname=None, A=None, E=None, G=None, fstIn=None, theta_p_in=None, FPM=False, verbose=False): """ FPM: fully populated matrix, if True, use the FPM format of hawc2 @@ -71,7 +72,7 @@ def beamDynToHawc2(BD_mainfile, BD_bladefile, H2_htcfile=None, H2_stfile=None, b bdLine = BD_mainfile.toDataFrame() prop = BD_bladefile.toDataFrame() - # --- Extract relevant info + # Define BeamDyn reference axis r_bar = prop['Span'].values kp_x_raw = bdLine['kp_xr_[m]'].values @@ -87,7 +88,7 @@ def beamDynToHawc2(BD_mainfile, BD_bladefile, H2_htcfile=None, H2_stfile=None, b kp_z = np.interp(r_bar, s_coord, kp_z_raw) twist = np.interp(r_bar, s_coord, twist_raw) - + # Create K and M matrices K = np.zeros((6,6),dtype='object') M = np.zeros((6,6),dtype='object') for i in np.arange(6): @@ -95,6 +96,52 @@ def beamDynToHawc2(BD_mainfile, BD_bladefile, H2_htcfile=None, H2_stfile=None, b K[i,j]=prop['K{}{}'.format(i+1,j+1)].values M[i,j]=prop['M{}{}'.format(i+1,j+1)].values + if fstIn != None: + fst = FASTInputDeck(fstIn, verbose=True) + Bld = fst.fst_vt['AeroDynBlade'] + AF = fst.fst_vt['af_data'] + BlSpn = Bld['BldAeroNodes'][:,0] + n_span = len(BlSpn) + ac_pos = np.zeros(n_span) + for iSpan in range(n_span): + ac_pos[iSpan] = fst.fst_vt['ac_data'][iSpan]['AirfoilRefPoint'][0] + # Define axis of aero centers + BlCrvAC = Bld['BldAeroNodes'][:,1] + BlSwpAC = Bld['BldAeroNodes'][:,2] + chord = Bld['BldAeroNodes'][:,5] + s_aero = BlSpn/BlSpn[-1] + ac_x = np.interp(r_bar, s_aero, BlCrvAC) + ac_y = np.interp(r_bar, s_aero, BlSwpAC) + ac = np.interp(r_bar, s_aero, ac_pos) + + # Get x and y coordinates of c2 axis + ac2c2 = (0.5 - ac) * chord + c2_x = ac_x + ac2c2 * np.sin(twist) + c2_y = ac_y + ac2c2 * np.cos(twist) + # # Get offsets from BD axis to c2 axis along the twisted frame of reference + c2BD_y = (c2_y - kp_y) / np.cos(twist) + c2BD_x = np.zeros_like(c2BD_y) # no x translation, we should be translating along the twisted chord + + # Translate matrices to from BD to c2 axis (translate along chord, x and twist are 0) + transform = TransformCrossSectionMatrix() + for iSpan in np.arange(len(K[0,0])): + K_bd_temp = np.zeros((6,6)) + M_bd_temp = np.zeros((6,6)) + for i in np.arange(6): + for j in np.arange(6): + K_bd_temp[i,j] = K[i,j][iSpan] + M_bd_temp[i,j] = M[i,j][iSpan] + K_c2_temp = transform.CrossSectionRotoTranslationMatrix(K_bd_temp, 0., c2BD_y[iSpan], 0.) + M_c2_temp = transform.CrossSectionRotoTranslationMatrix(M_bd_temp, 0., c2BD_y[iSpan], 0.) + for i in np.arange(6): + for j in np.arange(6): + K[i,j][iSpan]=K_c2_temp[i,j] + M[i,j][iSpan]=M_c2_temp[i,j] + + # Update BeamDyn axis to c2 axis + kp_x = c2_x + kp_y = c2_y + # Map 6x6 data to "beam" data # NOTE: theta_* are in [rad] EA, EIx, EIy, kxsGA, kysGA, GKt, x_C, y_C, x_S, y_S, theta_p, theta_s = K66toPropsDecoupled(K, theta_p_in) From 38c04ee08759239d933598a81aa4407b291019dc Mon Sep 17 00:00:00 2001 From: pibo Date: Tue, 17 Oct 2023 13:00:30 -0600 Subject: [PATCH 111/124] fix comment --- pyFAST/converters/beamDynToHawc2.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyFAST/converters/beamDynToHawc2.py b/pyFAST/converters/beamDynToHawc2.py index ad19ac5..6e16204 100644 --- a/pyFAST/converters/beamDynToHawc2.py +++ b/pyFAST/converters/beamDynToHawc2.py @@ -118,7 +118,7 @@ def beamDynToHawc2(BD_mainfile, BD_bladefile, H2_htcfile=None, H2_stfile=None, b ac2c2 = (0.5 - ac) * chord c2_x = ac_x + ac2c2 * np.sin(twist) c2_y = ac_y + ac2c2 * np.cos(twist) - # # Get offsets from BD axis to c2 axis along the twisted frame of reference + # Get offsets from BD axis to c2 axis along the twisted frame of reference c2BD_y = (c2_y - kp_y) / np.cos(twist) c2BD_x = np.zeros_like(c2BD_y) # no x translation, we should be translating along the twisted chord From 00446d5a6d885927ae8e987c10c275e806bebe3a Mon Sep 17 00:00:00 2001 From: pibo Date: Tue, 17 Oct 2023 13:39:25 -0600 Subject: [PATCH 112/124] rename variables --- pyFAST/converters/beamDynToHawc2.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pyFAST/converters/beamDynToHawc2.py b/pyFAST/converters/beamDynToHawc2.py index 6e16204..2de28da 100644 --- a/pyFAST/converters/beamDynToHawc2.py +++ b/pyFAST/converters/beamDynToHawc2.py @@ -102,9 +102,9 @@ def beamDynToHawc2(BD_mainfile, BD_bladefile, H2_htcfile=None, H2_stfile=None, b AF = fst.fst_vt['af_data'] BlSpn = Bld['BldAeroNodes'][:,0] n_span = len(BlSpn) - ac_pos = np.zeros(n_span) + le2ac_raw = np.zeros(n_span) for iSpan in range(n_span): - ac_pos[iSpan] = fst.fst_vt['ac_data'][iSpan]['AirfoilRefPoint'][0] + le2ac_raw[iSpan] = fst.fst_vt['ac_data'][iSpan]['AirfoilRefPoint'][0] # Define axis of aero centers BlCrvAC = Bld['BldAeroNodes'][:,1] BlSwpAC = Bld['BldAeroNodes'][:,2] @@ -112,14 +112,14 @@ def beamDynToHawc2(BD_mainfile, BD_bladefile, H2_htcfile=None, H2_stfile=None, b s_aero = BlSpn/BlSpn[-1] ac_x = np.interp(r_bar, s_aero, BlCrvAC) ac_y = np.interp(r_bar, s_aero, BlSwpAC) - ac = np.interp(r_bar, s_aero, ac_pos) + le2ac = np.interp(r_bar, s_aero, le2ac_raw) # Get x and y coordinates of c2 axis - ac2c2 = (0.5 - ac) * chord + ac2c2 = (0.5 - le2ac) * chord c2_x = ac_x + ac2c2 * np.sin(twist) c2_y = ac_y + ac2c2 * np.cos(twist) # Get offsets from BD axis to c2 axis along the twisted frame of reference - c2BD_y = (c2_y - kp_y) / np.cos(twist) + c2BD_y = np.sqrt( (c2_y - kp_y)**2 + (c2_x - kp_x)**2 ) c2BD_x = np.zeros_like(c2BD_y) # no x translation, we should be translating along the twisted chord # Translate matrices to from BD to c2 axis (translate along chord, x and twist are 0) From 83c84c50736bbab11064f3b76a383bda5a5b586d Mon Sep 17 00:00:00 2001 From: pibo Date: Tue, 17 Oct 2023 13:40:24 -0600 Subject: [PATCH 113/124] add comment --- pyFAST/converters/beamDynToHawc2.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyFAST/converters/beamDynToHawc2.py b/pyFAST/converters/beamDynToHawc2.py index 2de28da..0343dce 100644 --- a/pyFAST/converters/beamDynToHawc2.py +++ b/pyFAST/converters/beamDynToHawc2.py @@ -112,7 +112,7 @@ def beamDynToHawc2(BD_mainfile, BD_bladefile, H2_htcfile=None, H2_stfile=None, b s_aero = BlSpn/BlSpn[-1] ac_x = np.interp(r_bar, s_aero, BlCrvAC) ac_y = np.interp(r_bar, s_aero, BlSwpAC) - le2ac = np.interp(r_bar, s_aero, le2ac_raw) + le2ac = np.interp(r_bar, s_aero, le2ac_raw) # Leading edge to aerodynamic center (in chord) # Get x and y coordinates of c2 axis ac2c2 = (0.5 - le2ac) * chord From 18de6e161418fffcc0463129417fce58dcbb22b6 Mon Sep 17 00:00:00 2001 From: pibo Date: Tue, 17 Oct 2023 13:41:05 -0600 Subject: [PATCH 114/124] fix aother tiny comment --- pyFAST/converters/beamDynToHawc2.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyFAST/converters/beamDynToHawc2.py b/pyFAST/converters/beamDynToHawc2.py index 0343dce..d596059 100644 --- a/pyFAST/converters/beamDynToHawc2.py +++ b/pyFAST/converters/beamDynToHawc2.py @@ -122,7 +122,7 @@ def beamDynToHawc2(BD_mainfile, BD_bladefile, H2_htcfile=None, H2_stfile=None, b c2BD_y = np.sqrt( (c2_y - kp_y)**2 + (c2_x - kp_x)**2 ) c2BD_x = np.zeros_like(c2BD_y) # no x translation, we should be translating along the twisted chord - # Translate matrices to from BD to c2 axis (translate along chord, x and twist are 0) + # Translate matrices from BD to c2 axis (translate along chord, x and twist are 0) transform = TransformCrossSectionMatrix() for iSpan in np.arange(len(K[0,0])): K_bd_temp = np.zeros((6,6)) From a01f81967a7f0d3e786184306b28d501d9da7fc6 Mon Sep 17 00:00:00 2001 From: Emmanuel Branlard Date: Fri, 20 Oct 2023 13:12:15 -0600 Subject: [PATCH 115/124] IO: misc updates from welib --- pyFAST/input_output/converters.py | 46 ++++++++++++++++++---- pyFAST/input_output/excel_file.py | 10 +++-- pyFAST/input_output/fast_output_file.py | 23 ++++++++--- pyFAST/input_output/file_formats.py | 2 + pyFAST/input_output/flex_out_file.py | 2 +- pyFAST/input_output/matlabmat_file.py | 1 - pyFAST/input_output/pickle_file.py | 6 +++ pyFAST/input_output/wetb/hawc2/htc_file.py | 1 - 8 files changed, 70 insertions(+), 21 deletions(-) diff --git a/pyFAST/input_output/converters.py b/pyFAST/input_output/converters.py index 990d1e4..99dc276 100644 --- a/pyFAST/input_output/converters.py +++ b/pyFAST/input_output/converters.py @@ -4,7 +4,44 @@ # -------------------------------------------------------------------------------- # --- Writing pandas DataFrame to different formats # -------------------------------------------------------------------------------- -# The +def writeDataFrameToFormat(df, filename, fformat): + """ + Write a dataframe to disk based on user-specified fileformat + - df: pandas dataframe + - filename: filename + - fformat: fileformat in: ['csv', 'outb', 'parquet'] + """ + + if fformat=='outb': + dataFrameToOUTB(df, filename) + elif fformat=='parquet': + dataFrameToParquet(df, filename) + elif fformat=='csv': + dataFrameToCSV(df, filename, sep=',', index=False) + else: + raise Exception('File format not supported for dataframe export `{}`'.format(fformat)) + +def writeDataFrameAutoFormat(df, filename, fformat=None): + """ + Write a dataframe to disk based on extension + - df: pandas dataframe + - filename: filename + """ + if fformat is not None: + raise Exception() + base, ext = os.path.splitext(filename) + ext = ext.lower() + if ext in ['.outb']: + fformat = 'outb' + elif ext in ['.parquet']: + fformat = 'parquet' + elif ext in ['.csv']: + fformat = 'csv' + else: + print('[WARN] defaulting to csv, extension unknown: `{}`'.format(ext)) + fformat = 'csv' + + writeDataFrameToFormat(df, filename, fformat) def writeFileDataFrames(fileObject, writer, extension='.conv', filename=None, **kwargs): """ @@ -35,7 +72,6 @@ def writeFileDataFrames(fileObject, writer, extension='.conv', filename=None, ** else: writeDataFrame(df=dfs, writer=writer, filename=filename, **kwargs) - def writeDataFrame(df, writer, filename, **kwargs): """ Write a dataframe to disk based on a "writer" function. @@ -47,16 +83,10 @@ def writeDataFrame(df, writer, filename, **kwargs): # --- Low level writers def dataFrameToCSV(df, filename, sep=',', index=False, **kwargs): - base, ext = os.path.splitext(filename) - if len(ext)==0: - filename = base='.csv' df.to_csv(filename, sep=sep, index=index, **kwargs) def dataFrameToOUTB(df, filename, **kwargs): from .fast_output_file import writeDataFrame as writeDataFrameToOUTB - base, ext = os.path.splitext(filename) - if len(ext)==0: - filename = base='.outb' writeDataFrameToOUTB(df, filename, binary=True) def dataFrameToParquet(df, filename, **kwargs): diff --git a/pyFAST/input_output/excel_file.py b/pyFAST/input_output/excel_file.py index 0b9c9ab..8ea2728 100644 --- a/pyFAST/input_output/excel_file.py +++ b/pyFAST/input_output/excel_file.py @@ -75,8 +75,10 @@ def __repr__(self): def _toDataFrame(self): - #cols=['Alpha_[deg]','Cl_[-]','Cd_[-]','Cm_[-]'] - #dfs[name] = pd.DataFrame(data=..., columns=cols) - #df=pd.DataFrame(data=,columns=) - return self.data + if len(self.data)==1: + # Return a single dataframe + return self.data[list(self.data.keys())[0]] + else: + # Return dictionary + return self.data diff --git a/pyFAST/input_output/fast_output_file.py b/pyFAST/input_output/fast_output_file.py index f83ad6d..df03aac 100644 --- a/pyFAST/input_output/fast_output_file.py +++ b/pyFAST/input_output/fast_output_file.py @@ -235,23 +235,30 @@ def isBinary(filename): -def load_ascii_output(filename, method='numpy'): +def load_ascii_output(filename, method='numpy', encoding='ascii'): if method in ['forLoop','pandas']: from .file import numberOfLines nLines = numberOfLines(filename, method=2) - with open(filename) as f: + with open(filename, encoding=encoding, errors='ignore') as f: info = {} info['name'] = os.path.splitext(os.path.basename(filename))[0] # Header is whatever is before the keyword `time` - in_header = True header = [] - while in_header: + maxHeaderLines=35 + headerRead = False + for i in range(maxHeaderLines): l = f.readline() if not l: raise Exception('Error finding the end of FAST out file header. Keyword Time missing.') + # Check for utf-16 + if l[:3] == '\x00 \x00': + f.close() + encoding='' + print('[WARN] Attempt to re-read the file with encoding utf-16') + return load_ascii_output(filename=filename, method=method, encoding='utf-16') first_word = (l+' dummy').lower().split()[0] in_header= (first_word != 'time') and (first_word != 'alpha') if in_header: @@ -260,6 +267,10 @@ def load_ascii_output(filename, method='numpy'): info['description'] = header info['attribute_names'] = l.split() info['attribute_units'] = [unit[1:-1] for unit in f.readline().split()] + headerRead=True + break + if not headerRead: + raise WrongFormatError('Could not find the keyword "Time" or "Alpha" in the first {} lines of the file'.format(maxHeaderLines)) nHeader = len(header)+1 nCols = len(info['attribute_names']) @@ -285,13 +296,13 @@ def load_ascii_output(filename, method='numpy'): data = np.zeros((nRows, nCols)) for i in range(nRows): l = f.readline().strip() - sp = np.array(l.split()).astype(np.float) + sp = np.array(l.split()).astype(float) data[i,:] = sp[:nCols] elif method == 'listCompr': # --- Method 4 - List comprehension # Data, up to end of file or empty line (potential comment line at the end) - data = np.array([l.strip().split() for l in takewhile(lambda x: len(x.strip())>0, f.readlines())]).astype(np.float) + data = np.array([l.strip().split() for l in takewhile(lambda x: len(x.strip())>0, f.readlines())]).astype(float) else: raise NotImplementedError() diff --git a/pyFAST/input_output/file_formats.py b/pyFAST/input_output/file_formats.py index daa5400..595d545 100644 --- a/pyFAST/input_output/file_formats.py +++ b/pyFAST/input_output/file_formats.py @@ -11,6 +11,8 @@ def isRightFormat(fileformat, filename, **kwargs): except WrongFormatError: return False,None except: + # We Raise the Exception. + # It's the responsability of all the file classes to Return a WrongFormatError raise class FileFormat(): diff --git a/pyFAST/input_output/flex_out_file.py b/pyFAST/input_output/flex_out_file.py index ea8eb38..86fc617 100644 --- a/pyFAST/input_output/flex_out_file.py +++ b/pyFAST/input_output/flex_out_file.py @@ -134,7 +134,7 @@ def read_flex_res(filename, dtype=np.float32): def read_flex_sensor(sensor_file): - with open(sensor_file, encoding="utf-8") as fid: + with open(sensor_file, 'r') as fid: sensor_info_lines = fid.readlines()[2:] sensor_info = [] d=dict({ 'ID':[],'Gain':[],'Offset':[],'Unit':[],'Name':[],'Description':[]}); diff --git a/pyFAST/input_output/matlabmat_file.py b/pyFAST/input_output/matlabmat_file.py index e1aff75..2ed922d 100644 --- a/pyFAST/input_output/matlabmat_file.py +++ b/pyFAST/input_output/matlabmat_file.py @@ -63,7 +63,6 @@ def read(self, filename=None, **kwargs): raise EmptyFileError('File is empty:',self.filename) mfile = scipy.io.loadmat(self.filename) - import pdb; pdb.set_trace() def write(self, filename=None): """ Rewrite object to file, or write object to `filename` if provided """ diff --git a/pyFAST/input_output/pickle_file.py b/pyFAST/input_output/pickle_file.py index 65cbf86..97bd289 100644 --- a/pyFAST/input_output/pickle_file.py +++ b/pyFAST/input_output/pickle_file.py @@ -65,6 +65,12 @@ def _setData(self, data): else: self['data'] = data + def addDict(self, data): + self._setData(data) + + def additem(self, key, data): + self[key]=data + def read(self, filename=None, **kwargs): """ Reads the file self.filename, or `filename` if provided """ # --- Standard tests and exceptions (generic code) diff --git a/pyFAST/input_output/wetb/hawc2/htc_file.py b/pyFAST/input_output/wetb/hawc2/htc_file.py index 7648ac7..6b6bf3a 100644 --- a/pyFAST/input_output/wetb/hawc2/htc_file.py +++ b/pyFAST/input_output/wetb/hawc2/htc_file.py @@ -591,7 +591,6 @@ def unix_path(self, filename): if "__main__" == __name__: f = HTCFile(r"C:/Work/BAR-Local/Hawc2ToBeamDyn/sim.htc", ".") print(f.input_files()) - import pdb; pdb.set_trace() # f.save(r"C:\mmpe\HAWC2\models\DTU10MWRef6.0\htc\DTU_10MW_RWT_power_curve.htc") # # f = HTCFile(r"C:\mmpe\HAWC2\models\DTU10MWRef6.0\htc\DTU_10MW_RWT.htc", "../") From c84ae1f1326218f6100e138c7dd102917256c1c4 Mon Sep 17 00:00:00 2001 From: Emmanuel Branlard Date: Fri, 20 Oct 2023 13:27:39 -0600 Subject: [PATCH 116/124] Escaping some backslashes --- pyFAST/case_generation/examples/Example_ExcelFile.py | 6 ++++-- pyFAST/fastfarm/TurbSimCaseCreation.py | 2 +- pyFAST/input_output/__init__.py | 2 +- pyFAST/input_output/bladed_out_file.py | 2 +- 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/pyFAST/case_generation/examples/Example_ExcelFile.py b/pyFAST/case_generation/examples/Example_ExcelFile.py index 1971a45..9da2a73 100644 --- a/pyFAST/case_generation/examples/Example_ExcelFile.py +++ b/pyFAST/case_generation/examples/Example_ExcelFile.py @@ -23,9 +23,11 @@ def main(run=True): # --- Reading Excel file, converting it to a list of dictionaries, and generate input files dfs = io.excel_file.ExcelFile(parametricFile).toDataFrame() - df = dfs[list(dfs.keys())[0]] + try: + df = dfs[list(dfs.keys())[0]] + except: + df = dfs PARAMS = df.to_dict('records') - print(df) fastFiles=case_gen.templateReplace(PARAMS, ref_dir, outputDir=work_dir, removeRefSubFiles=True, removeAllowed=False, main_file=main_file) if run: diff --git a/pyFAST/fastfarm/TurbSimCaseCreation.py b/pyFAST/fastfarm/TurbSimCaseCreation.py index 3b6539b..99644cf 100644 --- a/pyFAST/fastfarm/TurbSimCaseCreation.py +++ b/pyFAST/fastfarm/TurbSimCaseCreation.py @@ -343,7 +343,7 @@ def WriteTSFile(fileIn, fileOut, params, NewFile=True, tpath=None, tmax=50, turb f.write(f'"0.0"\tCohExp\t\t- Coherence exponent for general model [-] (or "default")\n') f.write(f'\n') f.write(f'--------Coherent Turbulence Scaling Parameters-------------------\n') - f.write(f'".\EventData"\tCTEventPath\t\t- Name of the path where event data files are located\n') + f.write(f'".\\EventData"\tCTEventPath\t\t- Name of the path where event data files are located\n') f.write(f'"random"\tCTEventFile\t\t- Type of event files ("LES", "DNS", or "RANDOM")\n') f.write(f'true\tRandomize\t\t- Randomize the disturbance scale and locations? (true/false)\n') f.write(f'1\tDistScl\t\t- Disturbance scale [-] (ratio of event dataset height to rotor disk). (Ignored when Randomize = true.)\n') diff --git a/pyFAST/input_output/__init__.py b/pyFAST/input_output/__init__.py index f5bde70..b86f3a6 100644 --- a/pyFAST/input_output/__init__.py +++ b/pyFAST/input_output/__init__.py @@ -273,7 +273,7 @@ def detectFormat(filename, **kwargs): extMatch = True else: # Try patterns if present - extPatterns = [ef.replace('.','\.').replace('$','\$').replace('*','[.]*') for ef in myformat.extensions if '*' in ef] + extPatterns = [ef.replace('.',r'\.').replace('$',r'\$').replace('*','[.]*') for ef in myformat.extensions if '*' in ef] if len(extPatterns)>0: extPatMatch = [re.match(pat, ext) is not None for pat in extPatterns] extMatch = any(extPatMatch) diff --git a/pyFAST/input_output/bladed_out_file.py b/pyFAST/input_output/bladed_out_file.py index f6ac45c..d1f0dcd 100644 --- a/pyFAST/input_output/bladed_out_file.py +++ b/pyFAST/input_output/bladed_out_file.py @@ -77,7 +77,7 @@ def read_bladed_sensor_file(sensorfile): t_line = re.search(r'(?<=AXITICK).+?(?=AXISLAB)', combined_string, flags=re.DOTALL) t_line=t_line.group(0) # Replace consecutive whitespace characters with a single space - t_line = re.sub('\s+', ' ', t_line) + t_line = re.sub(r'\s+', ' ', t_line) except: pass From b44e1fc8f696d6765619cc61a70471cec08a1289 Mon Sep 17 00:00:00 2001 From: Emmanuel Branlard Date: Fri, 20 Oct 2023 13:43:50 -0600 Subject: [PATCH 117/124] IO: Excel, attempt to fix broken test --- pyFAST/case_generation/examples/Example_ExcelFile.py | 4 ++-- pyFAST/input_output/excel_file.py | 7 ++----- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/pyFAST/case_generation/examples/Example_ExcelFile.py b/pyFAST/case_generation/examples/Example_ExcelFile.py index 9da2a73..daca408 100644 --- a/pyFAST/case_generation/examples/Example_ExcelFile.py +++ b/pyFAST/case_generation/examples/Example_ExcelFile.py @@ -23,9 +23,9 @@ def main(run=True): # --- Reading Excel file, converting it to a list of dictionaries, and generate input files dfs = io.excel_file.ExcelFile(parametricFile).toDataFrame() - try: + if isinstance(dfs, dict): df = dfs[list(dfs.keys())[0]] - except: + else: df = dfs PARAMS = df.to_dict('records') fastFiles=case_gen.templateReplace(PARAMS, ref_dir, outputDir=work_dir, removeRefSubFiles=True, removeAllowed=False, main_file=main_file) diff --git a/pyFAST/input_output/excel_file.py b/pyFAST/input_output/excel_file.py index 8ea2728..d61653e 100644 --- a/pyFAST/input_output/excel_file.py +++ b/pyFAST/input_output/excel_file.py @@ -18,10 +18,7 @@ def formatName(): def _read(self): self.data=dict() # Reading all sheets - try: - xls = pd.ExcelFile(self.filename, engine='openpyxl') - except: - xls = pd.ExcelFile(self.filename) + xls = pd.ExcelFile(self.filename, engine='openpyxl') dfs = {} for sheet_name in xls.sheet_names: # Reading sheet @@ -70,7 +67,7 @@ def _write(self): writer.save() def __repr__(self): - s ='Class XXXX (attributes: data)\n' + s ='Class ExcelFile (attributes: data)\n' return s From 4785879aaad49a9692faad2b3dd6ec4a0c53a453 Mon Sep 17 00:00:00 2001 From: Emmanuel Branlard Date: Fri, 20 Oct 2023 13:46:12 -0600 Subject: [PATCH 118/124] Tools: misc bug fixes (fatigue, radial postpro) --- pyFAST/fastfarm/fastfarm.py | 22 ++++++------- pyFAST/postpro/postpro.py | 16 +++++---- pyFAST/tools/fatigue.py | 66 ++++++++++++++++++++++++++++++++----- 3 files changed, 77 insertions(+), 27 deletions(-) diff --git a/pyFAST/fastfarm/fastfarm.py b/pyFAST/fastfarm/fastfarm.py index 8d60ceb..926e438 100644 --- a/pyFAST/fastfarm/fastfarm.py +++ b/pyFAST/fastfarm/fastfarm.py @@ -604,6 +604,7 @@ def spanwisePostProFF(fastfarm_input,avgMethod='constantwindow',avgParam=30,D=1, vr=None vD=None D=0 + main = None else: main=FASTInputFile(fastfarm_input) iOut = main['OutRadii'] @@ -629,20 +630,19 @@ def spanwisePostProFF(fastfarm_input,avgMethod='constantwindow',avgParam=30,D=1, ColsInfo, nrMax = spanwiseColFastFarm(df.columns.values, nWT=nWT, nD=nD) dfRad = fastlib.extract_spanwise_data(ColsInfo, nrMax, df=None, ts=dfAvg.iloc[0]) #dfRad = fastlib.insert_radial_columns(dfRad, vr) - if vr is None: - dfRad.insert(0, 'i_[#]', np.arange(nrMax)+1) - else: - dfRad.insert(0, 'r_[m]', vr[:nrMax]) - dfRad['i/n_[-]']=np.arange(nrMax)/nrMax + if dfRad is not None: + dfRad.insert(0, 'i_[#]', np.arange(nrMax)+1) # For all, to ease comparison + if vr is not None: + dfRad.insert(0, 'r_[m]', vr[:nrMax]) # give priority to r_[m] when available + dfRad['i/n_[-]']=np.arange(nrMax)/nrMax # --- Extract downstream data ColsInfo, nDMax = diameterwiseColFastFarm(df.columns.values, nWT=nWT) dfDiam = fastlib.extract_spanwise_data(ColsInfo, nDMax, df=None, ts=dfAvg.iloc[0]) - #dfDiam = fastlib.insert_radial_columns(dfDiam) - if vD is None: - dfDiam.insert(0, 'i_[#]', np.arange(nDMax)+1) - else: - dfDiam.insert(0, 'x_[m]', vD[:nDMax]) - dfDiam['i/n_[-]'] = np.arange(nDMax)/nDMax + if dfDiam is not None: + dfDiam.insert(0, 'i_[#]', np.arange(nDMax)+1) # For all, to ease comparison + if vD is not None: + dfDiam.insert(0, 'x_[m]', vD[:nDMax]) + dfDiam['i/n_[-]'] = np.arange(nDMax)/nDMax return dfRad, dfRadialTime, dfDiam diff --git a/pyFAST/postpro/postpro.py b/pyFAST/postpro/postpro.py index f398380..0375b28 100644 --- a/pyFAST/postpro/postpro.py +++ b/pyFAST/postpro/postpro.py @@ -958,7 +958,7 @@ def spanwisePostProRows(df, FST_In=None): Cols=df.columns.values if r_AD is not None: ColsInfoAD, nrMaxAD = spanwiseColAD(Cols) - if r_ED is not None: + if r_ED_bld is not None: ColsInfoED, nrMaxED = spanwiseColED(Cols) if r_BD is not None: ColsInfoBD, nrMaxBD = spanwiseColBD(Cols) @@ -971,9 +971,9 @@ def spanwisePostProRows(df, FST_In=None): M_AD = np.zeros((len(v), len(dfRad_AD), len(dfRad_AD.columns))) Col_AD=dfRad_AD.columns.values M_AD[i, :, : ] = dfRad_AD.values - if r_ED is not None and len(r_ED)>0: + if r_ED_bld is not None and len(r_ED_bld)>0: dfRad_ED = extract_spanwise_data(ColsInfoED, nrMaxED, df=None, ts=df.iloc[i]) - dfRad_ED = insert_spanwise_columns(dfRad_ED, r_ED, R=R, IR=IR_ED) + dfRad_ED = insert_spanwise_columns(dfRad_ED, r_ED_bld, R=R, IR=IR_ED) if i==0: M_ED = np.zeros((len(v), len(dfRad_ED), len(dfRad_ED.columns))) Col_ED=dfRad_ED.columns.values @@ -1144,8 +1144,11 @@ def spanwiseConcat(df): chanName = ColsInfoAD[ic]['name'] colName = ColsInfoAD[ic]['cols'][ir] #print('Channel {}: colName {}'.format(chanName, colName)) - if ir+1 in IdxAvailableForThisChannel: - data[ir*nt:(ir+1)*nt, ic+2] = df[colName].values + try: + if ir+1 in IdxAvailableForThisChannel: + data[ir*nt:(ir+1)*nt, ic+2] = df[colName].values + except: + pass #else: # raise Exception('Channel {}: Index missing {}'.format(chanName, ic+1)) columns = ['Time_[s]'] + ['i_[-]'] + [ColsInfoAD[i]['name'] for i in range(nChan)] @@ -1781,7 +1784,6 @@ def integrateMomentTS(r, F): if __name__ == '__main__': - import welib.weio as weio - df = weio.read('ad_driver_yaw.6.outb').toDataFrame() + df = FASTOutputFile('ad_driver_yaw.6.outb').toDataFrame() dfCat = spanwiseConcat(df) print(dfCat) diff --git a/pyFAST/tools/fatigue.py b/pyFAST/tools/fatigue.py index c1e414e..e88936d 100644 --- a/pyFAST/tools/fatigue.py +++ b/pyFAST/tools/fatigue.py @@ -37,6 +37,10 @@ __all__ += ['rainflow_astm', 'rainflow_windap','eq_load','eq_load_and_cycles','cycle_matrix','cycle_matrix2'] +class SignalConstantError(Exception): + pass + + def equivalent_load(time, signal, m=3, Teq=1, bins=100, method='rainflow_windap', meanBin=True, binStartAt0=False, outputMore=False, debug=False): @@ -75,16 +79,30 @@ def equivalent_load(time, signal, m=3, Teq=1, bins=100, method='rainflow_windap' signal = signal[b] time = time[b] - T = time[-1]-time[0] # time length of signal (s) + try: + if len(time)<=1: + raise Exception() + if type(time[0]) is np.datetime64: + T = T/np.timedelta64(1,'s') # or T.item().total_seconds() + else: + T = time[-1]-time[0] # time length of signal (s). Will fail for signal of length 1 + if T==0: + raise Exception() + + neq = T/Teq # number of equivalent periods, see Eq. (26) of [1] - neq = T/Teq # number of equivalent periods, see Eq. (26) of [1] + # --- Range (S) and counts (N) + N, S, bins = find_range_count(signal, bins=bins, method=method, meanBin=meanBin, binStartAt0=binStartAt0) - # --- Range (S) and counts (N) - N, S, bins = find_range_count(signal, bins=bins, method=method, meanBin=meanBin, binStartAt0=binStartAt0) + # --- get DEL + DELi = S**m * N / neq + Leq = DELi.sum() ** (1/m) # See e.g. eq. (30) of [1] - # --- get DEL - DELi = S**m * N / neq - Leq = DELi.sum() ** (1/m) # See e.g. eq. (30) of [1] + except: + if outputMore: + return np.nan, np.nan, np.nan, np.nan, np.nan + else: + return np.nan if debug: for i,(b,n,s,DEL) in enumerate(zip(bins, N, S, DELi)): @@ -116,7 +134,11 @@ def find_range_count(signal, bins, method='rainflow_windap', meanBin=True, binSt if method in rainflow_func_dict.keys(): rainflow_func = rainflow_func_dict[method] - N, S, S_bin_edges, _, _ = cycle_matrix(signal, ampl_bins=bins, mean_bins=1, rainflow_func=rainflow_func, binStartAt0=binStartAt0) + try: + N, S, S_bin_edges, _, _ = cycle_matrix(signal, ampl_bins=bins, mean_bins=1, rainflow_func=rainflow_func, binStartAt0=binStartAt0) + except SignalConstantError: + return np.nan, np.nan, np.nan + S_bin_edges = S_bin_edges.flatten() N = N.flatten() S = S.flatten() @@ -217,7 +239,7 @@ def check_signal(signal): if signal.shape[1] > 1: raise TypeError('signal must have one column only, not: ' + str(signal.shape[1])) if np.min(signal) == np.max(signal): - raise TypeError("Signal contains no variation") + raise SignalConstantError("Signal is constant, cannot compute DLC and range") def rainflow_windap(signal, levels=255., thresshold=(255 / 50)): @@ -1166,8 +1188,34 @@ def test_equivalent_load_sines_sum(self): + def test_eqload_cornercases(self): + try: + import fatpack + hasFatpack=True + except: + hasFatpack=False + # Signal of length 1 + time=[0]; signal=[0] + Leq= equivalent_load(time, signal, m=3, Teq=1, bins=100, method='rainflow_windap') + np.testing.assert_equal(Leq, np.nan) + + # Datetime + time= [np.datetime64('2023-10-01'), np.datetime64('2023-10-02')] + signal= [0,1] + Leq= equivalent_load(time, signal, m=3, Teq=1, bins=100, method='rainflow_windap') + np.testing.assert_equal(Leq, np.nan) + + # Constant signal + time =[0,1] + signal =[1,1] + Leq= equivalent_load(time, signal, m=3, Teq=1, bins=100, method='rainflow_windap') + np.testing.assert_equal(Leq, np.nan) + if hasFatpack: + Leq= equivalent_load(time, signal, m=3, Teq=1, bins=100, method='fatpack') + np.testing.assert_equal(Leq, np.nan) if __name__ == '__main__': unittest.main() + From a7f68e6a29744fadd274b66d71d953b3061210b5 Mon Sep 17 00:00:00 2001 From: Emmanuel Branlard Date: Fri, 20 Oct 2023 13:46:32 -0600 Subject: [PATCH 119/124] Tools: adding more tools for harmo with welib --- pyFAST/tools/curve_fitting.py | 1446 +++++++++++++++++++++++++++++++++ pyFAST/tools/damping.py | 373 +++++++++ pyFAST/tools/spectral.py | 1442 ++++++++++++++++++++++++++++++++ pyFAST/tools/stats.py | 384 +++++++++ 4 files changed, 3645 insertions(+) create mode 100644 pyFAST/tools/curve_fitting.py create mode 100644 pyFAST/tools/damping.py create mode 100644 pyFAST/tools/spectral.py create mode 100644 pyFAST/tools/stats.py diff --git a/pyFAST/tools/curve_fitting.py b/pyFAST/tools/curve_fitting.py new file mode 100644 index 0000000..17eab9f --- /dev/null +++ b/pyFAST/tools/curve_fitting.py @@ -0,0 +1,1446 @@ +""" +Set of tools to fit a model to data. + +The quality of a fit is usually a strong function of the initial guess. +Because of this this package contains different kind of "helpers" and "wrapper" tools. + +FUNCTIONS +--------- + +This package can help fitting using: + 1) High level functions, e.g. fit_sinusoid +OR using the `model_fit` function that handles: + 2) User defined "eval" model, e.g. the user sets a string '{a}*x + {b}*x**2' + 3) Predefined models, e.g. Gaussian, logarithmic, weibull_pdf, etc. + 4) Predefined fitters, e.g. SinusoidFitter, DiscretePolynomialFitter, ContinuousPolynomialFitter + +1) The high level fitting functions available are: + - fit_sinusoid + - fit_polynomial + - fit_gaussian + +2) User defined model, using the `model_fit_function`: + - model_fit('eval: {a} + {b}*x**3 + {c}*x**5', x, y) + - model_fit('eval: {u_ref}*(x/{z_ref})**{alpha}', x, y, p0=(8,9,0.1), bounds=(0.001,100)) + User defined models, will require the user to provide an initial guess and potentially bounds + +3) Fitting using predefined models using the `model_fit` function : + - model_fit('predef: gaussian', x, y) + - model_fit('predef: gaussian-yoff', x, y) + - model_fit('predef: powerlaw_alpha', x, y, p0=(0.1), **fun_kwargs) + - model_fit('predef: powerlaw_u_alpha', x, y, **fun_kwargs) + - model_fit('predef: expdecay', x, y) + - model_fit('predef: weibull_pdf', x, y) + Predefined models have default values for bounds and guesses that can be overriden. + +4) Predefined fitters, wrapped with the `model_fit` function: + - model_fit('fitter: sinusoid', x, y) + - model_fit('fitter: polynomial_discrete', x, y, exponents=[0,2,4]) + - model_fit('fitter: polynomial_continuous', x, y, order=3) + Predefined fitters can handle bounds/initial guess better + +INPUTS: +-------- +All functions have the following inputs: + - x: array on the x-axis + - y: values on the y-axis (to be fitted against a model) +Additionally some functions have the following inputs: + - p0: initial values for parameters, either a string or a dict: + - string: the string is converted to a dictionary, assuming key value pairs + example: 'a=0, b=1.3' + - dictionary, then keys should corresponds to the parameters of the model + example: {'a':0, 'b':1.3} + - bounds: bounds for each parameters, either a string or a dictionary. + NOTE: pi and inf are available to set bounds + - if a string, the string is converted to a dictionary assuming key value pairs + example: 'a=(0,3), b=(-inf,pi)' + - if a dictionary, the keys should corresponds to the parameters of the model + example: {'a':(0,3), 'b':(-inf,pi)} + +OUTPUTS: +-------- +All functions returns the same outputs: + - y_fit : the fit to the y data + - pfit : the list of parameters used + - fitter: a `ModelFitter` object useful to manipulate the fit, in particular: + - fitter.model: dictionary with readable versions of the parameters, formula, + function to reevaluate the fit on a different x, etc. + - fitter.data: data used for the fit + - fitter.fit_data: perform another fit using different data + +MISC +---- +High-level fitters, predefined models or fitters can be added to this class. + +""" +import numpy as np +import scipy.optimize as so +import scipy.stats as stats +import string +import re +from collections import OrderedDict +from numpy import sqrt, pi, exp, cos, sin, log, inf, arctan # for user convenience +import six + +# --------------------------------------------------------------------------------} +# --- High level fitters +# --------------------------------------------------------------------------------{ +def fit_sinusoid(x,y,physical=False): + """ Fits a sinusoid to y with formula: + if physical is False: y_fit=A*sin(omega*x+phi)+B + if physical is True: y_fit=A*sin(2*pi(f+phi/360))+B """ + y_fit, pfit, fitter = model_fit('fitter: sinusoid', x, y, physical=physical) + return y_fit, pfit, fitter + +def fit_polynomial(x, y, order=None, exponents=None): + """ Fits a polynomial to y, either: + - full up to a given order: y_fit= {a_i} x^i , i=0..order + - or using a discrete set of exponents: y_fit= {a_i} x^e[i], i=0,..len(exponents) + OPTIONAL INPUTS: + - order: integer + Maximum order of polynomial, e.g. 2: for a x**0 + b x**1 + c x**2 + - exponents: array-like + Exponents to be used. e.g. [0,2,5] for a x**0 + b x**2 + c x**5 + """ + if order is not None: + y_fit, pfit, fitter = model_fit('fitter: polynomial_continuous', x, y, order=order) + else: + y_fit, pfit, fitter = model_fit('fitter: polynomial_discrete', x, y, exponents=exponents) + return y_fit, pfit, fitter + +def fit_gaussian(x, y, offset=False): + """ Fits a gaussin to y, with the following formula: + offset is True : '1/({sigma}*sqrt(2*pi)) * exp(-1/2 * ((x-{mu})/{sigma})**2)' + offset is False: '1/({sigma}*sqrt(2*pi)) * exp(-1/2 * ((x-{mu})/{sigma})**2) + {y0}' + """ + if offset: + return model_fit('predef: gaussian-yoff', x, y) + else: + return model_fit('predef: gaussian', x, y) + +# --------------------------------------------------------------------------------} +# --- Simple mid level fitter +# --------------------------------------------------------------------------------{ +def fit_polynomial_continuous(x, y, order): + """Fit a polynomial with a continuous set of exponents up to a given order + + Parameters + ---------- + x,y: see `model_fit` + order: integer + Maximum order of polynomial, e.g. 2: for a x**0 + b x**1 + c x**2 + + Returns + ------- + see `model_fit` + """ + pfit = np.polyfit(x,y,order) + y_fit = np.polyval(pfit,x) + + # coeffs_dict, e.g. {'a':xxx, 'b':xxx}, formula = 'a*x + b' + variables = string.ascii_lowercase[:order+1] + coeffs_dict = OrderedDict([(var,coeff) for i,(coeff,var) in enumerate(zip(pfit,variables))]) + formula = ' + '.join(['{}*x**{}'.format(var,order-i) for i,var in enumerate(variables)]) + formula = _clean_formula(formula) + + return y_fit,pfit,{'coeffs':coeffs_dict,'formula':formula,'fitted_function':lambda xx : np.polyval(pfit,xx)} + +def fit_polynomial_discrete(x, y, exponents): + """Fit a polynomial with a discrete set of exponents + + Parameters + ---------- + x,y: see `model_fit` + exponents: array-like + Exponents to be used. e.g. [0,2,5] for a x**0 + b x**2 + c x**5 + + Returns + ------- + see `model_fit` + """ + #exponents=-np.sort(-np.asarray(exponents)) + X_poly=np.array([]) + for i,e in enumerate(exponents): + if i==0: + X_poly = np.array([x**e]) + else: + X_poly = np.vstack((X_poly,x**e)) + try: + pfit = np.linalg.lstsq(X_poly.T, y, rcond=None)[0] + except: + pfit = np.linalg.lstsq(X_poly.T, y) + y_fit= np.dot(pfit, X_poly) + + variables = string.ascii_lowercase[:len(exponents)] + coeffs_dict = OrderedDict([(var,coeff) for i,(coeff,var) in enumerate(zip(pfit,variables))]) + formula = ' + '.join(['{}*x**{}'.format(var,e) for var,e in zip(variables,exponents)]) + formula = _clean_formula(formula) + + return y_fit,pfit,{'coeffs':coeffs_dict,'formula':formula} + + +def fit_powerlaw_u_alpha(x, y, z_ref=100, p0=(10,0.1)): + """ + p[0] : u_ref + p[1] : alpha + """ + pfit, _ = so.curve_fit(lambda x, *p : p[0] * (x / z_ref) ** p[1], x, y, p0=p0) + y_fit = pfit[0] * (x / z_ref) ** pfit[1] + coeffs_dict=OrderedDict([('u_ref',pfit[0]),('alpha',pfit[1])]) + formula = '{u_ref} * (z / {z_ref}) ** {alpha}' + fitted_fun = lambda xx: pfit[0] * (xx / z_ref) ** pfit[1] + return y_fit, pfit, {'coeffs':coeffs_dict,'formula':formula,'fitted_function':fitted_fun} + + +def polyfit2d(x, y, z, kx=3, ky=3, order=None): + ''' + Two dimensional polynomial fitting by least squares. + Fits the functional form f(x,y) = z. + + Notes + ----- + Resultant fit can be plotted with: + np.polynomial.polynomial.polygrid2d(x, y, soln.reshape((kx+1, ky+1))) + + Parameters + ---------- + x, y: array-like, 1d + x and y coordinates. + z: np.ndarray, 2d + Surface to fit. + kx, ky: int, default is 3 + Polynomial order in x and y, respectively. + order: int or None, default is None + If None, all coefficients up to maxiumum kx, ky, ie. up to and including x^kx*y^ky, are considered. + If int, coefficients up to a maximum of kx+ky <= order are considered. + + Returns + ------- + Return paramters from np.linalg.lstsq. + + soln: np.ndarray + Array of polynomial coefficients. + residuals: np.ndarray + rank: int + s: np.ndarray + + # The resultant fit can be visualised with: + # + # fitted_surf = np.polynomial.polynomial.polyval2d(x, y, soln.reshape((kx+1,ky+1))) + # plt.matshow(fitted_surf + + + ''' + + # grid coords + x, y = np.meshgrid(x, y) + # coefficient array, up to x^kx, y^ky + coeffs = np.ones((kx+1, ky+1)) + + # solve array + a = np.zeros((coeffs.size, x.size)) + + # for each coefficient produce array x^i, y^j + for index, (j, i) in enumerate(np.ndindex(coeffs.shape)): # TODO should it be i,j + # do not include powers greater than order + if order is not None and i + j > order: + arr = np.zeros_like(x) + else: + arr = coeffs[i, j] * x**i * y**j + a[index] = arr.ravel() + + # do leastsq fitting and return leastsq result + return np.linalg.lstsq(a.T, np.ravel(z), rcond=None) + + + +# --------------------------------------------------------------------------------} +# --- Predifined functions NOTE: they need to be registered in variable `MODELS` +# --------------------------------------------------------------------------------{ +def gaussian(x, p): + """ p = (mu,sigma) """ + return 1/(p[1]*np.sqrt(2*np.pi)) * np.exp(-1/2*((x-p[0])/p[1])**2) + +def gaussian_w_offset(x, p): + """ p = (mu,sigma,y0) """ + return 1/(p[1]*np.sqrt(2*np.pi)) * np.exp(-1/2*((x-p[0])/p[1])**2) + p[2] + +def logarithmic(x, p): + """ p = (a,b) """ + return p[0]*np.log(x)+p[1] + +def powerlaw_all(x, p): + """ p = (alpha,u_ref,z_ref) """ + return p[1] * (x / p[2]) ** p[0] + +def powerlaw_alpha(x, p, u_ref=10, z_ref=100): + """ p = alpha """ + return u_ref * (x / z_ref) ** p[0] + +def powerlaw_u_alpha(x, p, z_ref=100): + """ p = (alpha, u_ref) """ + return p[1] * (x / z_ref) ** p[0] + +def expdecay(x, p, z_ref=100): + """ p = (A, k, B) formula: {A}*exp(-{k}*x)+{B} """, + return p[0]* np.exp(-p[1]*x) + p[2] + +def weibull_pdf(x, p, z_ref=100): + """ p = (A, k) formula: {k}*x**({k}-1) / {A}**{k} * np.exp(-x/{A})**{k} """, + # NOTE: if x is 0, a divide by zero error is incountered if p[1]-1<0 + p=list(p) + return p[1] * x ** (p[1] - 1) / p[0] ** p[1] * np.exp(-(x / p[0]) ** p[1]) + +def sinusoid(x, p): + """ p = (A,omega,phi,B) """ + return p[0]*np.sin(p[1]*x+p[2]) + p[3] +def sinusoid_f(x, p): + """ p = (A,f,phi_deg,B) """ + return p[0]*np.sin(2*pi*(p[1]*x+p[2]/360)) + p[3] + + + +def secondorder_impulse(t, p): + """ p = (A, omega0, zeta, B, t0) """ + A, omega0, zeta, B, t0 = p + omegad = omega0 * sqrt(1-zeta**2) + phi = np.arctan2(zeta, sqrt(1-zeta**2)) + x = np.zeros(t.shape) + bp = t>=t0 + t = t[bp]-t0 + x[bp] += A * sin(omegad * t) * exp(-zeta * omega0 * t) + x+=B + return x + +def secondorder_step(t, p): + """ p = (A, omega0, zeta, B, t0) """ + A, omega0, zeta, B, t0 = p + omegad = omega0 * sqrt(1-zeta**2) + phi = np.arctan2(zeta, sqrt(1-zeta**2)) + x = np.zeros(t.shape) + bp = t>=t0 + t = t[bp]-t0 + x[bp] += A * ( 1- exp(-zeta*omega0 *t)/sqrt(1-zeta**2) * cos(omegad*t - phi)) + x+=B + return x + + +def gentorque(x, p): + """ + INPUTS: + x: generator or rotor speed + p= (RtGnSp, RtTq , Rgn2K , SlPc , SpdGenOn) + RtGnSp Rated generator speed for simple variable-speed generator control (HSS side) (rpm) + RtTq Rated generator torque/constant generator torque in Region 3 for simple variable-speed generator control (HSS side) (N-m) + Rgn2K Generator torque constant in Region 2 for simple variable-speed generator control (HSS side) (N-m/rpm^2) + SlPc Rated generator slip percentage in Region 2 1/2 for simple variable-speed generator control (%) + + OUTPUTS: + GenTrq: Generator torque [Nm] + + """ + + # Init + RtGnSp, RtTq , Rgn2K , SlPc, SpdGenOn = p + GenTrq=np.zeros(x.shape) + + xmin,xmax=np.min(x), np.max(x) +# if RtGnSp<(xmin+xmax)*0.4: +# return GenTrq + + # Setting up different regions + xR21_Start = RtGnSp*(1-SlPc/100) + bR0 = xSpdGenOn , x=xR21_Start , x<=RtGnSp) + bR3 = x>RtGnSp + # R21 + y1, y2 = Rgn2K*xR21_Start**2, RtTq + x1, x2 = xR21_Start , RtGnSp + m=(y2-y1)/(x2-x1) + GenTrq[bR21] = m*(x[bR21]-x1) + y1 # R21 + GenTrq[bR2] = Rgn2K * x[bR2]**2 # R2 + GenTrq[bR3] = RtTq # R3 + return GenTrq + + +MODELS =[ +# {'label':'User defined model', +# 'name':'eval:', +# 'formula':'{a}*x**2 + {b}', +# 'coeffs':None, +# 'consts':None, +# 'bounds':None }, +{'label':'Gaussian', 'handle':gaussian,'id':'predef: gaussian', +'formula':'1/({sigma}*sqrt(2*pi)) * exp(-1/2 * ((x-{mu})/{sigma})**2)', +'coeffs' :'mu=0, sigma=1', # Order Important +'consts' :None, +'bounds' :None}, +{'label':'Gaussian with y-offset','handle':gaussian_w_offset,'id':'predef: gaussian-yoff', +'formula':'1/({sigma}*sqrt(2*pi)) * exp(-1/2 * ((x-{mu})/{sigma})**2) + {y0}', +'coeffs' :'mu=0, sigma=1, y0=0', #Order Important +'consts' :None, +'bounds' :'sigma=(-inf,inf), mu=(-inf,inf), y0=(-inf,inf)'}, +{'label':'Exponential', 'handle': expdecay, 'id':'predef: expdecay', +'formula':'{A}*exp(-{k}*x)+{B}', +'coeffs' :'A=1, k=1, B=0', # Order Important +'consts' :None, +'bounds' :None}, +{'label':'Logarithmic', 'handle': logarithmic, 'id':'predef: logarithmic', +'formula':'{a}*log(x)+{b}', +'coeffs' :'a=1, b=0', # Order Important +'consts' :None, +'bounds' :None}, +{'label':'2nd order impulse/decay (manual)', 'handle': secondorder_impulse, 'id':'predef: secondorder_impulse', +'formula':'{A}*exp(-{zeta}*{omega}*(x-{x0})) * sin({omega}*sqrt(1-{zeta}**2))) +{B}', +'coeffs' :'A=1, omega=1, zeta=0.001, B=0, x0=0', # Order Important +'consts' :None, +'bounds' :'A=(-inf,inf), omega=(0,100), zeta=(0,1), B=(-inf,inf), x0=(-inf,inf)'}, +{'label':'2nd order step (manual)', 'handle': secondorder_step, 'id':'predef: secondorder_step', +'formula':'{A}*(1-exp(-{zeta}*{omega}*(x-{x0}))/sqrt(1-{zeta}**2) * cos({omega}*sqrt(1-{zeta}**2)-arctan({zeta}/sqrt(1-{zeta}**2)))) +{B}', +'coeffs' :'A=1, omega=1, zeta=0.001, B=0, x0=0', # Order Important +'consts' :None, +'bounds' :'A=(-inf,inf), omega=(0,100), zeta=(0,1), B=(-inf,inf), x0=(-inf,inf)'}, + +# --- Wind Energy +{'label':'Power law (alpha)', 'handle':powerlaw_alpha, 'id':'predef: powerlaw_alpha', +'formula':'{u_ref} * (z / {z_ref}) ** {alpha}', +'coeffs' : 'alpha=0.1', # Order important +'consts' : 'u_ref=10, z_ref=100', +'bounds' : 'alpha=(-1,1)'}, +{'label':'Power law (alpha,u)', 'handle':powerlaw_u_alpha, 'id':'predef: powerlaw_u_alpha', +'formula':'{u_ref} * (z / {z_ref}) ** {alpha}', +'coeffs': 'alpha=0.1, u_ref=10', # Order important +'consts': 'z_ref=100', +'bounds': 'u_ref=(0,inf), alpha=(-1,1)'}, +# 'powerlaw_all':{'label':'Power law (alpha,u,z)', 'handle':powerlaw_all, # NOTE: not that useful +# 'formula':'{u_ref} * (z / {z_ref}) ** {alpha}', +# 'coeffs': 'alpha=0.1, u_ref=10, z_ref=100', +# 'consts': None, +# 'bounds': 'u_ref=(0,inf), alpha=(-1,1), z_ref=(0,inf)'}, +{'label':'Weibull PDF', 'handle': weibull_pdf, 'id':'predef: weibull_pdf', +'formula':'{k}*x**({k}-1) / {A}**{k} * np.exp(-x/{A})**{k}', +'coeffs' :'A=1, k=1', # Order Important +'consts' :None, +'bounds' :'A=(0.1,inf), k=(0,5)'}, +{'label':'Generator Torque', 'handle': gentorque, 'id':'predef: gentorque', +'formula': '{RtGnSp} , {RtTq} , {Rgn2K} , {SlPc} , {SpdGenOn}', +'coeffs' : 'RtGnSp=100 , RtTq=1000 , Rgn2K=0.01 ,SlPc=5 , SpdGenOn=0', # Order Important +'consts' :None, +'bounds' :'RtGnSp=(0.1,inf) , RtTq=(1,inf), Rgn2K=(0.0,0.1) ,SlPc=(0,20) , SpdGenOn=(0,inf)'} +] + +# --------------------------------------------------------------------------------} +# --- Main function wrapper +# --------------------------------------------------------------------------------{ +def model_fit(func, x, y, p0=None, bounds=None, **fun_kwargs): + """ + Parameters + ---------- + func: string or function handle + - function handle + - string starting with "fitter: ": (see variable FITTERS) + - "fitter: polynomial_continuous 5' : polyfit order 5 + - "fitter: polynomial_discrete 0 2 3 ': fit polynomial of exponents 0 2 3 + - string providing an expression to evaluate, e.g.: + - "eval: {a}*x + {b}*x**2 " + - string starting with "predef": (see variable MODELS) + - "predef: powerlaw_alpha" : + - "predef: powerlaw_all" : + - "predef: gaussian " : + + x: array of x values + y: array of y values + p0: initial values for parameters, either a string or a dict: + - if a string: the string is converted to a dictionary, assuming key value pairs + example: 'a=0, b=1.3' + - if a dictionary, then keys should corresponds to the parameters of the model + example: {'a':0, 'b':1.3} + bounds: bounds for each parameters, either a string or a dictionary. + NOTE: pi and inf are available to set bounds + - if a string, the string is converted to a dictionary assuming key value pairs + example: 'a=(0,3), b=(-inf,pi)' + - if a dictionary, the keys should corresponds to the parameters of the model + example: {'a':(0,3), 'b':(-inf,pi)} + + Returns + ------- + y_fit: array with same shape as `x` + fitted data. + pfit : fitted parameters + fitter: ModelFitter object + """ + + if isinstance(func,six.string_types) and func.find('fitter:')==0: + # --- This is a high level fitter, we call the class + # The info about the class are storred in the global variable FITTERS + # See e.g. SinusoidFitter, DiscretePolynomialFitter + predef_fitters=[m['id'] for m in FITTERS] + if func not in predef_fitters: + raise Exception('Function `{}` not defined in curve_fitting module\n Available fitters: {}'.format(func,predef_fitters)) + i = predef_fitters.index(func) + FitterDict = FITTERS[i] + consts = FITTERS[i]['consts'] + args, missing = set_common_keys(consts, fun_kwargs) + if len(missing)>0: + raise Exception('Curve fitting with `{}` requires the following arguments {}. Missing: {}'.format(func,consts.keys(),missing)) + # Calling the class + fitter = FitterDict['handle'](x=x, y=y, p0=p0, bounds=bounds, **fun_kwargs) + else: + fitter = ModelFitter(func, x, y, p0=p0, bounds=bounds, **fun_kwargs) + + pfit = [v for _,v in fitter.model['coeffs'].items()] + return fitter.data['y_fit'], pfit , fitter + + +# --------------------------------------------------------------------------------} +# --- Main Class +# --------------------------------------------------------------------------------{ +class ModelFitter(): + def __init__(self,func=None, x=None, y=None, p0=None, bounds=None, **fun_kwargs): + + self.model={ + 'name':None, 'model_function':None, 'consts':fun_kwargs, 'formula': 'unavailable', # model signature + 'coeffs':None, 'formula_num':'unavailable', 'fitted_function':None, 'coeffs_init':p0, 'bounds':bounds, # model fitting + 'R2':None, + } + self.data={'x':x,'y':y,'y_fit':None} + + if func is None: + return + self.set_model(func, **fun_kwargs) + + # Initialize function if present + # Perform fit if data and function is present + if x is not None and y is not None: + self.fit_data(x,y,p0,bounds) + + def set_model(self,func, **fun_kwargs): + if callable(func): + # We don't have much additional info + self.model['model_function'] = func + self.model['name'] = func.__name__ + pass + + elif isinstance(func,six.string_types): + if func.find('predef:')==0: + # --- Minimization from a predefined function + predef_models=[m['id'] for m in MODELS] + if func not in predef_models: + raise Exception('Predefined function `{}` not defined in curve_fitting module\n Available functions: {}'.format(func,predef_models)) + i = predef_models.index(func) + ModelDict = MODELS[i] + self.model['model_function'] = ModelDict['handle'] + self.model['name'] = ModelDict['label'] + self.model['formula'] = ModelDict['formula'] + self.model['coeffs'] = extract_key_num(ModelDict['coeffs']) + self.model['coeffs_init'] = self.model['coeffs'].copy() + self.model['consts'] = extract_key_num(ModelDict['consts']) + self.model['bounds'] = extract_key_tuples(ModelDict['bounds']) + + elif func.find('eval:')==0: + # --- Minimization from a eval string + formula=func[5:] + # Extract coeffs {a} {b} {c}, replace by p[0] + variables, formula_eval = extract_variables(formula) + nParams=len(variables) + if nParams==0: + raise Exception('Formula should contains parameters in curly brackets, e.g.: {a}, {b}, {u_1}. No parameters found in {}'.format(formula)) + + # Check that the formula evaluates + x=np.array([1,2,5])*np.sqrt(2) # some random evaluation vector.. + p=[np.sqrt(2)/4]*nParams # some random initial conditions + try: + y=eval(formula_eval) + y=np.asarray(y) + if y.shape!=x.shape: + raise Exception('The formula does not return an array of same size as the input variable x. The formula must include `x`: {}'.format(formula_eval)) + except SyntaxError: + raise Exception('The formula does not evaluate, syntax error raised: {}'.format(formula_eval)) + except ZeroDivisionError: + pass + + # Creating the actual function + def func(x, p): + return eval(formula_eval) + + self.model['model_function'] = func + self.model['name'] = 'user function' + self.model['formula'] = formula + self.model['coeffs'] = OrderedDict([(k,v) for k,v in zip(variables,p)]) + self.model['coeffs_init'] = self.model['coeffs'].copy() + self.model['consts'] = {} + self.model['bounds'] = None + + else: + raise Exception('func string needs to start with `eval:` of `predef:`, func: {}'.format(func)) + else: + raise Exception('func should be string or callable') + + if fun_kwargs is None: + return + if len(fun_kwargs)==0: + return + if self.model['consts'] is None: + raise Exception('Fun_kwargs provided, but no function constants were defined') + + self.model['consts'], missing = set_common_keys(self.model['consts'], fun_kwargs ) + if len(missing)>0: + raise Exception('Curve fitting with function `{}` requires the following arguments {}. Missing: {}'.format(func.__name__,consts.keys(),missing)) + + def setup_bounds(self, bounds, nParams): + if bounds is not None: + self.model['bounds']=bounds # store in model + bounds=self.model['bounds'] # usemodel bounds as default + if bounds is not None: + if isinstance(bounds ,six.string_types): + bounds=extract_key_tuples(bounds) + + if isinstance(bounds ,dict): + if len(bounds)==0 or 'all' in bounds.keys(): + bounds=([-np.inf]*nParams,[np.inf]*nParams) + elif self.model['coeffs'] is not None: + b1=[] + b2=[] + for k in self.model['coeffs'].keys(): + if k in bounds.keys(): + b1.append(bounds[k][0]) + b2.append(bounds[k][1]) + else: + # TODO merge default bounds + raise Exception('Bounds dictionary is missing the key: `{}`'.format(k)) + bounds=(b1,b2) + else: + raise NotImplementedError('Bounds dictionary with no known model coeffs.') + else: + # so.curve_fit needs a 2-tuple + b1,b2=bounds[0],bounds[1] + if not hasattr(b1,'__len__'): + b1=[b1]*nParams + if not hasattr(b2,'__len__'): + b2=[b2]*nParams + bounds=(b1,b2) + else: + bounds=([-np.inf]*nParams,[np.inf]*nParams) + + self.model['bounds']=bounds # store in model + + def setup_guess(self, p0, bounds, nParams): + """ + Setup initial parameter values for the fit, based on what the user provided, and potentially the bounds + + INPUTS: + - p0: initial parameter values for the fit + - if a string (e.g. " a=1, b=3"), it's converted to a dict + - if a dict, the ordered keys of model['coeffs'] are used to sort p0 + - bounds: tuple of lower and upper bounds for each parameters. + Parameters are ordered as function of models['coeffs'] + bounds[0]: lower bounds or all parameters + bounds[1]: upper bounds or all parameters + + We can assume that the bounds are set + """ + def middleOfBounds(i): + """ return middle of bounds for parameter `i`""" + bLow = bounds[0][i] + bHigh = bounds[0][2] + if (bLow,bHigh)==(-np.inf,np.inf): + p_i=0 + elif bLow==-np.inf: + p_i = -abs(bHigh)*2 + elif bHigh== np.inf: + p_i = abs(bLow)*2 + else: + p_i = (bLow+bHigh)/2 + return p_i + + if isinstance(p0 ,six.string_types): + p0=extract_key_num(p0) + if len(p0)==0: + p0=None + + if p0 is None: + # There is some tricky logic here between the priority of bounds and coeffs + if self.model['coeffs'] is not None: + # We rely on function to give us decent init coefficients + p0 = ([v for _,v in self.model['coeffs'].items()]) + elif bounds is None: + p0 = ([0]*nParams) + else: + # use middle of bounds + p0 = [0]*nParams + for i,(b1,b2) in enumerate(zip(bounds[0],bounds[1])): + p0[i] = middleOfBounds(i) + p0 = (p0) + elif isinstance(p0,dict): + # User supplied a dictionary, we use the ordered keys of coeffs to sort p0 + p0_dict=p0.copy() + if self.model['coeffs'] is not None: + p0=[] + for k in self.model['coeffs'].keys(): + if k in p0_dict.keys(): + p0.append(p0_dict[k]) + else: + raise Exception('Guess dictionary is missing the key: `{}`'.format(k)) + else: + raise NotImplementedError('Guess dictionary with no known model coeffs.') + + + if not hasattr(p0,'__len__'): + p0=(p0,) + + # --- Last check that p0 is within bounds + if bounds is not None: + for p,k,lb,ub in zip(p0, self.model['coeffs'].keys(), bounds[0], bounds[1]): + if pub: + raise Exception('Parameter `{}` has the guess value {}, which is larger than the upper bound ({})'.format(k,p,ub)) + # TODO potentially set it as middle of bounds + + # --- Finally, store the initial guesses in the model + self.model['coeffs_init'] = p0 + + def fit(self, func, x, y, p0=None, bounds=None, **fun_kwargs): + """ Fit model defined by a function to data (x,y) """ + # Setup function + self.set_model(func, **fun_kwargs) + # Fit data to model + self.fit_data(x, y, p0, bounds) + + def clean_data(self,x,y): + x=np.asarray(x) + y=np.asarray(y) + bNaN=~np.isnan(y) + y=y[bNaN] + x=x[bNaN] + bNaN=~np.isnan(x) + y=y[bNaN] + x=x[bNaN] + self.data['x']=x + self.data['y']=y + return x,y + + def fit_data(self, x, y, p0=None, bounds=None): + """ fit data, assuming a model is already setup""" + if self.model['model_function'] is None: + raise Exception('Call set_function first') + + # Cleaning data, and store it in object + x,y=self.clean_data(x,y) + + # nParams + if isinstance(p0 ,six.string_types): + p0=extract_key_num(p0) + if len(p0)==0: + p0=None + if p0 is not None: + if hasattr(p0,'__len__'): + nParams=len(p0) + else: + nParams=1 + elif self.model['coeffs'] is not None: + nParams=len(self.model['coeffs']) + else: + raise Exception('Initial guess `p0` needs to be provided since we cant infer the size of the model coefficients.') + if self.model['coeffs'] is not None: + if len(self.model['coeffs'])!=nParams: + raise Exception('Inconsistent dimension between model guess (size {}) and the model parameters (size {})'.format(nParams,len(self.model['coeffs']))) + + # Bounds + self.setup_bounds(bounds,nParams) + + # Initial conditions + self.setup_guess(p0,self.model['bounds'],nParams) + + # Fitting + minimize_me = lambda x, *p : self.model['model_function'](x, p, **self.model['consts']) + pfit, pcov = so.curve_fit(minimize_me, x, y, p0=self.model['coeffs_init'], bounds=self.model['bounds']) + + # --- Reporting information about the fit (after the fit) + y_fit = self.model['model_function'](x, pfit, **self.model['consts']) + self.store_fit_info(y_fit, pfit) + + # --- Return a fitted function + self.model['fitted_function'] = lambda xx: self.model['model_function'](xx, pfit, **self.model['consts']) + + def store_fit_info(self, y_fit, pfit): + # --- Reporting information about the fit (after the fit) + self.data['y_fit']=y_fit + self.model['R2'] = rsquare(self.data['y'], y_fit) + if self.model['coeffs'] is not None: + if not isinstance(self.model['coeffs'], OrderedDict): + raise Exception('Coeffs need to be of type OrderedDict') + for k,v in zip(self.model['coeffs'].keys(), pfit): + self.model['coeffs'][k]=v + + # Replace numerical values in formula + if self.model['formula'] is not None: + formula_num=self.model['formula'] + for k,v in self.model['coeffs'].items(): + formula_num = formula_num.replace('{'+k+'}',str(v)) + for k,v in self.model['consts'].items(): + formula_num = formula_num.replace('{'+k+'}',str(v)) + self.model['formula_num'] = formula_num + + def formula_num(self, fmt=None): + """ return formula with coeffs and consts evaluted numerically""" + if fmt is None: + fmt_fun = lambda x: str(x) + elif isinstance(fmt,six.string_types): + fmt_fun = lambda x: ('{'+fmt+'}').format(x) + elif callable(fmt): + fmt_fun = fmt + formula_num=self.model['formula'] + for k,v in self.model['coeffs'].items(): + formula_num = formula_num.replace('{'+k+'}',fmt_fun(v)) + for k,v in self.model['consts'].items(): + formula_num = formula_num.replace('{'+k+'}',fmt_fun(v)) + return formula_num + + + + def plot(self, x=None, fig=None, ax=None): + if x is None: + x=self.data['x'] + + sFormula = _clean_formula(self.model['formula'],latex=True) + + import matplotlib.pyplot as plt + import matplotlib.patches as mpatches + + if fig is None: + fig,ax = plt.subplots(1, 1, sharey=False, figsize=(6.4,4.8)) # (6.4,4.8) + fig.subplots_adjust(left=0.12, right=0.95, top=0.95, bottom=0.11, hspace=0.20, wspace=0.20) + + ax.plot(self.data['x'], self.data['y'], '.', label='Data') + ax.plot(x, self.model['fitted_function'](x), '-', label='Model ' + sFormula) + + # Add extra info to the legend + handles, labels = ax.get_legend_handles_labels() # get existing handles and labels + empty_patch = mpatches.Patch(color='none', label='Extra label') # create a patch with no color + for k,v in self.model['coeffs'].items(): + handles.append(empty_patch) # add new patches and labels to list + labels.append(r'${:s}$ = {}'.format(pretty_param(k),pretty_num_short(v))) + handles.append(empty_patch) # add new patches and labels to list + labels.append('$R^2$ = {}'.format(pretty_num_short(self.model['R2']))) + ax.legend(handles, labels) + + + #ax.set_xlabel('') + #ax.set_ylabel('') + return fig,ax + + def print_guessbounds(self): + s='' + p0 = self.model['coeffs_init'] + bounds = self.model['bounds'] + for i,(k,v) in enumerate(self.model['coeffs'].items()): + print( (pretty_num(bounds[0][i]),pretty_num(p0[i]), pretty_num(bounds[1][i])) ) + s+='{:15s}: {:10s} < {:10s} < {:10s}\n'.format(k, pretty_num(bounds[0][i]),pretty_num(p0[i]), pretty_num(bounds[1][i])) + print(s) + + + def __repr__(self): + s='<{} object> with fields:\n'.format(type(self).__name__) + s+=' - data, dictionary with keys: \n' + s+=' - x: [{} ... {}], n: {} \n'.format(self.data['x'][0],self.data['x'][-1],len(self.data['x'])) + s+=' - y: [{} ... {}], n: {} \n'.format(self.data['y'][0],self.data['y'][-1],len(self.data['y'])) + s+=' - model, dictionary with keys: \n' + for k,v in self.model.items(): + s=s+' - {:15s}: {}\n'.format(k,v) + return s + + +# --------------------------------------------------------------------------------} +# --- Wrapper for predefined fitters +# --------------------------------------------------------------------------------{ +class PredefinedModelFitter(ModelFitter): + def __init__(self, x=None, y=None, p0=None, bounds=None, **kwargs): + ModelFitter.__init__(self,x=None, y=None, p0=p0, bounds=bounds) # NOTE: not passing data + + self.kwargs=kwargs + + if x is not None and y is not None: + self.fit_data(x,y,p0,bounds) + + def setup_model(self): + """ + Setup model: + - guess/coeffs_init: return params in format needed for curve_fit (p0,p1,p2,p3) + - bound : bounds in format needed for curve_fit ((low0,low1,low2), (high0, high1)) + - coeffs : OrderedDict, necessary for user print + - formula : necessary for user print + """ + #self.model['coeffs'] = OrderedDict([(var,1) for i,var in enumerate(variables)]) + #self.model['formula'] = '' + #self.model['coeffs_init']=p_guess + #self.model['bounds']=bounds_guess + raise NotImplementedError('To be implemented by child class') + + def model_function(self, x, p): + raise NotImplementedError('To be implemented by child class') + + def fit_data(self, x, y, p0=None, bounds=None): + # Cleaning data + x,y=self.clean_data(x,y) + + # --- setup model + # guess initial parameters, potential bounds, and set necessary data + self.setup_model() + + # --- Minimization + minimize_me = lambda x, *p : self.model_function(x, p) + if self.model['bounds'] is None: + pfit, pcov = so.curve_fit(minimize_me, x, y, p0=self.model['coeffs_init']) + else: + pfit, pcov = so.curve_fit(minimize_me, x, y, p0=self.model['coeffs_init'], bounds=self.model['bounds']) + # --- Reporting information about the fit (after the fit) + # And Return a fitted function + y_fit = self.model_function(x, pfit) + self.model['fitted_function']=lambda xx : self.model_function(xx, pfit) + self.store_fit_info(y_fit, pfit) + + def plot_guess(self, x=None, fig=None, ax=None): + """ plotthe guess values""" + if x is None: + x=self.data['x'] + import matplotlib.pyplot as plt + if fig is None: + fig,ax = plt.subplots(1, 1, sharey=False, figsize=(6.4,4.8)) # (6.4,4.8) + fig.subplots_adjust(left=0.12, right=0.95, top=0.95, bottom=0.11, hspace=0.20, wspace=0.20) + + p_guess = self.model['coeffs_init'] + + ax.plot(self.data['x'], self.data['y'] , '.', label='Data') + ax.plot(x, self.model_function(x,p_guess), '-', label='Model at guessed parameters') + ax.legend() + + +# --------------------------------------------------------------------------------} +# --- Predefined fitters +# --------------------------------------------------------------------------------{ +class SecondOrderFitterImpulse(PredefinedModelFitter): + + def model_function(self, x, p): + return secondorder_impulse(x, p) + + def setup_model(self): + """ p = (A, omega0, zeta, B, t0) """ + self.model['coeffs'] = OrderedDict([('A',1),('omega',1),('zeta',0.01),('B',0),('t0',0)]) + self.model['formula'] = '{A}*exp(-{zeta}*{omega}*(x-{x0}))*sin({omega}*sqrt(1-{zeta}**2)))+{B}' + + # --- Guess Initial values + x, y = self.data['x'],self.data['y'] + # TODO use signal + dt = x[1]-x[0] + omega0 = main_frequency(x,y) + A = np.max(y) - np.min(y) + B = np.mean(y) + zeta = 0.1 + y_start = y[0]+0.01*A + bDeviate = np.argwhere(abs(y-y_start)>abs(y_start-y[0]))[0] + t0 = x[bDeviate[0]] + p_guess = np.array([A, omega0, zeta, B, t0]) + self.model['coeffs_init'] = p_guess + # --- Set Bounds + T = x[-1]-x[0] + dt = x[1]-x[0] + om_min = 2*np.pi/T/2 + om_max = 2*np.pi/dt/2 + b_A = (A*0.1,A*3) + b_om = (om_min,om_max) + b_zeta = (0,1) + b_B = (np.min(y),np.max(y)) + b_x0 = (np.min(x),np.max(x)) + self.model['bounds'] = ((b_A[0],b_om[0],b_zeta[0],b_B[0],b_x0[0]),(b_A[1],b_om[1],b_zeta[1],b_B[1],b_x0[1])) + #self.plot_guess(); import matplotlib.pyplot as plt; plt.show() + #self.print_guessbounds(); + +class SecondOrderFitterStep(PredefinedModelFitter): + + def model_function(self, x, p): + return secondorder_step(x, p) + + def setup_model(self): + """ p = (A, omega0, zeta, B, t0) """ + self.model['coeffs'] = OrderedDict([('A',1),('omega',1),('zeta',0.01),('B',0),('t0',0)]) + self.model['formula'] ='{A}*(1-exp(-{zeta}*{omega}*(x-{x0}))/sqrt(1-{zeta}**2) * cos({omega}*sqrt(1-{zeta}**2)-arctan({zeta}/sqrt(1-{zeta}**2)))) +{B}' + # --- Guess Initial values + x, y = self.data['x'],self.data['y'] + # TODO use signal + omega0 = main_frequency(x,y) + A = np.max(y) - np.min(y) + B = y[0] + zeta = 0.1 + y_start = y[0]+0.01*A + bDeviate = np.argwhere(abs(y-y_start)>abs(y_start-y[0]))[0] + t0 = x[bDeviate[0]] + p_guess = np.array([A, omega0, zeta, B, t0]) + self.model['coeffs_init'] = p_guess + # --- Set Bounds + T = x[-1]-x[0] + dt = x[1]-x[0] + om_min = 2*np.pi/T/2 + om_max = 2*np.pi/dt/2 + b_A = (A*0.1,A*3) + b_om = (om_min,om_max) + b_zeta = (0,1) + b_B = (np.min(y),np.max(y)) + b_x0 = (np.min(x),np.max(x)) + self.model['bounds'] = ((b_A[0],b_om[0],b_zeta[0],b_B[0],b_x0[0]),(b_A[1],b_om[1],b_zeta[1],b_B[1],b_x0[1])) + #self.plot_guess(); import matplotlib.pyplot as plt; plt.show() + #self.print_guessbounds(); + +# --------------------------------------------------------------------------------} +# --- Predefined fitter +# --------------------------------------------------------------------------------{ +class ContinuousPolynomialFitter(ModelFitter): + def __init__(self,order=None, x=None, y=None, p0=None, bounds=None): + ModelFitter.__init__(self,x=None, y=None, p0=p0, bounds=bounds) + self.setOrder(int(order)) + if order is not None and x is not None and y is not None: + self.fit_data(x,y,p0,bounds) + + def setOrder(self, order): + self.order=order + if order is not None: + variables= string.ascii_lowercase[:order+1] + self.model['coeffs'] = OrderedDict([(var,1) for i,var in enumerate(variables)]) + formula = ' + '.join(['{}*x**{}'.format('{'+var+'}',order-i) for i,var in enumerate(variables)]) + self.model['formula'] = _clean_formula(formula) + + def fit_data(self, x, y, p0=None, bounds=None): + if self.order is None: + raise Exception('Polynomial Fitter not set, call function `setOrder` to set order') + # Cleaning data + x,y=self.clean_data(x,y) + + nParams=self.order+1 + # Bounds + self.setup_bounds(bounds, nParams) # TODO + # Initial conditions + self.setup_guess(p0, bounds, nParams) # TODO + + # Fitting + pfit = np.polyfit(x,y,self.order) + + # --- Reporting information about the fit (after the fit) + y_fit = np.polyval(pfit,x) + self.store_fit_info(y_fit, pfit) + + # --- Return a fitted function + self.model['fitted_function']=lambda xx : np.polyval(pfit,xx) + + +class DiscretePolynomialFitter(ModelFitter): + def __init__(self,exponents=None, x=None, y=None, p0=None, bounds=None): + ModelFitter.__init__(self,x=None, y=None, p0=p0, bounds=bounds) + self.setExponents(exponents) + if exponents is not None and x is not None and y is not None: + self.fit_data(x,y,p0,bounds) + + def setExponents(self, exponents): + self.exponents=exponents + if exponents is not None: + #exponents=-np.sort(-np.asarray(exponents)) + self.exponents=exponents + variables= string.ascii_lowercase[:len(exponents)] + self.model['coeffs'] = OrderedDict([(var,1) for i,var in enumerate(variables)]) + formula = ' + '.join(['{}*x**{}'.format('{'+var+'}',e) for var,e in zip(variables,exponents)]) + self.model['formula'] = _clean_formula(formula) + + def fit_data(self, x, y, p0=None, bounds=None): + if self.exponents is None: + raise Exception('Polynomial Fitter not set, call function `setExponents` to set exponents') + # Cleaning data, and store it in object + x,y=self.clean_data(x,y) + + nParams=len(self.exponents) + # Bounds + self.setup_bounds(bounds, nParams) # TODO + # Initial conditions + self.setup_guess(p0, bounds, nParams) # TODO + + X_poly=np.array([]) + for i,e in enumerate(self.exponents): + if i==0: + X_poly = np.array([x**e]) + else: + X_poly = np.vstack((X_poly,x**e)) + try: + pfit = np.linalg.lstsq(X_poly.T, y, rcond=None)[0] + except: + pfit = np.linalg.lstsq(X_poly.T, y) + + # --- Reporting information about the fit (after the fit) + y_fit= np.dot(pfit, X_poly) + self.store_fit_info(y_fit, pfit) + + # --- Return a fitted function + def fitted_function(xx): + y=np.zeros(xx.shape) + for i,(e,c) in enumerate(zip(self.exponents,pfit)): + y += c*xx**e + return y + self.model['fitted_function']=fitted_function + + +class SinusoidFitter(ModelFitter): + def __init__(self, physical=False, x=None, y=None, p0=None, bounds=None): + ModelFitter.__init__(self, x=None, y=None, p0=p0, bounds=bounds) + #self.setOrder(int(order)) + self.physical=physical + if physical: + self.model['coeffs'] = OrderedDict([('A',1),('f',1),('phi',0),('B',0)]) + self.model['formula'] = '{A} * sin(2*pi*({f}*x + {phi}/360)) + {B}' + else: + self.model['coeffs'] = OrderedDict([('A',1),('omega',1),('phi',0),('B',0)]) + self.model['formula'] = '{A} * sin({omega}*x + {phi}) + {B}' + + if x is not None and y is not None: + self.fit_data(x,y,p0,bounds) + + def fit_data(self, x, y, p0=None, bounds=None): + # Cleaning data + x,y=self.clean_data(x,y) + + # TODO use signal + guess_freq= main_frequency(x,y)/(2*np.pi) # [Hz] + guess_amp = np.std(y) * 2.**0.5 + guess_offset = np.mean(y) + if self.physical: + guess = np.array([guess_amp, guess_freq, 0., guess_offset]) + minimize_me = lambda x, *p : sinusoid_f(x, p) + else: + guess = np.array([guess_amp, 2.*np.pi*guess_freq, 0., guess_offset]) + minimize_me = lambda x, *p : sinusoid(x, p) + self.model['coeffs_init'] = guess + + pfit, pcov = so.curve_fit(minimize_me, x, y, p0=guess) + + # --- Reporting information about the fit (after the fit) + # And Return a fitted function + if self.physical: + y_fit = sinusoid_f(x, pfit) + self.model['fitted_function']=lambda xx : sinusoid_f(xx, pfit) + else: + y_fit = sinusoid(x, pfit) + self.model['fitted_function']=lambda xx : sinusoid(xx, pfit) + self.store_fit_info(y_fit, pfit) + + + +class GeneratorTorqueFitter(ModelFitter): + def __init__(self,x=None, y=None, p0=None, bounds=None): + ModelFitter.__init__(self,x=None, y=None, p0=p0, bounds=bounds) + +# RtGnSp, RtTq , Rgn2K , SlPc , SpdGenOn = p +# {'label':'Generator Torque', 'handle': gentorque, 'id':'predef: gentorque', +# 'formula': '{RtGnSp} , {RtTq} , {Rgn2K} , {SlPc} , {SpdGenOn}', + self.model['coeffs']= extract_key_num('RtGnSp=100 , RtTq=1000 , Rgn2K=0.01 ,SlPc=5 , SpdGenOn=0') +# 'consts' :None, +# 'bounds' :'RtGnSp=(0.1,inf) , RtTq=(1,inf), Rgn2K=(0.0,0.1) ,SlPc=(0,20) , SpdGenOn=(0,inf)'} + if x is not None and y is not None: + self.fit_data(x,y,p0,bounds) + + def fit_data(self, x, y, p0=None, bounds=None): + #nParams=5 + ## Bounds + #self.setup_bounds(bounds,nParams) # TODO + ## Initial conditions + #self.setup_guess(p0,bounds,nParams) # TODO + + # Cleaning data, and store it in object + x,y=self.clean_data(x,y) + + I = np.argsort(x) + x=x[I] + y=y[I] + + # Estimating deltas + xMin, xMax=np.min(x),np.max(x) + yMin, yMax=np.min(y),np.max(y) + DeltaX = (xMax-xMin)*0.02 + DeltaY = (yMax-yMin)*0.02 + + # Binning data + x_bin=np.linspace(xMin,xMax,min(200,len(x))) + x_lin=x_bin[0:-1]+np.diff(x_bin) + #y_lin=np.interp(x_lin,x,y) # TODO replace by bining + y_lin = np.histogram(y, x_bin, weights=y)[0]/ np.histogram(y, x_bin)[0] + y_lin, _, _ = stats.binned_statistic(x, y, statistic='mean', bins=x_bin) + x_lin, _, _ = stats.binned_statistic(x, x, statistic='mean', bins=x_bin) + bNaN=~np.isnan(y_lin) + y_lin=y_lin[bNaN] + x_lin=x_lin[bNaN] + + # --- Find good guess of parameters based on data + # SpdGenOn + iOn = np.where(y>0)[0][0] + SpdGenOn_0 = x[iOn] + SpdGenOn_Bnds = (max(x[iOn]-DeltaX,xMin), min(x[iOn]+DeltaX,xMax)) + # Slpc + Slpc_0 = 5 + Slpc_Bnds = (0,10) + # RtTq + RtTq_0 = yMax + RtTq_Bnds = (yMax-DeltaY, yMax+DeltaY) + # RtGnSp + iCloseRt = np.where(y>yMax*0.50)[0][0] + RtGnSp_0 = x[iCloseRt] + RtGnSp_Bnds = ( RtGnSp_0 -DeltaX*2, RtGnSp_0+DeltaX*2) + # Rgn2K + #print('>>>',SpdGenOn_0, RtGnSp_0) + bR2=np.logical_and(x>SpdGenOn_0, x ['a','b'] + The variables are replaced with p[0],..,p[n] in order of appearance + """ + regex = r"\{(.*?)\}" + matches = re.finditer(regex, sFormula, re.DOTALL) + formula_eval=sFormula + variables=[] + ivar=0 + for i, match in enumerate(matches): + for groupNum in range(0, len(match.groups())): + var = match.group(1) + if var not in variables: + variables.append(var) + formula_eval = formula_eval.replace('{'+match.group(1)+'}','p[{:d}]'.format(ivar)) + ivar+=1 + return variables, formula_eval + + +def extract_key_tuples(text): + """ + all=(0.1,-2),b=(inf,0), c=(-inf,0.3e+10) + """ + if text is None: + return {} + regex = re.compile(r'(?P[\w\-]+)=\((?P[0-9+epinf.-]*?),(?P[0-9+epinf.-]*?)\)($|,)') + return {match.group("key"): (float(match.group("value1")),float(match.group("value2"))) for match in regex.finditer(text.replace(' ',''))} + +def extract_key_num(text): + """ + all=0.1, b=inf, c=-0.3e+10 + """ + if text is None: + return {} + regex = re.compile(r'(?P[\w\-]+)=(?P[0-9+epinf.-]*?)($|,)') + return OrderedDict([(match.group("key"), float(match.group("value"))) for match in regex.finditer(text.replace(' ',''))]) + +def extract_key_miscnum(text): + """ + all=0.1, b=(inf,0), c=[-inf,0.3e+10,10,11]) + """ + def isint(s): + try: + int(s) + return True + except: + return False + + if text is None: + return {} + sp=re.compile('([\w]+)=').split(text.replace(' ','')) + if len(sp)<3: + return {} + sp=sp[1:] + keys = sp[0::2] + values = sp[1::2] + d={} + for (k,v) in zip(keys,values): + if v.find('(')>=0: + v=v.replace('(','').replace(')','') + v=v.split(',') + vect=tuple([float(val) for val in v if len(val.strip())>0]) + elif v.find('[')>=0: + v=v.replace('[','').replace(']','') + v=v.split(',') + vect=[int(val) if isint(val) else float(val) for val in v if len(val.strip())>0] # NOTE returning lists + elif v.find('True')>=0: + v=v.replace(',','').strip() + vect=True + elif v.find('False')>=0: + v=v.replace(',','').strip() + vect=False + else: + v=v.replace(',','').strip() + vect=int(v) if isint(v) else float(v) + d[k]=vect + return d + +def set_common_keys(dict_target, dict_source): + """ Set a dictionary using another one, missing keys in source dictionary are reported""" + keys_missing=[] + for k in dict_target.keys(): + if k in dict_source.keys(): + dict_target[k]=dict_source[k] + else: + keys_missing.append(k) + return dict_target, keys_missing + +def _clean_formula(s, latex=False): + s = s.replace('+-','-').replace('**1','').replace('*x**0','') + s = s.replace('np.','') + if latex: + #s = s.replace('{','$').replace('}','$') + s = s.replace('phi',r'\phi') + s = s.replace('alpha',r'\alpha') + s = s.replace('beta' ,r'\alpha') + s = s.replace('zeta' ,r'\zeta') + s = s.replace('mu' ,r'\mu' ) + s = s.replace('pi' ,r'\pi' ) + s = s.replace('sigma',r'\sigma') + s = s.replace('omega',r'\omega') + s = s.replace('_ref',r'_{ref}') # make this general + s = s.replace(r'(',r'{(') + s = s.replace(r')',r')}') + s = s.replace(r'**',r'^') + s = s.replace(r'*', '') + s = s.replace('sin',r'\sin') + s = s.replace('exp',r'\exp') + s = s.replace('sqrt',r'\sqrt') + s = r'$'+s+r'$' + else: + s = s.replace('{','').replace('}','') + return s + + +def main_frequency(t,y): + """ + Returns main frequency of a signal + NOTE: this tool below to welib.tools.signal_analysis, but put here for convenience + """ + dt = t[1]-t[0] # assume uniform spacing of time and frequency + om = np.fft.fftfreq(len(t), (dt))*2*np.pi + Fyy = abs(np.fft.fft(y)) + omega = abs(om[np.argmax(Fyy[1:])+1]) # exclude the zero frequency (mean) + return omega + +def rsquare(y, f): + """ Compute coefficient of determination of data fit model and RMSE + [r2] = rsquare(y,f) + RSQUARE computes the coefficient of determination (R-square) value from + actual data Y and model data F. + INPUTS + y : Actual data + f : Model fit + OUTPUT + R2 : Coefficient of determination + """ + # Compare inputs + if not np.all(y.shape == f.shape) : + raise Exception('Y and F must be the same size') + # Check for NaN + tmp = np.logical_not(np.logical_or(np.isnan(y),np.isnan(f))) + y = y[tmp] + f = f[tmp] + R2 = max(0,1-np.sum((y-f)**2)/np.sum((y-np.mean(y))** 2)) + return R2 + +def pretty_param(s): + if s in ['alpha','beta','delta','gamma','epsilon','zeta','lambda','mu','nu','pi','rho','sigma','phi','psi','omega']: + s = r'\{}'.format(s) + s = s.replace('_ref',r'_{ref}') # make this general.. + return s + +def pretty_num(x): + if abs(x)<1000 and abs(x)>1e-4: + return "{:9.4f}".format(x) + else: + return '{:.3e}'.format(x) + +def pretty_num_short(x,digits=3): + if digits==4: + if abs(x)<1000 and abs(x)>1e-1: + return "{:.4f}".format(x) + else: + return "{:.4e}".format(x) + elif digits==3: + if abs(x)<1000 and abs(x)>1e-1: + return "{:.3f}".format(x) + else: + return "{:.3e}".format(x) + elif digits==2: + if abs(x)<1000 and abs(x)>1e-1: + return "{:.2f}".format(x) + else: + return "{:.2e}".format(x) + + +if __name__ == '__main__': + # --- Writing example models to file for pyDatView tests + a,b,c = 2.0, 3.0, 4.0 + u_ref,z_ref,alpha=10,12,0.12 + mu,sigma=0.5,1.2 + x = np.linspace(0.1,30,20) + A,k,B=0.5,1.2,10 + y_exp=expdecay(x,(A,k,B)) + A, k = 10, 2.3, + y_weib=weibull_pdf(x,(A,k)) + y_log=logarithmic(x,(a,b)) + exponents=[0,3,5] + y_poly = a + b*x**3 + c*x**5 + y_power=powerlaw_all(x,(alpha,u_ref,z_ref)) + y_gauss=gaussian(x,(mu,sigma)) + A= 101; B= -200.5; omega = 0.4; phi = np.pi/3 + y_sin=sinusoid(x,(A,omega,phi,B)) + np.random.normal(0, 0.1, len(x)) + M=np.column_stack((x,y_poly,y_power,y_gauss,y_gauss+10,y_weib,y_exp,y_log,y_sin)) + np.savetxt('../TestFit.csv',M,header='x,poly,power,gauss,gauss_off,weib,expdecay,log,sin',delimiter=',') diff --git a/pyFAST/tools/damping.py b/pyFAST/tools/damping.py new file mode 100644 index 0000000..4f751b0 --- /dev/null +++ b/pyFAST/tools/damping.py @@ -0,0 +1,373 @@ +""" +Context: + +Logarithmic decrement: + + delta = 1/N log [ x(t) / x(t+N T_d)] = 2 pi zeta / sqrt(1-zeta^2) + +Damping ratio: + + zeta = delta / sqrt( 4 pi^2 + delta^2 ) + +Damped period, frequency: + + Td = 2pi / omega_d + + omegad = omega_0 sqrt(1-zeta**2) + +Damping exponent: + + + alpha = zeta omega_0 = delta/ T_d + + +""" + +import numpy as np + +__all__ = ['freqDampEstimator'] +__all__ += ['freqDampFromPeaks'] +__all__ += ['zetaEnvelop'] +__all__ += ['TestDamping'] + +def indexes(y, thres=0.3, min_dist=1, thres_abs=False): + """Peak detection routine. + + Finds the numeric index of the peaks in *y* by taking its first order difference. By using + *thres* and *min_dist* parameters, it is possible to reduce the number of + detected peaks. *y* must be signed. + + Parameters + ---------- + y : ndarray (signed) + 1D amplitude data to search for peaks. + thres : float, defining threshold. Only the peaks with amplitude higher than the + threshold will be detected. + if thres_abs is False: between [0., 1.], normalized threshold. + min_dist : int + Minimum distance between each detected peak. The peak with the highest + amplitude is preferred to satisfy this constraint. + thres_abs: boolean + If True, the thres value will be interpreted as an absolute value, instead of + a normalized threshold. + + Returns + ------- + ndarray + Array containing the numeric indexes of the peaks that were detected + """ + if isinstance(y, np.ndarray) and np.issubdtype(y.dtype, np.unsignedinteger): + raise ValueError("y must be signed") + + if not thres_abs: + thres = thres * (np.max(y) - np.min(y)) + np.min(y) + + min_dist = int(min_dist) + + # compute first order difference + dy = np.diff(y) + + # propagate left and right values successively to fill all plateau pixels (0-value) + zeros,=np.where(dy == 0) + + # check if the signal is totally flat + if len(zeros) == len(y) - 1: + return np.array([]) + + if len(zeros): + # compute first order difference of zero indexes + zeros_diff = np.diff(zeros) + # check when zeros are not chained together + zeros_diff_not_one, = np.add(np.where(zeros_diff != 1), 1) + # make an array of the chained zero indexes + zero_plateaus = np.split(zeros, zeros_diff_not_one) + + # fix if leftmost value in dy is zero + if zero_plateaus[0][0] == 0: + dy[zero_plateaus[0]] = dy[zero_plateaus[0][-1] + 1] + zero_plateaus.pop(0) + + # fix if rightmost value of dy is zero + if len(zero_plateaus) and zero_plateaus[-1][-1] == len(dy) - 1: + dy[zero_plateaus[-1]] = dy[zero_plateaus[-1][0] - 1] + zero_plateaus.pop(-1) + + # for each chain of zero indexes + for plateau in zero_plateaus: + median = np.median(plateau) + # set leftmost values to leftmost non zero values + dy[plateau[plateau < median]] = dy[plateau[0] - 1] + # set rightmost and middle values to rightmost non zero values + dy[plateau[plateau >= median]] = dy[plateau[-1] + 1] + + # find the peaks by using the first order difference + peaks = np.where((np.hstack([dy, 0.]) < 0.) + & (np.hstack([0., dy]) > 0.) + & (np.greater(y, thres)))[0] + + # handle multiple peaks, respecting the minimum distance + if peaks.size > 1 and min_dist > 1: + highest = peaks[np.argsort(y[peaks])][::-1] + rem = np.ones(y.size, dtype=bool) + rem[peaks] = False + + for peak in highest: + if not rem[peak]: + sl = slice(max(0, peak - min_dist), peak + min_dist + 1) + rem[sl] = True + rem[peak] = False + + peaks = np.arange(y.size)[~rem] + + return peaks + + +#indexes =indexes(x, thres=0.02/max(x), min_dist=1, thres_abs=true) +def logDecFromThreshold(x, threshold=None, bothSides=False, decay=True): + """ Detect maxima in a signal, computes the log deg based on it + """ + if bothSides: + ldPos,iTPos,stdPos,IPos,vldPos = logDecFromThreshold( x, threshold=threshold, decay=decay) + ldNeg,iTNeg,stdNeg,INeg,vldNeg = logDecFromThreshold(-x, threshold=threshold, decay=decay) + return (ldPos+ldNeg)/2, (iTPos+iTNeg)/2, (stdPos+stdNeg)/2, (IPos,INeg), (vldPos, vldNeg) + + if threshold is None: + threshold = np.mean(abs(x-np.mean(x)))/3; + I =indexes(x, thres=threshold, min_dist=1, thres_abs=True) + # Estimating "index" period + iT = round(np.median(np.diff(I))); + vn=np.arange(0,len(I)-1)+1 + # Quick And Dirty Way using one as ref and assuming all periods were found + if decay: + # For a decay we take the first peak as a reference + vLogDec = 1/vn*np.log( x[I[0]]/x[I[1:]] ) # Logarithmic decrement + else: + # For negative damping we take the last peak as a reference + vLogDec = 1/vn*np.log( x[I[-2::-1]]/x[I[-1]] ) # Logarithmic decrement + logdec = np.mean(vLogDec); + std_logdec = np.std(vLogDec) ; + return logdec, iT, std_logdec, I, vLogDec + +def logDecTwoTimes(x, t, i1, i2, Td): + t1, t2 = t[i1], t[i2] + x1, x2 = x[i1], x[i2] + N = (t2-t1)/Td + logdec = 1/N * np.log(x1/x2) + return logdec + +def zetaTwoTimes(x, t, i1, i2, Td): + logdec = logDecTwoTimes(x, t, i1, i2, Td) + zeta = logdec/np.sqrt(4*np.pi**2 + logdec**2) # damping ratio + return zeta + +def zetaRange(x, t, IPos, INeg, Td, decay): + """ + Compute naive zeta based on different peak values (first, mid, last) + """ + def naivezeta(i1, i2): + zetaP = zetaTwoTimes( x, t, IPos[i1], IPos[i2], Td) + zetaN = zetaTwoTimes( -x, t, INeg[i1], INeg[i2], Td) + return [zetaP, zetaN] + zetas = [] + # --- Computing naive log dec from first and last peaks + zetas += naivezeta(0, -1) + # --- Computing naive log dec from one peak and the middle one + if len(IPos)>3 and len(INeg)>3: + if decay: + i1, i2 = 0, int(len(IPos)/2) + else: + i1, i2 = -int(len(IPos)/2), -1 + zetas += naivezeta(i1, i2) + zetaSup = np.max(zetas) + zetaInf = np.min(zetas) + zetaMean = np.mean(zetas) + return zetaSup, zetaInf, zetaMean + +def zetaEnvelop(x, t, omega0, zeta, iRef, moff=0): + """ NOTE: x is assumed to be centered on 0""" + m = np.mean(x) + tref = t[iRef] + Aref = x[iRef]-m + epos = Aref*np.exp(-zeta*omega0*(t-tref))+m+moff + eneg = -Aref*np.exp(-zeta*omega0*(t-tref))+m+moff + return epos, eneg + + +def freqDampFromPeaks(x, t, threshold=None, plot=False, refPoint='mid'): + """ + Use Upper and lower peaks to compute log decrements between neighboring peaks + Previously called logDecFromDecay. + """ + info = {} + x = np.array(x).copy() + m = np.mean(x) + x = x-m # we remove the mean once and for all + if threshold is None: + threshold = np.mean(abs(x))/3 + + dt = t[1]-t[0] # todo signal with dt not uniform + + # Is it a decay or an exloding signal + xstart, xend = np.array_split(np.abs(x),2) + decay= np.mean(xstart)> np.mean(xend) + + # --- Computing log decs from positive and negative side and taking the mean + logdec,iT,std,(IPos,INeg), (vLogDecPos, vLogDecNeg) = logDecFromThreshold( x, threshold=threshold, bothSides=True, decay=decay) + + # --- Finding damped period + Td = iT*dt # Period of damped oscillations. Badly estimated due to dt resolution + # % Better estimate of period + # [T,~,iCross]=fGetPeriodFromZeroCrossing(x(1:IPos(end)),dt); + fd = 1/Td + + # --- Naive ranges + zetaMax, zetaMin, zetaMean = zetaRange(x, t, IPos, INeg, Td, decay) + + zeta = logdec/np.sqrt(4*np.pi**2 + logdec**2 ) # Damping Ratio + fn = fd/np.sqrt(1-zeta**2) + T0 = 1/fn + omega0=2*np.pi*fn + # --- Model + # Estimating signal params + alpha = logdec/Td + omega = 2*np.pi*fd + # Find amplitude at a reference peak + # (we chose the middle peak of the time series to minimize period drift before and after) + # We will adjust for phase and time offset later + i1 = IPos[int(len(IPos)/2)] + A1 = x[i1] + t1 = dt*i1 + # --- Find a zero up-crossing around our value of reference for phase determination + XX=x[i1:] + ineg = i1+np.where(XX<0)[0][0] + ipos = ineg-1 + xcross = [x[ipos],x[ineg]] + icross = [ipos,ineg] + i0 = np.interp(0,xcross,icross) # precise 0-up-crossing + t0 = dt*i0 + phi0 = np.mod(2*np.pi- omega*t0+np.pi/2,2*np.pi); + # --- Model + A = A1/(np.exp(-alpha*t1)*np.cos(omega*t1+phi0)); # Adjust for phase and time offset + x_model = A*np.exp(-alpha*t)*np.cos(omega*t+phi0)+m; + epos = A*np.exp(-alpha*t)+m + eneg = -A*np.exp(-alpha*t)+m + + if plot: + if refPoint=='mid': + iRef = i1 + elif refPoint=='start': + iRef = IPos[0] + else: + iRef = IPos[-1] + import matplotlib.pyplot as plt + print('LogDec.: {:.4f} - Damping ratio: {:.4f} - F_n: {:.4f} - F_d: {:.4f} - T_d:{:.3f} - T_n:{:.3f}'.format(logdec, zeta, fn, fd, Td,T0)) + fig,ax = plt.subplots(1, 1, sharey=False, figsize=(6.4,4.8)) # (6.4,4.8) + fig.subplots_adjust(left=0.12, right=0.95, top=0.95, bottom=0.11, hspace=0.20, wspace=0.20) + ax.plot(t, x+m) + ax.plot(t[IPos],x[IPos]+m,'o') + ax.plot(t[INeg],x[INeg]+m,'o') + epos, eneg = zetaEnvelop(x, t, omega0, zeta, iRef=iRef, moff=m) + ax.plot(t ,epos, 'k--', label=r'$\zeta={:.4f}$'.format(zeta)) + ax.plot(t ,eneg, 'k--') + epos, eneg = zetaEnvelop(x, t, omega0, zetaMax, iRef=iRef, moff=m) + ax.plot(t ,epos, 'b:', label=r'$\zeta={:.4f}$'.format(zetaMax)) + ax.plot(t ,eneg, 'b:') + epos, eneg = zetaEnvelop(x, t, omega0, zetaMin, iRef=iRef, moff=m) + ax.plot(t ,epos, 'r:', label=r'$\zeta={:.4f}$'.format(zetaMin)) + ax.plot(t ,eneg, 'r:') + #ax.plot(t ,x_model,'k:') + #ax.legend() + dx = np.max(abs(x-m)) + ax.set_ylim([m-dx*1.1 , m+dx*1.1]) + + # We return a dictionary + info['zeta'] = zeta + info['fd'] = fd + info['Td'] = Td + info['fn'] = fn + info['omega0'] = omega0 + info['IPos'] = IPos + info['INeg'] = INeg + # TODO + info['x_model'] = x_model + info['epos'] = epos + info['eneg'] = eneg + # + info['zeta'] = zeta + info['zetaMin'] = zetaMin + info['zetaMax'] = zetaMax + info['zetaMean'] = zetaMean + + return fn, zeta, info + + +def freqDampEstimator(x, t, opts): + """ + Estimate natural frequency and damping ratio. + Wrapper function to use different methods. + + """ + if opts['method']=='fromPeaks': + fn, zeta, info = freqDampFromPeaks(x, t) + else: + raise NotImplementedError() + return fn, zeta, info + + + +# --------------------------------------------------------------------------------} +# --- Unittests +# --------------------------------------------------------------------------------{ +import unittest + +class TestDamping(unittest.TestCase): + + def test_logdec_from_peaks(self): + plot = (__name__ == '__main__') + + for zeta in [0.1, -0.01]: + T0 = 10 + Td = T0 / np.sqrt(1-zeta**2) + delta = 2*np.pi*zeta/np.sqrt(1-zeta**2) # logdec + alpha = delta/Td + t = np.linspace(0,30*Td,2000) + x = np.cos(2*np.pi/Td*t)*np.exp(-alpha*t)+10; + fn, zeta_out, info = freqDampFromPeaks(x, t, plot=plot) + self.assertAlmostEqual(zeta , zeta_out,4) + self.assertAlmostEqual(1/T0 , fn ,2) + + if __name__ == '__main__': + import matplotlib.pyplot as plt + plt.show() + +if __name__ == '__main__': + unittest.main() +# import matplotlib.pyplot as plt +# import pydatview.io as weio +# df= weio.read('DampingExplodingExample2.csv').toDataFrame() +# M = df.values +# x= M[:,1] +# t= M[:,0] +# #for zeta in [-0.01, 0.1]: +# # T0 = 30 +# # Td = T0 / np.sqrt(1-zeta**2) +# # delta = 2*np.pi*zeta/np.sqrt(1-zeta**2) # logdec +# # alpha = delta/Td +# # x = np.cos(2*np.pi/Td*t)*np.exp(-alpha*t)+10; +# # df.insert(1,'PureDecay{}'.format(zeta), x) +# +# #df.to_csv('DECAY_Example.csv',index=False, sep=',') +# +# # Td = 10 +# # zeta = -0.01 # damping ratio (<1) +# # delta = 2*np.pi*zeta/np.sqrt(1-zeta**2) # logdec +# # alpha = delta/Td +# # t = np.linspace(0,30*Td,1000) +# # x = np.cos(2*np.pi/Td*t)*np.exp(-alpha*t)+10; +# # +# # fn, zeta, info = freqDampFromPeaks(x, t, plot=True, refPoint='mid') +# plt.show() + + + + diff --git a/pyFAST/tools/spectral.py b/pyFAST/tools/spectral.py new file mode 100644 index 0000000..e7d6857 --- /dev/null +++ b/pyFAST/tools/spectral.py @@ -0,0 +1,1442 @@ +# Tools for spectral analysis of a real valued signal. +# +# The functions in this file were adapted from the python package scipy according to the following license: +# +# License: +# Copyright 2001, 2002 Enthought, Inc. +# All rights reserved. +# +# Copyright 2003-2013 SciPy Developers. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: +# +# Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. +# Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. +# Neither the name of Enthought nor the names of the SciPy Developers may be used to endorse or promote products derived from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +import numpy as np +import pandas as pd +from six import string_types + +__all__ = ['fft_wrap','welch', 'psd', 'fft_amplitude'] +__all__ += ['pwelch', 'csd', 'coherence'] +__all__ += ['fnextpow2'] +__all__ += ['hann','hamming','boxcar','general_hamming','get_window'] +__all__ += ['TestSpectral'] + + +# --------------------------------------------------------------------------------} +# --- FFT wrap +# --------------------------------------------------------------------------------{ +def fft_wrap(t,y,dt=None, output_type='amplitude',averaging='None',averaging_window='hamming',detrend=False,nExp=None, nPerDecade=None): + """ + Wrapper to compute FFT amplitude or power spectra, with averaging. + INPUTS: + output_type : amplitude, PSD, f x PSD + averaging : None, Welch, Binning + averaging_window : Hamming, Hann, Rectangular + OUTPUTS: + frq: vector of frequencies + Y : Amplitude spectrum, PSD, or f * PSD + Info: a dictionary of info values + """ + + # Formatting inputs + output_type = output_type.lower() + averaging = averaging.lower() + averaging_window = averaging_window.lower() + t = np.asarray(t) + y = np.asarray(y) + n0 = len(y) + nt = len(t) + if len(t)!=len(y): + raise Exception('t and y should have the same length') + y = y[~np.isnan(y)] + n = len(y) + + if dt is None: + dtDelta0 = t[1]-t[0] + # Hack to use a constant dt + #dt = (np.max(t)-np.min(t))/(n0-1) + dt = (t[-1]-t[0])/(n0-1) + relDiff = abs(dtDelta0-dt)/dt*100 + #if dtDelta0 !=dt: + if relDiff>0.01: + print('[WARN] dt from tmax-tmin different from dt from t2-t1 {} {}'.format(dt, dtDelta0) ) + Fs = 1/dt + if averaging =='none': + frq, PSD, Info = psd(y, fs=Fs, detrend=detrend, return_onesided=True) + elif averaging =='binning': + frq, PSD, Info = psd_binned(y, fs=Fs, detrend=detrend, return_onesided=True, nPerDecade=nPerDecade) + elif averaging=='welch': + # --- Welch - PSD + #overlap_frac=0.5 + #return fnextpow2(np.sqrt(len(x)/(1-overlap_frac))) + nFFTAll=fnextpow2(n) + if nExp is None: + nExp=int(np.log(nFFTAll)/np.log(2))-1 + nPerSeg=2**nExp + if nPerSeg>n: + print('[WARN] Power of 2 value was too high and was reduced. Disable averaging to use the full spectrum.'); + nExp=int(np.log(nFFTAll)/np.log(2))-1 + nPerSeg=2**nExp + if averaging_window=='hamming': + window = hamming(nPerSeg, True)# True=Symmetric, like matlab + elif averaging_window=='hann': + window = hann(nPerSeg, True) + elif averaging_window=='rectangular': + window = boxcar(nPerSeg) + else: + raise Exception('Averaging window unknown {}'.format(averaging_window)) + frq, PSD, Info = pwelch(y, fs=Fs, window=window, detrend=detrend) + Info.nExp = nExp + else: + raise Exception('Averaging method unknown {}'.format(averaging)) + + # --- Formatting output + if output_type=='amplitude': + deltaf = frq[1]-frq[0] + Y = np.sqrt(PSD*2*deltaf) + # NOTE: the above should be the same as:Y=abs(Y[range(nhalf)])/n;Y[1:-1]=Y[1:-1]*2; + elif output_type=='psd': # one sided + Y = PSD + elif output_type=='f x psd': + Y = PSD*frq + else: + raise NotImplementedError('Contact developer') + if detrend: + frq= frq[1:] + Y = Y[1:] + return frq, Y, Info + + + +# --------------------------------------------------------------------------------} +# --- Spectral simple (averaging below) +# --------------------------------------------------------------------------------{ +def fft_amplitude(y, fs=1.0, detrend ='constant', return_onesided=True): + """ Returns FFT amplitude of signal """ + frq, PSD, Info = psd(y, fs=fs, detrend=detrend, return_onesided=return_onesided) + deltaf = frq[1]-frq[0] + Y = np.sqrt(PSD*2*deltaf) + return frq, Y, Info + + +def psd_binned(y, fs=1.0, nPerDecade=10, detrend ='constant', return_onesided=True): + """ + Return PSD binned with nPoints per decade + """ + # --- First return regular PSD + frq, PSD, Info = psd(y, fs=fs, detrend=detrend, return_onesided=return_onesided) + + add0=False + if frq[0]==0: + add0=True + f0 = 0 + PSD0 = PSD[0] + frq=frq[1:] + PSD=PSD[1:] + + # -- Then bin per decase + log_f = np.log10(frq) + ndecades = np.ceil(log_f[-1] -log_f[0]) + xbins = np.linspace(log_f[0], log_f[-1], int(ndecades*nPerDecade)) + + # Using Pandas to bin.. + df = pd.DataFrame(data=np.column_stack((log_f,PSD)), columns=['x','y']) + xmid = (xbins[:-1]+xbins[1:])/2 + df['Bin'] = pd.cut(df['x'], bins=xbins, labels=xmid ) # Adding a column that has bin attribute + df2 = df.groupby('Bin', observed=False).mean() # Average by bin + df2 = df2.reindex(xmid) + log_f_bin = df2['x'].values + PSD_bin = df2['y'].values + frq2= 10**log_f_bin + PSD2= PSD_bin + if add0: + frq2=np.concatenate( ([f0 ], frq2) ) + PSD2=np.concatenate( ([PSD0], PSD2) ) + b = ~np.isnan(frq2) + frq2 = frq2[b] + PSD2 = PSD2[b] + + #import matplotlib.pyplot as plt + #fig,ax = plt.subplots(1, 1, sharey=False, figsize=(6.4,4.8)) # (6.4,4.8) + #fig.subplots_adjust(left=0.12, right=0.95, top=0.95, bottom=0.11, hspace=0.20, wspace=0.20) + #ax.plot(log_f, PSD, label='') + #ax.plot(log_f_bin, PSD_bin, 'o', label='') + #for x in xbins: + # ax.axvline(x, ls=':', c=(0.5,0.5,0.5)) + #ax.set_xlabel('') + #ax.set_ylabel('') + #ax.legend() + #plt.show() + + #Info.df = frq[1]-frq[0] + #Info.fMax = frq[-1] + #Info.LFreq = len(frq) + #Info.LSeg = len(Y) + #Info.LWin = len(Y) + #Info.LOvlp = 0 + #Info.nFFT = len(Y) + #Info.nseg = 1 + Info.nPerDecade = nPerDecade + Info.xbins = xbins + + return frq2, PSD2, Info + + +def psd(y, fs=1.0, detrend ='constant', return_onesided=True): + """ Perform PSD without averaging """ + if not return_onesided: + raise NotImplementedError('Double sided todo') + + if detrend is None: + detrend=False + + if detrend=='constant' or detrend==True: + m=np.mean(y); + else: + m=0; + + n = len(y) + if n%2==0: + nhalf = int(n/2+1) + else: + nhalf = int((n+1)/2) + + frq = np.arange(nhalf)*fs/n; + Y = np.fft.rfft(y-m) #Y = np.fft.fft(y) + PSD = abs(Y[range(nhalf)])**2 /(n*fs) # PSD + PSD[1:-1] = PSD[1:-1]*2; + class InfoClass(): + pass + Info = InfoClass(); + Info.df = frq[1]-frq[0] + Info.fMax = frq[-1] + Info.LFreq = len(frq) + Info.LSeg = len(Y) + Info.LWin = len(Y) + Info.LOvlp = 0 + Info.nFFT = len(Y) + Info.nseg = 1 + return frq, PSD, Info + + +# --------------------------------------------------------------------------------} +# --- Windows +# --------------------------------------------------------------------------------{ +"""The suite of window functions.""" +def fnextpow2(x): + return 2**np.ceil( np.log(x)*0.99999999999/np.log(2)); + +def fDefaultWinLen(x,overlap_frac=0.5): + return fnextpow2(np.sqrt(len(x)/(1-overlap_frac))) + +def fDefaultWinLenMatlab(x): + return np.fix((len(x)-3)*2./9.) + +def _len_guards(M): + """Handle small or incorrect window lengths""" + if int(M) != M or M < 0: + raise ValueError('Window length M must be a non-negative integer') + return M <= 1 + +def _extend(M, sym): + """Extend window by 1 sample if needed for DFT-even symmetry""" + if not sym: + return M + 1, True + else: + return M, False + +def _truncate(w, needed): + """Truncate window by 1 sample if needed for DFT-even symmetry""" + if needed: + return w[:-1] + else: + return w + +def general_cosine(M, a, sym=True): + if _len_guards(M): + return np.ones(M) + M, needs_trunc = _extend(M, sym) + + fac = np.linspace(-np.pi, np.pi, M) + w = np.zeros(M) + for k in range(len(a)): + w += a[k] * np.cos(k * fac) + + return _truncate(w, needs_trunc) + + +def boxcar(M, sym=True): + """Return a boxcar or rectangular window. + + Also known as a rectangular window or Dirichlet window, this is equivalent + to no window at all. + """ + if _len_guards(M): + return np.ones(M) + M, needs_trunc = _extend(M, sym) + + w = np.ones(M, float) + + return _truncate(w, needs_trunc) + +def hann(M, sym=True): # same as hanning(*args, **kwargs): + return general_hamming(M, 0.5, sym) + + +def general_hamming(M, alpha, sym=True): + r"""Return a generalized Hamming window. + The generalized Hamming window is constructed by multiplying a rectangular + window by one period of a cosine function [1]_. + w(n) = \alpha - \left(1 - \alpha\right) \cos\left(\frac{2\pi{n}}{M-1}\right) + \qquad 0 \leq n \leq M-1 + """ + return general_cosine(M, [alpha, 1. - alpha], sym) + + +def hamming(M, sym=True): + r"""Return a Hamming window. + The Hamming window is a taper formed by using a raised cosine with + non-zero endpoints, optimized to minimize the nearest side lobe. + w(n) = 0.54 - 0.46 \cos\left(\frac{2\pi{n}}{M-1}\right) + \qquad 0 \leq n \leq M-1 + """ + return general_hamming(M, 0.54, sym) + +_win_equiv_raw = { + ('boxcar', 'box', 'ones', 'rect', 'rectangular'): (boxcar, False), + ('hamming', 'hamm', 'ham'): (hamming, False), + ('hanning', 'hann', 'han'): (hann, False), +} + +# Fill dict with all valid window name strings +_win_equiv = {} +for k, v in _win_equiv_raw.items(): + for key in k: + _win_equiv[key] = v[0] + +# Keep track of which windows need additional parameters +_needs_param = set() +for k, v in _win_equiv_raw.items(): + if v[1]: + _needs_param.update(k) + + +def get_window(window, Nx, fftbins=True): + """ + Return a window. + + Parameters + ---------- + window : string, float, or tuple + The type of window to create. See below for more details. + Nx : int + The number of samples in the window. + fftbins : bool, optional + If True (default), create a "periodic" window, ready to use with + `ifftshift` and be multiplied by the result of an FFT (see also + `fftpack.fftfreq`). + If False, create a "symmetric" window, for use in filter design. + """ + sym = not fftbins + try: + beta = float(window) + except (TypeError, ValueError): + args = () + if isinstance(window, tuple): + winstr = window[0] + if len(window) > 1: + args = window[1:] + elif isinstance(window, string_types): + if window in _needs_param: + raise ValueError("The '" + window + "' window needs one or " + "more parameters -- pass a tuple.") + else: + winstr = window + else: + raise ValueError("%s as window type is not supported." % + str(type(window))) + + try: + winfunc = _win_equiv[winstr] + except KeyError: + raise ValueError("Unknown window type.") + + params = (Nx,) + args + (sym,) + else: + winfunc = kaiser + params = (Nx, beta, sym) + + return winfunc(*params) + + + + + + +# --------------------------------------------------------------------------------} +# --- Helpers +# --------------------------------------------------------------------------------{ +def odd_ext(x, n, axis=-1): + """ + Odd extension at the boundaries of an array + Generate a new ndarray by making an odd extension of `x` along an axis. + """ + if n < 1: + return x + if n > x.shape[axis] - 1: + raise ValueError(("The extension length n (%d) is too big. " + + "It must not exceed x.shape[axis]-1, which is %d.") + % (n, x.shape[axis] - 1)) + left_end = axis_slice(x, start=0, stop=1, axis=axis) + left_ext = axis_slice(x, start=n, stop=0, step=-1, axis=axis) + right_end = axis_slice(x, start=-1, axis=axis) + right_ext = axis_slice(x, start=-2, stop=-(n + 2), step=-1, axis=axis) + ext = np.concatenate((2 * left_end - left_ext, + x, + 2 * right_end - right_ext), + axis=axis) + return ext + + +def even_ext(x, n, axis=-1): + """ + Even extension at the boundaries of an array + Generate a new ndarray by making an even extension of `x` along an axis. + """ + if n < 1: + return x + if n > x.shape[axis] - 1: + raise ValueError(("The extension length n (%d) is too big. " + + "It must not exceed x.shape[axis]-1, which is %d.") + % (n, x.shape[axis] - 1)) + left_ext = axis_slice(x, start=n, stop=0, step=-1, axis=axis) + right_ext = axis_slice(x, start=-2, stop=-(n + 2), step=-1, axis=axis) + ext = np.concatenate((left_ext, + x, + right_ext), + axis=axis) + return ext + + +def const_ext(x, n, axis=-1): + """ + Constant extension at the boundaries of an array + Generate a new ndarray that is a constant extension of `x` along an axis. + The extension repeats the values at the first and last element of + the axis. + """ + if n < 1: + return x + left_end = axis_slice(x, start=0, stop=1, axis=axis) + ones_shape = [1] * x.ndim + ones_shape[axis] = n + ones = np.ones(ones_shape, dtype=x.dtype) + left_ext = ones * left_end + right_end = axis_slice(x, start=-1, axis=axis) + right_ext = ones * right_end + ext = np.concatenate((left_ext, + x, + right_ext), + axis=axis) + return ext + + +def zero_ext(x, n, axis=-1): + """ + Zero padding at the boundaries of an array + Generate a new ndarray that is a zero padded extension of `x` along + an axis. + """ + if n < 1: + return x + zeros_shape = list(x.shape) + zeros_shape[axis] = n + zeros = np.zeros(zeros_shape, dtype=x.dtype) + ext = np.concatenate((zeros, x, zeros), axis=axis) + return ext + +def signaltools_detrend(data, axis=-1, type='linear', bp=0): + """ + Remove linear trend along axis from data. + + Parameters + ---------- + data : array_like + The input data. + axis : int, optional + The axis along which to detrend the data. By default this is the + last axis (-1). + type : {'linear', 'constant'}, optional + The type of detrending. If ``type == 'linear'`` (default), + the result of a linear least-squares fit to `data` is subtracted + from `data`. + If ``type == 'constant'``, only the mean of `data` is subtracted. + bp : array_like of ints, optional + A sequence of break points. If given, an individual linear fit is + performed for each part of `data` between two break points. + Break points are specified as indices into `data`. + + Returns + ------- + ret : ndarray + The detrended input data. + """ + if type not in ['linear', 'l', 'constant', 'c']: + raise ValueError("Trend type must be 'linear' or 'constant'.") + data = np.asarray(data) + dtype = data.dtype.char + if dtype not in 'dfDF': + dtype = 'd' + if type in ['constant', 'c']: + #print('Removing mean') + ret = data - np.expand_dims(np.mean(data, axis), axis) + return ret + else: + #print('Removing linear?') + dshape = data.shape + N = dshape[axis] + bp = sort(unique(r_[0, bp, N])) + if np.any(bp > N): + raise ValueError("Breakpoints must be less than length " + "of data along given axis.") + Nreg = len(bp) - 1 + # Restructure data so that axis is along first dimension and + # all other dimensions are collapsed into second dimension + rnk = len(dshape) + if axis < 0: + axis = axis + rnk + newdims = r_[axis, 0:axis, axis + 1:rnk] + newdata = reshape(np.transpose(data, tuple(newdims)), + (N, _prod(dshape) // N)) + newdata = newdata.copy() # make sure we have a copy + if newdata.dtype.char not in 'dfDF': + newdata = newdata.astype(dtype) + # Find leastsq fit and remove it for each piece + for m in range(Nreg): + Npts = bp[m + 1] - bp[m] + A = ones((Npts, 2), dtype) + A[:, 0] = cast[dtype](np.arange(1, Npts + 1) * 1.0 / Npts) + sl = slice(bp[m], bp[m + 1]) + coef, resids, rank, s = np.linalg.lstsq(A, newdata[sl]) + newdata[sl] = newdata[sl] - dot(A, coef) + # Put data back in original shape. + tdshape = take(dshape, newdims, 0) + ret = np.reshape(newdata, tuple(tdshape)) + vals = list(range(1, rnk)) + olddims = vals[:axis] + [0] + vals[axis:] + ret = np.transpose(ret, tuple(olddims)) + return ret + + + +# --------------------------------------------------------------------------------} +# --- Spectral Averaging +# --------------------------------------------------------------------------------{ +"""Tools for spectral analysis. """ + +def welch(x, fs=1.0, window='hann', nperseg=None, noverlap=None, nfft=None, + detrend='constant', return_onesided=True, scaling='density', + axis=-1): + """Interface identical to scipy.signal """ + + if detrend==True: + detrend='constant' + + freqs, Pxx = csd(x, x, fs, window, nperseg, noverlap, nfft, detrend, return_onesided, scaling, axis) + return freqs, Pxx.real + +#>>>> +def pwelch(x, window='hamming', noverlap=None, nfft=None, fs=1.0, nperseg=None, + detrend=False, return_onesided=True, scaling='density', + axis=-1): + r""" + NOTE: interface and default options modified to match matlab's implementation + >> detrend: default to False + >> window : default to 'hamming' + >> window: if an integer, use 'hamming(window, sym=True)' + + + Estimate power spectral density using Welch's method. + + Welch's method [1]_ computes an estimate of the power spectral + density by dividing the data into overlapping segments, computing a + modified periodogram for each segment and averaging the + periodograms. + + Parameters + ---------- + x : array_like + Time series of measurement values + fs : float, optional + Sampling frequency of the `x` time series. Defaults to 1.0. + window : str or tuple or array_like, optional + Desired window to use. If `window` is a string or tuple, it is + passed to `get_window` to generate the window values, which are + DFT-even by default. See `get_window` for a list of windows and + required parameters. If `window` is array_like it will be used + directly as the window and its length must be nperseg. Defaults + to a Hann window. + nperseg : int, optional + Length of each segment. Defaults to None, but if window is str or + tuple, is set to 256, and if window is array_like, is set to the + length of the window. + noverlap : int, optional + Number of points to overlap between segments. If `None`, + ``noverlap = nperseg // 2``. Defaults to `None`. + nfft : int, optional + Length of the FFT used, if a zero padded FFT is desired. If + `None`, the FFT length is `nperseg`. Defaults to `None`. + detrend : str or function or `False`, optional + Specifies how to detrend each segment. If `detrend` is a + string, it is passed as the `type` argument to the `detrend` + function. If it is a function, it takes a segment and returns a + detrended segment. If `detrend` is `False`, no detrending is + done. Defaults to 'constant'. + return_onesided : bool, optional + If `True`, return a one-sided spectrum for real data. If + `False` return a two-sided spectrum. Note that for complex + data, a two-sided spectrum is always returned. + scaling : { 'density', 'spectrum' }, optional + Selects between computing the power spectral density ('density') + where `Pxx` has units of V**2/Hz and computing the power + spectrum ('spectrum') where `Pxx` has units of V**2, if `x` + is measured in V and `fs` is measured in Hz. Defaults to + 'density' + axis : int, optional + Axis along which the periodogram is computed; the default is + over the last axis (i.e. ``axis=-1``). + + Returns + ------- + f : ndarray + Array of sample frequencies. + Pxx : ndarray + Power spectral density or power spectrum of x. + + See Also + -------- + periodogram: Simple, optionally modified periodogram + lombscargle: Lomb-Scargle periodogram for unevenly sampled data + + Notes + ----- + An appropriate amount of overlap will depend on the choice of window + and on your requirements. For the default Hann window an overlap of + 50% is a reasonable trade off between accurately estimating the + signal power, while not over counting any of the data. Narrower + windows may require a larger overlap. + + If `noverlap` is 0, this method is equivalent to Bartlett's method + [2]_. + + .. versionadded:: 0.12.0 + + References + ---------- + .. [1] P. Welch, "The use of the fast Fourier transform for the + estimation of power spectra: A method based on time averaging + over short, modified periodograms", IEEE Trans. Audio + Electroacoust. vol. 15, pp. 70-73, 1967. + .. [2] M.S. Bartlett, "Periodogram Analysis and Continuous Spectra", + Biometrika, vol. 37, pp. 1-16, 1950. + + """ + import math + def fnextpow2(x): + return 2**math.ceil( math.log(x)*0.99999999999/math.log(2)); + + # MANU >>> CHANGE OF DEFAULT OPTIONS + # MANU - If a length is provided use symmetric hamming window + if type(window)==int: + window=hamming(window, True) + # MANU - do not use 256 as default + if isinstance(window, string_types) or isinstance(window, tuple): + if nperseg is None: + if noverlap is None: + overlap_frac=0.5 + elif noverlap == 0: + overlap_frac=0 + else: + raise NotImplementedError('TODO noverlap set but not nperseg') + #nperseg = 256 # then change to default + nperseg=fnextpow2(math.sqrt(x.shape[-1]/(1-overlap_frac))); + + # MANU accepting true as detrend + if detrend==True: + detrend='constant' + + freqs, Pxx, Info = csd(x, x, fs, window, nperseg, noverlap, nfft, detrend, + return_onesided, scaling, axis, returnInfo=True) + + return freqs, Pxx.real, Info + + +def csd(x, y, fs=1.0, window='hann', nperseg=None, noverlap=None, nfft=None, + detrend='constant', return_onesided=True, scaling='density', axis=-1, + returnInfo=False + ): + r""" + Estimate the cross power spectral density, Pxy, using Welch's + method. + """ + + freqs, _, Pxy, Info = _spectral_helper(x, y, fs, window, nperseg, noverlap, nfft, + detrend, return_onesided, scaling, axis, + mode='psd') + + # Average over windows. + if len(Pxy.shape) >= 2 and Pxy.size > 0: + if Pxy.shape[-1] > 1: + Pxy = Pxy.mean(axis=-1) + else: + Pxy = np.reshape(Pxy, Pxy.shape[:-1]) + + if returnInfo: + return freqs, Pxy, Info + else: + return freqs, Pxy + + + +def coherence(x, y, fs=1.0, window='hann', nperseg=None, noverlap=None, + nfft=None, detrend='constant', axis=-1): + r""" + Estimate the magnitude squared coherence estimate, Cxy, of + discrete-time signals X and Y using Welch's method. + + ``Cxy = abs(Pxy)**2/(Pxx*Pyy)``, where `Pxx` and `Pyy` are power + spectral density estimates of X and Y, and `Pxy` is the cross + spectral density estimate of X and Y. + """ + + freqs, Pxx, Infoxx = welch(x, fs, window, nperseg, noverlap, nfft, detrend, axis=axis) + _, Pyy, Infoyy = welch(y, fs, window, nperseg, noverlap, nfft, detrend, axis=axis) + _, Pxy, Infoxy = csd(x, y, fs, window, nperseg, noverlap, nfft, detrend, axis=axis, returnInfo=True) + + Cxy = np.abs(Pxy)**2 / Pxx / Pyy + + return freqs, Cxy, Infoxx + + +def _spectral_helper(x, y, fs=1.0, window='hann', nperseg=None, noverlap=None, + nfft=None, detrend='constant', return_onesided=True, + scaling='spectrum', axis=-1, mode='psd', boundary=None, + padded=False): + """ Calculate various forms of windowed FFTs for PSD, CSD, etc. """ + if mode not in ['psd', 'stft']: + raise ValueError("Unknown value for mode %s, must be one of: " + "{'psd', 'stft'}" % mode) + + + + + + boundary_funcs = {'even': even_ext, + 'odd': odd_ext, + 'constant': const_ext, + 'zeros': zero_ext, + None: None} + + if boundary not in boundary_funcs: + raise ValueError("Unknown boundary option '{0}', must be one of: {1}" + .format(boundary, list(boundary_funcs.keys()))) + + # If x and y are the same object we can save ourselves some computation. + same_data = y is x + + if not same_data and mode != 'psd': + raise ValueError("x and y must be equal if mode is 'stft'") + + axis = int(axis) + + # Ensure we have np.arrays, get outdtype + x = np.asarray(x) + if not same_data: + y = np.asarray(y) + outdtype = np.result_type(x, y, np.complex64) + else: + outdtype = np.result_type(x, np.complex64) + + if not same_data: + # Check if we can broadcast the outer axes together + xouter = list(x.shape) + youter = list(y.shape) + xouter.pop(axis) + youter.pop(axis) + try: + outershape = np.broadcast(np.empty(xouter), np.empty(youter)).shape + except ValueError: + raise ValueError('x and y cannot be broadcast together.') + + if same_data: + if x.size == 0: + return np.empty(x.shape), np.empty(x.shape), np.empty(x.shape) + else: + if x.size == 0 or y.size == 0: + outshape = outershape + (min([x.shape[axis], y.shape[axis]]),) + emptyout = np.rollaxis(np.empty(outshape), -1, axis) + return emptyout, emptyout, emptyout + + if x.ndim > 1: + if axis != -1: + x = np.rollaxis(x, axis, len(x.shape)) + if not same_data and y.ndim > 1: + y = np.rollaxis(y, axis, len(y.shape)) + + # Check if x and y are the same length, zero-pad if necessary + if not same_data: + if x.shape[-1] != y.shape[-1]: + if x.shape[-1] < y.shape[-1]: + pad_shape = list(x.shape) + pad_shape[-1] = y.shape[-1] - x.shape[-1] + x = np.concatenate((x, np.zeros(pad_shape)), -1) + else: + pad_shape = list(y.shape) + pad_shape[-1] = x.shape[-1] - y.shape[-1] + y = np.concatenate((y, np.zeros(pad_shape)), -1) + + if nperseg is not None: # if specified by user + nperseg = int(nperseg) + if nperseg < 1: + raise ValueError('nperseg must be a positive integer') + + # parse window; if array like, then set nperseg = win.shape + win, nperseg = _triage_segments(window, nperseg,input_length=x.shape[-1]) + + if nfft is None: + nfft = nperseg + elif nfft < nperseg: + raise ValueError('nfft must be greater than or equal to nperseg.') + else: + nfft = int(nfft) + + if noverlap is None: + noverlap = nperseg//2 + else: + noverlap = int(noverlap) + if noverlap >= nperseg: + raise ValueError('noverlap must be less than nperseg.') + nstep = nperseg - noverlap + + # Padding occurs after boundary extension, so that the extended signal ends + # in zeros, instead of introducing an impulse at the end. + # I.e. if x = [..., 3, 2] + # extend then pad -> [..., 3, 2, 2, 3, 0, 0, 0] + # pad then extend -> [..., 3, 2, 0, 0, 0, 2, 3] + + if boundary is not None: + ext_func = boundary_funcs[boundary] + x = ext_func(x, nperseg//2, axis=-1) + if not same_data: + y = ext_func(y, nperseg//2, axis=-1) + + if padded: + # Pad to integer number of windowed segments + # I.e make x.shape[-1] = nperseg + (nseg-1)*nstep, with integer nseg + nadd = (-(x.shape[-1]-nperseg) % nstep) % nperseg + zeros_shape = list(x.shape[:-1]) + [nadd] + x = np.concatenate((x, np.zeros(zeros_shape)), axis=-1) + if not same_data: + zeros_shape = list(y.shape[:-1]) + [nadd] + y = np.concatenate((y, np.zeros(zeros_shape)), axis=-1) + + # Handle detrending and window functions + if not detrend: + def detrend_func(d): + return d + elif not hasattr(detrend, '__call__'): + def detrend_func(d): + return signaltools_detrend(d, type=detrend, axis=-1) + elif axis != -1: + # Wrap this function so that it receives a shape that it could + # reasonably expect to receive. + def detrend_func(d): + d = np.rollaxis(d, -1, axis) + d = detrend(d) + return np.rollaxis(d, axis, len(d.shape)) + else: + detrend_func = detrend + + if np.result_type(win,np.complex64) != outdtype: + win = win.astype(outdtype) + + if scaling == 'density': + scale = 1.0 / (fs * (win*win).sum()) + elif scaling == 'spectrum': + scale = 1.0 / win.sum()**2 + else: + raise ValueError('Unknown scaling: %r' % scaling) + + if mode == 'stft': + scale = np.sqrt(scale) + + if return_onesided: + if np.iscomplexobj(x): + sides = 'twosided' + #warnings.warn('Input data is complex, switching to ' 'return_onesided=False') + else: + sides = 'onesided' + if not same_data: + if np.iscomplexobj(y): + sides = 'twosided' + #warnings.warn('Input data is complex, switching to return_onesided=False') + else: + sides = 'twosided' + + if sides == 'twosided': + raise Exception('NOT IMPLEMENTED') + #freqs = fftpack.fftfreq(nfft, 1/fs) + elif sides == 'onesided': + freqs = np.fft.rfftfreq(nfft, 1/fs) + + # Perform the windowed FFTs + result = _fft_helper(x, win, detrend_func, nperseg, noverlap, nfft, sides) + + if not same_data: + # All the same operations on the y data + result_y = _fft_helper(y, win, detrend_func, nperseg, noverlap, nfft, + sides) + result = np.conjugate(result) * result_y + elif mode == 'psd': + result = np.conjugate(result) * result + + result *= scale + if sides == 'onesided' and mode == 'psd': + if nfft % 2: + result[..., 1:] *= 2 + else: + # Last point is unpaired Nyquist freq point, don't double + result[..., 1:-1] *= 2 + + time = np.arange(nperseg/2, x.shape[-1] - nperseg/2 + 1, + nperseg - noverlap)/float(fs) + if boundary is not None: + time -= (nperseg/2) / fs + + result = result.astype(outdtype) + + # All imaginary parts are zero anyways + if same_data and mode != 'stft': + result = result.real + + # Output is going to have new last axis for time/window index, so a + # negative axis index shifts down one + if axis < 0: + axis -= 1 + + # Roll frequency axis back to axis where the data came from + result = np.rollaxis(result, -1, axis) + + # TODO + class InfoClass(): + pass + Info = InfoClass(); + Info.df=freqs[1]-freqs[0] + Info.fMax=freqs[-1] + Info.LFreq=len(freqs) + Info.LSeg=nperseg + Info.LWin=len(win) + Info.LOvlp=noverlap + Info.nFFT=nfft + Info.nseg=-1 + #print('df:{:.3f} - fm:{:.2f} - nseg:{} - Lf:{:5d} - Lseg:{:5d} - Lwin:{:5d} - Lovlp:{:5d} - Nfft:{:5d} - Lsig:{}'.format(freqs[1]-freqs[0],freqs[-1],-1,len(freqs),nperseg,len(win),noverlap,nfft,x.shape[-1])) + return freqs, time, result, Info + + +def _fft_helper(x, win, detrend_func, nperseg, noverlap, nfft, sides): + """ Calculate windowed FFT """ + # Created strided array of data segments + if nperseg == 1 and noverlap == 0: + result = x[..., np.newaxis] + else: + # http://stackoverflow.com/a/5568169 + step = nperseg - noverlap + shape = x.shape[:-1]+((x.shape[-1]-noverlap)//step, nperseg) + strides = x.strides[:-1]+(step*x.strides[-1], x.strides[-1]) + result = np.lib.stride_tricks.as_strided(x, shape=shape, + strides=strides) + + # Detrend each data segment individually + result = detrend_func(result) + + # Apply window by multiplication + result = win * result + + # Perform the fft. Acts on last axis by default. Zero-pads automatically + if sides == 'twosided': + raise Exception('NOT IMPLEMENTED') + #func = fftpack.fft + else: + result = result.real + func = np.fft.rfft + result = func(result, n=nfft) + + return result + +def _triage_segments(window, nperseg,input_length): + """ + Parses window and nperseg arguments for spectrogram and _spectral_helper. + This is a helper function, not meant to be called externally. + """ + + #parse window; if array like, then set nperseg = win.shape + if isinstance(window, string_types) or isinstance(window, tuple): + # if nperseg not specified + if nperseg is None: + nperseg = 256 # then change to default + if nperseg > input_length: + print('nperseg = {0:d} is greater than input length ' + ' = {1:d}, using nperseg = {1:d}' + .format(nperseg, input_length)) + nperseg = input_length + win = get_window(window, nperseg) + else: + win = np.asarray(window) + if len(win.shape) != 1: + raise ValueError('window must be 1-D') + if input_length < win.shape[-1]: + raise ValueError('window is longer than input signal') + if nperseg is None: + nperseg = win.shape[0] + elif nperseg is not None: + if nperseg != win.shape[0]: + raise ValueError("value specified for nperseg is different from" + " length of window") + + return win, nperseg + + + + +# -------------------------------------------------------------------------------- +# --- Simple implementations to figure out the math +# -------------------------------------------------------------------------------- +def DFT(x, method='vectorized'): + """ + Calculate the Discrete Fourier Transform (DFT) of real signal x + + Definition: + for k in 0..N-1 (but defined for k in ZZ, see below for index) + + X_k = sum_n=0^{N-1} x_n e^{-i 2 pi k n /N} + + = sum_n=0^{N-1} x_n [ cos( 2 pi k n /N) - i sin( 2 pi k n /N) + + Xk are complex numbers. The amplitude/phase are: + A = |X_k|/N + phi = atan2(Im(Xk) / Re(Xk) + + Indices: + The DFT creates a periodic signal of period N + X[k] = X[k+N] + X[-k] = X[N-k] + + Therefore, any set of successive indices could be used. + For instance, with N=4: [0,1,2,3], [-1,0,1,2], [-2,-1,0,1] (canonical one) + [0,..N/2-1], [-N/2, ..N/2-1] + + If N is even + 0 is the 0th frequency (mean) + 1.....N/2-1 terms corresponds to positive frequencies + N/2.....N-1 terms corresponds to negative frequencies + If N is odd + 0 is the 0th frequency (mean) + 1.....(N-1)/2 terms corresponds to positive frequencies + (N+1)/2..N-1 terms corresponds to negative frequencies + + Frequencies convention: (see np.fft.fftfreq and DFT_freq) + f = [0, 1, ..., n/2-1, -n/2, ..., -1] / (dt*n) if n is even + f = [0, 1, ..., (n-1)/2, -(n-1)/2, ..., -1] / (dt*n) if n is odd + + NOTE: when n is even you could chose to go to +n/2 and start at -n/2+1 + The Python convention goes to n/2-1 and start at -n/2. + + Properties: + - if x is a real signal + X[-k] = X*[k] (=X[N-k]) + - Parseval's theorem: sum |x_n|^2 = sum |X_k|^2 (energy conservation) + + + """ + N = len(x) + + if method=='naive': + X = np.zeros_like(x, dtype=complex) + for k in np.arange(N): + for n in np.arange(N): + X[k] += x[n] * np.exp(-1j * 2*np.pi * k * n / N) + + elif method=='vectorized': + n = np.arange(N) + k = n.reshape((N, 1)) # k*n will be of shape (N x N) + e = np.exp(-2j * np.pi * k * n / N) + X = np.dot(e, x) + elif method=='fft': + X = np.fft.fft(x) + elif method=='fft_py': + X = recursive_fft(x) + else: + raise NotImplementedError() + + return X + +def IDFT(X, method='vectorized'): + """ + Calculate the Inverse Discrete Fourier Transform (IDFT) of complex coefficients X + + The transformation DFT->IDFT is fully reversible + + Definition: + for n in 0..N-1: + + x_n = 1/N sum_k=0^{N-1} X_k e^{i 2 pi k n/N} + = 1/N sum_k=0^{N-1} X_k [ cos( 2 pi k n/N) + i sin( 2 pi k n/N) + = 1/N sum_k=0^{N-1} A_k [ cos( 2 pi k n/N + phi_k) + i sin( 2 pi k n/N + phi_k) + + Xk are complex numbers, that can be written X[k] = A[k] e^{j phi[k]} therefore. + + Properties: + - if the "X" given as input come from a DFT, then the coefficients are periodic with period N + Therefore + X[-k] = X[N-k], + X[k] = X[N+k] + and therefore (see the discussion in the documentation of DFT), the summation from + k=0 to N-1 can be interpreted as a summation over any other indices set of length N. + - if "X" comes for the DFT of x, where x is a real signal, then: + X[-k] = X*[k] (=X[N-k]) + + - a converse is that, if X has conjugate symmetry (X[k]=X*[N-k]), then the IDFT will be real: + + x_n = 1/N sum_k=0^{N-1} A_k cos( 2 pi k n/N + phi_k) + 1/N sum_k={-(N-1)/2}^{(N-1)/2} A_k cos( 2 pi k n/N + phi_k) + + But remember that the A_k and phi_k need to satisfy the conjugate symmetry, so they are not + fully independent. + If we want x to be the sum over "N0" independent components, then we need to do the IDFT + of a spectrum "X" of length 2N0-1. + + Indices and frequency (python convention): + (see np.fft.fftfreq and DFT_freq) + f = [0, 1, ..., n/2-1, -n/2, ..., -1] / (dt*n) if n is even + f = [0, 1, ..., (n-1)/2, -(n-1)/2, ..., -1] / (dt*n) if n is odd + + When n is even, we lack symmetry of frequency, so it can potentially + make sense to enforce that the X[-n/2] component is 0 when generating + a signal with IDFT + + + + """ + N = len(X) + + if method in ['naive', 'manual', 'sum']: + x = np.zeros_like(X, dtype=complex) + for k in np.arange(N): + for n in np.arange(N): + x[k] += X[n] * np.exp(1j * 2*np.pi * k * n / N) + x = x/N + + elif method=='vectorized': + n = np.arange(N) + k = n.reshape((N, 1)) # k*n will be of shape (N x N) + e = np.exp(2j * np.pi * k * n / N) + x = np.dot(e, X) / N + + elif method=='ifft': + x = np.fft.ifft(X) + + #elif method=='ifft_py': + # x = IFFT(X) + else: + raise NotImplementedError('IDFT: Method {}'.format(method)) + + x = np.real_if_close(x) + + return x + +def DFT_freq(time=None, N=None, T=None, doublesided=True): + """ Returns the frequencies corresponding to a time vector `time`. + The signal "x" and "time" are assumed to have the same length + INPUTS: + - time: 1d array of time + OR + - N: number of time values + - T: time length of signal + """ + if time is not None: + N = len(time) + T = time[-1]-time[0] + dt = T/(N-1) + df = 1/(dt*N) + nhalf_pos, nhalf_neg = nhalf_fft(N) + if doublesided: + freq_pos = np.arange(nhalf_pos+1)*df + freq_neg = np.arange(nhalf_neg,0)*df + freq = np.concatenate((freq_pos, freq_neg)) + assert(len(freq) == N) + else: + # single sided + fMax = nhalf_pos * df + #freq = np.arange(0, fMax+df/2, df) + freq = np.arange(nhalf_pos+1)*df + return freq + +def IDFT_time(freq=None, doublesided=True): + """ Returns the time vector corresponding to a frequency vector `freq`. + + If doublesided is True + The signal "x" , "time" and freq are assumed to have the same length + Note: might lead to some inaccuracies, just use for double checking! + + INPUTS: + - freq: 1d array of time + """ + if doublesided: + N = len(freq) + time = freq*0 + if np.mod(N,2)==0: + nhalf=int(N/2)-1 + else: + nhalf=int((N-1)/2) + fMax = freq[nhalf] + df = (fMax-0)/(nhalf) + dt = 1/(df*N) + tMax= (N-1)*dt + #time = np.arange(0,(N-1)*dt+dt/2, dt) + time = np.linspace(0,tMax, N) + else: + raise NotImplementedError() + return time + +def recursive_fft(x): + """ + A recursive implementation of the 1D Cooley-Tukey FFT + + Returns the same as DFT (see documentation) + + Input should have a length of power of 2. + Reference: Kong, Siauw, Bayen - Python Numerical Methods + """ + N = len(x) + if not is_power_of_two(N): + raise Exception('Recursive FFT requires a power of 2') + + + if N == 1: + return x + else: + X_even = recursive_fft(x[::2]) + X_odd = recursive_fft(x[1::2]) + factor = np.exp(-2j*np.pi*np.arange(N)/ N) + X = np.concatenate([X_even+factor[:int(N/2)]*X_odd, X_even+factor[int(N/2):]*X_odd]) + return X + +def nhalf_fft(N): + """ + Follows the convention of fftfreq + fmax = f[nhalf_pos] = nhalf_pos*df (fftfreq convention) + + fpos = f[:nhalf_pos+1] + fneg = f[nhalf_pos+1:] + + """ + if N%2 ==0: + nhalf_pos = int(N/2)-1 + nhalf_neg = -int(N/2) + else: + nhalf_pos = int((N-1)/2) + nhalf_neg = -nhalf_pos + return nhalf_pos, nhalf_neg + + +def check_DFT_real(X): + """ Check that signal X is the DFT of a real signal + and that therefore IDFT(X) will return a real signal. + For this to be the case, we need conjugate symmetry: + X[k] = X[N-k]* + """ + from welib.tools.spectral import nhalf_fft + N = len(X) + nh, _ = nhalf_fft(N) + Xpos = X[1:nh+1] # we dont take the DC component [0] + Xneg = np.flipud(X[nh+1:]) # might contain one more frequency than the pos part + + if np.mod(N,2)==0: + # We have one extra negative frequency, we check that X is zero there and remove the value. + if Xneg[-1]!=0: + raise Exception('check_DFT_real: Component {} (first negative frequency) is {} instead of zero, but it should be zero if N is even.'.format(nh+1, X[nh+1])) + Xneg = Xneg[:-1] + + notConjugate = Xpos-np.conjugate(Xneg)!=0 + if np.any(notConjugate): + nNotConjugate=sum(notConjugate) + I = np.where(notConjugate)[0][:3] + 1 # +1 for DC component that was removed + raise Exception('check_DFT_real: {}/{} values of the spectrum are not complex conjugate of there symmetric frequency counterpart. See for instance indices: {}'.format(nNotConjugate, nh, I)) + +def double_sided_DFT_real(X1, N=None): + """ + Take a single sided part of a DFT (X1) and make it double sided signal X, of length N, + ensuring that the IDFT of X will be real. + This is done by ensuring conjugate symmetry: + X[k] = X[N-k]* + For N even, the first negative frequency component is set to 0 because it has no positive counterpart. + + Calling check_DFT_real(X) should return no Exception. + + INPUTS: + - X1: array of complex values of length N1 + - N: required length of the output array (2N1-1 or 2N1) + OUTPUTS: + - X: double sided spectrum: + [X1 flip(X1*[1:]) ] + or + [X1 [0] flip(X1*[1:]) ] + """ + if N is None: + N=2*len(X1)-1 # we make it an odd number to ensure symmetry of frequency + else: + if N not in [2*len(X1)-1, 2*len(X1), 2*len(X1)-2]: + raise Exception('N should be twice the length of the single sided spectrum, or one less.') + + if N % 2 ==0: + # Even number + if N == 2*len(X1)-2: + # rfftfreq + # TODO, there look into irfft to see the convention + X = np.concatenate((X1[:-1], [0], np.flipud(np.conjugate(X1[1:-1])))) + else: + X = np.concatenate((X1, [0], np.flipud(np.conjugate(X1[1:])))) + else: + X = np.concatenate((X1, np.flipud(np.conjugate(X1[1:])))) + return X + + +# --------------------------------------------------------------------------------} +# --- Helper functions +# --------------------------------------------------------------------------------{ +def is_power_of_two(n): + """ Uses bit manipulation to figure out if an integer is a power of two""" + return (n != 0) and (n & (n-1) == 0) + +def sinesum(time, As, freqs): + x =np.zeros_like(time) + for ai,fi in zip(As, freqs): + x += ai*np.sin(2*np.pi*fi*time) + return x + + +# --------------------------------------------------------------------------------} +# --- Unittests +# --------------------------------------------------------------------------------{ +import unittest + +class TestSpectral(unittest.TestCase): + + def default_signal(self, time, mean=0): + freqs=[1,4,7 ] # [Hz] + As =[3,1,1/2] # [misc] + x = sinesum(time, As, freqs) + mean + return x + + def compare_with_npfft(self, time, x): + # Compare lowlevels functions with npfft + # Useful to make sure the basic math is correct + N = len(time) + dt = (time[-1]-time[0])/(N-1) + tMax = time[-1] + + # --- Test frequency, dt/df/N-relationships + f_ref = np.fft.fftfreq(N, dt) + nhalf_pos, nhalf_neg = nhalf_fft(N) + fhalf = DFT_freq(time, doublesided = False) + freq = DFT_freq(time, doublesided = True) + df = freq[1]-freq[0] + fmax = fhalf[-1] + + np.testing.assert_almost_equal(fhalf , f_ref[:nhalf_pos+1], 10) + np.testing.assert_almost_equal(fhalf[-1] , np.max(f_ref), 10) + np.testing.assert_almost_equal( 1/(dt*df), N) + np.testing.assert_almost_equal(freq , f_ref, 10) + if N%2 == 0: + np.testing.assert_almost_equal(2*fmax/df, N-2 , 10) + else: + np.testing.assert_almost_equal(2*fmax/df, N-1 , 10) + + # --- Test DFT methods + X0 = DFT(x, method='fft') + X1 = DFT(x, method='naive') + X2 = DFT(x, method='vectorized') + + np.testing.assert_almost_equal(X1, X0, 10) + np.testing.assert_almost_equal(X2, X0, 10) + if is_power_of_two(N): + X3 = DFT(x, method='fft_py') + np.testing.assert_almost_equal(X3, X0, 10) + + # --- Test IDFT methods + x_back0 = IDFT(X0, method='ifft') + x_back1 = IDFT(X0, method='naive') + x_back2 = IDFT(X0, method='vectorized') + np.testing.assert_almost_equal(x_back1, x_back0, 10) + np.testing.assert_almost_equal(x_back2, x_back0, 10) + + np.testing.assert_almost_equal(x_back0, x, 10) + + def test_lowlevel_fft_even(self): + # Test lowlevel functions + time = np.linspace(0,10,16) # NOTE: need a power of two for fft_py + x = self.default_signal(time, mean=0) + self.compare_with_npfft(time, x) + + def test_lowlevel_fft_odd(self): + # Test lowlevel functions + time = np.linspace(0,10,17) + x = self.default_signal(time, mean=0) + self.compare_with_npfft(time, x) + + def test_fft_amplitude(self): + dt=0.1 + t=np.arange(0,10,dt); + f0=1; + A=5; + y=A*np.sin(2*np.pi*f0*t) + f,Y,_=fft_amplitude(y,fs=1/dt,detrend=False) + i=np.argmax(Y) + self.assertAlmostEqual(Y[i],A) + self.assertAlmostEqual(f[i],f0) + + def test_fft_binning(self): + dt=0.1 + t=np.arange(0,10,dt); + f0=1; + A=5; + y=A*np.sin(2*np.pi*f0*t) + + f, Y, Info = psd_binned(y, fs=1/dt, nPerDecade=10, detrend ='constant') + f2, Y2, Info2 = psd (y, fs=1/dt, detrend ='constant') + #print(f) + #print(Y) + + #import matplotlib.pyplot as plt + #fig,ax = plt.subplots(1, 1, sharey=False, figsize=(6.4,4.8)) # (6.4,4.8) + #fig.subplots_adjust(left=0.12, right=0.95, top=0.95, bottom=0.11, hspace=0.20, wspace=0.20) + #ax.plot( f2, Y2 , label='Full') + #ax.plot( f, Y , label='Binned') + #ax.set_xlabel('') + #ax.set_ylabel('') + #ax.legend() + #plt.show() + +if __name__ == '__main__': + #TestSpectral().test_fft_binning() + #TestSpectral().test_ifft() + #TestSpectral().test_lowlevel_fft_even() + #TestSpectral().test_lowlevel_fft_odd() + unittest.main() + diff --git a/pyFAST/tools/stats.py b/pyFAST/tools/stats.py new file mode 100644 index 0000000..ec9cc17 --- /dev/null +++ b/pyFAST/tools/stats.py @@ -0,0 +1,384 @@ +""" +Set of tools for statistics + - measures (R^2, RMSE) + - pdf distributions + - Binning + +""" +import numpy as np +import pandas as pd + +# --------------------------------------------------------------------------------} +# --- Stats measures +# --------------------------------------------------------------------------------{ +def comparison_stats(t1, y1, t2, y2, stats='sigRatio,eps,R2', method='mean', absVal=True): + """ + y1: ref + y2: other + + """ + from welib.tools.fatigue import equivalent_load + + sp=stats.split(',') + stats = {} + sStats=[] + + t1=np.asarray(t1).astype(float) + y1=np.asarray(y1).astype(float) + t2=np.asarray(t2).astype(float) + y2=np.asarray(y2).astype(float) + + # Loop on statistics requested + for s in sp: + s= s.strip().lower() + if s=='sigratio': + # Ratio of standard deviation: + sig_ref = float(np.nanstd(y1)) + sig_est = float(np.nanstd(y2)) + try: + r_sig = sig_est/sig_ref + except: + r_sig = np.nan + stats = {'sigRatio':r_sig} + sStats+= [r'$\sigma_\mathrm{est}/\sigma_\mathrm{ref} = $'+r'{:.3f}'.format(r_sig)] + + elif s=='eps': + # Mean relative error + eps = float(mean_rel_err(t1, y1, t2, y2, method=method, absVal=absVal)) + stats['eps'] = eps + sStats+=['$\epsilon=$'+r'{:.1f}%'.format(eps)] + + elif s=='r2': + # Rsquare + R2 = float(rsquare(y2, y1)[0]) + stats['R2'] = R2 + sStats+=[r'$R^2=$'+r'{:.3f}'.format(R2)] + + elif s=='epsleq': + Leq1 = equivalent_load(t1, y1, m=5, bins=100, method='fatpack') + Leq2 = equivalent_load(t2, y2, m=5, bins=100, method='fatpack') + epsLeq = (Leq2-Leq1)/Leq1*100 + stats['epsLeq'] = epsLeq + sStats+=[r'$\epsilon L_{eq}=$'+r'{:.1f}%'.format(epsLeq)] + + else: + raise NotImplementedError(s) + sStats=' - '.join(sStats) + return stats, sStats + + + +def rsquare(y, f, c = True): + """ Compute coefficient of determination of data fit model and RMSE + [r2 rmse] = rsquare(y,f) + [r2 rmse] = rsquare(y,f,c) + RSQUARE computes the coefficient of determination (R-square) value from + actual data Y and model data F. The code uses a general version of + R-square, based on comparing the variability of the estimation errors + with the variability of the original values. RSQUARE also outputs the + root mean squared error (RMSE) for the user's convenience. + Note: RSQUARE ignores comparisons involving NaN values. + INPUTS + Y : Actual data + F : Model fit + + # OPTION + C : Constant term in model + R-square may be a questionable measure of fit when no + constant term is included in the model. + [DEFAULT] TRUE : Use traditional R-square computation + FALSE : Uses alternate R-square computation for model + without constant term [R2 = 1 - NORM(Y-F)/NORM(Y)] + # OUTPUT + R2 : Coefficient of determination + RMSE : Root mean squared error """ + # Sanity + if not np.all(y.shape == f.shape) : + raise Exception('Y and F must be the same size') + y = np.asarray(y).astype(float) + f = np.asarray(f).astype(float) + # Check for NaN + tmp = np.logical_not(np.logical_or(np.isnan(y),np.isnan(f))) + y = y[tmp] + f = f[tmp] + if c: + r2 = max(0,1-np.sum((y-f)**2)/np.sum((y-np.mean(y))** 2)) + else: + r2 = 1 - np.sum((y - f) ** 2) / np.sum((y) ** 2) + if r2 < 0: + import warnings + warnings.warn('Consider adding a constant term to your model') + r2 = 0 + rmse = np.sqrt(np.mean((y - f) ** 2)) + return r2,rmse + +def mean_rel_err(t1=None, y1=None, t2=None, y2=None, method='meanabs', verbose=False, varname='', absVal=True): + """ + return mean relative error in % + + Methods: + 'mean' : 100 * |y1-y2|/mean(y1) + 'meanabs': 100 * |y1-y2|/mean(|y1|) + 'minmax': y1 and y2 scaled between 0.5 and 1.5 + |y1s-y2s|/|y1| + '0-2': signals are scalled between 0 & 2 + """ + def myabs(y): + if absVal: + return np.abs(y) + else: + return y + + + if t1 is None and t2 is None: + pass + else: + if len(y1)!=len(y2): + y2=np.interp(t1,t2,y2) + if method=='mean': + # Method 1 relative to mean + ref_val = np.nanmean(y1) + meanrelerr = np.nanmean(myabs(y2-y1)/ref_val)*100 + elif method=='meanabs': + ref_val = np.nanmean(abs(y1)) + meanrelerr = np.nanmean(myabs(y2-y1)/ref_val)*100 + elif method=='loc': + meanrelerr = np.nanmean(myabs(y2-y1)/abs(y1))*100 + elif method=='minmax': + # Method 2 scaling signals + Min=min(np.nanmin(y1), np.nanmin(y2)) + Max=max(np.nanmax(y1), np.nanmax(y2)) + y1=(y1-Min)/(Max-Min)+0.5 + y2=(y2-Min)/(Max-Min)+0.5 + meanrelerr = np.nanmean(myabs(y2-y1)/np.abs(y1))*100 + elif method=='1-2': + # transform values from 1 to 2 + Min=min(np.nanmin(y1), np.nanmin(y2)) + Max=max(np.nanmax(y1), np.nanmax(y2)) + y1 = (y1-Min)/(Max-Min)+1 + y2 = (y2-Min)/(Max-Min)+1 + meanrelerr = np.nanmean(myabs(y2-y1)/np.abs(y1))*100 + else: + raise Exception('Unknown method',method) + + if verbose: + if len(varname)>0: + print('Mean rel error {:15s} {:7.2f} %'.format(varname, meanrelerr)) + else: + print('Mean rel error {:7.2f} %'.format( meanrelerr)) + return meanrelerr + + +# --------------------------------------------------------------------------------} +# --- PDF +# --------------------------------------------------------------------------------{ +def pdf(y, method='histogram', n=50, **kwargs): + """ + Compute the probability density function. + Wrapper over the different methods present in this package + """ + if method =='sns': + xh, yh = pdf_sns(y, nBins=n, **kwargs) + elif method =='gaussian_kde': + xh, yh = pdf_gaussian_kde(y, nOut=n, **kwargs) + elif method =='histogram': + xh, yh = pdf_histogram(y, nBins=n, **kwargs) + else: + raise NotImplementedError(f'pdf method: {method}') + return xh, yh + + +def pdf_histogram(y,nBins=50, norm=True, count=False): + yh, xh = np.histogram(y[~np.isnan(y)], bins=nBins) + dx = xh[1] - xh[0] + xh = xh[:-1] + dx/2 + if count: + yh = yh / (len(n)*dx) # TODO DEBUG /VERIFY THIS + else: + yh = yh / (nBins*dx) + if norm: + yh=yh/np.trapz(yh,xh) + return xh,yh + +def pdf_gaussian_kde(data, bw='scott', nOut=100, cut=3, clip=(-np.inf,np.inf)): + """ + Returns a smooth probability density function (univariate kernel density estimate - kde) + Inspired from `_univariate_kdeplot` from `seaborn.distributions` + + INPUTS: + bw: float defining bandwidth or method (string) to find it (more or less sigma) + cut: number of bandwidth kept for x axis (e.g. 3 sigmas) + clip: (xmin, xmax) values + OUTPUTS: + x, y: where y(x) = pdf(data) + """ + from scipy import stats + from six import string_types + + data = np.asarray(data) + data = data[~np.isnan(data)] + # Gaussian kde + kde = stats.gaussian_kde(data, bw_method = bw) + # Finding a relevant support (i.e. x values) + if isinstance(bw, string_types): + bw_ = "scotts" if bw == "scott" else bw + bw = getattr(kde, "%s_factor" % bw_)() * np.std(data) + x_min = max(data.min() - bw * cut, clip[0]) + x_max = min(data.max() + bw * cut, clip[1]) + x = np.linspace(x_min, x_max, nOut) + # Computing kde on support + y = kde(x) + return x, y + + +def pdf_sklearn(y): + #from sklearn.neighbors import KernelDensity + #kde = KernelDensity(kernel='gaussian', bandwidth=0.75).fit(y) #you can supply a bandwidth + #x=np.linspace(0,5,100)[:, np.newaxis] + #log_density_values=kde.score_samples(x) + #density=np.exp(log_density) + pass + +def pdf_sns(y,nBins=50): + import seaborn.apionly as sns + hh=sns.distplot(y,hist=True,norm_hist=False).get_lines()[0].get_data() + xh=hh[0] + yh=hh[1] + return xh,yh + +# --------------------------------------------------------------------------------} +# --- Binning +# --------------------------------------------------------------------------------{ +def bin_DF(df, xbins, colBin, stats='mean'): + """ + Perform bin averaging of a dataframe + INPUTS: + - df : pandas dataframe + - xBins: end points delimiting the bins, array of ascending x values + - colBin: column name (string) of the dataframe, used for binning + OUTPUTS: + binned dataframe, with additional columns 'Counts' for the number + + """ + if colBin not in df.columns.values: + raise Exception('The column `{}` does not appear to be in the dataframe'.format(colBin)) + xmid = (xbins[:-1]+xbins[1:])/2 + df['Bin'] = pd.cut(df[colBin], bins=xbins, labels=xmid ) # Adding a column that has bin attribute + if stats=='mean': + df2 = df.groupby('Bin', observed=False).mean() # Average by bin + elif stats=='std': + df2 = df.groupby('Bin', observed=False).std() # std by bin + # also counting + df['Counts'] = 1 + dfCount=df[['Counts','Bin']].groupby('Bin', observed=False).sum() + df2['Counts'] = dfCount['Counts'] + # Just in case some bins are missing (will be nan) + df2 = df2.reindex(xmid) + return df2 + +def bin_signal(x, y, xbins=None, stats='mean', nBins=None): + """ + Perform bin averaging of a signal + INPUTS: + - x: x-values + - y: y-values, signal values + - xBins: end points delimiting the bins, array of ascending x values + OUTPUTS: + - xBinned, yBinned + + """ + if xbins is None: + xmin, xmax = np.min(x), np.max(x) + dx = (xmax-xmin)/nBins + xbins=np.arange(xmin, xmax+dx/2, dx) + df = pd.DataFrame(data=np.column_stack((x,y)), columns=['x','y']) + df2 = bin_DF(df, xbins, colBin='x', stats=stats) + return df2['x'].values, df2['y'].values + + + +def bin2d_signal(x, y, z, xbins=None, ybins=None, nXBins=None, nYBins=None): + """ + Bin signal z based on x and y values using xbins and ybins + + """ + if xbins is None: + xmin, xmax = np.min(x), np.max(x) + dx = (xmax-xmin)/nXBins + xbins=np.arange(xmin, xmax+dx/2, dx) + if ybins is None: + ymin, ymax = np.min(y), np.max(y) + dy = (ymax-ymin)/nYBins + ybins=np.arange(ymin, ymax+dy/2, dy) + + x = np.asarray(x).flatten() + y = np.asarray(y).flatten() + z = np.asarray(z).flatten() + + Counts = np.zeros((len(xbins)-1, len(ybins)-1)) + XMean = np.zeros((len(xbins)-1, len(ybins)-1))*np.nan + YMean = np.zeros((len(xbins)-1, len(ybins)-1))*np.nan + ZMean = np.zeros((len(xbins)-1, len(ybins)-1))*np.nan + ZStd = np.zeros((len(xbins)-1, len(ybins)-1))*np.nan + + xmid = xbins[:-1] + np.diff(xbins)/2 + ymid = ybins[:-1] + np.diff(ybins)/2 + YMid, XMid = np.meshgrid(ymid, xmid) + + for ixb, xb in enumerate(xbins[:-1]): + print(ixb) + bX = np.logical_and(x >= xb, x <= xbins[ixb+1]) # TODO decide on bounds + for iyb, yb in enumerate(ybins[:-1]): + bY = np.logical_and(y >= yb, y <= ybins[iyb+1]) # TODO decide on bounds + + bXY = np.logical_and(bX, bY) + Counts[ixb, iyb] = sum(bXY) + if Counts[ixb,iyb]>0: + ZMean [ixb, iyb] = np.mean(z[bXY]) + ZStd [ixb, iyb] = np.std( z[bXY]) + XMean [ixb, iyb] = np.mean(x[bXY]) + YMean [ixb, iyb] = np.mean(y[bXY]) + + return XMean, YMean, ZMean, ZStd, Counts, XMid, YMid + + + + + + +def azimuthal_average_DF(df, psiBin=np.arange(0,360+1,10), colPsi='Azimuth_[deg]', tStart=None, colTime='Time_[s]'): + """ + Average a dataframe based on azimuthal value + Returns a dataframe with same amount of columns as input, and azimuthal values as index + """ + if tStart is not None: + if colTime not in df.columns.values: + raise Exception('The column `{}` does not appear to be in the dataframe'.format(colTime)) + df=df[ df[colTime]>tStart].copy() + + dfPsi= bin_DF(df, psiBin, colPsi, stats='mean') + if np.any(dfPsi['Counts']<1): + print('[WARN] some bins have no data! Increase the bin size.') + + return dfPsi + + +def azimuthal_std_DF(df, psiBin=np.arange(0,360+1,10), colPsi='Azimuth_[deg]', tStart=None, colTime='Time_[s]'): + """ + Average a dataframe based on azimuthal value + Returns a dataframe with same amount of columns as input, and azimuthal values as index + """ + if tStart is not None: + if colTime not in df.columns.values: + raise Exception('The column `{}` does not appear to be in the dataframe'.format(colTime)) + df=df[ df[colTime]>tStart].copy() + + dfPsi= bin_DF(df, psiBin, colPsi, stats='std') + if np.any(dfPsi['Counts']<1): + print('[WARN] some bins have no data! Increase the bin size.') + + return dfPsi + + + + From c229ad5ebc46e396e76072453434ebafcc8a88c0 Mon Sep 17 00:00:00 2001 From: Emmanuel Branlard Date: Fri, 20 Oct 2023 13:50:16 -0600 Subject: [PATCH 120/124] IO: adding tests for bladed and flex files --- pyFAST/input_output/tests/test_bladed.py | 127 +++++++++++++++++++++++ pyFAST/input_output/tests/test_flex.py | 59 +++++++++++ 2 files changed, 186 insertions(+) create mode 100644 pyFAST/input_output/tests/test_bladed.py create mode 100644 pyFAST/input_output/tests/test_flex.py diff --git a/pyFAST/input_output/tests/test_bladed.py b/pyFAST/input_output/tests/test_bladed.py new file mode 100644 index 0000000..3f56ef4 --- /dev/null +++ b/pyFAST/input_output/tests/test_bladed.py @@ -0,0 +1,127 @@ +import os +import numpy as np +import re +import pandas as pd +import unittest +from .helpers_for_test import MyDir, reading_test + +from pyFAST.input_output.bladed_out_file import BladedFile + + + +class Test(unittest.TestCase): + + def test_001_read_all(self): + reading_test('Bladed_out_*.*', BladedFile) + + + def test_Bladed(self): + ## check for binary + F = BladedFile(os.path.join(MyDir,'Bladed_out_binary.$41')) + #F = BladedFile(os.path.join(MyDir,'Bladed_out_binary.$41')) + DF = F.toDataFrame() + self.assertAlmostEqual(DF['0.0m-Blade 1 Fx (Root axes) [N]'].values[0],146245.984375) + self.assertAlmostEqual(DF['0.0m-Blade 1 Fx (Root axes) [N]'].values[-1],156967.484375) + + ## check for ASCII + F = BladedFile(os.path.join(MyDir,'Bladed_out_ascii.$41')) + DF = F.toDataFrame() + self.assertAlmostEqual(DF['0.0m-Blade 1 Fx (Root axes) [N]'].values[0],146363.8) + self.assertAlmostEqual(DF['0.0m-Blade 1 Fx (Root axes) [N]'].values[-1],156967.22) + + def test_Bladed_case2_project(self): + F = BladedFile(os.path.join(MyDir,'Bladed_out_binary_case2.$PJ')) + DF = F.toDataFrame() + #print(DFS.keys()) + #DF=DFS['Misc'] + + #print(DF.shape) + #print(DF.columns) + #print(DF.columns[0]) + #print(DF.columns[50]) + self.assertEqual(DF.shape, (10, 89)) + self.assertEqual(DF.columns[0] , 'Time [s]') + self.assertEqual(DF.columns[1] , 'Time from start of simulation [s]') + self.assertEqual(DF.columns[27] , '26.41m-DPMOM1 [Nm/m]') + self.assertEqual(DF.columns[69], '38.75m-Blade 1 y-position [m]') + self.assertEqual(DF.columns[88], 'Foundation Fz [N]') + self.assertAlmostEqual(DF['Time from start of simulation [s]'][0] , 7.0 ) + self.assertAlmostEqual(DF['26.41m-DPMOM1 [Nm/m]'][0] , -226.85083, 5 ) + self.assertAlmostEqual(DF['38.75m-Blade 1 y-position [m]'].values[0], -27.949090957, 5 ) + self.assertAlmostEqual(DF['38.75m-Blade 1 y-position [m]'].values[-1], -39.96076965, 5 ) + self.assertAlmostEqual(DF['Foundation Fz [N]'][0] , -1092165.5 ) + self.assertAlmostEqual(DF['Foundation Fz [N]'].values[-1] , -1093664.75 ) + + self.assertFalse(DF.isnull().values.any()) + + def test_Bladed_case2_indiv(self): + F = BladedFile(os.path.join(MyDir,'Bladed_out_binary_case2.$12')) + DF=F.toDataFrame() + self.assertEqual(DF.shape, (10, 14)) + self.assertEqual(DF.columns[0] , 'Time [s]') + self.assertEqual(DF.columns[1] , 'POW2 [W]') + self.assertAlmostEqual(DF['POW2 [W]'].values[-1] , 1940463.0) + + F = BladedFile(os.path.join(MyDir,'Bladed_out_binary_case2.$25')) + DF=F.toDataFrame() + self.assertEqual(DF.shape, (10, 17)) + self.assertEqual(DF.columns[0] , 'Time [s]') + self.assertEqual(DF.columns[1] , '-15.0m-MXT [Nm]') + self.assertAlmostEqual(DF['-15.0m-MXT [Nm]'].values[-1], 1587526.625) + + F = BladedFile(os.path.join(MyDir,'Bladed_out_binary_case2.$69')) + DF=F.toDataFrame() + self.assertEqual(DF.shape, (10, 7)) + self.assertEqual(DF.columns[0] , 'Time [s]') + self.assertEqual(DF.columns[1] , 'Foundation Mx [Nm]') + self.assertAlmostEqual(DF['Foundation Mx [Nm]'].values[-1], 1587236.375) + + + F = BladedFile(os.path.join(MyDir,'Bladed_out_binary_case2.$37')) + DF=F.toDataFrame() + self.assertEqual(DF.shape, (522, 6)) + self.assertEqual(DF.columns[0] , 'Time [s]') + self.assertEqual(DF.columns[1] , 'Simulation Time [s]') + self.assertAlmostEqual(DF['State with largest error [N]'].values[-1], 9.0) + + # NOTE: this binary file is detected as ascii, and the reading fails.. + F = BladedFile(os.path.join(MyDir,'Bladed_out_binary_case2_fail.$55')) + DF=F.toDataFrame() + self.assertEqual(DF.shape, (50, 1)) + self.assertEqual(DF.columns[0] , 'Step size histogram [N]') + #self.assertTrue(np.isnan(DF['Step size histogram [N]'].values[-1])) + self.assertEqual(DF['Step size histogram [N]'].values[-1],0.0) + + # NOTE: this one is properly dected as binary + F = BladedFile(os.path.join(MyDir,'Bladed_out_binary_case2.$55')) + DF=F.toDataFrame() + self.assertEqual(DF.shape, (50, 1)) + self.assertEqual(DF.columns[0] , 'Step size histogram [N]') + self.assertEqual(DF['Step size histogram [N]'].values[-1], 0) + + F = BladedFile(os.path.join(MyDir,'Bladed_out_binary_case2.$46')) + DF=F.toDataFrame() + self.assertEqual(DF.shape, (10, 13)) + self.assertEqual(DF.columns[1] , 'Node 1-Water particle velocity in X direction [m/s]') + self.assertEqual(DF['Node 1-Water particle velocity in X direction [m/s]'].values[-1],-0.25) + + F = BladedFile(os.path.join(MyDir,'Bladed_out_binary_case2.$06')) + DF=F.toDataFrame() + self.assertEqual(DF.shape, (10, 5)) + self.assertEqual(DF.columns[1] , 'Generator torque [Nm]') + self.assertEqual(DF['Generator torque [Nm]'].values[-1],12852.1953125) + + F = BladedFile(os.path.join(MyDir,'Bladed_out_binary_case2.$23')) + DF=F.toDataFrame() + self.assertEqual(DF.shape, (10, 9)) + self.assertEqual(DF.columns[1] , 'Stationary hub Mx [Nm]') + self.assertEqual(DF['Stationary hub Mx [Nm]'].values[-1],1112279.375) + + +if __name__ == '__main__': + unittest.main() + #Test().test_001_read_all() + #Test().test_Bladed() + #Test().test_Bladed_case2_project() + #Test().test_Bladed_case2_indiv() + diff --git a/pyFAST/input_output/tests/test_flex.py b/pyFAST/input_output/tests/test_flex.py new file mode 100644 index 0000000..6a31e27 --- /dev/null +++ b/pyFAST/input_output/tests/test_flex.py @@ -0,0 +1,59 @@ +import unittest +import os +import numpy as np +from pyFAST.input_output.tests.helpers_for_test import MyDir, reading_test +from pyFAST.input_output.flex_profile_file import FLEXProfileFile +from pyFAST.input_output.flex_blade_file import FLEXBladeFile +from pyFAST.input_output.flex_wavekin_file import FLEXWaveKinFile +from pyFAST.input_output.flex_doc_file import FLEXDocFile + + +import pandas as pd + +class Test(unittest.TestCase): + + #def test_001_read_all(self, DEBUG=True): + # reading_test('FLEX*.*', weio.read) + + def DF(self,FN): + """ Reads a file with weio and return a dataframe """ + pass + #return Flex(os.path.join(MyDir,FN)).toDataFrame() + + def test_FLEXProfiles(self): + df = FLEXProfileFile(os.path.join(MyDir,'FLEXProfile.pro')).toDataFrame() + self.assertAlmostEqual(df['pc_set_2_t_57.0'].values[2,2],0.22711022) + + def test_FLEXBlade(self): + Bld=FLEXBladeFile(os.path.join(MyDir,'FLEXBlade002.bld')).toDataFrame() + self.assertAlmostEqual(Bld['r_[m]'].values[-1],61.5) + self.assertAlmostEqual(Bld['Mass_[kg/m]'].values[-1],10.9) + self.assertAlmostEqual(Bld['Chord_[m]'].values[3],3.979815059) + + def test_FLEXWaves(self): + wk = FLEXWaveKinFile(os.path.join(MyDir, 'FLEXWaveKin.wko')) + self.assertEqual(wk['MaxLongiVel'],2.064) + self.assertEqual(wk['Tp'] ,12.54) + self.assertEqual(len(wk['RelDepth']),12) + self.assertEqual(wk['data']['Time_[s]'].values[-1],3.0) + self.assertEqual(wk['data']['a_z=20.0_x=0.0_[m/s^2]'].values[-1],0.06) + + def test_FLEXDoc(self): + doc = FLEXDocFile(os.path.join(MyDir, 'FLEXDocFile.out')) + self.assertAlmostEqual(doc['RNA']['Mass'], 2.85e-6) + self.assertAlmostEqual(doc['Tower']['Length'], 1.0) + self.assertAlmostEqual(doc['Tower']['SectionData'].shape[0], 11) + self.assertAlmostEqual(doc['Tower']['SectionData'].shape[1], 9) + self.assertAlmostEqual(doc['Tower']['ShapeFunction_DOF1_Shape'].shape[0], 12) + self.assertAlmostEqual(doc['Foundation']['Mass'], 900000) + self.assertAlmostEqual(doc['Foundation']['ShapeFunction_DOF1_Shape'].shape[0], 101) + self.assertAlmostEqual(doc['Foundation']['ShapeFunction_DOF1_Shape']['H_[m]'].values[-1], 100) + self.assertAlmostEqual(doc['Foundation']['ShapeFunction_DOF1_Shape']['U_[m]'].values[-1], 1) + self.assertAlmostEqual(doc['Foundation']['ShapeFunction_DOF2_Shape']['U_[m]'].values[-1], 0) + self.assertEqual(type(doc['Blade']['ShapeFunction_DOF1_Shape']) is pd.DataFrame, True) + + +if __name__ == '__main__': +# Test().test_FLEXWaves() +# Test().test_FLEXDoc() + unittest.main() From a1c2d5b6428649dc12207bb711ced65bbb21c462 Mon Sep 17 00:00:00 2001 From: Emmanuel Branlard Date: Fri, 20 Oct 2023 14:15:17 -0600 Subject: [PATCH 121/124] Data: update inputs files to version 3.5.* --- ...OffshrBsline5MW_InflowWind_Steady8mps.dat} | 14 +++ .../NRELOffshrBsline5MW_ServoDyn_StC.dat | 2 +- .../NREL5MW/5MW_Land_Lin_BladeOnly/Main.1.lin | 6 +- .../NREL5MW/5MW_Land_Lin_Rotating/AeroDyn.dat | 44 ++++--- .../5MW_Land_Lin_Rotating/InflowWind.dat | 26 +++- .../5MW_Land_Lin_Templates/AeroDyn.dat | 42 ++++--- .../5MW_Land_Lin_Templates/InflowWind.dat | 28 +++-- .../5MW_Land_Lin_Templates/ServoDyn.dat | 2 +- data/NREL5MW/Main_Onshore.fst | 2 +- data/NREL5MW/Main_Onshore_OF2_BD.fst | 2 +- pyFAST/linearization/examples/README.md | 1 - .../examples/ex3a_MultiLinFile_Campbell.py | 2 +- pyFAST/linearization/examples/runCampbell.py | 9 +- .../linearization/examples/runLinTurbine.py | 113 ------------------ 14 files changed, 121 insertions(+), 172 deletions(-) rename data/NREL5MW/{onshore/InflowWind.dat => 5MW_Baseline/NRELOffshrBsline5MW_InflowWind_Steady8mps.dat} (77%) delete mode 100644 pyFAST/linearization/examples/runLinTurbine.py diff --git a/data/NREL5MW/onshore/InflowWind.dat b/data/NREL5MW/5MW_Baseline/NRELOffshrBsline5MW_InflowWind_Steady8mps.dat similarity index 77% rename from data/NREL5MW/onshore/InflowWind.dat rename to data/NREL5MW/5MW_Baseline/NRELOffshrBsline5MW_InflowWind_Steady8mps.dat index e46a5cc..3ab9b34 100644 --- a/data/NREL5MW/onshore/InflowWind.dat +++ b/data/NREL5MW/5MW_Baseline/NRELOffshrBsline5MW_InflowWind_Steady8mps.dat @@ -5,6 +5,7 @@ False Echo - Echo input data to .ech (flag) 1 WindType - switch for wind file type (1=steady; 2=uniform; 3=binary TurbSim FF; 4=binary Bladed-style FF; 5=HAWC format; 6=User defined; 7=native Bladed FF) 0 PropagationDir - Direction of wind propagation (meteorological rotation from aligned with X (positive rotates towards -Y) -- degrees) (not used for native Bladed format WindType=7) 0 VFlowAng - Upflow angle (degrees) (not used for native Bladed format WindType=7) + False VelInterpCubic - Use cubic interpolation for velocity in time (false=linear, true=cubic) [Used with WindType=2,3,4,5,7] 1 NWindVel - Number of points to output the wind velocity (0 to 9) 0 WindVxiList - List of coordinates in the inertial X direction (m) 0 WindVyiList - List of coordinates in the inertial Y direction (m) @@ -47,6 +48,19 @@ False TowerFile - Have tower file (.twr) (flag) ignored when WindTy 0 PLExp_Hawc - Power law exponent (-) (used for PL wind profile type only) 0.03 Z0 - Surface roughness length (m) (used for LG wind profile type only) 0 XOffset - Initial offset in +x direction (shift of wind box) +================== LIDAR Parameters =========================================================================== + 0 SensorType - Switch for lidar configuration (0 = None, 1 = Single Point Beam(s), 2 = Continuous, 3 = Pulsed) + 0 NumPulseGate - Number of lidar measurement gates (used when SensorType = 3) + 30 PulseSpacing - Distance between range gates (m) (used when SensorType = 3) + 0 NumBeam - Number of lidar measurement beams (0-5)(used when SensorType = 1) + -200 FocalDistanceX - Focal distance co-ordinates of the lidar beam in the x direction (relative to hub height) (only first coordinate used for SensorType 2 and 3) (m) + 0 FocalDistanceY - Focal distance co-ordinates of the lidar beam in the y direction (relative to hub height) (only first coordinate used for SensorType 2 and 3) (m) + 0 FocalDistanceZ - Focal distance co-ordinates of the lidar beam in the z direction (relative to hub height) (only first coordinate used for SensorType 2 and 3) (m) +0.0 0.0 0.0 RotorApexOffsetPos - Offset of the lidar from hub height (m) + 17 URefLid - Reference average wind speed for the lidar[m/s] + 0.25 MeasurementInterval - Time between each measurement [s] + False LidRadialVel - TRUE => return radial component, FALSE => return 'x' direction estimate + 1 ConsiderHubMotion - Flag whether to consider the hub motion's impact on Lidar measurements ====================== OUTPUT ================================================== False SumPrint - Print summary data to .IfW.sum (flag) OutList - The next line(s) contains a list of output parameters. See OutListParameters.xlsx for a listing of available output channels, (-) diff --git a/data/NREL5MW/5MW_Baseline/NRELOffshrBsline5MW_ServoDyn_StC.dat b/data/NREL5MW/5MW_Baseline/NRELOffshrBsline5MW_ServoDyn_StC.dat index 40fd14a..50688d7 100644 --- a/data/NREL5MW/5MW_Baseline/NRELOffshrBsline5MW_ServoDyn_StC.dat +++ b/data/NREL5MW/5MW_Baseline/NRELOffshrBsline5MW_ServoDyn_StC.dat @@ -3,7 +3,7 @@ Input file for tuned mass damper, module by Matt Lackner, Meghan Glade, and Semy ---------------------- SIMULATION CONTROL -------------------------------------- True Echo - Echo input data to .ech (flag) ---------------------- StC DEGREES OF FREEDOM ---------------------------------- - 2 StC_DOF_MODE - DOF mode (switch) {0: No StC or TLCD DOF; 1: StC_X_DOF, StC_Y_DOF, and/or StC_Z_DOF (three independent StC DOFs); 2: StC_XY_DOF (Omni-Directional StC); 3: TLCD; 4: Prescribed force/moment time series} + 2 StC_DOF_MODE - DOF mode (switch) {0: No StC or TLCD DOF; 1: StC_X_DOF, StC_Y_DOF, and/or StC_Z_DOF (three independent StC DOFs); 2: StC_XY_DOF (Omni-Directional StC); 3: TLCD; 4: Prescribed force/moment time series; 5: Force determined by external DLL} true StC_X_DOF - DOF on or off for StC X (flag) [Used only when StC_DOF_MODE=1] true StC_Y_DOF - DOF on or off for StC Y (flag) [Used only when StC_DOF_MODE=1] FALSE StC_Z_DOF - DOF on or off for StC Z (flag) [Used only when StC_DOF_MODE=1] diff --git a/data/NREL5MW/5MW_Land_Lin_BladeOnly/Main.1.lin b/data/NREL5MW/5MW_Land_Lin_BladeOnly/Main.1.lin index b984b1a..bff9943 100644 --- a/data/NREL5MW/5MW_Land_Lin_BladeOnly/Main.1.lin +++ b/data/NREL5MW/5MW_Land_Lin_BladeOnly/Main.1.lin @@ -1,8 +1,8 @@ -Linearized model: Predictions were generated on 19-Sep-2022 at 11:07:03 using OpenFAST, compiled as a 64-bit application using double precision at commit v3.2.0-dirty +Linearized model: Predictions were generated on 20-Oct-2023 at 14:04:02 using OpenFAST, compiled as a 64-bit application using single precision at commit v3.5.1-dirty linked with NWTC Subroutine Library; ElastoDyn -Description from the FAST input file: NREL Wind Turbine Modeling Workshop Simulation +Description from the FAST input file: NREL Wind Turbine - linearization for blades only, in vaccuum Simulation information: Simulation time: 0.0000 s @@ -101,7 +101,7 @@ C: 18 x 6 -1.867E+00 -1.735E+00 9.127E+00 -4.196E-03 -2.425E-03 7.085E-03 1.102E+03 -1.830E+04 -3.552E+03 2.476E+00 -2.557E+01 -2.758E+00 5.636E+03 4.914E+03 -2.406E+04 1.267E+01 6.866E+00 -1.868E+01 - 2.712E+01 -4.451E+02 -1.111E+02 6.096E-02 -6.220E-01 -8.622E-02 + 2.712E+01 -4.451E+02 -1.111E+02 6.096E-02 -6.220E-01 -8.621E-02 D: 18 x 4 0.000E+00 0.000E+00 0.000E+00 0.000E+00 0.000E+00 0.000E+00 0.000E+00 0.000E+00 diff --git a/data/NREL5MW/5MW_Land_Lin_Rotating/AeroDyn.dat b/data/NREL5MW/5MW_Land_Lin_Rotating/AeroDyn.dat index 7cf6d60..d0bd676 100644 --- a/data/NREL5MW/5MW_Land_Lin_Rotating/AeroDyn.dat +++ b/data/NREL5MW/5MW_Land_Lin_Rotating/AeroDyn.dat @@ -10,6 +10,7 @@ False Echo - Echo the input to ".AD.ech"? (flag False TwrAero - Calculate tower aerodynamic loads? (flag) True FrozenWake - Assume frozen wake during linearization? (flag) [used only when WakeMod=1 and when linearizing] False CavitCheck - Perform cavitation check? (flag) [AFAeroMod must be 1 when CavitCheck=true] +False Buoyancy - Include buoyancy effects? (flag) False CompAA - Flag to compute AeroAcoustics calculation [used only when WakeMod = 1 or 2] "unused" AA_InputFile - AeroAcoustics input file [used only when CompAA=true] ====== Environmental Conditions =================================================================== @@ -36,6 +37,8 @@ False TIDrag - Include the drag term in the tangential-induc ====== Beddoes-Leishman Unsteady Airfoil Aerodynamics Options ===================================== [used only when AFAeroMod=2] 3 UAMod - Unsteady Aero Model Switch (switch) {1=Baseline model (Original), 2=Gonzalez’s variant (changes in Cn,Cc,Cm), 3=Minemma/Pierce variant (changes in Cc and Cm)} [used only when AFAeroMod=2] True FLookup - Flag to indicate whether a lookup for f' will be calculated (TRUE) or whether best-fit exponential equations will be used (FALSE); if FALSE S1-S4 must be provided in airfoil input files (flag) [used only when AFAeroMod=2] +0.15 UAStartRad - Starting radius for dynamic stall (fraction of rotor radius) [used only when AFAeroMod=2] +1.0 UAEndRad - Ending radius for dynamic stall (fraction of rotor radius) [used only when AFAeroMod=2] ====== Airfoil Information ========================================================================= 1 AFTabMod - Interpolation method for multiple airfoil tables {1=1D interpolation on AoA (first table only); 2=2D interpolation on AoA and Re; 3=2D interpolation on AoA and UserProp} (-) 1 InCol_Alfa - The column in the airfoil tables that contains the angle of attack (-) @@ -57,22 +60,31 @@ True UseBlCm - Include aerodynamic pitching moment in calcul "../5MW_Baseline/NRELOffshrBsline5MW_AeroDyn_blade.dat" ADBlFile(1) - Name of file containing distributed aerodynamic properties for Blade #1 (-) "../5MW_Baseline/NRELOffshrBsline5MW_AeroDyn_blade.dat" ADBlFile(2) - Name of file containing distributed aerodynamic properties for Blade #2 (-) [unused if NumBl < 2] "../5MW_Baseline/NRELOffshrBsline5MW_AeroDyn_blade.dat" ADBlFile(3) - Name of file containing distributed aerodynamic properties for Blade #3 (-) [unused if NumBl < 3] -====== Tower Influence and Aerodynamics ============================================================= [used only when TwrPotent/=0, TwrShadow/=0, or TwrAero=True] - 12 NumTwrNds - Number of tower nodes used in the analysis (-) [used only when TwrPotent/=0, TwrShadow/=0, or TwrAero=True] -TwrElev TwrDiam TwrCd TwrTI -(m) (m) (-) (-) -0.0000000E+00 6.0000000E+00 1.0000000E+00 1.0000000E-01 -8.5261000E+00 5.7870000E+00 1.0000000E+00 1.0000000E-01 -1.7053000E+01 5.5740000E+00 1.0000000E+00 1.0000000E-01 -2.5579000E+01 5.3610000E+00 1.0000000E+00 1.0000000E-01 -3.4105000E+01 5.1480000E+00 1.0000000E+00 1.0000000E-01 -4.2633000E+01 4.9350000E+00 1.0000000E+00 1.0000000E-01 -5.1158000E+01 4.7220000E+00 1.0000000E+00 1.0000000E-01 -5.9685000E+01 4.5090000E+00 1.0000000E+00 1.0000000E-01 -6.8211000E+01 4.2960000E+00 1.0000000E+00 1.0000000E-01 -7.6738000E+01 4.0830000E+00 1.0000000E+00 1.0000000E-01 -8.5268000E+01 3.8700000E+00 1.0000000E+00 1.0000000E-01 -8.7600000E+01 3.8700000E+00 1.0000000E+00 1.0000000E-01 +====== Hub Properties ============================================================================== [used only when Buoyancy=True] +0.0 VolHub - Hub volume (m^3) +0.0 HubCenBx - Hub center of buoyancy x direction offset (m) +====== Nacelle Properties ========================================================================== [used only when Buoyancy=True] +0.0 VolNac - Nacelle volume (m^3) +0,0,0 NacCenB - Position of nacelle center of buoyancy from yaw bearing in nacelle coordinates (m) +====== Tail fin Aerodynamics ======================================================================== +False TFinAero - Calculate tail fin aerodynamics model (flag) +"unused" TFinFile - Input file for tail fin aerodynamics [used only when TFinAero=True] +====== Tower Influence and Aerodynamics ============================================================ [used only when TwrPotent/=0, TwrShadow/=0, TwrAero=True, or Buoyancy=True] + 12 NumTwrNds - Number of tower nodes used in the analysis (-) [used only when TwrPotent/=0, TwrShadow/=0, TwrAero=True, or Buoyancy=True] +TwrElev TwrDiam TwrCd TwrTI TwrCb !TwrTI used only with TwrShadow=2, TwrCb used only with Buoyancy=True +(m) (m) (-) (-) (-) +0.0000000E+00 6.0000000E+00 1.0000000E+00 1.0000000E-01 0.0 +8.5261000E+00 5.7870000E+00 1.0000000E+00 1.0000000E-01 0.0 +1.7053000E+01 5.5740000E+00 1.0000000E+00 1.0000000E-01 0.0 +2.5579000E+01 5.3610000E+00 1.0000000E+00 1.0000000E-01 0.0 +3.4105000E+01 5.1480000E+00 1.0000000E+00 1.0000000E-01 0.0 +4.2633000E+01 4.9350000E+00 1.0000000E+00 1.0000000E-01 0.0 +5.1158000E+01 4.7220000E+00 1.0000000E+00 1.0000000E-01 0.0 +5.9685000E+01 4.5090000E+00 1.0000000E+00 1.0000000E-01 0.0 +6.8211000E+01 4.2960000E+00 1.0000000E+00 1.0000000E-01 0.0 +7.6738000E+01 4.0830000E+00 1.0000000E+00 1.0000000E-01 0.0 +8.5268000E+01 3.8700000E+00 1.0000000E+00 1.0000000E-01 0.0 +8.7600000E+01 3.8700000E+00 1.0000000E+00 1.0000000E-01 0.0 ====== Outputs ==================================================================================== False SumPrint - Generate a summary file listing input options and interpolated properties to ".AD.sum"? (flag) 0 NBlOuts - Number of blade node outputs [0 - 9] (-) diff --git a/data/NREL5MW/5MW_Land_Lin_Rotating/InflowWind.dat b/data/NREL5MW/5MW_Land_Lin_Rotating/InflowWind.dat index 0397895..eb74f99 100644 --- a/data/NREL5MW/5MW_Land_Lin_Rotating/InflowWind.dat +++ b/data/NREL5MW/5MW_Land_Lin_Rotating/InflowWind.dat @@ -5,20 +5,21 @@ False Echo - Echo input data to .ech (flag) 1 WindType - switch for wind file type (1=steady; 2=uniform; 3=binary TurbSim FF; 4=binary Bladed-style FF; 5=HAWC format; 6=User defined; 7=native Bladed FF) 0 PropagationDir - Direction of wind propagation (meteorological rotation from aligned with X (positive rotates towards -Y) -- degrees) (not used for native Bladed format WindType=7) 0 VFlowAng - Upflow angle (degrees) (not used for native Bladed format WindType=7) + False VelInterpCubic - Use cubic interpolation for velocity in time (false=linear, true=cubic) [Used with WindType=2,3,4,5,7] 1 NWindVel - Number of points to output the wind velocity (0 to 9) 0 WindVxiList - List of coordinates in the inertial X direction (m) 0 WindVyiList - List of coordinates in the inertial Y direction (m) 90 WindVziList - List of coordinates in the inertial Z direction (m) ================== Parameters for Steady Wind Conditions [used only for WindType = 1] ========================= - 8 HWindSpeed - Horizontal windspeed (m/s) - 90 RefHt - Reference height for horizontal wind speed (m) + 8 HWindSpeed - Horizontal wind speed (m/s) + 90 RefHt - Reference height for horizontal wind speed (m) 0 PLExp - Power law exponent (-) ================== Parameters for Uniform wind file [used only for WindType = 2] ============================ -"unused" Filename_Uni - Filename of time series data for uniform wind field. (-) - 90 RefHt_Uni - Reference height for horizontal wind speed (m) +"unused" Filename_Uni - Filename of time series data for uniform wind field. (-) + 90 RefHt_Uni - Reference height for horizontal wind speed (m) 125.88 RefLength - Reference length for linear horizontal and vertical sheer (-) ================== Parameters for Binary TurbSim Full-Field files [used only for WindType = 3] ============== -"unused" FileName_BTS - Name of the Full field wind file to use (.bts) +"unused" FileName_BTS - Name of the Full field wind file to use (.bts) ================== Parameters for Binary Bladed-style Full-Field files [used only for WindType = 4 or WindType = 7] ========= "unused" FileNameRoot - WindType=4: Rootname of the full-field wind file to use (.wnd, .sum); WindType=7: name of the intermediate file with wind scaling values False TowerFile - Have tower file (.twr) (flag) ignored when WindType = 7 @@ -46,7 +47,20 @@ False TowerFile - Have tower file (.twr) (flag) ignored when WindTy 2 WindProfile - Wind profile type (0=constant;1=logarithmic,2=power law) 0 PLExp_Hawc - Power law exponent (-) (used for PL wind profile type only) 0.03 Z0 - Surface roughness length (m) (used for LG wind profile type only) - 0 XOffset - Initial offset in +x direction (shift of wind box) + 0 XOffset - Initial offset in +x direction (shift of wind box) +================== LIDAR Parameters =========================================================================== + 0 SensorType - Switch for lidar configuration (0 = None, 1 = Single Point Beam(s), 2 = Continuous, 3 = Pulsed) + 0 NumPulseGate - Number of lidar measurement gates (used when SensorType = 3) + 30 PulseSpacing - Distance between range gates (m) (used when SensorType = 3) + 0 NumBeam - Number of lidar measurement beams (0-5)(used when SensorType = 1) + -200 FocalDistanceX - Focal distance co-ordinates of the lidar beam in the x direction (relative to hub height) (only first coordinate used for SensorType 2 and 3) (m) + 0 FocalDistanceY - Focal distance co-ordinates of the lidar beam in the y direction (relative to hub height) (only first coordinate used for SensorType 2 and 3) (m) + 0 FocalDistanceZ - Focal distance co-ordinates of the lidar beam in the z direction (relative to hub height) (only first coordinate used for SensorType 2 and 3) (m) +0.0 0.0 0.0 RotorApexOffsetPos - Offset of the lidar from hub height (m) + 17 URefLid - Reference average wind speed for the lidar[m/s] + 0.25 MeasurementInterval - Time between each measurement [s] + False LidRadialVel - TRUE => return radial component, FALSE => return 'x' direction estimate + 1 ConsiderHubMotion - Flag whether to consider the hub motion's impact on Lidar measurements ====================== OUTPUT ================================================== False SumPrint - Print summary data to .IfW.sum (flag) OutList - The next line(s) contains a list of output parameters. See OutListParameters.xlsx for a listing of available output channels, (-) diff --git a/data/NREL5MW/5MW_Land_Lin_Templates/AeroDyn.dat b/data/NREL5MW/5MW_Land_Lin_Templates/AeroDyn.dat index 620a553..49a45d3 100644 --- a/data/NREL5MW/5MW_Land_Lin_Templates/AeroDyn.dat +++ b/data/NREL5MW/5MW_Land_Lin_Templates/AeroDyn.dat @@ -10,6 +10,7 @@ False Echo - Echo the input to ".AD.ech"? (flag False TwrAero - Calculate tower aerodynamic loads? (flag) True FrozenWake - Assume frozen wake during linearization? (flag) [used only when WakeMod=1 and when linearizing] False CavitCheck - Perform cavitation check? (flag) [AFAeroMod must be 1 when CavitCheck=true] +False Buoyancy - Include buoyancy effects? (flag) False CompAA - Flag to compute AeroAcoustics calculation [used only when WakeMod = 1 or 2] "unused" AA_InputFile - AeroAcoustics input file [used only when CompAA=true] ====== Environmental Conditions =================================================================== @@ -59,22 +60,31 @@ True UseBlCm - Include aerodynamic pitching moment in calcul "../5MW_Baseline/NRELOffshrBsline5MW_AeroDyn_blade.dat" ADBlFile(1) - Name of file containing distributed aerodynamic properties for Blade #1 (-) "../5MW_Baseline/NRELOffshrBsline5MW_AeroDyn_blade.dat" ADBlFile(2) - Name of file containing distributed aerodynamic properties for Blade #2 (-) [unused if NumBl < 2] "../5MW_Baseline/NRELOffshrBsline5MW_AeroDyn_blade.dat" ADBlFile(3) - Name of file containing distributed aerodynamic properties for Blade #3 (-) [unused if NumBl < 3] -====== Tower Influence and Aerodynamics ============================================================= [used only when TwrPotent/=0, TwrShadow/=0, or TwrAero=True] - 12 NumTwrNds - Number of tower nodes used in the analysis (-) [used only when TwrPotent/=0, TwrShadow/=0, or TwrAero=True] -TwrElev TwrDiam TwrCd TwrTI -(m) (m) (-) (-) -0.0000000E+00 6.0000000E+00 1.0000000E+00 0.1 -8.5261000E+00 5.7870000E+00 1.0000000E+00 0.1 -1.7053000E+01 5.5740000E+00 1.0000000E+00 0.1 -2.5579000E+01 5.3610000E+00 1.0000000E+00 0.1 -3.4105000E+01 5.1480000E+00 1.0000000E+00 0.1 -4.2633000E+01 4.9350000E+00 1.0000000E+00 0.1 -5.1158000E+01 4.7220000E+00 1.0000000E+00 0.1 -5.9685000E+01 4.5090000E+00 1.0000000E+00 0.1 -6.8211000E+01 4.2960000E+00 1.0000000E+00 0.1 -7.6738000E+01 4.0830000E+00 1.0000000E+00 0.1 -8.5268000E+01 3.8700000E+00 1.0000000E+00 0.1 -8.7600000E+01 3.8700000E+00 1.0000000E+00 0.1 +====== Hub Properties ============================================================================== [used only when Buoyancy=True] +0.0 VolHub - Hub volume (m^3) +0.0 HubCenBx - Hub center of buoyancy x direction offset (m) +====== Nacelle Properties ========================================================================== [used only when Buoyancy=True] +0.0 VolNac - Nacelle volume (m^3) +0,0,0 NacCenB - Position of nacelle center of buoyancy from yaw bearing in nacelle coordinates (m) +====== Tail fin Aerodynamics ======================================================================== +False TFinAero - Calculate tail fin aerodynamics model (flag) +"unused" TFinFile - Input file for tail fin aerodynamics [used only when TFinAero=True] +====== Tower Influence and Aerodynamics ============================================================ [used only when TwrPotent/=0, TwrShadow/=0, TwrAero=True, or Buoyancy=True] + 12 NumTwrNds - Number of tower nodes used in the analysis (-) [used only when TwrPotent/=0, TwrShadow/=0, TwrAero=True, or Buoyancy=True] +TwrElev TwrDiam TwrCd TwrTI TwrCb !TwrTI used only with TwrShadow=2, TwrCb used only with Buoyancy=True +(m) (m) (-) (-) (-) +0.0000000E+00 6.0000000E+00 1.0000000E+00 1.0000000E-01 0.0 +8.5261000E+00 5.7870000E+00 1.0000000E+00 1.0000000E-01 0.0 +1.7053000E+01 5.5740000E+00 1.0000000E+00 1.0000000E-01 0.0 +2.5579000E+01 5.3610000E+00 1.0000000E+00 1.0000000E-01 0.0 +3.4105000E+01 5.1480000E+00 1.0000000E+00 1.0000000E-01 0.0 +4.2633000E+01 4.9350000E+00 1.0000000E+00 1.0000000E-01 0.0 +5.1158000E+01 4.7220000E+00 1.0000000E+00 1.0000000E-01 0.0 +5.9685000E+01 4.5090000E+00 1.0000000E+00 1.0000000E-01 0.0 +6.8211000E+01 4.2960000E+00 1.0000000E+00 1.0000000E-01 0.0 +7.6738000E+01 4.0830000E+00 1.0000000E+00 1.0000000E-01 0.0 +8.5268000E+01 3.8700000E+00 1.0000000E+00 1.0000000E-01 0.0 +8.7600000E+01 3.8700000E+00 1.0000000E+00 1.0000000E-01 0.0 ====== Outputs ==================================================================================== False SumPrint - Generate a summary file listing input options and interpolated properties to ".AD.sum"? (flag) 0 NBlOuts - Number of blade node outputs [0 - 9] (-) diff --git a/data/NREL5MW/5MW_Land_Lin_Templates/InflowWind.dat b/data/NREL5MW/5MW_Land_Lin_Templates/InflowWind.dat index 3a4ca09..3ab9b34 100644 --- a/data/NREL5MW/5MW_Land_Lin_Templates/InflowWind.dat +++ b/data/NREL5MW/5MW_Land_Lin_Templates/InflowWind.dat @@ -1,20 +1,21 @@ ------- InflowWind INPUT FILE ------------------------------------------------------------------------- -InflowWind input file steady wind with no shear +Steady 8 m/s winds with no shear for FAST CertTests #20 and #25 --------------------------------------------------------------------------------------------------------------- False Echo - Echo input data to .ech (flag) 1 WindType - switch for wind file type (1=steady; 2=uniform; 3=binary TurbSim FF; 4=binary Bladed-style FF; 5=HAWC format; 6=User defined; 7=native Bladed FF) 0 PropagationDir - Direction of wind propagation (meteorological rotation from aligned with X (positive rotates towards -Y) -- degrees) (not used for native Bladed format WindType=7) 0 VFlowAng - Upflow angle (degrees) (not used for native Bladed format WindType=7) + False VelInterpCubic - Use cubic interpolation for velocity in time (false=linear, true=cubic) [Used with WindType=2,3,4,5,7] 1 NWindVel - Number of points to output the wind velocity (0 to 9) 0 WindVxiList - List of coordinates in the inertial X direction (m) 0 WindVyiList - List of coordinates in the inertial Y direction (m) 90 WindVziList - List of coordinates in the inertial Z direction (m) ================== Parameters for Steady Wind Conditions [used only for WindType = 1] ========================= - 8 HWindSpeed - Horizontal windspeed (m/s) + 8 HWindSpeed - Horizontal wind speed (m/s) 90 RefHt - Reference height for horizontal wind speed (m) 0 PLExp - Power law exponent (-) ================== Parameters for Uniform wind file [used only for WindType = 2] ============================ -"unused" Filename_Uni - Filename of time series data for uniform wind field. (-) +"unused" Filename_Uni - Filename of time series data for uniform wind field. (-) 90 RefHt_Uni - Reference height for horizontal wind speed (m) 125.88 RefLength - Reference length for linear horizontal and vertical sheer (-) ================== Parameters for Binary TurbSim Full-Field files [used only for WindType = 3] ============== @@ -23,9 +24,9 @@ False Echo - Echo input data to .ech (flag) "unused" FileNameRoot - WindType=4: Rootname of the full-field wind file to use (.wnd, .sum); WindType=7: name of the intermediate file with wind scaling values False TowerFile - Have tower file (.twr) (flag) ignored when WindType = 7 ================== Parameters for HAWC-format binary files [Only used with WindType = 5] ===================== -"unused" FileName_u - name of the file containing the u-component fluctuating wind (.bin) -"unused" FileName_v - name of the file containing the v-component fluctuating wind (.bin) -"unused" FileName_w - name of the file containing the w-component fluctuating wind (.bin) +"wasp\Output\basic_5u.bin" FileName_u - name of the file containing the u-component fluctuating wind (.bin) +"wasp\Output\basic_5v.bin" FileName_v - name of the file containing the v-component fluctuating wind (.bin) +"wasp\Output\basic_5w.bin" FileName_w - name of the file containing the w-component fluctuating wind (.bin) 64 nx - number of grids in the x direction (in the 3 files above) (-) 32 ny - number of grids in the y direction (in the 3 files above) (-) 32 nz - number of grids in the z direction (in the 3 files above) (-) @@ -46,7 +47,20 @@ False TowerFile - Have tower file (.twr) (flag) ignored when WindTy 2 WindProfile - Wind profile type (0=constant;1=logarithmic,2=power law) 0 PLExp_Hawc - Power law exponent (-) (used for PL wind profile type only) 0.03 Z0 - Surface roughness length (m) (used for LG wind profile type only) - 0 XOffset - Initial offset in +x direction (shift of wind box) + 0 XOffset - Initial offset in +x direction (shift of wind box) +================== LIDAR Parameters =========================================================================== + 0 SensorType - Switch for lidar configuration (0 = None, 1 = Single Point Beam(s), 2 = Continuous, 3 = Pulsed) + 0 NumPulseGate - Number of lidar measurement gates (used when SensorType = 3) + 30 PulseSpacing - Distance between range gates (m) (used when SensorType = 3) + 0 NumBeam - Number of lidar measurement beams (0-5)(used when SensorType = 1) + -200 FocalDistanceX - Focal distance co-ordinates of the lidar beam in the x direction (relative to hub height) (only first coordinate used for SensorType 2 and 3) (m) + 0 FocalDistanceY - Focal distance co-ordinates of the lidar beam in the y direction (relative to hub height) (only first coordinate used for SensorType 2 and 3) (m) + 0 FocalDistanceZ - Focal distance co-ordinates of the lidar beam in the z direction (relative to hub height) (only first coordinate used for SensorType 2 and 3) (m) +0.0 0.0 0.0 RotorApexOffsetPos - Offset of the lidar from hub height (m) + 17 URefLid - Reference average wind speed for the lidar[m/s] + 0.25 MeasurementInterval - Time between each measurement [s] + False LidRadialVel - TRUE => return radial component, FALSE => return 'x' direction estimate + 1 ConsiderHubMotion - Flag whether to consider the hub motion's impact on Lidar measurements ====================== OUTPUT ================================================== False SumPrint - Print summary data to .IfW.sum (flag) OutList - The next line(s) contains a list of output parameters. See OutListParameters.xlsx for a listing of available output channels, (-) diff --git a/data/NREL5MW/5MW_Land_Lin_Templates/ServoDyn.dat b/data/NREL5MW/5MW_Land_Lin_Templates/ServoDyn.dat index e2cffed..ddbe74c 100644 --- a/data/NREL5MW/5MW_Land_Lin_Templates/ServoDyn.dat +++ b/data/NREL5MW/5MW_Land_Lin_Templates/ServoDyn.dat @@ -98,7 +98,7 @@ false DLL_Ramp - Whether a linear ramp should be used between DLL_DT GenSpd_TLU GenTrq_TLU (rpm) (Nm) ---------------------- OUTPUT -------------------------------------------------- -True SumPrint - Print summary data to .sum (flag) (currently unused) +False SumPrint - Print summary data to .sum (flag) (currently unused) 1 OutFile - Switch to determine where output will be placed: {1: in module output file only; 2: in glue code output file only; 3: both} (currently unused) True TabDelim - Use tab delimiters in text tabular output file? (flag) (currently unused) "ES10.3E2" OutFmt - Format used for text tabular output (except time). Resulting field should be 10 characters. (quoted string) (currently unused) diff --git a/data/NREL5MW/Main_Onshore.fst b/data/NREL5MW/Main_Onshore.fst index 85b0e85..a7df7a8 100644 --- a/data/NREL5MW/Main_Onshore.fst +++ b/data/NREL5MW/Main_Onshore.fst @@ -34,7 +34,7 @@ False Echo - Echo input data to .ech (flag) "unused" BDBldFile(1) - Name of file containing BeamDyn input parameters for blade 1 (quoted string) "unused" BDBldFile(2) - Name of file containing BeamDyn input parameters for blade 2 (quoted string) "unused" BDBldFile(3) - Name of file containing BeamDyn input parameters for blade 3 (quoted string) -"onshore/InflowWind.dat" InflowFile - Name of file containing inflow wind input parameters (quoted string) +"5MW_Baseline/NRELOffshrBsline5MW_InflowWind_Steady8mps.dat" InflowFile - Name of file containing inflow wind input parameters (quoted string) "onshore/AeroDyn.dat" AeroFile - Name of file containing aerodynamic input parameters (quoted string) "onshore/ServoDyn_Simple.dat" ServoFile - Name of file containing control and electrical-drive input parameters (quoted string) "unused" HydroFile - Name of file containing hydrodynamic input parameters (quoted string) diff --git a/data/NREL5MW/Main_Onshore_OF2_BD.fst b/data/NREL5MW/Main_Onshore_OF2_BD.fst index 61c6c6d..6b48c90 100644 --- a/data/NREL5MW/Main_Onshore_OF2_BD.fst +++ b/data/NREL5MW/Main_Onshore_OF2_BD.fst @@ -34,7 +34,7 @@ False Echo - Echo input data to .ech (flag) "5MW_Baseline/NRELOffshrBsline5MW_BeamDyn.dat" BDBldFile(1) - Name of file containing BeamDyn input parameters for blade 1 (quoted string) "5MW_Baseline/NRELOffshrBsline5MW_BeamDyn.dat" BDBldFile(2) - Name of file containing BeamDyn input parameters for blade 2 (quoted string) "5MW_Baseline/NRELOffshrBsline5MW_BeamDyn.dat" BDBldFile(3) - Name of file containing BeamDyn input parameters for blade 3 (quoted string) -"onshore/InflowWind.dat" InflowFile - Name of file containing inflow wind input parameters (quoted string) +"5MW_Baseline/NRELOffshrBsline5MW_InflowWind_Steady8mps.dat" InflowFile - Name of file containing inflow wind input parameters (quoted string) "onshore/AeroDyn.dat" AeroFile - Name of file containing aerodynamic input parameters (quoted string) "onshore/ServoDyn_Simple.dat" ServoFile - Name of file containing control and electrical-drive input parameters (quoted string) "unused" HydroFile - Name of file containing hydrodynamic input parameters (quoted string) diff --git a/pyFAST/linearization/examples/README.md b/pyFAST/linearization/examples/README.md index 1b9fd67..dd76b04 100644 --- a/pyFAST/linearization/examples/README.md +++ b/pyFAST/linearization/examples/README.md @@ -13,4 +13,3 @@ Examples using higher-level functions: - `runCampbell.py`: Generates a Campbell diagram; write OpenFAST input files, run openfast, postprocess the linearization files, and generate the Campbell diagram plot. Requires an openfast executable. -- `runLinTurbine.py`: (advanced) Example to run a set of OpenFAST simulations with linearizations. Requires an OpenFAST exe. diff --git a/pyFAST/linearization/examples/ex3a_MultiLinFile_Campbell.py b/pyFAST/linearization/examples/ex3a_MultiLinFile_Campbell.py index 20d8099..6694a4f 100644 --- a/pyFAST/linearization/examples/ex3a_MultiLinFile_Campbell.py +++ b/pyFAST/linearization/examples/ex3a_MultiLinFile_Campbell.py @@ -49,7 +49,7 @@ # --- Step 5b: Run FAST with VIZ files to generate VTKs import pyFAST.case_generation.runner as runner simDir = os.path.dirname(fstFiles[0]) -fastExe = '../../../data/openfast3.3_x64s.exe' +fastExe = os.path.join(scriptDir, '../../../data/openfast.exe') ### Option 1 write a batch file and run it # batchfile = runner.writeBatch(os.path.join(simDir,'_RUNViz.bat'), vizFiles, fastExe=fastExe, flags='-VTKLin') # runner.runBatch(batchfile) diff --git a/pyFAST/linearization/examples/runCampbell.py b/pyFAST/linearization/examples/runCampbell.py index 11c7935..0f08415 100644 --- a/pyFAST/linearization/examples/runCampbell.py +++ b/pyFAST/linearization/examples/runCampbell.py @@ -40,18 +40,18 @@ def campbell_example(writeFSTfiles=True, runFAST=True, postproLin=True): # --- Step 1: Write OpenFAST inputs files for each operating points baseDict = {'DT':0.01} # Example of how inputs can be overriden (see case_gen.py templateReplace) - FSTfilenames = lin.writeLinearizationFiles(templateFstFile, simulationFolder, operatingPointsFile, nPerPeriod=nPerPeriod, baseDict=baseDict) + fstFiles = lin.writeLinearizationFiles(templateFstFile, simulationFolder, operatingPointsFile, nPerPeriod=nPerPeriod, baseDict=baseDict) # Create a batch script (optional) - runner.writeBatch(os.path.join(simulationFolder,'_RUN_ALL.bat'), FSTfilenames, fastExe=fastExe) + runner.writeBatch(os.path.join(simulationFolder,'_RUN_ALL.bat'), fstFiles, fastExe=fastExe) # --- Step 2: run OpenFAST if runFAST: - runner.run_fastfiles(FSTfilenames, fastExe=fastExe, parallel=True, showOutputs=True, nCores=4) + runner.run_fastfiles(fstFiles, fastExe=fastExe, parallel=True, showOutputs=True, nCores=4) # --- Step 3: Run MBC, identify Modes, generate CSV files, and binary modes if postproLin: - OP, Freq, Damp, _, _, modeID_file = lin.postproCampbell(FSTfilenames, writeModes=True, verbose=True) + OP, Freq, Damp, _, _, modeID_file = lin.postproCampbell(fstFiles, writeModes=True, verbose=True) # Edit the modeID file manually to identify the modes print('[TODO] Edit this file manually: ',modeID_file) @@ -70,7 +70,6 @@ def campbell_example(writeFSTfiles=True, runFAST=True, postproLin=True): # --- Step 5b: Run FAST with VIZ files to generate VTKs simDir = os.path.dirname(fstFiles[0]) - fastExe = '../../../data/openfast3.3_x64s.exe' ### Option 1 write a batch file and run it # batchfile = runner.writeBatch(os.path.join(simDir,'_RUNViz.bat'), vizFiles, fastExe=fastExe, flags='-VTKLin') # runner.runBatch(batchfile) diff --git a/pyFAST/linearization/examples/runLinTurbine.py b/pyFAST/linearization/examples/runLinTurbine.py deleted file mode 100644 index c581471..0000000 --- a/pyFAST/linearization/examples/runLinTurbine.py +++ /dev/null @@ -1,113 +0,0 @@ -""" Example to run a set of OpenFAST simulations with linearizations. -NOTE: for a more streamlined process to generate a Cmpbell diagram look at the example runCampbell.py - -This script uses a reference directory which contains a reference input file (templateFstFile) -1) The reference directory is copied to a working directory (`simulationFolder`). -2) All the fast input files are generated in this directory based on a list of dictionaries (`PARAMS`). -For each dictionary in this list: - - The keys are "path" to a input parameter, e.g. `EDFile|RotSpeed` or `TMax`. - These should correspond to the variables used in the FAST inputs files. - - The values are the values corresponding to this parameter -For instance: - PARAMS[0]['DT'] = 0.01 - PARAMS[0]['EDFile|RotSpeed'] = 5 - PARAMS[0]['InflowFile|HWindSpeed'] = 10 - -3) The simulations are run, successively distributed on `nCores` CPUs. - -4) The linearization file are postprocessed using MBC and the frequencies written to screen - -""" -import numpy as np -import pandas as pd -import os -import pyFAST.linearization.linearization as lin -import pyFAST.case_generation.case_gen as case_gen -import pyFAST.case_generation.runner as runner -import scipy as sp - -import matplotlib.pyplot as plt - -def run_linearization(): - # --- Parameters for this script - simulationFolder = '../../../data/NREL5MW/_5MW_Land_Lin_Trim2/' # Output folder for input files and linearization (will be created) - templateFstFile = '../../../data/NREL5MW/5MW_Land_Lin_Templates/Main_5MW_Land_Lin.fst' # Main file, used as a template - fastExe = '../../../data/openfast2.5_x64.exe' # Path to a FAST exe (and dll) - - # --- Defining the parametric study (list of dictionnaries with keys as FAST parameters) - WS = [14,16,18] - BaseDict = {'TMax': 600} - # Set some default options - BaseDict = case_gen.paramsLinearTrim(BaseDict) # Run linear trim case - print(BaseDict) - PARAMS=[] - for i,wsp in enumerate(WS): - p=BaseDict.copy() - p['CompServo'] = 1 - p['InflowFile|HWindSpeed'] = wsp - p['InflowFile|WindType'] = 1 # Setting steady wind - - # Set DOFs - p['EDFile|GenDOF'] = 'True' - p['EDFile|FlapDOF1'] = 'True' - p['EDFile|TwFADOF1'] = 'True' - - # NREL-5MW rated generator speed, torque, control params - p['ServoFile|VS_RtGnSp'] = 1173 * .9 - p['ServoFile|VS_RtTq'] = 47402 - p['ServoFile|VS_Rgn2K'] = 0.0226 - p['ServoFile|VS_SlPc'] = 10. - - # Trim solution will converge to this rotor speed - p['EDFile|RotSpeed'] = 12.1 - - # Set number of linearizations - p['CalcSteady'] = True - p['NLinTimes'] = 12 - p['OutFmt'] = '"ES20.12E3"' # Important for decent resolution - p['LinInputs'] = 0 - p['LinOutputs'] = 0 - p['TrimCase'] = 3 - p['TrimGain'] = 0.001 - - p['__name__']='{:03d}_ws{:04.1f}'.format(i,p['InflowFile|HWindSpeed']) - PARAMS.append(p) - i=i+1 - # --- Generating all files in a output directory - refDir = os.path.dirname(templateFstFile) - main_file = os.path.basename(templateFstFile) - fastfiles=case_gen.templateReplace(PARAMS, refDir, outputDir=simulationFolder, removeRefSubFiles=True, main_file=main_file) - print(fastfiles) - - # --- Creating a batch script just in case - runner.writeBatch(os.path.join(simulationFolder,'_RUN_ALL.bat'),fastfiles,fastExe=fastExe) - # --- Running the simulations - runner.run_fastfiles(fastfiles, fastExe=fastExe, parallel=True, showOutputs=True, nCores=1) - - # --- Simple Postprocessing - # (averaging each signal over the last period for each simulation) - #outFiles = [os.path.splitext(f)[0]+'.outb' for f in fastfiles] - # avg_results = postpro.averagePostPro(outFiles, avgMethod='periods', avgParam=1, ColMap = {'WS_[m/s]':'Wind1VelX_[m/s]'},ColSort='WS_[m/s]') - # avg_results.drop('Time_[s]',axis=1, inplace=True) - - return fastfiles - - - -if __name__ == '__main__': - - # 1. Run linearizations - fastfiles = run_linearization() - # 2. Do MBC - #MBC = lin.run_pyMBC(fastfiles) - MBC = lin.getMBCOPs(fastfiles) - - # Check natural frequencies against matlab - for mbc in MBC: - eigs = sp.linalg.eig(mbc['AvgA'])[0] - nat_freq_hz = (np.abs(eigs)/2/np.pi) - nat_freq_hz.sort() - print('Natural Freqs. (hz): {}'.format(nat_freq_hz)) - -if __name__=='__test__': - pass # this example needs an openfast binary From 4f616ed33c6f8929f804f74feb7efe5caf6b36c6 Mon Sep 17 00:00:00 2001 From: Emmanuel Branlard Date: Fri, 20 Oct 2023 14:20:05 -0600 Subject: [PATCH 122/124] IO: adding missing flex out file due to gitignore --- .../tests/example_files/FLEXDocFile.out | 864 ++++++++++++++++++ 1 file changed, 864 insertions(+) create mode 100644 pyFAST/input_output/tests/example_files/FLEXDocFile.out diff --git a/pyFAST/input_output/tests/example_files/FLEXDocFile.out b/pyFAST/input_output/tests/example_files/FLEXDocFile.out new file mode 100644 index 0000000..a5f4fa9 --- /dev/null +++ b/pyFAST/input_output/tests/example_files/FLEXDocFile.out @@ -0,0 +1,864 @@ +#Program Flex5 +#BaseName MT100 +#InfFile .\MT100.inf +#CurDir Flex_Monopile +#OutDir Flex_Monopile\ +#LogFile Flex_Monopile\doc\MT100.out +#ResFile Flex_Monopile\res\MT100.res +#TTFFile Flex_Monopile\ttf\MT100.ttf +############################################################################################### +### START INPUT FILE DATA +#WTGName Unknown +#Input_Filename .\MT100.inf +#Input_Comment StandstillNTMV=10Vdir=0misalign=0 +#Input_FoundationFileName Flex_Monopile\data\Tube100.fnd +#Input_TowerFileName Flex_Monopile\data\TubeLight1.twr +#Input_BladeFileName Flex_Monopile\data\TubeBlade_Light.bld +#Input_GeneratorFileName Flex_Monopile\-data\Tube.gen +#Input_BrakeFileName Flex_Monopile\-data\Tube.brk +#Input_PitchFileName Flex_Monopile\-data\Tube.pit +#Input_YawFileName Flex_Monopile\-data\default.yaw +#Input_ControllerFileName Flex_Monopile\-data\Tube.con +#Input_GustFileName Flex_Monopile\-Nogust +#Input_InitFileName Flex_Monopile\InitNoEquilibrium.gxi +#Input_QuakeFileName Flex_Monopile\-Noquake +### Rotor +#Rotor_NumberBlades 3 +#Rotor_FlapStiffnessFactor 1.00000 1.00000 1.00000 +#Rotor_EdgeStiffnessFactor 1.00000 1.00000 1.00000 +#Rotor_MassFactor 1.00000 1.00000 1.00000 +#Rotor_PitchOffset 0.000 0.000 0.000 [deg] +### WTG geometry +#Geometry_RotorConing 0.00 [deg] [GAMMA] +#Geometry_ShaftTilt 0.00 [deg] [TILT] +#Geometry_TowerTopExtension 1.00E-08 [m] [XKK2] +#Geometry_TowerAxis2NacelleCOG 0.00E+00 [m] [ZGKAB] +#Geometry_HubCentre2TowerAxis 8.00 [m] [ZNAV] +#Geometry_HubCentrer2HubCOG 0.00E+00 [m] [ZGNAV] +#Geometry_HubCentreMainBearing 0.00E+00 [m] [ZRN] +### Data for hub +#Hub_Mass 0.00E+00 [kg] +#Hub_MassMomentInertiaIx 0.00E+00 [kgm2] [IXNAV] +#Hub_MassMomentInertiaIy 0.00E+00 [kgm2] [IYNAV] +#Hub_MassMomentInertiaIz 0.00E+00 [kgm2] [IZNAV] +### Data for nacelle +#Nacelle_Mass 0.00E+00 [kg] +#Nacelle_MassMomentInertiaIx 0.00E+00 [kgm2] [IXKAB] +#Nacelle_MassMomentInertiaIy 0.00E+00 [kgm2] [IYKAB] +#Nacelle_MassMomentInertiaIz 0.00E+00 [kgm2] [IZKAB] +#Nacelle_StiffnessTorsionYaw 3.53E+09 [Nm/rad] [KTX] +#Nacelle_StiffnessTorsionTilt 2.90E+09 [Nm/rad] [KKY] +### Data for shaft +#Shaft_StiffnessX 7.08E+11 [Nm/rad] [KGAX] +#Shaft_StiffnessY 7.08E+11 [Nm/rad] [KGAY] +#Shaft_StiffnessTorsion 1.55E+09 [Nm/rad] [KTors] +### Data for generator and gearbox +#Generator_Inertia 0.00E+00 [kgm2] [IGen] +#Gearbox_Ratio 1.00E+00 [-] +### Environment conditions +#Environment_AirDensity 1.234 [kg/m3] +#Environment_Gravity 9.81 [m/s2] +#Environment_YawAngle 0.00 [deg] +#Environment_WindSpeed 10.00 [m/s] +#Environment_WindShearExp 0.000 [-] +#Environment_WindDirection 0.00 [deg] +#Environment_WindSlope 0.00 [deg] +#Environment_TurbulenceIntensity 0.000 [-] +#Environment_RTI-V 0.000 [-] +#Environment_RTI-W 0.000 [-] +### Simulation parameters +#Sim_TStart 0.000 [s] +#Sim_DT 0.010 [s] +#Sim_TMax 100.000 [s] +#Sim_InitialRotorSpeed 0.000 [rad/s] +#Sim_InitialPitch 0.000 [deg] +#Sim_InitialGeneratorState 0 [-] +#Sim_InitialAzimuth 0 [deg] +#Sim_NFIL 2 [-] +### Stall model +#Dynamicstall_Active 0 +### Wake model +#Wakemodel_Active 0 +### Data for aerodynamics loads on the nacelle and the hub +#Hub_FrontDrag 0.000 [m2] +#Hub_SideDrag 0.000 [m2] +#Hub_AeroCentre 0.000 [m] +#Nacelle_FrontDrag 0.000 [m2] +#Nacelle_SideDrag 0.000 [m2] +#Nacelle_AeroCentre 0.000 [m] +### WTG geometry (derived parameters) +#Geometry_RotorDiameter 1.90 [m] (= (2 (R+Xrod))*cos(Cone) ) +#Geometry_HubHeight 51.00 [m] [HNAV] (= TowerBottom2Hub + InterfaceHeight) +#Geometry_TowerBottom2Nacelle 1.00 [m] [XKT] (= TowerLength + TowerTopExtension) +#Geometry_TowerBottom2Hub 1.00 [m] [XRT] (= TowerBottom2Nacelle + HubCentre2TowerAxis*sin(tilt)) +#Geometry_InterfaceHeight 50.00 [m] [Htop] (= Foundation Htop) +### END INPUT FILE DATA +#Input_ProfileFileName Flex_Monopile\data\DummyProfile.pro +### RNA Data +#RNA_Mass 2.8500E-06 [kg] +#RNA_MassMomentInertiaIy_atK 1.8283E-04 [kgm2] (psi=0, at K, nacelle system) (Approximation) +#RNA_MassMomentInertiaIy_atTT 1.8283E-04 [kgm2] (psi=0, at TowerTop) (Approximation) +#RNA_DriveTrainInertia 8.6009E-07 [kgm2] (Iz rotor side: hub+rot+gen*n^2) +#RNA_TowerTop2COG -1.0000E-08 0.0000E+00 -8.0000E+00 [m] (in straight tower system, x-downwards, z-downwind) +#RNA_COG_inNac 0.0000E+00 0.0000E+00 -8.0000E+00 [m] (in nacelle system, tilted) +############################################################################################### +### START TOWER DATA +#Tower_File data/TubeLight1.twr +#Tower_Comment 1m tower, light +#Tower_ModelName general +#Tower_nDOF 4 +#Tower_nDOF_1D 2 +#Tower_nSections 11 +#Tower_Length 1.000 [m] +#Tower_Mass 1.124E-02 [kg] +#Tower_MassMoment 5.623E-03 [kg.m] (Ref. point T, H=0) +#Tower_MomentInertia 3.749E-03 [kg.m2] (Ref. point T, H=0) +#Tower_EI_avg 1.868E+12 [Nm2] +#Tower_m_avg 1.124E-02 [kg/m] +#Tower_E_avg 210 [GPa] +#Tower_Density_avg 0 [kg/m3] +#Tower_TopMass 0.00 [kg] +#Tower_InputLogDec 1.000E-04 1.000E-04 1.000E-04 1.000E-04 +#Tower_ShapeFunctionFrequencies 7199530.49745924169.160 [Hz] (NOT clamped frequency) +#Tower_Shadow_Factor 0.000 +#Tower_Drag_Factor 1.000 +#Tower_EstimatedTorsionStiffness 1.437E+12 [Nm/rad] (with G modulus = 8.08E+10 Pa) +#Tower_OverallTorsionStiffness 3.521E+09 [Nm/rad] +#Tower_TMD_Inputs 0 (1 if inputs present in tower file) +#Tower_SectionData +11 h[m] D[m] t[mm] M_lump[kg] Cd[-] E[GPa] rho[kg/m3] EI[Nm2] m[kg/m] + 0.00 8.000 45.0 0.0 0.60 2.100E+02 1.000E-02 1.868E+12 1.124E-02 + 0.10 8.000 45.0 0.0 0.60 2.100E+02 1.000E-02 1.868E+12 1.124E-02 + 0.20 8.000 45.0 0.0 0.60 2.100E+02 1.000E-02 1.868E+12 1.124E-02 + 0.40 8.000 45.0 0.0 0.60 2.100E+02 1.000E-02 1.868E+12 1.124E-02 + 0.49 8.000 45.0 0.0 0.60 2.100E+02 1.000E-02 1.868E+12 1.124E-02 + 0.50 8.000 45.0 0.0 0.60 2.100E+02 1.000E-02 1.868E+12 1.124E-02 + 0.60 8.000 45.0 0.0 0.60 2.100E+02 1.000E-02 1.868E+12 1.124E-02 + 0.70 8.000 45.0 0.0 0.60 2.100E+02 1.000E-02 1.868E+12 1.124E-02 + 0.80 8.000 45.0 0.0 0.60 2.100E+02 1.000E-02 1.868E+12 1.124E-02 + 0.90 8.000 45.0 0.0 0.60 2.100E+02 1.000E-02 1.868E+12 1.124E-02 + 1.00 8.000 45.0 0.0 0.60 2.100E+02 1.000E-02 1.868E+12 1.124E-02 +#Tower_Couplings_AlphaY 1.37891873289 4.85861838017 [rad/m] +#Tower_Couplings_AlphaZ -1.37891873289 -4.85861838017 [rad/m] +#Tower_ShapeFunction_DOF1_Shape (Unit displacement at point K) + H [m] K [rad/m] V [rad] U [m] + 0.00 3.51E+00 0.00E+00 0.00E+00 + 0.10 3.03E+00 3.27E-01 1.68E-02 + 0.20 2.55E+00 6.06E-01 6.38E-02 + 0.40 1.62E+00 1.02E+00 2.30E-01 + 0.49 1.23E+00 1.15E+00 3.28E-01 + 0.50 1.19E+00 1.16E+00 3.39E-01 + 0.60 8.07E-01 1.26E+00 4.61E-01 + 0.70 4.79E-01 1.33E+00 5.91E-01 + 0.80 2.24E-01 1.36E+00 7.25E-01 + 0.90 5.91E-02 1.38E+00 8.62E-01 + 1.00 3.12E-11 1.38E+00 1.00E+00 + 1.00 0.00E+00 1.38E+00 1.00E+00 +#Tower_ShapeFunction_DOF2_Shape (Unit angle at K, with K fixed) + H [m] K [rad/m] V [rad] U [m] + 0.00 -2.19E+01 0.00E+00 0.00E+00 + 0.10 -1.13E+01 -1.66E+00 -9.18E-02 + 0.20 -1.27E+00 -2.29E+00 -2.97E-01 + 0.40 1.33E+01 -1.08E+00 -6.83E-01 + 0.49 1.61E+01 2.48E-01 -7.22E-01 + 0.50 1.62E+01 4.10E-01 -7.18E-01 + 0.60 1.55E+01 1.99E+00 -5.98E-01 + 0.70 1.19E+01 3.37E+00 -3.27E-01 + 0.80 6.85E+00 4.30E+00 6.08E-02 + 0.90 2.12E+00 4.75E+00 5.18E-01 + 1.00 1.27E-09 4.86E+00 1.00E+00 + 1.00 0.00E+00 4.86E+00 1.00E+00 +### END TOWER DATA +############################################################################################### +### START FOUNDATION DATA +#Foundation_nSections 101 +#Foundation_Comment +1, Comment + +#Foundation_EAvg 210 [GPa] +#Foundation_RhoAvg 7850 [kg/m3] +#Foundation_Mass 900000 [kg] +#Foundation_KCorr 1 [-] (stiffness correction) +#Support_DampingIn 0.000 [log. decr.] +#Support_FreqIn 0.10 [Hz] +#Foundation_Htop 50.00 [m] (Distance from water level to tower bottom TODO) +#Foundation_WaterFillFactor 0.00 [-] (inside of pile filled to water level) +#Foundation_WaterLevelInPile 50.00 [m] +#Foundation_SectionData (Note: after the 4th column, format may vary depending on version) +101 h[m] D[m] t[mm] Mlump[kg] tmg[mm] rough Cm[-] Cd[-] + 0.00 8.197 44.8 0 0.0 0 1.00 0.20 + 1.00 8.197 44.8 0 0.0 0 1.00 0.20 + 2.00 8.197 44.8 0 0.0 0 1.00 0.20 + 3.00 8.197 44.8 0 0.0 0 1.00 0.20 + 4.00 8.197 44.8 0 0.0 0 1.00 0.20 + 5.00 8.197 44.8 0 0.0 0 1.00 0.20 + 6.00 8.197 44.8 0 0.0 0 1.00 0.20 + 7.00 8.197 44.8 0 0.0 0 1.00 0.20 + 8.00 8.197 44.8 0 0.0 0 1.00 0.20 + 9.00 8.197 44.8 0 0.0 0 1.00 0.20 + 10.00 8.197 44.8 0 0.0 0 1.00 0.20 + 11.00 8.197 44.8 0 0.0 0 1.00 0.20 + 12.00 8.197 44.8 0 0.0 0 1.00 0.20 + 13.00 8.197 44.8 0 0.0 0 1.00 0.20 + 14.00 8.197 44.8 0 0.0 0 1.00 0.20 + 15.00 8.197 44.8 0 0.0 0 1.00 0.20 + 16.00 8.197 44.8 0 0.0 0 1.00 0.20 + 17.00 8.197 44.8 0 0.0 0 1.00 0.20 + 18.00 8.197 44.8 0 0.0 0 1.00 0.20 + 19.00 8.197 44.8 0 0.0 0 1.00 0.20 + 20.00 8.197 44.8 0 0.0 0 1.00 0.20 + 21.00 8.197 44.8 0 0.0 0 1.00 0.20 + 22.00 8.197 44.8 0 0.0 0 1.00 0.20 + 23.00 8.197 44.8 0 0.0 0 1.00 0.20 + 24.00 8.197 44.8 0 0.0 0 1.00 0.20 + 25.00 8.197 44.8 0 0.0 0 1.00 0.20 + 26.00 8.197 44.8 0 0.0 0 1.00 0.20 + 27.00 8.197 44.8 0 0.0 0 1.00 0.20 + 28.00 8.197 44.8 0 0.0 0 1.00 0.20 + 29.00 8.197 44.8 0 0.0 0 1.00 0.20 + 30.00 8.197 44.8 0 0.0 0 1.00 0.20 + 31.00 8.197 44.8 0 0.0 0 1.00 0.20 + 32.00 8.197 44.8 0 0.0 0 1.00 0.20 + 33.00 8.197 44.8 0 0.0 0 1.00 0.20 + 34.00 8.197 44.8 0 0.0 0 1.00 0.20 + 35.00 8.197 44.8 0 0.0 0 1.00 0.20 + 36.00 8.197 44.8 0 0.0 0 1.00 0.20 + 37.00 8.197 44.8 0 0.0 0 1.00 0.20 + 38.00 8.197 44.8 0 0.0 0 1.00 0.20 + 39.00 8.197 44.8 0 0.0 0 1.00 0.20 + 40.00 8.197 44.8 0 0.0 0 1.00 0.20 + 41.00 8.197 44.8 0 0.0 0 1.00 0.20 + 42.00 8.197 44.8 0 0.0 0 1.00 0.21 + 43.00 8.197 44.8 0 0.0 0 1.00 0.21 + 44.00 8.197 44.8 0 0.0 0 1.00 0.21 + 45.00 8.197 44.8 0 0.0 0 1.00 0.22 + 46.00 8.197 44.8 0 0.0 0 1.00 0.22 + 47.00 8.197 44.8 0 0.0 0 1.00 0.23 + 48.00 8.197 44.8 0 0.0 0 1.00 0.21 + 49.00 8.197 44.8 0 0.0 0 1.00 0.21 + 50.00 8.197 44.8 0 0.0 0 1.00 0.22 + 51.00 8.197 44.8 0 0.0 0 1.00 0.22 + 52.00 8.197 44.8 0 0.0 0 1.00 0.22 + 53.00 8.197 44.8 0 0.0 0 1.00 0.23 + 54.00 8.197 44.8 0 0.0 0 1.00 0.23 + 55.00 8.197 44.8 0 0.0 0 1.00 0.23 + 56.00 8.197 44.8 0 0.0 0 1.00 0.23 + 57.00 8.197 44.8 0 0.0 0 1.00 0.23 + 58.00 8.197 44.8 0 0.0 0 1.00 0.23 + 59.00 8.197 44.8 0 0.0 0 1.00 0.23 + 60.00 8.197 44.8 0 0.0 0 1.00 0.23 + 61.00 8.197 44.8 0 0.0 0 1.00 0.23 + 62.00 8.197 44.8 0 0.0 0 1.00 0.23 + 63.00 8.197 44.8 0 0.0 0 1.00 0.23 + 64.00 8.197 44.8 0 0.0 0 1.00 0.23 + 65.00 8.197 44.8 0 0.0 0 1.00 0.23 + 66.00 8.197 44.8 0 0.0 0 1.00 0.23 + 67.00 8.197 44.8 0 0.0 0 1.00 0.23 + 68.00 8.197 44.8 0 0.0 0 1.00 0.23 + 69.00 8.197 44.8 0 0.0 0 1.00 0.23 + 70.00 8.197 44.8 0 0.0 0 1.00 0.23 + 71.00 8.197 44.8 0 0.0 0 1.00 0.23 + 72.00 8.197 44.8 0 0.0 0 1.00 0.23 + 73.00 8.197 44.8 0 0.0 0 1.00 0.23 + 74.00 8.197 44.8 0 0.0 0 1.00 0.23 + 75.00 8.197 44.8 0 0.0 0 1.00 0.23 + 76.00 8.197 44.8 0 0.0 0 1.00 0.23 + 77.00 8.197 44.8 0 0.0 0 1.00 0.23 + 78.00 8.197 44.8 0 0.0 0 1.00 0.23 + 79.00 8.197 44.8 0 0.0 0 1.00 0.23 + 80.00 8.197 44.8 0 0.0 0 1.00 0.23 + 81.00 8.197 44.8 0 0.0 0 1.00 0.23 + 82.00 8.197 44.8 0 0.0 0 1.00 0.23 + 83.00 8.197 44.8 0 0.0 0 1.00 0.23 + 84.00 8.197 44.8 0 0.0 0 1.00 0.23 + 85.00 8.197 44.8 0 0.0 0 1.00 0.23 + 86.00 8.197 44.8 0 0.0 0 1.00 0.23 + 87.00 8.197 44.8 0 0.0 0 1.00 0.23 + 88.00 8.197 44.8 0 0.0 0 1.00 0.23 + 89.00 8.197 44.8 0 0.0 0 1.00 0.23 + 90.00 8.197 44.8 0 0.0 0 1.00 0.23 + 91.00 8.197 44.8 0 0.0 0 1.00 0.23 + 92.00 8.197 44.8 0 0.0 0 1.00 0.23 + 93.00 8.197 44.8 0 0.0 0 1.00 0.23 + 94.00 8.197 44.8 0 0.0 0 1.00 0.23 + 95.00 8.197 44.8 0 0.0 0 1.00 0.23 + 96.00 8.197 44.8 0 0.0 0 1.00 0.23 + 97.00 8.197 44.8 0 0.0 0 1.00 0.23 + 98.00 8.197 44.8 0 0.0 0 1.00 0.23 + 99.00 8.197 44.8 0 0.0 0 1.00 0.23 + 100.00 8.197 44.8 0 0.0 0 1.00 0.23 +#Appurtenances_FileName data/Fake.app +#Appurtenances_Comment +#Appurtenances_SpreadFactor 1.00 +#Appurtenances_SectionData +0 Bottom Top OD phi Gap_to_MPile Applied +#Foundation_ShapeFunction_DOF1_Shape + H [m] K [rad/m] V [rad] U [m] + 0.00 6.00E-04 0.00E+00 0.00E+00 + 1.00 5.88E-04 5.94E-04 2.98E-04 + 2.00 5.76E-04 1.17E-03 1.18E-03 + 3.00 5.64E-04 1.75E-03 2.65E-03 + 4.00 5.52E-04 2.30E-03 4.67E-03 + 5.00 5.40E-04 2.85E-03 7.25E-03 + 6.00 5.28E-04 3.38E-03 1.03E-02 + 7.00 5.16E-04 3.91E-03 1.40E-02 + 8.00 5.04E-04 4.42E-03 1.82E-02 + 9.00 4.92E-04 4.91E-03 2.28E-02 + 10.00 4.80E-04 5.40E-03 2.80E-02 + 11.00 4.68E-04 5.87E-03 3.36E-02 + 12.00 4.56E-04 6.34E-03 3.97E-02 + 13.00 4.44E-04 6.79E-03 4.63E-02 + 14.00 4.32E-04 7.22E-03 5.33E-02 + 15.00 4.20E-04 7.65E-03 6.08E-02 + 16.00 4.08E-04 8.06E-03 6.86E-02 + 17.00 3.96E-04 8.47E-03 7.69E-02 + 18.00 3.84E-04 8.86E-03 8.55E-02 + 19.00 3.72E-04 9.23E-03 9.46E-02 + 20.00 3.60E-04 9.60E-03 1.04E-01 + 21.00 3.48E-04 9.95E-03 1.13E-01 + 22.00 3.36E-04 1.03E-02 1.23E-01 + 23.00 3.24E-04 1.06E-02 1.34E-01 + 24.00 3.12E-04 1.09E-02 1.45E-01 + 25.00 3.00E-04 1.12E-02 1.56E-01 + 26.00 2.88E-04 1.15E-02 1.68E-01 + 27.00 2.76E-04 1.18E-02 1.79E-01 + 28.00 2.64E-04 1.21E-02 1.91E-01 + 29.00 2.52E-04 1.23E-02 2.04E-01 + 30.00 2.40E-04 1.26E-02 2.16E-01 + 31.00 2.28E-04 1.28E-02 2.29E-01 + 32.00 2.16E-04 1.30E-02 2.42E-01 + 33.00 2.04E-04 1.32E-02 2.55E-01 + 34.00 1.92E-04 1.34E-02 2.68E-01 + 35.00 1.80E-04 1.36E-02 2.82E-01 + 36.00 1.68E-04 1.38E-02 2.95E-01 + 37.00 1.56E-04 1.39E-02 3.09E-01 + 38.00 1.44E-04 1.41E-02 3.23E-01 + 39.00 1.32E-04 1.42E-02 3.38E-01 + 40.00 1.20E-04 1.44E-02 3.52E-01 + 41.00 1.08E-04 1.45E-02 3.66E-01 + 42.00 9.60E-05 1.46E-02 3.81E-01 + 43.00 8.40E-05 1.47E-02 3.96E-01 + 44.00 7.20E-05 1.47E-02 4.10E-01 + 45.00 6.00E-05 1.48E-02 4.25E-01 + 46.00 4.80E-05 1.49E-02 4.40E-01 + 47.00 3.60E-05 1.49E-02 4.55E-01 + 48.00 2.40E-05 1.49E-02 4.70E-01 + 49.00 1.20E-05 1.49E-02 4.85E-01 + 50.00 -1.14E-18 1.50E-02 5.00E-01 + 51.00 -1.20E-05 1.49E-02 5.15E-01 + 52.00 -2.40E-05 1.49E-02 5.30E-01 + 53.00 -3.60E-05 1.49E-02 5.45E-01 + 54.00 -4.80E-05 1.49E-02 5.60E-01 + 55.00 -6.00E-05 1.48E-02 5.75E-01 + 56.00 -7.20E-05 1.47E-02 5.90E-01 + 57.00 -8.40E-05 1.47E-02 6.04E-01 + 58.00 -9.60E-05 1.46E-02 6.19E-01 + 59.00 -1.08E-04 1.45E-02 6.34E-01 + 60.00 -1.20E-04 1.44E-02 6.48E-01 + 61.00 -1.32E-04 1.42E-02 6.62E-01 + 62.00 -1.44E-04 1.41E-02 6.77E-01 + 63.00 -1.56E-04 1.39E-02 6.91E-01 + 64.00 -1.68E-04 1.38E-02 7.05E-01 + 65.00 -1.80E-04 1.36E-02 7.18E-01 + 66.00 -1.92E-04 1.34E-02 7.32E-01 + 67.00 -2.04E-04 1.32E-02 7.45E-01 + 68.00 -2.16E-04 1.30E-02 7.58E-01 + 69.00 -2.28E-04 1.28E-02 7.71E-01 + 70.00 -2.40E-04 1.26E-02 7.84E-01 + 71.00 -2.52E-04 1.23E-02 7.96E-01 + 72.00 -2.64E-04 1.21E-02 8.09E-01 + 73.00 -2.76E-04 1.18E-02 8.21E-01 + 74.00 -2.88E-04 1.15E-02 8.32E-01 + 75.00 -3.00E-04 1.12E-02 8.44E-01 + 76.00 -3.12E-04 1.09E-02 8.55E-01 + 77.00 -3.24E-04 1.06E-02 8.66E-01 + 78.00 -3.36E-04 1.03E-02 8.76E-01 + 79.00 -3.48E-04 9.95E-03 8.86E-01 + 80.00 -3.60E-04 9.60E-03 8.96E-01 + 81.00 -3.72E-04 9.23E-03 9.05E-01 + 82.00 -3.84E-04 8.86E-03 9.14E-01 + 83.00 -3.96E-04 8.47E-03 9.23E-01 + 84.00 -4.08E-04 8.06E-03 9.31E-01 + 85.00 -4.20E-04 7.65E-03 9.39E-01 + 86.00 -4.32E-04 7.22E-03 9.47E-01 + 87.00 -4.44E-04 6.79E-03 9.54E-01 + 88.00 -4.56E-04 6.34E-03 9.60E-01 + 89.00 -4.68E-04 5.87E-03 9.66E-01 + 90.00 -4.80E-04 5.40E-03 9.72E-01 + 91.00 -4.92E-04 4.91E-03 9.77E-01 + 92.00 -5.04E-04 4.42E-03 9.82E-01 + 93.00 -5.16E-04 3.91E-03 9.86E-01 + 94.00 -5.28E-04 3.38E-03 9.90E-01 + 95.00 -5.40E-04 2.85E-03 9.93E-01 + 96.00 -5.52E-04 2.30E-03 9.95E-01 + 97.00 -5.64E-04 1.75E-03 9.97E-01 + 98.00 -5.76E-04 1.17E-03 9.99E-01 + 99.00 -5.88E-04 5.94E-04 1.00E+00 + 100.00 -6.00E-04 -1.14E-16 1.00E+00 +#Foundation_ShapeFunction_DOF2_Shape + H [m] K [rad/m] V [rad] U [m] + 0.00 -2.00E-02 0.00E+00 0.00E+00 + 1.00 -1.94E-02 -1.97E-02 -9.90E-03 + 2.00 -1.88E-02 -3.88E-02 -3.92E-02 + 3.00 -1.82E-02 -5.73E-02 -8.73E-02 + 4.00 -1.76E-02 -7.52E-02 -1.54E-01 + 5.00 -1.70E-02 -9.25E-02 -2.38E-01 + 6.00 -1.64E-02 -1.09E-01 -3.38E-01 + 7.00 -1.58E-02 -1.25E-01 -4.56E-01 + 8.00 -1.52E-02 -1.41E-01 -5.89E-01 + 9.00 -1.46E-02 -1.56E-01 -7.37E-01 + 10.00 -1.40E-02 -1.70E-01 -9.00E-01 + 11.00 -1.34E-02 -1.84E-01 -1.08E+00 + 12.00 -1.28E-02 -1.97E-01 -1.27E+00 + 13.00 -1.22E-02 -2.09E-01 -1.47E+00 + 14.00 -1.16E-02 -2.21E-01 -1.69E+00 + 15.00 -1.10E-02 -2.33E-01 -1.91E+00 + 16.00 -1.04E-02 -2.43E-01 -2.15E+00 + 17.00 -9.80E-03 -2.53E-01 -2.40E+00 + 18.00 -9.20E-03 -2.63E-01 -2.66E+00 + 19.00 -8.60E-03 -2.72E-01 -2.92E+00 + 20.00 -8.00E-03 -2.80E-01 -3.20E+00 + 21.00 -7.40E-03 -2.88E-01 -3.48E+00 + 22.00 -6.80E-03 -2.95E-01 -3.78E+00 + 23.00 -6.20E-03 -3.01E-01 -4.07E+00 + 24.00 -5.60E-03 -3.07E-01 -4.38E+00 + 25.00 -5.00E-03 -3.13E-01 -4.69E+00 + 26.00 -4.40E-03 -3.17E-01 -5.00E+00 + 27.00 -3.80E-03 -3.21E-01 -5.32E+00 + 28.00 -3.20E-03 -3.25E-01 -5.64E+00 + 29.00 -2.60E-03 -3.28E-01 -5.97E+00 + 30.00 -2.00E-03 -3.30E-01 -6.30E+00 + 31.00 -1.40E-03 -3.32E-01 -6.63E+00 + 32.00 -8.00E-04 -3.33E-01 -6.96E+00 + 33.00 -2.00E-04 -3.33E-01 -7.30E+00 + 34.00 4.00E-04 -3.33E-01 -7.63E+00 + 35.00 1.00E-03 -3.33E-01 -7.96E+00 + 36.00 1.60E-03 -3.31E-01 -8.29E+00 + 37.00 2.20E-03 -3.29E-01 -8.62E+00 + 38.00 2.80E-03 -3.27E-01 -8.95E+00 + 39.00 3.40E-03 -3.24E-01 -9.28E+00 + 40.00 4.00E-03 -3.20E-01 -9.60E+00 + 41.00 4.60E-03 -3.16E-01 -9.92E+00 + 42.00 5.20E-03 -3.11E-01 -1.02E+01 + 43.00 5.80E-03 -3.05E-01 -1.05E+01 + 44.00 6.40E-03 -2.99E-01 -1.08E+01 + 45.00 7.00E-03 -2.92E-01 -1.11E+01 + 46.00 7.60E-03 -2.85E-01 -1.14E+01 + 47.00 8.20E-03 -2.77E-01 -1.17E+01 + 48.00 8.80E-03 -2.69E-01 -1.19E+01 + 49.00 9.40E-03 -2.60E-01 -1.22E+01 + 50.00 1.00E-02 -2.50E-01 -1.25E+01 + 51.00 1.06E-02 -2.40E-01 -1.27E+01 + 52.00 1.12E-02 -2.29E-01 -1.29E+01 + 53.00 1.18E-02 -2.17E-01 -1.32E+01 + 54.00 1.24E-02 -2.05E-01 -1.34E+01 + 55.00 1.30E-02 -1.92E-01 -1.36E+01 + 56.00 1.36E-02 -1.79E-01 -1.38E+01 + 57.00 1.42E-02 -1.65E-01 -1.39E+01 + 58.00 1.48E-02 -1.51E-01 -1.41E+01 + 59.00 1.54E-02 -1.36E-01 -1.42E+01 + 60.00 1.60E-02 -1.20E-01 -1.44E+01 + 61.00 1.66E-02 -1.03E-01 -1.45E+01 + 62.00 1.72E-02 -8.68E-02 -1.46E+01 + 63.00 1.78E-02 -6.93E-02 -1.46E+01 + 64.00 1.84E-02 -5.12E-02 -1.47E+01 + 65.00 1.90E-02 -3.25E-02 -1.47E+01 + 66.00 1.96E-02 -1.32E-02 -1.48E+01 + 67.00 2.02E-02 6.70E-03 -1.48E+01 + 68.00 2.08E-02 2.72E-02 -1.48E+01 + 69.00 2.14E-02 4.83E-02 -1.47E+01 + 70.00 2.20E-02 7.00E-02 -1.47E+01 + 71.00 2.26E-02 9.23E-02 -1.46E+01 + 72.00 2.32E-02 1.15E-01 -1.45E+01 + 73.00 2.38E-02 1.39E-01 -1.43E+01 + 74.00 2.44E-02 1.63E-01 -1.42E+01 + 75.00 2.50E-02 1.88E-01 -1.40E+01 + 76.00 2.56E-02 2.13E-01 -1.38E+01 + 77.00 2.62E-02 2.39E-01 -1.36E+01 + 78.00 2.68E-02 2.65E-01 -1.33E+01 + 79.00 2.74E-02 2.92E-01 -1.31E+01 + 80.00 2.80E-02 3.20E-01 -1.28E+01 + 81.00 2.86E-02 3.48E-01 -1.24E+01 + 82.00 2.92E-02 3.77E-01 -1.21E+01 + 83.00 2.98E-02 4.07E-01 -1.17E+01 + 84.00 3.04E-02 4.37E-01 -1.12E+01 + 85.00 3.10E-02 4.68E-01 -1.08E+01 + 86.00 3.16E-02 4.99E-01 -1.03E+01 + 87.00 3.22E-02 5.31E-01 -9.84E+00 + 88.00 3.28E-02 5.63E-01 -9.29E+00 + 89.00 3.34E-02 5.96E-01 -8.71E+00 + 90.00 3.40E-02 6.30E-01 -8.10E+00 + 91.00 3.46E-02 6.64E-01 -7.45E+00 + 92.00 3.52E-02 6.99E-01 -6.77E+00 + 93.00 3.58E-02 7.35E-01 -6.05E+00 + 94.00 3.64E-02 7.71E-01 -5.30E+00 + 95.00 3.70E-02 8.08E-01 -4.51E+00 + 96.00 3.76E-02 8.45E-01 -3.69E+00 + 97.00 3.82E-02 8.83E-01 -2.82E+00 + 98.00 3.88E-02 9.21E-01 -1.92E+00 + 99.00 3.94E-02 9.60E-01 -9.80E-01 + 100.00 4.00E-02 1.00E+00 1.75E-13 +### END FOUNDATION DATA +############################################################################################### +#### START SEASTATE DATA +#WaveFile Waves\Userdef.wko +#Environment_WaterDensity 1025 [kg/m3] +#Environment_CurrentSpeed 0 [m/s] +#Environment_CurrentExp 0 [-] +#Environment_CurrentDirection 0 [deg] +#Environment_WaveDirection 0 [deg] +#Environment_WaterDepth 50.00 [m] +#Environment_WaveHeight 0.00 [m] +#Environment_WavePeriod 12.00 [s] + +### END SEASTATE DATA +############################################################################################### +### START BLADE 1 DATA +#Blade_ModelName classic +#Blade_nSections 14 +#Blade_Length 9.500E-01 [m] +#Blade_Mass 9.500E-07 [kg] +#Blade_MassMoment_SMOM 4.522E-07 [kgm] +#Blade_MassMoment_SMOM1 4.522E-07 [kgm] +#Blade_MomentInertia 2.867E-07 [kgm2] +#Blade_ShapeFunctionFrequencies 61962742.583391317642.16861962742.583391317642.168 0.000 0.000 0.000 0.000 0.000 0.000 0.000 0.000 [Hz] +#Blade_ShapeFunction_DOF1_Name flap1 +#Blade_ShapeFunction_DOF1_Freq 61962742.583 [Hz] +#Blade_ShapeFunction_DOF1_Active 0 +#Blade_ShapeFunction_DOF1_Shape + x Uy Uz U_dir Vy Vz Ky Kz Vx Kx + 0.001 0.000000 0.000000 0.000 0.00000 0.000000 0.000000 3.891271 0.000000 0.000000 + 0.101 0.000000 0.018518 0.000 0.00000 0.360964 0.000000 3.327600 0.000000 0.000000 + 0.151 0.000000 0.040609 0.000 0.00000 0.520317 0.000000 3.046516 0.000000 0.000000 + 0.200 0.000000 0.069652 0.000 0.00000 0.662885 0.000000 2.772584 0.000000 0.000000 + 0.201 0.000000 0.070316 0.000 0.00000 0.665655 0.000000 2.767017 0.000000 0.000000 + 0.277 0.000000 0.128494 0.000 0.00000 0.860020 0.000000 2.347843 0.000000 0.000000 + 0.351 0.000000 0.198201 0.000 0.00000 1.019064 0.000000 1.950645 0.000000 0.000000 + 0.411 0.000000 0.262670 0.000 0.00000 1.126797 0.000000 1.640471 0.000000 0.000000 + 0.493 0.000000 0.360135 0.000 0.00000 1.244921 0.000000 1.240598 0.000000 0.000000 + 0.601 0.000000 0.500910 0.000 0.00000 1.353596 0.000000 0.771904 0.000000 0.000000 + 0.657 0.000000 0.577812 0.000 0.00000 1.390951 0.000000 0.562198 0.000000 0.000000 + 0.739 0.000000 0.693473 0.000 0.00000 1.426537 0.000000 0.305754 0.000000 0.000000 + 0.903 0.000000 0.930243 0.000 0.00000 1.453007 0.000000 0.017056 0.000000 0.000000 + 0.951 0.000000 1.000000 0.000 0.00000 1.453417 0.000000 0.000000 0.000000 0.000000 +#Blade_ShapeFunction_DOF2_Name flap2 +#Blade_ShapeFunction_DOF2_Freq 391317642.168 [Hz] +#Blade_ShapeFunction_DOF2_Active 0 +#Blade_ShapeFunction_DOF2_Shape + x Uy Uz U_dir Vy Vz Ky Kz Vx Kx + 0.001 0.000000 -0.000000 180.000 0.00000 0.000000 0.000000 -24.725038 0.000000 0.000000 + 0.101 0.000000 -0.102857 180.000 0.00000 -1.849398 0.000000 -12.261587 0.000000 0.000000 + 0.151 0.000000 -0.208144 180.000 0.00000 -2.311863 0.000000 -6.237015 0.000000 0.000000 + 0.200 0.000000 -0.326673 180.000 0.00000 -2.480368 0.000000 -0.640727 0.000000 0.000000 + 0.201 0.000000 -0.329154 180.000 0.00000 -2.480953 0.000000 -0.530868 0.000000 0.000000 + 0.277 0.000000 -0.511865 180.000 0.00000 -2.230183 0.000000 7.130093 0.000000 0.000000 + 0.351 0.000000 -0.652111 180.000 0.00000 -1.489115 0.000000 12.898757 0.000000 0.000000 + 0.411 0.000000 -0.716361 180.000 0.00000 -0.621215 0.000000 16.031242 0.000000 0.000000 + 0.493 0.000000 -0.711391 180.000 0.00000 0.766981 0.000000 17.827203 0.000000 0.000000 + 0.601 0.000000 -0.528154 180.000 0.00000 2.593288 0.000000 15.993304 0.000000 0.000000 + 0.657 0.000000 -0.359145 180.000 0.00000 3.419638 0.000000 13.519182 0.000000 0.000000 + 0.739 0.000000 -0.038562 180.000 0.00000 4.335083 0.000000 8.808747 0.000000 0.000000 + 0.903 0.000000 0.754234 0.000 0.00000 5.109884 0.000000 0.640048 0.000000 0.000000 + 0.951 0.000000 1.000000 0.000 0.00000 5.125245 0.000000 0.000000 0.000000 0.000000 +### END BLADE DATA +#nDOF 2/18 +############################################################################################### +### START INITIAL CONDITIONS +############################################################################################### +# InitialConditionFile InitNoEquilibrium.gxi +GX_iteration = No +# GX (init) + 0.000E+00 0.000E+00 +# GX0 (init) + 0.000E+00 0.000E+00 +# GXP (init) + 0.000E+00 0.000E+00 +### END INITIAL CONDITIONS +#GX (equilibrium) + 0.000E+00 0.000E+00 +#GX0 (equilibrium) + 0.000E+00 0.000E+00 +#GXP (equilibrium) + 0.000E+00 0.000E+00 +### START COMPONENT MATRICES +#Foundation_IsolatedMassMatrix (Foundation system - Foundation order) + 9.00E+05 0.00E+00 0.00E+00 0.00E+00 0.00E+00 0.00E+00 + 0.00E+00 4.94E+05 0.00E+00 0.00E+00 0.00E+00 9.02E+06 + 0.00E+00 0.00E+00 4.94E+05 0.00E+00 -9.02E+06 0.00E+00 + 0.00E+00 0.00E+00 0.00E+00 0.00E+00 0.00E+00 0.00E+00 + 0.00E+00 0.00E+00 -9.02E+06 0.00E+00 2.02E+08 0.00E+00 + 0.00E+00 9.02E+06 0.00E+00 0.00E+00 0.00E+00 2.02E+08 +#Foundation_IsolatedStiffnessMatrix (Foundation system - Foundation order) + 0.00E+00 0.00E+00 0.00E+00 0.00E+00 0.00E+00 0.00E+00 + 0.00E+00 2.40E+07 0.00E+00 0.00E+00 0.00E+00 1.20E+09 + 0.00E+00 0.00E+00 2.40E+07 0.00E+00 -1.20E+09 0.00E+00 + 0.00E+00 0.00E+00 0.00E+00 0.00E+00 0.00E+00 0.00E+00 + 0.00E+00 0.00E+00 -1.20E+09 0.00E+00 8.00E+10 0.00E+00 + 0.00E+00 1.20E+09 0.00E+00 0.00E+00 0.00E+00 8.00E+10 +#Foundation_IsolatedStiffnessMatrixCorrection (Foundation system - Foundation order) + 0.00E+00 0.00E+00 0.00E+00 0.00E+00 0.00E+00 0.00E+00 + 0.00E+00 -5.30E+04 0.00E+00 0.00E+00 0.00E+00 -8.82E+05 + 0.00E+00 0.00E+00 -5.30E+04 0.00E+00 8.82E+05 0.00E+00 + 0.00E+00 0.00E+00 0.00E+00 0.00E+00 0.00E+00 0.00E+00 + 0.00E+00 0.00E+00 8.83E+05 0.00E+00 -2.94E+07 0.00E+00 + 0.00E+00 -8.83E+05 0.00E+00 0.00E+00 0.00E+00 -2.94E+07 +#Foundation_IsolatedDampingMatrix (Foundation system - Foundation order) + 0.00E+00 0.00E+00 0.00E+00 0.00E+00 0.00E+00 0.00E+00 + 0.00E+00 1.22E+03 0.00E+00 0.00E+00 0.00E+00 6.08E+04 + 0.00E+00 0.00E+00 1.22E+03 0.00E+00 -6.08E+04 0.00E+00 + 0.00E+00 0.00E+00 0.00E+00 0.00E+00 0.00E+00 0.00E+00 + 0.00E+00 0.00E+00 -6.08E+04 0.00E+00 4.05E+06 0.00E+00 + 0.00E+00 6.08E+04 0.00E+00 0.00E+00 0.00E+00 4.05E+06 +#Tower_MassMatrix (Tower system) + 1.12E-02 0.00E+00 0.00E+00 0.00E+00 0.00E+00 0.00E+00 0.00E+00 0.00E+00 0.00E+00 0.00E+00 + 0.00E+00 1.12E-02 0.00E+00 0.00E+00 0.00E+00 -5.62E-03 0.00E+00 0.00E+00 4.43E-03 -2.40E-03 + 0.00E+00 0.00E+00 1.12E-02 0.00E+00 5.62E-03 0.00E+00 4.43E-03 -2.40E-03 0.00E+00 0.00E+00 + 0.00E+00 0.00E+00 0.00E+00 0.00E+00 0.00E+00 0.00E+00 0.00E+00 0.00E+00 0.00E+00 0.00E+00 + 0.00E+00 0.00E+00 5.62E-03 0.00E+00 3.75E-03 0.00E+00 3.20E-03 -4.94E-04 0.00E+00 0.00E+00 + 0.00E+00 -5.62E-03 0.00E+00 0.00E+00 0.00E+00 3.75E-03 0.00E+00 0.00E+00 -3.20E-03 4.94E-04 + 0.00E+00 0.00E+00 4.43E-03 0.00E+00 3.20E-03 0.00E+00 2.81E-03 -2.85E-06 0.00E+00 0.00E+00 + 0.00E+00 0.00E+00 -2.40E-03 0.00E+00 -4.94E-04 0.00E+00 -2.85E-06 2.77E-03 0.00E+00 0.00E+00 + 0.00E+00 4.43E-03 0.00E+00 0.00E+00 0.00E+00 -3.20E-03 0.00E+00 0.00E+00 2.81E-03 -2.85E-06 + 0.00E+00 -2.40E-03 0.00E+00 0.00E+00 0.00E+00 4.94E-04 0.00E+00 0.00E+00 -2.85E-06 2.77E-03 +#Tower_IsolatedMassMatrix (Tower system) + 2.81E-03 -2.85E-06 0.00E+00 0.00E+00 + -2.85E-06 2.77E-03 0.00E+00 0.00E+00 + 0.00E+00 0.00E+00 2.81E-03 -2.85E-06 + 0.00E+00 0.00E+00 -2.85E-06 2.77E-03 +#Tower_IsolatedStiffnessMatrix (Tower system) + 5.76E+12 0.00E+00 0.00E+00 0.00E+00 + 0.00E+00 2.31E+14 0.00E+00 0.00E+00 + 0.00E+00 0.00E+00 5.76E+12 0.00E+00 + 0.00E+00 0.00E+00 0.00E+00 2.31E+14 +#Tower_IsolatedStiffnessMatrixCorrection (Tower system) + -4.30E-02 -1.05E-02 0.00E+00 0.00E+00 + -1.18E-02 -2.29E-01 0.00E+00 0.00E+00 + 0.00E+00 0.00E+00 -4.30E-02 -1.05E-02 + 0.00E+00 0.00E+00 -1.18E-02 -2.29E-01 +#Tower_IsolatedDampingMatrix (Tower system) + 4.05E+00 0.00E+00 0.00E+00 0.00E+00 + 0.00E+00 2.55E+01 0.00E+00 0.00E+00 + 0.00E+00 0.00E+00 4.05E+00 0.00E+00 + 0.00E+00 0.00E+00 0.00E+00 2.55E+01 + +#Blade_GX + +#Blade_IsolatedMassMatrix (Blade system) + 9.5000E-07 0.0000E+00 0.0000E+00 0.0000E+00 -0.0000E+00 0.0000E+00 + 0.0000E+00 9.5000E-07 0.0000E+00 0.0000E+00 0.0000E+00 4.5220E-07 + 0.0000E+00 0.0000E+00 9.5000E-07 0.0000E+00 -4.5220E-07 0.0000E+00 + 0.0000E+00 0.0000E+00 0.0000E+00 0.0000E+00 0.0000E+00 0.0000E+00 + -0.0000E+00 0.0000E+00 -4.5220E-07 0.0000E+00 2.8670E-07 0.0000E+00 + 0.0000E+00 4.5220E-07 0.0000E+00 0.0000E+00 0.0000E+00 2.8670E-07 +#Blade_IsolatedStiffnessMatrix (Blade system) + +#Blade_IsolatedDampingMatrix (Blade system) + +### END COMPONENT MATRICES +############################################################################################### +#EVA_MassMatrix + 4.936E+05 0.000E+00 + 0.000E+00 4.936E+05 +#EVA_StiffnessMatrix + 2.395E+07 0.000E+00 + 0.000E+00 2.395E+07 +#EVA_DampingMatrix + 1.216E+03 0.000E+00 + 0.000E+00 1.216E+03 +#EVA_Eigenvectors + 0.000E+00 1.423E-03 + 1.423E-03 0.000E+00 +#EVA_Eigenfrequencies + 1.10851E+00 1.10851E+00 +#EVA_Eigenvalues + 4.851E+01 4.851E+01 +#EVA_Damping + 2.463E-03 2.463E-03 +#EVA_LogDec + 1.111E-03 1.111E-03 +#EVA_SummaryTable ; Eigenfrequencies and eigenvectors (distribution of energy) + + EV no. : 1 2 + + LogDec : 0.001 0.001 + DOF f (Hz) : 1.109 1.109 + + 2 Found : u_z 0 100 + 4 Found : u_y 100 0 + +#EVA_DTMax 0.301 [s] (recommended for stability) +############################################################################################### +#DOF_SummaryTable + dof mode on/off ShapeF Freq. [Hz] Damp. [log.dec.] + 1 Found : u_x stiff + 2 Found : u_z dynamic 0.00 (Hz) 0.000 + 3 Found : fi_y stiff + 4 Found : u_y dynamic 0.00 (Hz) 0.000 + 5 Found : fi_z stiff + 6 Found : fi_x stiff + 7 Tower : Long1 stiff + 8 Tower : Long2 stiff + 9 Tower : Lat1 stiff + 10 Tower : Lat2 stiff + 11 TwrNac: yaw stiff + 12 Nacell: tilt stiff + 13 Shaft : rot stiff + 14 Shaft : bendx stiff + 15 Shaft : bendy stiff + 16 Blade1: flap stiff + 17 Blade1: flap stiff + 18 Blade1: edge stiff + 19 Blade1: edge stiff + 20 Blade1: ---- stiff + 21 Blade1: ---- stiff + 22 Blade1: ---- stiff + 23 Blade1: ---- stiff + 24 Blade1: ---- stiff + 25 Blade1: ---- stiff + 26 Blade1: ---- stiff + 27 Blade1: ---- stiff + 28 Blade2: flap stiff + 29 Blade2: flap stiff + 30 Blade2: edge stiff + 31 Blade2: edge stiff + 32 Blade2: ---- stiff + 33 Blade2: ---- stiff + 34 Blade2: ---- stiff + 35 Blade2: ---- stiff + 36 Blade2: ---- stiff + 37 Blade2: ---- stiff + 38 Blade2: ---- stiff + 39 Blade2: ---- stiff + 40 Blade3: flap stiff + 41 Blade3: flap stiff + 42 Blade3: edge stiff + 43 Blade3: edge stiff + 44 Blade3: ---- stiff + 45 Blade3: ---- stiff + 46 Blade3: ---- stiff + 47 Blade3: ---- stiff + 48 Blade3: ---- stiff + 49 Blade3: ---- stiff + 50 Blade3: ---- stiff + 51 Blade3: ---- stiff + 52 Shaft : tors stiff + +############################################################################################### + +#RNA_COG_AboveTower 0.00 [m] (above tower top) + +#Global_Mode1_Shape (Extracted to RNA COG) + x_SB Lat1_(1) Long1_(2) +Fnd 0.00 0.000E+00 0.000E+00 +Fnd 1.00 2.980E-04 2.980E-04 +Fnd 2.00 1.184E-03 1.184E-03 +Fnd 3.00 2.646E-03 2.646E-03 +Fnd 4.00 4.672E-03 4.672E-03 +Fnd 5.00 7.250E-03 7.250E-03 +Fnd 6.00 1.036E-02 1.036E-02 +Fnd 7.00 1.401E-02 1.401E-02 +Fnd 8.00 1.818E-02 1.818E-02 +Fnd 9.00 2.284E-02 2.284E-02 +Fnd 10.00 2.800E-02 2.800E-02 +Fnd 11.00 3.364E-02 3.364E-02 +Fnd 12.00 3.974E-02 3.974E-02 +Fnd 13.00 4.631E-02 4.631E-02 +Fnd 14.00 5.331E-02 5.331E-02 +Fnd 15.00 6.075E-02 6.075E-02 +Fnd 16.00 6.861E-02 6.861E-02 +Fnd 17.00 7.687E-02 7.687E-02 +Fnd 18.00 8.554E-02 8.554E-02 +Fnd 19.00 9.458E-02 9.458E-02 +Fnd 20.00 1.040E-01 1.040E-01 +Fnd 21.00 1.137E-01 1.137E-01 +Fnd 22.00 1.239E-01 1.239E-01 +Fnd 23.00 1.344E-01 1.344E-01 +Fnd 24.00 1.452E-01 1.452E-01 +Fnd 25.00 1.563E-01 1.563E-01 +Fnd 26.00 1.676E-01 1.676E-01 +Fnd 27.00 1.793E-01 1.793E-01 +Fnd 28.00 1.913E-01 1.913E-01 +Fnd 29.00 2.035E-01 2.035E-01 +Fnd 30.00 2.160E-01 2.160E-01 +Fnd 31.00 2.287E-01 2.287E-01 +Fnd 32.00 2.417E-01 2.417E-01 +Fnd 33.00 2.548E-01 2.548E-01 +Fnd 34.00 2.682E-01 2.682E-01 +Fnd 35.00 2.818E-01 2.818E-01 +Fnd 36.00 2.955E-01 2.955E-01 +Fnd 37.00 3.094E-01 3.094E-01 +Fnd 38.00 3.235E-01 3.235E-01 +Fnd 39.00 3.377E-01 3.377E-01 +Fnd 40.00 3.520E-01 3.520E-01 +Fnd 41.00 3.665E-01 3.665E-01 +Fnd 42.00 3.810E-01 3.810E-01 +Fnd 43.00 3.957E-01 3.957E-01 +Fnd 44.00 4.104E-01 4.104E-01 +Fnd 45.00 4.253E-01 4.253E-01 +Fnd 46.00 4.401E-01 4.401E-01 +Fnd 47.00 4.551E-01 4.551E-01 +Fnd 48.00 4.700E-01 4.700E-01 +Fnd 49.00 4.850E-01 4.850E-01 +Fnd 50.00 5.000E-01 5.000E-01 +Fnd 51.00 5.150E-01 5.150E-01 +Fnd 52.00 5.300E-01 5.300E-01 +Fnd 53.00 5.449E-01 5.449E-01 +Fnd 54.00 5.599E-01 5.599E-01 +Fnd 55.00 5.748E-01 5.748E-01 +Fnd 56.00 5.896E-01 5.896E-01 +Fnd 57.00 6.043E-01 6.043E-01 +Fnd 58.00 6.190E-01 6.190E-01 +Fnd 59.00 6.335E-01 6.335E-01 +Fnd 60.00 6.480E-01 6.480E-01 +Fnd 61.00 6.623E-01 6.623E-01 +Fnd 62.00 6.765E-01 6.765E-01 +Fnd 63.00 6.906E-01 6.906E-01 +Fnd 64.00 7.045E-01 7.045E-01 +Fnd 65.00 7.183E-01 7.183E-01 +Fnd 66.00 7.318E-01 7.318E-01 +Fnd 67.00 7.452E-01 7.452E-01 +Fnd 68.00 7.583E-01 7.583E-01 +Fnd 69.00 7.713E-01 7.713E-01 +Fnd 70.00 7.840E-01 7.840E-01 +Fnd 71.00 7.965E-01 7.965E-01 +Fnd 72.00 8.087E-01 8.087E-01 +Fnd 73.00 8.207E-01 8.207E-01 +Fnd 74.00 8.324E-01 8.324E-01 +Fnd 75.00 8.438E-01 8.438E-01 +Fnd 76.00 8.548E-01 8.548E-01 +Fnd 77.00 8.656E-01 8.656E-01 +Fnd 78.00 8.761E-01 8.761E-01 +Fnd 79.00 8.862E-01 8.862E-01 +Fnd 80.00 8.960E-01 8.960E-01 +Fnd 81.00 9.054E-01 9.054E-01 +Fnd 82.00 9.145E-01 9.145E-01 +Fnd 83.00 9.231E-01 9.231E-01 +Fnd 84.00 9.314E-01 9.314E-01 +Fnd 85.00 9.393E-01 9.393E-01 +Fnd 86.00 9.467E-01 9.467E-01 +Fnd 87.00 9.537E-01 9.537E-01 +Fnd 88.00 9.603E-01 9.603E-01 +Fnd 89.00 9.664E-01 9.664E-01 +Fnd 90.00 9.720E-01 9.720E-01 +Fnd 91.00 9.772E-01 9.772E-01 +Fnd 92.00 9.818E-01 9.818E-01 +Fnd 93.00 9.860E-01 9.860E-01 +Fnd 94.00 9.896E-01 9.896E-01 +Fnd 95.00 9.928E-01 9.928E-01 +Fnd 96.00 9.953E-01 9.953E-01 +Fnd 97.00 9.974E-01 9.974E-01 +Fnd 98.00 9.988E-01 9.988E-01 +Fnd 99.00 9.997E-01 9.997E-01 +Fnd 100.00 1.000E+00 1.000E+00 +Twr 100.00 1.000E+00 1.000E+00 +Twr 100.10 1.000E+00 1.000E+00 +Twr 100.20 1.000E+00 1.000E+00 +Twr 100.40 1.000E+00 1.000E+00 +Twr 100.49 1.000E+00 1.000E+00 +Twr 100.50 1.000E+00 1.000E+00 +Twr 100.60 1.000E+00 1.000E+00 +Twr 100.70 1.000E+00 1.000E+00 +Twr 100.80 1.000E+00 1.000E+00 +Twr 100.90 1.000E+00 1.000E+00 +Twr 101.00 1.000E+00 1.000E+00 +COG 101.00 1.000E+00 1.000E+00 From e82647a40f065c9d595c5be94b1ae281fcd8aee8 Mon Sep 17 00:00:00 2001 From: Emmanuel Branlard Date: Fri, 20 Oct 2023 17:07:08 -0600 Subject: [PATCH 123/124] Tools: update for pyDatView scripter --- pyFAST/common.py | 3 + pyFAST/input_output/wetb/hawc2/htc_file.py | 4 +- pyFAST/postpro/postpro.py | 55 +++++++++++++++ pyFAST/tools/pandalib.py | 79 ++++++++++++++++++++++ 4 files changed, 139 insertions(+), 2 deletions(-) create mode 100644 pyFAST/common.py diff --git a/pyFAST/common.py b/pyFAST/common.py new file mode 100644 index 0000000..970b031 --- /dev/null +++ b/pyFAST/common.py @@ -0,0 +1,3 @@ + +class PYFASTException(Exception): + pass diff --git a/pyFAST/input_output/wetb/hawc2/htc_file.py b/pyFAST/input_output/wetb/hawc2/htc_file.py index 6b6bf3a..ca3237c 100644 --- a/pyFAST/input_output/wetb/hawc2/htc_file.py +++ b/pyFAST/input_output/wetb/hawc2/htc_file.py @@ -157,8 +157,8 @@ def isfile_case_insensitive(f): relpath = "../" * np.argmax(found) return abspath(pjoin(os.path.dirname(self.filename), relpath)) else: - raise ValueError( - "Modelpath cannot be autodetected for '%s'.\nInput files not found near htc file" % self.filename) + print("Modelpath cannot be autodetected for '%s'.\nInput files not found near htc file" % self.filename) + return 'unknown' def load(self): self.contents = OrderedDict() diff --git a/pyFAST/postpro/postpro.py b/pyFAST/postpro/postpro.py index 0375b28..79bdd8d 100644 --- a/pyFAST/postpro/postpro.py +++ b/pyFAST/postpro/postpro.py @@ -4,10 +4,14 @@ import numpy as np import re +import pyFAST.input_output as weio +from pyFAST.common import PYFASTException as WELIBException + # --- fast libraries from pyFAST.input_output.fast_input_file import FASTInputFile from pyFAST.input_output.fast_output_file import FASTOutputFile from pyFAST.input_output.fast_input_deck import FASTInputDeck +import pyFAST.fastfarm.fastfarm as fastfarm # --------------------------------------------------------------------------------} # --- Tools for IO @@ -903,6 +907,57 @@ def spanwisePostPro(FST_In=None,avgMethod='constantwindow',avgParam=5,out_ext='. # Combine all into a dictionary return out +def radialAvg(filename, avgMethod, avgParam, raw_name='', df=None, raiseException=True): + """ + Wrapper function, for instance used by pyDatView apply either: + spanwisePostPro or spanwisePostProFF (FAST.Farm) + """ + + base,out_ext = os.path.splitext(filename) + if df is None: + df = FASTOutputFile(filename).toDataFrame() + + # --- Detect if it's a FAST Farm file + sCols = ''.join(df.columns) + if sCols.find('WkDf')>1 or sCols.find('CtT')>0: + # --- FAST FARM files + Files=[base+ext for ext in ['.fstf','.FSTF','.Fstf','.fmas','.FMAS','.Fmas'] if os.path.exists(base+ext)] + if len(Files)==0: + fst_in=None + #raise Exception('Error: No .fstf file found with name: '+base+'.fstf') + else: + fst_in=Files[0] + + dfRad,_,dfDiam = fastfarm.spanwisePostProFF(fst_in,avgMethod=avgMethod,avgParam=avgParam,D=1,df=df) + dfs_new = [dfRad,dfDiam] + names_new=[raw_name+'_rad', raw_name+'_diam'] + else: + # --- FAST files + # HACK for AD file to find the right .fst file + iDotAD=base.lower().find('.ad') + if iDotAD>1: + base=base[:iDotAD] + # + Files=[base+ext for ext in ['.fst','.FST','.Fst','.dvr','.Dvr','.DVR'] if os.path.exists(base+ext)] + if len(Files)==0: + fst_in=None + #raise Exception('Error: No .fst file found with name: '+base+'.fst') + else: + fst_in=Files[0] + + try: + out = spanwisePostPro(fst_in, avgMethod=avgMethod, avgParam=avgParam, out_ext=out_ext, df = df) + dfRadED=out['ED_bld']; dfRadAD = out['AD']; dfRadBD = out['BD'] + dfs_new = [dfRadAD, dfRadED, dfRadBD] + names_new=[raw_name+'_AD', raw_name+'_ED', raw_name+'_BD'] + except: + if raiseException: + raise + else: + print('[WARN] radialAvg failed for filename {}'.format(filename)) + dfs_new =[None] + names_new=[''] + return dfs_new, names_new def spanwisePostProRows(df, FST_In=None): """ diff --git a/pyFAST/tools/pandalib.py b/pyFAST/tools/pandalib.py index fb648f4..5da1833 100644 --- a/pyFAST/tools/pandalib.py +++ b/pyFAST/tools/pandalib.py @@ -128,3 +128,82 @@ def remap_df(df, ColMap, bColKeepNewOnly=False, inPlace=False, dataDict=None, ve print('[WARN] Signals missing and omitted for ColKeep:\n '+'\n '.join(ColKeepMiss)) df=df[ColKeepSafe] return df + +def changeUnits(df, flavor='SI', inPlace=True): + """ Change units of a dataframe + + # TODO harmonize with dfToSIunits in welib.fast.tools.lin.py ! + """ + def splitunit(s): + iu=s.rfind('[') + if iu>0: + return s[:iu], s[iu+1:].replace(']','') + else: + return s, '' + def change_units_to_WE(s, c): + """ + Change units to wind energy units + s: channel name (string) containing units, typically 'speed_[rad/s]' + c: channel (array) + """ + svar, u = splitunit(s) + u=u.lower() + scalings = {} + # OLD = NEW + scalings['rad/s'] = (30/np.pi,'rpm') # TODO decide + scalings['rad' ] = (180/np.pi,'deg') + scalings['n'] = (1e-3, 'kN') + scalings['nm'] = (1e-3, 'kNm') + scalings['n-m'] = (1e-3, 'kNm') + scalings['n*m'] = (1e-3, 'kNm') + scalings['w'] = (1e-3, 'kW') + if u in scalings.keys(): + scale, new_unit = scalings[u] + s = svar+'['+new_unit+']' + c *= scale + return s, c + + def change_units_to_SI(s, c): + """ + Change units to SI units + TODO, a lot more units conversion needed...will add them as we go + s: channel name (string) containing units, typically 'speed_[rad/s]' + c: channel (array) + """ + svar, u = splitunit(s) + u=u.lower() + scalings = {} + # OLD = NEW + scalings['rpm'] = (np.pi/30,'rad/s') + scalings['rad' ] = (180/np.pi,'deg') + scalings['deg/s' ] = (np.pi/180,'rad/s') + scalings['kn'] = (1e3, 'N') + scalings['knm'] = (1e3, 'Nm') + scalings['kn-m'] = (1e3, 'Nm') + scalings['kn*m'] = (1e3, 'Nm') + scalings['kw'] = (1e3, 'W') + if u in scalings.keys(): + scale, new_unit = scalings[u] + s = svar+'['+new_unit+']' + c *= scale + return s, c + + if not inPlace: + raise NotImplementedError() + + if flavor == 'WE': + cols = [] + for i, colname in enumerate(df.columns): + colname_new, df.iloc[:,i] = change_units_to_WE(colname, df.iloc[:,i]) + cols.append(colname_new) + df.columns = cols + elif flavor == 'SI': + cols = [] + for i, colname in enumerate(df.columns): + colname_new, df.iloc[:,i] = change_units_to_SI(colname, df.iloc[:,i]) + cols.append(colname_new) + df.columns = cols + else: + raise NotImplementedError(flavor) + return df + From ad2558b2a8a88af82d6c3af3a6c8ed80cfb16946 Mon Sep 17 00:00:00 2001 From: Emmanuel Branlard Date: Fri, 20 Oct 2023 17:18:36 -0600 Subject: [PATCH 124/124] Update of version to 3.5.1 --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index 15a2799..d5c0c99 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.3.0 +3.5.1