From 40131a994ffe82a8ffa987a6b4a22a82a9ea2f16 Mon Sep 17 00:00:00 2001 From: Regis Thedin Date: Wed, 28 Jan 2026 16:31:20 -0700 Subject: [PATCH 1/5] 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 2/5] 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 3/5] 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 4/5] 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 5/5] 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)