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/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 diff --git a/openfast_toolbox/fastfarm/FASTFarmCaseCreation.py b/openfast_toolbox/fastfarm/FASTFarmCaseCreation.py index 829f643..c860d7e 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) @@ -80,6 +75,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') @@ -193,10 +189,10 @@ def __init__(self, seedValues = None, inflowPath = None, inflowType = None, - sweepYawMisalignment = False, 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. @@ -261,13 +257,15 @@ 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. 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) + 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 @@ -297,12 +295,12 @@ 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 self.verbose = verbose self.attempt = 1 + self.skipchecks = skipchecks self.flat = flat # Set aux variable self.templateFilesCreatedBool = False @@ -318,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 = '' @@ -344,13 +343,13 @@ 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.') - 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() @@ -358,7 +357,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 +403,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" @@ -499,6 +498,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): @@ -534,24 +536,58 @@ 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 + @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): - #### 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: @@ -560,6 +596,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 @@ -656,8 +696,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 +768,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 +787,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 +822,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,36 +853,29 @@ 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 - misalignment_ = self.allCases['misalignment' ].sel(case=case).values nADyn_ = self.allCases['nFullAeroDyn' ].sel(case=case).values nFED_ = self.allCases['nFulllElastoDyn'].sel(case=case).values yawCase_ = self.allCases['yawCase' ].sel(case=case).values - # Set current path name string. The case is of the following form: Case00_wdirp10_WSfalse_YMfalse_12fED_12ADyn + # 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.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): @@ -867,41 +900,46 @@ 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 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) and not self.skipchecks: 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: - try: - shutil.copy2(src, dst) - except FileExistsError: - if debug: - raise Exception(error) - error = dst + # Windows + WARN('Windows detected: creating a copies instead of symlinks.') + error = self._copy(src, dst, debug=debug) return error @@ -1018,7 +1056,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. @@ -1029,17 +1067,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 @@ -1047,7 +1078,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(): @@ -1070,7 +1101,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')) @@ -1095,7 +1126,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}"' @@ -1213,9 +1244,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.') @@ -1318,7 +1348,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. @@ -1328,9 +1359,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 -------------------------------------------------- @@ -1344,7 +1375,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', @@ -1366,16 +1397,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 @@ -1425,7 +1463,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 @@ -1482,8 +1523,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. '\ @@ -1648,29 +1690,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']: @@ -1681,19 +1722,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) @@ -1738,12 +1779,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): @@ -1779,7 +1821,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 @@ -1789,8 +1831,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 ), @@ -1801,8 +1843,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))) @@ -1817,11 +1859,9 @@ def _create_all_cond(self): def _create_all_cases(self): - # Generate the different "cases" (inflow angle and yaw misalignment bools). - # If misalignment true, then the actual yaw is yaw[turb]=np.random.uniform(low=-8.0, high=8.0). + # Generate the different "cases" (inflow angle). # Set sweep bools and multipliers - nCasesYMmultiplier = 2 if self.sweepYM else 1 nCasesROmultiplier = len(self.EDmodel) if len(self.ADmodel) == 1: self.sweepEDmodel = False @@ -1902,26 +1942,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']) @@ -2020,42 +2042,13 @@ 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.') - # Loops on all conditions/seeds creating Low-res TurbSim box (following openfast_toolbox/openfast_toolbox/fastfarm/examples/Ex1_TurbSimInputSetup.py) boxType='lowres' lowFilesName = [] + self.TSlowbox = [] for cond in range(self.nConditions): for seed in range(self.nSeeds): seedPath = self.getCondSeedPath(cond, seed) @@ -2079,10 +2072,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 @@ -2090,7 +2085,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) @@ -2137,19 +2132,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}") @@ -2167,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() @@ -2267,7 +2266,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): @@ -2275,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): @@ -2299,13 +2293,12 @@ 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, + # 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: @@ -2318,16 +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}") - - if self.verbose>2: - print(f'allHighBoxCases is:') - print(self.allHighBoxCases) + 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): @@ -2415,17 +2404,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 @@ -2454,11 +2449,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) @@ -2493,8 +2489,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}") @@ -2611,7 +2608,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') @@ -2621,16 +2618,16 @@ 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 - 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'] @@ -2638,8 +2635,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 @@ -2647,9 +2644,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) @@ -2724,30 +2724,28 @@ def FF_setup(self, outlistFF=None, **kwargs): self._FF_setup_LES(**kwargs) elif self.inflowStr == 'TurbSim': - all_bts = self.high_res_bts - - 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.') - - # --- 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.') + 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) - #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.') - 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): @@ -2774,9 +2772,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) @@ -2923,12 +2921,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: @@ -3007,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() @@ -3142,7 +3145,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: @@ -3181,8 +3187,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): @@ -3289,7 +3294,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)): @@ -3300,7 +3305,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)): @@ -3334,7 +3339,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) diff --git a/openfast_toolbox/fastfarm/examples/Ex1_FASTFarm_discretization.py b/openfast_toolbox/fastfarm/examples/Ex1_FASTFarm_discretization.py index 68df741..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, @@ -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; 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