From 40131a994ffe82a8ffa987a6b4a22a82a9ea2f16 Mon Sep 17 00:00:00 2001 From: Regis Thedin Date: Wed, 28 Jan 2026 16:31:20 -0700 Subject: [PATCH 01/15] FF: fix import conflicts with different versions of scipy --- openfast_toolbox/postpro/postpro.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/openfast_toolbox/postpro/postpro.py b/openfast_toolbox/postpro/postpro.py index 66ed21e..23cccef 100644 --- a/openfast_toolbox/postpro/postpro.py +++ b/openfast_toolbox/postpro/postpro.py @@ -4,10 +4,13 @@ import numpy as np import re try: - from scipy.integrate import cumulative_trapezoid - from numpy import trapezoid -except: + from scipy.integrate import cumulative_trapezoid +except ImportError: from scipy.integrate import cumtrapz as cumulative_trapezoid + +try: + from numpy import trapezoid +except ImportError: from numpy import trapz as trapezoid import openfast_toolbox.io as weio From 88cdd73d1a1f274cfaa40e783cd34db728ce012c Mon Sep 17 00:00:00 2001 From: Regis Thedin Date: Wed, 4 Feb 2026 15:29:39 -0700 Subject: [PATCH 02/15] Allow runner to have per-call extra flags --- openfast_toolbox/case_generation/runner.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/openfast_toolbox/case_generation/runner.py b/openfast_toolbox/case_generation/runner.py index 914abad..fb1994b 100644 --- a/openfast_toolbox/case_generation/runner.py +++ b/openfast_toolbox/case_generation/runner.py @@ -368,7 +368,8 @@ def writeBatch(batchfile, fastfiles, fastExe=None, nBatches=1, pause=False, flag - 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 + - flags_after: flags to be placed after the filename (single string if the same for every file, + or a list of strings if different for each file) - 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 @@ -394,8 +395,11 @@ def writeBatch(batchfile, fastfiles, fastExe=None, nBatches=1, pause=False, flag fastExe_rel = os.path.relpath(fastExe_abs, batchdir) if len(flags)>0: flags=' '+flags - if len(flags_after)>0: - flags_after=' '+flags_after + if isinstance(flags_after, str): + if len(flags_after)>0: + flags_after=' '+flags_after + elif isinstance(flags_after, list): + flags_after = [' '+f if len(f)>0 else f for f in flags_after] # Remove commandlines if outputs are already present if discard_if_ext_present: @@ -414,10 +418,11 @@ def writeb(batchfile, fastfiles): f.write('@echo off\n') if preCommands is not None: f.write(preCommands+'\n') - for ff in fastfiles: + for i, ff in enumerate(fastfiles): ff_abs = os.path.abspath(ff) ff_rel = os.path.relpath(ff_abs, batchdir) - cmd = fastExe_rel + flags + ' '+ ff_rel + flags_after + cmd = fastExe_rel + flags + ' '+ ff_rel + cmd += flags_after[i] if isinstance(flags_after, list) else flags_after if stdOutToFile: stdout = os.path.splitext(ff_rel)[0]+'.stdout' cmd += ' > ' +stdout From 2bf59959377ad73ab49b6a22c4a99937564300c9 Mon Sep 17 00:00:00 2001 From: Regis Thedin Date: Thu, 5 Feb 2026 11:01:40 -0700 Subject: [PATCH 03/15] FF: Fix symlinks, clarify comments, fix caseList --- .../fastfarm/FASTFarmCaseCreation.py | 66 +++++++++++-------- 1 file changed, 37 insertions(+), 29 deletions(-) diff --git a/openfast_toolbox/fastfarm/FASTFarmCaseCreation.py b/openfast_toolbox/fastfarm/FASTFarmCaseCreation.py index 829f643..0ed16f9 100644 --- a/openfast_toolbox/fastfarm/FASTFarmCaseCreation.py +++ b/openfast_toolbox/fastfarm/FASTFarmCaseCreation.py @@ -80,6 +80,7 @@ def checkIfExists(f): return False def shutilcopy2_untilSuccessful(src, dst): + # Fail-safe for filesystem issues shutil.copy2(src, dst) if not checkIfExists(dst): print(f'File {dst} not created. Trying again.\n') @@ -268,6 +269,8 @@ def __init__(self, Not fully tested. ptfm_rot: bool Whether or not platforms have headings or not (False in case of fixed farms or floating with all platforms facing 0deg) + flat: bool + Whether or not to create a flat directory structure (all cases in the same folder) verbose: int Verbosity level, given as integers <5 @@ -321,7 +324,7 @@ def __init__(self, self.condDirList = [] self.caseDirList = [] self.DLLfilepath = None - self.DLLext = None + self.DLLext = None self.batchfile_high = '' self.batchfile_low = '' self.batchfile_ff = '' @@ -344,7 +347,7 @@ def __init__(self, # # TODO TODO TODO - # Creating Cases and Conditions should have it's own function interface for the user can call for a given + # Creating Cases and Conditions should have its own function interface so the user can call 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.') @@ -358,7 +361,7 @@ def __init__(self, def __repr__(self): - s='<{} object> with the following content:\n'.format(type(self).__name__) + s = f'<{type(self).__name__} object> with the following content:\n' 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' @@ -404,14 +407,14 @@ def __repr__(self): s += f' - dt low: {self.dt_low} 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' - if self.inflowType !='LES': + if self.inflowType == 'TS': s += f' Low-res boxes created: {self.TSlowBoxFilesCreatedBool} .\n' s += f' High-resolution domain: \n' s += f' - ds high: {self.ds_high} m\n' s += f' - dt high: {self.dt_high} s\n' s += f' - Extent of high-res boxes: {self.extent_high} D total\n' - if self.inflowType !='LES': + if self.inflowType == 'TS': s += f' High-res boxes created: {self.TShighBoxFilesCreatedBool}.\n' s += f"\n" @@ -656,8 +659,9 @@ def _checkInputs(self): # 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)') + raise FFException(f"Asked for {len(self.yaw_init)} yaw condition(s) but provided {len(self.inflow_deg)} inflow angle(s). "\ + f"Each yaw_init condition should have a corresponding inflow_deg. Duplicate inflow_deg as needed. Note "\ + f"this is irrespective to the the vhub/shear/TIvalue sweeps.") # Check reduced-order models if self.ADmodel is None or self.ADmodel == 'ADyn': @@ -727,7 +731,7 @@ def _checkInputs(self): if None in (self.dt_high, self.ds_high, self.dt_low, self.ds_low): WARN(f'One or more temporal or spatial resolution for low- and high-res domains were not given.\n'+ f'Estimated values for {_MOD_WAKE_STR[self.mod_wake]} wake model shown below.') - self._determine_resolutions_from_dummy_amrwind_grid() + self._determine_resolutions_from_dummy_les_grid() # Check the temporal and spatial resolutions if provided if self.dt_low != None and self.dt_high!= None: @@ -746,7 +750,7 @@ def _checkInputs(self): - def _determine_resolutions_from_dummy_amrwind_grid(self): + def _determine_resolutions_from_dummy_les_grid(self): from openfast_toolbox.fastfarm.AMRWindSimulation import AMRWindSimulation @@ -781,12 +785,11 @@ def _determine_resolutions_from_dummy_amrwind_grid(self): print(f'`ds_high = {2*amr.ds_high_les}`; ', end='') print(f'`dt_low = {2*amr.dt_low_les}`; ', end='') print(f'`ds_low = {2*amr.ds_low_les}`; ') - #print(f' If the values above are okay, you can safely ignore this warning.\n') self.dt_high = amr.dt_high_les - self.ds_high = amr.dt_high_les + self.ds_high = amr.ds_high_les self.dt_low = amr.dt_low_les - self.ds_low = amr.dt_low_les + self.ds_low = amr.ds_low_les @@ -813,7 +816,7 @@ def _create_dir_structure(self): self.condDirList = condDirList # --- Creating Case List - caseDirList_ = [] + caseDirList = [] 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 @@ -822,27 +825,23 @@ def _create_dir_structure(self): 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 + # Set current path name string. The case is of the following form: Case00_wdirp10_12fED_12ADyn 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.sweepYM: - caseStr += f"_YM{str(misalignment_).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 += f"_yawCase{yawCase_}" - caseDirList_.append(caseStr) + caseDirList.append(caseStr) - self.caseDirList = caseDirList_ + self.caseDirList = caseDirList # --- Creating directories including seed directories for cond in range(self.nConditions): @@ -882,26 +881,35 @@ def _copy(self, src, dst, debug=False): def _symlink(self, src, dst, debug=False): + # If src is a relative path, reconstruct the absolute path based on dst directory + if not os.path.isabs(src): + src_abs = os.path.normpath(os.path.join(os.path.dirname(dst), src)) + else: + src_abs = src + if debug: - print('SRC:', src, os.path.exists(src)) - print('DST:', dst, os.path.exists(dst)) - error = f"Src file not found: {src}" - if not os.path.exists(src): + print('SRC ABS:', src_abs, os.path.exists(src_abs)) + print('SRC REL:', src, os.path.exists(src)) + print('DST :', dst, os.path.exists(dst)) + error = f"Src file not found: {src_abs}" + + if not os.path.exists(src_abs): raise Exception(error) - #return error if not os.path.exists(dst): if self._can_create_symlinks: + # Unix-based try: os.symlink(src, dst) except FileExistsError: - error = dst + error = f"Dst file already exists: {dst}. Skipping symlink." else: + # Windows try: shutil.copy2(src, dst) except FileExistsError: if debug: raise Exception(error) - error = dst + error = f"Dst file already exists: {dst}. Skipping copy." return error From b4d119b4c102d51bdc7fb906f0ff9da9a04ae233 Mon Sep 17 00:00:00 2001 From: Regis Thedin Date: Thu, 5 Feb 2026 11:07:42 -0700 Subject: [PATCH 04/15] FF: Fix bug introduced in PR69 for handling of repeated high-res boxes --- .../fastfarm/FASTFarmCaseCreation.py | 51 ++++++++++++++----- 1 file changed, 37 insertions(+), 14 deletions(-) diff --git a/openfast_toolbox/fastfarm/FASTFarmCaseCreation.py b/openfast_toolbox/fastfarm/FASTFarmCaseCreation.py index 0ed16f9..3e028ff 100644 --- a/openfast_toolbox/fastfarm/FASTFarmCaseCreation.py +++ b/openfast_toolbox/fastfarm/FASTFarmCaseCreation.py @@ -537,19 +537,38 @@ def files(self, module='AD'): @property def high_res_bts(self): + + # Not all individual high-res boxes are unique. If there are cases where the same high-res + # boxes are warranted (e.g. different nacelle yaw values), then copies/symlinks are made + highBoxesCaseDirList = [self.caseDirList[c] for c in self.allHighBoxCases.case.values] + highBoxesCaseIndex = [self.caseDirList.index(c) for c in highBoxesCaseDirList] + files = [] - #highBoxesCaseDirList = [self.caseDirList[c] for c in self.allHighBoxCases.case.values] - #for condDir in self.condDirList: - # for case in highBoxesCaseDirList: for cond in range(self.nConditions): - for case in range(self.nCases): + for case in highBoxesCaseIndex: for seed in range(self.nSeeds): dirpath = self.getHRTurbSimPath(cond, case, seed) for t in range(self.nTurbines): - #dirpath = os.path.join(self.path, condDir, case, f"Seed_{seed}/TurbSim") files.append(f'{dirpath}/HighT{t+1}.bts') + return files + + @property + def high_res_log(self): + highBoxesCaseDirList = [self.caseDirList[c] for c in self.allHighBoxCases.case.values] + highBoxesCaseIndex = [self.caseDirList.index(c) for c in highBoxesCaseDirList] + + files = [] + for cond in range(self.nConditions): + for case in highBoxesCaseIndex: + for seed in range(self.nSeeds): + dirpath = self.getHRTurbSimPath(cond, case, seed) + for t in range(self.nTurbines): + files.append(f'{dirpath}/log.high{t+1}.seed{seed}.txt') + return files + + # TODO create the same properties for the low res def _checkInputs(self): #### check if the turbine in the template FF input exists. @@ -2275,7 +2294,7 @@ def TS_low_slurm_submit(self, qos='normal', A=None, t=None, p=None, inplace=True def TS_low_createSymlinks(self): - # Create symbolic links for all of the time-series and the Low.bts files too + # Create symbolic links for all of the time-series and the Low.bts files for cond in range(self.nConditions): for case in range(self.nCases): for seed in range(self.nSeeds): @@ -2501,8 +2520,9 @@ def TS_high_batch_prepare(self, run=False, **kwargs): ext = ".bat" if os.name == "nt" else ".sh" batchfile = os.path.join(self.path, f'runAllHighBox{ext}') - TS_files = [f.replace('.bts', '.inp') for f in self.high_res_bts] - writeBatch(batchfile, TS_files, fastExe=self.tsbin, **kwargs) + TS_high_files = [f.replace('.bts', '.inp') for f in self.high_res_bts] + TS_high_logs = self.high_res_log + writeBatch(batchfile, TS_high_files, fastExe=self.tsbin, flags_after=[f"2>&1 | tee {log}" for log in TS_high_logs], **kwargs) self.batchfile_high = batchfile OK(f"Batch file written to {batchfile}") @@ -2629,12 +2649,12 @@ def TS_high_create_symlink(self): # 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 the allHighBoxCases array. Found repeated case number. Stopping') + raise FFException('Something is wrong with the 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 + # Current case is source (contains bts). Skipping it. continue # If we are here, the case is destination. Let's find the first case with the same wdir for source @@ -2646,8 +2666,8 @@ def TS_high_create_symlink(self): 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 + # Let's make sure the src and destination are the same case, except ROM bool and yaw angles + # The xarrays we are comparing here contain 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 @@ -2655,9 +2675,12 @@ def TS_high_create_symlink(self): for seed in range(self.nSeeds): src = os.path.join(self.getHRTurbSimPath(cond, src_case, seed), f'HighT{t+1}.bts') dst = os.path.join(self.getHRTurbSimPath(cond, case , seed), f'HighT{t+1}.bts') + #print(f'src is {src}') + #print(f'dst is {dst}') #src = os.path.join('..', '..', '..', '..', self.condDirList[cond], self.caseDirList[src_case], f'Seed_{seed}', 'TurbSim', f'HighT{t+1}.bts') - print('Emmanuel Says: TODO Check the line below') - src = os.path.relpath(src, dst) + #print('Emmanuel Says: TODO Check the line below') + src = os.path.relpath(src, os.path.dirname(dst)) + #print(f'rel src{src}\n') self._symlink(src, dst) From e172931976e1cb8d7d76aca8b5d7c5c81d5977cd Mon Sep 17 00:00:00 2001 From: Regis Thedin Date: Thu, 5 Feb 2026 15:48:55 -0700 Subject: [PATCH 05/15] FF: clean-up, small reorganization, and small print/warning fixes --- .../fastfarm/FASTFarmCaseCreation.py | 115 ++++++++++-------- 1 file changed, 61 insertions(+), 54 deletions(-) diff --git a/openfast_toolbox/fastfarm/FASTFarmCaseCreation.py b/openfast_toolbox/fastfarm/FASTFarmCaseCreation.py index 3e028ff..053da12 100644 --- a/openfast_toolbox/fastfarm/FASTFarmCaseCreation.py +++ b/openfast_toolbox/fastfarm/FASTFarmCaseCreation.py @@ -3,29 +3,21 @@ import subprocess from contextlib import contextmanager import numpy as np -np.random.seed(12) # For reproducibility (e.g. random azimuth) +import xarray as xr from openfast_toolbox.tools.strings import INFO, FAIL, OK, WARN, print_bold - from openfast_toolbox.io import FASTInputDeck, FASTInputFile, FASTOutputFile, TurbSimFile, VTKFile from openfast_toolbox.io.rosco_discon_file import ROSCODISCONFile from openfast_toolbox.fastfarm import writeFastFarm -from openfast_toolbox.fastfarm import plotFastFarmSetup # Make it available +from openfast_toolbox.fastfarm import plotFastFarmSetup from openfast_toolbox.fastfarm import defaultOutRadii from openfast_toolbox.fastfarm.TurbSimCaseCreation import TSCaseCreation, writeTimeSeriesFile -from openfast_toolbox.modules.servodyn import check_discon_library # Make it available - - -try: - import xarray as xr -except ImportError: - FAIL('The python package xarray is not installed. FFCaseCreation will not work fully.\nPlease install it using:\n`pip install xarray`') +from openfast_toolbox.modules.servodyn import check_discon_library +np.random.seed(12) # For reproducibility (e.g. random azimuth) _MOD_WAKE_STR = ['','polar', 'curled', 'cartesian'] - - def cosd(t): return np.cos(np.deg2rad(t)) def sind(t): return np.sin(np.deg2rad(t)) @@ -35,6 +27,9 @@ def sind(t): return np.sin(np.deg2rad(t)) # --------------------------------------------------------------------------------{ @contextmanager def safe_cd(newdir): + ''' + Change directory and return to previous on exit. Use with `with` statement. + ''' prevdir = os.getcwd() try: os.chdir(newdir) @@ -502,6 +497,9 @@ def getCondSeedPath(self, cond, seed): @property def FFFiles(self): + ''' + Create list of all FAST.Farm input files to be executed + ''' files = [] for cond in range(self.nConditions): for case in range(self.nCases): @@ -571,9 +569,10 @@ def high_res_log(self): # TODO create the same properties for the low res def _checkInputs(self): - #### check if the turbine in the template FF input exists. # --- Default arguments + if self.vhub is None: + self.vhub = [8] if self.inflow_deg is None: self.inflow_deg = [0]*len(self.vhub) if self.TIvalue is None: @@ -1045,7 +1044,7 @@ def copyTurbineFilesForEachCase(self, writeFiles=True): self.InflowWindFile.write( os.path.join(seedPath, self.IWfilename)) - # Before starting the loop, print once the info about the controller is no controller is present + # Before starting the loop, print once the info about the controller if no controller is present if not self.hasSrvD: if self.verbose>=1: if self.ServoDynFile != 'unused': # to prevent getting an error if ServoDyn is not being used. @@ -1240,9 +1239,8 @@ def copyTurbineFilesForEachCase(self, writeFiles=True): elif self.attempt > 5: FAIL(f"Not all turbine files were copied successfully after 5 tries.\n"\ "Check them manually. This shouldn't occur.\n"\ - "Consider finding fixing the bug and submitting a PR.") + "Consider fixing the bug and submitting a PR.") else: - #if self.verbose>0: OK(f'All files were copied successfully.') OK(f'All OpenFAST files were copied successfully.') @@ -1345,7 +1343,8 @@ def setTemplateFilename(self, templatePath=None, templateFiles=None, templateFST Inputs ------ - templateFSTF: - The path, relative or absolute to a FAST.Farm fstf input file. + The path, relative or absolute to a FAST.Farm fstf input file. If using this + option, all the other template files will be read from the fstf file. - templatePath: str The path of the directory where teh template files exist. @@ -1355,9 +1354,9 @@ def setTemplateFilename(self, templatePath=None, templateFiles=None, templateFST Keys should correspond to the variable names expected in the function. The values should be strings with the filenames or filepaths as appropriate. The keys *filename assumes the file exists inside templatePath and only the - filename is needed. The keys *filepath should contain the full path and no - assumption is made regarding its location. - All values should be explicitly defined. Unused ones should be set as None. + filename is needed. The keys *path should contain the full path and no + assumption is made regarding its location. Unused keys can be skipped or + set as None. Example call - OPTION 1 -------------------------------------------------- @@ -1371,7 +1370,7 @@ def setTemplateFilename(self, templatePath=None, templateFiles=None, templateFST Example call - OPTION 2 -------------------------------------------------- - templatePath = 'full/path/to/template/files/' + templatePath = '/full/path/to/template/files/' templateFiles = { 'FFfilename' : 'Model_FFarm.fstf' 'turbfilename' : 'Model.T', @@ -1393,16 +1392,23 @@ def setTemplateFilename(self, templatePath=None, templateFiles=None, templateFST 'ADbladefilename' : 'AeroDyn_Blade.dat', 'controllerInputfilename' : 'DISCON', # TODO - 'coeffTablefilename' : None, + 'coeffTablefilename' : None, # Needed if ADsk is used 'hydroDatapath' : '/full/path/to/hydroData', - 'libdisconfilepath' : '/full/path/to/controller/libdiscon.so', + 'libdisconfilepath' : '/full/path/to/controller/libdiscon.[so,dylib,dll]', 'turbsimLowfilepath' : './SampleFiles/template_Low_InflowXX_SeedY.inp', 'turbsimHighfilepath' : './SampleFiles/template_HighT1_InflowXX_SeedY.inp', } - setTemplateFilename(templatePath, templateFiles) + setTemplateFilename(templatePath=templatePath, templateFiles=templateFiles) """ + + if templatePath is None and templateFSTF is None: + raise FFException('Either templatePath or templateFSTF must be provided.') + if templatePath is not None and templateFSTF is not None: + FFException('Cannot provide both templatePath and templateFSTF at the same time.') + INFO('Reading and checking template files') + if verbose is None: verbose=self.verbose @@ -1452,7 +1458,10 @@ def setTemplateFilename(self, templatePath=None, templateFiles=None, templateFST # TODO TODO, not all templateFiles have the same convention, this needs to be changed. if templatePath is not None: if not os.path.isdir(templatePath): - raise ValueError(f'Template path {templatePath} does not seem to exist. Current directory is: {os.getcwd()}') + if os.path.isabs(templatePath): + raise ValueError(f'Absolute template path {templatePath} does not seem to exist.') + else: + raise ValueError(f'Relative template path {templatePath} does not seem to exist. Full path is: {os.path.abspath(templatePath)}.') for key, value in templateFiles.items(): if key in ['turbsimLowfilepath', 'turbsimHighfilepath', 'libdisconfilepath']: # We skip those keys because there convention is not clear @@ -1509,8 +1518,9 @@ def setTemplateFilename(self, templatePath=None, templateFiles=None, templateFST # - *filepath: the absolute or relative path wrt caller script # - *filename: os.path.basename(filepath), the filename only # -------------------------------------------------------------------------------- - for key, value in templateFiles.items(): - if verbose>0: INFO(f'Template {key:23s}={value}') + if verbose>0: + for key, value in templateFiles.items(): + INFO(f'Template {key:24s}={value}') if not valid_keys >= set(templateFiles.keys()): raise ValueError(f'Extra entries are present in the dictionary. '\ @@ -1675,29 +1685,28 @@ def checkIfExists(f): raise ValueError(f'FAST.Farm input file should end in ".fstf".') self.FFfilepath = value checkIfExists(self.FFfilepath) - #self.FFfilename = os.path.basename(value) # TODO TODO This is not used, and outputFFfilename is used elif key == 'controllerInputfilename': if not value.lower().endswith('.in'): - print(f'--- WARNING: The controller input file typically ends in "*.IN". Currently {value}. Double check.') + WARN(f'The controller input file typically ends in "*.IN". Currently {value}. Double check.') self.controllerInputfilepath = value checkIfExists(self.controllerInputfilepath) self.controllerInputfilename = os.path.basename(value) elif key == 'coeffTablefilename': - if not value.endswith('.csv'): + if not value.lower().endswith('.csv'): raise ValueError(f'The performance table file should end in "*.csv"') self.coeffTablefilepath = value checkIfExists(self.coeffTablefilepath) self.coeffTablefilename = os.path.basename(value) + # --- Directories and files given with full path elif key == 'hydroDatapath': self.hydrodatafilepath = value if not os.path.isdir(self.hydrodatafilepath): raise ValueError(f'The hydroData directory hydroDatapath should be a directory. Received {value}.') self.hydroDatapath = os.path.basename(value) - # --- TODO TODO TODO not clean convention elif key == 'libdisconfilepath': ext = os.path.splitext(value)[1].lower() if ext not in ['.so', '.dll', '.dylib', '.dummy']: @@ -1708,19 +1717,19 @@ def checkIfExists(f): else: self.libdisconfilepath = os.path.abspath(value).replace('\\','/') checkIfExists(self.libdisconfilepath) - self._create_copy_libdiscon() + #self._create_copy_libdiscon() self.hasController = True # --- TODO TODO TODO not clean convention elif key == 'turbsimLowfilepath': - if not value.endswith('.inp'): + if not value.lower().endswith('.inp'): raise ValueError(f'TurbSim file input for low-res box should end in ".inp".') self.turbsimLowfilepath = value checkIfExists(self.turbsimLowfilepath) # --- TODO TODO TODO not clean convention elif key == 'turbsimHighfilepath': - if not value.endswith('.inp'): + if not value.lower().endswith('.inp'): raise ValueError(f'TurbSim file input for high-res box should end in ".inp".') self.turbsimHighfilepath = value checkIfExists(self.turbsimHighfilepath) @@ -1765,12 +1774,13 @@ def _create_copy_libdiscon(self): self.DLLfilepath = os.path.join(DLL_parentDir, f'{libdisconfilename}.T') # No extension currLibdiscon = os.path.join(DLL_parentDir, f'{libdisconfilename}.T{t+1}.{self.DLLext}') if not os.path.isfile(currLibdiscon): - if self.verbose>0: print(f' Creating a copy of the controller {self.libdisconfilepath} in {currLibdiscon}') + if self.verbose>0: + INFO(f'Creating a copy of the controller {self.libdisconfilepath} in {currLibdiscon}') shutil.copy2(self.libdisconfilepath, currLibdiscon) copied=True if copied == False and self.verbose>0: - print(f' Copies of the controller {libdisconfilename}.T[1-{self.nTurbines}].{self.DLLext} already exists in {os.path.dirname(self.libdisconfilepath)}. Skipped step.') + INFO(f'Copies of the controller {libdisconfilename}.T[1-{self.nTurbines}].{self.DLLext} already exists in {os.path.dirname(self.libdisconfilepath)}. Skipped step.') def _open_template_files(self): @@ -1806,7 +1816,7 @@ def createAuxArrays(self): self._create_all_cases() if self.flat: if self.nCases==1 and self.nConditions==1: - self.flat + pass # keep flat=True for single case/condition else: self.flat = False @@ -1816,8 +1826,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'\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>0: INFO(f'The length of vhub, shear, and TI are the same. Assuming each position is a condition.') + if self.verbose>0: INFO(f'Creating {self.nConditions} conditions') self.allCond = xr.Dataset({'vhub': (['cond'], self.vhub ), 'shear': (['cond'], self.shear ), @@ -1828,8 +1838,8 @@ def _create_all_cond(self): 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} condition(s)') + if self.verbose>0: INFO(f'The length of vhub, shear, and TI are different. Assuming sweeps on all of them.') + if self.verbose>0: INFO(f'Creating {self.nConditions} condition(s)') # Repeat arrays as necessary to build xarray Dataset combination = np.vstack(list(itertools.product(self.vhub,self.shear,self.TIvalue))) @@ -1844,7 +1854,7 @@ def _create_all_cond(self): def _create_all_cases(self): - # Generate the different "cases" (inflow angle and yaw misalignment bools). + # Generate the different "cases" (inflow angle). # If misalignment true, then the actual yaw is yaw[turb]=np.random.uniform(low=-8.0, high=8.0). # Set sweep bools and multipliers @@ -2079,7 +2089,6 @@ def TS_low_dummy(self): def TS_low_setup(self, writeFiles=True, runOnce=False): INFO('Preparing TurbSim low resolution input files.') - # Loops on all conditions/seeds creating Low-res TurbSim box (following openfast_toolbox/openfast_toolbox/fastfarm/examples/Ex1_TurbSimInputSetup.py) boxType='lowres' lowFilesName = [] @@ -2352,10 +2361,6 @@ def getDomainParameters(self): print(f" The x offset between the turbine ref frame and turbsim is {self.xoffset_turbsOrigin2TSOrigin}") print(f" The y offset between the turbine ref frame and turbsim is {self.yoffset_turbsOrigin2TSOrigin}") - if self.verbose>2: - print(f'allHighBoxCases is:') - print(self.allHighBoxCases) - def TS_high_get_time_series(self): @@ -2805,9 +2810,9 @@ def _FF_setup_LES(self, seedsToKeep=1): for seed in range(self.seedsToKeep): # Remove TurbSim dir currpath = self.getHRTurbSimPath(cond, case, seed) - seedPath = self.getCaseSeedPath(cond, case, seed) if os.path.isdir(currpath): shutil.rmtree(currpath) # Create LES boxes dir + seedPath = self.getCaseSeedPath(cond, case, seed) currpath = os.path.join(seedPath, LESboxesDirName) if not os.path.isdir(currpath): os.makedirs(currpath) @@ -3173,7 +3178,10 @@ def _getBoxesParamsForFF(self, lowbts, highbts, dt_low_desired, D, HubHt, xWT, y def FF_batch_prepare(self, ffbin=None, run=False, **kwargs): - """ Writes a flat batch file for FASTFarm cases""" + """ + Writes a flat batch file for FASTFarm cases. + Allows specification of a different binary. + """ from openfast_toolbox.case_generation.runner import writeBatch if ffbin is not None: @@ -3212,8 +3220,7 @@ def FF_slurm_prepare(self, slurmfilepath, inplace=True, useSed=True, ffbin=None) raise ValueError (f'SLURM script for FAST.Farm {slurmfilepath} does not exist.') self.slurmfilename_ff = os.path.basename(slurmfilepath) - - WARN('Implementation Note: Developper help needed. This function requires sed. Please use regexp similar to what was done for `TS_low_slurm_prepare` or `TS_high_slurm_prepare`.') + WARN('Implementation Note: Developer help needed. This function requires sed. Please use regexp similar to what was done for `TS_low_slurm_prepare` or `TS_high_slurm_prepare`.') for cond in range(self.nConditions): for case in range(self.nCases): @@ -3320,7 +3327,7 @@ def set_wake_model_params(self, C_HWkDfl_OY=None, C_HWkDfl_xY=None, k_VortexDeca # User is passing C_HWkDfl_OY and C_HWkDfl_xY, thus polar wake. Check others. if k_VortexDecay is None and k_vCurl is None: if self.mod_wake != 1: - raise ValueError(f'Passed C_HWkDfl_OY and C_HWkDfl_xY but the wake model requested is not polar.') + WARN(f'Passed C_HWkDfl_OY and C_HWkDfl_xY but the wake model requested is not polar. Leaving k_VortexDecay and k_vCurl unmodified') if not isinstance(C_HWkDfl_OY, (int, float)): raise ValueError(f'C_HWkDfl_OY should be a scalar. Received {C_HWkDfl_OY}.') if not isinstance(C_HWkDfl_xY, (int, float)): @@ -3331,7 +3338,7 @@ def set_wake_model_params(self, C_HWkDfl_OY=None, C_HWkDfl_xY=None, k_VortexDeca # User is passing k_VortexDecay and k_vCurl, thus curled wake. Check others. if C_HWkDfl_OY is None and C_HWkDfl_xY is None: if self.mod_wake != 2: - raise ValueError(f'Passed k_VortexDecay and k_vCurl but the wake model requested is not curl.') + WARN(f'Passed k_VortexDecay and k_vCurl but the wake model requested is not curl. Leaving C_HWkDfl_OY and C_HWkDfl_xY unmodified') if not isinstance(k_VortexDecay, (int, float)): raise ValueError(f'k_VortexDecay should be a scalar. Received {k_VortexDecay}.') if not isinstance(k_vCurl, (int, float)): @@ -3365,7 +3372,7 @@ def save(self, dill_filename='ffcase_obj.dill'): try: import dill except ImportError: - FAIL('The python package fill is not installed. FFCaseCreation cannot be saved to disk.\nPlease install it using:\n`pip install dill`') + FAIL('The python package dill is not installed. FFCaseCreation cannot be saved to disk.\nPlease install it using:\n`pip install dill`') return objpath = os.path.join(self.path, dill_filename) From 7eac100e1515b60f789bcfc14908711a66801a8b Mon Sep 17 00:00:00 2001 From: Regis Thedin Date: Fri, 6 Feb 2026 11:25:28 -0700 Subject: [PATCH 06/15] Fix unittest asserts given bugfixes; resolve test deprecation warning --- .github/workflows/development-pipeline.yml | 2 +- .../fastfarm/examples/Ex1_FASTFarm_discretization.py | 10 ++++------ openfast_toolbox/modules/elastodyn.py | 6 +++++- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/.github/workflows/development-pipeline.yml b/.github/workflows/development-pipeline.yml index 5e9e460..01756dc 100644 --- a/.github/workflows/development-pipeline.yml +++ b/.github/workflows/development-pipeline.yml @@ -33,7 +33,7 @@ jobs: python-version: 3.12 python: xvfb-run python3 pip_arg: "" - - os: macos-13 + - os: macos-15 python-version: 3.12 python: python3 pip_arg: "" diff --git a/openfast_toolbox/fastfarm/examples/Ex1_FASTFarm_discretization.py b/openfast_toolbox/fastfarm/examples/Ex1_FASTFarm_discretization.py index 68df741..228a461 100644 --- a/openfast_toolbox/fastfarm/examples/Ex1_FASTFarm_discretization.py +++ b/openfast_toolbox/fastfarm/examples/Ex1_FASTFarm_discretization.py @@ -187,8 +187,6 @@ def main(test=False): """ - - return ffcase, ffcase2 @@ -200,9 +198,9 @@ def main(test=False): if __name__=='__test__': ffcase, ffcase2 = main(test=True) - np.testing.assert_equal(ffcase.ds_low, 0.9) + np.testing.assert_equal(ffcase.ds_low, 20) np.testing.assert_equal(ffcase.dt_low, 0.9) - np.testing.assert_equal(ffcase.ds_high, 0.3) + np.testing.assert_equal(ffcase.ds_high, 5) np.testing.assert_equal(ffcase.dt_high, 0.3) np.testing.assert_array_equal(ffcase.extent_low, [3, 6, 3, 3, 2] ) np.testing.assert_equal(ffcase.vhub , [8]) @@ -211,9 +209,9 @@ def main(test=False): np.testing.assert_equal(ffcase.shear , [0]) np.testing.assert_equal(ffcase.TIvalue, [10]) - np.testing.assert_equal(ffcase2.ds_low, 2.0) + np.testing.assert_equal(ffcase2.ds_low, 25) np.testing.assert_equal(ffcase2.dt_low, 2.0) - np.testing.assert_equal(ffcase2.ds_high, 0.5) + np.testing.assert_equal(ffcase2.ds_high, 5) np.testing.assert_equal(ffcase2.dt_high, 0.5) np.testing.assert_array_equal(ffcase2.extent_low, [3, 6, 3, 3, 2] ) diff --git a/openfast_toolbox/modules/elastodyn.py b/openfast_toolbox/modules/elastodyn.py index 6da35a9..53b01d1 100644 --- a/openfast_toolbox/modules/elastodyn.py +++ b/openfast_toolbox/modules/elastodyn.py @@ -108,8 +108,12 @@ def RotMat_AxisAngle(u,theta): """ Returns the rotation matrix for a rotation around an axis u, with an angle theta """ R=np.zeros((3,3)) - ux,uy,uz=u + u = np.asarray(u).ravel() # Ensure 1D array + ux,uy,uz = u[0], u[1], u[2] c,s=np.cos(theta),np.sin(theta) + # Ensure scalar values to avoid numpy deprecation warnings + ux, uy, uz = float(ux), float(uy), float(uz) + c, s = float(c), float(s) R[0,0]=ux**2*(1-c)+c ; R[0,1]=ux*uy*(1-c)-uz*s; R[0,2]=ux*uz*(1-c)+uy*s; R[1,0]=uy*ux*(1-c)+uz*s ; R[1,1]=uy**2*(1-c)+c ; R[1,2]=uy*uz*(1-c)-ux*s R[2,0]=uz*ux*(1-c)-uy*s ; R[2,1]=uz*uy*(1-c)+ux*s; R[2,2]=uz**2*(1-c)+c; From 9ce33f2b00237662be2699701ad1d96bb490ef00 Mon Sep 17 00:00:00 2001 From: Regis Thedin Date: Fri, 6 Feb 2026 11:34:56 -0700 Subject: [PATCH 07/15] FF: remove unnecessary TS_low_dummy and check successful TS completion - TS_low_dummy was introcuced in PR69 but we already had a mechanism for it (using the `runOnce` flag. - The check of the output of turbsim was there before and was removed on PR69, only checking the presence and non-zero-bytes value of the output files. Now we put the check back. PR69 handles the errors in a different way and thus wouldn't need this check, but when using SLURM scripts it is still needed --- .../fastfarm/FASTFarmCaseCreation.py | 56 ++++--------------- 1 file changed, 11 insertions(+), 45 deletions(-) diff --git a/openfast_toolbox/fastfarm/FASTFarmCaseCreation.py b/openfast_toolbox/fastfarm/FASTFarmCaseCreation.py index 053da12..7bed82b 100644 --- a/openfast_toolbox/fastfarm/FASTFarmCaseCreation.py +++ b/openfast_toolbox/fastfarm/FASTFarmCaseCreation.py @@ -2057,35 +2057,6 @@ def _isclose(a, b, tol=1): - def TS_low_dummy(self): - boxType='lowres' - tmp_dir='_turbsim_temp' - seedPath = tmp_dir - if not os.path.isdir(seedPath): - os.makedirs(seedPath) - - # ---------------- TurbSim Low boxes setup ------------------ # - # 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=0)['vhub' ].values - shear_ = self.allCond.sel(cond=0)['shear' ].values - tivalue_ = self.allCond.sel(cond=0)['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 - TSlowbox = TSCaseCreation(D_, HubHt_, Vhub_, tivalue_, shear_, x=xlocs_, y=ylocs_, zbot=self.zbot, - cmax=self.cmax, fmax=self.fmax, Cmeander=self.Cmeander, boxType='lowres', extent=self.extent_low, - ds_low=self.ds_low, dt_low=self.dt_low, ds_high=self.ds_high, dt_high=self.dt_high, mod_wake=self.mod_wake) - - return TSlowbox - def TS_low_setup(self, writeFiles=True, runOnce=False): INFO('Preparing TurbSim low resolution input files.') @@ -2335,7 +2306,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.TSlowbox = self.TS_low_dummy() + self.TS_low_setup(writeFiles=False, runOnce=True) # Figure out how many (and which) high boxes actually need to be executed. Remember that yaw misalignment, SED/ADsk models, # and sweep in yaw do not require extra TurbSim runs @@ -2762,25 +2733,20 @@ def FF_setup(self, outlistFF=None, **kwargs): elif self.inflowStr == 'TurbSim': all_bts = self.high_res_bts + # Check if the high-res boxes from TurbSim are present and non-zero size for bts in all_bts: if not os.path.isfile(bts): - raise FFException(f'File Missing: {bts}\nAll TurbSim boxes need to be completed before this step can be done.') + raise FFException(f'File missing: {bts}\nAll TurbSim boxes need to be completed before this step can be done.') if os.path.getsize(bts)==0: raise FFException(f'File has zero size: {bts}\n All TurbSim boxes need to be completed before this step can be done.') - - # --- Legacy, check log file from TurbSim - # We need to make sure the TurbSim boxes have been executed. Let's check the last line of the logfile - #highbox_path = os.path.join(self.path, self.condDirList[0], self.caseDirList[0], 'Seed_0', 'TurbSim', 'HighT1.bts') - #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 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.') + + # Now check if the boxes have been executed successfully. Let's check the last line of the logfile. + for f in self.high_res_log: + last = None + with open(f) as file: + for last in (line for line in file if line.rstrip('\n')): pass + if last is None or 'TurbSim terminated normally' not in last: + raise FFException(f'TurbSim not successful: {f}.') self._FF_setup_TS(**kwargs) From fd578bed7e65057ae53f6b009602c08f8e01b517 Mon Sep 17 00:00:00 2001 From: Regis Thedin Date: Fri, 6 Feb 2026 11:41:36 -0700 Subject: [PATCH 08/15] FF: add output file to local batch runs of TS so it can be checked --- openfast_toolbox/fastfarm/FASTFarmCaseCreation.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/openfast_toolbox/fastfarm/FASTFarmCaseCreation.py b/openfast_toolbox/fastfarm/FASTFarmCaseCreation.py index 7bed82b..7f4b66b 100644 --- a/openfast_toolbox/fastfarm/FASTFarmCaseCreation.py +++ b/openfast_toolbox/fastfarm/FASTFarmCaseCreation.py @@ -2144,19 +2144,22 @@ def TS_low_batch_prepare(self, tsbin=None, run=False, **kwargs): from openfast_toolbox.case_generation.runner import writeBatch if tsbin is not None: + WARN(f'Overwritting the TurbSim binary from the previously set {self.tsbin} to {tsbin}.') self.tsbin = tsbin self._checkTSBinary() ext = ".bat" if os.name == "nt" else ".sh" batchfile = os.path.join(self.path, f'runAllLowBox{ext}') - TS_files = [] + TS_low_files = [] + TS_low_logs = [] for cond in range(self.nConditions): for seed in range(self.nSeeds): seedpath = self.getCondSeedPath(cond, seed) - TS_files.append(f'{seedpath}/Low.inp') + TS_low_files.append(os.path.join(seedpath, 'Low.inp')) + TS_low_logs.append(os.path.join(seedpath, f'log.low.seed{seed}.txt')) - writeBatch(batchfile, TS_files, fastExe=self.tsbin, **kwargs) + writeBatch(batchfile, TS_low_files, fastExe=self.tsbin, flags_after=[f"2>&1 | tee {log}" for log in TS_low_logs], **kwargs) self.batchfile_low = batchfile OK(f"Batch file written to {batchfile}") From 4442277b7033fed58f9a7d9a6d9287593b33ae7e Mon Sep 17 00:00:00 2001 From: Regis Thedin Date: Mon, 9 Feb 2026 12:41:50 -0700 Subject: [PATCH 09/15] FF: better handling of low- and high-res list of files; add skipcheck the skipcheck flag allows us to test all the way to FF_setup, as opposed to stopping at the low-res generation --- .../fastfarm/FASTFarmCaseCreation.py | 93 +++++++++++++------ 1 file changed, 64 insertions(+), 29 deletions(-) diff --git a/openfast_toolbox/fastfarm/FASTFarmCaseCreation.py b/openfast_toolbox/fastfarm/FASTFarmCaseCreation.py index 7f4b66b..2b42b23 100644 --- a/openfast_toolbox/fastfarm/FASTFarmCaseCreation.py +++ b/openfast_toolbox/fastfarm/FASTFarmCaseCreation.py @@ -193,6 +193,7 @@ def __init__(self, refTurb_rot = 0, #ptfm_rot = False, flat=False, + skipchecks=False, verbose = 0): ''' Full setup of a FAST.Farm simulations, can create setups for LES- or TurbSim-driven scenarios. @@ -266,6 +267,8 @@ def __init__(self, Whether or not platforms have headings or not (False in case of fixed farms or floating with all platforms facing 0deg) flat: bool Whether or not to create a flat directory structure (all cases in the same folder) + skipchecks: bool + Whether or not to skip checks for TurbSim runs andthe existence of files before creating symlinks. Only used for tests. verbose: int Verbosity level, given as integers <5 @@ -301,6 +304,7 @@ def __init__(self, #self.ptfm_rot = ptfm_rot self.verbose = verbose self.attempt = 1 + self.skipchecks = skipchecks self.flat = flat # Set aux variable self.templateFilesCreatedBool = False @@ -348,7 +352,7 @@ def __init__(self, if self.verbose>0: print(f'Creating auxiliary arrays for all conditions and cases... Done.') - if path is not None: + if self.path is not None: # TODO TODO, this should only be done when user ask for input file creation if self.verbose>0: print(f'Creating directory structure and copying files...', end='\r') self._create_dir_structure() @@ -566,7 +570,24 @@ def high_res_log(self): files.append(f'{dirpath}/log.high{t+1}.seed{seed}.txt') return files - # TODO create the same properties for the low res + @property + def low_res_bts(self): + files = [] + for cond in range(self.nConditions): + for seed in range(self.nSeeds): + dirpath = self.getCondSeedPath(cond, seed) + files.append(f'{dirpath}/Low.bts') + return files + + @property + def low_res_log(self): + files = [] + for cond in range(self.nConditions): + for seed in range(self.nSeeds): + dirpath = self.getCondSeedPath(cond, seed) + files.append(f'{dirpath}/log.low.seed{seed}.txt') + return files + def _checkInputs(self): @@ -581,6 +602,10 @@ def _checkInputs(self): self.shear = [0]*len(self.vhub) if self.tmax is None: self.tmax = 0.00001 + self.tmax_low = self.tmax + + if self.skipchecks: + WARN('Skipping checks on TurbSim files and symlinks. This should only be used for testing purposes.') # Check the wind turbine dict @@ -884,16 +909,16 @@ def _copy(self, src, dst, debug=False): print('SRC:', src, os.path.exists(src)) print('DST:', dst, os.path.exists(dst)) error = f"Src file not found: {src}" - if not os.path.exists(src): + if not os.path.exists(src) or not self.skipchecks: raise Exception(error) #return error if not os.path.exists(dst): - #try: - shutil.copy2(src, dst) - #except FileExistsError: - # if debug: - # raise Exception(error) - # error = dst + try: + shutil.copy2(src, dst) + except FileExistsError: + if debug: + raise Exception(error) + error = f"Dst file already exists: {dst}. Skipping copy." return error @@ -911,7 +936,7 @@ def _symlink(self, src, dst, debug=False): print('DST :', dst, os.path.exists(dst)) error = f"Src file not found: {src_abs}" - if not os.path.exists(src_abs): + if not os.path.exists(src_abs) and not self.skipchecks: raise Exception(error) if not os.path.exists(dst): if self._can_create_symlinks: @@ -922,12 +947,8 @@ def _symlink(self, src, dst, debug=False): error = f"Dst file already exists: {dst}. Skipping symlink." else: # Windows - try: - shutil.copy2(src, dst) - except FileExistsError: - if debug: - raise Exception(error) - error = f"Dst file already exists: {dst}. Skipping copy." + WARN('Windows detected: creating a copies instead of symlinks.') + error = self._copy(src, dst, debug=debug) return error @@ -2063,6 +2084,7 @@ def TS_low_setup(self, writeFiles=True, runOnce=False): boxType='lowres' lowFilesName = [] + self.TSlowbox = [] for cond in range(self.nConditions): for seed in range(self.nSeeds): seedPath = self.getCondSeedPath(cond, seed) @@ -2086,10 +2108,12 @@ def TS_low_setup(self, writeFiles=True, runOnce=False): # 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 - self.TSlowbox = TSCaseCreation(D_, HubHt_, Vhub_, tivalue_, shear_, x=xlocs_, y=ylocs_, zbot=self.zbot, + currTSlowbox = TSCaseCreation(D_, HubHt_, Vhub_, tivalue_, shear_, x=xlocs_, y=ylocs_, zbot=self.zbot, cmax=self.cmax, fmax=self.fmax, Cmeander=self.Cmeander, boxType='lowres', extent=self.extent_low, ds_low=self.ds_low, dt_low=self.dt_low, ds_high=self.ds_high, dt_high=self.dt_high, mod_wake=self.mod_wake) + self.TSlowbox.append(currTSlowbox) + if runOnce: return # Write the actual TurbSim input file. Here we set the total simulation time to one time-step @@ -2097,7 +2121,7 @@ def TS_low_setup(self, writeFiles=True, runOnce=False): # flowfield is shorter than the requested total simulation time. So if we ask for the low-res # with the exact length we want, the high-res boxes might be shorter than tmax. Note that the # total FAST.Farm simulation time remains unmodified from what the user requested. - self.TSlowbox.writeTSFile(fileIn=self.turbsimLowfilepath, fileOut=currentTSLowFile, tmax=self.tmax+self.dt_low, verbose=self.verbose) + currTSlowbox.writeTSFile(fileIn=self.turbsimLowfilepath, fileOut=currentTSLowFile, tmax=self.tmax+self.dt_low, verbose=self.verbose) # Modify some values and save file (some have already been set in the call above) Lowinp = FASTInputFile(currentTSLowFile) @@ -2421,17 +2445,23 @@ def TS_high_get_time_series(self): def TS_high_setup(self, writeFiles=True): INFO('Preparing TurbSim high resolution input files.') - #todo: Check if the low-res boxes were created successfully + # Check low-res box(es) + if not self.skipchecks: + if not self.TSlowBoxFilesCreatedBool: + raise FFException('The low-res boxes files have not been created yet. Please run TS_low_setup first.') + self.check_turbsim_success(self.low_res_bts, self.low_res_log) # Create symbolic links for the low-res boxes # TODO TODO TODO Simply store address of files self.TS_low_createSymlinks() - # Open low-res boxes and extract time-series at turbine locations - self.TS_high_get_time_series() + if not self.skipchecks: + # 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 highFilesName = [] + self.TShighbox = [] 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 @@ -2460,11 +2490,12 @@ def TS_high_setup(self, writeFiles=True): 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, + currTShighbox = TSCaseCreation(D_, HubHt_, Vhub_, tivalue_, shear_, x=xloc_, y=yloc_, zbot=self.zbot, cmax=self.cmax, fmax=self.fmax, Cmeander=self.Cmeander, boxType='highres', extent=self.extent_high, ds_low=self.ds_low, dt_low=self.dt_low, ds_high=self.ds_high, dt_high=self.dt_high, mod_wake=self.mod_wake) - currentTS.writeTSFile(fileIn=self.turbsimHighfilepath, fileOut=currentTSHighFile, tmax=self.tmax_low, turb=t, verbose=self.verbose) + currTShighbox.writeTSFile(fileIn=self.turbsimHighfilepath, fileOut=currentTSHighFile, tmax=self.tmax_low, turb=t, verbose=self.verbose) + self.TShighbox.append(currTShighbox) # Modify some values and save file (some have already been set in the call above) Highinp = FASTInputFile(currentTSHighFile) @@ -2928,12 +2959,16 @@ def _FF_setup_TS(self): templateFSTF = self.FFfilepath outputFSTF = os.path.join(seedPath, self.outputFFfilename) - # 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')) # TODO TODO TODO Get Path - 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 - d = self._getBoxesParamsForFF(lowbts, highbts, self.dt_low, D_, HubHt_, xWT, yt) + if self.skipchecks: + d = None + else: + # 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')) # TODO TODO TODO Get Path + 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 + d = self._getBoxesParamsForFF(lowbts, highbts, self.dt_low, D_, HubHt_, xWT, yt) + self.dtemp = d #todo remove # Write the file if self.flat: From 2e83ceb4a6ae200dba6c5ffdcd9bfb25109033e3 Mon Sep 17 00:00:00 2001 From: Regis Thedin Date: Mon, 9 Feb 2026 12:48:17 -0700 Subject: [PATCH 10/15] FF: completely remove sweep on yaw misalignment --- .../fastfarm/FASTFarmCaseCreation.py | 58 ++++--------------- 1 file changed, 12 insertions(+), 46 deletions(-) diff --git a/openfast_toolbox/fastfarm/FASTFarmCaseCreation.py b/openfast_toolbox/fastfarm/FASTFarmCaseCreation.py index 2b42b23..4b00ffa 100644 --- a/openfast_toolbox/fastfarm/FASTFarmCaseCreation.py +++ b/openfast_toolbox/fastfarm/FASTFarmCaseCreation.py @@ -189,7 +189,6 @@ def __init__(self, seedValues = None, inflowPath = None, inflowType = None, - sweepYawMisalignment = False, refTurb_rot = 0, #ptfm_rot = False, flat=False, @@ -258,8 +257,6 @@ def __init__(self, Full path of the LES data, if driven by LES. If None, the setup will be for TurbSim inflow. inflowPath 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 inflowPath can be 3 paths, related to each condition. - 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. Not fully tested. @@ -298,7 +295,6 @@ def __init__(self, self.nSeeds = nSeeds self.inflowPath = inflowPath self.inflowType = inflowType - self.sweepYM = sweepYawMisalignment self.seedValues = seedValues self.refTurb_rot = refTurb_rot #self.ptfm_rot = ptfm_rot @@ -320,10 +316,11 @@ def __init__(self, self.hasBD = False self.multi_HD = False self.multi_MD = False - self.condDirList = [] - self.caseDirList = [] - self.DLLfilepath = None - self.DLLext = None + self.tmax_low = tmax + self.condDirList = [] + self.caseDirList = [] + self.DLLfilepath = None + self.DLLext = None self.batchfile_high = '' self.batchfile_low = '' self.batchfile_ff = '' @@ -863,7 +860,6 @@ 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 - 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 @@ -872,8 +868,6 @@ def _create_dir_structure(self): 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.sweepYM: - # caseStr += f"_YM{str(misalignment_).lower()}" if self.sweepEDmodel: caseStr += f"_{nFED_}fED" if self.sweepADmodel: @@ -1076,17 +1070,10 @@ def copyTurbineFilesForEachCase(self, writeFiles=True): 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 phi_deg_ = self.allCases.sel(case=case, turbine=t)['phi'].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 - # Update each turbine's elastic model if EDmodel_ == 'FED': self.ElastoDynFile['RotSpeed'] = self.bins.sel(wspd=Vhub_, method='nearest').RotSpeed.values @@ -1094,7 +1081,7 @@ def copyTurbineFilesForEachCase(self, writeFiles=True): 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['NacYaw'] = yaw_deg_ self.ElastoDynFile['PtfmYaw'] = phi_deg_ # The blade file entry `BldFile[1-3]` is not actually read. Sometimes we see `BldFile([1-3])`. if 'BldFile1' in self.ElastoDynFile.keys(): @@ -1117,7 +1104,7 @@ def copyTurbineFilesForEachCase(self, writeFiles=True): 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_ + self.SElastoDynFile['NacYaw'] = yaw_deg_ if writeFiles: self.SElastoDynFile.write(os.path.join(currPath,f'{self.SEDfilename}{t+1}_mod.dat')) @@ -1142,7 +1129,7 @@ def copyTurbineFilesForEachCase(self, writeFiles=True): # Update each turbine's ServoDyn if self.hasSrvD: - self.ServoDynFile['YawNeut'] = yaw_deg_ + yaw_mis_deg_ + self.ServoDynFile['YawNeut'] = yaw_deg_ self.ServoDynFile['VSContrl'] = 5 self.ServoDynFile['DLL_FileName'] = f'"{self.DLLfilepath}{t+1}.{self.DLLext}"' self.ServoDynFile['DLL_InFile'] = f'"{self.controllerInputfilename}"' @@ -1876,10 +1863,8 @@ def _create_all_cond(self): def _create_all_cases(self): # Generate the different "cases" (inflow angle). - # If misalignment true, then the actual yaw is yaw[turb]=np.random.uniform(low=-8.0, high=8.0). # Set sweep bools and multipliers - nCasesYMmultiplier = 2 if self.sweepYM else 1 nCasesROmultiplier = len(self.EDmodel) if len(self.ADmodel) == 1: self.sweepEDmodel = False @@ -1960,26 +1945,8 @@ def _create_all_cases(self): 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']) @@ -2335,11 +2302,10 @@ def getDomainParameters(self): 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 yaw misalignment, SED/ADsk models, + # Figure out how many (and which) high boxes actually need to be executed. Remember that 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 - # 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: @@ -2649,7 +2615,7 @@ def TS_high_slurm_submit(self, qos='normal', A=None, t=None, p=None, inplace=Tru def TS_high_create_symlink(self): - # Create symlink of all the high boxes for the cases with yaw misalignment. These are the "repeated" boxes + # Create symlink of all the high boxes for the cases with different turbine properties (e.g. yaw). These are the "repeated" boxes if self.verbose>0: print(f'Creating symlinks for all the high-resolution boxes') @@ -2668,7 +2634,7 @@ def TS_high_create_symlink(self): 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'] + varsToDrop = ['yaw','yawCase','ADmodel','EDmodel','nFullAeroDyn','nFulllElastoDyn'] dst_xr = self.allCases.sel(case=case, drop=True).drop_vars(varsToDrop) currwdir = dst_xr['inflow_deg'] From 0e9d5d93f23bf5a666816f73a398e4bc6c858de0 Mon Sep 17 00:00:00 2001 From: Regis Thedin Date: Mon, 9 Feb 2026 12:50:39 -0700 Subject: [PATCH 11/15] FF: checking actual successful completion of TS boxes Removed in PR69, now added back in a more robust way. Just checking the existence of the file is not enough. --- .../fastfarm/FASTFarmCaseCreation.py | 77 +++++++++---------- 1 file changed, 37 insertions(+), 40 deletions(-) diff --git a/openfast_toolbox/fastfarm/FASTFarmCaseCreation.py b/openfast_toolbox/fastfarm/FASTFarmCaseCreation.py index 4b00ffa..c860d7e 100644 --- a/openfast_toolbox/fastfarm/FASTFarmCaseCreation.py +++ b/openfast_toolbox/fastfarm/FASTFarmCaseCreation.py @@ -536,7 +536,6 @@ def files(self, module='AD'): @property def high_res_bts(self): - # Not all individual high-res boxes are unique. If there are cases where the same high-res # boxes are warranted (e.g. different nacelle yaw values), then copies/symlinks are made highBoxesCaseDirList = [self.caseDirList[c] for c in self.allHighBoxCases.case.values] @@ -549,12 +548,10 @@ def high_res_bts(self): dirpath = self.getHRTurbSimPath(cond, case, seed) for t in range(self.nTurbines): files.append(f'{dirpath}/HighT{t+1}.bts') - return files @property def high_res_log(self): - highBoxesCaseDirList = [self.caseDirList[c] for c in self.allHighBoxCases.case.values] highBoxesCaseIndex = [self.caseDirList.index(c) for c in highBoxesCaseDirList] @@ -2168,9 +2165,10 @@ def TS_low_batch_run(self, showOutputs=True, showCommand=True, verbose=True, ** - def TS_low_slurm_prepare(self, slurmfilepath, inplace=True, useSed=False, tsbin=None): + def TS_low_slurm_prepare(self, slurmfilepath, tsbin=None): if tsbin is not None: + WARN(f'Overwritting the TurbSim binary from the previously set {self.tsbin} to {tsbin}.') self.tsbin = tsbin self._checkTSBinary() @@ -2276,22 +2274,17 @@ def TS_low_createSymlinks(self): turbSimPath = self.getHRTurbSimPath(cond, case, seed) dst = os.path.join(turbSimPath, 'Low.bts') src = os.path.join(condSeedPath, 'Low.bts') - if not os.path.exists(src): - raise FFException(f'BTS file not existing: {src}\nTurbSim must be run on the low-res input files first.') - if self._can_create_symlinks: - # --- Unix based - # We create a symlink at - # dst = path/cond/case/seed/DISCON.in - # pointing to : - # src = '../../Seed_0/Low.bts' # We use relative path to help if the whole path directory is moved - src = os.path.join( os.path.relpath(condSeedPath, turbSimPath), 'Low.bts') - try: - os.symlink(src, dst) - except FileExistsError: - print(f' File {dst} already exists. Skipping symlink.') - else: - # --- Windows - self._copy(src, dst) + if not os.path.exists(src) and not self.skipchecks: + raise FFException(f'BTS file does not exis: {src}\nTurbSim must be run on the low-res input files first.') + + # --- Unix based + # We create a symlink at + # dst = path/cond/case/seed/Low.bts + # pointing to : + # src = '../../Seed_0/Low.bts' # We use relative path to help if the whole path directory is moved + src = os.path.join( os.path.relpath(condSeedPath, turbSimPath), 'Low.bts') + self._symlink(src, dst) + def getDomainParameters(self): @@ -2318,12 +2311,12 @@ def getDomainParameters(self): 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.yoffset_turbsOrigin2TSOrigin = -( (self.TSlowbox[0].ymax - self.TSlowbox[0].ymin)/2 + self.TSlowbox[0].ymin ) self.xoffset_turbsOrigin2TSOrigin = -self.extent_low[0]*self.D if self.verbose>0: - print(f" The x offset between the turbine ref frame and turbsim is {self.xoffset_turbsOrigin2TSOrigin}") - print(f" The y offset between the turbine ref frame and turbsim is {self.yoffset_turbsOrigin2TSOrigin}") + INFO(f" The x offset between the turbine ref frame and turbsim is {self.xoffset_turbsOrigin2TSOrigin}") + INFO(f" The y offset between the turbine ref frame and turbsim is {self.yoffset_turbsOrigin2TSOrigin}") def TS_high_get_time_series(self): @@ -2731,27 +2724,30 @@ def FF_setup(self, outlistFF=None, **kwargs): self._FF_setup_LES(**kwargs) elif self.inflowStr == 'TurbSim': - all_bts = self.high_res_bts - - # Check if the high-res boxes from TurbSim are present and non-zero size - for bts in all_bts: - if not os.path.isfile(bts): - raise FFException(f'File missing: {bts}\nAll TurbSim boxes need to be completed before this step can be done.') - if os.path.getsize(bts)==0: - raise FFException(f'File has zero size: {bts}\n All TurbSim boxes need to be completed before this step can be done.') - - # Now check if the boxes have been executed successfully. Let's check the last line of the logfile. - for f in self.high_res_log: - last = None - with open(f) as file: - for last in (line for line in file if line.rstrip('\n')): pass - if last is None or 'TurbSim terminated normally' not in last: - raise FFException(f'TurbSim not successful: {f}.') - + if not self.skipchecks: + self.check_turbsim_success(self.low_res_bts, self.low_res_log) + self.check_turbsim_success(self.high_res_bts, self.high_res_log) self._FF_setup_TS(**kwargs) + def check_turbsim_success(self, btsfiles, logfiles): + for bts in btsfiles: + if not os.path.isfile(bts): + raise FFException(f'File missing: {bts}\nAll TurbSim boxes need to be completed before this step can be done.') + if os.path.getsize(bts)==0: + raise FFException(f'File has zero size: {bts}\n All TurbSim boxes need to be completed before this step can be done.') + + for f in logfiles: + last = None + with open(f) as file: + for last in (line for line in file if line.rstrip('\n')): pass + if last is None or 'TurbSim terminated normally' not in last: + raise FFException(f'TurbSim not successful: {f}.') + + return True + + def _FF_setup_LES(self, seedsToKeep=1): self.seedsToKeep = seedsToKeep @@ -3013,6 +3009,7 @@ 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() _, meanU_Low = lowbts.midValues() From 6bc54ba3c2d367d4db2714bc509158753a430bbc Mon Sep 17 00:00:00 2001 From: Regis Thedin Date: Mon, 9 Feb 2026 13:09:03 -0700 Subject: [PATCH 12/15] Fix FF tests given change in signature --- .../fastfarm/examples/Ex1_FASTFarm_discretization.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openfast_toolbox/fastfarm/examples/Ex1_FASTFarm_discretization.py b/openfast_toolbox/fastfarm/examples/Ex1_FASTFarm_discretization.py index 228a461..e33cd40 100644 --- a/openfast_toolbox/fastfarm/examples/Ex1_FASTFarm_discretization.py +++ b/openfast_toolbox/fastfarm/examples/Ex1_FASTFarm_discretization.py @@ -136,7 +136,7 @@ def main(test=False): # --- 1.2 Getting the default resolution and plotting the layout # -------------------------------------------------------------------------------- # Below we provide the minimal set of arguments needed to compute the resolution automatically. - ffcase = FFCaseCreation(wts=wts, vhub=vhub, + ffcase = FFCaseCreation(path=path, wts=wts, vhub=vhub, mod_wake=mod_wake, inflowType=inflowType) @@ -151,7 +151,7 @@ def main(test=False): dt_high = 0.50 # [s] dt_low = 2.00 # [s] ds_low = 25 # [m] - ffcase2 = FFCaseCreation(wts=wts, vhub=vhub, + ffcase2 = FFCaseCreation(path=path, wts=wts, vhub=vhub, dt_high=dt_high, dt_low=dt_low, ds_low=ds_low, mod_wake=mod_wake, From 8e9cb8814b2f004168dce00efee065928f96602c Mon Sep 17 00:00:00 2001 From: Regis Thedin Date: Tue, 10 Feb 2026 10:42:46 -0700 Subject: [PATCH 13/15] FF: clean-up, remove defaults from constructor --- .../fastfarm/FASTFarmCaseCreation.py | 80 +++++++++---------- 1 file changed, 36 insertions(+), 44 deletions(-) diff --git a/openfast_toolbox/fastfarm/FASTFarmCaseCreation.py b/openfast_toolbox/fastfarm/FASTFarmCaseCreation.py index c860d7e..7c6fa5c 100644 --- a/openfast_toolbox/fastfarm/FASTFarmCaseCreation.py +++ b/openfast_toolbox/fastfarm/FASTFarmCaseCreation.py @@ -38,6 +38,7 @@ def safe_cd(newdir): os.chdir(prevdir) def check_files_exist(*args): + # TODO: this is only called during the turbsim example to check if files exist. consider deleting it import os b = [] if len(args)>1: @@ -165,11 +166,11 @@ def load(fullpath, dill_filename='ffcase_obj.dill'): class FFCaseCreation: def __init__(self, - path=None, - wts=None, + path, + wts, tmax=None, zbot=1, - vhub=None, + vhub=[8], shear=None, TIvalue=None, inflow_deg=None, @@ -341,16 +342,14 @@ def __init__(self, if self.verbose>0: print(f'Setting rotor parameters... Done.') - # - # TODO TODO TODO - # Creating Cases and Conditions should have its own function interface so the user can call + # TODO: Creating Cases and Conditions should have its own function interface so the user can call 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.path is not None: - # TODO TODO, this should only be done when user ask for input file creation + # TODO this should only be done when user ask for input file creation 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.') @@ -588,10 +587,10 @@ def _checkInputs(self): # --- Default arguments if self.vhub is None: self.vhub = [8] - if self.inflow_deg is None: - self.inflow_deg = [0]*len(self.vhub) if self.TIvalue is None: self.TIvalue = [10]*len(self.vhub) + if self.inflow_deg is None: + self.inflow_deg = [0]*len(self.vhub) if self.shear is None: self.shear = [0]*len(self.vhub) if self.tmax is None: @@ -601,7 +600,6 @@ def _checkInputs(self): if self.skipchecks: WARN('Skipping checks on TurbSim files and symlinks. This should only be used for testing purposes.') - # 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, ', @@ -792,7 +790,7 @@ def _determine_resolutions_from_dummy_les_grid(self): from openfast_toolbox.fastfarm.AMRWindSimulation import AMRWindSimulation # Create values and keep variable names consistent across interfaces - dummy_dt = 0.1 # TODO TODO TODO determine it based on fmax + dummy_dt = 0.1 # TODO determine it based on fmax 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) @@ -816,12 +814,12 @@ def _determine_resolutions_from_dummy_les_grid(self): print_bold(f' High-resolution: ds_high: {amr.ds_high_les} m, dt_high: {amr.dt_high_les} s') print_bold(f' Low-resolution: ds_low : {amr.ds_low_les} m, dt_low: {amr.dt_low_les} s') INFO (f'If the above values are too fine or manual tuning is warranted, specify them manually.') - print(f' To do that, specify the values directly to `FFCaseCreation`, e.g.:') - print(f' ', end='') - print(f'`dt_high = {2*amr.dt_high_les}`; ', end='') - print(f'`ds_high = {2*amr.ds_high_les}`; ', end='') - print(f'`dt_low = {2*amr.dt_low_les}`; ', end='') - print(f'`ds_low = {2*amr.ds_low_les}`; ') + INFO (f' To do that, specify the values directly to `FFCaseCreation`, e.g.:') + INFO(f' ', end='') + INFO(f'`dt_high = {2*amr.dt_high_les}`; ', end='') + INFO(f'`ds_high = {2*amr.ds_high_les}`; ', end='') + INFO(f'`dt_low = {2*amr.dt_low_les}`; ', end='') + INFO(f'`ds_low = {2*amr.ds_low_les}`; ') self.dt_high = amr.dt_high_les self.ds_high = amr.ds_high_les @@ -1112,7 +1110,7 @@ def copyTurbineFilesForEachCase(self, writeFiles=True): if 'Skew_Mod' in self.AeroDynFile.keys(): self.AeroDynFile['Skew_Mod'] = 1 self.AeroDynFile['SkewMomCorr'] = True - #self.AeroDynFile['BEM_Mod'] = 2 # TODO let the user decide. Commented out by Emmanuel + self.AeroDynFile['BEM_Mod'] = 2 self.AeroDynFile['IntegrationMethod'] = 4 # 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])}"' @@ -1418,7 +1416,7 @@ def setTemplateFilename(self, templatePath=None, templateFiles=None, templateFST verbose=self.verbose # Set default values - #TODO TODO TODO Replace this by a dictionary so that we can for loop over it more easily + #TODO Replace this by a dictionary so that we can for loop over it more easily self.EDfilename = "unused"; self.EDfilepath = "unused" self.SEDfilename = "unused"; self.SEDfilepath = "unused" self.HDfilename = "unused"; self.HDfilepath = "unused" @@ -1440,8 +1438,8 @@ def setTemplateFilename(self, templatePath=None, templateFiles=None, templateFST self.libdisconfilepath = "unused" self.coeffTablefilename = "unused" self.hydroDatapath = "unused" - self.turbsimLowfilepath = "unused" # TODO Convention unclear - self.turbsimHighfilepath = "unused" # TODO Convention unclear + self.turbsimLowfilepath = "unused" + self.turbsimHighfilepath = "unused" self.FFfilename = "unused"; self.FFfilepath = "unused" @@ -1460,7 +1458,7 @@ def setTemplateFilename(self, templatePath=None, templateFiles=None, templateFST templateFiles = templateFiles.copy() # we create a copy to avoid changing the user input # Join templatePath to most templateFiles - # TODO TODO, not all templateFiles have the same convention, this needs to be changed. + # TODO not all templateFiles have the same convention, this needs to be changed. if templatePath is not None: if not os.path.isdir(templatePath): if os.path.isabs(templatePath): @@ -1468,15 +1466,17 @@ def setTemplateFilename(self, templatePath=None, templateFiles=None, templateFST else: raise ValueError(f'Relative template path {templatePath} does not seem to exist. Full path is: {os.path.abspath(templatePath)}.') for key, value in templateFiles.items(): - if key in ['turbsimLowfilepath', 'turbsimHighfilepath', 'libdisconfilepath']: - # We skip those keys because there convention is not clear - WARN(f'Not adding templatePath to key `{key}`.\nImplementation and behavrio might change in a future release.') - #INFO(f'Template {key:23s}={templateFiles[key]}') - continue if value == 'unused' or value is None: continue - templateFiles[key] = os.path.join(templatePath, f"{value}").replace('\\','/') - #INFO(f'Template {key:23s}={templateFiles[key]}') + if key in ['turbsimLowfilepath', 'turbsimHighfilepath', 'libdisconfilepath']: + # We skip those keys because their convention is not clear + #WARN(f'Not adding templatePath to key `{key}`.\nImplementation and behavior might change in a future release.') + #INFO(f'Template {key:24s}={templateFiles[key]}') + templateFiles[key] = f"{value}".replace('\\','/') + #continue + else: + templateFiles[key] = os.path.join(templatePath, f"{value}").replace('\\','/') + INFO(f'Template {key:24s}={templateFiles[key]}') # --- The user provided a FSTF file from which we override the templateFiles if templateFSTF is not None: @@ -1508,11 +1508,10 @@ def setTemplateFilename(self, templatePath=None, templateFiles=None, templateFST if '.T.' in filebase: filebase = fread[key_deck].rsplit('.T.', 1)[0]+'.T' templateFiles[key_tpl] = filebase.replace('\\','/') - #INFO(f'Template {key_tpl:23s}={filebase}') + #INFO(f'Template {key_tpl:24s}={filebase}') - # TODO In theory, we should need the templatePath beyond this point. templatePath = os.path.dirname(templateFiles['FFfilename']) - self.templatePathabs = os.path.abspath(templatePath).replace('\\','/') # TODO, in theory, we shouldn't need to store that. + self.templatePathabs = os.path.abspath(templatePath).replace('\\','/') # -------------------------------------------------------------------------------- # NOTE: BEYOND THIS POINT THE VALUES OF TEMPLATE FILES ARE EITHER: @@ -1725,14 +1724,14 @@ def checkIfExists(f): #self._create_copy_libdiscon() self.hasController = True - # --- TODO TODO TODO not clean convention + # --- TODO not clean convention elif key == 'turbsimLowfilepath': if not value.lower().endswith('.inp'): raise ValueError(f'TurbSim file input for low-res box should end in ".inp".') self.turbsimLowfilepath = value checkIfExists(self.turbsimLowfilepath) - # --- TODO TODO TODO not clean convention + # --- TODO not clean convention elif key == 'turbsimHighfilepath': if not value.lower().endswith('.inp'): raise ValueError(f'TurbSim file input for high-res box should end in ".inp".') @@ -2411,7 +2410,7 @@ def TS_high_setup(self, writeFiles=True): self.check_turbsim_success(self.low_res_bts, self.low_res_log) # Create symbolic links for the low-res boxes - # TODO TODO TODO Simply store address of files + # TODO Simply store address of files self.TS_low_createSymlinks() if not self.skipchecks: @@ -2644,12 +2643,7 @@ def TS_high_create_symlink(self): for seed in range(self.nSeeds): src = os.path.join(self.getHRTurbSimPath(cond, src_case, seed), f'HighT{t+1}.bts') dst = os.path.join(self.getHRTurbSimPath(cond, case , seed), f'HighT{t+1}.bts') - #print(f'src is {src}') - #print(f'dst is {dst}') - #src = os.path.join('..', '..', '..', '..', self.condDirList[cond], self.caseDirList[src_case], f'Seed_{seed}', 'TurbSim', f'HighT{t+1}.bts') - #print('Emmanuel Says: TODO Check the line below') src = os.path.relpath(src, os.path.dirname(dst)) - #print(f'rel src{src}\n') self._symlink(src, dst) @@ -2925,12 +2919,11 @@ def _FF_setup_TS(self): d = None else: # 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')) # TODO TODO TODO Get Path + lowbts = TurbSimFile(os.path.join(seedPath,'TurbSim', 'Low.bts')) # TODO Get Path 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 d = self._getBoxesParamsForFF(lowbts, highbts, self.dt_low, D_, HubHt_, xWT, yt) - self.dtemp = d #todo remove # Write the file if self.flat: @@ -3399,7 +3392,7 @@ def plot(self, figsize=(14,7), fontsize=13, saveFig=False, returnFig=False, figF [dst.y.values-(dst.D.values/2)*cosd(yaw+phi), dst.y.values+(dst.D.values/2)*cosd(yaw+phi)], c=color, alpha=alphas[j]) - # TODO TODO Plot high-res grid + # TODO Plot high-res grid #x_high = X0_High[wt] + np.arange(nX_High+1)*dX_High #y_high = Y0_High[wt] + np.arange(nY_High+1)*dY_High #z_high = Z0_High[wt] + np.arange(nZ_High+1)*dZ_High @@ -3447,7 +3440,6 @@ def plot(self, figsize=(14,7), fontsize=13, saveFig=False, returnFig=False, figF if __name__ == '__main__': - from welib.essentials import * # ----------------------------------------------------------------------------- # --------------------------- Farm parameters --------------------------------- # ----------------------------------------------------------------------------- From 6e02fe04ea32518e2db3c19efb655e5ac8773f81 Mon Sep 17 00:00:00 2001 From: Regis Thedin Date: Tue, 10 Feb 2026 10:44:07 -0700 Subject: [PATCH 14/15] FF: remove `sed` from SLURM script creation; small fixes --- .../fastfarm/FASTFarmCaseCreation.py | 232 +++++++----------- .../examples/Ex2a_FASTFarm_TurbSim_driven.py | 10 +- openfast_toolbox/fastfarm/fastfarm.py | 2 +- 3 files changed, 96 insertions(+), 148 deletions(-) diff --git a/openfast_toolbox/fastfarm/FASTFarmCaseCreation.py b/openfast_toolbox/fastfarm/FASTFarmCaseCreation.py index 7c6fa5c..35e8f88 100644 --- a/openfast_toolbox/fastfarm/FASTFarmCaseCreation.py +++ b/openfast_toolbox/fastfarm/FASTFarmCaseCreation.py @@ -947,6 +947,9 @@ def copyTurbineFilesForEachCase(self, writeFiles=True): if not self.templateFilesCreatedBool: raise SyntaxError('Template files not set. Call `setTemplateFilename` before calling this function.') + # Create copies of the controller + self._create_copy_libdiscon() + # Loops on all conditions/cases creating DISCON and *Dyn files for cond in range(self.nConditions): if self.verbose>0: print(f'Processing condition {self.condDirList[cond]}') @@ -2184,65 +2187,37 @@ def TS_low_slurm_prepare(self, slurmfilepath, tsbin=None): if self.nSeeds > 6: WARN(f'The memory-per-cpu on the low-res boxes SLURM script might be too low given {self.nSeeds} seeds.') - if useSed: - self.slurmfilename_low = os.path.basename(slurmfilepath) - shutil.copy2(slurmfilepath, os.path.join(self.path, self.slurmfilename_low)) - # Change job name (for convenience only) - sed_command = f"sed -i 's|^#SBATCH --job-name=lowBox|#SBATCH --job-name=lowBox_{os.path.basename(self.path)}|g' {self.slurmfilename_low}" - self.sed_inplace(sed_command, inplace) - # Change logfile name (for convenience only) - sed_command = f"sed -i 's|#SBATCH --output log.lowBox|#SBATCH --output log.turbsim_low|g' {self.slurmfilename_low}" - self.sed_inplace(sed_command, inplace) - # Change memory per cpu - sed_command = f"sed -i 's|--mem-per-cpu=25000M|--mem-per-cpu={memory_per_cpu}M|g' {self.slurmfilename_low}" - self.sed_inplace(sed_command, inplace) - # Change number of nodes values - sed_command = f"sed -i 's|^#SBATCH --nodes.*|#SBATCH --nodes={int(np.ceil(self.nConditions*self.nSeeds/6))}|g' {self.slurmfilename_low}" - self.sed_inplace(sed_command, inplace) - # Change the fastfarm binary to be called - sed_command = f"""sed -i "s|^turbsimbin.*|turbsimbin='{self.tsbin}'|g" {self.slurmfilename_low}""" - self.sed_inplace(sed_command, inplace) - # Change the path inside the script to the desired one - sed_command = f"""sed -i "s|^basepath.*|basepath='{self.path}'|g" {self.slurmfilename_low}""" - self.sed_inplace(sed_command, inplace) - # Assemble list of conditions and write it - listtoprint = "' '".join(self.condDirList) - sed_command = f"""sed -i "s|^condList.*|condList=('{listtoprint}')|g" {self.slurmfilename_low}""" - self.sed_inplace(sed_command, inplace) - # Change the number of seeds - sed_command = f"sed -i 's|^nSeeds.*|nSeeds={self.nSeeds}|g' {self.slurmfilename_low}" - self.sed_inplace(sed_command, inplace) - else: - self.slurmfilename_low = os.path.join(self.path, os.path.basename(slurmfilepath)) - shutil.copy2(slurmfilepath, self.slurmfilename_low) - - # Python version - with open(self.slurmfilename_low, "r") as f: - lines = f.read() - - # Replacements - jobname = f"#SBATCH --job-name=lowBox_{os.path.basename(self.path)}" - logfile = "#SBATCH --output log.turbsim_low" - memcpu = f"--mem-per-cpu={memory_per_cpu}M" - nodes = f"#SBATCH --nodes={int(np.ceil(self.nConditions * self.nSeeds / 6))}" - turbsim = f"turbsimbin='{self.tsbin}'" - basepath = f"basepath='{self.path}'" - condlist = "condList=('{}')".format("' '".join(self.condDirList)) - seeds = f"nSeeds={self.nSeeds}" - - # Apply substitutions - import re - lines = re.sub(r"^#SBATCH --job-name=.*", jobname, lines, flags=re.M) - lines = re.sub(r"^#SBATCH --output .*", logfile, lines, flags=re.M) - lines = re.sub(r"--mem-per-cpu=\d+M", memcpu, lines) - lines = re.sub(r"^#SBATCH --nodes=.*", nodes, lines, flags=re.M) - lines = re.sub(r"^turbsimbin=.*", turbsim, lines, flags=re.M) - lines = re.sub(r"^basepath=.*", basepath,lines, flags=re.M) - lines = re.sub(r"^condList=.*", condlist,lines, flags=re.M) - lines = re.sub(r"^nSeeds=.*", seeds, lines, flags=re.M) - - with open(self.slurmfilename_low, "w") as f: - f.write(lines) + self.slurmfilename_low = os.path.join(self.path, os.path.basename(slurmfilepath)) + shutil.copy2(slurmfilepath, self.slurmfilename_low) + + # Python version + with open(self.slurmfilename_low, "r") as f: + lines = f.read() + + # Replacements + jobname = f"#SBATCH --job-name=lowBox_{os.path.basename(self.path)}" + logfile = "#SBATCH --output log.turbsim_low" + memcpu = f"--mem-per-cpu={memory_per_cpu}M" + nodes = f"#SBATCH --nodes={int(np.ceil(self.nConditions * self.nSeeds / 6))}" + turbsim = f"turbsimbin='{self.tsbin}'" + basepath = f"basepath='{self.path}'" + condlist = "condList=('{}')".format("' '".join(self.condDirList)) + seeds = f"nSeeds={self.nSeeds}" + + # Apply substitutions + import re + lines = re.sub(r"^#SBATCH --job-name=.*", jobname, lines, flags=re.M) + lines = re.sub(r"^#SBATCH --output .*", logfile, lines, flags=re.M) + lines = re.sub(r"--mem-per-cpu=\d+M", memcpu, lines) + lines = re.sub(r"^#SBATCH --nodes=.*", nodes, lines, flags=re.M) + lines = re.sub(r"^turbsimbin=.*", turbsim, lines, flags=re.M) + lines = re.sub(r"^basepath=.*", basepath,lines, flags=re.M) + lines = re.sub(r"^condList=.*", condlist,lines, flags=re.M) + lines = re.sub(r"^nSeeds=.*", seeds, lines, flags=re.M) + + with open(self.slurmfilename_low, "w") as f: + f.write(lines) + INFO(f'File written: {self.slurmfilename_low}') @@ -2507,7 +2482,7 @@ def TS_high_batch_run(self, showOutputs=True, showCommand=True, verbose=True, ** raise FFException(f'Batch file failed: {self.batchfile_high}') - def TS_high_slurm_prepare(self, slurmfilepath, inplace=True, useSed=False): + def TS_high_slurm_prepare(self, slurmfilepath): # --------------------------------------------------- # ----- Prepare SLURM script for High-res boxes ----- # --------------------------------------------------- @@ -2516,75 +2491,40 @@ def TS_high_slurm_prepare(self, slurmfilepath, inplace=True, useSed=False): raise ValueError (f'SLURM script for high-res box {slurmfilepath} does not exist.') ntasks = self.nConditions*self.nHighBoxCases*self.nSeeds*self.nTurbines - - if useSed: - self.slurmfilename_high = os.path.basename(slurmfilepath) - shutil.copy2(slurmfilepath, os.path.join(self.path, self.slurmfilename_high)) - - # Change job name (for convenience only) - sed_command = f"sed -i 's|^#SBATCH --job-name.*|#SBATCH --job-name=highBox_{os.path.basename(self.path)}|g' {self.slurmfilename_high}" - self.sed_inplace(sed_command, inplace) - # Change logfile name (for convenience only) - sed_command = f"sed -i 's|#SBATCH --output log.highBox|#SBATCH --output log.turbsim_high|g' {self.slurmfilename_high}" - self.sed_inplace(sed_command, inplace) - # Change number of nodes values - sed_command = f"sed -i 's|^#SBATCH --nodes.*|#SBATCH --nodes={int(np.ceil(ntasks/36))}|g' {self.slurmfilename_high}" - self.sed_inplace(sed_command, inplace) - # Change the fastfarm binary to be called - sed_command = f"""sed -i "s|^turbsimbin.*|turbsimbin='{self.tsbin}'|g" {self.slurmfilename_high}""" - self.sed_inplace(sed_command, inplace) - # Change the path inside the script to the desired one - sed_command = f"""sed -i "s|^basepath.*|basepath='{self.path}'|g" {self.slurmfilename_high}""" - self.sed_inplace(sed_command, inplace) - # Change number of turbines - sed_command = f"sed -i 's|^nTurbines.*|nTurbines={self.nTurbines}|g' {self.slurmfilename_high}" - self.sed_inplace(sed_command, inplace) - # Change number of seeds - sed_command = f"sed -i 's|^nSeeds.*|nSeeds={self.nSeeds}|g' {self.slurmfilename_high}" - self.sed_inplace(sed_command, inplace) - # Assemble list of conditions and write it - listtoprint = "' '".join(self.condDirList) - sed_command = f"""sed -i "s|^condList.*|condList=('{listtoprint}')|g" {self.slurmfilename_high}""" - self.sed_inplace(sed_command, inplace) - # 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}""" - self.sed_inplace(sed_command, inplace) - else: - self.slurmfilename_high = os.path.join(self.path, os.path.basename(slurmfilepath)) - shutil.copy2(slurmfilepath, self.slurmfilename_high) - - with open(self.slurmfilename_high, "r") as f: - lines = f.read() - - # Prepare replacement strings - jobname = f"#SBATCH --job-name=highBox_{os.path.basename(self.path)}" - logfile = "#SBATCH --output log.turbsim_high" - nodes = f"#SBATCH --nodes={int(np.ceil(ntasks/36))}" - turbsim = f"turbsimbin='{self.tsbin}'" - basepath = f"basepath='{self.path}'" - nTurb = f"nTurbines={self.nTurbines}" - nSeed = f"nSeeds={self.nSeeds}" - condlist = "condList=('{}')".format("' '".join(self.condDirList)) - highBoxesCaseDirList = [self.caseDirList[c] for c in self.allHighBoxCases.case.values] - caselist = "caseList=('{}')".format("' '".join(highBoxesCaseDirList)) - - # Apply substitutions - import re - lines = re.sub(r"^#SBATCH --job-name.*", jobname, lines, flags=re.M) - lines = re.sub(r"^#SBATCH --output .*", logfile, lines, flags=re.M) - lines = re.sub(r"^#SBATCH --nodes.*", nodes, lines, flags=re.M) - lines = re.sub(r"^turbsimbin.*", turbsim, lines, flags=re.M) - lines = re.sub(r"^basepath.*", basepath,lines, flags=re.M) - lines = re.sub(r"^nTurbines.*", nTurb, lines, flags=re.M) - lines = re.sub(r"^nSeeds.*", nSeed, lines, flags=re.M) - lines = re.sub(r"^condList.*", condlist,lines, flags=re.M) - lines = re.sub(r"^caseList.*", caselist,lines, flags=re.M) - - with open(self.slurmfilename_high, "w") as f: - f.write(lines) + self.slurmfilename_high = os.path.join(self.path, os.path.basename(slurmfilepath)) + shutil.copy2(slurmfilepath, self.slurmfilename_high) + + with open(self.slurmfilename_high, "r") as f: + lines = f.read() + + # Prepare replacement strings + jobname = f"#SBATCH --job-name=highBox_{os.path.basename(self.path)}" + logfile = "#SBATCH --output log.turbsim_high" + nodes = f"#SBATCH --nodes={int(np.ceil(ntasks/36))}" + turbsim = f"turbsimbin='{self.tsbin}'" + basepath = f"basepath='{self.path}'" + nTurb = f"nTurbines={self.nTurbines}" + nSeed = f"nSeeds={self.nSeeds}" + condlist = "condList=('{}')".format("' '".join(self.condDirList)) + highBoxesCaseDirList = [self.caseDirList[c] for c in self.allHighBoxCases.case.values] + caselist = "caseList=('{}')".format("' '".join(highBoxesCaseDirList)) + + # Apply substitutions + import re + lines = re.sub(r"^#SBATCH --job-name.*", jobname, lines, flags=re.M) + lines = re.sub(r"^#SBATCH --output .*", logfile, lines, flags=re.M) + lines = re.sub(r"^#SBATCH --nodes.*", nodes, lines, flags=re.M) + lines = re.sub(r"^turbsimbin.*", turbsim, lines, flags=re.M) + lines = re.sub(r"^basepath.*", basepath,lines, flags=re.M) + lines = re.sub(r"^nTurbines.*", nTurb, lines, flags=re.M) + lines = re.sub(r"^nSeeds.*", nSeed, lines, flags=re.M) + lines = re.sub(r"^condList.*", condlist,lines, flags=re.M) + lines = re.sub(r"^caseList.*", caselist,lines, flags=re.M) + with open(self.slurmfilename_high, "w") as f: + f.write(lines) + + INFO(f'File written: {self.slurmfilename_high}') def TS_high_slurm_submit(self, qos='normal', A=None, t=None, p=None, inplace=True): @@ -2694,20 +2634,28 @@ def FF_setup(self, outlistFF=None, **kwargs): # 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=10 - planes_xy = [self.zhub+self.zbot] - planes_yz = np.unique(np.round(xWT+offset, 2)) - planes_xz = np.unique(np.round(yWT, 2)) + aligned_cases = self.allCases.where(self.allCases['inflow_deg'] == 0, drop=True) + # Ensure at least one case with inflow_deg == 0 exists before selecting + if 'case' not in aligned_cases.dims or aligned_cases.sizes.get('case', 0) == 0: + WARN("No case with inflow_deg == 0 found; unable to set aligned plane for sampling.") + planes_xy = [self.zhub+self.zbot] + planes_yz = [0] + planes_xz = [0] + else: + alignedTurbs = aligned_cases.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=10 + planes_xy = [self.zhub+self.zbot] + planes_yz = np.unique(np.round(xWT+offset, 2)) + planes_xz = np.unique(np.round(yWT, 2)) # Number of planes must be at most 9 self.planes_xy = planes_xy[0:9] diff --git a/openfast_toolbox/fastfarm/examples/Ex2a_FASTFarm_TurbSim_driven.py b/openfast_toolbox/fastfarm/examples/Ex2a_FASTFarm_TurbSim_driven.py index b100394..dd280c1 100644 --- a/openfast_toolbox/fastfarm/examples/Ex2a_FASTFarm_TurbSim_driven.py +++ b/openfast_toolbox/fastfarm/examples/Ex2a_FASTFarm_TurbSim_driven.py @@ -46,8 +46,8 @@ from openfast_toolbox.fastfarm.FASTFarmCaseCreation import FFCaseCreation -from openfast_toolbox.fastfarm.FASTFarmCaseCreation import check_files_exist, check_discon_library # Helper functions -from openfast_toolbox.fastfarm.fastfarm import plotFastFarmSetup # Low level FAST.Farm functions +from openfast_toolbox.fastfarm.FASTFarmCaseCreation import check_files_exist, check_discon_library, modifyProperty +from openfast_toolbox.fastfarm.fastfarm import plotFastFarmSetup scriptDir = os.path.dirname(__file__) @@ -177,11 +177,11 @@ def main(test=False): # END OF USER INPUT # ----------------------------------------------------------------------------- - # Plot turbines locations + # Plot turbines locations fig = plotFastFarmSetup(wts) if not test: - check_files_exist(ffbin, tsbin, templateFSTF, templateFiles) - check_discon_library(libdisconfilepath); + check_files_exist(ffbin, tsbin, templateFSTF, templateFiles) + check_discon_library(libdiscon) # ----------------------------------------------------------------------------- # -------------------- FAST.Farm initial setup -------------------------------- diff --git a/openfast_toolbox/fastfarm/fastfarm.py b/openfast_toolbox/fastfarm/fastfarm.py index ffc3175..422f190 100644 --- a/openfast_toolbox/fastfarm/fastfarm.py +++ b/openfast_toolbox/fastfarm/fastfarm.py @@ -353,7 +353,7 @@ def writeFastFarm(outputFile, templateFile, xWT, yWT, zWT, FFTS=None, OutListT1= if isinstance(FFTS[k],int): fst[k] = FFTS[k] else: - fst[k] = np.around(FFTS[k],3) + fst[k] = np.around(FFTS[k],5) fst['WrDisDT'] = FFTS['DT_Low'] # --- Set turbine names, position, and box extent From a37d7cb8999e2b6c218197aae8c6871292066d82 Mon Sep 17 00:00:00 2001 From: Regis Thedin Date: Tue, 10 Feb 2026 10:44:43 -0700 Subject: [PATCH 15/15] Add `dill` to required packages --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index 10d01dd..599779d 100644 --- a/setup.py +++ b/setup.py @@ -40,6 +40,7 @@ "chardet", "xarray", # for netcdf files "pytest", + "dill", ], test_suite="pytest", tests_require=["pytest"],