From a0bed9faa518b4d115716561d621b523bc0eefcc Mon Sep 17 00:00:00 2001 From: Yuksel-Rudy Date: Mon, 8 Sep 2025 15:28:58 -0600 Subject: [PATCH 01/19] Adding features of including Hs, Tp, and current forces into the openfast_toolbox --- .../fastfarm/FASTFarmCaseCreation.py | 292 +++++++++++------- .../examples/Ex3_FFarmCompleteSetup.py | 2 +- 2 files changed, 184 insertions(+), 110 deletions(-) diff --git a/openfast_toolbox/fastfarm/FASTFarmCaseCreation.py b/openfast_toolbox/fastfarm/FASTFarmCaseCreation.py index e479169..d88c688 100644 --- a/openfast_toolbox/fastfarm/FASTFarmCaseCreation.py +++ b/openfast_toolbox/fastfarm/FASTFarmCaseCreation.py @@ -94,6 +94,9 @@ def __init__(self, shear, TIvalue, inflow_deg, + waveHs = None, + waveTp = None, + hydroForce = None, dt_high_les = None, ds_high_les = None, extent_high = None, @@ -133,6 +136,14 @@ def __init__(self, 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 + waveHs: list of scalars or single scalar + Wave heights to sweep on. Accepts a list or single value. + If not given, seaState does not change + waveTp: list of scalars or single scalar + Wave periods to sweep on. Accepts a list or single value. + If not given, seaState does not change + hydroForce: list of scalars or single scalar + Additional force in the x-axis to be added to hydrodyn files to simulate current. 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 @@ -196,6 +207,9 @@ def __init__(self, self.shear = shear self.TIvalue = TIvalue self.inflow_deg = inflow_deg + self.waveHs = waveHs + self.waveTp = waveTp + self.hydroForce = hydroForce self.dt_high_les = dt_high_les self.ds_high_les = ds_high_les self.dt_low_les = dt_low_les @@ -258,6 +272,8 @@ def __repr__(self): 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' - Significant wave height: {self.waveHs}\n' + s += f' - Peak Wave period: {self.waveTp}\n' s += f'\nCase details:\n' s += f' - Number of conditions: {self.nConditions}\n' for c in self.condDirList: @@ -379,7 +395,8 @@ def _checkInputs(self): 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 - + self.waveHs = [self.waveHs] if isinstance(self.waveHs,(float,int)) else self.waveHs + self.waveTp = [self.waveTp] if isinstance(self.waveTp,(float,int)) else self.waveTp # Fill turbine parameters arrays if not given if self.yaw_init is None: yaw = np.ones((1,self.nTurbines))*0 @@ -590,9 +607,14 @@ def _create_dir_structure(self): Vhub_ = self.allCond['vhub' ].isel(cond=cond).values shear_ = self.allCond['shear' ].isel(cond=cond).values tivalue_ = self.allCond['TIvalue' ].isel(cond=cond).values + if self.waveHs is not None and self.waveTp is not None: + waveHs_ = self.allCond['waveHs' ].isel(cond=cond).values + waveTp_ = self.allCond['waveTp' ].isel(cond=cond).values - # Set current path name string - condStr = f'Cond{cond:02d}_v{Vhub_:04.1f}_PL{shear_}_TI{tivalue_}' + # Set current path name string + condStr = f'Cond{cond:02d}_v{Vhub_:04.1f}_PL{shear_}_TI{tivalue_}_Hs{waveHs_:04.1f}_Tp{waveTp_:04.1f}' + else: + condStr = f'Cond{cond:02d}_v{Vhub_:04.1f}_PL{shear_}_TI{tivalue_}' condDirList.append(condStr) condPath = os.path.join(self.path, condStr) @@ -662,12 +684,23 @@ def copyTurbineFilesForEachCase(self, writeFiles=True): # Update parameters to be changed in the SeaState files if self.hasSS: - self.SeaStateFile['WaveHs'] = self.bins.sel(wspd=Vhub_, method='nearest').WaveHs.values - self.SeaStateFile['WaveTp'] = self.bins.sel(wspd=Vhub_, method='nearest').WaveTp.values - self.SeaStateFile['WvHiCOffD'] = 2.0*np.pi/self.SeaStateFile['WaveTp'] - self.SeaStateFile['WvLowCOffS'] = 2.0*np.pi/self.SeaStateFile['WaveTp'] - if writeFiles: - self.SeaStateFile.write(os.path.join(currPath, self.SSfilename)) + if self.waveHs is not None and self.waveTp is not None: + self.SeaStateFile['WaveHs'] = self.waveHs[cond] + self.SeaStateFile['WaveTp'] = self.waveTp[cond] + self.SeaStateFile['WvHiCOffD'] = 2.0*np.pi/self.SeaStateFile['WaveTp'] + self.SeaStateFile['WvLowCOffS'] = 2.0*np.pi/self.SeaStateFile['WaveTp'] + for seed in range(self.nSeeds): + seedPath = os.path.join(currPath, f'Seed_{seed}') + self.SeaStateFile['WaveSeed(1)'] = self.seedValues[seed] + if writeFiles: + self.SeaStateFile.write(os.path.join(seedPath, self.SSfilename)) + else: + self.SeaStateFile['WaveHs'] = self.bins.sel(wspd=Vhub_, method='nearest').WaveHs.values + self.SeaStateFile['WaveTp'] = self.bins.sel(wspd=Vhub_, method='nearest').WaveTp.values + self.SeaStateFile['WvHiCOffD'] = 2.0*np.pi/self.SeaStateFile['WaveTp'] + self.SeaStateFile['WvLowCOffS'] = 2.0*np.pi/self.SeaStateFile['WaveTp'] + if writeFiles: + self.SeaStateFile.write(os.path.join(currPath, self.SSfilename)) # HydroDyn (farm-wide) if self.hasHD and writeFiles: @@ -816,6 +849,8 @@ def copyTurbineFilesForEachCase(self, writeFiles=True): # Update each turbine's HydroDyn if self.hasHD: self.HydroDynFile['PtfmRefY'] = self.allCases.sel(case=case, turbine=t)['phi'].values + if self.hydroForce: + self.HydroDynFile['KDAdd'][0] = f' {self.hydroForce} 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]' if writeFiles: self.HydroDynFile.write(os.path.join(currPath,f'{self.HDfilename}{t+1}_mod.dat')) @@ -829,87 +864,89 @@ def copyTurbineFilesForEachCase(self, writeFiles=True): if writeFiles and self.hasBath: if t==0: shutilcopy2_untilSuccessful(self.bathfilepath, os.path.join(currPath, self.bathfilename)) - # 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 self.hasHD: - self.turbineFile['CompHydro'] = 1 - if self.multi_HD: - self.turbineFile['HydroFile'] = f'"{self.HDfilename}{t+1}_mod.dat"' + for seed in range(self.nSeeds): + seedPath = os.path.join(currPath, f'Seed_{seed}') + # 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 self.hasHD: + self.turbineFile['CompHydro'] = 1 + if self.multi_HD: + self.turbineFile['HydroFile'] = f'"../{self.HDfilename}{t+1}_mod.dat"' + else: + self.turbineFile['HydroFile'] = f'"../{self.HDfilename}"' else: - self.turbineFile['HydroFile'] = f'"{self.HDfilename}"' - else: - self.turbineFile['CompHydro'] = 0 - self.turbineFile['HydroFile'] = f'"unused"' + self.turbineFile['CompHydro'] = 0 + self.turbineFile['HydroFile'] = f'"unused"' - if self.hasSS: - self.turbineFile['CompSeaSt'] = 1 - self.turbineFile['SeaStFile'] = self.SSfilename - else: - # This might be a v.3.x file without the CompSeaSt entry - if 'CompSeaSt' in self.turbineFile.keys(): - self.turbineFile['CompSeaSt'] = 0 - self.turbineFile['SeaStFile'] = f'"unused"' + if self.hasSS: + self.turbineFile['CompSeaSt'] = 1 + self.turbineFile['SeaStFile'] = self.SSfilename + else: + # This might be a v.3.x file without the CompSeaSt entry + if 'CompSeaSt' in self.turbineFile.keys(): + self.turbineFile['CompSeaSt'] = 0 + self.turbineFile['SeaStFile'] = f'"unused"' + + if self.hasMD: + if self.multi_MD: + self.turbineFile['CompMooring'] = 3 # {0=None; 1=MAP++; 2=FEAMooring; 3=MoorDyn; 4=OrcaFlex} + self.turbineFile['MooringFile'] = f'"../{self.MDfilename}{t+1}_mod.dat' + else: + # Should be in .fstf and not in .fst (updated later when ff file is written). + pass + else: + self.turbineFile['CompMooring'] = 0 + self.turbineFile['MooringFile'] = f'"unused"' - if self.hasMD: - if self.multi_MD: - self.turbineFile['CompMooring'] = 3 # {0=None; 1=MAP++; 2=FEAMooring; 3=MoorDyn; 4=OrcaFlex} - self.turbineFile['MooringFile'] = f'"{self.MDfilename}{t+1}_mod.dat' + if self.hasSubD: + self.turbineFile['CompSub'] = 1 else: - # Should be in .fstf and not in .fst (updated later when ff file is written). - pass - else: - self.turbineFile['CompMooring'] = 0 - self.turbineFile['MooringFile'] = f'"unused"' + self.turbineFile['CompSub'] = 0 + self.turbineFile['SubFile'] = f'"../{self.SubDfilename}"' + + if EDmodel_ == 'FED': + self.turbineFile['CompElast'] = 1 # 1: full ElastoDyn; 2: full ElastoDyn + BeamDyn; 3: Simplified ElastoDyn + 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['SubFile'] = f'"unused"' + self.turbineFile['CompHydro'] = 0 # need to be disabled with SED + self.turbineFile['HydroFile'] = f'"unused"' + self.turbineFile['IntMethod'] = 3 + self.turbineFile['EDFile'] = f'"../{self.SEDfilename}{t+1}_mod.dat"' + + self.turbineFile['BDBldFile(1)'] = f'"../{self.BDbladefilename}"' + self.turbineFile['BDBldFile(2)'] = f'"../{self.BDbladefilename}"' + self.turbineFile['BDBldFile(3)'] = f'"../{self.BDbladefilename}"' + 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.ADfilename}"' + elif ADmodel_ == 'ADsk': + # 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.ADskfilename}"' + if writeFiles: + if t==0: shutilcopy2_untilSuccessful(self.coeffTablefilepath, os.path.join(currPath,self.coeffTablefilename)) - if self.hasSubD: - self.turbineFile['CompSub'] = 1 - else: - self.turbineFile['CompSub'] = 0 - self.turbineFile['SubFile'] = f'"{self.SubDfilename}"' + if self.hasSrvD: + self.turbineFile['ServoFile'] = f'"../{self.SrvDfilename}{t+1}_mod.dat"' + self.turbineFile['CompServo'] = 1 + else: + self.turbineFile['ServoFile'] = f'"unused"' + self.turbineFile['CompServo'] = 0 - if EDmodel_ == 'FED': - self.turbineFile['CompElast'] = 1 # 1: full ElastoDyn; 2: full ElastoDyn + BeamDyn; 3: Simplified ElastoDyn - 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['SubFile'] = f'"unused"' - self.turbineFile['CompHydro'] = 0 # need to be disabled with SED - self.turbineFile['HydroFile'] = f'"unused"' - self.turbineFile['IntMethod'] = 3 - self.turbineFile['EDFile'] = f'"./{self.SEDfilename}{t+1}_mod.dat"' - - self.turbineFile['BDBldFile(1)'] = f'"{self.BDbladefilename}"' - self.turbineFile['BDBldFile(2)'] = f'"{self.BDbladefilename}"' - self.turbineFile['BDBldFile(3)'] = f'"{self.BDbladefilename}"' - 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.ADfilename}"' - elif ADmodel_ == 'ADsk': - # 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.ADskfilename}"' + 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: - if t==0: shutilcopy2_untilSuccessful(self.coeffTablefilepath, os.path.join(currPath,self.coeffTablefilename)) - - if self.hasSrvD: - self.turbineFile['ServoFile'] = f'"{self.SrvDfilename}{t+1}_mod.dat"' - self.turbineFile['CompServo'] = 1 - else: - self.turbineFile['ServoFile'] = f'"unused"' - self.turbineFile['CompServo'] = 0 - - - 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')) + self.turbineFile.write( os.path.join(seedPath,f'{self.turbfilename}{t+1}.fst')) if self.verbose>0: print(f'Done processing condition {self.condDirList[cond]} ') @@ -958,8 +995,13 @@ def _were_all_turbine_files_copied(self): # Check SeaState if self.hasSS: - _ = checkIfExists(os.path.join(currPath,self.SSfilename)) - if not _: return False + if self.waveHs is not None and self.waveTp is not None: + for seed in range(self.nSeeds): + _ = checkIfExists(os.path.join(currPath,f'Seed_{seed}',self.SSfilename)) + if not _: return False + else: + _ = checkIfExists(os.path.join(currPath,self.SSfilename)) + if not _: return False # Check SubDyn if self.hasSubD: @@ -1013,8 +1055,10 @@ def _were_all_turbine_files_copied(self): _ = checkIfExists(os.path.join(currPath,self.ADbladefilename)) if not _: return False - _ = checkIfExists(os.path.join(currPath,f'{self.turbfilename}{t+1}.fst')) - if not _: return False + for seed in range(self.nSeeds): + seedPath = os.path.join(currPath, f'Seed_{seed}') + _ = checkIfExists(os.path.join(seedPath,f'{self.turbfilename}{t+1}.fst')) + if not _: return False # If we get to this point, all files exist return True @@ -1398,34 +1442,64 @@ def createAuxArrays(self): def _create_all_cond(self): + if self.waveHs is not None and self.waveTp is not None: + if len(self.vhub)==len(self.shear) and len(self.shear)==len(self.TIvalue) and len(self.TIvalue)==len(self.waveHs) and len(self.waveHs)==len(self.waveTp): + self.nConditions = len(self.vhub) + + if self.verbose>1: print(f'\nThe length of vhub, shear, TI, waveHs, and WaveTp 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 ), + 'TIvalue': (['cond'], self.TIvalue), + 'waveHs': (['cond'], self.waveHs), + 'waveTp': (['cond'], self.waveTp)}, + coords={'cond': np.arange(self.nConditions)} ) + + else: + import itertools + self.nConditions = len(self.vhub) * len(self.shear) * len(self.TIvalue) * len(self.waveHs) * len(self.waveTp) + + if self.verbose>1: print(f'The length of vhub, shear, TI, Hs, and Tp are different. Assuming sweep on each of them.') + if self.verbose>0: print(f'Creating {self.nConditions} conditions') + + # Repeat arrays as necessary to build xarray Dataset + combination = np.vstack(list(itertools.product(self.vhub,self.shear,self.TIvalue, + self.waveHs, self.waveTp))) + + self.allCond = xr.Dataset({'vhub': (['cond'], combination[:,0]), + 'shear': (['cond'], combination[:,1]), + 'TIvalue': (['cond'], combination[:,2]), + 'waveHs': (['cond'], combination[:,3]), + 'waveTp': (['cond'], combination[:,4])}, + coords={'cond': np.arange(self.nConditions)} ) + else: + if len(self.vhub)==len(self.shear) and len(self.shear)==len(self.TIvalue): + self.nConditions = len(self.vhub) - 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'\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') - 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 ), + 'TIvalue': (['cond'], self.TIvalue)}, + coords={'cond': np.arange(self.nConditions)} ) + + else: + import itertools + self.nConditions = len(self.vhub) * len(self.shear) * len(self.TIvalue) * len(self.waveHs) * len(self.waveTp) - self.allCond = xr.Dataset({'vhub': (['cond'], self.vhub ), - 'shear': (['cond'], self.shear ), - 'TIvalue': (['cond'], self.TIvalue)}, - coords={'cond': np.arange(self.nConditions)} ) + 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') - 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') - - # 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)} ) - + # Repeat arrays as necessary to build xarray Dataset + combination = np.vstack(list(itertools.product(self.vhub,self.shear,self.TIvalue, + self.waveHs, self.waveTp))) + self.allCond = xr.Dataset({'vhub': (['cond'], combination[:,0]), + 'shear': (['cond'], combination[:,1]), + 'TIvalue': (['cond'], combination[:,2])}, + coords={'cond': np.arange(self.nConditions)} ) diff --git a/openfast_toolbox/fastfarm/examples/Ex3_FFarmCompleteSetup.py b/openfast_toolbox/fastfarm/examples/Ex3_FFarmCompleteSetup.py index 61d0b7f..0f92774 100644 --- a/openfast_toolbox/fastfarm/examples/Ex3_FFarmCompleteSetup.py +++ b/openfast_toolbox/fastfarm/examples/Ex3_FFarmCompleteSetup.py @@ -152,7 +152,7 @@ def main(): if inflowType == 'TS': case.TS_low_setup() case.TS_low_slurm_prepare(slurm_TS_low) - #case.TS_low_slurm_submit() + #case.TS_low_slurm_submit()`` case.TS_high_setup() case.TS_high_slurm_prepare(slurm_TS_high) From 8d43254a6e5f726e62fa4862d030b8e32f54a93a Mon Sep 17 00:00:00 2001 From: Yuksel-Rudy Date: Mon, 8 Sep 2025 16:06:38 -0600 Subject: [PATCH 02/19] adding feature of copying airfoils into the casepath instead of pointing towards the template file --- .../fastfarm/FASTFarmCaseCreation.py | 27 ++++++++++++++++--- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/openfast_toolbox/fastfarm/FASTFarmCaseCreation.py b/openfast_toolbox/fastfarm/FASTFarmCaseCreation.py index d88c688..0abeb16 100644 --- a/openfast_toolbox/fastfarm/FASTFarmCaseCreation.py +++ b/openfast_toolbox/fastfarm/FASTFarmCaseCreation.py @@ -827,9 +827,20 @@ def copyTurbineFilesForEachCase(self, writeFiles=True): self.AeroDynFile['ADBlFile(1)'] = self.AeroDynFile['ADBlFile(2)'] = self.AeroDynFile['ADBlFile(3)'] = f'"{self.ADbladefilename}"' self.AeroDynFile['Wake_Mod'] = 1 # self.AeroDynFile['UA_Mod'] = 0 - # Adjust the Airfoil path to point to the templatePath (1:-1 to remove quotes) - self.AeroDynFile['AFNames'] = [f'"{os.path.join(self.templatePathabs, "Airfoils", i[1:-1].split("Airfoils/", 1)[-1])}"' - for i in self.AeroDynFile['AFNames'] ] + + # If the user provides airfoils path, we will copy it to the case folder + if self.airfoilspath is not None and writeFiles: + srcF = os.path.join(self.templatePath, 'Airfoils') + dstF = os.path.join(currPath, 'Airfoils') + os.makedirs(dstF, exist_ok=True) + for file in os.listdir(srcF): + src = os.path.join(srcF, file) + dst = os.path.join(dstF, file) + shutil.copy2(src, dst) + else: + # Adjust the Airfoil path to point to the templatePath (1:-1 to remove quotes) + self.AeroDynFile['AFNames'] = [f'"{os.path.join(self.templatePathabs, "Airfoils", i[1:-1].split("Airfoils/", 1)[-1])}"' + for i in self.AeroDynFile['AFNames'] ] if writeFiles: if t==0: shutilcopy2_untilSuccessful(self.ADbladefilepath, os.path.join(currPath,self.ADbladefilename)) if t==0: self.AeroDynFile.write(os.path.join(currPath,f'{self.ADfilename}')) @@ -1108,6 +1119,7 @@ def setTemplateFilename(self, templatePath=None, templateFiles=None): 'controllerInputfilename' : 'DISCON', 'coeffTablefilename' : None, 'hydroDatapath' : '/full/path/to/hydroData', + 'airfoilspath' : '/full/path/to/airfoils', 'turbsimLowfilepath' : './SampleFiles/template_Low_InflowXX_SeedY.inp', 'turbsimHighfilepath' : './SampleFiles/template_HighT1_InflowXX_SeedY.inp', 'FFfilename' : 'Model_FFarm.fstf' @@ -1138,6 +1150,7 @@ def setTemplateFilename(self, templatePath=None, templateFiles=None): self.libdisconfilepath = "unused" self.coeffTablefilename = "unused" self.hydroDatapath = "unused" + self.airfoilspath = "unused" self.turbsimLowfilepath = "unused" self.turbsimHighfilepath = "unused" self.FFfilename = "unused" @@ -1155,7 +1168,7 @@ def setTemplateFilename(self, templatePath=None, templateFiles=None): valid_keys = {'EDfilename', 'SEDfilename', 'HDfilename', 'MDfilename', 'bathfilename', 'SSfilename', 'SrvDfilename', 'ADfilename', 'ADskfilename', 'SubDfilename', 'IWfilename', 'BDfilename', 'BDbladefilename', 'EDbladefilename', 'EDtowerfilename', 'ADbladefilename', 'turbfilename', - 'libdisconfilepath', 'controllerInputfilename', 'coeffTablefilename', 'hydroDatapath', + 'libdisconfilepath', 'controllerInputfilename', 'coeffTablefilename', 'hydroDatapath', 'airfoilspath', 'FFfilename', 'turbsimLowfilepath', 'turbsimHighfilepath'} if not isinstance(templateFiles, dict): raise ValueError(f'templateFiles should be a dictionary with the following valid entries: {valid_keys}') @@ -1344,6 +1357,12 @@ def checkIfExists(f): raise ValueError(f'The hydroData directory hydroDatapath should be a directory. Received {value}.') self.hydroDatapath = value + elif key == 'airfoilspath': + self.airfoilsfilepath = os.path.join(self.templatePath, value) + if not os.path.isdir(self.airfoilsfilepath): + raise ValueError(f'The airfoils directory airfoilspath should be a directory. Received {value}.') + self.airfoilsfilepath = value + elif key == 'turbsimLowfilepath': if not value.endswith('.inp'): raise ValueError(f'TurbSim file input for low-res box should end in ".inp".') From b510ba55957a8cf5814acd6a5f9de7f036d985e8 Mon Sep 17 00:00:00 2001 From: Yuksel-Rudy Date: Tue, 9 Sep 2025 11:21:42 -0600 Subject: [PATCH 03/19] Let's not add turbsorigin2TSOrigin, it's causing the yz-windows to be out of the low-res boxes. --- openfast_toolbox/fastfarm/FASTFarmCaseCreation.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openfast_toolbox/fastfarm/FASTFarmCaseCreation.py b/openfast_toolbox/fastfarm/FASTFarmCaseCreation.py index 0abeb16..e3010b8 100644 --- a/openfast_toolbox/fastfarm/FASTFarmCaseCreation.py +++ b/openfast_toolbox/fastfarm/FASTFarmCaseCreation.py @@ -2233,8 +2233,8 @@ def FF_setup(self, outlistFF=None, **kwargs): 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 + xWT = alignedTurbs['Tx'].values # + self.xoffset_turbsOrigin2TSOrigin # when adding the turbsOrigin2TSOrigin, it's causing the windows to be out of the low-res box, causing error + yWT = alignedTurbs['Ty'].values # + self.yoffset_turbsOrigin2TSOrigin elif self.inflowStr == 'LES': # Turbine location in LES reference frame xWT = alignedTurbs['Tx'].values From 677c6e3c41631d23a29a65b8fa4547e172ab6906 Mon Sep 17 00:00:00 2001 From: Yuksel-Rudy Date: Tue, 9 Sep 2025 15:27:46 -0600 Subject: [PATCH 04/19] adding variation in hydroforce to simulate varying current conditions --- openfast_toolbox/fastfarm/FASTFarmCaseCreation.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/openfast_toolbox/fastfarm/FASTFarmCaseCreation.py b/openfast_toolbox/fastfarm/FASTFarmCaseCreation.py index e3010b8..fdab6c2 100644 --- a/openfast_toolbox/fastfarm/FASTFarmCaseCreation.py +++ b/openfast_toolbox/fastfarm/FASTFarmCaseCreation.py @@ -397,6 +397,7 @@ def _checkInputs(self): self.inflow_deg = [self.inflow_deg] if isinstance(self.inflow_deg,(float,int)) else self.inflow_deg self.waveHs = [self.waveHs] if isinstance(self.waveHs,(float,int)) else self.waveHs self.waveTp = [self.waveTp] if isinstance(self.waveTp,(float,int)) else self.waveTp + self.hydroForce = [self.hydroForce] if isinstance(self.hydroForce,(float,int)) else self.hydroForce # Fill turbine parameters arrays if not given if self.yaw_init is None: yaw = np.ones((1,self.nTurbines))*0 @@ -607,12 +608,13 @@ def _create_dir_structure(self): Vhub_ = self.allCond['vhub' ].isel(cond=cond).values shear_ = self.allCond['shear' ].isel(cond=cond).values tivalue_ = self.allCond['TIvalue' ].isel(cond=cond).values - if self.waveHs is not None and self.waveTp is not None: + if self.waveHs is not None and self.waveTp is not None and self.hydroForce is not None: waveHs_ = self.allCond['waveHs' ].isel(cond=cond).values waveTp_ = self.allCond['waveTp' ].isel(cond=cond).values + hydroF_ = self.allCond['hydroForce'].isel(cond=cond).values # Set current path name string - condStr = f'Cond{cond:02d}_v{Vhub_:04.1f}_PL{shear_}_TI{tivalue_}_Hs{waveHs_:04.1f}_Tp{waveTp_:04.1f}' + condStr = f'Cond{cond:02d}_v{Vhub_:04.1f}_PL{shear_}_TI{tivalue_}_Hs{waveHs_:04.1f}_Tp{waveTp_:04.1f}_HF{hydroF_:+05.1f}' else: condStr = f'Cond{cond:02d}_v{Vhub_:04.1f}_PL{shear_}_TI{tivalue_}' condDirList.append(condStr) @@ -861,7 +863,7 @@ def copyTurbineFilesForEachCase(self, writeFiles=True): if self.hasHD: self.HydroDynFile['PtfmRefY'] = self.allCases.sel(case=case, turbine=t)['phi'].values if self.hydroForce: - self.HydroDynFile['KDAdd'][0] = f' {self.hydroForce} 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]' + self.HydroDynFile['KDAdd'][0] = f' {self.hydroForce[cond]} 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]' if writeFiles: self.HydroDynFile.write(os.path.join(currPath,f'{self.HDfilename}{t+1}_mod.dat')) From 58e7945bbf5b3ef44fa0aca41544b0d2cd5f5eea Mon Sep 17 00:00:00 2001 From: Yuksel-Rudy Date: Tue, 9 Sep 2025 15:52:25 -0600 Subject: [PATCH 05/19] adding bug fixes --- .../fastfarm/FASTFarmCaseCreation.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/openfast_toolbox/fastfarm/FASTFarmCaseCreation.py b/openfast_toolbox/fastfarm/FASTFarmCaseCreation.py index fdab6c2..f06e831 100644 --- a/openfast_toolbox/fastfarm/FASTFarmCaseCreation.py +++ b/openfast_toolbox/fastfarm/FASTFarmCaseCreation.py @@ -274,6 +274,7 @@ def __repr__(self): s += f' - TI (%): {self.TIvalue}\n' s += f' - Significant wave height: {self.waveHs}\n' s += f' - Peak Wave period: {self.waveTp}\n' + s += f' - Additional Hydrodynamic forces +x : {self.hydroForce}\n' s += f'\nCase details:\n' s += f' - Number of conditions: {self.nConditions}\n' for c in self.condDirList: @@ -1463,18 +1464,19 @@ def createAuxArrays(self): def _create_all_cond(self): - if self.waveHs is not None and self.waveTp is not None: - if len(self.vhub)==len(self.shear) and len(self.shear)==len(self.TIvalue) and len(self.TIvalue)==len(self.waveHs) and len(self.waveHs)==len(self.waveTp): + if self.waveHs is not None and self.waveTp is not None and self.hydroForce: + if len(self.vhub)==len(self.shear) and len(self.shear)==len(self.TIvalue) and len(self.TIvalue)==len(self.waveHs) and len(self.waveHs)==len(self.waveTp) and len(self.waveTp)==len(self.hydroForce): self.nConditions = len(self.vhub) if self.verbose>1: print(f'\nThe length of vhub, shear, TI, waveHs, and WaveTp 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 ), - 'TIvalue': (['cond'], self.TIvalue), - 'waveHs': (['cond'], self.waveHs), - 'waveTp': (['cond'], self.waveTp)}, + self.allCond = xr.Dataset({'vhub': (['cond'], self.vhub ), + 'shear': (['cond'], self.shear ), + 'TIvalue': (['cond'], self.TIvalue), + 'waveHs': (['cond'], self.waveHs), + 'waveTp': (['cond'], self.waveTp), + 'hydroForce': (['cond'], self.hydroForce)}, coords={'cond': np.arange(self.nConditions)} ) else: From 4f88018c45628028f3cfed7460efb4fabd69001d Mon Sep 17 00:00:00 2001 From: Yuksel-Rudy Date: Tue, 9 Sep 2025 16:06:19 -0600 Subject: [PATCH 06/19] bug fix --- openfast_toolbox/fastfarm/FASTFarmCaseCreation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openfast_toolbox/fastfarm/FASTFarmCaseCreation.py b/openfast_toolbox/fastfarm/FASTFarmCaseCreation.py index f06e831..74c525c 100644 --- a/openfast_toolbox/fastfarm/FASTFarmCaseCreation.py +++ b/openfast_toolbox/fastfarm/FASTFarmCaseCreation.py @@ -615,7 +615,7 @@ def _create_dir_structure(self): hydroF_ = self.allCond['hydroForce'].isel(cond=cond).values # Set current path name string - condStr = f'Cond{cond:02d}_v{Vhub_:04.1f}_PL{shear_}_TI{tivalue_}_Hs{waveHs_:04.1f}_Tp{waveTp_:04.1f}_HF{hydroF_:+05.1f}' + condStr = f'Cond{cond:02d}_v{Vhub_:04.1f}_PL{shear_}_TI{tivalue_}_Hs{waveHs_:04.1f}_Tp{waveTp_:04.1f}_HF{hydroF_:06.1f}' else: condStr = f'Cond{cond:02d}_v{Vhub_:04.1f}_PL{shear_}_TI{tivalue_}' condDirList.append(condStr) From 7ddfafb497888ed9a81e1d3f20545735453dbbf5 Mon Sep 17 00:00:00 2001 From: Yuksel-Rudy Date: Tue, 9 Sep 2025 16:24:21 -0600 Subject: [PATCH 07/19] adding both Fx and Fy for currents --- .../fastfarm/FASTFarmCaseCreation.py | 34 ++++++++++++------- 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/openfast_toolbox/fastfarm/FASTFarmCaseCreation.py b/openfast_toolbox/fastfarm/FASTFarmCaseCreation.py index 74c525c..3fd7839 100644 --- a/openfast_toolbox/fastfarm/FASTFarmCaseCreation.py +++ b/openfast_toolbox/fastfarm/FASTFarmCaseCreation.py @@ -96,7 +96,8 @@ def __init__(self, inflow_deg, waveHs = None, waveTp = None, - hydroForce = None, + hydroFx = None, + hydroFy = None, dt_high_les = None, ds_high_les = None, extent_high = None, @@ -142,8 +143,10 @@ def __init__(self, waveTp: list of scalars or single scalar Wave periods to sweep on. Accepts a list or single value. If not given, seaState does not change - hydroForce: list of scalars or single scalar + hydroFx: list of scalars or single scalar Additional force in the x-axis to be added to hydrodyn files to simulate current. + hydroFy: list of scalars or single scalar + Additional force in the y-axis to be added to hydrodyn files to simulate current. 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 @@ -209,7 +212,8 @@ def __init__(self, self.inflow_deg = inflow_deg self.waveHs = waveHs self.waveTp = waveTp - self.hydroForce = hydroForce + self.hydroFx = hydroFx + self.hydroFy = hydroFy self.dt_high_les = dt_high_les self.ds_high_les = ds_high_les self.dt_low_les = dt_low_les @@ -274,7 +278,8 @@ def __repr__(self): s += f' - TI (%): {self.TIvalue}\n' s += f' - Significant wave height: {self.waveHs}\n' s += f' - Peak Wave period: {self.waveTp}\n' - s += f' - Additional Hydrodynamic forces +x : {self.hydroForce}\n' + s += f' - Additional Hydrodynamic forces +x : {self.hydroFx}\n' + s += f' - Additional Hydrodynamic forces +y : {self.hydroFy}\n' s += f'\nCase details:\n' s += f' - Number of conditions: {self.nConditions}\n' for c in self.condDirList: @@ -398,7 +403,8 @@ def _checkInputs(self): self.inflow_deg = [self.inflow_deg] if isinstance(self.inflow_deg,(float,int)) else self.inflow_deg self.waveHs = [self.waveHs] if isinstance(self.waveHs,(float,int)) else self.waveHs self.waveTp = [self.waveTp] if isinstance(self.waveTp,(float,int)) else self.waveTp - self.hydroForce = [self.hydroForce] if isinstance(self.hydroForce,(float,int)) else self.hydroForce + self.hydroFx = [self.hydroFx] if isinstance(self.hydroFx,(float,int)) else self.hydroFx + self.hydroFy = [self.hydroFy] if isinstance(self.hydroFy,(float,int)) else self.hydroFy # Fill turbine parameters arrays if not given if self.yaw_init is None: yaw = np.ones((1,self.nTurbines))*0 @@ -609,10 +615,11 @@ def _create_dir_structure(self): Vhub_ = self.allCond['vhub' ].isel(cond=cond).values shear_ = self.allCond['shear' ].isel(cond=cond).values tivalue_ = self.allCond['TIvalue' ].isel(cond=cond).values - if self.waveHs is not None and self.waveTp is not None and self.hydroForce is not None: + if self.waveHs is not None and self.waveTp is not None and self.hydroFx is not None and self.hydroFy is not None: waveHs_ = self.allCond['waveHs' ].isel(cond=cond).values waveTp_ = self.allCond['waveTp' ].isel(cond=cond).values - hydroF_ = self.allCond['hydroForce'].isel(cond=cond).values + hydroFx_ = self.allCond['hydroFx' ].isel(cond=cond).values + hydroFy_ = self.allCond['hydroFy' ].isel(cond=cond).values # Set current path name string condStr = f'Cond{cond:02d}_v{Vhub_:04.1f}_PL{shear_}_TI{tivalue_}_Hs{waveHs_:04.1f}_Tp{waveTp_:04.1f}_HF{hydroF_:06.1f}' @@ -863,8 +870,10 @@ def copyTurbineFilesForEachCase(self, writeFiles=True): # Update each turbine's HydroDyn if self.hasHD: self.HydroDynFile['PtfmRefY'] = self.allCases.sel(case=case, turbine=t)['phi'].values - if self.hydroForce: - self.HydroDynFile['KDAdd'][0] = f' {self.hydroForce[cond]} 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]' + if self.hydroFx: + self.HydroDynFile['KDAdd'][0] = f' {self.hydroFx[cond]} 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]' + if self.hydroFy: + self.HydroDynFile['KDAdd'][1] = f' {self.hydroFxy[cond]}' if writeFiles: self.HydroDynFile.write(os.path.join(currPath,f'{self.HDfilename}{t+1}_mod.dat')) @@ -1464,8 +1473,8 @@ def createAuxArrays(self): def _create_all_cond(self): - if self.waveHs is not None and self.waveTp is not None and self.hydroForce: - if len(self.vhub)==len(self.shear) and len(self.shear)==len(self.TIvalue) and len(self.TIvalue)==len(self.waveHs) and len(self.waveHs)==len(self.waveTp) and len(self.waveTp)==len(self.hydroForce): + if self.waveHs is not None and self.waveTp is not None and self.hydroFx is not None and self.hydroFy is not None: + if len(self.vhub)==len(self.shear) and len(self.shear)==len(self.TIvalue) and len(self.TIvalue)==len(self.waveHs) and len(self.waveHs)==len(self.waveTp) and len(self.waveTp)==len(self.hydroFx) and len(self.hydroFx)==len(self.hydroFy): self.nConditions = len(self.vhub) if self.verbose>1: print(f'\nThe length of vhub, shear, TI, waveHs, and WaveTp are the same. Assuming each position is a condition.', end='\r') @@ -1476,7 +1485,8 @@ def _create_all_cond(self): 'TIvalue': (['cond'], self.TIvalue), 'waveHs': (['cond'], self.waveHs), 'waveTp': (['cond'], self.waveTp), - 'hydroForce': (['cond'], self.hydroForce)}, + 'hydroFx': (['cond'], self.hydroFx), + 'hydroFy': (['cond'], self.hydroFy)}, coords={'cond': np.arange(self.nConditions)} ) else: From ac837797131b3064433afd3fb3a662f038a18b76 Mon Sep 17 00:00:00 2001 From: Yuksel-Rudy Date: Tue, 9 Sep 2025 16:30:45 -0600 Subject: [PATCH 08/19] fixing bug about name with HFx and HFy --- openfast_toolbox/fastfarm/FASTFarmCaseCreation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openfast_toolbox/fastfarm/FASTFarmCaseCreation.py b/openfast_toolbox/fastfarm/FASTFarmCaseCreation.py index 3fd7839..0df7082 100644 --- a/openfast_toolbox/fastfarm/FASTFarmCaseCreation.py +++ b/openfast_toolbox/fastfarm/FASTFarmCaseCreation.py @@ -622,7 +622,7 @@ def _create_dir_structure(self): hydroFy_ = self.allCond['hydroFy' ].isel(cond=cond).values # Set current path name string - condStr = f'Cond{cond:02d}_v{Vhub_:04.1f}_PL{shear_}_TI{tivalue_}_Hs{waveHs_:04.1f}_Tp{waveTp_:04.1f}_HF{hydroF_:06.1f}' + condStr = f'Cond{cond:02d}_v{Vhub_:04.1f}_PL{shear_}_TI{tivalue_}_Hs{waveHs_:04.1f}_Tp{waveTp_:04.1f}_HFx{hydroFx_:06.1f}_HFy{hydroFy_:06.1f}' else: condStr = f'Cond{cond:02d}_v{Vhub_:04.1f}_PL{shear_}_TI{tivalue_}' condDirList.append(condStr) From 212c12ecef5129e35fd9be58d340d5d44964d74e Mon Sep 17 00:00:00 2001 From: Yuksel-Rudy Date: Tue, 9 Sep 2025 16:32:04 -0600 Subject: [PATCH 09/19] fixing bug about adding hydroFy into hydrodyn input files --- openfast_toolbox/fastfarm/FASTFarmCaseCreation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openfast_toolbox/fastfarm/FASTFarmCaseCreation.py b/openfast_toolbox/fastfarm/FASTFarmCaseCreation.py index 0df7082..dbaa3c3 100644 --- a/openfast_toolbox/fastfarm/FASTFarmCaseCreation.py +++ b/openfast_toolbox/fastfarm/FASTFarmCaseCreation.py @@ -873,7 +873,7 @@ def copyTurbineFilesForEachCase(self, writeFiles=True): if self.hydroFx: self.HydroDynFile['KDAdd'][0] = f' {self.hydroFx[cond]} 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]' if self.hydroFy: - self.HydroDynFile['KDAdd'][1] = f' {self.hydroFxy[cond]}' + self.HydroDynFile['KDAdd'][1] = f' {self.hydroFy[cond]}' if writeFiles: self.HydroDynFile.write(os.path.join(currPath,f'{self.HDfilename}{t+1}_mod.dat')) From 49eeadb29f83052a241a5a8c6ef90849c01f396a Mon Sep 17 00:00:00 2001 From: Rudy Alkarem Date: Sun, 14 Sep 2025 19:30:24 -0600 Subject: [PATCH 10/19] fixing Ind_BldPitch. It should be an int. --- openfast_toolbox/fastfarm/FASTFarmCaseCreation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openfast_toolbox/fastfarm/FASTFarmCaseCreation.py b/openfast_toolbox/fastfarm/FASTFarmCaseCreation.py index dbaa3c3..899fd56 100644 --- a/openfast_toolbox/fastfarm/FASTFarmCaseCreation.py +++ b/openfast_toolbox/fastfarm/FASTFarmCaseCreation.py @@ -745,8 +745,8 @@ def copyTurbineFilesForEachCase(self, writeFiles=True): shutilcopy2_untilSuccessful(self.cpctcqfilepath, os.path.join(currPath,self.cpctcqfilename)) self.DISCONFile['PerfFileName'] = f'{self.cpctcqfilename}' + self.DISCONFile['Ind_BldPitch'] = self.DISCONFile['Ind_BldPitch'].astype(int) self.DISCONFile.write(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 # the compilation process and it is likely that a very long string with the full path will get cut. So we need to From afefd06db1f788525327473fda296c337385cdb6 Mon Sep 17 00:00:00 2001 From: Rudy Alkarem Date: Mon, 15 Sep 2025 09:09:34 -0600 Subject: [PATCH 11/19] Correcting ds high and low box computations from amrWindSimulations --- openfast_toolbox/fastfarm/FASTFarmCaseCreation.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openfast_toolbox/fastfarm/FASTFarmCaseCreation.py b/openfast_toolbox/fastfarm/FASTFarmCaseCreation.py index 899fd56..b9dc7d0 100644 --- a/openfast_toolbox/fastfarm/FASTFarmCaseCreation.py +++ b/openfast_toolbox/fastfarm/FASTFarmCaseCreation.py @@ -598,9 +598,9 @@ def _determine_resolutions_from_dummy_amrwind_grid(self): 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.ds_high_les = amr.ds_high_les self.dt_low_les = amr.dt_low_les - self.ds_low_les = amr.dt_low_les + self.ds_low_les = amr.ds_low_les From cf174e7622432f0e011308076b0914dd48ff6586 Mon Sep 17 00:00:00 2001 From: Rudy Alkarem Date: Mon, 15 Sep 2025 15:54:50 -0600 Subject: [PATCH 12/19] Adding waveDir as an option to the tool: 1) waveDir can be passed down to FFarm-toolbox following the same logic as the other variables. 2) I turned off manual mode when creating TS files to get varying low resolution boxes given varying hub-height wind speeds. --- .../fastfarm/FASTFarmCaseCreation.py | 52 ++++++++++++------- .../fastfarm/TurbSimCaseCreation.py | 4 ++ 2 files changed, 36 insertions(+), 20 deletions(-) diff --git a/openfast_toolbox/fastfarm/FASTFarmCaseCreation.py b/openfast_toolbox/fastfarm/FASTFarmCaseCreation.py index b9dc7d0..47298f1 100644 --- a/openfast_toolbox/fastfarm/FASTFarmCaseCreation.py +++ b/openfast_toolbox/fastfarm/FASTFarmCaseCreation.py @@ -96,6 +96,7 @@ def __init__(self, inflow_deg, waveHs = None, waveTp = None, + waveDir = None, hydroFx = None, hydroFy = None, dt_high_les = None, @@ -143,6 +144,9 @@ def __init__(self, waveTp: list of scalars or single scalar Wave periods to sweep on. Accepts a list or single value. If not given, seaState does not change + waveDir: list of scalars or single scalar + Wave directions to sweep on. Accepts a list or single value. + If not given, wave direction is whatever given in the template SeaState file hydroFx: list of scalars or single scalar Additional force in the x-axis to be added to hydrodyn files to simulate current. hydroFy: list of scalars or single scalar @@ -212,6 +216,7 @@ def __init__(self, self.inflow_deg = inflow_deg self.waveHs = waveHs self.waveTp = waveTp + self.waveDir = waveDir self.hydroFx = hydroFx self.hydroFy = hydroFy self.dt_high_les = dt_high_les @@ -278,6 +283,7 @@ def __repr__(self): s += f' - TI (%): {self.TIvalue}\n' s += f' - Significant wave height: {self.waveHs}\n' s += f' - Peak Wave period: {self.waveTp}\n' + s += f' - Wave direction: {self.waveDir}\n' s += f' - Additional Hydrodynamic forces +x : {self.hydroFx}\n' s += f' - Additional Hydrodynamic forces +y : {self.hydroFy}\n' s += f'\nCase details:\n' @@ -403,8 +409,9 @@ def _checkInputs(self): self.inflow_deg = [self.inflow_deg] if isinstance(self.inflow_deg,(float,int)) else self.inflow_deg self.waveHs = [self.waveHs] if isinstance(self.waveHs,(float,int)) else self.waveHs self.waveTp = [self.waveTp] if isinstance(self.waveTp,(float,int)) else self.waveTp - self.hydroFx = [self.hydroFx] if isinstance(self.hydroFx,(float,int)) else self.hydroFx - self.hydroFy = [self.hydroFy] if isinstance(self.hydroFy,(float,int)) else self.hydroFy + self.waveDir = [self.waveDir] if isinstance(self.waveDir,(float,int)) else self.waveDir + self.hydroFx = [self.hydroFx] if isinstance(self.hydroFx,(float,int)) else self.hydroFx + self.hydroFy = [self.hydroFy] if isinstance(self.hydroFy,(float,int)) else self.hydroFy # Fill turbine parameters arrays if not given if self.yaw_init is None: yaw = np.ones((1,self.nTurbines))*0 @@ -615,14 +622,15 @@ def _create_dir_structure(self): Vhub_ = self.allCond['vhub' ].isel(cond=cond).values shear_ = self.allCond['shear' ].isel(cond=cond).values tivalue_ = self.allCond['TIvalue' ].isel(cond=cond).values - if self.waveHs is not None and self.waveTp is not None and self.hydroFx is not None and self.hydroFy is not None: + if self.waveHs is not None and self.waveTp is not None and self.waveDir is not None and self.hydroFx is not None and self.hydroFy is not None: waveHs_ = self.allCond['waveHs' ].isel(cond=cond).values waveTp_ = self.allCond['waveTp' ].isel(cond=cond).values + waveDir_ = self.allCond['waveDir' ].isel(cond=cond).values hydroFx_ = self.allCond['hydroFx' ].isel(cond=cond).values hydroFy_ = self.allCond['hydroFy' ].isel(cond=cond).values # Set current path name string - condStr = f'Cond{cond:02d}_v{Vhub_:04.1f}_PL{shear_}_TI{tivalue_}_Hs{waveHs_:04.1f}_Tp{waveTp_:04.1f}_HFx{hydroFx_:06.1f}_HFy{hydroFy_:06.1f}' + condStr = f'Cond{cond:02d}_v{Vhub_:04.1f}_PL{shear_}_TI{tivalue_}_Hs{waveHs_:04.1f}_Tp{waveTp_:04.1f}_Wdir{waveDir:02d}_HFx{hydroFx_:06.1f}_HFy{hydroFy_:06.1f}' else: condStr = f'Cond{cond:02d}_v{Vhub_:04.1f}_PL{shear_}_TI{tivalue_}' condDirList.append(condStr) @@ -694,9 +702,10 @@ def copyTurbineFilesForEachCase(self, writeFiles=True): # Update parameters to be changed in the SeaState files if self.hasSS: - if self.waveHs is not None and self.waveTp is not None: - self.SeaStateFile['WaveHs'] = self.waveHs[cond] - self.SeaStateFile['WaveTp'] = self.waveTp[cond] + if self.waveHs is not None and self.waveTp is not None and self.waveDir is not None: + self.SeaStateFile['WaveHs'] = self.waveHs[cond] + self.SeaStateFile['WaveTp'] = self.waveTp[cond] + self.SeaStateFile['WaveDir'] = self.waveDir[cond] self.SeaStateFile['WvHiCOffD'] = 2.0*np.pi/self.SeaStateFile['WaveTp'] self.SeaStateFile['WvLowCOffS'] = 2.0*np.pi/self.SeaStateFile['WaveTp'] for seed in range(self.nSeeds): @@ -1018,7 +1027,7 @@ def _were_all_turbine_files_copied(self): # Check SeaState if self.hasSS: - if self.waveHs is not None and self.waveTp is not None: + if self.waveHs is not None and self.waveTp is not None and self.waveDir is not None: for seed in range(self.nSeeds): _ = checkIfExists(os.path.join(currPath,f'Seed_{seed}',self.SSfilename)) if not _: return False @@ -1473,11 +1482,11 @@ def createAuxArrays(self): def _create_all_cond(self): - if self.waveHs is not None and self.waveTp is not None and self.hydroFx is not None and self.hydroFy is not None: - if len(self.vhub)==len(self.shear) and len(self.shear)==len(self.TIvalue) and len(self.TIvalue)==len(self.waveHs) and len(self.waveHs)==len(self.waveTp) and len(self.waveTp)==len(self.hydroFx) and len(self.hydroFx)==len(self.hydroFy): + if self.waveHs is not None and self.waveTp is not None and self.waveDir is not None and self.hydroFx is not None and self.hydroFy is not None: + if len(self.vhub)==len(self.shear) and len(self.shear)==len(self.TIvalue) and len(self.TIvalue)==len(self.waveHs) and len(self.waveHs)==len(self.waveTp) and len(self.waveTp)==len(self.waveDir) and len(self.waveDir)==len(self.hydroFx) and len(self.hydroFx)==len(self.hydroFy): self.nConditions = len(self.vhub) - if self.verbose>1: print(f'\nThe length of vhub, shear, TI, waveHs, and WaveTp are the same. Assuming each position is a condition.', end='\r') + if self.verbose>1: print(f'\nThe length of vhub, shear, TI, waveHs, waveTp, waveDir, hydroFx, and hydroFy 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 ), @@ -1485,26 +1494,30 @@ def _create_all_cond(self): 'TIvalue': (['cond'], self.TIvalue), 'waveHs': (['cond'], self.waveHs), 'waveTp': (['cond'], self.waveTp), + 'waveDir': (['cond'], self.waveDir), 'hydroFx': (['cond'], self.hydroFx), 'hydroFy': (['cond'], self.hydroFy)}, coords={'cond': np.arange(self.nConditions)} ) else: import itertools - self.nConditions = len(self.vhub) * len(self.shear) * len(self.TIvalue) * len(self.waveHs) * len(self.waveTp) + self.nConditions = len(self.vhub) * len(self.shear) * len(self.TIvalue) * len(self.waveHs) * len(self.waveTp) * len(self.waveDir) * len(self.hydroFx) * len(self.hydroFy) if self.verbose>1: print(f'The length of vhub, shear, TI, Hs, and Tp are different. Assuming sweep on each of them.') if self.verbose>0: print(f'Creating {self.nConditions} conditions') # Repeat arrays as necessary to build xarray Dataset combination = np.vstack(list(itertools.product(self.vhub,self.shear,self.TIvalue, - self.waveHs, self.waveTp))) + self.waveHs, self.waveTp, self.waveDir, self.hydroFx, self.hydroFy))) - self.allCond = xr.Dataset({'vhub': (['cond'], combination[:,0]), - 'shear': (['cond'], combination[:,1]), + self.allCond = xr.Dataset({'vhub': (['cond'], combination[:,0]), + 'shear' : (['cond'], combination[:,1]), 'TIvalue': (['cond'], combination[:,2]), - 'waveHs': (['cond'], combination[:,3]), - 'waveTp': (['cond'], combination[:,4])}, + 'waveHs' : (['cond'], combination[:,3]), + 'waveTp' : (['cond'], combination[:,4]), + 'waveDir': (['cond'], combination[:,4]), + 'hydroFx': (['cond'], combination[:,4]), + 'hydroFy': (['cond'], combination[:,4])}, coords={'cond': np.arange(self.nConditions)} ) else: if len(self.vhub)==len(self.shear) and len(self.shear)==len(self.TIvalue): @@ -1520,14 +1533,13 @@ def _create_all_cond(self): else: import itertools - self.nConditions = len(self.vhub) * len(self.shear) * len(self.TIvalue) * len(self.waveHs) * len(self.waveTp) + 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') # Repeat arrays as necessary to build xarray Dataset - combination = np.vstack(list(itertools.product(self.vhub,self.shear,self.TIvalue, - self.waveHs, self.waveTp))) + combination = np.vstack(list(itertools.product(self.vhub,self.shear,self.TIvalue))) self.allCond = xr.Dataset({'vhub': (['cond'], combination[:,0]), 'shear': (['cond'], combination[:,1]), diff --git a/openfast_toolbox/fastfarm/TurbSimCaseCreation.py b/openfast_toolbox/fastfarm/TurbSimCaseCreation.py index 99644cf..af93ac8 100644 --- a/openfast_toolbox/fastfarm/TurbSimCaseCreation.py +++ b/openfast_toolbox/fastfarm/TurbSimCaseCreation.py @@ -61,6 +61,10 @@ def __init__(self, D, HubHt, Vhub, TI, PLexp, x, y, z=None, zbot=1.0, cmax=5.0, else: manual_ds_low = True + # Force manual modes off: + manual_mode = False + manual_ds_low = False + # Set parameters for convenience self.Cmeander = Cmeander self.boxType = boxType From 6e21fa701321d9e425dadfb10e01a5ca1b7b98e5 Mon Sep 17 00:00:00 2001 From: Rudy Alkarem Date: Mon, 15 Sep 2025 16:09:32 -0600 Subject: [PATCH 13/19] fixing condStr name --- openfast_toolbox/fastfarm/FASTFarmCaseCreation.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openfast_toolbox/fastfarm/FASTFarmCaseCreation.py b/openfast_toolbox/fastfarm/FASTFarmCaseCreation.py index 47298f1..af0b72b 100644 --- a/openfast_toolbox/fastfarm/FASTFarmCaseCreation.py +++ b/openfast_toolbox/fastfarm/FASTFarmCaseCreation.py @@ -630,7 +630,8 @@ def _create_dir_structure(self): hydroFy_ = self.allCond['hydroFy' ].isel(cond=cond).values # Set current path name string - condStr = f'Cond{cond:02d}_v{Vhub_:04.1f}_PL{shear_}_TI{tivalue_}_Hs{waveHs_:04.1f}_Tp{waveTp_:04.1f}_Wdir{waveDir:02d}_HFx{hydroFx_:06.1f}_HFy{hydroFy_:06.1f}' + condStr = f'Cond{cond:02d}_v{Vhub_:04.1f}_PL{shear_}_TI{tivalue_}_Hs{waveHs_:04.1f}_Tp{waveTp_:04.1f}_Wdir{waveDir_:02d}_HFx{hydroFx_:06.1f}_HFy{hydroFy_:06.1f}' + # condStr = f'Cond{cond:02d}_v{Vhub_:04.1f}_PL{shear_}_TI{tivalue_}_Hs{waveHs_:04.1f}_Tp{waveTp_:04.1f}_HFx{hydroFx_:06.1f}_HFy{hydroFy_:06.1f}' else: condStr = f'Cond{cond:02d}_v{Vhub_:04.1f}_PL{shear_}_TI{tivalue_}' condDirList.append(condStr) From 8afdd7b777156c1583e5d3ba969f39426aecb26a Mon Sep 17 00:00:00 2001 From: Rudy Alkarem Date: Tue, 16 Sep 2025 11:40:52 -0600 Subject: [PATCH 14/19] libdiscon.so for each turbine now is copied inside the case folder and not in the template. --- openfast_toolbox/fastfarm/FASTFarmCaseCreation.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openfast_toolbox/fastfarm/FASTFarmCaseCreation.py b/openfast_toolbox/fastfarm/FASTFarmCaseCreation.py index af0b72b..a660893 100644 --- a/openfast_toolbox/fastfarm/FASTFarmCaseCreation.py +++ b/openfast_toolbox/fastfarm/FASTFarmCaseCreation.py @@ -1438,8 +1438,8 @@ def _create_copy_libdiscon(self): 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') + currLibdiscon = os.path.join(self.path, f'{libdisconfilename}.T{t+1}.so') + self.DLLfilepath = os.path.join(self.path, 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}') shutil.copy2(self.libdisconfilepath, currLibdiscon) From 70403d1e85dd7ca9d48b33771f70d2e724fcbed2 Mon Sep 17 00:00:00 2001 From: Rudy Alkarem Date: Tue, 16 Sep 2025 12:49:21 -0600 Subject: [PATCH 15/19] updating the name of the condition folder so it better fits the parameters --- openfast_toolbox/fastfarm/FASTFarmCaseCreation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openfast_toolbox/fastfarm/FASTFarmCaseCreation.py b/openfast_toolbox/fastfarm/FASTFarmCaseCreation.py index a660893..9b82ffe 100644 --- a/openfast_toolbox/fastfarm/FASTFarmCaseCreation.py +++ b/openfast_toolbox/fastfarm/FASTFarmCaseCreation.py @@ -630,7 +630,7 @@ def _create_dir_structure(self): hydroFy_ = self.allCond['hydroFy' ].isel(cond=cond).values # Set current path name string - condStr = f'Cond{cond:02d}_v{Vhub_:04.1f}_PL{shear_}_TI{tivalue_}_Hs{waveHs_:04.1f}_Tp{waveTp_:04.1f}_Wdir{waveDir_:02d}_HFx{hydroFx_:06.1f}_HFy{hydroFy_:06.1f}' + condStr = f'Cond{cond:02d}_v{Vhub_:04.1f}_PL{shear_}_TI{tivalue_:02.1f}_Hs{waveHs_:02.1f}_Tp{waveTp_:02.1f}_Wdir{waveDir_:02.1f}_HFx{hydroFx_:06.1f}_HFy{hydroFy_:06.1f}' # condStr = f'Cond{cond:02d}_v{Vhub_:04.1f}_PL{shear_}_TI{tivalue_}_Hs{waveHs_:04.1f}_Tp{waveTp_:04.1f}_HFx{hydroFx_:06.1f}_HFy{hydroFy_:06.1f}' else: condStr = f'Cond{cond:02d}_v{Vhub_:04.1f}_PL{shear_}_TI{tivalue_}' From 284b71cbed2163ce8a7f29cfe396e2a5bf2183fe Mon Sep 17 00:00:00 2001 From: Rudy Alkarem Date: Fri, 19 Sep 2025 15:38:39 -0600 Subject: [PATCH 16/19] Turbine not positioned in the center of high-res boxes fixes --- .../fastfarm/FASTFarmCaseCreation.py | 27 ++++++++++--------- .../fastfarm/TurbSimCaseCreation.py | 6 ++--- 2 files changed, 17 insertions(+), 16 deletions(-) diff --git a/openfast_toolbox/fastfarm/FASTFarmCaseCreation.py b/openfast_toolbox/fastfarm/FASTFarmCaseCreation.py index 9b82ffe..67d8fed 100644 --- a/openfast_toolbox/fastfarm/FASTFarmCaseCreation.py +++ b/openfast_toolbox/fastfarm/FASTFarmCaseCreation.py @@ -2600,8 +2600,8 @@ def _getBoxesParamsForFF(self, lowbts, highbts, dt_low_desired, D, HubHt, xWT, y # --- 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 + X0_High = X0_Low + (X0_desired-X0_Low)/dX_High*dX_High # removed np.floor because it would prevent turbines being centered + Y0_High = Y0_Low + (Y0_desired-Y0_Low)/dY_High*dY_High # removed np.floor because it would prevent turbines being centered if self.verbose>2: print(f' Low Box \t\t High box ') @@ -2648,19 +2648,20 @@ def _getBoxesParamsForFF(self, lowbts, highbts, dt_low_desired, D, HubHt, xWT, y d['U_mean_Low'] = meanU_Low d['U_mean_High'] = meanU_High - + + # Commenting this for now: it was creating problems when the turbine is not exactly centered in the high-res box so I removed the np.floor # --- 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 + # 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') + # 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 diff --git a/openfast_toolbox/fastfarm/TurbSimCaseCreation.py b/openfast_toolbox/fastfarm/TurbSimCaseCreation.py index af93ac8..add6622 100644 --- a/openfast_toolbox/fastfarm/TurbSimCaseCreation.py +++ b/openfast_toolbox/fastfarm/TurbSimCaseCreation.py @@ -61,9 +61,9 @@ def __init__(self, D, HubHt, Vhub, TI, PLexp, x, y, z=None, zbot=1.0, cmax=5.0, else: manual_ds_low = True - # Force manual modes off: - manual_mode = False - manual_ds_low = False + # # Force manual modes off: + # manual_mode = False + # manual_ds_low = False # Set parameters for convenience self.Cmeander = Cmeander From 3bef0181e82de3c8527c94002543657608ca4284 Mon Sep 17 00:00:00 2001 From: Rudy Alkarem Date: Mon, 6 Oct 2025 10:25:00 -0600 Subject: [PATCH 17/19] updating the TS interpretation from low-box to high-box for different dt. --- .../fastfarm/FASTFarmCaseCreation.py | 24 +++++++++++++------ 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/openfast_toolbox/fastfarm/FASTFarmCaseCreation.py b/openfast_toolbox/fastfarm/FASTFarmCaseCreation.py index 67d8fed..9c41ef2 100644 --- a/openfast_toolbox/fastfarm/FASTFarmCaseCreation.py +++ b/openfast_toolbox/fastfarm/FASTFarmCaseCreation.py @@ -1784,7 +1784,8 @@ def TS_low_setup(self, writeFiles=True, runOnce=False): 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) + # Lowinp['TimeStep'] = 1/(2*self.fmax) + Lowinp['TimeStep'] = np.round(Lowinp['TimeStep'], 2) if writeFiles: lowFileName = os.path.join(seedPath, 'Low.inp') Lowinp.write(lowFileName) @@ -1994,6 +1995,9 @@ def TS_high_get_time_series(self): Vmid = bts['u'][0,:,jMid,kMid] time = bts.t + # Given the nature of how TS decides on the total time, let's get the actual tmax from the low-res + self.tmax_low = time[-1] + # 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 ) @@ -2002,11 +2006,17 @@ def TS_high_get_time_series(self): 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) - + + # Map it to high-res time and dt (both) + time_hr = np.arange(time[0], self.tmax_low+self.dt_high_les, self.dt_high_les) + uvel_hr = np.interp(time_hr, time, uvel) + vvel_hr = np.interp(time_hr, time, vvel) + wvel_hr = np.interp(time_hr, time, wvel) + # Checks - assert len(time)==len(uvel) - assert len(uvel)==len(vvel) - assert len(vvel)==len(wvel) + assert len(time_hr)==len(uvel_hr) + assert len(uvel_hr)==len(vvel_hr) + assert len(vvel_hr)==len(wvel_hr) # 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') @@ -2024,7 +2034,7 @@ def TS_high_get_time_series(self): 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) + writeTimeSeriesFile(timeSeriesOutputFile, yoffset, Hub_series, uvel_hr, vvel_hr, wvel_hr, time_hr) @@ -2076,7 +2086,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) - currentTS.writeTSFile(self.turbsimHighfilepath, currentTSHighFile, tmax=self.tmax, turb=t, verbose=self.verbose) + currentTS.writeTSFile(self.turbsimHighfilepath, currentTSHighFile, tmax=self.tmax_low, turb=t, verbose=self.verbose) # Modify some values and save file (some have already been set in the call above) Highinp = FASTInputFile(currentTSHighFile) From 4a76fa3b8e76790478fa25db00fd8fdfa842f387 Mon Sep 17 00:00:00 2001 From: Rudy Alkarem Date: Thu, 13 Nov 2025 11:01:07 -0700 Subject: [PATCH 18/19] libdiscon.so overwrite. --- openfast_toolbox/fastfarm/FASTFarmCaseCreation.py | 2 +- openfast_toolbox/fastfarm/examples/Ex3_FFarmCompleteSetup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/openfast_toolbox/fastfarm/FASTFarmCaseCreation.py b/openfast_toolbox/fastfarm/FASTFarmCaseCreation.py index 9c41ef2..84dc35b 100644 --- a/openfast_toolbox/fastfarm/FASTFarmCaseCreation.py +++ b/openfast_toolbox/fastfarm/FASTFarmCaseCreation.py @@ -1440,7 +1440,7 @@ def _create_copy_libdiscon(self): libdisconfilename = os.path.splitext(os.path.basename(self.libdisconfilepath))[0] currLibdiscon = os.path.join(self.path, f'{libdisconfilename}.T{t+1}.so') self.DLLfilepath = os.path.join(self.path, f'{libdisconfilename}.T') - if not os.path.isfile(currLibdiscon): + if True: # I always want to overwrite libdiscon.so to avoid deleting old versions but here's the condition if you wanna check: not os.path.isfile(currLibdiscon) if self.verbose>0: print(f' Creating a copy of the controller {libdisconfilename}.so in {currLibdiscon}') shutil.copy2(self.libdisconfilepath, currLibdiscon) copied=True diff --git a/openfast_toolbox/fastfarm/examples/Ex3_FFarmCompleteSetup.py b/openfast_toolbox/fastfarm/examples/Ex3_FFarmCompleteSetup.py index 0f92774..61d0b7f 100644 --- a/openfast_toolbox/fastfarm/examples/Ex3_FFarmCompleteSetup.py +++ b/openfast_toolbox/fastfarm/examples/Ex3_FFarmCompleteSetup.py @@ -152,7 +152,7 @@ def main(): if inflowType == 'TS': case.TS_low_setup() case.TS_low_slurm_prepare(slurm_TS_low) - #case.TS_low_slurm_submit()`` + #case.TS_low_slurm_submit() case.TS_high_setup() case.TS_high_slurm_prepare(slurm_TS_high) From 9ee3c58118c27bc161262a2cb4786cc14ec6827d Mon Sep 17 00:00:00 2001 From: Rudy Alkarem Date: Wed, 26 Nov 2025 09:35:59 -0700 Subject: [PATCH 19/19] including initial conditions such as surge and sway to be fed to ElastoDyn --- .../fastfarm/FASTFarmCaseCreation.py | 31 ++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/openfast_toolbox/fastfarm/FASTFarmCaseCreation.py b/openfast_toolbox/fastfarm/FASTFarmCaseCreation.py index 84dc35b..f9ee174 100644 --- a/openfast_toolbox/fastfarm/FASTFarmCaseCreation.py +++ b/openfast_toolbox/fastfarm/FASTFarmCaseCreation.py @@ -363,6 +363,9 @@ def _checkInputs(self): t_x = self.wts[t]['x'] t_y = self.wts[t]['y'] t_z = self.wts[t]['z'] + x_i = self.wts[t].get('xi', 0) + y_i = self.wts[t].get('yi', 0) + z_i = self.wts[t].get('zi', 0) t_D = self.wts[t]['D'] t_zhub = self.wts[t]['zhub'] t_cmax = self.wts[t]['cmax'] @@ -386,6 +389,12 @@ def _checkInputs(self): 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(x_i,(float,int)): + raise ValueError (f'The `xi` value for the turbine {t+1} should be an integer or float. Received {x_i}.') + if not isinstance(y_i,(float,int)): + raise ValueError (f'The `yi` value for the turbine {t+1} should be an integer or float. Received {y_i}.') + if not isinstance(z_i,(float,int)): + raise ValueError (f'The `zi` value for the turbine {t+1} should be an integer or float. Received {z_i}.') 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)): @@ -807,6 +816,9 @@ def copyTurbineFilesForEachCase(self, writeFiles=True): yaw_deg_ = self.allCases.sel(case=case, turbine=t)['yaw'].values yaw_mis_deg_ = self.allCases.sel(case=case, turbine=t)['yawmis'].values phi_deg_ = self.allCases.sel(case=case, turbine=t)['phi'].values + xi_ = self.allCases.sel(case=case, turbine=t)['xi'].values + yi_ = self.allCases.sel(case=case, turbine=t)['yi'].values + zi_ = self.allCases.sel(case=case, turbine=t)['zi'].values ADmodel_ = self.allCases.sel(case=case, turbine=t)['ADmodel'].values EDmodel_ = self.allCases.sel(case=case, turbine=t)['EDmodel'].values @@ -824,10 +836,16 @@ def copyTurbineFilesForEachCase(self, writeFiles=True): self.ElastoDynFile['BlPitch(3)'] = self.bins.sel(wspd=Vhub_, method='nearest').BlPitch.values self.ElastoDynFile['NacYaw'] = yaw_deg_ + yaw_mis_deg_ + self.ElastoDynFile['PtfmSurge'] = xi_ + self.ElastoDynFile['PtfmSway'] = yi_ + self.ElastoDynFile['PtfmHeave'] = zi_ + self.ElastoDynFile['PtfmRoll'] = 0.0 + self.ElastoDynFile['PtfmPitch'] = 0.0 self.ElastoDynFile['PtfmYaw'] = phi_deg_ self.ElastoDynFile['BldFile1'] = self.ElastoDynFile['BldFile2'] = self.ElastoDynFile['BldFile3'] = f'"{self.EDbladefilename}"' self.ElastoDynFile['TwrFile'] = f'"{self.EDtowerfilename}"' self.ElastoDynFile['Azimuth'] = round(np.random.uniform(low=0, high=360)) # start at a random value + if writeFiles: if t==0: shutilcopy2_untilSuccessful(self.EDbladefilepath, os.path.join(currPath,self.EDbladefilename)) if t==0: shutilcopy2_untilSuccessful(self.EDtowerfilepath, os.path.join(currPath,self.EDtowerfilename)) @@ -1583,11 +1601,17 @@ def _create_all_cases(self): phi = self.wts_rot_ds.sel(inflow_deg=wdir)['phi'].values D = self.wts_rot_ds.sel(inflow_deg=wdir)['D'].values zhub = self.wts_rot_ds.sel(inflow_deg=wdir)['zhub'].values + xi = self.wts_rot_ds.sel(inflow_deg=wdir).get('xi', 0).values # initial platform condition (if they are given) + yi = self.wts_rot_ds.sel(inflow_deg=wdir).get('yi', 0).values + zi = self.wts_rot_ds.sel(inflow_deg=wdir).get('zi', 0).values oneCase = xr.Dataset({ 'Tx': (['case','turbine'], [x ]), 'Ty': (['case','turbine'], [y ]), 'Tz': (['case','turbine'], [z ]), + 'xi': (['case','turbine'], [xi ]), + 'yi': (['case','turbine'], [yi ]), + 'zi': (['case','turbine'], [zi ]), 'phi': (['case','turbine'], [phi ]), 'D': (['case','turbine'], [D ]), 'zhub': (['case','turbine'], [zhub]), @@ -1668,15 +1692,20 @@ def _rotate_wts(self): xori = self.wts[i]['x'] yori = self.wts[i]['y'] + xiori = self.wts[i]['xi'] + yiori = self.wts[i]['yi'] x = ref['x'] + (xori - ref['x']) * cosd(inflow) - (yori - ref['y']) * sind(inflow) y = ref['y'] + (xori - ref['x']) * sind(inflow) + (yori - ref['y']) * cosd(inflow) + xi = ref['xi'] + (xiori - ref['xi']) * cosd(inflow) - (yiori - ref['yi']) * sind(inflow) + yi = ref['yi'] + (xiori - ref['xi']) * sind(inflow) + (yiori - ref['yi']) * cosd(inflow) z = turb['z'] + zi = ref['zi'] phi = turb['phi_deg'] + inflow D = turb['D'] zhub = turb['zhub'] wts_rot[inflow,i] = {'x':x, 'y':y, 'z':z, 'phi': phi, - 'D':D, 'zhub':zhub, + 'D':D, 'zhub':zhub, 'xi':xi, 'yi':yi, 'zi':zi } self.wts_rot_ds = pd.DataFrame.from_dict(wts_rot, orient='index').to_xarray().rename({'level_0':'inflow_deg','level_1':'turbine'})