diff --git a/data/.gitignore b/data/.gitignore new file mode 100644 index 0000000..a915a64 --- /dev/null +++ b/data/.gitignore @@ -0,0 +1,2 @@ +*.lin +*.sum.yaml diff --git a/openfast_toolbox/__init__.py b/openfast_toolbox/__init__.py index 511afef..59b54b2 100644 --- a/openfast_toolbox/__init__.py +++ b/openfast_toolbox/__init__.py @@ -8,7 +8,10 @@ from .io.fast_input_deck import FASTInputDeck # Add version to package -with open(os.path.join(os.path.dirname(__file__), "..", "VERSION")) as fid: - __version__ = fid.read().strip() +try: + with open(os.path.join(os.path.dirname(__file__), "..", "VERSION")) as fid: + __version__ = fid.read().strip() +except: + __version__='v0.0.0-Unknown' diff --git a/openfast_toolbox/case_generation/runner.py b/openfast_toolbox/case_generation/runner.py index df7ed10..914abad 100644 --- a/openfast_toolbox/case_generation/runner.py +++ b/openfast_toolbox/case_generation/runner.py @@ -1,9 +1,11 @@ # --- For cmd.py import os +import sys import subprocess import multiprocessing import collections +from contextlib import contextmanager import glob import pandas as pd import numpy as np @@ -14,9 +16,19 @@ # --- Fast libraries from openfast_toolbox.io.fast_input_file import FASTInputFile from openfast_toolbox.io.fast_output_file import FASTOutputFile +from openfast_toolbox.tools.strings import FAIL, OK FAST_EXE='openfast' +@contextmanager +def safe_cd(newdir): + prevdir = os.getcwd() + try: + os.chdir(newdir) + yield + finally: + os.chdir(prevdir) + # --------------------------------------------------------------------------------} # --- Tools for executing FAST # --------------------------------------------------------------------------------{ @@ -72,10 +84,10 @@ def _report(p): # --- Giving a summary if len(Failed)==0: if verbose: - print('[ OK ] All simulations run successfully.') + OK('All simulations run successfully.') return True, Failed else: - print('[FAIL] {}/{} simulations failed:'.format(len(Failed),len(inputfiles))) + FAIL('{}/{} simulations failed:'.format(len(Failed),len(inputfiles))) for p in Failed: print(' ',p.input_file) return False, Failed @@ -113,34 +125,178 @@ class Dummy(): p.exe = exe return p -def runBatch(batchfiles, showOutputs=True, showCommand=True, verbose=True): +def in_jupyter(): + try: + from IPython import get_ipython + return 'ipykernel' in str(type(get_ipython())) + except: + return False + +def stream_output(std, buffer_lines=5, prefix='|', line_count=True): + if in_jupyter(): + from IPython.display import display, update_display + # --- Jupyter mode --- + #handles = [display("DUMMY LINE FOR BUFFER", display_id=True) for _ in range(buffer_lines)] + #buffer = [] + #for line in std: + # line = line.rstrip() + # buffer.append(line) + # if len(buffer) > buffer_lines: + # buffer.pop(0) + # # update all display slots + # for i, handle in enumerate(handles): + # text = buffer[i] if i < len(buffer) else "" + # update_display(text, display_id=handle.display_id) + + # --- alternative using HTML + from IPython.display import display, update_display, HTML + import html as _html + + # --- Jupyter mode with HTML --- + handles = [display(HTML("
{}DUMMY LINE FOR BUFFER
".format(prefix)), display_id=True) + for _ in range(buffer_lines)] + buffer = [] + iLine=0 + for line in std: + iLine+=1 + line = line.rstrip("\r\n") + if line_count: + line = f"{iLine:>5}: {line}" + line = f"{prefix}{line}" + buffer.append(line) + if len(buffer) > buffer_lines: + buffer.pop(0) + # update all display slots + for i, handle in enumerate(handles): + text = buffer[i] if i < len(buffer) else "" + + html_text = "
{}
".format(_html.escape(text) if text else " ") + update_display(HTML(html_text), display_id=handle.display_id) + + else: + import shutil + + term_width = shutil.get_terminal_size((80, 20)).columns + for _ in range(buffer_lines): + print('DummyLine') + # --- Terminal mode --- + buffer = [] + iLine = 0 + for line in std: + iLine += 1 + line = line.rstrip() + line = line.rstrip() + if line_count: + line = f"{iLine:>5}: {line}" + line = f"{prefix}{line}" + line = line[:term_width] # truncate to fit in one line + buffer.append(line) + if len(buffer) > buffer_lines: + buffer.pop(0) + sys.stdout.write("\033[F\033[K" * len(buffer)) + for l in buffer: + print(l) + sys.stdout.flush() + +def stdHandler(std, method='show'): + from collections import deque + import sys + + if method =='show': + for line in std: + print(line, end='') + return None + + elif method =='store': + return std.read() # read everything + + elif method.startswith('buffer'): + buffer_lines = int(method.split('_')[1]) + buffer = deque(maxlen=buffer_lines) + print('------ Beginning of buffer outputs ----------------------------------') + stream_output(std, buffer_lines=buffer_lines) + print('------ End of buffer outputs ----------------------------------------') + return None + + + + + +def runBatch(batchfiles, showOutputs=True, showCommand=True, verbose=True, newWindow=False, closeWindow=True, shell_cmd='bash', nBuffer=0): """ Run one or several batch files TODO: error handling, status, parallel + + showOutputs=True => stdout & stderr printed live + showOutputs=False => stdout captured internally, stderr printed live + + For output to show in a Jupyter notebook, we cannot use stdout=None, or stderr=None, we need to use Pipe + """ + import sys + windows = (os.name == "nt") if showOutputs: STDOut= None + std_method = 'show' + if nBuffer>0: + std_method=f'buffer_{nBuffer}' else: - STDOut= open(os.devnull, 'w') - - curDir = os.getcwd() - print('Current directory', curDir) + std_method = 'store' + #STDOut= open(os.devnull, 'w') + #STDOut= subprocess.DEVNULL + STDOut= subprocess.PIPE def runOneBatch(batchfile): batchfile = batchfile.strip() batchfile = batchfile.replace('\\','/') batchDir = os.path.dirname(batchfile) + batchfileRel = os.path.relpath(batchfile, batchDir) + if windows: + command = [batchfileRel] + else: + command = [shell_cmd, batchfileRel] + if showCommand: - print('>>>> Running batch file:', batchfile) - print(' in directory:', batchDir) - try: - os.chdir(batchDir) - returncode=subprocess.call([batchfile], stdout=STDOut, stderr=subprocess.STDOUT, shell=shell) - except: - os.chdir(curDir) - returncode=-10 - return returncode + print('[INFO] Running batch file:', batchfileRel) + print(' using command:', command) + print(' in directory:', batchDir) + + if newWindow: + # --- Launch a new window (windows only for now) + if windows: + cmdflag= '/c' if closeWindow else '/k' + subprocess.Popen(f'start cmd {cmdflag} {batchfileRel}', shell=True, cwd=batchDir) + return 0 + else: + raise NotImplementedError('Running batch in `newWindow` only implemented on Windows.') + else: + # --- We wait for outputs + stdout_data = None + with safe_cd(batchDir): # Automatically go back to current directory + try: + # --- Option 2 + # Use Popen so we can print outputs live + #proc = subprocess.Popen([batchfileRel], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=shell, text=True ) + proc = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=shell, text=True ) + # Print or store stdout + stdout_data = stdHandler(proc.stdout, method=std_method) + # Always print errors output line by line + for line in proc.stderr: + print(line, end='') + proc.wait() + returncode = proc.returncode + # Dump stdout if there was an error + if returncode != 0 and stdout_data: + print("\n--- Captured stdout ---") + print(stdout_data) + except FileNotFoundError as e: + print('[FAIL] Running Batch failed, a file or command was not found see below:\n'+str(e)) + returncode=-10 + except Exception as e: + print('[FAIL] Running Batch failed, see below:\n'+str(e)) + returncode=-10 + return returncode shell=False if isinstance(batchfiles,list): @@ -152,20 +308,20 @@ def runOneBatch(batchfile): Failed.append(batchfile) if len(Failed)>0: returncode=1 - print('[FAIL] {}/{} Batch files failed.'.format(len(Failed),len(batchfiles))) + FAIL('{}/{} Batch files failed.'.format(len(Failed),len(batchfiles))) print(Failed) else: returncode=0 if verbose: - print('[ OK ] {} batch filse ran successfully.'.format(len(batchfiles))) + OK('{} batch files ran successfully.'.format(len(batchfiles))) # TODO else: returncode = runOneBatch(batchfiles) if returncode==0: if verbose: - print('[ OK ] Batch file ran successfully.') + OK('Batch file ran successfully.') else: - print('[FAIL] Batch file failed:',batchfiles) + FAIL('Batch file failed: '+str(batchfiles)) return returncode @@ -200,6 +356,7 @@ def writeBatch(batchfile, fastfiles, fastExe=None, nBatches=1, pause=False, flag discard_if_ext_present=None, dispatch=False, stdOutToFile=False, + preCommands=None, echo=True): """ Write one or several batch file, all paths are written relative to the batch file directory. The batch file will consist of lines of the form: @@ -255,6 +412,8 @@ def writeb(batchfile, fastfiles): if not echo: if os.name == 'nt': f.write('@echo off\n') + if preCommands is not None: + f.write(preCommands+'\n') for ff in fastfiles: ff_abs = os.path.abspath(ff) ff_rel = os.path.relpath(ff_abs, batchdir) diff --git a/openfast_toolbox/converters/openfastToHawc2.py b/openfast_toolbox/converters/openfastToHawc2.py index 393bed8..1e37d47 100644 --- a/openfast_toolbox/converters/openfastToHawc2.py +++ b/openfast_toolbox/converters/openfastToHawc2.py @@ -25,6 +25,8 @@ def FAST2Hawc2(fstIn, htcTemplate, htcOut, OPfile=None, TwrFAFreq=0.1, TwrSSFreq ED = fst.fst_vt['ElastoDyn'] AD = fst.fst_vt['AeroDyn15'] Bld = fst.fst_vt['AeroDynBlade'] + if isinstance(Bld, list): + Bld = Bld[0] AF = fst.fst_vt['af_data'] twrOF = fst.fst_vt['ElastoDynTower'] BD = fst.fst_vt['BeamDyn'] diff --git a/openfast_toolbox/fastfarm/AMRWindSimulation.py b/openfast_toolbox/fastfarm/AMRWindSimulation.py index a45af88..15c7d99 100644 --- a/openfast_toolbox/fastfarm/AMRWindSimulation.py +++ b/openfast_toolbox/fastfarm/AMRWindSimulation.py @@ -2,6 +2,7 @@ import os from openfast_toolbox.fastfarm.FASTFarmCaseCreation import getMultipleOf +from openfast_toolbox.tools.strings import INFO, FAIL, OK, WARN, print_bold class AMRWindSimulation: ''' @@ -180,7 +181,7 @@ def _checkInputs(self): # For convenience, the turbines should not be zero-indexed if 'name' in self.wts[0]: if self.wts[0]['name'] != 'T1': - if self.verbose>0: print(f"WARNING: Recommended turbine numbering should start at 1. Currently it is zero-indexed.") + if self.verbose>0: WARN(f"Recommended turbine numbering should start at 1. Currently it is zero-indexed.") # Flags of given/calculated spatial resolution for warning/error printing purposes @@ -188,12 +189,12 @@ def _checkInputs(self): self.given_ds_lr = False warn_msg = "" if self.ds_hr is not None: - warn_msg += f"WARNING: HIGH-RES SPATIAL RESOLUTION GIVEN. CONVERTING FATAL ERRORS ON HIGH-RES BOXES CHECKS TO WARNINGS." + warn_msg += f"HIGH-RES SPATIAL RESOLUTION GIVEN. CONVERTING FATAL ERRORS ON HIGH-RES BOXES CHECKS TO WARNINGS." self.given_ds_hr = True if self.ds_lr is not None: - warn_msg += f"WARNING: LOW-RES SPATIAL RESOLUTION GIVEN. CONVERTING FATAL ERRORS ON LOW-RES BOX CHECKS TO WARNINGS." + warn_msg += f"LOW-RES SPATIAL RESOLUTION GIVEN. CONVERTING FATAL ERRORS ON LOW-RES BOX CHECKS TO WARNINGS." self.given_ds_lr = True - if self.verbose>0: print(f'{warn_msg}\n') + if self.verbose>0 and len(warn_msg)>0: WARN(f'{warn_msg}') a=1 @@ -275,6 +276,8 @@ def _calc_sampling_time(self): # Calculate dt of high-res per guidelines dt_hr_max = 1 / (2 * self.fmax_max) self.dt_high_les = getMultipleOf(dt_hr_max, multipleof=self.dt) # Ensure dt_hr is a multiple of the AMR-Wind timestep + if self.dt_high_les ==0: + raise ValueError(f"AMR-Wind timestep dt={self.dt} is too coarse for high resolution domain! The time step based on `fmax` is {dt_hr_max}, which is too small to be rounded as a multiple of dt.") else: # The dt of high-res is given self.dt_high_les = self.dt_hr @@ -339,7 +342,7 @@ def _calc_grid_resolution(self): error_msg = f"AMR-Wind grid spacing of {self.ds_max_at_hr_level} m at the high-res box level of {self.level_hr} is too coarse for "\ f"the high resolution domain. AMR-Wind grid spacing at level {self.level_hr} must be at least {self.ds_high_les} m." if self.given_ds_hr: - if self.verbose>0: print(f'WARNING: {error_msg}') + if self.verbose>0 and len(error_msg)>0: WARN(f'{error_msg}') else: raise ValueError(error_msg) @@ -351,7 +354,7 @@ def _calc_grid_resolution(self): f"to the call to `AMRWindSimulation`. Note that sampled values will no longer be at the cell centers, as you will be requesting "\ f"sampling at {self.ds_low_les} m while the underlying grid will be at {self.ds_max_at_lr_level} m.\n --- SUPRESSING FURTHER ERRORS ---" if self.given_ds_lr: - if self.verbose>0: print(f'WARNING: {error_msg}') + if self.verbose>0 and len(error_msg)>0: WARN(f'{error_msg}') else: raise ValueError(error_msg) @@ -382,8 +385,8 @@ def _calc_grid_resolution_lr(self): # For curled wake model: ds_lr_max = self.cmeander_max * self.dt_low_les * self.vhub**2 / 5 ds_low_les = getMultipleOf(ds_lr_max, multipleof=self.ds_hr) - if self.verbose>0: print(f"Low-res spatial resolution should be at least {ds_lr_max:.2f} m, but since it needs to be a multiple of high-res "\ - f"resolution of {self.ds_hr}, we pick ds_low to be {ds_low_les} m") + if self.verbose>0: INFO(f"Low-res spatial resolution (ds_low) should be >={ds_lr_max:.2f} m.\nTo be a multiple of ds_high"\ + f"={self.ds_hr} m, we pick ds_low={ds_low_les} m") #self.ds_lr = self.ds_low_les return ds_low_les @@ -636,7 +639,7 @@ def _check_grid_placement_single(self, sampling_xyzgrid_lhr, amr_xyzgrid_at_lhr_ f"AMR-Wind grid (subset): {amr_xyzgrid_at_lhr_level_cc[amr_index ]}, {amr_xyzgrid_at_lhr_level_cc[amr_index+1]}, "\ f"{amr_xyzgrid_at_lhr_level_cc[amr_index+2]}, {amr_xyzgrid_at_lhr_level_cc[amr_index+3]}, ..." if self.given_ds_lr: - if self.verbose>0: print(f'WARNING: {error_msg}') + if self.verbose>0 and len(error_msg)>0: WARN(f'{error_msg}') else: raise ValueError(error_msg) diff --git a/openfast_toolbox/fastfarm/FASTFarmCaseCreation.py b/openfast_toolbox/fastfarm/FASTFarmCaseCreation.py index 09f0517..762aad1 100644 --- a/openfast_toolbox/fastfarm/FASTFarmCaseCreation.py +++ b/openfast_toolbox/fastfarm/FASTFarmCaseCreation.py @@ -1,16 +1,75 @@ import pandas as pd import os, sys, shutil import subprocess +from contextlib import contextmanager import numpy as np -import xarray as xr +np.random.seed(12) # For reproducibility (e.g. random azimuth) -from openfast_toolbox.io import FASTInputFile, FASTOutputFile, TurbSimFile, VTKFile +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, fastFarmTurbSimExtent, plotFastFarmSetup +from openfast_toolbox.fastfarm import writeFastFarm +from openfast_toolbox.fastfarm import plotFastFarmSetup # Make it available +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`') + + +_MOD_WAKE_STR = ['','polar', 'curled', 'cartesian'] + + def cosd(t): return np.cos(np.deg2rad(t)) def sind(t): return np.sin(np.deg2rad(t)) + + +# --------------------------------------------------------------------------------} +# --- File IO utils +# --------------------------------------------------------------------------------{ +@contextmanager +def safe_cd(newdir): + prevdir = os.getcwd() + try: + os.chdir(newdir) + yield + finally: + os.chdir(prevdir) + +def check_files_exist(*args): + import os + b = [] + if len(args)>1: + for a in args: + b.append(check_files_exist(a)) + return np.all(b) + files = args[0] + if isinstance(files, list): + for f in files: + b.append(check_files_exist(f)) + return np.all(b) + elif isinstance(files, dict): + for k,v in files.items(): + b.append(check_files_exist(v)) + return np.all(b) + elif isinstance(files, str): + # + f = files + if not os.path.exists(f): + FAIL(f'File not found: {f}') + return False + else: + OK (f'File exists : {f}') + return True + else: + raise NotImplementedError(f'Input of unknown type: {files}') + def checkIfExists(f): if os.path.basename(f) == 'unused': return True @@ -26,6 +85,22 @@ def shutilcopy2_untilSuccessful(src, dst): print(f'File {dst} not created. Trying again.\n') shutilcopy2_untilSuccessful(src,dst) +def hasSymlink(): + # If running on a platform without os.symlink (e.g. very old Python) + import tempfile + if not hasattr(os, "symlink"): + return False + try: + with tempfile.TemporaryDirectory() as tmp: + target = os.path.join(tmp, "target") + link = os.path.join(tmp, "link") + open(target, "w").close() + os.symlink(target, link) # attempt creation + return True + except (OSError, NotImplementedError): + # OSError covers Windows privilege errors, NotImplementedError covers unsupported FS + return False + def getMultipleOf(val, multipleof): ''' Get integer multiple of a quantity. @@ -57,6 +132,12 @@ def modifyProperty(fullfilename, entry, value): f.write(fullfilename) return +class FFException(Exception): + def __init__(self, message): + FAIL(message) + super().__init__(message) + + def load(fullpath, dill_filename='ffcase_obj.dill'): ''' Function to load dill objects saved with self.save() @@ -83,17 +164,19 @@ def load(fullpath, dill_filename='ffcase_obj.dill'): return obj + + class FFCaseCreation: def __init__(self, - path, - wts, - tmax, - zbot, - vhub, - shear, - TIvalue, - inflow_deg, + path=None, + wts=None, + tmax=None, + zbot=1, + vhub=None, + shear=None, + TIvalue=None, + inflow_deg=None, dt_high = None, ds_high = None, extent_high = None, @@ -106,13 +189,14 @@ def __init__(self, yaw_init = None, ADmodel = None, EDmodel = None, - nSeeds = 6, + nSeeds = None, seedValues = None, inflowPath = None, inflowType = None, sweepYawMisalignment = False, refTurb_rot = 0, #ptfm_rot = False, + flat=False, verbose = 0): ''' Full setup of a FAST.Farm simulations, can create setups for LES- or TurbSim-driven scenarios. @@ -188,7 +272,8 @@ def __init__(self, Verbosity level, given as integers <5 ''' - + np.random.seed(12) # For reproducibility (e.g. random azimuth) + # --- Store in object self.path = path self.wts = wts self.tmax = tmax @@ -218,6 +303,29 @@ def __init__(self, #self.ptfm_rot = ptfm_rot self.verbose = verbose self.attempt = 1 + self.flat = flat + # Set aux variable + self.templateFilesCreatedBool = False + self.TSlowBoxFilesCreatedBool = False + self.TShighBoxFilesCreatedBool = False + self.hasController = False + self.hasSrvD = False + self.hasHD = False + self.hasMD = False + self.hasBath = False + self.hasSS = False + self.hasSubD = False + self.hasBD = False + self.multi_HD = False + self.multi_MD = False + self.condDirList = [] + self.caseDirList = [] + self.DLLfilepath = None + self.DLLext = None + self.batchfile_high = '' + self.batchfile_low = '' + self.batchfile_ff = '' + if self.verbose is False: self.verbose = 0 @@ -225,24 +333,35 @@ def __init__(self, self._checkInputs() if self.verbose>0: print(f'Checking inputs... Done.') + if self.verbose>0: print(f'Checking if we can create symlinks...', end='\r') + self._can_create_symlinks = hasSymlink() + if self.verbose>0: print(f'Checking if we can create symlinks... {self._can_create_symlinks}') if self.verbose>0: print(f'Setting rotor parameters...', end='\r') self._setRotorParameters() if self.verbose>0: print(f'Setting rotor parameters... Done.') + # + # TODO TODO TODO + # Creating Cases and Conditions should have it's own function interface for the user can call for a given if self.verbose>0: print(f'Creating auxiliary arrays for all conditions and cases...', end='\r') self.createAuxArrays() if self.verbose>0: print(f'Creating auxiliary arrays for all conditions and cases... Done.') - if self.verbose>0: print(f'Creating directory structure and copying files...', end='\r') - self._create_dir_structure() - if self.verbose>0: print(f'Creating directory structure and copying files... Done.') + if 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() + if self.verbose>0: print(f'Creating directory structure and copying files... Done.') + + self._checkBinaries() def __repr__(self): - s = f'Requested parameters:\n' + s='<{} object> with the following content:\n'.format(type(self).__name__) + s += f'Requested parameters:\n' s += f' - Case path: {self.path}\n' s += f' - Wake model: {self.mod_wake} (1:Polar; 2:Curl; 3:Cartesian)\n' if self.inflowType == 'TS': @@ -259,7 +378,7 @@ def __repr__(self): s += f' - Wind speeds at hub height (m/s): {self.vhub}\n' s += f' - Shear exponent: {self.shear}\n' s += f' - TI (%): {self.TIvalue}\n' - s += f'\nCase details:\n' + s += f'Case details:\n' s += f' - Number of conditions: {self.nConditions}\n' for c in self.condDirList: s += f" {c}\n" @@ -273,8 +392,6 @@ def __repr__(self): s += f" ...\n" for c in self.caseDirList[-5:]: s += f" {c}\n" - s += f"\n\n" - if self.inflowType == 'TS': s += f'Turbulence boxes: TurbSim\n' @@ -284,47 +401,172 @@ def __repr__(self): s += f'LES turbulence boxes details:\n' s += f' Path: {self.inflowPath}\n' - - if self.TSlowBoxFilesCreatedBool or self.inflowType == 'LES': - s += f' Low-resolution domain: \n' - s += f' - ds low: {self.ds_low} m\n' - 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' - else: - s += f'Low-res boxes not created yet.\n' - - - if self.TShighBoxFilesCreatedBool or self.inflowType == 'LES': - 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' - else: - s += f'High-res boxes not created yet.\n' + s += f' Low-resolution domain: \n' + s += f' - ds low: {self.ds_low} m\n' + 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': + 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': + s += f' High-res boxes created: {self.TShighBoxFilesCreatedBool}.\n' s += f"\n" return s + def _checkBinaries(self): - def _checkInputs(self): + # Check the FAST.Farm binary + if self.ffbin is None: + self.ffbin = shutil.which('FAST.Farm') + if not self.ffbin: + raise ValueError(f'No FAST.Farm binary was given and none could be found in $PATH.') + if self.verbose>0: + WARN('No FAST.Farm binary has been given. Using {self.ffbin}') + elif not os.path.isfile(self.ffbin): + raise ValueError (f'The FAST.Farm binary given does not exist: {self.ffbin}.') + + # Check the TurbSim binary + if self.inflowType == 'TS': + if self.tsbin is None: + self.tsbin = shutil.which('turbsim') + if not self.tsbin: + raise ValueError(f'No TurbSim binary was given and none could be found in $PATH.') + if self.verbose>0: + WARN('No TurbSim binary has been given. Using {self.tsbin}') + elif not os.path.isfile(self.tsbin): + raise ValueError (f'The TurbSim binary given does not exist: {self.tsbin}.') + + + + # --------------------------------------------------------------------------------} + # --- Object properties + # --------------------------------------------------------------------------------{ + @property + def D(self): return self.wts[0]['D'] + + @property + def nTurbines(self): return len(self.wts) + + @property + def zhub(self): return self.wts[0]['zhub'] + + @property + def cmax(self): return self.wts[0]['cmax'] + + @property + def fmax(self): return self.wts[0]['fmax'] + + @property + def Cmeander(self): return self.wts[0]['Cmeander'] + + # --------------------------------------------------------------------------------} + # --- Path and file handling + # --------------------------------------------------------------------------------{ + def getCondPath(self, cond): + if self.flat: + return self.path + else: + return os.path.join(self.path, self.condDirList[cond]) + + def getCasePath(self, cond, case): + if self.flat: + return self.getCondPath(cond) + else: + return os.path.join(self.getCondPath(cond), self.caseDirList[case]) + + def getCaseSeedPath(self, cond, case, seed): + casePath = self.getCasePath(cond, case) + if self.flat: + return casePath + else: + return os.path.join(casePath, f'Seed_{seed}') + + def getHRTurbSimPath(self, cond, case, seed): + return os.path.join( self.getCaseSeedPath(cond, case, seed), 'TurbSim' ) + + def getCondSeedPath(self, cond, seed): + condPath = self.getCondPath(cond) + if self.flat: + return os.path.join(condPath, 'TurbSim') + else: + return os.path.join(condPath, f'Seed_{seed}') + + @property + def FFFiles(self): + files = [] + for cond in range(self.nConditions): + for case in range(self.nCases): + for seed in range(self.nSeeds): + ff_file = os.path.join(self.getCaseSeedPath(cond, case, seed), self.outputFFfilename) + files.append(ff_file) + return files + + def files(self, module='AD'): + # TODO These files shoule be stored internally when created for easy access + + # Modules that don't vary + modules_MAP={ + 'AD':self.ADfilename, + 'ED':self.EDfilename, + 'FST':self.turbfilename, + 'SrvD':self.SrvDfilename, + } # TODO use an internal dictionary like that for all the template files.. + basename = modules_MAP[module] + files=[] + if module in ['ED', 'SrvD' ,'FST']: + ext='.fst' if module=='FST' else '.dat' + for cond in range(self.nConditions): + for case in range(self.nCases): + casePath = self.getCasePath(cond, case) + for turb in range(self.nTurbines): + files.append(os.path.join(casePath, f'{basename}{turb+1}{ext}')) + else: + for cond in range(self.nConditions): + for case in range(self.nCases): + files.append( os.path.join(self.getCasePath(cond, case), basename) ) + return files + + @property + def high_res_bts(self): + 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 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 + + + def _checkInputs(self): #### check if the turbine in the template FF input exists. - - # Create case path is doesn't exist - if not os.path.exists(self.path): - os.makedirs(self.path) + # --- Default arguments + 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.shear is None: + self.shear = [0]*len(self.vhub) + if self.tmax is None: + self.tmax = 0.00001 + + # 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, ', f'z, D, zhub, cmax, fmax, Cmeander, phi_deg. The only optional entry is phi_deg.') - self.nTurbines = len(self.wts) - self.D = self.wts[0]['D'] - self.zhub = self.wts[0]['zhub'] - self.cmax = self.wts[0]['cmax'] - self.fmax = self.wts[0]['fmax'] - self.Cmeander = self.wts[0]['Cmeander'] # Check the platform heading and initialize as zero if needed if 'phi_deg' not in self.wts[0]: # check key for first turbine @@ -412,26 +654,6 @@ def _checkInputs(self): raise ValueError(f'The extent of high boxes is not enough to cover the rotor diameter. '\ 'The extent high is given as the total extent, and it needs to be greater than 1.') - # Check the FAST.Farm binary - if self.ffbin is None: - self.ffbin = shutil.which('FAST.Farm') - if not self.ffbin: - raise ValueError(f'No FAST.Farm binary was given and none could be found in $PATH.') - if self.verbose>0: - print('WARNING: No FAST.Farm binary has been given. Using {self.ffbin}') - elif not os.path.isfile(self.ffbin): - raise ValueError (f'The FAST.Farm binary given does not exist.') - - # Check the TurbSim binary - if self.inflowType == 'TS': - if self.tsbin is None: - self.tsbin = shutil.which('turbsim') - if not self.tsbin: - raise ValueError(f'No TurbSim binary was given and none could be found in $PATH.') - if self.verbose>0: - print('WARNING: No TurbSim binary has been given. Using {self.tsbin}') - elif not os.path.isfile(self.tsbin): - raise ValueError (f'The TurbSim binary given does not exist.') # Check turbine conditions arrays for consistency if len(self.inflow_deg) != len(self.yaw_init): @@ -463,7 +685,7 @@ def _checkInputs(self): f'in the ADmodel and EDmodel arrays ({np.shape(self.ADmodel)[1]})') # Check on seed parameters - if self.nSeeds is None and self.inflowType == 'LES': + if self.nSeeds is None: self.nSeeds = 1 if not isinstance(self.nSeeds,int): raise ValueError(f'An integer number of seeds should be requested. Got {self.nSeeds}.') @@ -489,7 +711,7 @@ def _checkInputs(self): self.Mod_AmbWind = 1 for p in self.inflowPath: if not os.path.isdir(p): - print(f'WARNING: The LES path {p} does not exist') + WARN(f'The LES path {p} does not exist') # LES is requested, so domain limits must be given if None in (self.dt_high, self.ds_high, self.dt_low, self.ds_low): raise ValueError (f'An LES-driven case was requested, but one or more grid parameters were not given. '\ @@ -504,9 +726,8 @@ def _checkInputs(self): # Check the ds and dt for the high- and low-res boxes. If not given, call the # AMR-Wind auxiliary function with dummy domain limits. if None in (self.dt_high, self.ds_high, self.dt_low, self.ds_low): - mod_wake_str = ['','polar', 'curled', 'cartesian'] - print(f'WARNING: One or more temporal or spatial resolution for low- and high-res domains were not given.') - print(f' Estimated values for {mod_wake_str[self.mod_wake]} wake model shown below.') + 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() # Check the temporal and spatial resolutions if provided @@ -523,20 +744,6 @@ def _checkInputs(self): if self.refTurb_rot >= self.nTurbines: raise ValueError(f'The index for the reference turbine for the farm to be rotated around is greater than the number of turbines') - # Set aux variable - self.templateFilesCreatedBool = False - self.TSlowBoxFilesCreatedBool = False - self.TShighBoxFilesCreatedBool = False - self.hasController = False - self.hasSrvD = False - self.hasHD = False - self.hasMD = False - self.hasBath = False - self.hasSS = False - self.hasSubD = False - self.hasBD = False - self.multi_HD = False - self.multi_MD = False @@ -545,7 +752,7 @@ def _determine_resolutions_from_dummy_amrwind_grid(self): from openfast_toolbox.fastfarm.AMRWindSimulation import AMRWindSimulation # Create values and keep variable names consistent across interfaces - dummy_dt = 0.1 + dummy_dt = 0.1 # TODO TODO 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) @@ -565,15 +772,17 @@ def _determine_resolutions_from_dummy_amrwind_grid(self): dt_hr = self.dt_high, dt_lr = self.dt_low, mod_wake = self.mod_wake) - print(f'Calculated values:') - print(f' High-resolution: ds: {amr.ds_high_les} m, dt: {amr.dt_high_les} s') - print(f' Low-resolution: ds: {amr.ds_low_les} m, dt: {amr.dt_low_les} s\n') - print(f'WARNING: If the above values are too fine or manual tuning is warranted, specify them manually.') - print(f' To do that, specify, e.g., `dt_high = {2*amr.dt_high_les}` to the call to `FFCaseCreation`.') - print(f' `ds_high = {2*amr.ds_high_les}`') - print(f' `dt_low = {2*amr.dt_low_les}`') - print(f' `ds_low = {2*amr.ds_low_les}`') - print(f' If the values above are okay, you can safely ignore this warning.\n') + INFO(f'Resolution - Calculated values:') + 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}`; ') + #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 @@ -586,8 +795,12 @@ def _determine_resolutions_from_dummy_amrwind_grid(self): def _create_dir_structure(self): # Create directory structure CondXX_*/CaseYY_*/Seed_Z/TurbSim; and CondXX_*/Seed_Y # Also saves the dir structure on array to write on SLURM script in the future. + # Create case path is doesn't exist + if not os.path.exists(self.path): + os.makedirs(self.path) + + # --- Creating Condition list condDirList = [] - caseDirList_ = [] for cond in range(self.nConditions): # Recover information about current condition for directory naming purposes Vhub_ = self.allCond['vhub' ].isel(cond=cond).values @@ -597,58 +810,104 @@ def _create_dir_structure(self): # Set current path name string condStr = f'Cond{cond:02d}_v{Vhub_:04.1f}_PL{shear_}_TI{tivalue_}' condDirList.append(condStr) - condPath = os.path.join(self.path, condStr) + + self.condDirList = condDirList - 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 + # --- Creating Case List + 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 + 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_}" - # Set current path name string. The case is of the following form: Case00_wdirp10_WSfalse_YMfalse_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) - casePath = os.path.join(condPath, caseStr) + caseDirList_.append(caseStr) + + self.caseDirList = caseDirList_ + + # --- Creating directories including seed directories + for cond in range(self.nConditions): + for case in range(self.nCases): + casePath = self.getCasePath(cond, case) if not os.path.exists(casePath): os.makedirs(casePath) for seed in range(self.nSeeds): - seedPath = os.path.join(casePath, f'Seed_{seed}') + seedPath = self.getCaseSeedPath(cond, case, seed) if not os.path.exists(seedPath): os.makedirs(seedPath) - turbsimPath = os.path.join(seedPath, 'TurbSim') - if not os.path.exists(turbsimPath): os.makedirs(turbsimPath) + turbSimPath = self.getHRTurbSimPath(cond, case, seed) + if not os.path.exists(turbSimPath): os.makedirs(turbSimPath) # The following loop creates the turbsim files for low box. That should really only happen if inflowStr is `TurbSim`. # It does happen regardless because when the inflow is LES, it will be later on deleted. for seed in range(self.nSeeds): - seedPath = os.path.join(condPath, f'Seed_{seed}') + seedPath = self.getCondSeedPath(cond, seed) if not os.path.exists(seedPath): os.makedirs(seedPath) - - # Get rid of duplicate entries due to the nature of the loop (equiv to only getting the first nCases entries) - self.condDirList = condDirList - self.caseDirList = sorted(list(set(caseDirList_))) - assert self.caseDirList==caseDirList_[:self.nCases] + def _copy(self, src, dst, debug=False): + 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): + 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 + return error + + + + def _symlink(self, src, dst, debug=False): + 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): + raise Exception(error) + #return error + if not os.path.exists(dst): + if self._can_create_symlinks: + try: + os.symlink(src, dst) + except FileExistsError: + error = dst + else: + try: + shutil.copy2(src, dst) + except FileExistsError: + if debug: + raise Exception(error) + error = dst + return error def copyTurbineFilesForEachCase(self, writeFiles=True): + INFO('Copying OpenFAST files from template to simulation directory') if not self.templateFilesCreatedBool: raise SyntaxError('Template files not set. Call `setTemplateFilename` before calling this function.') @@ -658,7 +917,7 @@ def copyTurbineFilesForEachCase(self, writeFiles=True): if self.verbose>0: print(f'Processing condition {self.condDirList[cond]}') for case in range(self.nCases): if self.verbose>0: print(f' Processing case {self.caseDirList[case]}', end='\r') - currPath = os.path.join(self.path, self.condDirList[cond], self.caseDirList[case]) + currPath = self.getCasePath(cond, case) # Recover info about the current CondXX_*/CaseYY_* Vhub_ = self.allCond.sel(cond=cond)['vhub'].values @@ -677,7 +936,7 @@ def copyTurbineFilesForEachCase(self, writeFiles=True): if not self.multi_HD: self.HydroDynFile.write(os.path.join(currPath, self.HDfilename)) # Copy HydroDyn Data directory - srcF = os.path.join(self.templatePath, self.hydroDatapath) + srcF = self.hydrodatafilepath dstF = os.path.join(currPath, self.hydroDatapath) os.makedirs(dstF, exist_ok=True) for file in os.listdir(srcF): @@ -697,6 +956,7 @@ def copyTurbineFilesForEachCase(self, writeFiles=True): # Write updated DISCON if writeFiles and self.hasController: if not hasattr(self, 'cpctcqfilepath'): + # TODO DO THIS WITH THE OTHER TEMPLATE FILES! # Only do this once (allows re-running of the setup) self.cpctcqfilepath = self.DISCONFile['PerfFileName'] self.cpctcqfilename = os.path.basename(self.cpctcqfilepath) # Typically Cp_Ct_Cq..txt @@ -717,16 +977,26 @@ def copyTurbineFilesForEachCase(self, writeFiles=True): # give the relative path. We give the path as the current one, so here we create a link to ensure it will work # regardless of how the controller was compiled. There is no harm in having this extra link even if it's not needed. if self.hasController: - notepath = os.getcwd(); os.chdir(self.path) for seed in range(self.nSeeds): - try: - src = os.path.join('..', self.controllerInputfilename) - dst = os.path.join(self.condDirList[cond], self.caseDirList[case], f'Seed_{seed}', self.controllerInputfilename) + seedPath = self.getCaseSeedPath(cond, case, seed) + dst = os.path.join(seedPath, self.controllerInputfilename) + if self._can_create_symlinks: + # --- Unix based + # We create a symlink at + # dst = path/cond/case/seed/DISCON.in + # pointing to : + # src = '../DISCON.in' + src = os.path.join('../', self.controllerInputfilename) if writeFiles: - os.symlink(src, dst) - except FileExistsError: - pass - os.chdir(notepath) + try: + os.symlink(src, dst) + except FileExistsError: + pass + else: + # --- Windows + src = self.controllerInputfilepath + if writeFiles: + self._copy(src, dst, debug=False) # Write InflowWind files. For FAST.Farm, each OpenFAST instance needs to have an IW file. For LES-driven cases (Mod_AmbWind=1), # it is still needed, even though WindType is not used. For TS-driven, it is also needed and WindType should be 3. Here we use @@ -745,7 +1015,8 @@ def copyTurbineFilesForEachCase(self, writeFiles=True): self.InflowWindFile.write( os.path.join(currPath,self.IWfilename)) if self.Mod_AmbWind == 3: # only for TS-driven cases for seed in range(self.nSeeds): - self.InflowWindFile.write( os.path.join(currPath,f'Seed_{seed}',self.IWfilename)) + seedPath = self.getCaseSeedPath(cond, case, seed) + 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 @@ -811,7 +1082,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 + #self.AeroDynFile['BEM_Mod'] = 2 # TODO let the user decide. Commented out by Emmanuel 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])}"' @@ -827,7 +1098,7 @@ def copyTurbineFilesForEachCase(self, writeFiles=True): if self.hasSrvD: self.ServoDynFile['YawNeut'] = yaw_deg_ + yaw_mis_deg_ self.ServoDynFile['VSContrl'] = 5 - self.ServoDynFile['DLL_FileName'] = f'"{self.DLLfilepath}{t+1}.so"' + self.ServoDynFile['DLL_FileName'] = f'"{self.DLLfilepath}{t+1}.{self.DLLext}"' self.ServoDynFile['DLL_InFile'] = f'"{self.controllerInputfilename}"' if writeFiles: self.ServoDynFile.write( os.path.join(currPath,f'{self.SrvDfilename}{t+1}_mod.dat')) @@ -938,14 +1209,15 @@ def copyTurbineFilesForEachCase(self, writeFiles=True): if writeFiles: if self._were_all_turbine_files_copied() == False and self.attempt<=5: self.attempt += 1 - print(f'Not all files were copied successfully. Trying again. Attempt number {self.attempt}.') + WARN(f'Not all files were copied successfully. Trying again. Attempt number {self.attempt}.') self.copyTurbineFilesForEachCase() elif self.attempt > 5: - print(f"WARNING: Not all turbine files were copied successfully after 5 tries.") - print(f" Check them manually. This shouldn't occur. Consider finding ") - print(f" and fixing the bug and submitting a PR.") + 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.") else: - if self.verbose>0: print(f'Passed check: all files were copied successfully.') + #if self.verbose>0: OK(f'All files were copied successfully.') + OK(f'All OpenFAST files were copied successfully.') def _were_all_turbine_files_copied(self): @@ -955,7 +1227,7 @@ def _were_all_turbine_files_copied(self): for cond in range(self.nConditions): for case in range(self.nCases): - currPath = os.path.join(self.path, self.condDirList[cond], self.caseDirList[case]) + currPath = self.getCasePath(cond, case) # Check HydroDyn if self.hasHD and not self.multi_HD: @@ -1002,7 +1274,8 @@ def _were_all_turbine_files_copied(self): if not _: return False if self.Mod_AmbWind == 3: # only for TS-driven cases for seed in range(self.nSeeds): - _ = checkIfExists(os.path.join(currPath,f'Seed_{seed}',self.IWfilename)) + seedPath = self.getCaseSeedPath(cond, case, seed) + _ = checkIfExists(os.path.join(seedPath,self.IWfilename)) if not _: return False for t in range(self.nTurbines): @@ -1039,16 +1312,19 @@ def _were_all_turbine_files_copied(self): return True - def setTemplateFilename(self, templatePath=None, templateFiles=None): + def setTemplateFilename(self, templatePath=None, templateFiles=None, templateFSTF=None, verbose=None): """ Function to receive, check, and set all the template files. Inputs ------ - templatePath: str + - templateFSTF: + The path, relative or absolute to a FAST.Farm fstf input file. + + - templatePath: str The path of the directory where teh template files exist. - templateFiles: dict + - templateFiles: dict A dictionary containing the filenames and their corresponding types as keys. Keys should correspond to the variable names expected in the function. The values should be strings with the filenames or filepaths as appropriate. @@ -1057,18 +1333,29 @@ def setTemplateFilename(self, templatePath=None, templateFiles=None): assumption is made regarding its location. All values should be explicitly defined. Unused ones should be set as None. + Example call - OPTION 1 + -------------------------------------------------- + templateFSTF = 'path/to/main.fstf' + templateFiles = { + 'turbsimLowfilepath' : './SampleFiles/template_Low_InflowXX_SeedY.inp', + 'turbsimHighfilepath' : './SampleFiles/template_HighT1_InflowXX_SeedY.inp', + } + setTemplateFilename(templateFSTF=templateFSTF, templateFiles=templateFiles) + - Example call - ------------ + Example call - OPTION 2 + -------------------------------------------------- templatePath = 'full/path/to/template/files/' templateFiles = { + 'FFfilename' : 'Model_FFarm.fstf' + 'turbfilename' : 'Model.T', 'EDfilename' : 'ElastoDyn.T', - 'SEDfilename' : None, + 'SrvDfilename' : 'ServoDyn.T', 'HDfilename' : 'HydroDyn.dat', # ending with .T for per-turbine HD, .dat for holisitc 'MDfilename' : 'MoorDyn.T', # ending with .T for per-turbine MD, .dat for holistic + 'SEDfilename' : None, 'bathfilename' : 'bathymetry.txt', 'SSfilename' : 'SeaState.dat', - 'SrvDfilename' : 'ServoDyn.T', 'ADfilename' : 'AeroDyn.dat', 'ADskfilename' : None, 'SubDfilename' : None, @@ -1078,20 +1365,23 @@ def setTemplateFilename(self, templatePath=None, templateFiles=None): 'EDbladefilename' : 'ElastoDyn_Blade.dat', 'EDtowerfilename' : 'ElastoDyn_Tower.dat', 'ADbladefilename' : 'AeroDyn_Blade.dat', - 'turbfilename' : 'Model.T', - 'libdisconfilepath' : '/full/path/to/controller/libdiscon.so', 'controllerInputfilename' : 'DISCON', + # TODO 'coeffTablefilename' : None, 'hydroDatapath' : '/full/path/to/hydroData', + 'libdisconfilepath' : '/full/path/to/controller/libdiscon.so', 'turbsimLowfilepath' : './SampleFiles/template_Low_InflowXX_SeedY.inp', 'turbsimHighfilepath' : './SampleFiles/template_HighT1_InflowXX_SeedY.inp', - 'FFfilename' : 'Model_FFarm.fstf' } setTemplateFilename(templatePath, templateFiles) """ + INFO('Reading and checking template files') + if verbose is None: + verbose=self.verbose # Set default values + #TODO TODO 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" @@ -1113,32 +1403,95 @@ def setTemplateFilename(self, templatePath=None, templateFiles=None): self.libdisconfilepath = "unused" self.coeffTablefilename = "unused" self.hydroDatapath = "unused" - self.turbsimLowfilepath = "unused" - self.turbsimHighfilepath = "unused" - self.FFfilename = "unused" + self.turbsimLowfilepath = "unused" # TODO Convention unclear + self.turbsimHighfilepath = "unused" # TODO Convention unclear + self.FFfilename = "unused"; self.FFfilepath = "unused" - # Check and set the templatePath - if templatePath is None: - print(f'--- WARNING: No template files given. Complete setup will not be possible') - return - if not os.path.isdir(templatePath): - raise ValueError(f'Template path {templatePath} does not seem to exist.') - self.templatePath = templatePath - self.templatePathabs = os.path.abspath(self.templatePath) - # Check and set the templateFiles valid_keys = {'EDfilename', 'SEDfilename', 'HDfilename', 'MDfilename', 'bathfilename', 'SSfilename', 'SrvDfilename', 'ADfilename', 'ADskfilename', 'SubDfilename', 'IWfilename', 'BDfilename', 'BDbladefilename', 'EDbladefilename', 'EDtowerfilename', 'ADbladefilename', 'turbfilename', 'libdisconfilepath', 'controllerInputfilename', 'coeffTablefilename', 'hydroDatapath', 'FFfilename', 'turbsimLowfilepath', 'turbsimHighfilepath'} + + if templateFiles is None: + templateFiles={} + if not isinstance(templateFiles, dict): raise ValueError(f'templateFiles should be a dictionary with the following valid entries: {valid_keys}') + 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. + 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()}') + 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]}') + + # --- The user provided a FSTF file from which we override the templateFiles + if templateFSTF is not None: + templateFiles['FFfilename'] = templateFSTF + # --- + baseDir = os.path.dirname(templateFSTF) + fstf = FASTInputFile(templateFSTF) + fstFilename = os.path.join(baseDir,fstf['WindTurbines'][0,3].strip('"').strip()) + # Read all existing files from FST file into an input deck + dck = FASTInputDeck(fstFilename, verbose=False) + fread = dck.inputFilesRead + # Check and set the templateFiles from the input deck + KEY_MAP = {'ED':'EDfilename', 'EDtwr':'EDtowerfilename', 'EDbld':'EDbladefilename', 'SED':'SEDfilename', + 'AD':'ADfilename', 'ADbld':'ADbladefilename', 'ADdsk':'ADskfilename', + 'HD':'HDfilename', 'MD':'MDfilename', 'SeaSt':'SSfilename', + 'SrvD':'SrvDfilename', 'SrvDdll':'libdisconfilepath', 'SrvDini':'controllerInputfilename', + 'SD':'SubDfilename', + 'IW':'IWfilename', + 'BD':'BDfilename', 'BDbld':'BDbladefilename', + 'Fst':'turbfilename'} + #, 'bathfilename', # TODO + # 'coeffTablefilename', # TODO + for key_deck, key_tpl in KEY_MAP.items(): + if key_deck in fread.keys(): + if key_tpl in templateFiles: + WARN(f'Template {key_tpl} is provided in templateFiles, not using value from FSTF file.') + else: + filebase = fread[key_deck] + 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}') + + # 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. + + # -------------------------------------------------------------------------------- + # NOTE: BEYOND THIS POINT THE VALUES OF TEMPLATE FILES ARE EITHER: + # - ABSOLUTE OR + # - RELATIVE TO CALLER SCRIPT + # and the script should work no matter what. + # The convention is : + # - *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 not valid_keys >= set(templateFiles.keys()): raise ValueError(f'Extra entries are present in the dictionary. '\ f'Extra keys: {list(set(templateFiles.keys()) - valid_keys)}.') + + def checkIfExists(f): if os.path.basename(f) == 'unused': return @@ -1150,193 +1503,202 @@ def checkIfExists(f): if value == 'unused' or value is None: continue + # --- Per turbine templates # Map the template file types to the specific checks - if key == 'EDfilename': + if key == 'turbfilename': + if not value.endswith('.T'): + raise ValueError(f'Name the template turbine file "*.T.fst" and give "*.T" as `turbfilename`') + self.turbfilepath = value + ".fst" + checkIfExists(self.turbfilepath) + self.turbfilename = os.path.basename(value) + + elif key == 'EDfilename': if not value.endswith('.T'): raise ValueError(f'Name the template ED file "*.T.dat" and give "*.T" as `EDfilename`') - self.EDfilepath = os.path.join(self.templatePath, f"{value}.dat") + self.EDfilepath = value + ".dat" checkIfExists(self.EDfilepath) - self.EDfilename = value + self.EDfilename = os.path.basename(value) elif key == 'SEDfilename': if not value.endswith('.T'): raise ValueError(f'Name the template SED file "*.T.dat" and give "*.T" as `SEDfilename`') - self.SEDfilepath = os.path.join(self.templatePath, f"{value}.dat") + self.SEDfilepath = value + ".dat" checkIfExists(self.SEDfilepath) - self.SEDfilename = value + self.SEDfilename = os.path.basename(value) elif key == 'HDfilename': if value.endswith('.dat'): self.multi_HD = False - self.HDfilepath = os.path.join(self.templatePath, value) + self.HDfilepath = value elif value.endswith('.T'): self.multi_HD = True - self.HDfilepath = os.path.join(self.templatePath, f'{value}.dat') + self.HDfilepath = value + ".dat" else: raise ValueError(f'The HydroDyn filename should end in either `.dat` (for single ', \ f'farm-wide HydroDyn) or `.T` (for per-turbine HydroDyn).') checkIfExists(self.HDfilepath) - self.HDfilename = value + self.HDfilename = os.path.basename(value) self.hasHD = True elif key == 'MDfilename': if value.endswith('.dat'): self.multi_MD = False - self.MDfilepath = os.path.join(self.templatePath, value) + self.MDfilepath = value elif value.endswith('.T'): self.multi_MD = True - self.MDfilepath = os.path.join(self.templatePath, f'{value}.dat') + self.MDfilepath = value + ".dat" else: raise ValueError(f'The MoorDyn filename should end in either `.dat` (for single ', \ f'farm-wide MoorDyn) or `.T` (for per-turbine MoorDyn).') checkIfExists(self.MDfilepath) - self.MDfilename = value + self.MDfilename = os.path.basename(value) self.hasMD = True + elif key == 'SrvDfilename': + if not value.endswith('.T'): + raise ValueError(f'Name the template ServoDyn file "*.T.dat" and give "*.T" as `SrvDfilename`') + self.SrvDfilepath = value + ".dat" + checkIfExists(self.SrvDfilepath) + self.SrvDfilename = os.path.basename(value) + self.hasSrvD = True + + # --- Files that are NOT changing from turbine to turbines elif key == 'bathfilename': - self.bathfilepath = os.path.join(self.templatePath, value) + self.bathfilepath = value checkIfExists(self.bathfilepath) - self.bathfilename = value + self.bathfilename = os.path.basename(value) self.hasBath = True + elif key == 'SSfilename': if not value.endswith('.dat'): raise ValueError(f'The SeaState filename should end in `.dat`.') - self.SSfilepath = os.path.join(self.templatePath, value) + self.SSfilepath = value checkIfExists(self.SSfilepath) - self.SSfilename = value + self.SSfilename = os.path.basename(value) self.hasSS = True - elif key == 'SrvDfilename': - if not value.endswith('.T'): - raise ValueError(f'Name the template ServoDyn file "*.T.dat" and give "*.T" as `SrvDfilename`') - self.SrvDfilepath = os.path.join(self.templatePath, f"{value}.dat") - checkIfExists(self.SrvDfilepath) - self.SrvDfilename = value - self.hasSrvD = True - elif key == 'ADfilename': if not value.endswith('.dat'): raise ValueError(f'The AeroDyn filename should end in `.dat`.') - self.ADfilepath = os.path.join(self.templatePath, value) + self.ADfilepath = value checkIfExists(self.ADfilepath) - self.ADfilename = value + self.ADfilename = os.path.basename(value) elif key == 'ADskfilename': if not value.endswith('.dat'): raise ValueError(f'The AeroDisk filename should end in `.dat`.') - self.ADskfilepath = os.path.join(self.templatePath, value) + self.ADskfilepath = value checkIfExists(self.ADskfilepath) - self.ADskfilename = value + self.ADskfilename = os.path.basename(value) self.hasController = False self.hasSrvD = False elif key == 'SubDfilename': if not value.endswith('.dat'): raise ValueError(f'The SubDyn filename should end in `.dat`.') - self.SubDfilepath = os.path.join(self.templatePath, value) + self.SubDfilepath = value checkIfExists(self.SubDfilepath) - self.SubDfilename = value + self.SubDfilename = os.path.basename(value) self.hasSubD = True elif key == 'IWfilename': - if not value.endswith('.dat'): + if not value.lower().endswith('.dat'): raise ValueError(f'The InflowWind filename should end in `.dat`.') - self.IWfilepath = os.path.join(self.templatePath, value) + self.IWfilepath = value checkIfExists(self.IWfilepath) - self.IWfilename = value + self.IWfilename = os.path.basename(value) elif key == 'BDfilename': - if not value.endswith('.dat'): + if not value.lower().endswith('.dat'): raise ValueError(f'The BeamDyn filename should end in `.dat`.') - self.BDfilepath = os.path.join(self.templatePath, value) + self.BDfilepath = value checkIfExists(self.BDfilepath) - self.BDfilename = value + self.BDfilename = os.path.basename(value) self.hasBD = True elif key == 'BDbladefilename': - if not value.endswith('.dat'): + if not value.lower().endswith('.dat'): raise ValueError(f'The BeamDyn blade filename should end in `.dat`.') - self.BDbladefilepath = os.path.join(self.templatePath, value) + self.BDbladefilepath = value checkIfExists(self.BDbladefilepath) - self.BDbladefile = value + self.BDbladefile = os.path.basename(value) elif key == 'EDbladefilename': - if not value.endswith('.dat'): + if not value.lower().endswith('.dat'): raise ValueError(f'The ElastoDyn blade filename should end in `.dat`.') - self.EDbladefilepath = os.path.join(self.templatePath, value) + self.EDbladefilepath = value checkIfExists(self.EDbladefilepath) - self.EDbladefilename = value + self.EDbladefilename = os.path.basename(value) elif key == 'EDtowerfilename': - if not value.endswith('.dat'): + if not value.lower().endswith('.dat'): raise ValueError(f'The ElastoDyn tower filename should end in `.dat`.') - self.EDtowerfilepath = os.path.join(self.templatePath, value) + self.EDtowerfilepath = value checkIfExists(self.EDtowerfilepath) - self.EDtowerfilename = value + self.EDtowerfilename = os.path.basename(value) elif key == 'ADbladefilename': - if not value.endswith('.dat'): + if not value.lower().endswith('.dat'): raise ValueError(f'The AeroDyn blade filename should end in `.dat`.') - self.ADbladefilepath = os.path.join(self.templatePath, value) + self.ADbladefilepath = value checkIfExists(self.ADbladefilepath) - self.ADbladefilename = value + self.ADbladefilename = os.path.basename(value) - elif key == 'turbfilename': - if not value.endswith('.T'): - raise ValueError(f'Name the template turbine file "*.T.fst" and give "*.T" as `turbfilename`') - self.turbfilepath = os.path.join(self.templatePath, f"{value}.fst") - checkIfExists(self.turbfilepath) - self.turbfilename = value - - elif key == 'libdisconfilepath': - if not value.endswith('.so'): - raise ValueError(f'The libdiscon file should end in "*.so"') - if os.path.isabs(value): - self.libdisconfilepath = value - else: - self.libdisconfilepath = os.path.abspath(value) - checkIfExists(self.libdisconfilepath) - self._create_copy_libdiscon() - self.hasController = True + elif key == 'FFfilename': + if not value.endswith('.fstf'): + 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.endswith('.IN'): + if not value.lower().endswith('.in'): print(f'--- WARNING: The controller input file typically ends in "*.IN". Currently {value}. Double check.') - self.controllerInputfilepath = os.path.join(self.templatePath, value) + self.controllerInputfilepath = value checkIfExists(self.controllerInputfilepath) - self.controllerInputfilename = value + self.controllerInputfilename = os.path.basename(value) elif key == 'coeffTablefilename': if not value.endswith('.csv'): raise ValueError(f'The performance table file should end in "*.csv"') - self.coeffTablefilepath = os.path.join(self.templatePath, value) + self.coeffTablefilepath = value checkIfExists(self.coeffTablefilepath) - self.coeffTablefilename = value + self.coeffTablefilename = os.path.basename(value) elif key == 'hydroDatapath': - self.hydrodatafilepath = os.path.join(self.templatePath, value) + 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 = 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']: + raise ValueError(f'The libdiscon file should have extension ".so", ".dll", or ".dylib"') + self.DLLext = ext[1:] # No dot + if os.path.isabs(value): + self.libdisconfilepath = value + else: + self.libdisconfilepath = os.path.abspath(value).replace('\\','/') + checkIfExists(self.libdisconfilepath) + self._create_copy_libdiscon() + self.hasController = True + + # --- TODO TODO TODO not clean convention elif key == 'turbsimLowfilepath': if not value.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'): raise ValueError(f'TurbSim file input for high-res box should end in ".inp".') self.turbsimHighfilepath = value checkIfExists(self.turbsimHighfilepath) - elif key == 'FFfilename': - if not value.endswith('.fstf'): - raise ValueError(f'FAST.Farm input file should end in ".fstf".') - self.FFfilepath = os.path.join(self.templatePath, value) - checkIfExists(self.FFfilepath) - self.FFfilename = value # Perform some checks @@ -1371,16 +1733,18 @@ def _create_copy_libdiscon(self): # Make copies of libdiscon for each turbine if they don't exist copied = False for t in range(self.nTurbines): + # NOTE: libdisconfilepath contains extension + DLL_parentDir = os.path.dirname(self.libdisconfilepath).replace('\\','/') libdisconfilename = os.path.splitext(os.path.basename(self.libdisconfilepath))[0] - currLibdiscon = os.path.join(os.path.dirname(self.libdisconfilepath), f'{libdisconfilename}.T{t+1}.so') - self.DLLfilepath = os.path.join(os.path.dirname(self.libdisconfilepath), f'{libdisconfilename}.T') + 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 {libdisconfilename}.so in {currLibdiscon}') + if self.verbose>0: print(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}].so already exists in {os.path.dirname(self.libdisconfilepath)}. Skipped step.') + print(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): @@ -1414,6 +1778,11 @@ def createAuxArrays(self): self._rotate_wts() self._create_all_cond() self._create_all_cases() + if self.flat: + if self.nCases==1 and self.nConditions==1: + self.flat + else: + self.flat = False def _create_all_cond(self): @@ -1434,7 +1803,7 @@ def _create_all_cond(self): self.nConditions = len(self.vhub) * len(self.shear) * len(self.TIvalue) if self.verbose>1: print(f'The length of vhub, shear, and TI are different. Assuming sweep on each of them.') - if self.verbose>0: print(f'Creating {self.nConditions} conditions') + if self.verbose>0: print(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))) @@ -1652,14 +2021,45 @@ 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 = [] for cond in range(self.nConditions): for seed in range(self.nSeeds): - seedPath = os.path.join(self.path, self.condDirList[cond], f'Seed_{seed}') + seedPath = self.getCondSeedPath(cond, seed) # ---------------- TurbSim Low boxes setup ------------------ # # Set file to be created @@ -1691,7 +2091,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(self.turbsimLowfilepath, currentTSLowFile, tmax=self.tmax+self.dt_low, verbose=self.verbose) + self.TSlowbox.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) @@ -1699,14 +2099,20 @@ def TS_low_setup(self, writeFiles=True, runOnce=False): Lowinp['PLExp'] = shear_ #Lowinp['latitude'] = latitude # Not used when IECKAI model is selected. Lowinp['InCDec1'] = Lowinp['InCDec2'] = Lowinp['InCDec3'] = f'"{a} {b/(8.1*Lambda1):.8f}"' + lowFileName = os.path.join(seedPath, 'Low.inp') + if writeFiles: - lowFileName = os.path.join(seedPath, 'Low.inp') Lowinp.write(lowFileName) + lowFilesName.append(lowFileName) # Let's remove the original file os.remove(os.path.join(seedPath, 'Low_stillToBeModified.inp')) self.TSlowBoxFilesCreatedBool = True + if len(lowFilesName)==1: + OK(f'TurbSim low resolution input files generated, see e.g.:\n{lowFilesName[0]}') + elif len(lowFilesName)>1: + OK(f'TurbSim low resolution input files generated, see e.g.:\n{lowFilesName[0]}\n{lowFilesName[-1]}') def sed_inplace(self, sed_command, inplace): @@ -1725,7 +2131,40 @@ def sed_inplace(self, sed_command, inplace): shutil.move(os.path.join(self.path,'temp.txt'), os.path.join(self.path,filename)) - def TS_low_slurm_prepare(self, slurmfilepath, inplace=True): + + def TS_low_batch_prepare(self, run=False, **kwargs): + """ Writes a flat batch file for TurbSim low""" + + from openfast_toolbox.case_generation.runner import writeBatch + + ext = ".bat" if os.name == "nt" else ".sh" + batchfile = os.path.join(self.path, f'runAllLowBox{ext}') + + TS_files = [] + for cond in range(self.nConditions): + for seed in range(self.nSeeds): + seedpath = self.getCondSeedPath(cond, seed) + TS_files.append(f'{seedpath}/Low.inp') + + writeBatch(batchfile, TS_files, fastExe=self.tsbin, **kwargs) + self.batchfile_low = batchfile + OK(f"Batch file written to {batchfile}") + + if run: + self.TS_low_batch_run() + + def TS_low_batch_run(self, showOutputs=True, showCommand=True, verbose=True, **kwargs): + from openfast_toolbox.case_generation.runner import runBatch + if not os.path.exists(self.batchfile_low): + raise FFException(f'Batch file does not exist: {self.batchfile_low}.\nMake sure you run TS_low_batch_prepare first.') + stat = runBatch(self.batchfile_low, showOutputs=showOutputs, showCommand=showCommand, verbose=verbose, **kwargs) + if stat!=0: + raise FFException(f'Batch file failed: {self.batchfile_low}') + + + + + def TS_low_slurm_prepare(self, slurmfilepath, inplace=True, useSed=False): # -------------------------------------------------- # ----- Prepare SLURM script for Low-res boxes ----- @@ -1733,41 +2172,73 @@ def TS_low_slurm_prepare(self, slurmfilepath, inplace=True): if not os.path.isfile(slurmfilepath): raise ValueError (f'SLURM script for low-res box {slurmfilepath} does not exist.') - self.slurmfilename_low = os.path.basename(slurmfilepath) - - shutil.copy2(slurmfilepath, os.path.join(self.path, self.slurmfilename_low)) # Determine memory-per-cpu memory_per_cpu = int(150000/self.nSeeds) - - # Change job name (for convenience only) - 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) - + if self.nSeeds > 6: - print(f'--- WARNING: The memory-per-cpu on the low-res boxes SLURM script might be too low given {self.nSeeds} seeds.') + 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) + INFO(f'File written: {self.slurmfilename_low}') def TS_low_slurm_submit(self, qos='normal', A=None, t=None, p=None, inplace=True): @@ -1790,27 +2261,38 @@ 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 - - notepath = os.getcwd() - os.chdir(self.path) for cond in range(self.nConditions): for case in range(self.nCases): for seed in range(self.nSeeds): - try: - src = os.path.join('..', '..', '..', '..', self.condDirList[cond], f'Seed_{seed}', 'Low.bts') - dst = os.path.join(self.condDirList[cond], self.caseDirList[case], f'Seed_{seed}', 'TurbSim', 'Low.bts') - os.symlink(src, dst) - except FileExistsError: - print(f' File {dst} already exists. Skipping symlink.') - os.chdir(notepath) + condSeedPath = self.getCondSeedPath(cond, seed) + 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) def getDomainParameters(self): + INFO('Computing low and high res extent according to TurbSim capabilities') # If the low box setup hasn't been called (e.g. LES run), do it once to get domain extents if not self.TSlowBoxFilesCreatedBool: if self.verbose>1: print(' Running a TurbSim setup once to get domain extents') - self.TS_low_setup(writeFiles=False, runOnce=True) + self.TSlowbox = self.TS_low_dummy() # 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 @@ -1833,8 +2315,8 @@ def getDomainParameters(self): self.xoffset_turbsOrigin2TSOrigin = -self.extent_low[0]*self.D if self.verbose>0: - print(f" The y offset between the turbine ref frame and turbsim is {self.yoffset_turbsOrigin2TSOrigin}") print(f" The x offset between the turbine ref frame and turbsim is {self.xoffset_turbsOrigin2TSOrigin}") + print(f" The y offset between the turbine ref frame and turbsim is {self.yoffset_turbsOrigin2TSOrigin}") if self.verbose>2: print(f'allHighBoxCases is:') @@ -1846,7 +2328,7 @@ def TS_high_get_time_series(self): # Loop on all conditions/seeds extracting time series from the Low box at turbines location for cond in range(self.nConditions): for seed in range(self.nSeeds): - condSeedPath = os.path.join(self.path, self.condDirList[cond], f'Seed_{seed}') + condSeedPath = self.getCondSeedPath(cond, seed) # Read output .bts for current seed bts = TurbSimFile(os.path.join(condSeedPath, 'Low.bts')) @@ -1857,7 +2339,7 @@ def TS_high_get_time_series(self): # Get actual case number given the high-box that need to be saved case = self.allHighBoxCases.isel(case=case)['case'].values - caseSeedPath = os.path.join(self.path, self.condDirList[cond], self.caseDirList[case], f'Seed_{seed}', 'TurbSim') + caseSeedPath = self.getHRTurbSimPath(cond, case, seed) for t in range(self.nTurbines): # Recover turbine properties of the current case @@ -1924,10 +2406,12 @@ 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 # 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 @@ -1942,7 +2426,7 @@ def TS_high_setup(self, writeFiles=True): if self.verbose>3: print(f'Generating high-res box setup for cond {cond} ({self.condDirList[cond]}), case {case} ({self.caseDirList[case]}).') for seed in range(self.nSeeds): - seedPath = os.path.join(self.path, self.condDirList[cond], self.caseDirList[case], f'Seed_{seed}', 'TurbSim') + seedPath = self.getHRTurbSimPath(cond, case, seed) for t in range(self.nTurbines): @@ -1967,7 +2451,7 @@ def TS_high_setup(self, writeFiles=True): 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(self.turbsimHighfilepath, currentTSHighFile, tmax=self.tmax_low, turb=t, verbose=self.verbose) + currentTS.writeTSFile(fileIn=self.turbsimHighfilepath, fileOut=currentTSHighFile, tmax=self.tmax_low, turb=t, verbose=self.verbose) # Modify some values and save file (some have already been set in the call above) Highinp = FASTInputFile(currentTSHighFile) @@ -1988,50 +2472,115 @@ def TS_high_setup(self, writeFiles=True): os.remove(os.path.join(seedPath, f'HighT{t+1}_stillToBeModified.inp')) self.TShighBoxFilesCreatedBool = True + if len(highFilesName)==1: + OK(f'TurbSim high resolution input files generated, see e.g.:\n{highFilesName[0]}') + elif len(highFilesName)>1: + OK(f'TurbSim high resolution input files generated, see e.g.:\n{highFilesName[0]}\n{highFilesName[-1]}') + - def TS_high_slurm_prepare(self, slurmfilepath, inplace=True): + def TS_high_batch_prepare(self, run=False, **kwargs): + """ Writes a flat batch file for TurbSim low""" + from openfast_toolbox.case_generation.runner import writeBatch + + 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) + self.batchfile_high = batchfile + + OK(f"Batch file written to {batchfile}") + + if run: + self.TS_high_batch_run() + + def TS_high_batch_run(self, showOutputs=True, showCommand=True, verbose=True, **kwargs): + from openfast_toolbox.case_generation.runner import runBatch + if not os.path.exists(self.batchfile_high): + raise FFException(f'Batch file does not exist: {self.batchfile_high}.\nMake sure you run TS_high_batch_prepare first.') + stat = runBatch(self.batchfile_high, showOutputs=showOutputs, showCommand=showCommand, verbose=verbose, **kwargs) + if stat!=0: + raise FFException(f'Batch file failed: {self.batchfile_high}') + + + def TS_high_slurm_prepare(self, slurmfilepath, inplace=True, useSed=False): # --------------------------------------------------- # ----- Prepare SLURM script for High-res boxes ----- # --------------------------------------------------- if not os.path.isfile(slurmfilepath): raise ValueError (f'SLURM script for high-res box {slurmfilepath} does not exist.') - self.slurmfilename_high = os.path.basename(slurmfilepath) - ntasks = self.nConditions*self.nHighBoxCases*self.nSeeds*self.nTurbines - 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) + + + 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) @@ -2060,14 +2609,12 @@ def TS_high_create_symlink(self): if self.verbose>0: print(f'Creating symlinks for all the high-resolution boxes') - notepath = os.getcwd() - os.chdir(self.path) for cond in range(self.nConditions): for case in range(self.nCases): # In order to do the symlink let's check if the current case is source (has bts). If so, skip if. If not, find its equivalent source casematch = self.allHighBoxCases['case'] == case if len(np.where(casematch)) != 1: - raise ValueError (f'Something is wrong with your allHighBoxCases array. Found repeated case number. Stopping') + raise ValueError (f'Something is wrong with the allHighBoxCases array. Found repeated case number. Stopping') src_id = np.where(casematch)[0] @@ -2091,14 +2638,12 @@ def TS_high_create_symlink(self): # Now that we have the correct arrays, we perform the loop on the turbines and seeds for t in range(self.nTurbines): for seed in range(self.nSeeds): - src = os.path.join('..', '..', '..', '..', self.condDirList[cond], self.caseDirList[src_case], f'Seed_{seed}', 'TurbSim', f'HighT{t+1}.bts') - dst = os.path.join(self.condDirList[cond], self.caseDirList[case], f'Seed_{seed}', 'TurbSim', f'HighT{t+1}.bts') - - try: - os.symlink(src, dst) - except FileExistsError: - if self.verbose>1: print(f' File {dst} already exists. Skipping symlink.') - os.chdir(notepath) + 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') + #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) + self._symlink(src, dst) def FF_setup(self, outlistFF=None, **kwargs): @@ -2172,17 +2717,27 @@ 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 - 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.') + #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 + #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.') + #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) @@ -2195,13 +2750,12 @@ def _FF_setup_LES(self, seedsToKeep=1): # Clean unnecessary directories and files created by the general setup for cond in range(self.nConditions): for seed in range(self.nSeeds): - currpath = os.path.join(self.path, self.condDirList[cond], f'Seed_{seed}') + currpath = self.getCondSeedPath(cond, seed) if os.path.isdir(currpath): shutil.rmtree(currpath) for case in range(self.nCases): - #shutil.rmtree(os.path.join(path, condDirList[cond], caseDirList[case], f'Seed_0','InflowWind.dat')) # needs to exist for seed in range(seedsToKeep,self.nSeeds): - currpath = os.path.join(self.path, self.condDirList[cond], self.caseDirList[case], f'Seed_{seed}') + currpath = self.getCaseSeedPath(cond, case, seed) if os.path.isdir(currpath): shutil.rmtree(currpath) @@ -2212,35 +2766,30 @@ def _FF_setup_LES(self, seedsToKeep=1): for case in range(self.nCases): for seed in range(self.seedsToKeep): # Remove TurbSim dir - currpath = os.path.join(self.path, self.condDirList[cond], self.caseDirList[case], f'Seed_{seed}', 'TurbSim') + currpath = self.getHRTurbSimPath(cond, case, seed) + seedPath = self.getCaseSeedPath(cond, case, seed) if os.path.isdir(currpath): shutil.rmtree(currpath) # Create LES boxes dir - currpath = os.path.join(self.path,self.condDirList[cond],self.caseDirList[case],f'Seed_{seed}',LESboxesDirName) + currpath = os.path.join(seedPath, LESboxesDirName) if not os.path.isdir(currpath): os.makedirs(currpath) # Low-res box - try: - src = os.path.join(self.inflowPath[cond], 'Low') - dst = os.path.join(self.path, self.condDirList[cond], self.caseDirList[case], f'Seed_{seed}', LESboxesDirName, 'Low') - os.symlink(src, dst) - except FileExistsError: - print(f'Directory {dst} already exists. Skipping symlink.') + src = os.path.join(self.inflowPath[cond], 'Low') + dst = os.path.join(seedPath, LESboxesDirName, 'Low') + self._symlink(src, dst) # High-res boxes for t in range(self.nTurbines): - try: - src = os.path.join(self.inflowPath[cond], f"HighT{t+1}_inflow{str(self.allCases.sel(case=case).inflow_deg.values).replace('-','m')}deg") - dst = os.path.join(self.path,self.condDirList[cond], self.caseDirList[case], f'Seed_{seed}', LESboxesDirName, f'HighT{t+1}') - os.symlink(src, dst) - except FileExistsError: - print(f'Directory {dst} already exists. Skipping symlink.') + src = os.path.join(self.inflowPath[cond], f"HighT{t+1}_inflow{str(self.allCases.sel(case=case).inflow_deg.values).replace('-','m')}deg") + dst = os.path.join(seedPath, LESboxesDirName, f'HighT{t+1}') + self._symlink(src, dst) # Loops on all conditions/cases and cases for FAST.Farm for cond in range(self.nConditions): for case in range(self.nCases): for seed in range(seedsToKeep): - seedPath = os.path.join(self.path, self.condDirList[cond], self.caseDirList[case], f'Seed_{seed}') + seedPath = self.getCaseSeedPath(cond, case, seed) # Recover case properties D_ = self.allCases['D' ].max().values # Getting the maximum in case different turbines are present @@ -2251,7 +2800,7 @@ def _FF_setup_LES(self, seedsToKeep=1): zWT = self.allCases.sel(case=case)['Tz'].values # --------------- FAST.Farm ----------------- # - templateFSTF = os.path.join(self.templatePath, self.FFfilename) + templateFSTF = self.FFfilepath outputFSTF = os.path.join(seedPath, self.outputFFfilename) # Write the file (mostly for turbine locations here @@ -2295,8 +2844,12 @@ def _FF_setup_LES(self, seedsToKeep=1): ff_file['NumRadii'] = int(np.ceil(3*D_/(2*self.dr) + 1)) ff_file['NumPlanes'] = int(np.ceil( 20*D_/(self.dt_low*Vhub_*(1-1/6)) ) ) - # Ensure radii outputs are within [0, NumRadii-1] ff_file['OutRadii'] = [ff_file['OutRadii']] if isinstance(ff_file['OutRadii'],(float,int)) else ff_file['OutRadii'] + # If NOutRadii is 0 we find some default radii + if ff_file['NOutRadii']==0: + ff_file['OutRadii'] = defaultOutRadii(ff_file['dr'], ff_file['NumRadii'], self.D/2)[0] + ff_file['NOutRadii']= len(ff_file['OutRadii']) + # Ensure radii outputs are within [0, NumRadii-1] for i, r in enumerate(ff_file['OutRadii']): if r > ff_file['NumRadii']-1: ff_file['NOutRadii'] = i @@ -2341,7 +2894,7 @@ def _FF_setup_TS(self): for case in range(self.nCases): if self.verbose>0: print(f' Processing all {self.nSeeds} seeds of case {self.caseDirList[case]}', end='\r') for seed in range(self.nSeeds): - seedPath = os.path.join(self.path, self.condDirList[cond], self.caseDirList[case], f'Seed_{seed}') + seedPath = self.getCaseSeedPath(cond, case, seed) # Recover case properties D_ = self.allCases['D' ].max().values # Getting the maximum in case different turbines are present @@ -2360,19 +2913,23 @@ def _FF_setup_TS(self): # --------------- FAST.Farm ----------------- # - templateFSTF = os.path.join(self.templatePath, self.FFfilename) + 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')) + 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) # Write the file + if self.flat: + turbineTemplateFullFilename=f"{self.turbfilename}1.fst" + else: + turbineTemplateFullFilename=f"../{self.turbfilename}1.fst" writeFastFarm(outputFSTF, templateFSTF, xWT, yt, zWT, d, OutListT1=self.outlistFF, - noLeadingZero=True, turbineTemplateFullFilename=f"../{self.turbfilename}1.fst") + noLeadingZero=True, turbineTemplateFullFilename=turbineTemplateFullFilename) # Open saved file and change additional values manually or make sure we have the correct ones ff_file = FASTInputFile(outputFSTF) @@ -2403,8 +2960,13 @@ def _FF_setup_TS(self): ff_file['NumRadii'] = int(np.ceil(3*D_/(2*self.dr) + 1)) ff_file['NumPlanes'] = int(np.ceil( 20*D_/(self.dt_low*Vhub_*(1-1/6)) ) ) - # Ensure radii outputs are within [0, NumRadii-1] ff_file['OutRadii'] = [ff_file['OutRadii']] if isinstance(ff_file['OutRadii'],(float,int)) else ff_file['OutRadii'] + # If NOutRadii is 0 we find some default radii + if ff_file['NOutRadii']==0: + ff_file['OutRadii'] = defaultOutRadii(ff_file['dr'], ff_file['NumRadii'], self.D/2)[0] + ff_file['NOutRadii']= len(ff_file['OutRadii']) + + # Ensure radii outputs are within [0, NumRadii-1] for i, r in enumerate(ff_file['OutRadii']): if r > ff_file['NumRadii']-1: ff_file['NOutRadii'] = i @@ -2572,8 +3134,30 @@ def _getBoxesParamsForFF(self, lowbts, highbts, dt_low_desired, D, HubHt, xWT, y return d + def FF_batch_prepare(self, run=False, **kwargs): + """ Writes a flat batch file for FASTFarm cases""" + from openfast_toolbox.case_generation.runner import writeBatch + + ext = ".bat" if os.name == "nt" else ".sh" + batchfile = os.path.join(self.path, f'runAllFASTFarm{ext}') + + writeBatch(batchfile, self.FFFiles, fastExe=self.ffbin, **kwargs) + self.batchfile_ff = batchfile + + OK(f"Batch file written to {batchfile}") + + if run: + self.FF_batch_run() - def FF_slurm_prepare(self, slurmfilepath, inplace=True): + def FF_batch_run(self, showOutputs=True, showCommand=True, verbose=True, **kwargs): + from openfast_toolbox.case_generation.runner import runBatch + if not os.path.exists(self.batchfile_ff): + raise FFException(f'Batch file does not exist: {self.batchfile_ff}.\nMake sure you run FF_batch_prepare first.') + stat = runBatch(self.batchfile_ff, showOutputs=showOutputs, showCommand=showCommand, verbose=verbose, **kwargs) + if stat!=0: + raise FFException(f'Batch file failed: {self.batchfile_ff}') + + def FF_slurm_prepare(self, slurmfilepath, inplace=True, useSed=True): # ---------------------------------------------- # ----- Prepare SLURM script for FAST.Farm ----- # ------------- ONE SCRIPT PER CASE ------------ @@ -2584,6 +3168,8 @@ def FF_slurm_prepare(self, slurmfilepath, inplace=True): 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`.') + for cond in range(self.nConditions): for case in range(self.nCases): for seed in range(self.nSeeds): @@ -2617,7 +3203,6 @@ def FF_slurm_prepare(self, slurmfilepath, inplace=True): self.sed_inplace(sed_command, inplace) - def FF_slurm_submit(self, qos='normal', A=None, t=None, p=None, delay=4, inplace=True): # ---------------------------------- @@ -2723,7 +3308,7 @@ def loop_through_all_and_modify_file(self, file_to_modify, property_to_modify, v for cond in range(self.nConditions): for case in range(self.nCases): for seed in range(self.nSeeds): - ff_file = os.path.join(self.path, self.condDirList[cond], self.caseDirList[case], f'Seed_{seed}', file_to_modify) + ff_file = os.path.join(self.getCaseSeedPath(cond, case, seed), file_to_modify) if not os.path.exists(ff_file): raise ValueError(f'Method only applies to files inside seed directories. File {ff_file} not found.') modifyProperty(ff_file, property_to_modify, value) @@ -2732,15 +3317,19 @@ def save(self, dill_filename='ffcase_obj.dill'): ''' Save object to disk ''' - - import 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`') + return objpath = os.path.join(self.path, dill_filename) dill.dump(self, file=open(objpath, 'wb')) + OK('FAST.Farm case setup saved to file: {}'.format(objpath)) - def plot(self, figsize=(14,7), fontsize=13, saveFig=True, returnFig=False, figFormat='png', showTurbNumber=False, showLegend=True): + def plot(self, figsize=(14,7), fontsize=13, saveFig=False, returnFig=False, figFormat='png', showTurbNumber=False, showLegend=True): import matplotlib.pyplot as plt fig, ax = plt.subplots(figsize=figsize) @@ -2790,16 +3379,26 @@ def plot(self, figsize=(14,7), fontsize=13, saveFig=True, returnFig=False, figFo ax.plot([dst.x.values-(dst.D.values/2)*sind(yaw+phi), dst.x.values+(dst.D.values/2)*sind(yaw+phi)], [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 + #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 + #if grid: + # ax.vlines(x_high, ymin=y_high[0], ymax=y_high[-1], ls='--', lw=0.4, color=col(wt)) + # ax.hlines(y_high, xmin=x_high[0], xmax=x_high[-1], ls='--', lw=0.4, color=col(wt)) + + # plot convex hull of farm (or line) for given inflow turbs = self.wts_rot_ds.sel(inflow_deg=inflow)[['x','y']].to_array().transpose() try: from scipy.spatial import ConvexHull hull = ConvexHull(turbs) for simplex in hull.simplices: - ax.plot(turbs[simplex, 0], turbs[simplex, 1], 'gray', alpha=alphas[j], label=f'Inflow {inflow.values} deg') + ax.plot(turbs[simplex, 0], turbs[simplex, 1], 'gray', alpha=alphas[j], label=f'Turbine Envelop - Inflow {inflow.values} deg') except: # All turbines are in a line. Plotting a line instead of convex hull - ax.plot(turbs[:,0], turbs[:,1], 'gray', alpha=alphas[j], label=f'Inflow {inflow.values} deg') + ax.plot(turbs[:,0], turbs[:,1], 'gray', alpha=alphas[j], label=f'Turbine Line - Inflow {inflow.values} deg') # Remove duplicate entries from legend @@ -2827,3 +3426,45 @@ def plot(self, figsize=(14,7), fontsize=13, saveFig=True, returnFig=False, figFo + +if __name__ == '__main__': + from welib.essentials import * + # ----------------------------------------------------------------------------- + # --------------------------- Farm parameters --------------------------------- + # ----------------------------------------------------------------------------- + # ----------- General turbine parameters + cmax = 5 # Maximum blade chord (m) + fmax = 10/6 # Maximum excitation frequency (Hz) + Cmeander = 1.9 # Meandering constant (-) + D = 240 # Rotor diameter (m) + zhub = 150 # Hub height (m) + + # ----------- Wind farm + # The wts dictionary holds information of each wind turbine. The allowed entries + # are: x, y, z, D, zhub, cmax, fmax, Cmeander, and phi_deg. The phi_deg is the + # only entry that is optional and is related to floating platform heading angle, + # given in degrees. The angle phi_deg is not illustrated on the example below. + wts = { + 0 :{'x': 0.0, 'y': 0, 'z':0.0, 'D':D, 'zhub':zhub, 'cmax':cmax, 'fmax':fmax, 'Cmeander':Cmeander, 'name':'T1'}, + 1 :{'x': 5*D, 'y': 0, 'z':0.0, 'D':D, 'zhub':zhub, 'cmax':cmax, 'fmax':fmax, 'Cmeander':Cmeander, 'name':'T2'}, + } + # ----------------------------------------------------------------------------- + # ------------------- Inflow conditions and input files ----------------------- + # ----------------------------------------------------------------------------- + # ----------- Inflow parameters + inflowType = 'TS' # TS: TurbSim, or LES: LES (VTK files needs to exist) + + # ----------- Desired sweeps + vhub = [8] + + + ffcc = FFCaseCreation(wts=wts, vhub=vhub, inflowType=inflowType, verbose=1) + print(ffcc) + ffcc.plot() + import matplotlib.pyplot as plt + plt.show() + + + + + diff --git a/openfast_toolbox/fastfarm/TurbSimCaseCreation.py b/openfast_toolbox/fastfarm/TurbSimCaseCreation.py index 1cd36a2..e74ea71 100644 --- a/openfast_toolbox/fastfarm/TurbSimCaseCreation.py +++ b/openfast_toolbox/fastfarm/TurbSimCaseCreation.py @@ -1,4 +1,5 @@ import numpy as np +from openfast_toolbox.io import FASTInputFile class TSCaseCreation: @@ -216,16 +217,19 @@ def plotSetup(self, fig=None, ax=None): fig.tight_layout return fig, ax - def writeTSFile(self, fileIn, fileOut, NewFile=True, tpath=None, tmax=50, turb=None, verbose=0): + def writeTSFile(self, fileOut, fileIn=None, NewFile=False, tpath=None, tmax=50, turb=None, verbose=0): """ Write a TurbSim primary input file. See WriteTSFile below. """ - WriteTSFile(fileIn, fileOut, self, NewFile=NewFile, tpath=tpath, tmax=tmax, turb=turb, verbose=verbose) + if fileIn is None or fileIn in ['unused', '"unused"']: + NewFile = True + if verbose>0: print('[INFO] No TurbSim template provided, using a Dummy TurbSim file') + WriteTSFile(fileIn=fileIn, fileOut=fileOut, params=self, NewFile=NewFile, tpath=tpath, tmax=tmax, turb=turb, verbose=verbose) -def WriteTSFile(fileIn, fileOut, params, NewFile=True, tpath=None, tmax=50, turb=None, verbose=0): +def WriteTSFile(fileOut, fileIn, params, NewFile=True, tpath=None, tmax=50, turb=None, verbose=0): """ Write a TurbSim primary input file, @@ -240,6 +244,21 @@ def WriteTSFile(fileIn, fileOut, params, NewFile=True, tpath=None, tmax=50, turb Turbine number to be printed on the time series file. Only needed if boxType='highres' + params: + object with fields: + highres + lowres + nz + ny + dt + [tmax] + HubHt_for_TS + Height + Width + TI + RefHt + URef + PLexp """ if params.boxType=='highres' and not isinstance(turb, int): @@ -247,116 +266,203 @@ def WriteTSFile(fileIn, fileOut, params, NewFile=True, tpath=None, tmax=50, turb if params.boxType=='lowres' and turb is not None: print("WARNING: `turb` is not used when boxType is 'lowres'. Remove `turb` to dismiss this warning.") - if NewFile == True: - if verbose>1: print(f'Writing a new {fileOut} file from scratch') + if NewFile: + if verbose>1: print(f'[INFO] Writing a new {fileOut} file from scratch') + WriteDummyTSFile(fileOut) + fileIn=fileOut + # --- Writing FFarm input file from scratch - with open(fileOut, 'w') as f: - f.write(f'--------TurbSim v2.00.* Input File------------------------\n') - f.write(f'for Certification Test #1 (Kaimal Spectrum, formatted FF files).\n') - f.write(f'---------Runtime Options-----------------------------------\n') - f.write(f'False\tEcho\t\t- Echo input data to .ech (flag)\n') - f.write(f'123456\tRandSeed1\t\t- First random seed (-2147483648 to 2147483647)\n') - f.write(f'RanLux\tRandSeed2\t\t- Second random seed (-2147483648 to 2147483647) for intrinsic pRNG, or an alternative pRNG: "RanLux" or "RNSNLW"\n') - f.write(f'False\tWrBHHTP\t\t- Output hub-height turbulence parameters in binary form? (Generates RootName.bin)\n') - f.write(f'False\tWrFHHTP\t\t- Output hub-height turbulence parameters in formatted form? (Generates RootName.dat)\n') - f.write(f'False\tWrADHH\t\t- Output hub-height time-series data in AeroDyn form? (Generates RootName.hh)\n') - f.write(f'True\tWrADFF\t\t- Output full-field time-series data in TurbSim/AeroDyn form? (Generates RootName.bts)\n') - f.write(f'False\tWrBLFF\t\t- Output full-field time-series data in BLADED/AeroDyn form? (Generates RootName.wnd)\n') - f.write(f'False\tWrADTWR\t\t- Output tower time-series data? (Generates RootName.twr)\n') - f.write(f'False\tWrFMTFF\t\t- Output full-field time-series data in formatted (readable) form? (Generates RootName.u, RootName.v, RootName.w)\n') - f.write(f'False\tWrACT\t\t- Output coherent turbulence time steps in AeroDyn form? (Generates RootName.cts)\n') - f.write(f'True\tClockwise\t\t- Clockwise rotation looking downwind? (used only for full-field binary files - not necessary for AeroDyn)\n') - f.write(f'0\tScaleIEC\t\t- Scale IEC turbulence models to exact target standard deviation? [0=no additional scaling; 1=use hub scale uniformly; 2=use individual scales]\n') - f.write(f'\n') - f.write(f'--------Turbine/Model Specifications-----------------------\n') - f.write(f'{params.nz:.0f}\tNumGrid_Z\t\t- Vertical grid-point matrix dimension\n') - f.write(f'{params.ny:.0f}\tNumGrid_Y\t\t- Horizontal grid-point matrix dimension\n') - f.write(f'{params.dt:.6f}\tTimeStep\t\t- Time step [seconds]\n') - f.write(f'{tmax:.4f}\tAnalysisTime\t\t- Length of analysis time series [seconds] (program will add time if necessary: AnalysisTime = MAX(AnalysisTime, UsableTime+GridWidth/MeanHHWS) )\n') - f.write(f'"ALL"\tUsableTime\t\t- Usable length of output time series [seconds] (program will add GridWidth/MeanHHWS seconds unless UsableTime is "ALL")\n') - f.write(f'{params.HubHt_for_TS:.3f}\tHubHt\t\t- Hub height [m] (should be > 0.5*GridHeight)\n') - f.write(f'{params.Height:.3f}\tGridHeight\t\t- Grid height [m]\n') - f.write(f'{params.Width:.3f}\tGridWidth\t\t- Grid width [m] (should be >= 2*(RotorRadius+ShaftLength))\n') - f.write(f'0\tVFlowAng\t\t- Vertical mean flow (uptilt) angle [degrees]\n') - f.write(f'0\tHFlowAng\t\t- Horizontal mean flow (skew) angle [degrees]\n') - f.write(f'\n') - f.write(f'--------Meteorological Boundary Conditions-------------------\n') - if params.boxType=='lowres': - f.write(f'"IECKAI"\tTurbModel\t\t- Turbulence model ("IECKAI","IECVKM","GP_LLJ","NWTCUP","SMOOTH","WF_UPW","WF_07D","WF_14D","TIDAL","API","IECKAI","TIMESR", or "NONE")\n') - f.write(f'"unused"\tUserFile\t\t- Name of the file that contains inputs for user-defined spectra or time series inputs (used only for "IECKAI" and "TIMESR" models)\n') - elif params.boxType=='highres': - f.write(f'"TIMESR"\tTurbModel\t\t- Turbulence model ("IECKAI","IECVKM","GP_LLJ","NWTCUP","SMOOTH","WF_UPW","WF_07D","WF_14D","TIDAL","API","USRINP","TIMESR", or "NONE")\n') - f.write(f'"USRTimeSeries_T{turb}.txt"\tUserFile\t\t- Name of the file that contains inputs for user-defined spectra or time series inputs (used only for "IECKAI" and "TIMESR" models)\n') - else: - raise ValueError("boxType can only be 'lowres' or 'highres'. Stopping.") - f.write(f'1\tIECstandard\t\t- Number of IEC 61400-x standard (x=1,2, or 3 with optional 61400-1 edition number (i.e. "1-Ed2") )\n') - f.write(f'"{params.TI:.3f}\t"\tIECturbc\t\t- IEC turbulence characteristic ("A", "B", "C" or the turbulence intensity in percent) ("KHTEST" option with NWTCUP model, not used for other models)\n') - f.write(f'"NTM"\tIEC_WindType\t\t- IEC turbulence type ("NTM"=normal, "xETM"=extreme turbulence, "xEWM1"=extreme 1-year wind, "xEWM50"=extreme 50-year wind, where x=wind turbine class 1, 2, or 3)\n') - f.write(f'"default"\tETMc\t\t- IEC Extreme Turbulence Model "c" parameter [m/s]\n') - f.write(f'"PL"\tWindProfileType\t\t- Velocity profile type ("LOG";"PL"=power law;"JET";"H2L"=Log law for TIDAL model;"API";"PL";"TS";"IEC"=PL on rotor disk, LOG elsewhere; or "default")\n') - f.write(f'"unused"\tProfileFile\t\t- Name of the file that contains input profiles for WindProfileType="USR" and/or TurbModel="USRVKM" [-]\n') - f.write(f'{params.RefHt:.3f}\tRefHt\t\t- Height of the reference velocity (URef) [m]\n') - f.write(f'{params.URef:.3f}\tURef\t\t- Mean (total) velocity at the reference height [m/s] (or "default" for JET velocity profile) [must be 1-hr mean for API model; otherwise is the mean over AnalysisTime seconds]\n') - f.write(f'350\tZJetMax\t\t- Jet height [m] (used only for JET velocity profile, valid 70-490 m)\n') - f.write(f'"{params.PLexp:.3f}"\tPLExp\t\t- Power law exponent [-] (or "default")\n') - f.write(f'"default"\tZ0\t\t- Surface roughness length [m] (or "default")\n') - f.write(f'\n') - f.write(f'--------Non-IEC Meteorological Boundary Conditions------------\n') - f.write(f'"default"\tLatitude\t\t- Site latitude [degrees] (or "default")\n') - f.write(f'0.05\tRICH_NO\t\t- Gradient Richardson number [-]\n') - f.write(f'"default"\tUStar\t\t- Friction or shear velocity [m/s] (or "default")\n') - f.write(f'"default"\tZI\t\t- Mixing layer depth [m] (or "default")\n') - f.write(f'"default"\tPC_UW\t\t- Hub mean u\'w\' Reynolds stress [m^2/s^2] (or "default" or "none")\n') - f.write(f'"default"\tPC_UV\t\t- Hub mean u\'v\' Reynolds stress [m^2/s^2] (or "default" or "none")\n') - f.write(f'"default"\tPC_VW\t\t- Hub mean v\'w\' Reynolds stress [m^2/s^2] (or "default" or "none")\n') - f.write(f'\n') - f.write(f'--------Spatial Coherence Parameters----------------------------\n') - f.write(f'"IEC"\tSCMod1\t\t- u-component coherence model ("GENERAL","IEC","API","NONE", or "default")\n') - f.write(f'"IEC"\tSCMod2\t\t- v-component coherence model ("GENERAL","IEC","NONE", or "default")\n') - f.write(f'"IEC"\tSCMod3\t\t- w-component coherence model ("GENERAL","IEC","NONE", or "default")\n') - f.write(f'"12.0 {0.12/(8.1*42):.8f}"\tInCDec1\t- u-component coherence parameters for general or IEC models [-, m^-1] (e.g. "10.0 0.3e-3" in quotes) (or "default")\n') - f.write(f'"12.0 {0.12/(8.1*42):.8f}"\tInCDec2\t- v-component coherence parameters for general or IEC models [-, m^-1] (e.g. "10.0 0.3e-3" in quotes) (or "default")\n') - f.write(f'"12.0 {0.12/(8.1*42):.8f}"\tInCDec3\t- w-component coherence parameters for general or IEC models [-, m^-1] (e.g. "10.0 0.3e-3" in quotes) (or "default")\n') - f.write(f'"0.0"\tCohExp\t\t- Coherence exponent for general model [-] (or "default")\n') - f.write(f'\n') - f.write(f'--------Coherent Turbulence Scaling Parameters-------------------\n') - f.write(f'".\\EventData"\tCTEventPath\t\t- Name of the path where event data files are located\n') - f.write(f'"random"\tCTEventFile\t\t- Type of event files ("LES", "DNS", or "RANDOM")\n') - f.write(f'true\tRandomize\t\t- Randomize the disturbance scale and locations? (true/false)\n') - f.write(f'1\tDistScl\t\t- Disturbance scale [-] (ratio of event dataset height to rotor disk). (Ignored when Randomize = true.)\n') - f.write(f'0.5\tCTLy\t\t- Fractional location of tower centerline from right [-] (looking downwind) to left side of the dataset. (Ignored when Randomize = true.)\n') - f.write(f'0.5\tCTLz\t\t- Fractional location of hub height from the bottom of the dataset. [-] (Ignored when Randomize = true.)\n') - f.write(f'30\tCTStartTime\t\t- Minimum start time for coherent structures in RootName.cts [seconds]\n') - f.write(f'\n') - f.write(f'====================================================\n') - f.write(f'! NOTE: Do not add or remove any lines in this file!\n') - f.write(f'====================================================\n') - - else: - print(f'Modifying {fileIn} to be {fileOut}') - - NewPars = [int(params.nz), int(params.ny), int(params.dt), format(params.HubHt,'.2f'), format(params.Height,'.2f'), format(params.Width,'.2f'), format(params.TI,'.2f'), format(params.RefHt,'.2f'), format(params.URef,'.2f'), int(params.PLexp)] - ModVars = ['NumGrid_Z','NumGrid_Y','TimeStep','HubHt','GridHeight','GridWidth','IECturb','RefHt','URef','PLExp'] - wt=0 - with open(fileOut, 'w+') as new_file: - with open(fileIn) as old_file: - for line in old_file.readlines(): - newline = line - for index,tmpVar in enumerate(ModVars): - if tmpVar in line: - newline = str(NewPars[index])+'\t!!Orig is: '+line - if '.fst' in line: - newline =str('{params.x[wt]:.3f}\t\t{params.y[wt]:.3f}\t\t{params.z[wt]:.3f}\t\t{tpath}_WT{wt+1:d}.fst"\t{params.X0_High[wt]:.3f}\t\t{params.Y0_High[wt]:.3f}\t\t{params.Z0_High:.3f}\t\t{params.dX_High:.3f}\t\t{params.dY_High:.3f}\t\t{params.dZ_High:.3f}\n') - wt+=1 - new_file.write(newline) - - -def writeTimeSeriesFile(fileOut,yloc,zloc,u,v,w,time): +# with open(fileOut, 'w') as f: +# f.write( '--------TurbSim v2.00.* Input File------------------------\n') +# f.write( 'for Certification Test #1 (Kaimal Spectrum, formatted FF files).\n') +# f.write( '---------Runtime Options-----------------------------------\n') +# f.write( 'False\tEcho\t\t- Echo input data to .ech (flag)\n') +# f.write( '123456\tRandSeed1\t\t- First random seed (-2147483648 to 2147483647)\n') +# f.write( 'RanLux\tRandSeed2\t\t- Second random seed (-2147483648 to 2147483647) for intrinsic pRNG, or an alternative pRNG: "RanLux" or "RNSNLW"\n') +# f.write( 'False\tWrBHHTP\t\t- Output hub-height turbulence parameters in binary form? (Generates RootName.bin)\n') +# f.write( 'False\tWrFHHTP\t\t- Output hub-height turbulence parameters in formatted form? (Generates RootName.dat)\n') +# f.write( 'False\tWrADHH\t\t- Output hub-height time-series data in AeroDyn form? (Generates RootName.hh)\n') +# f.write( 'True\tWrADFF\t\t- Output full-field time-series data in TurbSim/AeroDyn form? (Generates RootName.bts)\n') +# f.write( 'False\tWrBLFF\t\t- Output full-field time-series data in BLADED/AeroDyn form? (Generates RootName.wnd)\n') +# f.write( 'False\tWrADTWR\t\t- Output tower time-series data? (Generates RootName.twr)\n') +# f.write( 'False\tWrHAWCFF\t\t- Output full-field time-series data in HAWC form? (Generates RootName-u.bin, RootName-v.bin, RootName-w.bin, RootName.hawc)\n') +# f.write( 'False\tWrFMTFF\t\t- Output full-field time-series data in formatted (readable) form? (Generates RootName.u, RootName.v, RootName.w)\n') +# f.write( 'False\tWrACT\t\t- Output coherent turbulence time steps in AeroDyn form? (Generates RootName.cts)\n') +# #f.write(f'True\tClockwise\t\t- Clockwise rotation looking downwind? (used only for full-field binary files - not necessary for AeroDyn)\n') +# f.write( '0\tScaleIEC\t\t- Scale IEC turbulence models to exact target standard deviation? [0=no additional scaling; 1=use hub scale uniformly; 2=use individual scales]\n') +# f.write( '\n') +# f.write( '--------Turbine/Model Specifications-----------------------\n') +# f.write(f'{params.nz:.0f}\tNumGrid_Z\t\t- Vertical grid-point matrix dimension\n') +# f.write(f'{params.ny:.0f}\tNumGrid_Y\t\t- Horizontal grid-point matrix dimension\n') +# f.write(f'{params.dt:.6f}\tTimeStep\t\t- Time step [seconds]\n') +# f.write(f'{tmax:.4f}\tAnalysisTime\t\t- Length of analysis time series [seconds] (program will add time if necessary: AnalysisTime = MAX(AnalysisTime, UsableTime+GridWidth/MeanHHWS) )\n') +# f.write( '"ALL"\tUsableTime\t\t- Usable length of output time series [seconds] (program will add GridWidth/MeanHHWS seconds unless UsableTime is "ALL")\n') +# f.write(f'{params.HubHt_for_TS:.3f}\tHubHt\t\t- Hub height [m] (should be > 0.5*GridHeight)\n') +# f.write(f'{params.Height:.3f}\tGridHeight\t\t- Grid height [m]\n') +# f.write(f'{params.Width:.3f}\tGridWidth\t\t- Grid width [m] (should be >= 2*(RotorRadius+ShaftLength))\n') +# f.write( '0\tVFlowAng\t\t- Vertical mean flow (uptilt) angle [degrees]\n') +# f.write( '0\tHFlowAng\t\t- Horizontal mean flow (skew) angle [degrees]\n') +# f.write( '\n') +# f.write( '--------Meteorological Boundary Conditions-------------------\n') +# if params.boxType=='lowres': +# f.write( '"IECKAI"\tTurbModel\t\t- Turbulence model ("IECKAI","IECVKM","GP_LLJ","NWTCUP","SMOOTH","WF_UPW","WF_07D","WF_14D","TIDAL","API","IECKAI","TIMESR", or "NONE")\n') +# f.write( '"unused"\tUserFile\t\t- Name of the file that contains inputs for user-defined spectra or time series inputs (used only for "IECKAI" and "TIMESR" models)\n') +# elif params.boxType=='highres': +# f.write( '"TIMESR"\tTurbModel\t\t- Turbulence model ("IECKAI","IECVKM","GP_LLJ","NWTCUP","SMOOTH","WF_UPW","WF_07D","WF_14D","TIDAL","API","USRINP","TIMESR", or "NONE")\n') +# f.write(f'"USRTimeSeries_T{turb}.txt"\tUserFile\t\t- Name of the file that contains inputs for user-defined spectra or time series inputs (used only for "IECKAI" and "TIMESR" models)\n') +# else: +# raise ValueError("boxType can only be 'lowres' or 'highres'. Stopping.") +# f.write( '1\tIECstandard\t\t- Number of IEC 61400-x standard (x=1,2, or 3 with optional 61400-1 edition number (i.e. "1-Ed2") )\n') +# f.write(f'"{params.TI:.3f}\t"\tIECturbc\t\t- IEC turbulence characteristic ("A", "B", "C" or the turbulence intensity in percent) ("KHTEST" option with NWTCUP model, not used for other models)\n') +# f.write( '"NTM"\tIEC_WindType\t\t- IEC turbulence type ("NTM"=normal, "xETM"=extreme turbulence, "xEWM1"=extreme 1-year wind, "xEWM50"=extreme 50-year wind, where x=wind turbine class 1, 2, or 3)\n') +# f.write( '"default"\tETMc\t\t- IEC Extreme Turbulence Model "c" parameter [m/s]\n') +# f.write( '"PL"\tWindProfileType\t\t- Velocity profile type ("LOG";"PL"=power law;"JET";"H2L"=Log law for TIDAL model;"API";"PL";"TS";"IEC"=PL on rotor disk, LOG elsewhere; or "default")\n') +# f.write( '"unused"\tProfileFile\t\t- Name of the file that contains input profiles for WindProfileType="USR" and/or TurbModel="USRVKM" [-]\n') +# f.write(f'{params.RefHt:.3f}\tRefHt\t\t- Height of the reference velocity (URef) [m]\n') +# f.write(f'{params.URef:.3f}\tURef\t\t- Mean (total) velocity at the reference height [m/s] (or "default" for JET velocity profile) [must be 1-hr mean for API model; otherwise is the mean over AnalysisTime seconds]\n') +# f.write( '350\tZJetMax\t\t- Jet height [m] (used only for JET velocity profile, valid 70-490 m)\n') +# f.write(f'"{params.PLexp:.3f}"\tPLExp\t\t- Power law exponent [-] (or "default")\n') +# f.write( '"default"\tZ0\t\t- Surface roughness length [m] (or "default")\n') +# f.write( '\n') +# f.write( '--------Non-IEC Meteorological Boundary Conditions------------\n') +# f.write( '"default"\tLatitude\t\t- Site latitude [degrees] (or "default")\n') +# f.write( '0.05\tRICH_NO\t\t- Gradient Richardson number [-]\n') +# f.write( '"default"\tUStar\t\t- Friction or shear velocity [m/s] (or "default")\n') +# f.write( '"default"\tZI\t\t- Mixing layer depth [m] (or "default")\n') +# f.write( '"default"\tPC_UW\t\t- Hub mean u\'w\' Reynolds stress [m^2/s^2] (or "default" or "none")\n') +# f.write( '"default"\tPC_UV\t\t- Hub mean u\'v\' Reynolds stress [m^2/s^2] (or "default" or "none")\n') +# f.write( '"default"\tPC_VW\t\t- Hub mean v\'w\' Reynolds stress [m^2/s^2] (or "default" or "none")\n') +# f.write( '\n') +# f.write( '--------Spatial Coherence Parameters----------------------------\n') +# f.write( '"IEC"\tSCMod1\t\t- u-component coherence model ("GENERAL","IEC","API","NONE", or "default")\n') +# f.write( '"IEC"\tSCMod2\t\t- v-component coherence model ("GENERAL","IEC","NONE", or "default")\n') +# f.write( '"IEC"\tSCMod3\t\t- w-component coherence model ("GENERAL","IEC","NONE", or "default")\n') +# f.write(f'"12.0 {0.12/(8.1*42):.8f}"\tInCDec1\t- u-component coherence parameters for general or IEC models [-, m^-1] (e.g. "10.0 0.3e-3" in quotes) (or "default")\n') +# f.write(f'"12.0 {0.12/(8.1*42):.8f}"\tInCDec2\t- v-component coherence parameters for general or IEC models [-, m^-1] (e.g. "10.0 0.3e-3" in quotes) (or "default")\n') +# f.write(f'"12.0 {0.12/(8.1*42):.8f}"\tInCDec3\t- w-component coherence parameters for general or IEC models [-, m^-1] (e.g. "10.0 0.3e-3" in quotes) (or "default")\n') +# f.write( '"0.0"\tCohExp\t\t- Coherence exponent for general model [-] (or "default")\n') +# f.write(f'\n') +# f.write(f'--------Coherent Turbulence Scaling Parameters-------------------\n') +# f.write( '".\\unused\t\t- Name of the path where event data files are located\n') +# f.write( '"random"\tCTEventFile\t\t- Type of event files ("LES", "DNS", or "RANDOM")\n') +# f.write( 'true\tRandomize\t\t- Randomize the disturbance scale and locations? (true/false)\n') +# f.write( '1\tDistScl\t\t- Disturbance scale [-] (ratio of event dataset height to rotor disk). (Ignored when Randomize = true.)\n') +# f.write( '0.5\tCTLy\t\t- Fractional location of tower centerline from right [-] (looking downwind) to left side of the dataset. (Ignored when Randomize = true.)\n') +# f.write( '0.5\tCTLz\t\t- Fractional location of hub height from the bottom of the dataset. [-] (Ignored when Randomize = true.)\n') +# f.write( '30\tCTStartTime\t\t- Minimum start time for coherent structures in RootName.cts [seconds]\n') +# f.write( '\n') +# f.write( '====================================================\n') +# f.write( '! NOTE: Do not add or remove any lines in this file!\n') +# f.write( '====================================================\n') + if verbose>1: print(f'[INFO] Modifying {fileIn} to be {fileOut}') + wt=0 + ts = FASTInputFile(fileIn) + ts['NumGrid_Z'] = int(params.nz) + ts['NumGrid_Y'] = int(params.ny) + ts['TimeStep'] = round(params.dt, 6) + ts['AnalysisTime'] = round(tmax, 4) + ts['HubHt'] = round(params.HubHt_for_TS, 3) + ts['GridHeight'] = round(params.Height, 3) + ts['GridWidth'] = round(params.Width , 3) + if params.boxType=='lowres': + ts['TurbModel'] = '"IECKAI"' + ts['UserFile'] = '"unused"' + elif params.boxType=='highres': + ts['TurbModel'] = '"TIMSR"' + ts['UserFile'] = f'"USRTimeSeries_T{turb}.txt"' + ts['IECTurbc'] = round(float(params.TI ) , 3) + ts['RefHt'] = round(float(params.RefHt) , 3) + ts['URef'] = round(float(params.URef ) , 3) + ts['PLExp'] = round(float(params.PLexp) , 3) + ts.write(fileOut) + + + +def WriteDummyTSFile(fileOut): + with open(fileOut, 'w') as f: + f.write('--------TurbSim v2.00.* Input File------------------------\n') + f.write('for Certification Test #1 (Kaimal Spectrum, formatted FF files).\n') + f.write('---------Runtime Options-----------------------------------\n') + f.write('False\tEcho\t\t- Echo input data to .ech (flag)\n') + f.write('123456\tRandSeed1\t\t- First random seed (-2147483648 to 2147483647)\n') + f.write('RanLux\tRandSeed2\t\t- Second random seed (-2147483648 to 2147483647) for intrinsic pRNG, or an alternative pRNG: "RanLux" or "RNSNLW"\n') + f.write('False\tWrBHHTP\t\t- Output hub-height turbulence parameters in binary form? (Generates RootName.bin)\n') + f.write('False\tWrFHHTP\t\t- Output hub-height turbulence parameters in formatted form? (Generates RootName.dat)\n') + f.write('False\tWrADHH\t\t- Output hub-height time-series data in AeroDyn form? (Generates RootName.hh)\n') + f.write('True\tWrADFF\t\t- Output full-field time-series data in TurbSim/AeroDyn form? (Generates RootName.bts)\n') + f.write('False\tWrBLFF\t\t- Output full-field time-series data in BLADED/AeroDyn form? (Generates RootName.wnd)\n') + f.write('False\tWrADTWR\t\t- Output tower time-series data? (Generates RootName.twr)\n') + f.write('False\tWrHAWCFF\t\t- Output full-field time-series data in HAWC form? (Generates RootName-u.bin, RootName-v.bin, RootName-w.bin, RootName.hawc)\n') + f.write('False\tWrFMTFF\t\t- Output full-field time-series data in formatted (readable) form? (Generates RootName.u, RootName.v, RootName.w)\n') + f.write('False\tWrACT\t\t- Output coherent turbulence time steps in AeroDyn form? (Generates RootName.cts)\n') + f.write('0\tScaleIEC\t\t- Scale IEC turbulence models to exact target standard deviation? [0=no additional scaling; 1=use hub scale uniformly; 2=use individual scales]\n') + f.write('\n') + f.write('--------Turbine/Model Specifications-----------------------\n') + f.write('10\tNumGrid_Z\t\t- Vertical grid-point matrix dimension\n') + f.write('10\tNumGrid_Y\t\t- Horizontal grid-point matrix dimension\n') + f.write('0.1\tTimeStep\t\t- Time step [seconds]\n') + f.write('50.0\tAnalysisTime\t\t- Length of analysis time series [seconds] (program will add time if necessary: AnalysisTime = MAX(AnalysisTime, UsableTime+GridWidth/MeanHHWS) )\n') + f.write('"ALL"\tUsableTime\t\t- Usable length of output time series [seconds] (program will add GridWidth/MeanHHWS seconds unless UsableTime is "ALL")\n') + f.write('100.0\tHubHt\t\t- Hub height [m] (should be > 0.5*GridHeight)\n') + f.write('50.0\tGridHeight\t\t- Grid height [m]\n') + f.write('50.0\tGridWidth\t\t- Grid width [m] (should be >= 2*(RotorRadius+ShaftLength))\n') + f.write('0\tVFlowAng\t\t- Vertical mean flow (uptilt) angle [degrees]\n') + f.write('0\tHFlowAng\t\t- Horizontal mean flow (skew) angle [degrees]\n') + f.write('\n') + f.write('--------Meteorological Boundary Conditions-------------------\n') + f.write('"IECKAI"\tTurbModel\t\t- Turbulence model ("IECKAI","IECVKM","GP_LLJ","NWTCUP","SMOOTH","WF_UPW","WF_07D","WF_14D","TIDAL","API","IECKAI","TIMESR", or "NONE")\n') + f.write('"unused"\tUserFile\t\t- Name of the file that contains inputs for user-defined spectra or time series inputs (used only for "IECKAI" and "TIMESR" models)\n') + f.write('1\tIECstandard\t\t- Number of IEC 61400-x standard (x=1,2, or 3 with optional 61400-1 edition number (i.e. "1-Ed2") )\n') + f.write('"A"\t\tIECturbc\t\t- IEC turbulence characteristic ("A", "B", "C" or the turbulence intensity in percent) ("KHTEST" option with NWTCUP model, not used for other models)\n') + f.write('"NTM"\tIEC_WindType\t\t- IEC turbulence type ("NTM"=normal, "xETM"=extreme turbulence, "xEWM1"=extreme 1-year wind, "xEWM50"=extreme 50-year wind, where x=wind turbine class 1, 2, or 3)\n') + f.write('"default"\tETMc\t\t- IEC Extreme Turbulence Model "c" parameter [m/s]\n') + f.write('"PL"\tWindProfileType\t\t- Velocity profile type ("LOG";"PL"=power law;"JET";"H2L"=Log law for TIDAL model;"API";"PL";"TS";"IEC"=PL on rotor disk, LOG elsewhere; or "default")\n') + f.write('"unused"\tProfileFile\t\t- Name of the file that contains input profiles for WindProfileType="USR" and/or TurbModel="USRVKM" [-]\n') + f.write('100\tRefHt\t\t- Height of the reference velocity (URef) [m]\n') + f.write('10.0\tURef\t\t- Mean (total) velocity at the reference height [m/s] (or "default" for JET velocity profile) [must be 1-hr mean for API model; otherwise is the mean over AnalysisTime seconds]\n') + f.write('350\tZJetMax\t\t- Jet height [m] (used only for JET velocity profile, valid 70-490 m)\n') + f.write('"0.2"\tPLExp\t\t- Power law exponent [-] (or "default")\n') + f.write('"default"\tZ0\t\t- Surface roughness length [m] (or "default")\n') + f.write('\n') + f.write('--------Non-IEC Meteorological Boundary Conditions------------\n') + f.write('"default"\tLatitude\t\t- Site latitude [degrees] (or "default")\n') + f.write('0.05\tRICH_NO\t\t- Gradient Richardson number [-]\n') + f.write('"default"\tUStar\t\t- Friction or shear velocity [m/s] (or "default")\n') + f.write('"default"\tZI\t\t- Mixing layer depth [m] (or "default")\n') + f.write('"default"\tPC_UW\t\t- Hub mean u\'w\' Reynolds stress [m^2/s^2] (or "default" or "none")\n') + f.write('"default"\tPC_UV\t\t- Hub mean u\'v\' Reynolds stress [m^2/s^2] (or "default" or "none")\n') + f.write('"default"\tPC_VW\t\t- Hub mean v\'w\' Reynolds stress [m^2/s^2] (or "default" or "none")\n') + f.write('\n') + f.write('--------Spatial Coherence Parameters----------------------------\n') + f.write('"IEC"\tSCMod1\t\t- u-component coherence model ("GENERAL","IEC","API","NONE", or "default")\n') + f.write('"IEC"\tSCMod2\t\t- v-component coherence model ("GENERAL","IEC","NONE", or "default")\n') + f.write('"IEC"\tSCMod3\t\t- w-component coherence model ("GENERAL","IEC","NONE", or "default")\n') + f.write(f'"12.0 {0.12/(8.1*42):.8f}"\tInCDec1\t- u-component coherence parameters for general or IEC models [-, m^-1] (e.g. "10.0 0.3e-3" in quotes) (or "default")\n') + f.write(f'"12.0 {0.12/(8.1*42):.8f}"\tInCDec2\t- v-component coherence parameters for general or IEC models [-, m^-1] (e.g. "10.0 0.3e-3" in quotes) (or "default")\n') + f.write(f'"12.0 {0.12/(8.1*42):.8f}"\tInCDec3\t- w-component coherence parameters for general or IEC models [-, m^-1] (e.g. "10.0 0.3e-3" in quotes) (or "default")\n') + f.write('"0.0"\tCohExp\t\t- Coherence exponent for general model [-] (or "default")\n') + f.write('\n') + f.write('--------Coherent Turbulence Scaling Parameters-------------------\n') + f.write('"unused"\tCTEventPath\t\t- Name of the path where event data files are located\n') + f.write('"random"\tCTEventFile\t\t- Type of event files ("LES", "DNS", or "RANDOM")\n') + f.write('true\tRandomize\t\t- Randomize the disturbance scale and locations? (true/false)\n') + f.write('1\tDistScl\t\t- Disturbance scale [-] (ratio of event dataset height to rotor disk). (Ignored when Randomize = true.)\n') + f.write('0.5\tCTLy\t\t- Fractional location of tower centerline from right [-] (looking downwind) to left side of the dataset. (Ignored when Randomize = true.)\n') + f.write('0.5\tCTLz\t\t- Fractional location of hub height from the bottom of the dataset. [-] (Ignored when Randomize = true.)\n') + f.write('30\tCTStartTime\t\t- Minimum start time for coherent structures in RootName.cts [seconds]\n') + f.write('\n') + f.write('====================================================\n') + f.write('! NOTE: Do not add or remove any lines in this file!\n') + f.write('====================================================\n') + + + +def writeTimeSeriesFile(fileOut,yloc,zloc,u,v,w,time, verbose=0): """ Write a TurbSim primary input file, """ - print(f'Writing {fileOut}') + if verbose>0: print(f'Writing {fileOut}') # --- Writing TurbSim user-defined time series file with open(fileOut, 'w') as f: f.write( '--------------TurbSim v2.00.* User Time Series Input File-----------------------\n') diff --git a/openfast_toolbox/fastfarm/examples/SampleFiles/FF_ForInitialDebug.fstf b/openfast_toolbox/fastfarm/examples/SampleFiles/FF_ForInitialDebug.fstf new file mode 100644 index 0000000..89e575d --- /dev/null +++ b/openfast_toolbox/fastfarm/examples/SampleFiles/FF_ForInitialDebug.fstf @@ -0,0 +1,115 @@ +------- FAST.Farm for OpenFAST INPUT FILE ------------------------------------------------- +Simulation with one turbine using precursor VTK +--- SIMULATION CONTROL --- +False Echo - Echo input data to .ech? (flag) +FATAL AbortLevel - Error level when simulation should abort (string) {"WARNING", "SEVERE", "FATAL"} +5.0 TMax - Total run time (s) [>=0.0] +2 Mod_AmbWind - Ambient wind model (-) (switch) {1: high-fidelity precursor in VTK format, 2: one InflowWind module, 3: multiple instances of InflowWind module} +2 Mod_WaveField - Wave field handling (-) (switch) {1: use individual HydroDyn inputs without adjustment, 2: adjust wave phases based on turbine offsets from farm origin} +0 Mod_SharedMooring - Shared mooring system model (switch) {0: None, 3=MoorDyn}} +--- SHARED MOORING SYSTEM --- [used only for Mod_SharedMoor>0] +"" SharedMoorFile - Name of file containing shared mooring system input parameters (quoted string) [used only when Mod_SharedMooring > 0] +0.04 DT_Mooring - Time step for farm-level mooring coupling with each turbine (s) [used only when Mod_SharedMooring > 0] +True WrMooringVis - Write shared mooring visualization, at the global FAST.Farm time step (-) [only used for Mod_SharedMooring=3] +--- AMBIENT WIND: PRECURSOR IN VTK FORMAT --- [used only for Mod_AmbWind=1] +3.0 DT_Low-VTK - Time step for low -resolution wind data input files; will be used as the global FAST.Farm time step (s) [>0.0] +0.1 DT_High-VTK - Time step for high-resolution wind data input files (s) [>0.0] +"unused" WindFilePath - Path name to VTK wind data files from precursor (string) +False ChkWndFiles - Check all the ambient wind files for data consistency? (flag) +--- AMBIENT WIND: INFLOWWIND MODULE --- [used only for Mod_AmbWind=2 or 3] +3.0 DT_Low - Time step for low -resolution wind data interpolation; will be used as the global FAST.Farm time step (s) [>0.0] +0.1 DT_High - Time step for high-resolution wind data interpolation (s) [>0.0] +2 NX_Low - Number of low -resolution spatial nodes in X direction for wind data interpolation (-) [>=2] +2 NY_Low - Number of low -resolution spatial nodes in Y direction for wind data interpolation (-) [>=2] +2 NZ_Low - Number of low -resolution spatial nodes in Z direction for wind data interpolation (-) [>=2] +-1000.0 X0_Low - Origin of low -resolution spatial nodes in X direction for wind data interpolation (m) +-1000.0 Y0_Low - Origin of low -resolution spatial nodes in Y direction for wind data interpolation (m) +0.0 Z0_Low - Origin of low -resolution spatial nodes in Z direction for wind data interpolation (m) +2000.0 dX_Low - Spacing of low -resolution spatial nodes in X direction for wind data interpolation (m) [>0.0] +2000.0 dY_Low - Spacing of low -resolution spatial nodes in Y direction for wind data interpolation (m) [>0.0] +800.0 dZ_Low - Spacing of low -resolution spatial nodes in Z direction for wind data interpolation (m) [>0.0] +2 NX_High - Number of high-resolution spatial nodes in X direction for wind data interpolation (-) [>=2] +2 NY_High - Number of high-resolution spatial nodes in Y direction for wind data interpolation (-) [>=2] +2 NZ_High - Number of high-resolution spatial nodes in Z direction for wind data interpolation (-) [>=2] +"IW_ForInitialDebug.dat" InflowFile - Name of file containing InflowWind module input parameters (quoted string) +--- WIND TURBINES --- +1 NumTurbines - Number of wind turbines (-) [>=1] [last 6 columns below used only for Mod_AmbWind=2 or 3] +WT_X WT_Y WT_Z WT_FASTInFile X0_High Y0_High Z0_High dX_High dY_High dZ_High +(m) (m) (m) (string) (m) (m) (m) (m) (m) (m) +0.0 0.0 0.0 "WT.T.fst" -500.0 -500.0 1.0 1000.0 1000.0 500.0 +--- WAKE DYNAMICS --- +1 Mod_Wake - Switch between wake formulations {1:Polar, 2:Curl, 3:Cartesian} (-) (switch) +240 RotorDiamRef - Reference turbine rotor diameter for wake calculations (m) [>0.0] +30.0 dr - Radial increment of radial finite-difference grid (m) [>0.0] +40 NumRadii - Number of radii in the radial finite-difference grid (-) [>=2] +140 NumPlanes - Number of wake planes (-) [>=2] +0.14 f_c - Cutoff (corner) frequency of the low-pass time-filter for the wake advection, deflection, and meandering model [recommended=1.28*U0/R] (Hz) [>0.0] or DEFAULT [DEFAULT=12.5/R, R estimated from dr and NumRadii, not recommended] +DEFAULT C_HWkDfl_O - Calibrated parameter in the correction for wake deflection defining the horizontal offset at the rotor (m ) or DEFAULT [DEFAULT= 0.0 ] +DEFAULT C_HWkDfl_OY - Calibrated parameter in the correction for wake deflection defining the horizontal offset at the rotor scaled with yaw error (m/deg) or DEFAULT [DEFAULT= 0.0 if Mod_Wake is 2, 0.3 otherwise] +DEFAULT C_HWkDfl_x - Calibrated parameter in the correction for wake deflection defining the horizontal offset scaled with downstream distance (- ) or DEFAULT [DEFAULT= 0.0 ] +DEFAULT C_HWkDfl_xY - Calibrated parameter in the correction for wake deflection defining the horizontal offset scaled with downstream distance and yaw error (1/deg) or DEFAULT [DEFAULT= 0.0 if Mod_Wake is 2, -0.004 otherwise] +DEFAULT C_NearWake - Calibrated parameter for the near-wake correction (-) [>1.0 and <2.5] or DEFAULT [DEFAULT=1.8] +DEFAULT k_vAmb - Calibrated parameters for the influence of the ambient turbulence in the eddy viscosity (set of 5 parameters: k, FMin, DMin, DMax, Exp) (-) [>=0.0, >=0.0 and <=1.0, >=0.0, >DMin, >=0.0] or DEFAULT [DEFAULT=0.05, 1.0, 0.0, 1.0, 0.01] +DEFAULT k_vShr - Calibrated parameters for the influence of the shear layer in the eddy viscosity (set of 5 parameters: k, FMin, DMin, DMax, Exp) (-) [>=0.0, >=0.0 and <=1.0, >=0.0, >DMin, >=0.0] or DEFAULT [DEFAULT=0.016, 0.2, 3.0, 25.0, 0.1] +DEFAULT Mod_WakeDiam - Wake diameter calculation model (-) (switch) {1: rotor diameter, 2: velocity based, 3: mass-flux based, 4: momentum-flux based} or DEFAULT [DEFAULT=1] +DEFAULT C_WakeDiam - Calibrated parameter for wake diameter calculation (-) [>0.0 and <0.99] or DEFAULT [DEFAULT=0.95] [unused for Mod_WakeDiam=1] +DEFAULT Mod_Meander - Spatial filter model for wake meandering (-) (switch) {1: uniform, 2: truncated jinc, 3: windowed jinc} or DEFAULT [DEFAULT=3] +DEFAULT C_Meander - Calibrated parameter for wake meandering (-) [>=1.0] or DEFAULT [DEFAULT=1.9] +--- CURLED-WAKE PARAMETERS [only used if Mod_Wake=2 or 3] --- +DEFAULT Swirl - Switch to include swirl velocities in wake (-) (switch) [DEFAULT=TRUE] +DEFAULT k_VortexDecay - Vortex decay constant for curl (-) [DEFAULT=0.0001] +DEFAULT NumVortices - The number of vortices in the curled wake model (-) [DEFAULT=100] +DEFAULT sigma_D - The width of the vortices in the curled wake model non-dimesionalized by rotor diameter (-) [DEFAULT=0.2] +DEFAULT FilterInit - Switch to filter the initial wake plane deficit and select the number of grid points for the filter {0: no filter, 1: filter of size 1} or DEFAULT [DEFAULT=1] [unused for Mod_Wake=1] (switch) +DEFAULT k_vCurl - Calibrated parameter for scaling the eddy viscosity in the curled-wake model (-) [>=0] or DEFAULT [DEFAULT=2.0 ] +DEFAULT Mod_Projection - Switch to select how the wake plane velocity is projected in AWAE {1: keep all components, 2: project against plane normal} or DEFAULT [DEFAULT=1: if Mod_Wake is 1 or 3, or DEFAULT=2: if Mod_Wake is 2] (switch) +--- WAKE-ADDED TURBULENCE --- +0 WAT - Switch between wake-added turbulence box options {0: no wake added turbulence, 1: predefined turbulence box, 2: user defined turbulence box} (switch) +"../WAT_MannBoxDB/FFDB_D100_512x512x64.u" WAT_BoxFile - Filepath to the file containing the u-component of the turbulence box (either predefined or user-defined) (quoted string) +512, 512, 64 WAT_NxNyNz - Number of points in the x, y, and z directions of the WAT_BoxFile [used only if WAT=2, derived value if WAT=1] (-) +5.0, 5.0, 5.0 WAT_DxDyDz - Distance (in meters) between points in the x, y, and z directions of the WAT_BoxFile [used only if WAT=2, derived value if WAT=1] (m) +default WAT_ScaleBox - Flag to scale the input turbulence box to zero mean and unit standard deviation at every node [DEFAULT=False] (flag) +default WAT_k_Def - Calibrated parameters for the influence of the maximum wake deficit on wake-added turbulence (set of 5 parameters: k_Def, FMin, DMin, DMax, Exp) (-) [>=0.0, >=0.0 and <=1.0, >=0.0, >DMin, >=0.0] or DEFAULT [DEFAULT=[0.6, 0.0, 0.0, 2.0, 1.0 ]] +default WAT_k_Grad - Calibrated parameters for the influence of the radial velocity gradient of the wake deficit on wake-added turbulence (set of 5 parameters: k_Grad, FMin, DMin, DMax, Exp) (-) [>=0.0, >=0.0 and <=1.0, >=0.0, >DMin, >=0.0] or DEFAULT [DEFAULT=[3.0, 0.0, 0.0, 12.0, 0.65] +--- VISUALIZATION --- +True WrDisWind - Write low- and high-resolution disturbed wind data to .Low.Dis.t.vtk etc.? (flag) +0 NOutDisWindXY - Number of XY planes for output of disturbed wind data across the low-resolution domain to .Low.DisXY.t.vtk (-) [0 to 999] +90.0 OutDisWindZ - Z coordinates of XY planes for output of disturbed wind data across the low-resolution domain (m) [1 to NOutDisWindXY] [unused for NOutDisWindXY=0] +0 NOutDisWindYZ - Number of YZ planes for output of disturbed wind data across the low-resolution domain to /Low.DisYZ.t.vtk (-) [0 to 999] +1126,1189 OutDisWindX - X coordinates of YZ planes for output of disturbed wind data across the low-resolution domain (m) [1 to NOutDisWindYZ] [unused for NOutDisWindYZ=0] +0 NOutDisWindXZ - Number of XZ planes for output of disturbed wind data across the low-resolution domain to /Low.DisXZ.t.vtk (-) [0 to 999] +1000.0 OutDisWindY - Y coordinates of XZ planes for output of disturbed wind data across the low-resolution domain (m) [1 to NOutDisWindXZ] [unused for NOutDisWindXZ=0] +3.0 WrDisDT - Time step for disturbed wind visualization output (s) [>0.0] or DEFAULT [DEFAULT=DT_Low or DT_Low-VTK] [unused for WrDisWind=False and NOutDisWindXY=NOutDisWindYZ=NOutDisWindXZ=0] +--- OUTPUT --- +False SumPrint - Print summary data to .sum? (flag) +99999.9 ChkptTime - Amount of time between creating checkpoint files for potential restart (s) [>0.0] +0.0 TStart - Time to begin tabular output (s) [>=0.0] +1 OutFileFmt - Format for tabular (time-marching) output file (switch) {1: text file [.out], 2: binary file [.outb], 3: both} +True TabDelim - Use tab delimiters in text tabular output file? (flag) {uses spaces if False} +"ES10.3E2" OutFmt - Format used for text tabular output, excluding the time channel. Resulting field should be 10 characters. (quoted string) +DEFAULT OutAllPlanes - Output all wake planes at all time steps. [DEFAULT=False] +7 NOutRadii - Number of radial nodes for wake output for an individual rotor (-) [0 to 20] +0, 2, 5, 11, 17, 21, 39 OutRadii - List of radial nodes for wake output for an individual rotor (-) [1 to NOutRadii] [unused for NOutRadii=0] +2 NOutDist - Number of downstream distances for wake output for an individual rotor (-) [0 to 9 ] +126.0, 189.0 OutDist - List of downstream distances for wake output for an individual rotor (m) [1 to NOutDist ] [unused for NOutDist =0] +1 NWindVel - Number of points for wind output (-) [0 to 9] +1000.0 WindVelX - List of coordinates in the X direction for wind output (m) [1 to NWindVel] [unused for NWindVel=0] +1000.0 WindVelY - List of coordinates in the Y direction for wind output (m) [1 to NWindVel] [unused for NWindVel=0] +90.0 WindVelZ - List of coordinates in the Z direction for wind output (m) [1 to NWindVel] [unused for NWindVel=0] + OutList - The next line(s) contains a list of output parameters. See OutListParameters.xlsx for a listing of available output channels (quoted string) +"W1VAmbx" +"RtVRelT1" +"YawErrT1" +"TIAmbT1" +"CtT1N02 , CtT1N03 , CtT1N04 , CtT1N05 , CtT1N06" +"WkAxsXT1D1 , WkAxsXT1D2" +"WkAxsYT1D1 , WkAxsYT1D2" +"WkAxsZT1D1 , WkAxsZT1D2" +"WkPosXT1D1 , WkPosXT1D2" +"WkPosYT1D1 , WkPosYT1D2" +"WkPosZT1D1 , WkPosZT1D2" +"WkDfVxT1N01D1, WkDfVxT1N02D1, WkDfVxT1N03D1, WkDfVxT1N04D1, WkDfVxT1N05D1, WkDfVxT1N06D1, WkDfVxT1N07D1" +"WkDfVxT1N01D2, WkDfVxT1N02D2, WkDfVxT1N03D2, WkDfVxT1N04D2, WkDfVxT1N05D2, WkDfVxT1N06D2, WkDfVxT1N07D2" +"WkDfVrT1N01D1, WkDfVrT1N02D1, WkDfVrT1N03D1, WkDfVrT1N04D1, WkDfVrT1N05D1, WkDfVrT1N06D1, WkDfVrT1N07D1" +"WkDfVrT1N01D2, WkDfVrT1N02D2, WkDfVrT1N03D2, WkDfVrT1N04D2, WkDfVrT1N05D2, WkDfVrT1N06D2, WkDfVrT1N07D2" +END of input file (the word "END" must appear in the first 3 columns of this last OutList line) diff --git a/openfast_toolbox/fastfarm/fastfarm.py b/openfast_toolbox/fastfarm/fastfarm.py index 28a8071..ffc3175 100644 --- a/openfast_toolbox/fastfarm/fastfarm.py +++ b/openfast_toolbox/fastfarm/fastfarm.py @@ -1,11 +1,17 @@ +import matplotlib.pyplot as plt import os import glob import numpy as np import pandas as pd + from openfast_toolbox.io.fast_input_file import FASTInputFile from openfast_toolbox.io.fast_output_file import FASTOutputFile +from openfast_toolbox.io.fast_input_deck import FASTInputDeck from openfast_toolbox.io.turbsim_file import TurbSimFile import openfast_toolbox.postpro as fastlib +from openfast_toolbox.tools.strings import INFO, FAIL, OK, WARN, print_bold +from openfast_toolbox.tools.grids import BoundingBox, RegularGrid +from openfast_toolbox.modules.elastodyn import rotor_disk_points # --------------------------------------------------------------------------------} # --- Small helper functions @@ -400,117 +406,222 @@ def setFastFarmOutputs(fastFarmFile, OutListT1): fst['OutList']=OutList fst.write(fastFarmFile) +def defaultOutRadii(dr, nr, R): + """ + Finds OutRadii with good resolution at root and tip + - dr = NumRadii + OUTPUTS: + - OutRadii + """ + r_plane = dr * np.arange(nr) # TODO, check + R = R*1.1 # Account for some expansion + R0 = 0; + R1 = R*0.25 + R2 = R*0.75 + R3 = R*1.5 + R4 = min(2.5*R, r_plane[-1]) + r1 = np.linspace(R0 , R1, 4) + r2 = np.linspace(r1[-1], R2, 6) + r3 = np.linspace(r2[-1], R3, 7) + r4 = np.linspace(r3[-1], R4, 4) + r_out = np.unique(np.concatenate((r1,r2,r3,r4))) + ir_out_all = np.unique(np.round(r_out/dr).astype(int)) + if len(ir_out_all)<20: + r1 = np.linspace(R0 , R1, 5) + r2 = np.linspace(r1[-1], R2, 7) + r3 = np.linspace(r2[-1], R3, 8) + r4 = np.linspace(r3[-1], R4, 5) + r_out = np.unique(np.concatenate((r1,r2,r3,r4))) + ir_out_all = np.unique(np.round(r_out/dr).astype(int)) + + ir_out = ir_out_all[:20] + ir_out[-1] = ir_out_all[-1] + r_out = ir_out * dr + ir_out +=1 # Fortran is 1 based + return list(ir_out), r_out + +def printWT(fstf): + """ Print the table of wind turbines within the FAST.Farm input file""" + from openfast_toolbox.tools.strings import prettyMat + print('Col: X Y Z FST X0_High Y0_High Z0_High dX_High dY_High dZ_High') + print('Id : 0'+''.join([f"{x:10}" for x in range(1,10)])) + WT = fstf['WindTurbines'].copy() + for iwt in range(len(WT)): + s= WT[iwt,3].strip('"') + s='/long/path/to/WT.fst' + if len(s)>10: + s = s[:4]+'[...]' + s='{:9s}'.format(s) + WT[iwt,3]= s + WT[iwt,3]= np.nan + print(prettyMat(WT, sindent=' ', center0=False, nchar=9, digits=4)) + #print('WindTurbine Array:\n', fstf['WindTurbines']) + + +def col(i): + """ Colors""" + Colrs=plt.rcParams['axes.prop_cycle'].by_key()['color'] + return Colrs[ np.mod(i,len(Colrs)) ] + + + +def plotFastFarmWTs(wts, fig=None, figsize=(13,5)): + if fig is None: + fig = plt.figure(figsize=figsize) + ax = fig.add_subplot(111,aspect="equal") + + Dmax = -100 + for iwt, (k,wt) in enumerate(wts.items()): + name = wt['name'] if 'name' in wt else "WT{}".format(iwt+1) + ax.plot(wt['x'], wt['y'], 'o', ms=8, mew=2, c=col(iwt), label=name) + if 'D' in wt: + Dmax = max(Dmax, wt['D']) + ax.plot([wt['x'], wt['x']], [wt['y']-wt['D']/2,wt['y']+wt['D']/2], '-', lw=2, c=col(iwt)) + xmin, xmax = ax.get_xlim() + ymin, ymax = ax.get_ylim() + + D = np.abs(Dmax) + ax.set_xlim(xmin - D, xmax + D) + ax.set_ylim(ymin - D, ymax + D) + ax.legend() + ax.grid(ls=':', lw=0.5) + ax.set_xlabel("x [m]") + ax.set_ylabel("y [m]") + fig.tight_layout -def plotFastFarmSetup(fastFarmFile, grid=True, fig=None, D=None, plane='XY', hubHeight=None, showLegend=True): - """ """ - import matplotlib.pyplot as plt + return fig - def col(i): - Colrs=plt.rcParams['axes.prop_cycle'].by_key()['color'] - return Colrs[ np.mod(i,len(Colrs)) ] - def boundingBox(x, y): - """ return x and y coordinates to form a box marked by the min and max of x and y""" - x_bound = [x[0],x[-1],x[-1],x[0] ,x[0]] - y_bound = [y[0],y[0] ,y[-1],y[-1],y[0]] - return x_bound, y_bound +def plotFastFarmSetup(ff, grid=True, fig=None, D=None, plane=None, hubHeight=None, showLegend=True, figsize=(13.5,8)): + """ + Plot a FAST.Farm setup. + INPUTS: + - ff: may have different type: + - a string indicating the fastFarm input file + - a FASTInputFile object, containing a FASTFarm file + - a dict of wts + - an object of FASTFarmCaseCreation + - plane: if None, plots a figure in all three planes, otherwise plane in ['XY','YZ','XY'] + """ + from openfast_toolbox.fastfarm.FASTFarmCaseCreation import FFCaseCreation + + # --- accept differnt kind of inputs + if isinstance(ff, str): + # --- Read FAST.Farm input file + fst=FASTInputFile(ff) + elif isinstance(ff, FASTInputFile): + fst = ff + elif isinstance(ff, FFCaseCreation): + ffcase = fastfarm_input + return + elif isinstance(ff, dict): + if 'x' in ff[list(ff.keys())[0]].keys(): + return plotFastFarmWTs(ff, figsize=figsize) + else: + raise NotImplementedError('Unsopported input type for argument ff') + + + parentDir = os.path.dirname(fst.filename) + + + # --- Getting geometry + WT = fst['WindTurbines'] + + # Creating low and high res grid objects + if fst['Mod_AmbWind'] in [2, 3]: + x_low = fst['X0_Low'] + np.arange(fst['NX_Low'])*fst['DX_Low'] + y_low = fst['Y0_Low'] + np.arange(fst['NY_Low'])*fst['DY_Low'] + z_low = fst['Z0_Low'] + np.arange(fst['NZ_Low'])*fst['DZ_Low'] + low = RegularGrid(x0=fst['X0_Low'], nx=fst['NX_Low'], dx=fst['DX_Low'], + y0=fst['Y0_Low'], ny=fst['NY_Low'], dy=fst['DY_Low'], + z0=fst['Z0_Low'], nz=fst['NZ_Low'], dz=fst['DZ_Low']) + X0_DX = WT[:,4:].astype(float) + high = [] + for iwt in range(len(WT)): + high.append(RegularGrid(x0=X0_DX[iwt,0], nx=fst['NX_High'], dx=X0_DX[iwt,3], + y0=X0_DX[iwt,1], ny=fst['NY_High'], dy=X0_DX[iwt,4], + z0=X0_DX[iwt,2], nz=fst['NZ_High'], dz=X0_DX[iwt,5])) + if not low.contains_grid(high[iwt]): + FAIL(f'plotFastFarmSetup: Bounding box of high-res grid for turbine {iwt+1} not fully contained in low-res grid.') + + # Getting turbine locations and disk points + disk_points = [None]*len(WT) + pWT = np.zeros((len(WT), 3)) + pHub = np.zeros((len(WT), 3)) + for iwt in range(len(WT)): + pWT[iwt] = [float(WT[iwt,0]), float(WT[iwt,1]), 0 ] # Turbine coordinates + + # --- See if we can get more information from the turbine + fstFile = os.path.join(parentDir, WT[iwt,3].strip('"')) + if not os.path.exists(fstFile): + WARN('Unable to read OpenFAST file {fstFile}, drawing will be approximate.') + try: + # Get dimensions and disk points from ElastoDyn + dck = FASTInputDeck(fstFile, readlist=['ED']) + ED = dck.fst_vt['ElastoDyn'] + pHub[iwt], disk_points[iwt] = rotor_disk_points(ED, nP=30, origin=pWT[iwt]) + bbTurb = BoundingBox(disk_points[iwt][0,:], disk_points[iwt][1,:], disk_points[iwt][2,:]) + if not high[iwt].contains_bb(bbTurb): + FAIL(f'plotFastFarmSetup: Bounding box of rotor for turbine {iwt+1} not fully contained in high-res grid') + except: + WARN('Unable to read ElastoDyn file, disk points will be approximate.') + if disk_points[iwt] is None: + if D is None and hubHeight is None: + WARN('hubHeight and D not provided, unable to draw rotor disk points') + D = np.nan + hubHeight = 0 + elif D is not None and hubHeight is None: + WARN('hubHeight is unknown, Assuming hubHeight=1.3 D.') + hubHeight=1.3*D + elif D is None and hubHeight is not None: + WARN('D is unknown, assuming D=0.7*hubHeight.') + D=0.7*hubHeight + theta = np.linspace(0,2*np.pi, 40) + pHub[iwt] = pWT[iwt] + pHub[iwt][2] += hubHeight + y = pHub[iwt][1] + D/2*np.cos(theta) + z = pHub[iwt][2] + D/2*np.sin(theta) + x = pHub[iwt][0] + y*0 + disk_points[iwt]=np.vstack([x,y,z]) + WARN('hubHeight is unknown, WT z position will be approximate.') + + # --- Plots + if plane is None: + planes = ['XY', 'XZ', 'YZ'] + else: + planes = [plane] + labels = ["x [m]", "y [m]", "z [m]"] + plane2I= {'XY':(0,1), 'YZ':(1,2), 'XZ':(0,2)} - # --- Read FAST.Farm input file - fst=FASTInputFile(fastFarmFile) + for plane in planes: + iX, iY = plane2I[plane] - if fig is None: - fig = plt.figure(figsize=(13.5,8)) + fig = plt.figure(figsize=figsize) ax = fig.add_subplot(111,aspect="equal") - WT=fst['WindTurbines'] - xWT = WT[:,0].astype(float) - yWT = WT[:,1].astype(float) - zWT = yWT*0 - if hubHeight is not None: - zWT += hubHeight - - if plane == 'XY': - pass - elif plane == 'XZ': - yWT = zWT - elif plane == 'YZ': - xWT = yWT - yWT = zWT - else: - raise Exception("Plane should be 'XY' 'XZ' or 'YZ'") - - if fst['Mod_AmbWind'] == 2: - x_low = fst['X0_Low'] + np.arange(fst['NX_Low']+1)*fst['DX_Low'] - y_low = fst['Y0_Low'] + np.arange(fst['NY_Low']+1)*fst['DY_Low'] - z_low = fst['Z0_Low'] + np.arange(fst['NZ_Low']+1)*fst['DZ_Low'] - if plane == 'XZ': - y_low = z_low - elif plane == 'YZ': - x_low = y_low - y_low = z_low - # Plot low-res box - x_bound_low, y_bound_low = boundingBox(x_low, y_low) - ax.plot(x_bound_low, y_bound_low ,'--k',lw=2,label='Low-res') - # Plot Low res grid lines - if grid: - ax.vlines(x_low, ymin=y_low[0], ymax=y_low[-1], ls='-', lw=0.3, color=(0.3,0.3,0.3)) - ax.hlines(y_low, xmin=x_low[0], xmax=x_low[-1], ls='-', lw=0.3, color=(0.3,0.3,0.3)) - - X0_High = WT[:,4].astype(float) - Y0_High = WT[:,5].astype(float) - Z0_High = WT[:,6].astype(float) - dX_High = WT[:,7].astype(float)[0] - dY_High = WT[:,8].astype(float)[0] - dZ_High = WT[:,9].astype(float)[0] - nX_High = fst['NX_High'] - nY_High = fst['NY_High'] - nZ_High = fst['NZ_High'] - - # high-res boxes - for wt in range(len(xWT)): - 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 - if plane == 'XZ': - y_high = z_high - elif plane == 'YZ': - x_high = y_high - y_high = z_high - - x_bound_high, y_bound_high = boundingBox(x_high, y_high) - ax.plot(x_bound_high, y_bound_high, '-', lw=2, c=col(wt)) - # Plot High res grid lines - if grid: - ax.vlines(x_high, ymin=y_high[0], ymax=y_high[-1], ls='--', lw=0.4, color=col(wt)) - ax.hlines(y_high, xmin=x_high[0], xmax=x_high[-1], ls='--', lw=0.4, color=col(wt)) - - # Plot turbines - for wt in range(len(xWT)): - ax.plot(xWT[wt], yWT[wt], 'x', ms=8, mew=2, c=col(wt),label="WT{}".format(wt+1)) - if plane=='XY' and D is not None: - ax.plot([xWT[wt],xWT[wt]], [yWT[wt]-D/2,yWT[wt]+D/2], '-', lw=2, c=col(wt)) - elif plane=='XZ' and D is not None and hubHeight is not None: - ax.plot([xWT[wt],xWT[wt]], [yWT[wt]-D/2,yWT[wt]+D/2], '-', lw=2, c=col(wt)) - elif plane=='YZ' and D is not None and hubHeight is not None: - theta = np.linspace(0,2*np.pi, 40) - x = xWT[wt] + D/2*np.cos(theta) - y = yWT[wt] + D/2*np.sin(theta) - ax.plot(x, y, '-', lw=2, c=col(wt)) - - #plt.legend(bbox_to_anchor=(1.05,1.015),frameon=False) - if showLegend: - ax.legend() - if plane=='XY': - ax.set_xlabel("x [m]") - ax.set_ylabel("y [m]") - elif plane=='XZ': - ax.set_xlabel("x [m]") - ax.set_ylabel("z [m]") - elif plane=='YZ': - ax.set_xlabel("y [m]") - ax.set_ylabel("z [m]") - fig.tight_layout - # fig.savefig('FFarmLayout.pdf',bbox_to_inches='tight',dpi=500) + # --- Plot Low and High res + if fst['Mod_AmbWind'] in [2, 3]: + # Plot low-res box + optsBB = dict(ls='--', lw=2, color='k', label='Low-res') + optsGd = dict(ls='-', lw=0.3, color=(0.3,0.3,0.3)) + low.plot(ax, plane=plane, grid=grid, optsBB=optsBB, optsGd=optsGd) + for iwt in range(len(WT)): + # high-res boxes + optsBB = dict(ls='-', lw=2 , color=col(iwt)) + optsGd = dict(ls='--', lw=0.4, color=col(iwt)) + high[iwt].plot(ax, plane=plane, grid=grid, optsBB=optsBB, optsGd=optsGd) + # --- Plot Turbine location and disc area + for iwt in range(len(WT)): + ax.plot(pWT[iwt][iX], pWT[iwt][iY], 'x', ms=8, mew=2, c=col(iwt),label="WT{}".format(iwt+1)) + ax.plot(pHub[iwt][iX], pHub[iwt][iY], 'o', ms=8, mew=2, c=col(iwt)) + if disk_points[iwt] is not None: + ax.fill(disk_points[iwt][iX, :], disk_points[iwt][iY, :], facecolor=col(iwt), alpha=0.3, edgecolor=col(iwt), linewidth=2) + ax.plot(disk_points[iwt][iX, :], disk_points[iwt][iY, :], '-', c=col(iwt), linewidth=2) + if showLegend: + ax.legend() + ax.set_xlabel(labels[iX]) + ax.set_ylabel(labels[iY]) + fig.tight_layout return fig @@ -654,3 +765,35 @@ def spanwisePostProFF(fastfarm_input,avgMethod='constantwindow',avgParam=30,D=1, dfDiam['i/n_[-]'] = np.arange(nDMax)/nDMax return dfRad, dfRadialTime, dfDiam +if __name__ == '__main__': + import numpy as np + import matplotlib.pyplot as plt + from mpl_toolkits.mplot3d import Axes3D + + #plotFastFarmSetup('../../template/FF.fstf') + + +# # --- Test --- +# # some arbitrary cloud of points +# np.random.seed(0) +# x = np.random.randn(30) +# y = np.random.randn(30) +# z = np.random.randn(30) +# +# # get bounding box line coords +# points = boundingBox_points_3D(x, y, z) +# +# # plot +# fig = plt.figure() +# ax = fig.add_subplot(111, projection='3d') +# +# # scatter points +# ax.scatter(x, y, z, c='b', marker='o') +# +# # bounding box edges +# ax.plot(points[:,0], points[:,1], points[:,2], 'r-', lw=2) +# +# ax.set_xlabel('X') +# ax.set_ylabel('Y') +# ax.set_zlabel('Z') +# plt.show() diff --git a/openfast_toolbox/fastfarm/postpro/ff_postpro.py b/openfast_toolbox/fastfarm/postpro/ff_postpro.py index bf7c7af..78e2b4c 100644 --- a/openfast_toolbox/fastfarm/postpro/ff_postpro.py +++ b/openfast_toolbox/fastfarm/postpro/ff_postpro.py @@ -13,6 +13,19 @@ def _get_fstf_filename(caseobj): else: return 'FFarm_mod' +def _getCasePath(caseobj, cond, case): + try: + return caseobj.getCasePath(cond, case) + except: + print('>>> get CasePath failed') + return os.path.join(caseobj.path, caseobj.condDirList[cond], caseobj.caseDirList[case]) + +def _getCaseSeedPath(caseobj, cond, case, seed): + try: + return caseobj.getCaseSeedPath(cond, case, seed) + except: + print('>>> get CaseSeedPath failed') + return os.path.join(caseobj.path, caseobj.condDirList[cond], caseobj.caseDirList[case], f'Seed_{seed}') def readTurbineOutputPar(caseobj, dt_openfast, dt_processing, saveOutput=True, output='zarr', iCondition=0, fCondition=-1, iCase=0, fCase=-1, iSeed=0, fSeed=-1, iTurbine=0, fTurbine=-1, @@ -190,8 +203,12 @@ def readTurbineOutput(caseobj, dt_openfast, dt_processing=1, saveOutput=True, ou # Read or process turbine output if os.path.isdir(outputzarr) or os.path.isfile(outputnc): # Data already processed. Reading output - if output == 'zarr': turbs = xr.open_zarr(outputzarr) - elif output == 'nc': turbs = xr.open_dataset(outputnc) + if output == 'zarr': + print(f'Output file {outputzarr} exists. Loading it.') + turbs = xr.open_zarr(outputzarr) + elif output == 'nc': + print(f'Output file {outputnc} exists. Loading it.') + turbs = xr.open_dataset(outputnc) else: print(f'{outfilename}.{output} does not exist. Reading output data...') # Processed data not saved. Reading it @@ -206,7 +223,7 @@ def readTurbineOutput(caseobj, dt_openfast, dt_processing=1, saveOutput=True, ou turbs_t=[] for t in np.arange(iTurbine, fTurbine, 1): print(f'Processing Condition {cond}, Case {case}, Seed {seed}, turbine {t+1}') - ff_file = os.path.join(caseobj.path, caseobj.condDirList[cond], caseobj.caseDirList[case], f'Seed_{seed}', f'{_get_fstf_filename(caseobj)}.T{t+1}.outb') + ff_file = os.path.join(_getCaseSeedPath(caseobj, cond, case, seed), f'{_get_fstf_filename(caseobj)}.T{t+1}.outb') df = FASTOutputFile(ff_file).toDataFrame() # Won't be able to send to xarray if columns are non-unique if not df.columns.is_unique: @@ -465,7 +482,7 @@ def readFFPlanes(caseobj, slicesToRead=['x','y','z'], verbose=False, saveOutput= for case in np.arange(iCase, fCase, 1): Slices_seed = [] for seed in np.arange(iSeed, fSeed, 1): - seedPath = os.path.join(caseobj.path, caseobj.condDirList[cond], caseobj.caseDirList[case], f'Seed_{seed}') + seedPath = _getCaseSeedPath(caseobj, cond, case, seed) # Read FAST.Farm input to determine outputs ff_file = FASTInputFile(os.path.join(seedPath,f'{_get_fstf_filename(caseobj)}.fstf')) diff --git a/openfast_toolbox/fastfarm/tests/test_turbsimExtent.py b/openfast_toolbox/fastfarm/tests/test_turbsimExtent.py index b47ad19..b1fab2e 100644 --- a/openfast_toolbox/fastfarm/tests/test_turbsimExtent.py +++ b/openfast_toolbox/fastfarm/tests/test_turbsimExtent.py @@ -54,7 +54,7 @@ def test_box_extent(self): np.testing.assert_almost_equal(FFTS['Y0_High'] , [-48 , 2 ], 5) # --- Write Fast Farm file with layout and Low and High res extent - templateFSTF = os.path.join(MyDir, '../examples/SampleFiles/TestCase.fstf') # template file used for FastFarm input file, need to exist + templateFSTF = os.path.join(MyDir, '../examples/SampleFiles/FF_ForInitialDebug.fstf') # template file used for FastFarm input file, need to exist outputFSTF = os.path.join(MyDir, '../examples/SampleFiles/_TestCase_mod.fstf') # new file that will be written writeFastFarm(outputFSTF, templateFSTF, xWT, yWT, zWT, FFTS=FFTS) #import matplotlib.pyplot as plt diff --git a/openfast_toolbox/io/examples/Example_EditOpenFASTModel.py b/openfast_toolbox/io/examples/Example_EditOpenFASTModel.py index aa61a1c..4d9be7c 100644 --- a/openfast_toolbox/io/examples/Example_EditOpenFASTModel.py +++ b/openfast_toolbox/io/examples/Example_EditOpenFASTModel.py @@ -30,7 +30,7 @@ print('> Hub radius: ',ED['HubRad']) print('> Tip radius: ',ED['TipRad']) print('> Hub mass: ',ED['HubMass']) -ED['TipRadius'] = 64 # Modifying the data +ED['TipRad'] = 64 # Modifying the data #ED.write('_NewFile.dat') # write a new file with modified data diff --git a/openfast_toolbox/io/fast_input_deck.py b/openfast_toolbox/io/fast_input_deck.py index 9228652..069202c 100644 --- a/openfast_toolbox/io/fast_input_deck.py +++ b/openfast_toolbox/io/fast_input_deck.py @@ -12,13 +12,17 @@ class FASTInputDeck(dict): """Container for input files that make up a FAST input deck""" + @property + def readlist_default(self): + return ['Fst','ED','SED','AD','ADdsk', 'BD','BDbld','EDtwr','EDbld','ADbld','AF','AC','OLAF','IW','HD','SeaSt','SrvD','SrvDdll','SrvDini', 'SD','MD'] + def __init__(self, fullFstPath='', readlist=['all'], verbose=False): """Read FAST master file and read inputs for FAST modules INPUTS: - fullFstPath: - readlist: list of module files to be read, or ['all'], modules are identified as follows: - ['Fst','ED','AD','BD','BDbld','EDtwr','EDbld','ADbld','AF','AC','IW','HD','SrvD','SD','MD'] + ['Fst','ED','AD','BD','BDbld','EDtwr','EDbld','ADbld','AF','AC','OLAF','IW','HD','SrvD','SD','MD'] where: AF: airfoil polars AC: airfoil coordinates (if present) @@ -36,7 +40,7 @@ def __init__(self, fullFstPath='', readlist=['all'], verbose=False): if not type(self.readlist) is list: self.readlist=[readlist] if 'all' in self.readlist: - self.readlist = ['Fst','ED','AD','BD','BDbld','EDtwr','EDbld','ADbld','AF','AC','IW','HD','SrvD','SD','MD'] + self.readlist = self.readlist_default else: self.readlist = ['Fst']+self.readlist @@ -89,9 +93,9 @@ def ED(self): return ED - def readAD(self, filename=None, readlist=None, verbose=False, key='AeroDyn15'): + def readAD(self, filename=None, readlist=None, verbose=False, key='AeroDyn15', key_short='AD'): """ - readlist: 'AD','AF','AC' + readlist: 'AD','AF','AC','OLAF' """ if readlist is not None: readlist_bkp = self.readlist @@ -99,7 +103,7 @@ def readAD(self, filename=None, readlist=None, verbose=False, key='AeroDyn15'): if not type(self.readlist) is list: self.readlist=[readlist] if 'all' in self.readlist: - self.readlist = ['Fst','ED','AD','BD','BDbld','EDtwr','EDbld','ADbld','AF','AC','IW','HD','SrvD','SD','MD'] + self.readlist = self.readlist_default if filename is None: filename = self.fst_vt['Fst']['AeroFile'] @@ -109,30 +113,49 @@ def readAD(self, filename=None, readlist=None, verbose=False, key='AeroDyn15'): self.verbose = verbose - self.fst_vt[key] = self._read(filename,'AD') + # AD + AD = self._read(filename, key_short) + self.fst_vt[key] = AD + + if AD is not None: + # ADbld - AeroDyn Blades + #bld_file = os.path.join(baseDir, AD['ADBlFile(1)']) + #self.fst_vt['AeroDynBlade'] = self._read(bld_file,'ADbld') + for i in range(10): + try: + AD['ADBlFile({})'.format(i+1)] + except KeyError: + nBlades = i + break + self.fst_vt['AeroDynBlade'] = [] + for i in range(nBlades): + bld_file = os.path.join(baseDir, self.fst_vt[key]['ADBlFile({})'.format(i+1)]) + self.fst_vt['AeroDynBlade'].append(self._read(bld_file,'ADbld')) + # OLAF + hasOLAF = False + if 'WakeMod' in AD: + hasOLAF = AD['Wake_Mod']==3 + elif 'WakeMod' in AD: + hasOLAF = AD['WakeMod']==3 + if hasOLAF: + self.fst_vt['OLAF'] = self._read(AD['OLAFInputFileName'], 'OLAF') - if self.fst_vt[key] is not None: - # Blades - bld_file = os.path.join(baseDir, self.fst_vt[key]['ADBlFile(1)']) - self.fst_vt['AeroDynBlade'] = self._read(bld_file,'ADbld') - #self.fst_vt['AeroDynBlade'] = [] - #for i in range(3): - # bld_file = os.path.join(os.path.dirname(self.fst_vt['Fst']['AeroFile']), self.fst_vt[key]['ADBlFile({})'.format(i+1)]) - # self.fst_vt['AeroDynBlade'].append(self._read(bld_file,'ADbld')) # Polars self.fst_vt['af_data']=[] # TODO add to "AeroDyn" for afi, af_filename in enumerate(self.fst_vt['AeroDyn15']['AFNames']): - af_filename = os.path.join(baseDir,af_filename).replace('"','') + af_filename = clean_path(os.path.join(baseDir, af_filename)) + # AF - Airfoil file try: polar = self._read(af_filename, 'AF') except: polar=None print('[FAIL] reading polar {}'.format(af_filename)) self.fst_vt['af_data'].append(polar) + # AC - Airfoil coordinates if polar is not None: coordFile = polar['NumCoords'] if isinstance(coordFile,str): - coordFile = coordFile.replace('"','') + coordFile = clean_path(coordFile) baseDirCoord=os.path.dirname(af_filename) if coordFile[0]=='@': ac_filename = os.path.join(baseDirCoord,coordFile[1:]) @@ -140,7 +163,7 @@ def readAD(self, filename=None, readlist=None, verbose=False, key='AeroDyn15'): self.fst_vt['ac_data'].append(coords) # --- Backward compatibility - self.AD = self.fst_vt[key] + self.AD = AD self.ADversion='AD15' if key=='AeroDyn15' else 'AD14' if readlist is not None: @@ -165,15 +188,15 @@ def inputFiles(self): def _relpath(self, k1, k2=None, k3=None): try: if k2 is None: - return self.fst_vt['Fst'][k1].replace('"','') + return clean_path(self.fst_vt['Fst'][k1]) else: - parent = os.path.dirname(self.fst_vt['Fst'][k1]).replace('"','') + parent = clean_path(os.path.dirname(self.fst_vt['Fst'][k1])) if type(k3)==list: for k in k3: if k in self.fst_vt[k2].keys(): - child = self.fst_vt[k2][k].replace('"','') + child = clean_path(self.fst_vt[k2][k]) else: - child = self.fst_vt[k2][k3].replace('"','') + child = clean_path(self.fst_vt[k2][k3]) return os.path.join(parent, child) except: return 'none' @@ -194,7 +217,7 @@ def ED_bld_path(self): return self._fullpath(self._relpath('EDFile','ElastoDyn', def _fullpath(self, relfilepath): - relfilepath = relfilepath.replace('"','') + relfilepath = clean_path(relfilepath) basename = os.path.basename(relfilepath) if basename.lower() in self.unusedNames: return 'none' @@ -227,7 +250,6 @@ def read(self, filename=None): else: self.version='F7' - if self.version=='AD_driver': # ---- AD Driver # InflowWind @@ -252,7 +274,10 @@ def read(self, filename=None): # ---- Regular OpenFAST file # ElastoDyn if 'EDFile' in self.fst_vt['Fst'].keys(): - self.fst_vt['ElastoDyn'] = self._read(self.fst_vt['Fst']['EDFile'],'ED') + if self.fst_vt['Fst']['CompElast']==3: + self.fst_vt['ElastoDyn'] = self._read(self.fst_vt['Fst']['EDFile'],'SED') + else: + self.fst_vt['ElastoDyn'] = self._read(self.fst_vt['Fst']['EDFile'],'ED') if self.fst_vt['ElastoDyn'] is not None: twr_file = self.ED_twr_path bld_file = self.ED_bld_path @@ -265,18 +290,34 @@ def read(self, filename=None): # AeroDyn if self.fst_vt['Fst']['CompAero']>0: - key = 'AeroDyn14' if self.fst_vt['Fst']['CompAero']==1 else 'AeroDyn15' - self.readAD(key=key, readlist=self.readlist) + # key = 'AeroDyn14' if self.fst_vt['Fst']['CompAero']==1 else 'AeroDyn15' + key = 'AeroDyn15' + key_short = 'AD' + if self.fst_vt['Fst']['CompAero']==1: + key_short = 'ADdsk' + self.readAD(key=key, readlist=self.readlist, key_short=key_short) # ServoDyn if self.fst_vt['Fst']['CompServo']>0: self.fst_vt['ServoDyn'] = self._read(self.fst_vt['Fst']['ServoFile'],'SrvD') - # TODO Discon + if self.fst_vt['ServoDyn'] is not None: + dll_file = clean_path(os.path.join(os.path.dirname(self.inputFilesRead['SrvD']), self.fst_vt['ServoDyn']['DLL_FileName'])) + ini_file = clean_path(os.path.join(os.path.dirname(self.inputFilesRead['SrvD']), self.fst_vt['ServoDyn']['DLL_InFile'])) + if 'SrvDdll' in self.readlist: + self.inputFilesRead['SrvDdll'] = dll_file + if 'SrvDini' in self.readlist: + self.inputFilesRead['SrvDini'] = ini_file + # TODO Actually read them... # HydroDyn - if self.fst_vt['Fst']['CompHydro']== 1: + if self.fst_vt['Fst']['CompHydro']>0: self.fst_vt['HydroDyn'] = self._read(self.fst_vt['Fst']['HydroFile'],'HD') + # SeaState + if 'CompSeaSt' in self.fst_vt['Fst']: + if self.fst_vt['Fst']['CompSeaSt']>0: + self.fst_vt['SeaState'] = self._read(self.fst_vt['Fst']['SeaStFile'],'SeaSt') + # SubDyn if self.fst_vt['Fst']['CompSub'] == 1: self.fst_vt['SubDyn'] = self._read(self.fst_vt['Fst']['SubFile'], 'SD') @@ -301,7 +342,7 @@ def read(self, filename=None): if not hasattr(self,'AD'): self.AD = None if self.AD is not None: - self.AD.Bld1 = self.fst_vt['AeroDynBlade'] + self.AD.Bld1 = self.fst_vt['AeroDynBlade'][0] self.AD.AF = self.fst_vt['af_data'] self.IW = self.fst_vt['InflowWind'] self.BD = self.fst_vt['BeamDyn'] @@ -314,7 +355,7 @@ def unusedNames(self): def _read(self, relfilepath, shortkey): """ read any openfast input """ - relfilepath =relfilepath.replace('"','') + relfilepath =clean_path(relfilepath) basename = os.path.basename(relfilepath) # Only read what the user requested to be read @@ -472,6 +513,11 @@ def __repr__(self): s+='\n' return s +def clean_path(path): + path = path.replace('"','') + path = path.replace("\\", "/") + return path + if __name__ == "__main__": fst=FASTInputDeck('NREL5MW.fst') print(fst) diff --git a/openfast_toolbox/io/fast_input_file.py b/openfast_toolbox/io/fast_input_file.py index 99446d2..31e8b71 100644 --- a/openfast_toolbox/io/fast_input_file.py +++ b/openfast_toolbox/io/fast_input_file.py @@ -669,7 +669,11 @@ def _read(self, IComment=None): else: nTabLines = self[d['tabDimVar']] #print('Reading table {} Dimension {} (based on {})'.format(d['label'],nTabLines,d['tabDimVar'])); - d['value'], d['tabColumnNames'], d['tabUnits'] = parseFASTNumTable(self.filename,lines[i:i+nTabLines+nHeaders], nTabLines, i, nHeaders, tableType=tab_type, varNumLines=d['tabDimVar']) + try: + d['value'], d['tabColumnNames'], d['tabUnits'] = parseFASTNumTable(self.filename,lines[i:i+nTabLines+nHeaders], nTabLines, i, nHeaders, tableType=tab_type, varNumLines=d['tabDimVar']) + except: + import pdb; pdb.set_trace() + _, d['descr'] = splitAfterChar(lines[i], '!') i += nTabLines+nHeaders-1 @@ -845,7 +849,7 @@ def mat_tostring(M,fmt='24.16e'): if d['isComment']: s+='{}'.format(d['value']) elif d['tabType']==TABTYPE_NOT_A_TAB: - if isinstance(d['value'], list): + if isinstance(d['value'], list) or isinstance(d['value'],np.ndarray): sList=', '.join([str(x) for x in d['value']]) s+=toStringVLD(sList, d['label'], d['descr']) else: @@ -981,6 +985,13 @@ def _toDataFrame(self): Cols = Cols + ShapeCols name=d['label'] + if 'AFCoeff' in name: + if '_' in name: + i = int(name.split('_')[1]) + Re = self['Re_'+str(i)] + else: + Re = self['Re'] + name = 'AFCoeff_Re{:.2f}'.format(Re) if name=='DampingCoeffs': pass @@ -1868,6 +1879,44 @@ def _toDataFrame(self): @property def _IComment(self): return [1] + def resample(self, n=10, r=None): + def multiInterp(x, xp, fp, extrap='bounded'): + """ See welib.tools.signal_analysis """ + x = np.asarray(x) + xp = np.asarray(xp) + j = np.searchsorted(xp, x, 'left') - 1 + dd = np.zeros(len(x)) #*np.nan + bOK = np.logical_and(j>=0, j< len(xp)-1) + jOK = j[bOK] + dd[bOK] = (x[bOK] - xp[jOK]) / (xp[jOK + 1] - xp[jOK]) + jBef=j + jAft=j+1 + bLower =j<0 + bUpper =j>=len(xp)-1 + jAft[bUpper] = len(xp)-1 + jBef[bUpper] = len(xp)-1 + jAft[bLower] = 0 + jBef[bLower] = 0 + return (1 - dd) * fp[:,jBef] + fp[:,jAft] * dd + # current data + M_old= self['BldAeroNodes'] + r_old = M_old[:,0] + if r is None: + r_new = np.linspace(r_old[0], r_old[-1], n) + else: + r_new = r + M_new = multiInterp(r_new, r_old, M_old.T).T + # Handling precision, but keeping first and last value + M_new = np.around(M_new, 5) + if r_new[0]==r_old[0]: + M_new[0,:] = M_old[0,:] + if r_new[-1]==r_old[-1]: + M_new[-1,:] = M_old[-1,:] + M_new[:,6] = np.around(M_new[:,6],0).astype(int) + self['BldAeroNodes']=M_new + self['NumBlNds']=len(r_new) + return self + # --------------------------------------------------------------------------------} # --- AeroDyn Polar @@ -1985,16 +2034,22 @@ def _write(self): self.data[i]['label'] = labFull def _toDataFrame(self): + # --- We rely on parent class, it has an if statement for AFCoeff already... dfs = FASTInputFileBase._toDataFrame(self) if not isinstance(dfs, dict): dfs={'AFCoeff':dfs} - for k,df in dfs.items(): + # --- Adding more columns + for i,(k,df) in enumerate(dfs.items()): sp = k.split('_') - if len(sp)==2: - labOffset='_'+sp[1] + if len(dfs)>1: + labOffset='_'+str(i+1) else: labOffset='' + #if len(sp)==2: + # labOffset='_'+sp[1] + #else: + # labOffset='' alpha = df['Alpha_[deg]'].values*np.pi/180. Cl = df['Cl_[-]'].values Cd = df['Cd_[-]'].values @@ -2066,6 +2121,81 @@ def _IComment(self): return I + # --- Helper functions + @property + def reynolds(self): + return self.getAll('re') + + def getPolar(self, i): + if i not in range(0, self['NumTabs']): + raise IndexError('Index {} for Polar should be between 0 and {}'.format(i, self['NumTabs']-1)) + if self['NumTabs']==1: + return self[f'AFCoeff'].copy() + else: + return self[f'AFCoeff_{i+1}'].copy() + + def setPolar(self, i, M): + if i not in range(0, self['NumTabs']): + raise IndexError('Index {} for Polar should be between 0 and {}'.format(i, self['NumTabs']-1)) + if self['NumTabs']==1: + M_old = self[f'AFCoeff'] + else: + M_old = self[f'AFCoeff_{i+1}'] + if M_old.shape[1] != M.shape[1]: + # Actually, does it? + raise Exception('Number of columns must match previous data when setting a polar') + #self[f'AFCoeff_{i+1}']=M + if self['NumTabs']==1: + self[f'AFCoeff']=M + else: + ID = self.getID(f'AFCoeff_{i+1}') + self[f'NumAlf_{i+1}']=M.shape[0] + self.data[ID]['value']=M + + + def getAll(self, key): + """ + Examples: + pol.getAll('re') + """ + if self['NumTabs']==1: + return np.array([self[key]]) + else: + return np.array([self[key + f'_{i}'] for i in range(1, self['NumTabs']+1)]) + + def setAll(self, key, value=None, offset=None): + """ + Examples: + pol.setAll('T_f0', 6 ) + pol.setAll('alpha1', offset=+2 ) + pol.setAll('alpha2', offset=-2 ) + """ + if self['NumTabs']==1: + if value is not None: + self[f'{key}'] = value + if offset is not None: + self[f'{key}'] += offset + else: + for i in range(1, self['NumTabs']+1): + ID = self.getID(f'{key}_{i}') + if value is not None: + self.data[ID]['value'] = value + if offset is not None: + self.data[ID]['value']+= offset + + def calcUnsteadyParams(self): + from welib.airfoils.Polar import Polar + for i in range(1, self['NumTabs']+1): + offset=f'_{i}' if self['NumTabs']>1 else '' + M = self['AFCoeff'+offset] + pol = Polar(alpha=M[:,0], cl=M[:,1], cd=M[:,2], cm=M[:,3], radians=False, name=os.path.basename(self.filename)+f'_Table{i}') + d = pol.unsteadyParams(dictOut=True) + for k,v in d.items(): + self[k+offset] = v + + + + # --------------------------------------------------------------------------------} # --- ExtPtfm # --------------------------------------------------------------------------------{ diff --git a/openfast_toolbox/io/rosco_discon_file.py b/openfast_toolbox/io/rosco_discon_file.py index b7f2891..d17f34b 100644 --- a/openfast_toolbox/io/rosco_discon_file.py +++ b/openfast_toolbox/io/rosco_discon_file.py @@ -136,14 +136,14 @@ def toString(self): FMTs['{:<4.4f}']=['WE_FOPoles_v'] FMTs['{:<10.8f}']=['WE_FOPoles'] FMTs['{:<10.3f}']=['PS_BldPitchMin'] - FMTs['{:<7.0f}']=['PerfTableSize'] + FMTs['{:<7.0f}']=['PerfTableSize','Ind_BldPitch'] fmtFloat='{:<014.5f}' for fmt,keys in FMTs.items(): if param in keys: fmtFloat=fmt break if type(v) is str: - sval='"{:15s}" '.format(v) + sval='{:15s} '.format('"'+v+'"') elif hasattr(v, '__len__'): if isinstance(v[0], (np.floating, float)): sval=' '.join([fmtFloat.format(vi) for vi in v] )+' ' diff --git a/openfast_toolbox/io/tests/example_files/FASTIn_Blade.dat b/openfast_toolbox/io/tests/example_files/FASTIn_Blade.dat index 2cb5efe..77cb06e 100644 --- a/openfast_toolbox/io/tests/example_files/FASTIn_Blade.dat +++ b/openfast_toolbox/io/tests/example_files/FASTIn_Blade.dat @@ -1,7 +1,7 @@ ------- AERODYN v15.00.* BLADE DEFINITION INPUT FILE ------------------------------------- SNL SWiFT V27 baseline aerodynamic blade input properties ====== Blade Properties ================================================================= -22 20 NumBlNds - Number of blade nodes used in the analysis (-) +22 NumBlNds - Number of blade nodes used in the analysis (-) BlSpn BlCrvAC BlSwpAC BlCrvAng BlTwist BlChord BlAFID (m) (m) (m) (deg) (deg) (m) (-) 0.0 0.0 0.0 0.0 13.9573 0.589 1 diff --git a/openfast_toolbox/io/tests/test_fast_input.py b/openfast_toolbox/io/tests/test_fast_input.py index 21be156..7b26a92 100644 --- a/openfast_toolbox/io/tests/test_fast_input.py +++ b/openfast_toolbox/io/tests/test_fast_input.py @@ -110,10 +110,10 @@ def test_FASTADPolMulti(self): F.test_ascii(bCompareWritesOnly=False,bDelete=True) dfs = F.toDataFrame() - self.assertTrue('AFCoeff_2' in dfs.keys()) + self.assertTrue('AFCoeff_Re0.06' in dfs.keys()) - df1 = dfs['AFCoeff_1'] - df2 = dfs['AFCoeff_2'] + df1 = dfs['AFCoeff_Re0.05'] + df2 = dfs['AFCoeff_Re0.06'] self.assertTrue('Cn_pot_[-]' in df2.keys()) self.assertEqual(df1.shape[0],23) diff --git a/openfast_toolbox/modules/elastodyn.py b/openfast_toolbox/modules/elastodyn.py new file mode 100644 index 0000000..6da35a9 --- /dev/null +++ b/openfast_toolbox/modules/elastodyn.py @@ -0,0 +1,2131 @@ +""" + +COORDINATE SYSTEMS: + NOTE: xIEC, yIEC, zIEC = xED, -zED, yED + + | ElastoDyn | IEC | + | z* | i | inertial + | a* | t | tower + | t* | te | tower elements + | b* | p | tower top + | d* | n | nacelle (including yaw) + | c* | s | non rotating shaft + | e* | a | rotating shaft + | f* | | teetered + | g* | h | hub (including delta 3 for 2-bladed + | g*' | | hub coordinate aligned with a given blade (rotated with azimuth between blade) + | i* | cK | coned for blade K (rotatged with cone angle) + | j* | bK | pitched for blade K (rotated with pitch angle) + | Lj* | | (rotated with structural twist) + + +BODIES: + | ElastoDyn | IEC | + | E | E | : earth/inertial frame + | X | F | : platform body + | N | N | : nacelle body + | A | | : tail-furl body + +POINTS: + | ElastoDyn | IEC | + | Z | F | : platform reference + | Y | Gf | : platform COG + | T0 | T | : tower-bottom + | O | N | : tower-top / base-plate + | U | Gn | : Nacelle COG + | V | | : Rotor furl axis + | D | | : COG of structure that furls (excluding the rotor) + | IMU | | : Nacelle IMU + | P | | : Teeter pin + | Q | | : Apex of rotation, rotor center + | C | Gh | : Hub COG + | W | | : specified point on the tail-furl axis + | I | | : tail boom COG + | J | | : tail fin COG + + +""" + +import numpy as np +import os +import matplotlib.pyplot as plt +from openfast_toolbox.io.fast_input_file import FASTInputFile +from openfast_toolbox.tools.eva import eigMCK + +# --------------------------------------------------------------------------------} +# --- GLOBAL CONSTANTS +# --------------------------------------------------------------------------------{ +# INTEGER(IntKi), PARAMETER :: MaxBl = 3 ! Maximum number of blades allowed in simulation +# INTEGER(IntKi), PARAMETER :: NumBE = 1 ! Number of blade-edge modes +# INTEGER(IntKi), PARAMETER :: NumBF = 2 ! Number of blade-flap modes +DOF_Sg = 0 # DOF index for platform surge +DOF_Sw = 1 # DOF index for platform sway +DOF_Hv = 2 # DOF index for platform heave +DOF_R = 3 # DOF index for platform roll +DOF_P = 4 # DOF index for platform pitch +DOF_Y = 5 # DOF index for platform yaw +DOF_TFA1 = 6 # DOF index for 1st tower fore-aft mode +DOF_TSS1 = 7 # DOF index for 1st tower side-to-side mode +DOF_TFA2 = 8 # DOF index for 2nd tower fore-aft mode +DOF_TSS2 = 9 # DOF index for 2nd tower side-to-side mode +DOF_Yaw = 10 # DOF index for nacelle-yaw +DOF_RFrl = 11 # DOF index for rotor-furl +DOF_GeAz = 12 # DOF index for the generator azimuth +DOF_DrTr = 13 # DOF index for drivetrain rotational-flexibility +DOF_TFrl = 14 # DOF index for tail-furl +# INTEGER(IntKi), PARAMETER :: DOF_BE (MaxBl,NumBE) = RESHAPE( & ! DOF indices for blade edge: +# (/ 17, 20, 23 /), (/MaxBl,NumBE/) ) ! 1st blade edge mode for blades 1,2, and 3, respectively 17 + 3*(K-1) +# INTEGER(IntKi), PARAMETER :: DOF_BF (MaxBl,NumBF) = RESHAPE( & ! DOF indices for blade flap: +# (/ 16, 19, 22, & ! 1st blade flap mode for blades 1,2, and 3, respectively 16 + 3*(K-1) +# 18, 21, 24 /), (/MaxBl,NumBF/) ) ! 2nd blade flap mode for blades 1,2, and 3, respectively 18 + 3*(K-1) +# INTEGER(IntKi), PARAMETER :: DOF_Teet = 22 !DOF_TFrl + 2*(NumBE+NumBF)+ 1 ! DOF index for rotor-teeter +ED_MaxDOFs = 24 +# INTEGER(IntKi), PARAMETER :: NPA = 9 ! Number of DOFs that contribute to the angular velocity of the tail (body A) in the inertia frame. +# INTEGER(IntKi), PARAMETER :: NPB = 7 ! Number of DOFs that contribute to the angular velocity of the tower top / baseplate (body B) in the inertia frame. +# INTEGER(IntKi), PARAMETER :: NPF = 7 ! Number of DOFs that contribute to the angular velocity of the tower elements (body F) in the inertia frame (body F) in the inertia frame. +# INTEGER(IntKi), PARAMETER :: NPG = 10 ! Number of DOFs that contribute to the angular velocity of the generator (body G) in the inertia frame. +# INTEGER(IntKi), PARAMETER :: NPL = 11 ! Number of DOFs that contribute to the angular velocity of the low-speed shaft (body L) in the inertia frame. +NPN = 8 # Number of DOFs that contribute to the angular velocity of the nacelle (body N) in the inertia frame. +# INTEGER(IntKi), PARAMETER :: NPR = 9 ! Number of DOFs that contribute to the angular velocity of the structure that furls with the rotor (not including rotor) (body R) in the inertia frame. +NPX = 3 # Number of DOFs that contribute to the angular velocity of the platform (body X) in the inertia frame. +PX = [ DOF_R, DOF_P, DOF_Y ] # Array of DOF indices (pointers) that contribute to the angular velocity of the platform (body X) in the inertia frame. +# INTEGER(IntKi), PARAMETER :: PF(NPF) = (/ DOF_R, DOF_P, DOF_Y, DOF_TFA1, DOF_TSS1, DOF_TFA2, DOF_TSS2 /) ! Array of DOF indices (pointers) that contribute to the angular velocity of the tower elements (body F) in the inertia frame. +# INTEGER(IntKi), PARAMETER :: PB(NPB) = (/ DOF_R, DOF_P, DOF_Y, DOF_TFA1, DOF_TSS1, DOF_TFA2, DOF_TSS2 /) ! Array of DOF indices (pointers) that contribute to the angular velocity of the tower top / baseplate (body B) in the inertia frame. +PN = [ DOF_R, DOF_P, DOF_Y, DOF_TFA1, DOF_TSS1, DOF_TFA2, DOF_TSS2, DOF_Yaw ] # Array of DOF indices (pointers) that contribute to the angular velocity of the nacelle (body N) in the inertia frame. +# INTEGER(IntKi), PARAMETER :: PR(NPR) = (/ DOF_R, DOF_P, DOF_Y, DOF_TFA1, DOF_TSS1, DOF_TFA2, DOF_TSS2, DOF_Yaw, DOF_RFrl /) ! Array of DOF indices (pointers) that contribute to the angular velocity of the structure that furls with the rotor (not including rotor) (body R) in the inertia frame. +# INTEGER(IntKi), PARAMETER :: PL(NPL) = (/ DOF_R, DOF_P, DOF_Y, DOF_TFA1, DOF_TSS1, DOF_TFA2, DOF_TSS2, DOF_Yaw, DOF_RFrl, DOF_GeAz, DOF_DrTr /) ! Array of DOF indices (pointers) that contribute to the angular velocity of the low-speed shaft (body L) in the inertia frame. +# INTEGER(IntKi), PARAMETER :: PG(NPG) = (/ DOF_R, DOF_P, DOF_Y, DOF_TFA1, DOF_TSS1, DOF_TFA2, DOF_TSS2, DOF_Yaw, DOF_RFrl, DOF_GeAz /) ! Array of DOF indices (pointers) that contribute to the angular velocity of the generator (body G) in the inertia frame. +# INTEGER(IntKi), PARAMETER :: PA(NPA) = (/ DOF_R, DOF_P, DOF_Y, DOF_TFA1, DOF_TSS1, DOF_TFA2, DOF_TSS2, DOF_Yaw, DOF_TFrl /) ! Array of DOF indices (pointers) that contribute to the angular velocity of the tail (body A) in the inertia frame. + + + + + +# --------------------------------------------------------------------------------} +# --- Simple Geometry functions +# --------------------------------------------------------------------------------{ + +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 + c,s=np.cos(theta),np.sin(theta) + 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; + return R + +def orth_vect(u): + """ Given one vector, returns a 3x1 orthonormal vector to it""" + u=(u/np.linalg.norm(u)).ravel() + if abs(u[0])>=1/np.sqrt(3): + v = np.array([[-u[1]],[u[0]] ,[0]] ) + elif abs(u[1])>=1/np.sqrt(3): + v = np.array([[0] ,[-u[2]],[u[1]]] ) + elif abs(u[2])>=1/np.sqrt(3): + v = np.array([[u[2]] ,[0] ,[-u[0]]]) + else: + raise Exception('Cannot find orthogonal vector to a zero vector: {} '.format(u)) + return v/np.linalg.norm(v) + +def rotor_disk_points(ED, nP=10, origin=(0,0,0)): + """ Points of the rotor disk, taken from wiz + Needs further improvements, e.g. for Ptfm motion + """ + ED = getEDClass(ED) # if ED is a filename + + # TODO Ptfm.. + + tilt = ED['ShftTilt']*np.pi/180 + yaw = (ED['NacYaw'])*np.pi/180 + hT = ED['TowerHt'] + ED['Twr2Shft'] + t2h = ED['OverHang'] * np.sin(tilt) + dX_hub = ED['OverHang']*np.cos(yaw) + dY_hub = ED['OverHang']*np.sin(yaw) + R = ED['TipRad']*np.cos(ED['PreCone(1)']*np.pi/180) + + origin = np.asarray(origin).reshape(3,1) + r_hub0 = np.asarray([ED['OverHang'], 0, hT+t2h]).reshape(3,1) + origin + r_TT = np.asarray([0 , 0, hT]).reshape(3,1) + origin + + e_vert_g = np.array([0 ,0, 1] ).reshape(3,1) + e_shaft_g0 = np.array([np.cos(tilt),0,np.sin(tilt)]).reshape(3,1) + T_wt2g = RotMat_AxisAngle(e_vert_g, yaw) + # Rotating the shaft vector so that its coordinate follow the new yaw position + e_shaft_g = np.dot(T_wt2g , e_shaft_g0) + r_hub = r_TT + np.dot(T_wt2g , r_hub0-r_TT) + + #e_shaft_g = e_shaft_g0 + #r_hub =r_hub0 + + e_r = R*orth_vect(e_shaft_g) + + points=np.zeros((3,nP)) + theta=np.linspace(0,2*np.pi,nP) # dTheta=2 pi /(np-1) + for i,t in enumerate(theta): + T=RotMat_AxisAngle(e_shaft_g, t) + points[:,i]= r_hub.ravel()+np.dot(T,e_r).ravel() + return r_hub.flatten(), points + + + + + + + + + + + + + + + + +# --------------------------------------------------------------------------------} +# --- Shape function approach +# --------------------------------------------------------------------------------{ +def fitShapeFunction(x_bar, phi, exp=None, scale=True, plot=None): + """ + Return polynomial fit for a given shapefunction + The fit is such that phi_fit = a_i x_bar^e_i + + See also: from welib.yams.flexibility.polyshape + INPUTS: + - x : dimensionless spanwise coordinate, from 0 to 1 + The points 0 and 1 need not be present. + - exp: exponents of the polynomial. Should be length of coeff. + If None, exp = [2,3,4,5,6] as used in OpenFAST + - scale: if True, scale the coefficients such that the sum is 1 + OUTPUTS: + - pfit: fitted coefficients: [a_i] such that phi_fit = a_i x_bar^e_i + - y_fit: fitted values phi_fit(x_bar) + - fig: figure handle + - fitter: object with dictionary fields 'coeffs', 'formula', 'fitted_function' + """ + if exp is None: + exp = np.arange(2,7) + if np.any(x_bar)<0 or np.any(x_bar)>1: + raise Exception('`x_bar` should be between 0 and 1') + + from welib.tools.curve_fitting import model_fit + phi_fit, pfit, fitter = model_fit('fitter: polynomial_discrete', x_bar, phi, exponents=exp) + + # --- Manipulation of the fitted model... + pfit = np.around(pfit, 8) # sticking to 8 significant digits + + if scale: + scale = np.sum(pfit) + if np.abs(scale)<1e-8: + print('[WARN] ElastoDyn: fitShapeFunction: Problem, sum of coefficient is close to 0') + else: + pfit = np.array(pfit)/scale + pfit = np.around(pfit, 8) # sticking to 8 significant digits. NOTE: this might mess up the scale again.. + def fitted_function(xx): + y=np.zeros(xx.shape) + for i,(e,c) in enumerate(zip(exp, pfit)): + y += c * xx**e + return y + phi_fit = fitted_function(x_bar) + + if plot: + fig,ax = plt.subplots(1, 1, sharey=False, figsize=(6.4,4.8)) # (6.4,4.8) + fig.subplots_adjust(left=0.12, right=0.95, top=0.95, bottom=0.11, hspace=0.20, wspace=0.20) + ax.plot(x_bar, phi , label='Input Shape') + ax.plot(x_bar, phi_fit, '--', label='Fitted Shape') + ax.set_xlabel('x/L [-]') + ax.set_ylabel('Shape function [-]') + ax.legend(loc='upper left') + else: + fig=None + + + return pfit, phi_fit, fig + + +def DTfreq(InFile): + """ Returns ElastoDyn torsion drive train frequency + INPUTS: + - InFile: fst file + OUTUPTS: + - f0: natural frequency + - fd: damped frequency + - zeta: damping ratio + """ + from welib.yams.windturbine import FASTWindTurbine + WT = FASTWindTurbine(InFile) + nGear = WT.ED['GBRatio'] + K_DT = WT.ED['DTTorSpr'] + D_DT = WT.ED['DTTorDmp'] + Jr_LSS = WT.rot.inertia[0,0]*1.000 # bld + hub, LSS + Jg_LSS = WT.gen.inertia[0,0] # gen, LSS + + M = np.array([[Jr_LSS+Jg_LSS,-Jg_LSS],[-Jg_LSS,Jg_LSS]]) + K = K_DT*np.array([[0, 0],[0, 1]]) + C = D_DT*np.array([[0, 0],[0, 1]]) + + fd, zeta, Q, f0 = eigMCK(M, C, K) + + # f0 = np.sqrt(K_DT/Jr_LSS + K_DT/Jg_LSS)/(2*np.pi)) + return f0, fd, zeta + + +def SHP(Fract, FlexL, ModShpAry, Deriv): + """ SHP calculates the Derive-derivative of the shape function ModShpAry at Fract. + NOTES: This function only works for Deriv = 0, 1, or 2. + Taken from ElastoDyn.f90 + """ + Swtch = np.zeros((3, 1)); # Initialize Swtch(:) to 0 + Swtch[Deriv] = 1; + shp = 0.0; + if Deriv==0: + for i in np.arange(len(ModShpAry)): + shp = shp + ModShpAry[i]*( Fract**(i+2) ) + else: + for i in np.arange(len(ModShpAry)): + I = i + 1 + J = I + 1; + CoefTmp = Swtch[0] + Swtch[1]*J + Swtch[2]*I*J; + if ( (J == 2) and (Deriv == 2) ): + shp = ModShpAry[i]*CoefTmp /( FlexL**Deriv ); + else: + shp = shp + ModShpAry[i]*CoefTmp*( Fract**( J - Deriv ) )/( FlexL**Deriv ); + return shp + + +def getEDClass(class_or_filename): + """ + Return ElastoDyn instance of FileCl + INPUT: either + - an instance of FileCl, as returned by reading the file, ED = weio.read(ED_filename) + - a filepath to a ElastoDyn input file + - a filepath to a main OpenFAST input file + """ + if hasattr(class_or_filename,'startswith'): # if string + ED = FASTInputFile(class_or_filename) + if 'EDFile' in ED.keys(): # User provided a .fst file... + parentDir=os.path.dirname(class_or_filename) + EDfilename = os.path.join(parentDir, ED['EDFile'].replace('"','')) + ED = FASTInputFile(EDfilename) + else: + ED = class_or_filename + return ED + + + +def ED_Parameters(fstFilename, split=False): + + fst = FASTInputFile(fstFilename) + if 'EDFile' in fst.keys(): # User provided a .fst file... + parentDir=os.path.dirname(fstFilename) + EDfilename = os.path.join(parentDir, fst['EDFile'].replace('"','')) + ED = getEDClass(EDfilename) + else: + raise NotImplementedError() + + prot, pbld, phub = rotorParameters(ED) + ptwr = towerParameters(ED, gravity=fst['Gravity'], RotMass=prot['RotMass']) + + #pbld[0] = bladeDerivedParameters(pbld[0], inertiaAtBladeRoot=True) + #ptwr = towerDerivedParameters(ptwr) + + # --- Platform + pptfm={} + pptfm['PtfmRefzt'] = ED['PtfmRefzt'] + pptfm['PtfmCMxt'] = ED['PtfmCMxt'] + pptfm['PtfmCMyt'] = ED['PtfmCMyt'] + + + pmisc = {} + pmisc['rZT0zt'] = ED['TowerBsHt'] - ED['PtfmRefzt'] # zt-component of position vector rZT0. + pmisc['RefTwrHt'] = ED['TowerHt'] - ED['PtfmRefzt'] # Vertical distance between ElastoDyn's undisplaced tower height (variable TowerHt) and ElastoDyn's inertia frame reference point (variable PtfmRef). + pmisc['rZYzt'] = ED['PtfmCMzt'] - ED['PtfmRefzt'] + + + # --- Nacelle + pnac = {} + pnac['NacCMxn'] = ED['NacCMxn'] + pnac['NacCMyn'] = ED['NacCMyn'] + pnac['NacCMzn'] = ED['NacCMzn'] + + # --- Generator + pgen={} + + # --- Shaft + psft={} + + # Calculate the turbine mass: + #ptwr['TurbMass'] = ptwr['TwrTpMass'] + ptwr['TwrMass']; + p=dict() + if not split: + p.update(pmisc) + p.update(pptfm) + p.update(ptwr) + p.update(pnac) + p.update(pgen) + p.update(psft) + p.update(phub) + p.update(prot) + p.update(pbld[0]) + return p + else: + p['misc'] = pmisc + p['ptfm'] = pptfm + p['twr'] = ptwr + p['nac'] = pnac + p['gen'] = pgen + p['sft'] = psft + p['hub'] = phub + p['rot'] = prot + p['bld'] = pbld + return p + + + + +def rotorParameters(EDfilename, identicalBlades=True, pbld1=None): + """ + Return rotor parameters computed like ElastoDyn + p: rotor parametesr + pbld: list of parameters for each blades. + """ + ED = getEDClass(EDfilename) + + if pbld1 is None: + if identicalBlades: + pbld = [bladeParameters(EDfilename, 1)]*ED['NumBl'] + else: + pbld=[bladeParameters(EDfilename, ibld+1) for ibld in range(ED['NumBl'])] + else: + pbld = [pbld1]*ED['NumBl'] + + p=dict() + p['RotMass'] = sum([pbld[k]['BldMass'] for k in range(ED['NumBl'])]) + p['RotIner'] = sum([(pbld[k]['SecondMom'] + pbld[k]['BldMass']*ED['HubRad']*(2.0*pbld[k]['BldCG'] + ED['HubRad']))*(np.cos(pbld[k]['PreCone'])**2) for k in range(ED['NumBl'])]) + #if ( p.NumBl == 2 ) % 2-blader + # p.Hubg1Iner = ( InputFileData.HubIner - p.HubMass*( ( p.UndSling - p.HubCM )^2 ) )/( p.CosDel3^2 ); + # p.Hubg2Iner = p.Hubg1Iner; + #else % 3-blader + # p.Hubg1Iner = GetFASTPar(edDataOut, 'HubIner'); + # p.Hubg2Iner = 0.0; + #end + phub=dict() + phub['HubMass'] = ED['HubMass'] + phub['HubIner'] = ED['HubIner'] + + p['RotMass'] += phub['HubMass'] + p['RotIner'] += phub['HubIner'] + + p['TwrTpMass'] = p['RotMass'] + ED['NacMass'] + ED['YawBrMass']; + + return p, pbld, phub + + +def bladeParameters(EDfilename, ibld=1, RotSpeed=1, AdjBlMs=None): + """ + Compute blade parameters in a way similar to OpenFAST + See Routine Coeff from ElastoDyn.f90 + RotSpeed: used for rotational stiffening. Use 1 for unit contribution (proportioanl to omega**2) [rad/s] + """ + from welib.yams.flexibility import polyshape + from welib.yams.flexibility import GMBeam, GKBeam + # --- Read inputs + ED = getEDClass(EDfilename) + try: + EDbld = os.path.join(os.path.dirname(ED.filename), ED['BldFile({})'.format(ibld)].replace('"','')) + except: + EDbld = os.path.join(os.path.dirname(ED.filename), ED['BldFile{}'.format(ibld)].replace('"','')) + bld = FASTInputFile(EDbld) + bldProp = bld.toDataFrame() + + # --- + p=dict() + p['HubRad'] = ED['HubRad'] + p['BldNodes'] = ED['BldNodes'] + p['BldFlexL'] = ED['TipRad']- ED['HubRad'] # Length of the flexible portion of the blade. + n=ED['BldNodes'] + p['DRNodes'] = np.ones(ED['BldNodes'])*p['BldFlexL']/ED['BldNodes'] + bld_fract = np.arange(1./ED['BldNodes']/2., 1, 1./ED['BldNodes']) + p['RNodes'] = bld_fract*p['BldFlexL'] + + # Adjust mass + if AdjBlMs is None: + p['AdjBlMs'] = bld['AdjBlMs'] + else: + p['AdjBlMs'] = AdjBlMs + bldProp['BMassDen_[kg/m]'] *= p['AdjBlMs'] + + + # --- Interpolate the blade properties to this discretization: + p['RNodesNorm'] = p['RNodes']/p['BldFlexL']; # Normalized radius to analysis nodes relative to hub ( -1 < RNodesNorm(:) < 1 ) + p['Bl_s_span'] = np.concatenate(([0], p['RNodesNorm'], [1]))*p['BldFlexL']; # Normalized radius to analysis nodes relative to hub ( -1 < RNodesNorm(:) < 1 ) + p['Bl_s_span_norm'] = p['Bl_s_span']/p['BldFlexL'] + p['BlFract']= bldProp['BlFract_[-]'].values + StrcTwst = bldProp['StrcTwst_[deg]'].values + p['ThetaS'] = np.interp(p['RNodesNorm'], p['BlFract'], bldProp['StrcTwst_[deg]']) + p['MassB'] = np.interp(p['RNodesNorm'], p['BlFract'], bldProp['BMassDen_[kg/m]']) ; + p['StiffBF'] = np.interp(p['RNodesNorm'], p['BlFract'], bldProp['FlpStff_[Nm^2]']) + p['StiffBE'] = np.interp(p['RNodesNorm'], p['BlFract'], bldProp['EdgStff_[Nm^2]']) + p['m_full'] = np.interp(p['Bl_s_span_norm'], p['BlFract'], bldProp['BMassDen_[kg/m]']) ; + p['EI_F_full'] = np.interp(p['Bl_s_span_norm'], p['BlFract'], bldProp['FlpStff_[Nm^2]']) + p['EI_E_full'] = np.interp(p['Bl_s_span_norm'], p['BlFract'], bldProp['EdgStff_[Nm^2]']) + p['ThetaS'] = np.concatenate( ([StrcTwst[0]], p['ThetaS'] , [StrcTwst[-1]]) ) + # Set the blade damping and stiffness tuner + try: + p['BldFDamp'] = [bld['BldFlDmp(1)'], bld['BldFlDmp(2)'] ] + p['BldEDamp'] = [bld['BldEdDmp(1)']] + p['FStTunr'] = [bld['FlStTunr(1)'], bld['FlStTunr(2)'] ] + except: + p['BldFDamp'] = [bld['BldFlDmp1'], bld['BldFlDmp2'] ] + p['BldEDamp'] = [bld['BldEdDmp1']] + p['FStTunr'] = [bld['FlStTunr1'], bld['FlStTunr2'] ] + # Set the mode shape coefficients + p['BldFl1Sh'] = [bld[c] for c in ['BldFl1Sh(2)', 'BldFl1Sh(3)', 'BldFl1Sh(4)', 'BldFl1Sh(5)', 'BldFl1Sh(6)']] + p['BldFl2Sh'] = [bld[c] for c in ['BldFl2Sh(2)', 'BldFl2Sh(3)', 'BldFl2Sh(4)', 'BldFl2Sh(5)', 'BldFl2Sh(6)']] + p['BldEdgSh'] = [bld[c] for c in ['BldEdgSh(2)', 'BldEdgSh(3)', 'BldEdgSh(4)', 'BldEdgSh(5)', 'BldEdgSh(6)']] + p['CThetaS'] = np.cos(p['ThetaS']*np.pi/180); + p['SThetaS'] = np.sin(p['ThetaS']*np.pi/180); + + p['PreCone']= ED['PreCone({:d})'.format(ibld)]*np.pi/180 + + # --- Inertial properties + # Initialize BldMass(), FirstMom(), and SecondMom() using TipMass() effects + p['TipMass'] = ED['TipMass({:d})'.format(ibld)] + p['BElmntMass'] = p['MassB']*p['DRNodes'] # Mass of blade element + p['BldMass'] = sum(p['BElmntMass']) + p['TipMass'] + p['FirstMom'] = sum(p['BElmntMass']*p['RNodes']) + p['TipMass']*p['BldFlexL'] # wrt blade root + p['SecondMom'] = sum(p['BElmntMass']*p['RNodes']**2) + p['TipMass']*p['BldFlexL']*p['BldFlexL'] # wrt blade root + p['FMomAbvNd'] = np.zeros(n) + # Integrate to find FMomAbvNd: + for J in np.arange(ED['BldNodes']-1,-1,-1): # Loop through the blade nodes / elements in reverse + p['FMomAbvNd'][J] = (0.5*p['BElmntMass'][J] )*(ED['HubRad'] + p['RNodes'][J] + 0.5*p['DRNodes'][J]) + if J == n-1: # Outermost blade element + p['FMomAbvNd'][J] += p['TipMass'] * ED['TipRad']; # TipMass effects: + else: + # Add to p['FMomAbvNd(K,J) the effects from the (not yet used) portion of element J+1 + p['FMomAbvNd'][J] += p['FMomAbvNd'][J+1] + (0.5*p['BElmntMass'][J+1])*( ED['HubRad'] + p['RNodes'][J+1] - 0.5*p['DRNodes'][J+1] ); + # Calculate BldCG() using FirstMom() and BldMass(); and calculate RotMass and RotIner: + p['BldCG']= p['FirstMom']/p['BldMass']; + p['MBF'] = np.zeros((2, 2)) + p['MBE'] = np.zeros((1, 1)) + p['KBFCent'] = np.zeros((2, 2)) + p['KBECent'] = np.zeros((1, 1)) + p['KBF'] = np.zeros((2, 2)) + p['KBE'] = np.zeros((1, 1)) + # Initialize the generalized blade masses using tip mass effects: + p['MBF'][0,0] = p['TipMass']; + p['MBF'][1,1] = p['TipMass']; + p['MBE'][0,0] = p['TipMass']; + + # Shape functions and derivatives at all nodes and tip&root (2 additional points) + exp = np.arange(2,7) + p['ShapeF1_full'], p['dShapeF1_full'],p['ddShapeF1_full'] = polyshape(p['Bl_s_span'], coeff=p['BldFl1Sh'], exp=exp, x_max=p['BldFlexL'], doscale=False) + p['ShapeF2_full'], p['dShapeF2_full'],p['ddShapeF2_full'] = polyshape(p['Bl_s_span'], coeff=p['BldFl2Sh'], exp=exp, x_max=p['BldFlexL'], doscale=False) + p['ShapeE1_full'], p['dShapeE1_full'],p['ddShapeE1_full'] = polyshape(p['Bl_s_span'], coeff=p['BldEdgSh'], exp=exp, x_max=p['BldFlexL'], doscale=False) + + # Integrate to find the generalized mass of the blade (including tip mass effects). + # Ignore the cross-correlation terms of MBF (i.e. MBF(i,j) where i ~= j) since these terms will never be used. + p['MBF'][0,0] = sum(p['BElmntMass']*p['ShapeF1_full'][1:-1]**2) + p['MBF'][1,1] = sum(p['BElmntMass']*p['ShapeF2_full'][1:-1]**2) + p['MBE'][0,0] = sum(p['BElmntMass']*p['ShapeE1_full'][1:-1]**2) + + ElmntStff = p['StiffBF']*p['DRNodes'] # Flapwise stiffness of blade element J + p['KBF'][0,0] = sum(ElmntStff*p['ddShapeF1_full'][1:-1]*p['ddShapeF1_full'][1:-1]) + p['KBF'][0,1] = sum(ElmntStff*p['ddShapeF1_full'][1:-1]*p['ddShapeF2_full'][1:-1]) + p['KBF'][1,0] = sum(ElmntStff*p['ddShapeF2_full'][1:-1]*p['ddShapeF1_full'][1:-1]) + p['KBF'][1,1] = sum(ElmntStff*p['ddShapeF2_full'][1:-1]*p['ddShapeF2_full'][1:-1]) + ElmntStff = p['StiffBE']*p['DRNodes'] # Edgewise stiffness of blade element J + p['KBE'][0,0] = sum(ElmntStff*p['ddShapeE1_full'][1:-1]*p['ddShapeE1_full'][1:-1]) + + # Integrate to find the centrifugal-term of the generalized flapwise and edgewise + # stiffness of the blades. Ignore the cross-correlation terms of KBFCent (i.e. + # KBFCent(i,j) where i ~= j) since these terms will never be used. + ElmntStff = p['FMomAbvNd']*p['DRNodes']*RotSpeed**2 # Centrifugal stiffness of blade element J + p['KBFCent'][0,0] = sum(ElmntStff*p['dShapeF1_full'][1:-1]**2) + p['KBFCent'][1,1] = sum(ElmntStff*p['dShapeF2_full'][1:-1]**2) + p['KBECent'][0,0] = sum(ElmntStff*p['dShapeE1_full'][1:-1]**2) + + # Calculate the 2nd derivatives of the twisted shape functions (include root and tip): + p['TwistedSF'] = np.zeros((2, 3, ED['BldNodes']+2, 3)); # x/y, BF1/BF2/BE, node, deriv + p['TwistedSF'][0,0,:,2] = p['ddShapeF1_full'][:]*p['CThetaS'][:] # 2nd deriv. of Phi1(J) for blade K + p['TwistedSF'][1,0,:,2] = -p['ddShapeF1_full'][:]*p['SThetaS'][:] # 2nd deriv. of Psi1(J) for blade K + p['TwistedSF'][0,1,:,2] = p['ddShapeF2_full'][:]*p['CThetaS'][:] # 2nd deriv. of Phi2(J) for blade K + p['TwistedSF'][1,1,:,2] = -p['ddShapeF2_full'][:]*p['SThetaS'][:] # 2nd deriv. of Psi2(J) for blade K + p['TwistedSF'][0,2,:,2] = p['ddShapeE1_full'][:]*p['SThetaS'][:] # 2nd deriv. of Phi3(J) for blade K + p['TwistedSF'][1,2,:,2] = p['ddShapeE1_full'][:]*p['CThetaS'][:] # 2nd deriv. of Psi3(J) for blade K + # Integrate to find the 1st derivatives of the twisted shape functions: + for J in np.arange(n): # Loop through the blade nodes / elements + TwstdSF= np.zeros((2, 3, 2)) + order=1; + for I in [0,1]: # Loop through Phi and Psi + for L in [0,1,2]: # Loop through all blade DOFs + TwstdSF[I,L,order] = p['TwistedSF'][I,L,J+1,order+1]*0.5*p['DRNodes'][J]; + p['TwistedSF'][I,L,J+1,order] = TwstdSF[I,L,order]; + if J != 0: # All but the innermost blade element + # Add the effects from the (not yet used) portion of element J-1 + for I in [0,1]: # Loop through Phi and Psi + for L in [0,1,2]: # Loop through all blade DOFs + p['TwistedSF'][I,L,J+1,order] += p['TwistedSF'][I,L,J,order] + TwstdSFOld[I,L,order]; + # Store the TwstdSF and AxRdBld terms of the current element (these will be used for the next element) +# TwstdSFOld = TwstdSF; +# for J in np.arange(n): # Loop through the blade nodes / elements +# TwstdSF= np.zeros((2, 3, 2)) + # Integrate to find the twisted shape functions themselves (i.e., their zeroeth derivative): + order = 0 + for I in [0,1]: # Loop through Phi and Psi + for L in [0,1,2]: # Loop through all blade DOFs + TwstdSF[I,L, order] = p['TwistedSF'][I,L,J+1, order+1]*0.5*p['DRNodes'][J]; + p['TwistedSF'][I,L,J+1,order] = TwstdSF[ I,L, order ]; + if J != 0: # All but the innermost blade element + # Add the effects from the (not yet used) portion of element J-1 + for I in [0,1]: # Loop through Phi and Psi + for L in [0,1,2]: # Loop through all blade DOFs + p['TwistedSF'][I,L,J+1,order] += p['TwistedSF'][I,L,J,order] + TwstdSFOld[I,L, order]; + TwstdSFOld = TwstdSF; + # Integrate to find the 1st and zeroeth derivatives of the twisted shape functions at the tip: + for I in [0,1]: # Loop through Phi and Psi + for L in [0,1,2]: # Loop through all blade DOFs + p['TwistedSF'][I,L,-1,1] = p['TwistedSF'][I,L,-2,1] + TwstdSFOld[I,L,1] + p['TwistedSF'][I,L,-1,0] = p['TwistedSF'][I,L,-2,0] + TwstdSFOld[I,L,0] + # Blade root + p['TwistedSF'][:,:,0,1] = 0.0; + p['TwistedSF'][:,:,0,0] = 0.0; + + # Integrate to find the blade axial reduction shape functions: + p['AxRedBld'] = np.zeros((3, 3, ED['BldNodes']+2)); # + for J in np.arange(n): # Loop through the blade nodes / elements + AxRdBld= np.zeros((3, 3)) + for I in [0,1,2]: # Loop through all blade DOFs + for L in [0,1,2]: # Loop through all blade DOFs + AxRdBld[I,L] = 0.5*p['DRNodes'][J]*( p['TwistedSF'][0,I,J+1,1]*p['TwistedSF'][0,L,J+1,1] + p['TwistedSF'][1,I,J+1,1]*p['TwistedSF'][1,L,J+1,1] ); + p['AxRedBld'][I,L,J+1] = AxRdBld[I,L] + if J != 0: # All but the innermost blade element + # Add the effects from the (not yet used) portion of element J-1 + for I in [0,1,2]: # Loop through all blade DOFs + for L in [0,1,2]: # Loop through all blade DOFs + p['AxRedBld'][I,L,J+1] += p['AxRedBld'][I,L,J] + AxRdBldOld[I,L] + AxRdBldOld = AxRdBld; + # Integrate to find the blade axial reduction shape functions at the tip: + for I in [0,1,2]: # Loop through all blade DOFs + for L in [0,1,2]: # Loop through all blade DOFs + p['AxRedBld'][I,L,-1] = p['AxRedBld'][I,L,-2] + AxRdBldOld[I,L] + # Blade root + p['AxRedBld'] [:,:,0 ] = 0.0; + + # Apply the flapwise modal stiffness tuners of the blades to KBF(): + for I in [0,1]: # Loop through flap DOFs + for L in [0,1]: # Loop through flap DOFs + p['KBF'][I,L] = np.sqrt( p['FStTunr'][I]*p['FStTunr'][L] )*p['KBF'][I,L]; + # Calculate the blade natural frequencies: + p['FreqBF'] = np.zeros((2,3)) + p['FreqBE'] = np.zeros((1,3)) + for I in [0,1]: # Loop through flap DOFs + p['FreqBF'][I,0] = (1/2/np.pi)*np.sqrt( p['KBF'][I,I] /( p['MBF'][I,I] - p['TipMass']) )# Natural blade I-flap frequency w/o centrifugal stiffening nor tip mass effects + p['FreqBF'][I,1] = (1/2/np.pi)*np.sqrt( p['KBF'][I,I] / p['MBF'][I,I] )# Natural blade I-flap frequency w/o centrifugal stiffening, but w/ tip mass effects + p['FreqBF'][I,2] = (1/2/np.pi)*np.sqrt( ( p['KBF'][I,I] + p['KBFCent'][I,I])/ p['MBF'][I,I] )# Natural blade I-flap frequency w/ centrifugal stiffening and tip mass effects + I=0 + p['FreqBE'][I,0] = (1/2/np.pi)*np.sqrt( p['KBE'][I,I] /( p['MBE'][I,I] - p['TipMass']) )# Natural blade 1-edge frequency w/o centrifugal stiffening nor tip mass effects + p['FreqBE'][I,1] = (1/2/np.pi)*np.sqrt( p['KBE'][I,I] / p['MBE'][I,I] )# Natural Blade 1-edge frequency w/o centrifugal stiffening, but w/ tip mass effects + p['FreqBE'][I,2] = (1/2/np.pi)*np.sqrt( ( p['KBE'][I,I] + p['KBECent'][I,I])/ p['MBE'][I,I] )# Natural Blade 1-edge frequency w/ centrifugal stiffening and tip mass effects + # Calculate the generalized damping of the blades: + p['CBF'] = np.zeros((2,2)) + p['CBE'] = np.zeros((1,1)) + for I in [0,1]: # Loop through flap DOFs + for L in [0,1]: # Loop through flap DOFs + p['CBF'][I,L] = ( 0.01*p['BldFDamp'][L] )*p['KBF'][I,L]/( np.pi*p['FreqBF'][L,0] ); + L=0; I=0; + p['CBE'][I,L] = ( 0.01*p['BldEDamp'][L] )*p['KBE'][I,L]/( np.pi*p['FreqBE'][L,0] ); + + nq = 3 + + # --- Twisted and untwisted shape functions + nNodes=n+2 + p['Ut'] = np.zeros((nq, 3, nNodes)) + p['Vt'] = np.zeros((nq, 3, nNodes)) + p['Kt'] = np.zeros((nq, 3, nNodes)) + p['U'] = np.zeros((nq, 3, nNodes)) + p['V'] = np.zeros((nq, 3, nNodes)) + p['K'] = np.zeros((nq, 3, nNodes)) + for j,idir,name in zip(range(0,nq), (0,0,1), ('F1','F2','E1')): # direction is x, x, y + p['Ut'][j][0,:] = p['TwistedSF'][0, j, :, 0] # x + p['Ut'][j][1,:] = p['TwistedSF'][1, j, :, 0] # y + p['Vt'][j][0,:] = p['TwistedSF'][0, j, :, 1] # x + p['Vt'][j][1,:] = p['TwistedSF'][1, j, :, 1] # y + p['Kt'][j][0,:] = p['TwistedSF'][0, j, :, 2] # x + p['Kt'][j][1,:] = p['TwistedSF'][1, j, :, 2] # y + p['U'][j][idir,:] = p['Shape'+name+'_full'] + p['V'][j][idir,:] = p['dShape'+name+'_full'] + p['K'][j][idir,:] = p['ddShape'+name+'_full'] + + # --- Parameters consistent with "YAMS" flexibility module + p['s_span'] = p['Bl_s_span'] + p['m'] = p['m_full'] + p['EI'] = np.zeros((3,nNodes)) + p['EI'][0,:] = p['EI_F_full'] + p['EI'][1,:] = p['EI_E_full'] + + p['s_G0'] = np.zeros((3, len(p['Bl_s_span']))) + p['s_G0'][2,:] = p['s_span'] # TODO add hub radius + + #KK0 = GKBeam(s_span, EI, PhiK, bOrth=False) + #if bStiffening: + # KKg = GKBeamStiffnening(s_span, PhiV, gravity, m, Mtop, Omega, main_axis=main_axis) + # KKg_self= GKBeamStiffnening(s_span, PhiV, gravity, m, Mtop, Omega, main_axis=main_axis, bSelfWeight=True , bMtop=False, bRot=False) + # KKg_Mtop= GKBeamStiffnening(s_span, PhiV, gravity, m, Mtop, Omega, main_axis=main_axis, bSelfWeight=False, bMtop=True, bRot=False) + # KKg_rot = GKBeamStiffnening(s_span, PhiV, gravity, m, Mtop, Omega, main_axis=main_axis, bSelfWeight=False, bMtop=False, bRot=True) +# MM, IT = GMBeam(s_G0, p['s_span'], p['m_full'], p['Ut'], rot_terms=True, method='OpenFAST', main_axis='z', U_untwisted=p['U']) + return p + + +def bladeDerivedParameters(p, inertiaAtBladeRoot=True): + """ Compute blade derived parameters, suitable for SID: + - Inertial matrices + - Stiffness matrices + The parameters are computed "by hand" (as opposed to using GMBeam) + NOTE: this function is mostly for debugging purposes. + Use GMBeam with method='OpenFAST' instead + """ + + nq = 3 + if inertiaAtBladeRoot: + rh = 0 + else: + rh = p['HubRad'] # Hub Radius # TODO make this an option if from blade root or not + + # --- Rigid mass matrix terms + # Mxt term + p['mdCM'] = np.zeros((3)) + p['mdCM'][2]= sum(p['BElmntMass'][:]*(p['RNodes']+rh)); + # + p['mdCM_M1'] = np.zeros((3,nq)) + for j in np.arange(nq): + p['mdCM_M1'][0,j]= sum(p['TwistedSF'][0, j, 1:-1, 0]*p['BElmntMass']) + p['mdCM_M1'][1,j]= sum(p['TwistedSF'][1, j, 1:-1, 0]*p['BElmntMass']) + + p['J'] = np.zeros((3,3)) + p['J'][0,0] = sum(p['BElmntMass'][:]*(p['RNodes']+rh)**2) + p['J'][1,1] = sum(p['BElmntMass'][:]*(p['RNodes']+rh)**2) + # --- Elastic matrices + p['Ke'] = np.zeros((nq,nq)) + p['Ke0'] = np.zeros((nq,nq)) # Without any stiffening + p['De'] = np.zeros((nq,nq)) + p['Me'] = np.zeros((nq,nq)) + # Me + p['Me'][0,0]= p['MBF'][0, 0] + p['Me'][0,1]= p['MBF'][0, 1] + p['Me'][1,0]= p['MBF'][1, 0] + p['Me'][1,1]= p['MBF'][1, 1] + p['Me'][2,2]= p['MBE'][0, 0] + # Ke + p['Ke0'][0,0]= p['KBF'][0, 0] + p['Ke0'][0,1]= p['KBF'][0, 1] + p['Ke0'][1,0]= p['KBF'][1, 0] + p['Ke0'][1,1]= p['KBF'][1, 1] + p['Ke0'][2,2]= p['KBE'][0, 0] + p['Ke'] = p['Ke0'] # OpenFAST does not put the stiffening in this + # De + p['De'][0,0]= p['CBF'][0, 0] + p['De'][0,1]= p['CBF'][0, 1] + p['De'][1,0]= p['CBF'][1, 0] + p['De'][1,1]= p['CBF'][1, 1] + p['De'][2,2]= p['CBE'][0, 0] + + # KgOm + p['Kg_Om'] = np.zeros((nq,nq)) + p['Kg_Om'][0,0] = p['KBFCent'][0,0] + p['Kg_Om'][1,1] = p['KBFCent'][1,1] + p['Kg_Om'][2,2] = p['KBECent'][0,0] + + # --- Elastic mass matrix terms + # Ct + p['Ct'] = np.zeros((nq,3)) + p['Ct'][0,0] = sum(np.squeeze(p['TwistedSF'][0, 0, 1:-1, 0])*p['BElmntMass'][:])# 1st mode acceleration in x + p['Ct'][0,1] = sum(np.squeeze(p['TwistedSF'][1, 0, 1:-1, 0])*p['BElmntMass'][:])# 1st mode acceleration in y + p['Ct'][1,0] = sum(np.squeeze(p['TwistedSF'][0, 1, 1:-1, 0])*p['BElmntMass'][:])# 2nd mode acceleration in x + p['Ct'][1,1] = sum(np.squeeze(p['TwistedSF'][1, 1, 1:-1, 0])*p['BElmntMass'][:])# 2nd mode acceleration in y + p['Ct'][2,0] = sum(np.squeeze(p['TwistedSF'][0, 2, 1:-1, 0])*p['BElmntMass'][:])# 3nd mode acceleration in x + p['Ct'][2,1] = sum(np.squeeze(p['TwistedSF'][1, 2, 1:-1, 0])*p['BElmntMass'][:])# 3nd mode acceleration in y + + # Cr + p['Cr'] = np.zeros((nq,3)) + p['Cr'][0,0]= -sum((p['RNodes']+rh)*np.squeeze(p['TwistedSF'][1, 0, 1:-1, 0])*p['BElmntMass'][:])# 1st mode acceleration about x axis (1) -> movement in negative y + p['Cr'][0,1]= sum((p['RNodes']+rh)*np.squeeze(p['TwistedSF'][0, 0, 1:-1, 0])*p['BElmntMass'][:])# 1st mode acceleration about y axis (2) -> movement in x + p['Cr'][1,0]= -sum((p['RNodes']+rh)*np.squeeze(p['TwistedSF'][1, 1, 1:-1, 0])*p['BElmntMass'][:])# 2nd mode acceleration about x axis (1) -> movement in negative y + p['Cr'][1,1]= sum((p['RNodes']+rh)*np.squeeze(p['TwistedSF'][0, 1, 1:-1, 0])*p['BElmntMass'][:])# 2nd mode acceleration about y axis (2) -> movement in x + p['Cr'][2,0]= -sum((p['RNodes']+rh)*np.squeeze(p['TwistedSF'][1, 2, 1:-1, 0])*p['BElmntMass'][:])# 3nd mode acceleration about x axis (1) -> movement in negative y + p['Cr'][2,1]= sum((p['RNodes']+rh)*np.squeeze(p['TwistedSF'][0, 2, 1:-1, 0])*p['BElmntMass'][:])# 3nd mode acceleration about y axis (2) -> movement in x + + # --- Oe, Oe_j = \int [~Phi_j] [~s] = { \int [~s] [~Phi_j] }^t = -1/2 *(Gr_j)^t + # M0: nq x 6 + # Straight blade: Oe6_j= [0, 0, 0, 0, szy, szx] + p['Oe6'] = np.zeros((nq,6)) + for j in np.arange(nq): + szx = sum((p['RNodes']+rh) * p['TwistedSF'][0, j, 1:-1, 0]*p['BElmntMass']) + szy = sum((p['RNodes']+rh) * p['TwistedSF'][1, j, 1:-1, 0]*p['BElmntMass']) + p['Oe6'][j,4] = szy + p['Oe6'][j,5] = szx + + # M1: nq x 6 x nq + #Oe = np.zeros((nf,3,3)) + #Oe6= np.zeros((nf,6)) + o=0 # derivative order 0=shape + Oe_M1 = np.zeros((nq,6,nq)) + SS=np.zeros((nq,nq,3,3)) + for j in np.arange(nq): + for k in np.arange(nq): + s=np.zeros((3,3)) + for i1 in [0,1]: # NOTE: z,2 is 0 + for i2 in [0,1]: # NOTE: z,2 is 0 + s[i1,i2] = sum(np.squeeze(p['TwistedSF'][i1, k, 1:-1, o])*np.squeeze(p['TwistedSF'][i2, j, 1:-1, o])*p['BElmntMass']) + SS[j,k,:,:]=s + #Oe6_j= [ -(syy+szz), -(sxx+szz), -(sxx+syy), sxy+syx, syz+szy, sxz+szx] + Oe_M1[j,:,k] = [-(s[1,1]+s[2,2]), -(s[0,0]+s[2,2]), -(s[0,0]+s[1,1]), s[0,1]+s[1,0], s[1,2]+s[2,1], s[0,2]+s[2,0] ] + p['SS_M1']=SS + # Centrifugal stiffening + # TODO TODO + # TODO TODO TODO + # Below is for flap1 + edge1 not flap1 flap2 edge + Oe_M1g = np.zeros((nq,6,nq)) + Oe_M1g[0,0,0]= p['KBFCent'][0,0]; + Oe_M1g[0,1,0]= p['KBFCent'][0,0]; + Oe_M1g[2,0,2]= p['KBECent'][0,0]; + Oe_M1g[2,1,2]= p['KBECent'][0,0]; + + p['Oe_M1'] = Oe_M1 + p['Oe_M1g'] = Oe_M1g + + + return p + + +def towerParameters(EDfilename, gravity, RotMass=None, noInertialCouplings=True): + """ + Compute tower parameters exactly like OpenFAST + See Routine Coeff from ElastoDyn.f90 + """ + from welib.yams.flexibility import polyshape + # --- Read inputs + ED = getEDClass(EDfilename) + EDtwr = os.path.join(os.path.dirname(ED.filename), ED['TwrFile'].replace('"','')) + twr = FASTInputFile(EDtwr) + twrProp = twr.toDataFrame() + n = ED['TwrNodes'] + + # --- + p=dict() + + # --- Main dimensions + p['TwrFlexL'] = ED['TowerHt'] - ED['TowerBsHt'] # Height / length of the flexible portion of the tower. + p['TwrNodes'] = ED['TwrNodes'] + n = ED['TwrNodes'] + nModesPerDir = 2 + nDeriv = 3 + # --- Spanwise nodes + twr_fract = np.arange(1./n/2., 1, 1./n) + p['DHNodes'] = np.ones(n)*p['TwrFlexL']/n + p['HNodes'] = twr_fract * p['TwrFlexL'] # NOTE: we don't add the TowerBsHt! + p['HNodesNorm'] = p['HNodes']/p['TwrFlexL'] + p['Twr_s_span'] = np.concatenate(([0], p['HNodesNorm'], [1]))*p['TwrFlexL']; # Midpoints + 0 and L + p['Twr_s_span_norm'] = p['Twr_s_span']/p['TwrFlexL'] + # --- Interpolate properties to new nodal positions + HtFract= twrProp['HtFract_[-]'].values + p['MassT'] = np.interp(p['HNodesNorm'], HtFract, twrProp['TMassDen_[kg/m]']) ; + p['StiffTFA'] = np.interp(p['HNodesNorm'], HtFract, twrProp['TwFAStif_[Nm^2]']) ; + p['StiffTSS'] = np.interp(p['HNodesNorm'], HtFract, twrProp['TwSSStif_[Nm^2]']) ; + p['m_full'] = np.interp(p['Twr_s_span_norm'],HtFract, twrProp['TMassDen_[kg/m]']) ; + p['EI_FA_full'] = np.interp(p['Twr_s_span_norm'], HtFract, twrProp['TwFAStif_[Nm^2]']) + p['EI_SS_full'] = np.interp(p['Twr_s_span_norm'], HtFract, twrProp['TwSSStif_[Nm^2]']) + # Shape coefficients + p['TwFAM1Sh'] = [twr[c] for c in ['TwFAM1Sh(2)', 'TwFAM1Sh(3)', 'TwFAM1Sh(4)', 'TwFAM1Sh(5)', 'TwFAM1Sh(6)']] + p['TwFAM2Sh'] = [twr[c] for c in ['TwFAM2Sh(2)', 'TwFAM2Sh(3)', 'TwFAM2Sh(4)', 'TwFAM2Sh(5)', 'TwFAM2Sh(6)']] + p['TwSSM1Sh'] = [twr[c] for c in ['TwSSM1Sh(2)', 'TwSSM1Sh(3)', 'TwSSM1Sh(4)', 'TwSSM1Sh(5)', 'TwSSM1Sh(6)']] + p['TwSSM2Sh'] = [twr[c] for c in ['TwSSM2Sh(2)', 'TwSSM2Sh(3)', 'TwSSM2Sh(4)', 'TwSSM2Sh(5)', 'TwSSM2Sh(6)']] + p['FAStTunr'] = [twr['FAStTunr(1)'], twr['FAStTunr(2)']] + p['SSStTunr'] = [twr['SSStTunr(1)'], twr['SSStTunr(2)']] + p['TwrFADmp'] = [twr['TwrFADmp(1)'], twr['TwrFADmp(2)']] + p['TwrSSDmp'] = [twr['TwrSSDmp(1)'], twr['TwrSSDmp(2)']] + # Calculate the tower-top mass: + p['TwrTpMass'] = RotMass + ED['NacMass'] + ED['YawBrMass']; + p['TElmntMass'] = p['MassT']*p['DHNodes'] # Mass of tower element J + p['TwrMass'] = sum(p['TElmntMass']) + # Mass above node + p['TMssAbvNd'] = np.zeros(n) + for J in np.arange(n-1,-1,-1): + p['TMssAbvNd'][J] = 0.5*p['TElmntMass'][J]; + if J == n-1: # Uppermost tower element + # Add the TwrTpMass effects: + p['TMssAbvNd'][J] = p['TMssAbvNd'][J] + p['TwrTpMass'] + else: # All other tower elements + # Add to TMssAbvNd'][J] the effects from the (not yet used) portion of element J+1 + p['TMssAbvNd'][J] = 0.5*p['TElmntMass'][J+1] + p['TMssAbvNd'][J] + p['TMssAbvNd'][J+1]; + + # --- Tower shape functions (all derivatives) for mode 1&2 FA and SS + p['TwrFASF'] = np.zeros((nModesPerDir, n+2, nDeriv)) # NOTE: full (+2) + p['TwrSSSF'] = np.zeros((nModesPerDir, n+2, nDeriv)) # NOTE: full (+2) + p['TwrFASF'][0,:,0],p['TwrFASF'][0,:,1],p['TwrFASF'][0,:,2] = polyshape(p['Twr_s_span'], coeff=p['TwFAM1Sh'], x_max=p['TwrFlexL'], doscale=False) + p['TwrFASF'][1,:,0],p['TwrFASF'][1,:,1],p['TwrFASF'][1,:,2] = polyshape(p['Twr_s_span'], coeff=p['TwFAM2Sh'], x_max=p['TwrFlexL'], doscale=False) + p['TwrSSSF'][0,:,0],p['TwrSSSF'][0,:,1],p['TwrSSSF'][0,:,2] = polyshape(p['Twr_s_span'], coeff=p['TwSSM1Sh'], x_max=p['TwrFlexL'], doscale=False) + p['TwrSSSF'][1,:,0],p['TwrSSSF'][1,:,1],p['TwrSSSF'][1,:,2] = polyshape(p['Twr_s_span'], coeff=p['TwSSM2Sh'], x_max=p['TwrFlexL'], doscale=False) + + # --- Generalized mass + p['MTFA'] = np.zeros((2, 2)) + p['MTSS'] = np.zeros((2, 2)) + for I in [0,1]: # Loop through all tower modes in a single direction + p['MTFA'][I,I] = p['TwrTpMass']; + p['MTSS'][I,I] = p['TwrTpMass']; + for I in [0,1]: # Loop through all tower DOFs in one direction + for L in [0,1]: # Loop through all tower DOFs in one direction + p['MTFA'][I,L] += sum(p['TElmntMass']*p['TwrFASF'][I,1:-1,0]*p['TwrFASF'][L,1:-1,0]) + p['MTSS'][I,L] += sum(p['TElmntMass']*p['TwrSSSF'][I,1:-1,0]*p['TwrSSSF'][L,1:-1,0]) + if noInertialCouplings: + p['MTFA'][0,1]=0 + p['MTFA'][1,0]=0 + p['MTSS'][0,1]=0 + p['MTSS'][1,0]=0 + + # --- Generalized stiffness + p['KTFA'] = np.zeros((nModesPerDir, nModesPerDir)) + p['KTSS'] = np.zeros((nModesPerDir, nModesPerDir)) + for I in [0,1]: # Loop through all tower DOFs in one direction + for L in [0,1]: # Loop through all tower DOFs in one direction + p['KTFA'][I,L] = sum(p['StiffTFA']*p['DHNodes']*p['TwrFASF'][I,1:-1,2]*p['TwrFASF'][L,1:-1,2]) + p['KTSS'][I,L] = sum(p['StiffTSS']*p['DHNodes']*p['TwrSSSF'][I,1:-1,2]*p['TwrSSSF'][L,1:-1,2]) + # --- Self-weight geometrical stiffness + p['KTFAGrav'] = np.zeros((nModesPerDir, nModesPerDir)) + p['KTSSGrav'] = np.zeros((nModesPerDir, nModesPerDir)) + for I in [0,1]: # Loop through all tower DOFs in one direction + p['KTFAGrav'][I,I] = sum( - p['TMssAbvNd']*p['DHNodes']*p['TwrFASF'][I,1:-1,1]**2)*gravity + p['KTSSGrav'][I,I] = sum( - p['TMssAbvNd']*p['DHNodes']*p['TwrSSSF'][I,1:-1,1]**2)*gravity + # --- Tower top geometric stiffness + p['KTFAGravTT'] = np.zeros((nModesPerDir, nModesPerDir)) + p['KTSSGravTT'] = np.zeros((nModesPerDir, nModesPerDir)) + for I in [0,1]: # Loop through all tower DOFs in one direction + # TODO CHECK SIGN + p['KTFAGravTT'][I,I] = sum(- p['TwrTpMass']* p['DHNodes']*p['TwrFASF'][I,1:-1,1]**2)*gravity + p['KTSSGravTT'][I,I] = sum(- p['TwrTpMass']* p['DHNodes']*p['TwrSSSF'][I,1:-1,1]**2)*gravity + # --- Integrate to find the tower axial reduction shape functions: + p['AxRedTFA']= np.zeros((nModesPerDir, nModesPerDir, n+2)) # NOTE: full (+2) + p['AxRedTSS']= np.zeros((nModesPerDir, nModesPerDir, n+2)) # NOTE: full (+2) + for J in np.arange(n): # Loop through the tower nodes / elements + AxRdTFA= np.zeros((2, 2)) + AxRdTSS= np.zeros((2, 2)) + for I in [0,1]: # Loop through all tower DOFs in one direction + for L in [0,1]: # Loop through all tower DOFs in one direction + AxRdTFA [I,L] = 0.5*p['DHNodes'][J]*p['TwrFASF'][I,J+1,1]*p['TwrFASF'][L,J,1]; + AxRdTSS [I,L] = 0.5*p['DHNodes'][J]*p['TwrSSSF'][I,J+1,1]*p['TwrSSSF'][L,J,1]; + p['AxRedTFA'][I,L,J+1] = AxRdTFA[I,L] + p['AxRedTSS'][I,L,J+1] = AxRdTSS[I,L] + if J != 0: # All but the lowermost tower element + # Add the effects from the (not yet used) portion of element J-1 + for I in [0,1]: # Loop through all tower DOFs in one direction + for L in [0,1]: # Loop through all tower DOFs in one direction + p['AxRedTFA'][I,L,J+1] += p['AxRedTFA'][I,L,J]+ AxRdTFAOld[I,L] + p['AxRedTSS'][I,L,J+1] += p['AxRedTSS'][I,L,J]+ AxRdTSSOld[I,L] + # Store the AxRdTFA and AxRdTSS terms of the current element (these will be used for the next element) + AxRdTFAOld = AxRdTFA; + AxRdTSSOld = AxRdTSS; + # Integrate to find the tower axial reduction shape functions at the tower-top: + for I in [0,1]: # Loop through all tower DOFs in one direction + for L in [0,1]: # Loop through all tower DOFs in one direction + p['AxRedTFA'][I,L,-1] = p['AxRedTFA'][I,L,-2] + AxRdTFAOld[I,L] + p['AxRedTSS'][I,L,-1] = p['AxRedTSS'][I,L,-2] + AxRdTSSOld[I,L] + # Apply the modal stiffness tuners of the tower to KTFA() and KTSS(): + for I in [0,1]: # Loop through all tower DOFs in one direction + for L in [0,1]: # Loop through all tower DOFs in one direction + p['KTFA'][I,L] = np.sqrt( p['FAStTunr'][I]*p['FAStTunr'][L] )*p['KTFA'][I,L] + p['KTSS'][I,L] = np.sqrt( p['SSStTunr'][I]*p['SSStTunr'][L] )*p['KTSS'][I,L] + # Calculate the tower natural frequencies: + p['FreqTFA'] = np.zeros((nModesPerDir, 3)) # NOTE: third frequency not computed by OpenFAST + p['FreqTSS'] = np.zeros((nModesPerDir, 3)) + p['CTFA'] = np.zeros((nModesPerDir, 2)) + p['CTSS'] = np.zeros((nModesPerDir, 2)) + for I in [0,1]: # Loop through all tower DOFs in one direction + allKTFAGrav= p['KTFAGrav'][I,I] + p['KTFAGravTT'][I,I] + allKTSSGrav= p['KTSSGrav'][I,I] + p['KTSSGravTT'][I,I] + p['FreqTFA'][I,0] = (1/2/np.pi)*np.sqrt( p['KTFA'][I,I] /( p['MTFA'][I,I] - p['TwrTpMass'] ) ) # Natural tower I-fore-aft frequency w/o gravitational destiffening nor tower-top mass effects + p['FreqTFA'][I,1] = (1/2/np.pi)*np.sqrt( ( p['KTFA'][I,I] + p['KTFAGrav'][I,I] )/ p['MTFA'][I,I] ) # Natural tower I-fore-aft frequency w/ gravitational destiffening and tower-top mass effects + p['FreqTFA'][I,2] = (1/2/np.pi)*np.sqrt( ( p['KTFA'][I,I] + allKTFAGrav )/ p['MTFA'][I,I] ) # Natural tower I-fore-aft frequency w/ gravitational destiffening and tower-top mass effects + p['FreqTSS'][I,0] = (1/2/np.pi)*np.sqrt( p['KTSS'][I,I] /( p['MTSS'][I,I] - p['TwrTpMass'] ) ) # Natural tower I-side-to-side frequency w/o gravitational destiffening nor tower-top mass effects + p['FreqTSS'][I,1] = (1/2/np.pi)*np.sqrt( ( p['KTSS'][I,I] + p['KTSSGrav'][I,I] )/ p['MTSS'][I,I] ) # Natural tower I-side-to-side frequency w/ gravitational destiffening and tower-top mass effects + p['FreqTSS'][I,2] = (1/2/np.pi)*np.sqrt( ( p['KTSS'][I,I] + allKTSSGrav )/ p['MTSS'][I,I] ) # Natural tower I-side-to-side frequency w/ gravitational destiffening and tower-top mass effects + # Calculate the generalized damping of the tower: + for I in [0,1]: # Loop through all tower DOFs in one direction + for L in [0,1]: # Loop through all tower DOFs in one direction + p['CTFA'][I,L] = ( 0.01*p['TwrFADmp'][L] )*p['KTFA'][I,L]/( np.pi*p['FreqTFA'][L,0] ); + p['CTSS'][I,L] = ( 0.01*p['TwrSSDmp'][L] )*p['KTSS'][I,L]/( np.pi*p['FreqTSS'][L,0] ); + + + # --- Shape functions (FA1 FA2 SS1 SS2) + nq=4 + nNodes=n+2 + p['U'] = np.zeros((nq, 3, nNodes)) + p['V'] = np.zeros((nq, 3, nNodes)) + p['K'] = np.zeros((nq, 3, nNodes)) + j=0 + for idir, name in zip((0,1),('FA','SS')): + for jm in [0,1]: + p['U'][j][idir,:] = p['Twr{}SF'.format(name)][jm, :, 0] + p['V'][j][idir,:] = p['Twr{}SF'.format(name)][jm, :, 1] + p['K'][j][idir,:] = p['Twr{}SF'.format(name)][jm, :, 2] + j+=1 + # --- Parameters consistent with "YAMS" flexibility module + p['s_span'] = p['Twr_s_span'] + p['m'] = p['m_full'] + p['EI'] = np.zeros((3,n+2)) + p['EI'][0,:] = p['EI_FA_full'] + p['EI'][1,:] = p['EI_SS_full'] + + p['s_G0'] = np.zeros((3, len(p['Twr_s_span']))) + p['s_G0'][2,:] = p['Twr_s_span'] # TODO add hub radius +# for k,v in p.items(): +# if hasattr(v, '__len__'): +# v = np.asarray(v) +# if len(v.shape)>=3: +# print('{:15s}:({})'.format(k,v.shape)) +# elif len(v.shape)==2: +# print('{:15s}:\n {} ({})'.format(k,v, v.shape)) +# else: +# n=len(v) +# print('{:15s}:{} ({})'.format(k,v,n)) +# else: +# print('{:15s}:{}'.format(k,v)) + + + # --- "purely" elastic mass + p['MTFA_e'] = p['MTFA'] + p['MTSS_e'] = p['MTSS'] + for I in [0,1]: #Loop through all tower modes in a single direction + p['MTFA_e'][I,I] = p['MTFA'][I,I] - p['TwrTpMass'] + p['MTSS_e'][I,I] = p['MTSS'][I,I] - p['TwrTpMass'] + + # TODO TODO TODO Check signs + p['KTFAGrav_nd'] = -p['KTFAGrav']/gravity # reverting to a dimensionless factor + p['KTSSGrav_nd'] = -p['KTSSGrav']/gravity # reverting to a dimensionless factor + p['KTFAGravTT_nd'] = -p['KTFAGravTT']/gravity/p['TwrTpMass'] # reverting to a dimensionless factor + p['KTSSGravTT_nd'] = -p['KTSSGravTT']/gravity/p['TwrTpMass'] # reverting to a dimensionless factor + + + return p + +def towerDerivedParameters(p): + """ Compute blade derived parameters, suitable for SID: + - Inertial matrices + - Stiffness matrices + The parameters are computed "by hand" (as opposed to using GMBeam) + NOTE: this function is mostly for debugging purposes. + Use GMBeam with method='OpenFAST' instead + + Order of shape functions: FA1 FA2 SS1 SS2 + """ + + # param.tower_Ct1_1_1_3= p.KTFAGrav(1, 1); + # param.tower_Ct1_2_2_3= p.KTSSGrav(1, 1); + + # param.tower_frame_11_origin1_1_1_1= 1; + # param.tower_frame_11_origin1_2_2_1= 1; + # param.tower_frame_11_phi1_1_3_1 = p.KTFAGravTT(1, 1) ; + # param.tower_frame_11_phi1_2_3_2 = p.KTSSGravTT(1, 1) ; + # param.tower_frame_11_psi0_1_2 = -p.TwrFASF(1, end, 2); + # param.tower_frame_11_psi0_2_1 = p.TwrSSSF(1, end, 2) ; + + nq = 4 + # --- Rigid mass matrix terms + p['mdCM'] = np.zeros((3)) + p['mdCM'][2]= sum(p['TElmntMass'][:]*(p['HNodes'])); + p['J'] = np.zeros((3,3)) + p['J'][0,0] = sum(p['TElmntMass'][:]*(p['HNodes'])**2) + p['J'][1,1] = sum(p['TElmntMass'][:]*(p['HNodes'])**2) + # --- Elastic matrices + p['Ke0'] = np.zeros((nq,nq)) # Without stiffening + p['De'] = np.zeros((nq,nq)) + p['Me'] = np.zeros((nq,nq)) + for I in [0,1]: # Loop through all tower DOFs in one direction + for L in [0,1]: # Loop through all tower DOFs in one direction + p['Me'][I ,L] = p['MTFA_e'][I, L] + p['Me'][I+2,L+2] = p['MTSS_e'][I, L] + p['Ke0'][I ,L] = p['KTFA'][I, L] + p['Ke0'][I+2,L+2] = p['KTSS'][I, L] + p['De'][I ,L] = p['CTFA'][I, L] + p['De'][I+2,L+2] = p['CTSS'][I, L] + + # --- Self-weight, and top mass geometrical stiffness + p['Kg_SW'] = np.zeros((nq,nq)) + p['Kg_TM'] = np.zeros((nq,nq)) + for I in [0,1]: + for L in [0,1]: # Loop through all tower DOFs in one direction + p['Kg_SW'][I, L] = p['KTFAGrav'][I,L] + p['Kg_SW'][I+2, L+2] = p['KTSSGrav'][I,L] + p['Kg_TM'][I, L] = p['KTFAGravTT'][I,L] + p['Kg_TM'][I+2, L+2] = p['KTSSGravTT'][I,L] + + p['Ke'] = p['Ke0'] + p['Kg_SW'] # + p['Kg_TM'] # NOTE: TM not included, needs further validation + # --- Elastic mass matrix terms + # Ct + p['Ct'] = np.zeros((nq,3)) + p['Ct'][0,0] = sum(np.squeeze(p['TwrFASF'][0, 1:-1, 0])*p['TElmntMass'][:]) + p['Ct'][1,0] = sum(np.squeeze(p['TwrFASF'][1, 1:-1, 0])*p['TElmntMass'][:]) + p['Ct'][2,1] = sum(np.squeeze(p['TwrSSSF'][0, 1:-1, 0])*p['TElmntMass'][:]) + p['Ct'][3,1] = sum(np.squeeze(p['TwrSSSF'][1, 1:-1, 0])*p['TElmntMass'][:]) + # Cr + p['Cr'] = np.zeros((nq,3)) + p['Cr'][0,1]= sum((p['HNodes'])*np.squeeze(p['TwrFASF'][0, 1:-1, 0])*p['TElmntMass'][:]) + p['Cr'][1,1]= sum((p['HNodes'])*np.squeeze(p['TwrFASF'][1, 1:-1, 0])*p['TElmntMass'][:]) + p['Cr'][2,0]= -sum((p['HNodes'])*np.squeeze(p['TwrSSSF'][0, 1:-1, 0])*p['TElmntMass'][:]) + p['Cr'][3,0]= -sum((p['HNodes'])*np.squeeze(p['TwrSSSF'][1, 1:-1, 0])*p['TElmntMass'][:]) + return p + + + +def ED_qDict2q(qDict): + """ + From a dictionary of values, return the flat array of degrees of freedom ordered liked ElastoDyn. + The dictionary is a convenient way to just specify few DOFs + Example: + qDict={'Hv':0} + """ + q = np.zeros(ED_MaxDOFs) + + qMAP = {'Sg':DOF_Sg,'Sw':DOF_Sw,'Hv':DOF_Hv, 'R':DOF_R, 'P':DOF_P, 'Y':DOF_Y} + qMAP.update({'TFA1':DOF_TFA1, 'TSS1':DOF_TSS1, 'TFA2':DOF_TFA2, 'TSS2':DOF_TSS2}) + qMAP.update({'Yaw':DOF_Yaw, 'RFrl':DOF_RFrl, 'GeAz':DOF_GeAz, 'DrTr':DOF_DrTr}) + + for k,v in qDict.items(): + if k not in qMAP.keys(): + raise Exception('Key {} not supported by qMAP'.format(k)) + if k=='TSS1' or k=='TSS2': + q[qMAP[k]] = -v # <<<< NOTE: DOF has a negative convention + else: + q[qMAP[k]] = v + return q + + +def EDVec2IEC(v, offset=None): + """ Convert a vector/array of coordinates from ElastoDyn coordinate system to IEC""" + if offset is None: + offset = np.array([0,0,0]) + v = np.asarray(v) + if len(v.shape)==1: + vIEC = np.array([ v[0]+offset[0], -v[2]+offset[1], v[1]+offset[2]]) + elif len(v.shape)==2 and v.shape[1]==3: + vIEC = np.column_stack( [ v[:,0]+offset[0], -v[:,2]+offset[1], v[:,1]+offset[2]] ) + else: + raise NotImplementedError() + return vIEC + +def EDSysVectoIECDCM(a1, a2, a3): + """ Return DCM from ElastoDyn coordsys vectors (in a different coordinate system)""" + R_g2t = np.zeros((3,3)) + R_g2t[:,0] = [ a1[0], -a3[0], a2[0]] + R_g2t[:,1] = [-a1[2], a3[2], -a2[2]] + R_g2t[:,2] = [ a1[1], -a3[1], a2[1]] + return R_g2t + +def EDSysVecRot(R,z1,z2,z3): + """ + Apply transformation matrix to vectors to obtain other vectors + """ + a1 = R[0,0]*z1 + R[0,1]*z2 + R[0,2]*z3 + a2 = R[1,0]*z1 + R[1,1]*z2 + R[1,2]*z3 + a3 = R[2,0]*z1 + R[2,1]*z2 + R[2,2]*z3 + return a1, a2 ,a3 + + + + +def ED_CoordSys(q=None, qDict=None, TwrFASF=None, TwrSSSF=None): + """ + Return ElastoDyn coordinate systems in ElastoDyn and IEC convention: + + See ElastoDyn line 5978 + See ElastoDyn line 1678 + + INPUTS: + - q + OR + - qDict + """ + # + if qDict is not None: + q = ED_qDict2q(qDict) + + + from welib.yams.rotations import smallRot_OF + # --- Inertial frame coordinate system: + z1 = np.array([ 1, 0, 0]) # Vector / direction z1 (= xi from the IEC coord. system). + z2 = np.array([ 0, 1, 0]) # Vector / direction z2 (= zi from the IEC coord. system). + z3 = np.array([ 0, 0, 1]) # Vector / direction z3 (= -yi from the IEC coord. system). + R_g2g = np.eye(3) + # --- Tower base / platform coordinate system: + # Vector / direction a1 (= xt from the IEC coord. system) + # Vector / direction a2 (= zt from the IEC coord. system) + # Vector / direction a3 (= -yt from the IEC coord. system) + R = smallRot_OF(q[DOF_R], q[DOF_Y], -q[DOF_P]) + a1, a2, a3 = EDSysVecRot(R, z1, z2, z3) + # IEC Coordinate system + R_g2t = EDSysVectoIECDCM(a1, a2, a3) + + # --- Tower element-fixed coordinate system: + # Vector / direction t1 for tower node J (= Lxt from the IEC coord. system). + # Vector / direction t2 for tower node J (= Lzt from the IEC coord. system). + # Vector / direction t3 for tower node J (= -Lyt from the IEC coord. system). + if TwrFASF is not None: + TwrNodes = TwrFASF.shape[1] - 2 + t1 = np.zeros((TwrNodes,3)) + t2 = np.zeros((TwrNodes,3)) + t3 = np.zeros((TwrNodes,3)) + R_g2Ts = np.zeros((TwrNodes,3,3))# List of transformations from global to tower elements + for j in range(TwrNodes): + # Slope V: Mode 1 V Mode 2 V + ThetaFA = -TwrFASF[0,j,1]*q[DOF_TFA1] - TwrFASF[1,j,1]*q[DOF_TFA2] + ThetaSS = TwrSSSF[0,j,1]*q[DOF_TSS1] + TwrSSSF[1,j,1]*q[DOF_TSS2] + R = smallRot_OF(ThetaSS, 0, ThetaFA) + t1[j,:], t2[j,:], t3[j,:] = EDSysVecRot(R, a1, a2, a3) + R_g2Ts[j,:,:] =EDSysVectoIECDCM(t1[j,:], t2[j,:], t3[j,:]) + + # --- Tower-top / base plate coordinate system: + # Slope V: Mode 1 V Mode 2 V + #print('>>> Coupling coeffs FA', -TwrFASF[0,-1,1], -TwrFASF[0,-1,1]) + #print('>>> Coupling coeffs SS', TwrSSSF[0,-1,1], TwrSSSF[0,-1,1]) + ThetaFA = -TwrFASF[0,-1,1]*q[DOF_TFA1] - TwrFASF[1,-1,1]*q[DOF_TFA2] + ThetaSS = TwrSSSF[0,-1,1]*q[DOF_TSS1] + TwrSSSF[1,-1,1]*q[DOF_TSS2] + R = smallRot_OF(ThetaSS, 0, ThetaFA) + b1, b2, b3 = EDSysVecRot(R, a1, a2, a3) + R_g2b = EDSysVectoIECDCM(b1, b2, b3) + else: + t1, t2, t3 = None, None, None + R_g2Ts = None + + b1, b2, b3 = a1, a2, a3 + R_g2b = R_g2t + + # --- Nacelle / yaw coordinate system: + CNacYaw = np.cos(q[DOF_Yaw]) + SNacYaw = np.sin(q[DOF_Yaw]) + d1 = CNacYaw*b1 - SNacYaw*b3 # Vector / direction d1 (= xn from the IEC coord. system). + d2 = b2 # Vector / direction d2 (= zn from the IEC coord. system). + d3 = SNacYaw*b1 + CNacYaw*b3 # Vector / direction d3 (= -yn from the IEC coord. system). + R_g2n = EDSysVectoIECDCM(d1, d2, d3) + + # --- To be continued + # ... + # ! Rotor-furl coordinate system: + # + # CRotFurl = COS( x%QT(DOF_RFrl) ) + # SRotFurl = SIN( x%QT(DOF_RFrl) ) + # + # CoordSys%rf1 = ( ( 1.0 - p%CRFrlSkw2*p%CRFrlTlt2 )*CRotFurl + p%CRFrlSkw2*p%CRFrlTlt2 )*CoordSys%d1 & + # + ( p%CRFrlSkew*p%CSRFrlTlt*( 1.0 - CRotFurl ) - p%SRFrlSkew*p%CRFrlTilt*SRotFurl )*CoordSys%d2 & + # + ( p%CSRFrlSkw*p%CRFrlTlt2*( CRotFurl - 1.0 ) - p%SRFrlTilt*SRotFurl )*CoordSys%d3 + # CoordSys%rf2 = ( p%CRFrlSkew*p%CSRFrlTlt*( 1.0 - CRotFurl ) + p%SRFrlSkew*p%CRFrlTilt*SRotFurl )*CoordSys%d1 & + # + ( p%CRFrlTlt2* CRotFurl + p%SRFrlTlt2 )*CoordSys%d2 & + # + ( p%SRFrlSkew*p%CSRFrlTlt*( CRotFurl - 1.0 ) + p%CRFrlSkew*p%CRFrlTilt*SRotFurl )*CoordSys%d3 + # CoordSys%rf3 = ( p%CSRFrlSkw*p%CRFrlTlt2*( CRotFurl - 1.0 ) + p%SRFrlTilt*SRotFurl )*CoordSys%d1 & + # + ( p%SRFrlSkew*p%CSRFrlTlt*( CRotFurl - 1.0 ) - p%CRFrlSkew*p%CRFrlTilt*SRotFurl )*CoordSys%d2 & + # + ( ( 1.0 - p%SRFrlSkw2*p%CRFrlTlt2 )*CRotFurl + p%SRFrlSkw2*p%CRFrlTlt2 )*CoordSys%d3 + # CoordSys%rfa = p%CRFrlSkew*p%CRFrlTilt*CoordSys%d1 + p%SRFrlTilt*CoordSys%d2 - p%SRFrlSkew*p%CRFrlTilt*CoordSys%d3 + # + # + # ! Shaft coordinate system: + # CoordSys%c1 = p%CShftSkew*p%CShftTilt*CoordSys%rf1 + p%SShftTilt*CoordSys%rf2 - p%SShftSkew*p%CShftTilt*CoordSys%rf3 ! Vector / direction c1 (= xs from the IEC coord. system). + # CoordSys%c2 = -p%CShftSkew*p%SShftTilt*CoordSys%rf1 + p%CShftTilt*CoordSys%rf2 + p%SShftSkew*p%SShftTilt*CoordSys%rf3 ! Vector / direction c2 (= zs from the IEC coord. system). + # CoordSys%c3 = p%SShftSkew* CoordSys%rf1 + p%CShftSkew* CoordSys%rf3 ! Vector / direction c3 (= -ys from the IEC coord. system). + # ! Azimuth coordinate system: + # CAzimuth = COS( x%QT(DOF_DrTr) + x%QT(DOF_GeAz) ) + # SAzimuth = SIN( x%QT(DOF_DrTr) + x%QT(DOF_GeAz) ) + # CoordSys%e1 = CoordSys%c1 ! Vector / direction e1 (= xa from the IEC coord. system). + # CoordSys%e2 = CAzimuth*CoordSys%c2 + SAzimuth*CoordSys%c3 ! Vector / direction e2 (= ya from the IEC coord. system). + # CoordSys%e3 = -SAzimuth*CoordSys%c2 + CAzimuth*CoordSys%c3 ! Vector / direction e3 (= za from the IEC coord. system). + # ! Teeter coordinate system: + # ! Lets define TeetAng, which is the current teeter angle (= QT(DOF_Teet) for + # ! 2-blader or 0 for 3-blader) and is used in place of QT(DOF_Teet) + # ! throughout SUBROUTINE RtHS(). Doing it this way, we can run the same + # ! equations of motion for both the 2 and 3-blader configurations even + # ! though a 3-blader does not have a teetering DOF. + # IF ( p%NumBl == 2 ) THEN ! 2-blader + # dat%TeetAng = x%QT (DOF_Teet) + # dat%TeetAngVel = x%QDT(DOF_Teet) + # ELSE ! 3-blader + # dat%TeetAng = 0.0 ! Teeter is not an available DOF for a 3-blader + # dat%TeetAngVel = 0.0 ! Teeter is not an available DOF for a 3-blader + # ENDIF + # CTeetAng = COS( dat%TeetAng ) + # STeetAng = SIN( dat%TeetAng ) + # CoordSys%f1 = CTeetAng*CoordSys%e1 - STeetAng*CoordSys%e3 ! Vector / direction f1. + # CoordSys%f2 = CoordSys%e2 ! Vector / direction f2. + # CoordSys%f3 = STeetAng*CoordSys%e1 + CTeetAng*CoordSys%e3 ! Vector / direction f3. + # ! Hub / delta-3 coordinate system: + # CoordSys%g1 = CoordSys%f1 ! Vector / direction g1 (= xh from the IEC coord. system). + # CoordSys%g2 = p%CosDel3*CoordSys%f2 + p%SinDel3*CoordSys%f3 ! Vector / direction g2 (= yh from the IEC coord. system). + # CoordSys%g3 = -p%SinDel3*CoordSys%f2 + p%CosDel3*CoordSys%f3 ! Vector / direction g3 (= zh from the IEC coord. system). + # DO K = 1,p%NumBl ! Loop through all blades + # ! Hub (Prime) coordinate system rotated to match blade K. + # gRotAng = p%TwoPiNB*(K-1) + # CgRotAng = COS( gRotAng ) + # SgRotAng = SIN( gRotAng ) + # g1Prime = CoordSys%g1 + # g2Prime = CgRotAng*CoordSys%g2 + SgRotAng*CoordSys%g3 + # g3Prime = -SgRotAng*CoordSys%g2 + CgRotAng*CoordSys%g3 + # ! Coned coordinate system: + # CoordSys%i1(K,:) = p%CosPreC(K)*g1Prime - p%SinPreC(K)*g3Prime ! i1(K,:) = vector / direction i1 for blade K (= xcK from the IEC coord. system). + # CoordSys%i2(K,:) = g2Prime ! i2(K,:) = vector / direction i2 for blade K (= ycK from the IEC coord. system). + # CoordSys%i3(K,:) = p%SinPreC(K)*g1Prime + p%CosPreC(K)*g3Prime ! i3(K,:) = vector / direction i3 for blade K (= zcK from the IEC coord. system). + # ! Blade / pitched coordinate system: + # CosPitch = COS( REAL(BlPitch(K),R8Ki) ) + # SinPitch = SIN( REAL(BlPitch(K),R8Ki) ) + # CoordSys%j1(K,:) = CosPitch*CoordSys%i1(K,:) - SinPitch*CoordSys%i2(K,:) ! j1(K,:) = vector / direction j1 for blade K (= xbK from the IEC coord. system). + # CoordSys%j2(K,:) = SinPitch*CoordSys%i1(K,:) + CosPitch*CoordSys%i2(K,:) ! j2(K,:) = vector / direction j2 for blade K (= ybK from the IEC coord. system). + # CoordSys%j3(K,:) = CoordSys%i3(K,:) ! j3(K,:) = vector / direction j3 for blade K (= zbK from the IEC coord. system). + # DO J = 0,p%TipNode ! Loop through the blade nodes / elements + # ! Blade coordinate system aligned with local structural axes (not element fixed): + # Lj1 = p%CThetaS(K,J)*CoordSys%j1(K,:) - p%SThetaS(K,J)*CoordSys%j2(K,:) ! vector / direction Lj1 at node J for blade K + # Lj2 = p%SThetaS(K,J)*CoordSys%j1(K,:) + p%CThetaS(K,J)*CoordSys%j2(K,:) ! vector / direction Lj2 at node J for blade K + # Lj3 = CoordSys%j3(K,:) ! vector / direction Lj3 at node J for blade K + # ! Blade element-fixed coordinate system aligned with local structural axes: + # ThetaOoP = p%TwistedSF(K,1,1,J,1)*x%QT( DOF_BF(K,1) ) & + # + p%TwistedSF(K,1,2,J,1)*x%QT( DOF_BF(K,2) ) & + # + p%TwistedSF(K,1,3,J,1)*x%QT( DOF_BE(K,1) ) + # ThetaIP = - p%TwistedSF(K,2,1,J,1)*x%QT( DOF_BF(K,1) ) & + # - p%TwistedSF(K,2,2,J,1)*x%QT( DOF_BF(K,2) ) & + # - p%TwistedSF(K,2,3,J,1)*x%QT( DOF_BE(K,1) ) + # ThetaLxb = p%CThetaS(K,J)*ThetaIP - p%SThetaS(K,J)*ThetaOoP + # ThetaLyb = p%SThetaS(K,J)*ThetaIP + p%CThetaS(K,J)*ThetaOoP + # CALL SmllRotTrans( 'blade deflection (ElastoDyn SetCoordSy)', ThetaLxb, ThetaLyb, 0.0_R8Ki, TransMat, TRIM(Num2LStr(t))//' s', ErrStat2, ErrMsg2 ) ! Get the transformation matrix, TransMat, from blade coordinate system aligned with local structural axes (not element fixed) to blade element-fixed coordinate system aligned with local structural axes. + # CoordSys%n1(K,J,:) = TransMat(1,1)*Lj1 + TransMat(1,2)*Lj2 + TransMat(1,3)*Lj3 ! Vector / direction n1 for node J of blade K (= LxbK from the IEC coord. system). + # CoordSys%n2(K,J,:) = TransMat(2,1)*Lj1 + TransMat(2,2)*Lj2 + TransMat(2,3)*Lj3 ! Vector / direction n2 for node J of blade K (= LybK from the IEC coord. system). + # CoordSys%n3(K,J,:) = TransMat(3,1)*Lj1 + TransMat(3,2)*Lj2 + TransMat(3,3)*Lj3 ! Vector / direction n3 for node J of blade K (= LzbK from the IEC coord. system). + # ! skip these next CoordSys variables at the root and the tip; they are required only for AD14: + # if (j == 0 .or. j==p%TipNode) cycle + # ! Blade element-fixed coordinate system used for calculating and returning + # ! aerodynamics loads: + # ! This coordinate system is rotated about positive n3 by the angle + # ! BlPitch(K) + ThetaS(K,J) and is coincident with the i-vector triad + # ! when the blade is undeflected. + # CPitPTwstS = CosPitch*p%CThetaS(K,J) - SinPitch*p%SThetaS(K,J) ! = COS( BlPitch(K) + ThetaS(K,J) ) found using the sum of angles formulae of cosine. + # SPitPTwstS = CosPitch*p%SThetaS(K,J) + SinPitch*p%CThetaS(K,J) ! = SIN( BlPitch(K) + ThetaS(K,J) ) found using the sum of angles formulae of sine. + # CoordSys%m1(K,J,:) = CPitPTwstS*CoordSys%n1(K,J,:) + SPitPTwstS*CoordSys%n2(K,J,:) ! m1(K,J,:) = vector / direction m1 for node J of blade K (used to calc. and return aerodynamic loads from AeroDyn). + # CoordSys%m2(K,J,:) = -SPitPTwstS*CoordSys%n1(K,J,:) + CPitPTwstS*CoordSys%n2(K,J,:) ! m2(K,J,:) = vector / direction m2 for node J of blade K (used to calc. and return aerodynamic loads from AeroDyn). + # CoordSys%m3(K,J,:) = CoordSys%n3(K,J,:) ! m3(K,J,:) = vector / direction m3 for node J of blade K (used to calc. and return aerodynamic loads from AeroDyn). + # ! Calculate the trailing edge coordinate system used in noise calculations. + # ! This coordinate system is blade element-fixed and oriented with the local + # ! aerodynamic axes (te2 points toward trailing edge, te1 points toward + # ! suction surface): + # CPitPTwstA = CosPitch*p%CAeroTwst(J) - SinPitch*p%SAeroTwst(J) ! = COS( BlPitch(K) + AeroTwst(J) ) found using the sum of angles formulae of cosine. + # SPitPTwstA = CosPitch*p%SAeroTwst(J) + SinPitch*p%CAeroTwst(J) ! = SIN( BlPitch(K) + AeroTwst(J) ) found using the sum of angles formulae of sine. + # CoordSys%te1(K,J,:) = CPitPTwstA*CoordSys%m1(K,J,:) - SPitPTwstA*CoordSys%m2(K,J,:) ! te1(K,J,:) = vector / direction te1 for node J of blade K (used to calc. noise and to calc. and return aerodynamic loads from AeroDyn). + # CoordSys%te2(K,J,:) = SPitPTwstA*CoordSys%m1(K,J,:) + CPitPTwstA*CoordSys%m2(K,J,:) ! te2(K,J,:) = vector / direction te2 for node J of blade K (used to calc. noise and to calc. and return aerodynamic loads from AeroDyn). + # CoordSys%te3(K,J,:) = CoordSys%m3(K,J,:) ! te3(K,J,:) = vector / direction te3 for node J of blade K (used to calc. noise and to calc. and return aerodynamic loads from AeroDyn). + # ENDDO ! J - Blade nodes / elements + # ENDDO ! K - Blades + # ! Tail-furl coordinate system: + # CTailFurl = COS( x%QT(DOF_TFrl) ) + # STailFurl = SIN( x%QT(DOF_TFrl) ) + # CoordSys%tf1 = ( ( 1.0 - p%CTFrlSkw2*p%CTFrlTlt2 )*CTailFurl + p%CTFrlSkw2*p%CTFrlTlt2 )*CoordSys%d1 & + # + ( p%CTFrlSkew*p%CSTFrlTlt*( 1.0 - CTailFurl ) - p%STFrlSkew*p%CTFrlTilt*STailFurl )*CoordSys%d2 & + # + ( p%CSTFrlSkw*p%CTFrlTlt2*( CTailFurl - 1.0 ) - p%STFrlTilt*STailFurl )*CoordSys%d3 + # CoordSys%tf2 = ( p%CTFrlSkew*p%CSTFrlTlt*( 1.0 - CTailFurl ) + p%STFrlSkew*p%CTFrlTilt*STailFurl )*CoordSys%d1 & + # + ( p%CTFrlTlt2* CTailFurl + p%STFrlTlt2 )*CoordSys%d2 & + # + ( p%STFrlSkew*p%CSTFrlTlt*( CTailFurl - 1.0 ) + p%CTFrlSkew*p%CTFrlTilt*STailFurl )*CoordSys%d3 + # CoordSys%tf3 = ( p%CSTFrlSkw*p%CTFrlTlt2*( CTailFurl - 1.0 ) + p%STFrlTilt*STailFurl )*CoordSys%d1 & + # + ( p%STFrlSkew*p%CSTFrlTlt*( CTailFurl - 1.0 ) - p%CTFrlSkew*p%CTFrlTilt*STailFurl )*CoordSys%d2 & + # + ( ( 1.0 - p%STFrlSkw2*p%CTFrlTlt2 )*CTailFurl + p%STFrlSkw2*p%CTFrlTlt2 )*CoordSys%d3 + # CoordSys%tfa = p%CTFrlSkew*p%CTFrlTilt*CoordSys%d1 + p%STFrlTilt*CoordSys%d2 - p%STFrlSkew*p%CTFrlTilt*CoordSys%d3 + # R_ + + CoordSys = dict() + # ED + CoordSys['z1'] = z1 + CoordSys['z2'] = z2 + CoordSys['z3'] = z3 + CoordSys['a1'] = a1 + CoordSys['a2'] = a2 + CoordSys['a3'] = a3 + CoordSys['t1'] = t1 + CoordSys['t2'] = t2 + CoordSys['t3'] = t3 + CoordSys['d1'] = d1 + CoordSys['d2'] = d2 + CoordSys['d3'] = d3 + # IEC + CoordSys['R_g2f'] = R_g2t + CoordSys['R_g2t'] = R_g2t + CoordSys['R_g2Ts'] = R_g2Ts # To tower elements + CoordSys['R_g2n'] = R_g2n # To nacelle (including nacelle yaw) + return CoordSys + + +def ED_Positions(q=None, qDict=None, CoordSys=None, p=None, dat=None, IEC=None): + """ + See ElastoDyn.f90 CalculatePositions + # !> This routine is used to calculate the positions stored in other states that are used in both the + # !! CalcOutput and CalcContStateDeriv routines. + # SUBROUTINE CalculatePositions( p, x, CoordSys, dat ) + """ + if qDict is not None: + QT = ED_qDict2q(qDict) + else: + QT = q + + if dat is None: + dat = dict() + TTopNode=-1 + # Define the position vectors between the various points on the wind turbine + # that are not dependent on the distributed tower or blade parameters: + dat['rZ'] = QT[DOF_Sg]* CoordSys['z1'] + QT[DOF_Hv] * CoordSys['z2'] - QT[DOF_Sw]* CoordSys['z3'] # Position vector from inertia frame origin to platform reference (point Z). + dat['rZY'] = p['rZYzt']* CoordSys['a2'] + p['PtfmCMxt']*CoordSys['a1'] - p['PtfmCMyt']*CoordSys['a3'] # Position vector from platform reference (point Z) to platform mass center (point Y). + dat['rZT0'] = p['rZT0zt']* CoordSys['a2'] # Position vector from platform reference (point Z) to tower base (point T(0)) + dat['rZO'] = ( QT[DOF_TFA1] + QT[DOF_TFA2] )*CoordSys['a1'] # Position vector from platform reference (point Z) to tower-top / base plate (point O). + dat['rZO'] += ( p['RefTwrHt'] - 0.5*( p['AxRedTFA'][0,0,TTopNode]*QT[DOF_TFA1]*QT[DOF_TFA1] \ + + p['AxRedTFA'][1,1,TTopNode]*QT[DOF_TFA2]*QT[DOF_TFA2] \ + + 2.0*p['AxRedTFA'][0,1,TTopNode]*QT[DOF_TFA1]*QT[DOF_TFA2] \ + + p['AxRedTSS'][0,0,TTopNode]*QT[DOF_TSS1]*QT[DOF_TSS1] \ + + p['AxRedTSS'][1,1,TTopNode]*QT[DOF_TSS2]*QT[DOF_TSS2] \ + + 2.0*p['AxRedTSS'][0,1,TTopNode]*QT[DOF_TSS1]*QT[DOF_TSS2] ))*CoordSys['a2'] + dat['rZO'] += ( QT[DOF_TSS1] + QT[DOF_TSS2])*CoordSys['a3'] + dat['rOU'] = p['NacCMxn']*CoordSys['d1'] + p['NacCMzn'] *CoordSys['d2'] - p['NacCMyn'] *CoordSys['d3'] # Position vector from tower-top / base plate (point O) to nacelle center of mass (point U). +# dat['rOV'] = p['RFrlPnt_n(1)*CoordSys['d1 + p['RFrlPnt_n(3)*CoordSys['d2 - p['RFrlPnt_n(2)*CoordSys['d3 ! Position vector from tower-top / base plate (point O) to specified point on rotor-furl axis (point V). +# dat['rVIMU'] = p['rVIMUxn*CoordSys['rf1 + p['rVIMUzn *CoordSys['rf2 - p['rVIMUyn *CoordSys['rf3 ! Position vector from specified point on rotor-furl axis (point V) to nacelle IMU (point IMU). +# dat['rVD'] = p['rVDxn*CoordSys['rf1 + p['rVDzn *CoordSys['rf2 - p['rVDyn *CoordSys['rf3 ! Position vector from specified point on rotor-furl axis (point V) to center of mass of structure that furls with the rotor (not including rotor) (point D). +# dat['rVP'] = p['rVPxn*CoordSys['rf1 + p['rVPzn *CoordSys['rf2 - p['rVPyn *CoordSys['rf3 + p['OverHang*CoordSys['c1 ! Position vector from specified point on rotor-furl axis (point V) to teeter pin (point P). +# dat['rPQ'] = -p['UndSling*CoordSys['g1 ! Position vector from teeter pin (point P) to apex of rotation (point Q). +# dat['rQC'] = p['HubCM*CoordSys['g1 ! Position vector from apex of rotation (point Q) to hub center of mass (point C). +# dat['rOW'] = p['TFrlPnt_n(1)*CoordSys['d1 + p['TFrlPnt_n(3) *CoordSys['d2 - p['TFrlPnt_n(2)*CoordSys['d3 ! Position vector from tower-top / base plate (point O) to specified point on tail-furl axis (point W). +# dat['rWI'] = p['rWIxn*CoordSys['tf1 + p['rWIzn*CoordSys['tf2 - p['rWIyn*CoordSys['tf3 ! Position vector from specified point on tail-furl axis (point W) to tail boom center of mass (point I). +# dat['rWJ'] = p['rWJxn*CoordSys['tf1 + p['rWJzn*CoordSys['tf2 - p['rWJyn*CoordSys['tf3 ! Position vector from specified point on tail-furl axis (point W) to tail fin center of mass (point J). +# dat['rPC'] = dat['rPQ'] + dat['rQC'] # Position vector from teeter pin (point P) to hub center of mass (point C). + dat['rT0O'] = dat['rZO'] - dat['rZT0'] # Position vector from the tower base (point T(0)) to tower-top / base plate (point O). + dat['rO'] = dat['rZ'] + dat['rZO'] # Position vector from inertial frame origin to tower-top / base plate (point O). +# dat['rV'] = dat['rO'] + dat['rOV'] # Position vector from inertial frame origin to specified point on rotor-furl axis (point V) +# !dat['rP'] = dat['rO'] + dat['rOV'] + dat['rVP'] # Position vector from inertial frame origin to teeter pin (point P). +# dat['rP'] = dat['rV'] + dat['rVP'] # Position vector from inertial frame origin to teeter pin (point P). +# dat['rQ'] = dat['rP'] + dat['rPQ'] # Position vector from inertial frame origin to apex of rotation (point Q). +# dat['rJ'] = dat['rO'] + dat['rOW']+ dat['rWJ'] # Position vector from inertial frame origin to tail fin center of mass (point J).' +# +# +# DO K = 1,p['NumBl ! Loop through all blades +# ! Calculate the position vector of the tip: +# dat['rS0S(:,K,p['TipNode) = ( p['TwistedSF(K,1,1,p['TipNode,0)*QT( DOF_BF(K,1) ) & ! Position vector from the blade root (point S(0)) to the blade tip (point S(p['BldFlexL)). +# + p['TwistedSF(K,1,2,p['TipNode,0)*QT( DOF_BF(K,2) ) & +# + p['TwistedSF(K,1,3,p['TipNode,0)*QT( DOF_BE(K,1) ) )*CoordSys['j1(K,:) & +# + ( p['TwistedSF(K,2,1,p['TipNode,0)*QT( DOF_BF(K,1) ) & +# + p['TwistedSF(K,2,2,p['TipNode,0)*QT( DOF_BF(K,2) ) & +# + p['TwistedSF(K,2,3,p['TipNode,0)*QT( DOF_BE(K,1) ) )*CoordSys['j2(K,:) & +# + ( p['BldFlexL - 0.5* & +# ( p['AxRedBld(K,1,1,p['TipNode)*QT( DOF_BF(K,1) )*QT( DOF_BF(K,1) ) & +# + p['AxRedBld(K,2,2,p['TipNode)*QT( DOF_BF(K,2) )*QT( DOF_BF(K,2) ) & +# + p['AxRedBld(K,3,3,p['TipNode)*QT( DOF_BE(K,1) )*QT( DOF_BE(K,1) ) & +# + 2.*p['AxRedBld(K,1,2,p['TipNode)*QT( DOF_BF(K,1) )*QT( DOF_BF(K,2) ) & +# + 2.*p['AxRedBld(K,2,3,p['TipNode)*QT( DOF_BF(K,2) )*QT( DOF_BE(K,1) ) & +# + 2.*p['AxRedBld(K,1,3,p['TipNode)*QT( DOF_BF(K,1) )*QT( DOF_BE(K,1) ) ) )*CoordSys['j3(K,:) +# dat['rQS (:,K,p['TipNode) = dat['rS0S(:,K,p['TipNode) + p['HubRad*CoordSys['j3(K,:) ! Position vector from apex of rotation (point Q) to the blade tip (point S(p['BldFlexL)). +# dat['rS (:,K,p['TipNode) = dat['rQS (:,K,p['TipNode) + dat['rQ ! Position vector from inertial frame origin to the blade tip (point S(p['BldFlexL)). +# +# ! position vectors for blade root node: +# dat['rQS (:,K,0) = p['HubRad*CoordSys['j3(K,:) +# dat['rS (:,K,0) = p['HubRad*CoordSys['j3(K,:) + dat['rQ +# ! Calculate the position vector from the teeter pin to the blade root: +# dat['rPS0(:,K) = dat['rPQ + p['HubRad*CoordSys['j3(K,:) ! Position vector from teeter pin (point P) to blade root (point S(0)). +# DO J = 1,p['BldNodes ! Loop through the blade nodes / elements +# ! Calculate the position vector of the current node: +# dat['rS0S(:,K,J) = ( p['TwistedSF(K,1,1,J,0)*QT( DOF_BF(K,1) ) & ! Position vector from the blade root (point S(0)) to the current node (point S(RNodes(J)). +# + p['TwistedSF(K,1,2,J,0)*QT( DOF_BF(K,2) ) & +# + p['TwistedSF(K,1,3,J,0)*QT( DOF_BE(K,1) ) )*CoordSys['j1(K,:) & +# + ( p['TwistedSF(K,2,1,J,0)*QT( DOF_BF(K,1) ) & +# + p['TwistedSF(K,2,2,J,0)*QT( DOF_BF(K,2) ) & +# + p['TwistedSF(K,2,3,J,0)*QT( DOF_BE(K,1) ) )*CoordSys['j2(K,:) & +# + ( p['RNodes(J) - 0.5* & +# ( p['AxRedBld(K,1,1,J)*QT( DOF_BF(K,1) )*QT( DOF_BF(K,1) ) & +# + p['AxRedBld(K,2,2,J)*QT( DOF_BF(K,2) )*QT( DOF_BF(K,2) ) & +# + p['AxRedBld(K,3,3,J)*QT( DOF_BE(K,1) )*QT( DOF_BE(K,1) ) & +# + 2.0*p['AxRedBld(K,1,2,J)*QT( DOF_BF(K,1) )*QT( DOF_BF(K,2) ) & +# + 2.0*p['AxRedBld(K,2,3,J)*QT( DOF_BF(K,2) )*QT( DOF_BE(K,1) ) & +# + 2.0*p['AxRedBld(K,1,3,J)*QT( DOF_BF(K,1) )*QT( DOF_BE(K,1) ) ) )*CoordSys['j3(K,:) +# dat['rQS (:,K,J) = dat['rS0S(:,K,J) + p['HubRad*CoordSys['j3(K,:) ! Position vector from apex of rotation (point Q) to the current node (point S(RNodes(J)). +# dat['rS (:,K,J) = dat['rQS (:,K,J) + dat['rQ ! Position vector from inertial frame origin to the current node (point S(RNodes(J)). +# END DO !J = 1,p['BldNodes ! Loop through the blade nodes / elements +# END DO !K = 1,p['NumBl + # --- Tower element positions + TwrNodes = p['TwrFASF'].shape[1] - 2 + dat['rZT'] = np.zeros((TwrNodes+1,3)) + dat['rT0T'] = np.zeros((TwrNodes,3)) + dat['rT'] = np.zeros((TwrNodes,3)) + dat['rZT'][0,:] = dat['rZT0'] + # TODO vectorize + # TODO TODO TODO check that this is correct due to messy ED indexing (some starting at 0 some at 1) + for j in range(TwrNodes): + jj=j+1 + # Calculate the position vector of the current node: + dat['rT0T'][j,:] = ( p['TwrFASF'][0,jj,0]*QT[DOF_TFA1] + p['TwrFASF'][1,jj,0]*QT[DOF_TFA2] )*CoordSys['a1']# Position vector from base of flexible portion of tower (point T(0)) to current node (point T(j)). + dat['rT0T'][j,:] += + ( p['HNodes'][j] - 0.5*( p['AxRedTFA'][0,0,j]*QT[DOF_TFA1]*QT[DOF_TFA1] \ + + p['AxRedTFA'][1,1,jj]*QT[DOF_TFA2]*QT[DOF_TFA2] \ + + 2.0*p['AxRedTFA'][0,1,jj]*QT[DOF_TFA1]*QT[DOF_TFA2] \ + + p['AxRedTSS'][0,0,jj]*QT[DOF_TSS1]*QT[DOF_TSS1] \ + + p['AxRedTSS'][1,1,jj]*QT[DOF_TSS2]*QT[DOF_TSS2] \ + + 2.0*p['AxRedTSS'][0,1,jj]*QT[DOF_TSS1]*QT[DOF_TSS2] ) )*CoordSys['a2'] + dat['rT0T'][j,:] += ( p['TwrSSSF'][0,jj,0]*QT[DOF_TSS1] + p['TwrSSSF'][1,jj,0]*QT[DOF_TSS2] )*CoordSys['a3'] + dat['rZT'][jj,:] = dat['rZT0'] + dat['rT0T'][j,:] # Position vector from platform reference (point Z) to the current node (point T(HNodes(j)). + dat['rT'][j,:] = dat['rZ'] + dat['rZT'][jj,:] # Position vector from inertial frame origin to the current node (point T(HNodes(j)). + + + # --- IEC + if IEC is None: + IEC = dict() + IEC['r_F0'] = np.array([0,0,p['PtfmRefzt']]) + IEC['r_F'] = EDVec2IEC(dat['rZ']) + IEC['r_F0'] # ED ReftPtfm point + IEC['r_FT'] = EDVec2IEC(dat['rZT0']) # Tower base from Floater + IEC['r_T'] = IEC['r_F'] + IEC['r_FT'] + IEC['r_TN'] = EDVec2IEC(dat['rT0O']) # Tower top / base plate/ nacelle origin from tower base + IEC['r_N'] = EDVec2IEC(dat['rO'])+ IEC['r_F0'] # Tower top / base plate/ nacelle origin + IEC['r_NGn'] = EDVec2IEC(dat['rOU']) # From Tower top to nacelle COG + IEC['r_Gn'] = IEC['r_N'] + IEC['r_NGn'] # Nacelle COG + IEC['r_Ts'] = EDVec2IEC(dat['rT'], IEC['r_F0'] ) + + return dat, IEC + + +def ED_AngPosVelPAcc(q=None, qd=None, qDict=None, qdDict=None, CoordSys=None, p=None, dat=None, IEC=None): + """ + See ElastoDyn.f90 CalculateAngularPosVelPAcc + This routine is used to calculate the angular positions, velocities, and partial accelerations stored in other states that are used in + both the CalcOutput and CalcContStateDeriv routines. + SUBROUTINE CalculateAngularPosVelPAcc( p, x, CoordSys, dat ) + + """ + if qDict is not None: + QT = ED_qDict2q(qDict) + else: + QT = q + if qdDict is not None: + QDT = ED_qDict2q(qdDict) + else: + QDT = qd + + if dat is None: + dat = dict() +# REAL(ReKi) :: AngVelHM (3) ! Angular velocity of eleMent J of blade K (body M) in the hub (body H). +# REAL(ReKi) :: AngAccELt (3) ! Portion of the angular acceleration of the low-speed shaft (body L) in the inertia frame (body E for earth) associated with everything but the QD2T()'s. + #NDOF=ED_MaxDOFs + NDOF=11 # TODO + TwrNodes = p['TwrFASF'].shape[1] - 2 + NumBl=3 + # These angular velocities are allocated to start numbering a dimension with 0 instead of 1: + dat['PAngVelEB']=np.zeros((NDOF,2,3)) + dat['PAngVelER']=np.zeros((NDOF,2,3)) + dat['PAngVelEX']=np.zeros((NDOF,2,3)) + dat['PAngVelEA']=np.zeros((NDOF,2,3)) + dat['PAngVelEN']=np.zeros((NDOF,2,3)) + #dat['PAngVelEG']=np.zeros((NDOF,2,3)) + #dat['PAngVelEH']=np.zeros((NDOF,2,3)) + #dat['PAngVelEL']=np.zeros((NDOF,2,3)) + dat['PAngVelEF']=np.zeros((TwrNodes+1, NDOF,2,3)) + #dat['PAngVelEM']=np.zeros((NumBl,0:p%TipNode,NDOF,2,3)) + + dat['AngPosXF']=np.zeros((TwrNodes+1,3)) + dat['AngPosEF']=np.zeros((TwrNodes+1,3)) + dat['AngVelEF']=np.zeros((TwrNodes+1,3)) + + + + # --------------------------------------------------------------------------------} + # --- Angular and partial angular velocities + # --------------------------------------------------------------------------------{ + # Define the angular and partial angular velocities of all of the rigid bodies in the inertia frame: + # NOTE: PAngVelEN(I,D,:) = the Dth-derivative of the partial angular velocity of DOF I for body N in body E. + # --- Platform "X" + dat['PAngVelEX'][ :,0,:] = 0.0 + dat['PAngVelEX'][DOF_R ,0,:] = CoordSys['z1'] + dat['PAngVelEX'][DOF_P ,0,:] = -CoordSys['z3'] + dat['PAngVelEX'][DOF_Y ,0,:] = CoordSys['z2'] + dat['AngVelEX'] = QDT[DOF_R ]*dat['PAngVelEX'][DOF_R,0,:] + QDT[DOF_P]*dat['PAngVelEX'][DOF_P,0,:] + QDT[DOF_Y]*dat['PAngVelEX'][DOF_Y,0,:] + dat['AngPosEX'] = QT [DOF_R ]*dat['PAngVelEX'][DOF_R,0,:] + QT [DOF_P]*dat['PAngVelEX'][DOF_P,0,:] + QT [DOF_Y]*dat['PAngVelEX'][DOF_Y,0,:] + # --- Tower top "B" + dat['PAngVelEB'][ :,0,:] = dat['PAngVelEX'][:,0,:] + dat['PAngVelEB'][DOF_TFA1,0,:] = -p['TwrFASF'][0,-1,1]*CoordSys['a3'] + dat['PAngVelEB'][DOF_TSS1,0,:] = p['TwrSSSF'][0,-1,1]*CoordSys['a1'] + dat['PAngVelEB'][DOF_TFA2,0,:] = -p['TwrFASF'][1,-1,1]*CoordSys['a3'] + dat['PAngVelEB'][DOF_TSS2,0,:] = p['TwrSSSF'][1,-1,1]*CoordSys['a1'] + dat['AngVelEB'] = dat['AngVelEX'].copy() + dat['AngVelEB'] += QDT[DOF_TFA1]*dat['PAngVelEB'][DOF_TFA1,0,:] + dat['AngVelEB'] += QDT[DOF_TSS1]*dat['PAngVelEB'][DOF_TSS1,0,:] + dat['AngVelEB'] += QDT[DOF_TFA2]*dat['PAngVelEB'][DOF_TFA2,0,:] + dat['AngVelEB'] += QDT[DOF_TSS2]*dat['PAngVelEB'][DOF_TSS2,0,:] + dat['AngPosXB'] = QT [DOF_TFA1]*dat['PAngVelEB'][DOF_TFA1,0,:] + dat['AngPosXB'] += QT [DOF_TSS1]*dat['PAngVelEB'][DOF_TSS1,0,:] + dat['AngPosXB'] += QT [DOF_TFA2]*dat['PAngVelEB'][DOF_TFA2,0,:] + dat['AngPosXB'] += QT [DOF_TSS2]*dat['PAngVelEB'][DOF_TSS2,0,:] + # --- Nacelle "N" (yawed tower top) + dat['PAngVelEN'][ :,0,:]= dat['PAngVelEB'][:,0,:] + dat['PAngVelEN'][DOF_Yaw ,0,:]= CoordSys['d2'] + dat['AngVelEN'] = dat['AngVelEB'] + QDT[DOF_Yaw ]*dat['PAngVelEN'][DOF_Yaw ,0,:] +# +# dat['PAngVelER( :,0,:)= dat['PAngVelEN(:,0,:) +# dat['PAngVelER(DOF_RFrl,0,:)= CoordSys['rfa +# dat['AngVelER = dat['AngVelEN + QDT(DOF_RFrl)*dat['PAngVelER(DOF_RFrl,0,:) +# +# dat['PAngVelEL( :,0,:)= dat['PAngVelER(:,0,:) +# dat['PAngVelEL(DOF_GeAz,0,:)= CoordSys['c1 +# dat['PAngVelEL(DOF_DrTr,0,:)= CoordSys['c1 +# dat['AngVelEL = dat['AngVelER + QDT(DOF_GeAz)*dat['PAngVelEL(DOF_GeAz,0,:) & +# + QDT(DOF_DrTr)*dat['PAngVelEL(DOF_DrTr,0,:) +# +# dat['PAngVelEH( :,0,:)= dat['PAngVelEL(:,0,:) +# dat['AngVelEH = dat['AngVelEL +# IF ( p['NumBl == 2 ) THEN ! 2-blader +# dat['PAngVelEH(DOF_Teet,0,:)= CoordSys['f2 +# dat['AngVelEH = dat['AngVelEH + QDT(DOF_Teet)*dat['PAngVelEH(DOF_Teet,0,:) +# ENDIF +# +# dat['PAngVelEG( :,0,:) = dat['PAngVelER(:,0,:) +# dat['PAngVelEG(DOF_GeAz,0,:) = p['GBRatio*CoordSys['c1 +# dat['AngVelEG = dat['AngVelER + QDT(DOF_GeAz)*dat['PAngVelEG(DOF_GeAz,0,:) +# +# dat['PAngVelEA( :,0,:) = dat['PAngVelEN(:,0,:) +# dat['PAngVelEA(DOF_TFrl,0,:) = CoordSys['tfa +# dat['AngVelEA = dat['AngVelEN + QDT(DOF_TFrl)*dat['PAngVelEA(DOF_TFrl,0,:) +# +# +# +# ! Define the 1st derivatives of the partial angular velocities of all +# ! of the rigid bodies in the inertia frame and the portion of the angular +# ! acceleration of the rigid bodies in the inertia frame associated with +# ! everything but the QD2T()'s: +# dat['PAngVelEX( :,1,:) = 0.0 +# dat['AngAccEXt = 0.0 +# dat['PAngVelEB( :,1,:) = dat['PAngVelEX(:,1,:) +# dat['PAngVelEB(DOF_TFA1,1,:) = CROSS_PRODUCT( dat['AngVelEX, dat['PAngVelEB(DOF_TFA1,0,:) ) +# dat['PAngVelEB(DOF_TSS1,1,:) = CROSS_PRODUCT( dat['AngVelEX, dat['PAngVelEB(DOF_TSS1,0,:) ) +# dat['PAngVelEB(DOF_TFA2,1,:) = CROSS_PRODUCT( dat['AngVelEX, dat['PAngVelEB(DOF_TFA2,0,:) ) +# dat['PAngVelEB(DOF_TSS2,1,:) = CROSS_PRODUCT( dat['AngVelEX, dat['PAngVelEB(DOF_TSS2,0,:) ) +# dat['AngAccEBt = dat['AngAccEXt + QDT(DOF_TFA1)*dat['PAngVelEB(DOF_TFA1,1,:) & +# + QDT(DOF_TSS1)*dat['PAngVelEB(DOF_TSS1,1,:) & +# + QDT(DOF_TFA2)*dat['PAngVelEB(DOF_TFA2,1,:) & +# + QDT(DOF_TSS2)*dat['PAngVelEB(DOF_TSS2,1,:) +# dat['PAngVelEN( :,1,:) = dat['PAngVelEB(:,1,:) +# dat['PAngVelEN(DOF_Yaw ,1,:) = CROSS_PRODUCT( dat['AngVelEB, dat['PAngVelEN(DOF_Yaw ,0,:) ) +# dat['AngAccENt = dat['AngAccEBt + QDT(DOF_Yaw )*dat['PAngVelEN(DOF_Yaw ,1,:) +# +# dat['PAngVelER( :,1,:) = dat['PAngVelEN(:,1,:) +# dat['PAngVelER(DOF_RFrl,1,:) = CROSS_PRODUCT( dat['AngVelEN, dat['PAngVelER(DOF_RFrl,0,:) ) +# dat['AngAccERt = dat['AngAccENt + QDT(DOF_RFrl)*dat['PAngVelER(DOF_RFrl,1,:) +# +# dat['PAngVelEL( :,1,:) = dat['PAngVelER(:,1,:) +# dat['PAngVelEL(DOF_GeAz,1,:) = CROSS_PRODUCT( dat['AngVelER, dat['PAngVelEL(DOF_GeAz,0,:) ) +# dat['PAngVelEL(DOF_DrTr,1,:) = CROSS_PRODUCT( dat['AngVelER, dat['PAngVelEL(DOF_DrTr,0,:) ) +# AngAccELt = dat['AngAccERt + QDT(DOF_GeAz)*dat['PAngVelEL(DOF_GeAz,1,:) & +# + QDT(DOF_DrTr)*dat['PAngVelEL(DOF_DrTr,1,:) +# +# dat['PAngVelEH( :,1,:) = dat['PAngVelEL(:,1,:) +# dat['AngAccEHt = AngAccELt +# IF ( p['NumBl == 2 ) THEN ! 2-blader +# dat['PAngVelEH(DOF_Teet,1,:) = CROSS_PRODUCT( dat['AngVelEH, dat['PAngVelEH(DOF_Teet,0,:) ) +# dat['AngAccEHt = dat['AngAccEHt + QDT(DOF_Teet)*dat['PAngVelEH(DOF_Teet,1,:) +# ENDIF +# +# dat['PAngVelEG( :,1,:) = dat['PAngVelER(:,1,:) +# dat['PAngVelEG(DOF_GeAz,1,:) = CROSS_PRODUCT( dat['AngVelER, dat['PAngVelEG(DOF_GeAz,0,:) ) +# dat['AngAccEGt = dat['AngAccERt + QDT(DOF_GeAz)*dat['PAngVelEG(DOF_GeAz,1,:) +# +# dat['PAngVelEA( :,1,:) = dat['PAngVelEN(:,1,:) +# dat['PAngVelEA(DOF_TFrl,1,:) = CROSS_PRODUCT( dat['AngVelEN, dat['PAngVelEA(DOF_TFrl,0,:) ) +# dat['AngAccEAt = dat['AngAccENt + QDT(DOF_TFrl)*dat['PAngVelEA(DOF_TFrl,1,:) + +# DO K = 1,p['NumBl ! Loop through all blades +# DO J = 0,p['TipNode ! Loop through the blade nodes / elements +# ! Define the partial angular velocities of the current node (body M(RNodes(J))) in the inertia frame: +# ! NOTE: PAngVelEM(K,J,I,D,:) = the Dth-derivative of the partial angular velocity +# ! of DOF I for body M of blade K, element J in body E. +# +# dat['PAngVelEM(K,J, :,0,:) = dat['PAngVelEH(:,0,:) +# dat['PAngVelEM(K,J,DOF_BF(K,1),0,:) = - p['TwistedSF(K,2,1,J,1)*CoordSys['j1(K,:) & +# + p['TwistedSF(K,1,1,J,1)*CoordSys['j2(K,:) +# dat['PAngVelEM(K,J,DOF_BF(K,2),0,:) = - p['TwistedSF(K,2,2,J,1)*CoordSys['j1(K,:) & +# + p['TwistedSF(K,1,2,J,1)*CoordSys['j2(K,:) +# dat['PAngVelEM(K,J,DOF_BE(K,1),0,:) = - p['TwistedSF(K,2,3,J,1)*CoordSys['j1(K,:) & +# + p['TwistedSF(K,1,3,J,1)*CoordSys['j2(K,:) +# AngVelHM = QDT(DOF_BF(K,1))*dat['PAngVelEM(K,J,DOF_BF(K,1),0,:) & +# + QDT(DOF_BF(K,2))*dat['PAngVelEM(K,J,DOF_BF(K,2),0,:) & +# + QDT(DOF_BE(K,1))*dat['PAngVelEM(K,J,DOF_BE(K,1),0,:) +# dat['AngVelEM(:,J,K ) = dat['AngVelEH + AngVelHM +# dat['AngPosHM(:,K,J ) = QT (DOF_BF(K,1))*dat['PAngVelEM(K,J,DOF_BF(K,1),0,:) & +# + QT (DOF_BF(K,2))*dat['PAngVelEM(K,J,DOF_BF(K,2),0,:) & +# + QT (DOF_BE(K,1))*dat['PAngVelEM(K,J,DOF_BE(K,1),0,:) +# dat['AngAccEKt(:,J ,K) = dat['AngAccEHt + QDT(DOF_BF(K,1))*dat['PAngVelEM(K,J,DOF_BF(K,1),1,:) & +# + QDT(DOF_BF(K,2))*dat['PAngVelEM(K,J,DOF_BF(K,2),1,:) & +# + QDT(DOF_BE(K,1))*dat['PAngVelEM(K,J,DOF_BE(K,1),1,:) +# ! Define the 1st derivatives of the partial angular velocities of the current node (body M(RNodes(J))) in the inertia frame: +# ! NOTE: These are currently unused by the code, therefore, they need not +# ! be calculated. Thus, they are currently commented out. If it +# ! turns out that they are ever needed (i.e., if inertias of the +# ! blade elements are ever added, etc...) simply uncomment out these computations: +# ! dat['PAngVelEM(K,J, :,1,:) = dat['PAngVelEH(:,1,:) +# ! dat['PAngVelEM(K,J,DOF_BF(K,1),1,:) = CROSS_PRODUCT( dat['AngVelEH, PAngVelEM(K,J,DOF_BF(K,1),0,:) ) +# ! dat['PAngVelEM(K,J,DOF_BF(K,2),1,:) = CROSS_PRODUCT( dat['AngVelEH, PAngVelEM(K,J,DOF_BF(K,2),0,:) ) +# ! dat['PAngVelEM(K,J,DOF_BE(K,1),1,:) = CROSS_PRODUCT( dat['AngVelEH, PAngVelEM(K,J,DOF_BE(K,1),0,:) ) +# END DO !J = 1,p['BldNodes ! Loop through the blade nodes / elements +# END DO !K = 1,p['NumBl + # --- Tower values: + for J in range(TwrNodes+1): +# ! Define the partial angular velocities (and their 1st derivatives) of the +# ! current node (body F(HNodes(J)) in the inertia frame. +# ! Also define the overall angular velocity of the current node in the inertia frame. +# ! Also, define the portion of the angular acceleration of the current node +# ! in the inertia frame associated with everything but the QD2T()'s: +# ! NOTE: PAngVelEF(J,I,D,:) = the Dth-derivative of the partial angular velocity +# ! of DOF I for body F of element J in body E. + dat['PAngVelEF'] [J, :,0,:] = dat['PAngVelEX'][:,0,:] + dat['PAngVelEF'] [J,DOF_TFA1,0,:] = -p['TwrFASF'][0,J,1]*CoordSys['a3'] # Local slope + dat['PAngVelEF'] [J,DOF_TSS1,0,:] = p['TwrSSSF'][0,J,1]*CoordSys['a1'] + dat['PAngVelEF'] [J,DOF_TFA2,0,:] = -p['TwrFASF'][1,J,1]*CoordSys['a3'] + dat['PAngVelEF'] [J,DOF_TSS2,0,:] = p['TwrSSSF'][1,J,1]*CoordSys['a1'] + dat['PAngVelEF'] [J, :,1,:] = dat['PAngVelEX'][:,1,:] + dat['PAngVelEF'] [J,DOF_TFA1,1,:] = np.cross( dat['AngVelEX'] , dat['PAngVelEF'][J,DOF_TFA1,0,:] ) + dat['PAngVelEF'] [J,DOF_TSS1,1,:] = np.cross( dat['AngVelEX'] , dat['PAngVelEF'][J,DOF_TSS1,0,:] ) + dat['PAngVelEF'] [J,DOF_TFA2,1,:] = np.cross( dat['AngVelEX'] , dat['PAngVelEF'][J,DOF_TFA2,0,:] ) + dat['PAngVelEF'] [J,DOF_TSS2,1,:] = np.cross( dat['AngVelEX'] , dat['PAngVelEF'][J,DOF_TSS2,0,:] ) + dat['AngVelEF'] [J,:] = dat['AngVelEX'].copy() + dat['AngVelEF'] [J,:] += QDT[DOF_TFA1]*dat['PAngVelEF'][J,DOF_TFA1,0,:] + dat['AngVelEF'] [J,:] += QDT[DOF_TSS1]*dat['PAngVelEF'][J,DOF_TSS1,0,:] + dat['AngVelEF'] [J,:] += QDT[DOF_TFA2]*dat['PAngVelEF'][J,DOF_TFA2,0,:] + dat['AngVelEF'] [J,:] += QDT[DOF_TSS2]*dat['PAngVelEF'][J,DOF_TSS2,0,:] + dat['AngPosXF'] [J,:] = QT [DOF_TFA1]*dat['PAngVelEF'][J,DOF_TFA1,0,:] + dat['AngPosXF'] [J,:] += QT [DOF_TSS1]*dat['PAngVelEF'][J,DOF_TSS1,0,:] + dat['AngPosXF'] [J,:] += QT [DOF_TFA2]*dat['PAngVelEF'][J,DOF_TFA2,0,:] + dat['AngPosXF'] [J,:] += QT [DOF_TSS2]*dat['PAngVelEF'][J,DOF_TSS2,0,:] + dat['AngPosEF'] [J,:] = dat['AngPosEX'] + dat['AngPosXF'][J,:] +# dat['AngAccEFt'][J,:] = dat['AngAccEXt'] +# dat['AngAccEFt'][J,:] += QDT[DOF_TFA1]*dat['PAngVelEF'][J,DOF_TFA1,1,:] +# dat['AngAccEFt'][J,:] += QDT[DOF_TSS1]*dat['PAngVelEF'][J,DOF_TSS1,1,:] +# dat['AngAccEFt'][J,:] += QDT[DOF_TFA2]*dat['PAngVelEF'][J,DOF_TFA2,1,:] +# dat['AngAccEFt'][J,:] += QDT[DOF_TSS2]*dat['PAngVelEF'][J,DOF_TSS2,1,:] + # --- + if IEC is None: + IEC = dict() + IEC['theta_f'] = EDVec2IEC(dat['AngPosEX']) + IEC['omega_f'] = EDVec2IEC(dat['AngVelEX']) + IEC['omega_t'] = IEC['omega_f'].copy() + IEC['theta_fn'] = EDVec2IEC(dat['AngPosXB']) # TODO TODO ADD YAW + IEC['theta_n'] = IEC['theta_f'] + IEC['theta_fn'] + IEC['omega_n'] = EDVec2IEC(dat['AngVelEN']) + + IEC['omega_Ts'] = EDVec2IEC(dat['AngVelEF']) # Tower nodes ang vel + IEC['theta_fTs'] = EDVec2IEC(dat['AngPosXF']) # Tower nodes ang pos from platform + IEC['theta_Ts'] = EDVec2IEC(dat['AngPosEF']) # Tower nodes ang pos from platform + + return dat, IEC + + +def ED_LinVelPAcc(q=None, qd=None, qDict=None, qdDict=None, CoordSys=None, p=None, dat=None, IEC=None): + """ + See ElastoDyn.f90 CalculateLinearVelPAcc + !> This routine is used to calculate the linear velocities and accelerations stored in other states that are used in + !! both the CalcOutput and CalcContStateDeriv routines. + SUBROUTINE CalculateLinearVelPAcc( p, x, CoordSys, dat ) + + BODIES: + E: the earth/inertial frame + X: the platform body + N: the nacelle body + A: the tail-furl body + + """ + if qDict is not None: + QT = ED_qDict2q(qDict) + else: + QT = q + if qdDict is not None: + QDT = ED_qDict2q(qdDict) + else: + QDT = qd + + if dat is None: + dat = dict() +# ALLOCATE( RtHS%LinVelES( Dims, 0:p%TipNode, p%NumBl ), & +# RtHS%AngVelEM( Dims, 0:p%TipNode, p%NumBl ), STAT=ErrStat ) +# ! These linear velocities are allocated to start numbering a dimension with 0 instead of 1: +# ALLOCATE ( RtHS%PLinVelEIMU(p%NDOF,0:1,Dims) , STAT=ErrStat ) +# ALLOCATE ( RtHS%PLinVelEO(p%NDOF,0:1,Dims) , STAT=ErrStat ) +# ALLOCATE ( RtHS%PLinVelES(p%NumBl,0:p%TipNode,p%NDOF,0:1,Dims) , STAT=ErrStat ) +# ALLOCATE ( RtHS%PLinVelET(0:p%TwrNodes,p%NDOF,0:1,Dims) , STAT=ErrStat ) +# ALLOCATE ( RtHS%PLinVelEC(p%NDOF,0:1,3) , STAT=ErrStat ) +# ALLOCATE ( RtHS%PLinVelED(p%NDOF,0:1,3) , STAT=ErrStat ) +# ALLOCATE ( RtHS%PLinVelEI(p%NDOF,0:1,3) , STAT=ErrStat ) +# ALLOCATE ( RtHS%PLinVelEJ(p%NDOF,0:1,3) , STAT=ErrStat ) +# ALLOCATE ( RtHS%PLinVelEP(p%NDOF,0:1,3) , STAT=ErrStat ) +# ALLOCATE ( RtHS%PLinVelEQ(p%NDOF,0:1,3) , STAT=ErrStat ) +# RtHS%PLinVelEW(p%NDOF,0:1,3) , & +# RtHS%PLinVelEY(p%NDOF,0:1,3) , STAT=ErrStat ) + NDOF=11 # TODO + TwrNodes = p['TwrFASF'].shape[1] - 2 + NumBl=3 + + dat['PLinVelEZ'] = np.zeros((NDOF,2, 3)) + dat['PLinVelEO'] = np.zeros((NDOF,2, 3)) + dat['PLinVelEU'] = np.zeros((NDOF,2, 3)) + dat['PLinVelET'] = np.zeros((TwrNodes+1,NDOF,2, 3)) +# !------------------------------------------------------------------------------------------------- +# ! Partial linear velocities and accelerations +# !------------------------------------------------------------------------------------------------- +# ! Define the partial linear velocities (and their 1st derivatives) of all of +# ! the points on the wind turbine in the inertia frame that are not +# ! dependent on the distributed tower or blade parameters. Also, define +# ! the portion of the linear acceleration of the points in the inertia +# ! frame associated with everything but the QD2T()'s: +# ! NOTE: PLinVelEX(I,D,:) = the Dth-derivative of the partial linear velocity +# ! of DOF I for point X in body E. +# EwXXrZY = CROSS_PRODUCT( dat['AngVelEX, dat['rZY ) ! +# EwRXrVD = np.cross( dat['AngVelER'], dat['rVD'] ) # Cross products +# EwRXrVP = np.cross( dat['AngVelER'], dat['rVP'] ) # in the following +# EwHXrPQ = np.cross( dat['AngVelEH'], dat['rPQ'] ) # DO...LOOPs +# EwHXrQC = np.cross( dat['AngVelEH'], dat['rQC'] ) # +# EwNXrOW = np.cross( dat['AngVelEN'], dat['rOW'] ) # +# EwAXrWI = np.cross( dat['AngVelEA'], dat['rWI'] ) # +# EwAXrWJ = np.cross( dat['AngVelEA'], dat['rWJ'] ) # + + # --- Platform reference point "Z" + dat['PLinVelEZ'][ :,:,:] = 0.0 + dat['PLinVelEZ'][DOF_Sg ,0,:] = CoordSys['z1'] + dat['PLinVelEZ'][DOF_Sw ,0,:] = -CoordSys['z3'] + dat['PLinVelEZ'][DOF_Hv ,0,:] = CoordSys['z2'] + dat['LinVelEZ'] = QDT[DOF_Sg]*dat['PLinVelEZ'][DOF_Sg,0,:]+ QDT[DOF_Sw]*dat['PLinVelEZ'][DOF_Sw,0,:]+ QDT[DOF_Hv]*dat['PLinVelEZ'][DOF_Hv,0,:] + + # --- Platform COG "Y" +# dat['PLinVelEY( :,:,:) = dat['PLinVelEZ(:,:,:) +# DO I = 1,NPX ! Loop through all DOFs associated with the angular motion of the platform (body X) +# TmpVec0 = CROSS_PRODUCT( dat['PAngVelEX(PX(I) ,0,:), dat['rZY ) +# TmpVec1 = CROSS_PRODUCT( dat['PAngVelEX(PX(I) ,0,:), EwXXrZY ) +# dat['PLinVelEY(PX(I),0,:) = TmpVec0 + dat['PLinVelEY(PX(I) ,0,:) +# dat['PLinVelEY(PX(I),1,:) = TmpVec1 + dat['PLinVelEY(PX(I) ,1,:) +# dat['LinAccEYt = dat['LinAccEYt + x%QDT(PX(I) )*dat['PLinVelEY(PX(I) ,1,:) + + # --- Tower Top "O" + dat['PLinVelEO'] = dat['PLinVelEZ'][:,:,:].copy() + dat['PLinVelEO'][DOF_TFA1,0,:] = CoordSys['a1'] - (p['AxRedTFA'][0,0,-1]* QT[DOF_TFA1] + p['AxRedTFA'][0,1,-1]* QT[DOF_TFA2])*CoordSys['a2'] + dat['PLinVelEO'][DOF_TSS1,0,:] = CoordSys['a3'] - (p['AxRedTSS'][0,0,-1]* QT[DOF_TSS1] + p['AxRedTSS'][0,1,-1]* QT[DOF_TSS2])*CoordSys['a2'] + dat['PLinVelEO'][DOF_TFA2,0,:] = CoordSys['a1'] - (p['AxRedTFA'][1,1,-1]* QT[DOF_TFA2] + p['AxRedTFA'][0,1,-1]* QT[DOF_TFA1])*CoordSys['a2'] + dat['PLinVelEO'][DOF_TSS2,0,:] = CoordSys['a3'] - (p['AxRedTSS'][1,1,-1]* QT[DOF_TSS2] + p['AxRedTSS'][0,1,-1]* QT[DOF_TSS1])*CoordSys['a2'] + TmpVec1 = np.cross( dat['AngVelEX'] , dat['PLinVelEO'][DOF_TFA1,0,:] ) + TmpVec2 = np.cross( dat['AngVelEX'] , dat['PLinVelEO'][DOF_TSS1,0,:] ) + TmpVec3 = np.cross( dat['AngVelEX'] , dat['PLinVelEO'][DOF_TFA2,0,:] ) + TmpVec4 = np.cross( dat['AngVelEX'] , dat['PLinVelEO'][DOF_TSS2,0,:] ) + dat['PLinVelEO'][DOF_TFA1,1,:] = TmpVec1 - ( p['AxRedTFA'][0,0,-1]*QDT[DOF_TFA1] + p['AxRedTFA'][0,1,-1]*QDT[DOF_TFA2] )*CoordSys['a2'] + dat['PLinVelEO'][DOF_TSS1,1,:] = TmpVec2 - ( p['AxRedTSS'][0,0,-1]*QDT[DOF_TSS1] + p['AxRedTSS'][0,1,-1]*QDT[DOF_TSS2] )*CoordSys['a2'] + dat['PLinVelEO'][DOF_TFA2,1,:] = TmpVec3 - ( p['AxRedTFA'][1,1,-1]*QDT[DOF_TFA2] + p['AxRedTFA'][0,1,-1]*QDT[DOF_TFA1] )*CoordSys['a2'] + dat['PLinVelEO'][DOF_TSS2,1,:] = TmpVec4 - ( p['AxRedTSS'][1,1,-1]*QDT[DOF_TSS2] + p['AxRedTSS'][0,1,-1]*QDT[DOF_TSS1] )*CoordSys['a2'] + LinVelXO = QDT[DOF_TFA1]*dat['PLinVelEO'][DOF_TFA1,0,:] \ + + QDT[DOF_TSS1]*dat['PLinVelEO'][DOF_TSS1,0,:] \ + + QDT[DOF_TFA2]*dat['PLinVelEO'][DOF_TFA2,0,:] \ + + QDT[DOF_TSS2]*dat['PLinVelEO'][DOF_TSS2,0,:] + dat['LinAccEOt'] = QDT[DOF_TFA1]*dat['PLinVelEO'][DOF_TFA1,1,:] \ + + QDT[DOF_TSS1]*dat['PLinVelEO'][DOF_TSS1,1,:] \ + + QDT[DOF_TFA2]*dat['PLinVelEO'][DOF_TFA2,1,:] \ + + QDT[DOF_TSS2]*dat['PLinVelEO'][DOF_TSS2,1,:] + + dat['LinVelEO'] = LinVelXO + dat['LinVelEZ'].copy() + EwXXrZO = np.cross( dat['AngVelEX'], dat['rZO'] ) # + for I in range(NPX): # Loop through all DOFs associated with the angular motion of the platform (body X) + TmpVec0 = np.cross( dat['PAngVelEX'][PX[I] ,0,:], dat['rZO'] ) + TmpVec1 = np.cross( dat['PAngVelEX'][PX[I] ,0,:], EwXXrZO + LinVelXO ) + dat['PLinVelEO'][PX[I],0,:] = TmpVec0 + dat['PLinVelEO'][PX[I] ,0,:] + dat['PLinVelEO'][PX[I],1,:] = TmpVec1 + dat['PLinVelEO'][PX[I] ,1,:] + dat['LinVelEO'] += QDT[PX[I] ]*dat['PLinVelEO'][PX[I],0,:] + dat['LinAccEOt'] += QDT[PX[I] ]*dat['PLinVelEO'][PX[I],1,:] + + # --- Nacelle COG "U" + dat['PLinVelEU'] = dat['PLinVelEO'].copy() + dat['LinVelEU'] = dat['LinVelEZ'].copy() # NOTE: MANU + dat['LinAccEUt'] = 0 + EwNXrOU = np.cross( dat['AngVelEN'], dat['rOU'] ) # + for I in range(NPN): # Loop through all DOFs associated with the angular motion of the nacelle (body N) + TmpVec0 = np.cross( dat['PAngVelEN'][PN[I] ,0,:], dat['rOU'] ) + TmpVec1 = np.cross( dat['PAngVelEN'][PN[I] ,0,:], EwNXrOU ) + TmpVec2 = np.cross( dat['PAngVelEN'][PN[I] ,1,:], dat['rOU'] ) + dat['PLinVelEU'][PN[I],0,:] = TmpVec0 + dat['PLinVelEU'][PN[I] ,0,:] + dat['PLinVelEU'][PN[I],1,:] = TmpVec1 + TmpVec2 + dat['PLinVelEU'][PN[I] ,1,:] + dat['LinVelEU'] += QDT[PN[I] ]*dat['PLinVelEU'][PN[I],0,:] # NOTE: TODO TODO THIS IS MANU TRIAL + dat['LinAccEUt'] += QDT[PN[I] ]*dat['PLinVelEU'][PN[I],1,:] + + # --- Rotor Furl "V" +# dat['PLinVelEV'] = dat['PLinVelEO'].copy() +# EwNXrOV = np.cross( dat['AngVelEN'], dat['rOV'] ) # +# for I in range(NPN): # Loop through all DOFs associated with the angular motion of the nacelle (body N) +# TmpVec0 = np.cross( dat['PAngVelEN(PN[I] ,0,:), dat['rOV ) +# TmpVec1 = np.cross( dat['PAngVelEN(PN[I] ,0,:), EwNXrOV ) +# TmpVec2 = np.cross( dat['PAngVelEN(PN[I] ,1,:), dat['rOV ) +# dat['PLinVelEV'][PN[I],0,:] = TmpVec0 + dat['PLinVelEV[PN[I],0,:] +# dat['PLinVelEV'][PN[I],1,:] = TmpVec1 + TmpVec2 + dat['PLinVelEV[PN[I],1,:] +# LinAccEVt += QDT(PN[I] )*dat['PLinVelEV'][PN[I] ,1,:] +# +# dat['PLinVelED( :,:,:) = dat['PLinVelEV'].copy() +# for I in range(NPR): # Loop through all DOFs associated with the angular motion of the structure that furls with the rotor (not including rotor) (body R) +# TmpVec0 = CROSS_PRODUCT( dat['PAngVelER(PR[I] ,0,:), dat['rVD ) +# TmpVec1 = CROSS_PRODUCT( dat['PAngVelER(PR[I] ,0,:), EwRXrVD ) +# TmpVec2 = CROSS_PRODUCT( dat['PAngVelER(PR[I] ,1,:), dat['rVD ) +# dat['PLinVelED(PR[I],0,:) = TmpVec0 + dat['PLinVelED'][PR[I] ,0,:] +# dat['PLinVelED(PR[I],1,:) = TmpVec1 + TmpVec2 + dat['PLinVelED'][PR[I] ,1,:] +# dat['LinAccEDt += QDT(PR[I] )*dat['PLinVelED'][PR[I] ,1,:] +# + # --- Nacelle IMU +# dat['PLinVelEIMU'] = dat['PLinVelEV'].copy() +# dat['LinVelEIMU'] = dat['LinVelEZ'].copy() +# EwRXrVIMU = np.cross( dat['AngVelER'], dat['rVIMU'] ) # that are used +# for I in range(NPR): # Loop through all DOFs associated with the angular motion of the structure that furls with the rotor (not including rotor) (body R) +# TmpVec0 = CROSS_PRODUCT( dat['PAngVelER(PR[I] ,0,:), dat['rVIMU ) +# TmpVec1 = CROSS_PRODUCT( dat['PAngVelER(PR[I] ,0,:), EwRXrVIMU ) +# TmpVec2 = CROSS_PRODUCT( dat['PAngVelER(PR[I] ,1,:), dat['rVIMU ) +# dat['PLinVelEIMU(PR[I],0,:) = TmpVec0 + dat['PLinVelEIMU(PR[I] ,0,:) +# dat['PLinVelEIMU(PR[I],1,:) = TmpVec1 + TmpVec2 + dat['PLinVelEIMU(PR[I] ,1,:) +# dat['LinVelEIMU'] += QDT(PR[I] )*dat['PLinVelEIMU(PR[I] ,0,:) +# dat['LinAccEIMUt'] += QDT(PR[I] )*dat['PLinVelEIMU(PR[I] ,1,:) +# +# dat['PLinVelEP( :,:,:) = dat['PLinVelEV(:,:,:) +# for I in range(NPR): # Loop through all DOFs associated with the angular motion of the structure that furls with the rotor (not including rotor) (body R) +# TmpVec0 = CROSS_PRODUCT( dat['PAngVelER(PR[I] ,0,:), dat['rVP ) +# TmpVec1 = CROSS_PRODUCT( dat['PAngVelER(PR[I] ,0,:), EwRXrVP ) +# TmpVec2 = CROSS_PRODUCT( dat['PAngVelER(PR[I] ,1,:), dat['rVP ) +# dat['PLinVelEP(PR[I],0,:) = TmpVec0 + dat['PLinVelEP(PR[I] ,0,:) +# dat['PLinVelEP(PR[I],1,:) = TmpVec1 + TmpVec2 + dat['PLinVelEP(PR[I] ,1,:) +# LinAccEPt = LinAccEPt + QDT(PR[I] )*dat['PLinVelEP(PR[I] ,1,:) +# + # --- Rotor center "Q" +# dat['PLinVelEQ'] = dat['PLinVelEP'].copy() +# dat['LinVelEQ'] = dat['LinVelEZ'].copy() +# DO I = 1,p['NPH ! Loop through all DOFs associated with the angular motion of the hub (body H) +# TmpVec0 = CROSS_PRODUCT( dat['PAngVelEH(p['PH(I) ,0,:), dat['rPQ ) +# TmpVec1 = CROSS_PRODUCT( dat['PAngVelEH(p['PH(I) ,0,:), EwHXrPQ ) +# TmpVec2 = CROSS_PRODUCT( dat['PAngVelEH(p['PH(I) ,1,:), dat['rPQ ) +# dat['PLinVelEQ(p['PH(I),0,:) = TmpVec0 + dat['PLinVelEQ(p['PH(I) ,0,:) +# dat['PLinVelEQ(p['PH(I),1,:) = TmpVec1 + TmpVec2 + dat['PLinVelEQ(p['PH(I) ,1,:) +# dat['LinVelEQ += QDT(p['PH(I) )*dat['PLinVelEQ(p['PH(I) ,0,:) +# LinAccEQt += QDT(p['PH(I) )*dat['PLinVelEQ(p['PH(I) ,1,:) +# + # --- Hub COG "C" +# dat['PLinVelEC( :,:,:) = dat['PLinVelEQ(:,:,:) +# DO I = 1,p['NPH ! Loop through all DOFs associated with the angular motion of the hub (body H) +# TmpVec0 = CROSS_PRODUCT( dat['PAngVelEH(p['PH(I) ,0,:), dat['rQC ) +# TmpVec1 = CROSS_PRODUCT( dat['PAngVelEH(p['PH(I) ,0,:), EwHXrQC ) +# TmpVec2 = CROSS_PRODUCT( dat['PAngVelEH(p['PH(I) ,1,:), dat['rQC ) +# dat['PLinVelEC(p['PH(I),0,:) = TmpVec0 + dat['PLinVelEC(p['PH(I) ,0,:) +# dat['PLinVelEC(p['PH(I),1,:) = TmpVec1 + TmpVec2 + dat['PLinVelEC(p['PH(I) ,1,:) +# dat['LinAccECt = dat['LinAccECt + QDT(p['PH(I) )*dat['PLinVelEC(p['PH(I) ,1,:) +# +# +# DO K = 1,p['NumBl ! Loop through all blades +# DO J = 0,p['TipNode ! Loop through the blade nodes / elements +# ! Define the partial linear velocities (and their 1st derivatives) of the +# ! current node (point S(RNodes(J))) in the inertia frame. Also define +# ! the overall linear velocity of the current node in the inertia frame. +# ! Also, define the portion of the linear acceleration of the current node +# ! in the inertia frame associated with everything but the QD2T()'s: +# +# EwHXrQS = CROSS_PRODUCT( dat['AngVelEH, dat['rQS(:,K,J) ) +# +# dat['PLinVelES(K,J, :,:,:) = dat['PLinVelEQ(:,:,:) +# dat['PLinVelES(K,J,DOF_BF(K,1),0,:) = p['TwistedSF(K,1,1,J,0) *CoordSys['j1(K,:) & !bjj: this line can be optimized +# + p['TwistedSF(K,2,1,J,0) *CoordSys['j2(K,:) & +# - ( p['AxRedBld(K,1,1,J)*QT ( DOF_BF(K,1) ) & +# + p['AxRedBld(K,1,2,J)*QT ( DOF_BF(K,2) ) & +# + p['AxRedBld(K,1,3,J)*QT ( DOF_BE(K,1) ) )*CoordSys['j3(K,:) +# dat['PLinVelES(K,J,DOF_BE(K,1),0,:) = p['TwistedSF(K,1,3,J,0) *CoordSys['j1(K,:) & +# + p['TwistedSF(K,2,3,J,0) *CoordSys['j2(K,:) & +# - ( p['AxRedBld(K,3,3,J)*QT ( DOF_BE(K,1) ) & +# + p['AxRedBld(K,2,3,J)*QT ( DOF_BF(K,2) ) & +# + p['AxRedBld(K,1,3,J)*QT ( DOF_BF(K,1) ) )*CoordSys['j3(K,:) +# dat['PLinVelES(K,J,DOF_BF(K,2),0,:) = p['TwistedSF(K,1,2,J,0) *CoordSys['j1(K,:) & +# + p['TwistedSF(K,2,2,J,0) *CoordSys['j2(K,:) & +# - ( p['AxRedBld(K,2,2,J)*QT ( DOF_BF(K,2) ) & +# + p['AxRedBld(K,1,2,J)*QT ( DOF_BF(K,1) ) & +# + p['AxRedBld(K,2,3,J)*QT ( DOF_BE(K,1) ) )*CoordSys['j3(K,:) +# +# TmpVec1 = CROSS_PRODUCT( dat['AngVelEH, dat['PLinVelES(K,J,DOF_BF(K,1),0,:) ) +# TmpVec2 = CROSS_PRODUCT( dat['AngVelEH, dat['PLinVelES(K,J,DOF_BE(K,1),0,:) ) +# TmpVec3 = CROSS_PRODUCT( dat['AngVelEH, dat['PLinVelES(K,J,DOF_BF(K,2),0,:) ) +# +# dat['PLinVelES(K,J,DOF_BF(K,1),1,:) = TmpVec1 & +# - ( p['AxRedBld(K,1,1,J)*QDT( DOF_BF(K,1) ) & +# + p['AxRedBld(K,1,2,J)*QDT( DOF_BF(K,2) ) & +# + p['AxRedBld(K,1,3,J)*QDT( DOF_BE(K,1) ) )*CoordSys['j3(K,:) +# dat['PLinVelES(K,J,DOF_BE(K,1),1,:) = TmpVec2 & +# - ( p['AxRedBld(K,3,3,J)*QDT( DOF_BE(K,1) ) & +# + p['AxRedBld(K,2,3,J)*QDT( DOF_BF(K,2) ) & +# + p['AxRedBld(K,1,3,J)*QDT( DOF_BF(K,1) ) )*CoordSys['j3(K,:) +# dat['PLinVelES(K,J,DOF_BF(K,2),1,:) = TmpVec3 & +# - ( p['AxRedBld(K,2,2,J)*QDT( DOF_BF(K,2) ) & +# + p['AxRedBld(K,1,2,J)*QDT( DOF_BF(K,1) ) & +# + p['AxRedBld(K,2,3,J)*QDT( DOF_BE(K,1) ) )*CoordSys['j3(K,:) +# +# LinVelHS = QDT( DOF_BF(K,1) )*dat['PLinVelES(K,J,DOF_BF(K,1),0,:) & +# + QDT( DOF_BE(K,1) )*dat['PLinVelES(K,J,DOF_BE(K,1),0,:) & +# + QDT( DOF_BF(K,2) )*dat['PLinVelES(K,J,DOF_BF(K,2),0,:) +# dat['LinAccESt(:,K,J) = QDT( DOF_BF(K,1) )*dat['PLinVelES(K,J,DOF_BF(K,1),1,:) & +# + QDT( DOF_BE(K,1) )*dat['PLinVelES(K,J,DOF_BE(K,1),1,:) & +# + QDT( DOF_BF(K,2) )*dat['PLinVelES(K,J,DOF_BF(K,2),1,:) +# +# dat['LinVelES(:,J,K) = LinVelHS + dat['LinVelEZ +# DO I = 1,p['NPH ! Loop through all DOFs associated with the angular motion of the hub (body H) +# +# TmpVec0 = CROSS_PRODUCT( dat['PAngVelEH(p['PH(I),0,:), dat['rQS(:,K,J) ) !bjj: this line can be optimized +# TmpVec1 = CROSS_PRODUCT( dat['PAngVelEH(p['PH(I),0,:), EwHXrQS + LinVelHS ) !bjj: this line can be optimized +# TmpVec2 = CROSS_PRODUCT( dat['PAngVelEH(p['PH(I),1,:), dat['rQS(:,K,J) ) !bjj: this line can be optimized +# +# dat['PLinVelES(K,J,p['PH(I),0,:) = dat['PLinVelES(K,J,p['PH(I),0,:) + TmpVec0 !bjj: this line can be optimized +# dat['PLinVelES(K,J,p['PH(I),1,:) = dat['PLinVelES(K,J,p['PH(I),1,:) + TmpVec1 + TmpVec2 !bjj: this line can be optimized +# +# dat['LinVelES(:,J,K) = dat['LinVelES(:,J,K) + QDT(p['PH(I))*dat['PLinVelES(K,J,p['PH(I),0,:) !bjj: this line can be optimized +# dat['LinAccESt(:,K,J) = dat['LinAccESt(:,K,J) + QDT(p['PH(I))*dat['PLinVelES(K,J,p['PH(I),1,:) !bjj: this line can be optimized +# +# END DO ! I - all DOFs associated with the angular motion of the hub (body H) +# +# END DO !J = 0,p['TipNodes ! Loop through the blade nodes / elements +# +# +# !JASON: USE TipNode HERE INSTEAD OF BldNodes IF YOU ALLOCATE AND DEFINE n1, n2, n3, m1, m2, AND m3 TO USE TipNode. THIS WILL REQUIRE THAT THE AERODYNAMIC AND STRUCTURAL TWISTS, AeroTwst() AND ThetaS(), BE KNOWN AT THE TIP!!! +# !IF (.NOT. p['BD4Blades) THEN +# ! dat['LinVelESm2(K) = DOT_PRODUCT( dat['LinVelES(:,p['TipNode,K), CoordSys['m2(K,p['BldNodes,:) ) +# !END IF +# +# END DO !K = 1,p['NumBl +# +# +# dat['PLinVelEW( :,:,:) = dat['PLinVelEO(:,:,:) +# DO I = 1,NPN ! Loop through all DOFs associated with the angular motion of the nacelle (body N) +# +# TmpVec0 = CROSS_PRODUCT( dat['PAngVelEN(PN(I) ,0,:), dat['rOW ) +# TmpVec1 = CROSS_PRODUCT( dat['PAngVelEN(PN(I) ,0,:), EwNXrOW ) +# TmpVec2 = CROSS_PRODUCT( dat['PAngVelEN(PN(I) ,1,:), dat['rOW ) +# +# dat['PLinVelEW(PN(I),0,:) = TmpVec0 + dat['PLinVelEW(PN(I) ,0,:) +# dat['PLinVelEW(PN(I),1,:) = TmpVec1 + TmpVec2 + dat['PLinVelEW(PN(I) ,1,:) +# +# LinAccEWt = LinAccEWt + QDT(PN(I) )*dat['PLinVelEW(PN(I) ,1,:) +# +# ENDDO ! I - all DOFs associated with the angular motion of the nacelle (body N) +# +# +# ! Velocities of point I (tail boom center of mass) +# dat['PLinVelEI( :,:,:) = dat['PLinVelEW(:,:,:) +# DO I = 1,NPA ! Loop through all DOFs associated with the angular motion of the tail (body A) +# +# TmpVec0 = CROSS_PRODUCT( dat['PAngVelEA(PA(I) ,0,:), dat['rWI ) +# TmpVec1 = CROSS_PRODUCT( dat['PAngVelEA(PA(I) ,0,:), EwAXrWI ) +# TmpVec2 = CROSS_PRODUCT( dat['PAngVelEA(PA(I) ,1,:), dat['rWI ) +# +# dat['PLinVelEI(PA(I),0,:) = TmpVec0 + dat['PLinVelEI(PA(I) ,0,:) +# dat['PLinVelEI(PA(I),1,:) = TmpVec1 + TmpVec2 + dat['PLinVelEI(PA(I) ,1,:) +# +# dat['LinAccEIt = dat['LinAccEIt + QDT(PA(I) )*dat['PLinVelEI(PA(I) ,1,:) +# +# ENDDO ! I - all DOFs associated with the angular motion of the tail (body A) +# +# +# ! Velocities of point J (tail fin center of mass) +# dat['PLinVelEJ( :,:,:) = dat['PLinVelEW(:,:,:) +# dat['LinVelEJ = dat['LinVelEZ +# DO I = 1,NPA ! Loop through all DOFs associated with the angular motion of the tail (body A) +# +# TmpVec0 = CROSS_PRODUCT( dat['PAngVelEA(PA(I) ,0,:), dat['rWJ ) +# TmpVec1 = CROSS_PRODUCT( dat['PAngVelEA(PA(I) ,0,:), EwAXrWJ ) +# TmpVec2 = CROSS_PRODUCT( dat['PAngVelEA(PA(I) ,1,:), dat['rWJ ) +# +# dat['PLinVelEJ(PA(I),0,:) = TmpVec0 + dat['PLinVelEJ(PA(I) ,0,:) +# dat['PLinVelEJ(PA(I),1,:) = TmpVec1 + TmpVec2 + dat['PLinVelEJ(PA(I) ,1,:) +# +# dat['LinVelEJ = dat['LinVelEJ + QDT(PA(I) )*dat['PLinVelEJ(PA(I) ,0,:) +# dat['LinAccEJt = dat['LinAccEJt + QDT(PA(I) )*dat['PLinVelEJ(PA(I) ,1,:) +# +# ENDDO ! I - all DOFs associated with the angular motion of the tail (body A) +# +# + # --- Tower nodes + dat['LinAccETt'] = np.zeros((TwrNodes+1,3)) + dat['LinVelET'] = np.zeros((TwrNodes+1,3)) + for J in range(TwrNodes+1): #Loop through the tower nodes / elements + # Define the partial linear velocities (and their 1st derivatives) of the current node (point T(HNodes(J))) in the inertia frame. + # Also define the overall linear velocity of the current node in the inertia frame. + # Also, define the portion of the linear acceleration of the current node in the inertia frame associated with + # everything but the QD2T()'s: + EwXXrZT = np.cross( dat['AngVelEX'], dat['rZT'][J,:] ) + dat['PLinVelET'][J, :,:,:] = dat['PLinVelEZ'].copy() + dat['PLinVelET'][J,DOF_TFA1,0,:] = p['TwrFASF'][0,J,0]*CoordSys['a1'] - ( p['AxRedTFA'][0,0,J]* QT[DOF_TFA1] + p['AxRedTFA'][0,1,J]* QT[DOF_TFA2] )*CoordSys['a2'] + dat['PLinVelET'][J,DOF_TSS1,0,:] = p['TwrSSSF'][0,J,0]*CoordSys['a3'] - ( p['AxRedTSS'][0,0,J]* QT[DOF_TSS1] + p['AxRedTSS'][0,1,J]* QT[DOF_TSS2] )*CoordSys['a2'] + dat['PLinVelET'][J,DOF_TFA2,0,:] = p['TwrFASF'][1,J,0]*CoordSys['a1'] - ( p['AxRedTFA'][1,1,J]* QT[DOF_TFA2] + p['AxRedTFA'][0,1,J]* QT[DOF_TFA1] )*CoordSys['a2'] + dat['PLinVelET'][J,DOF_TSS2,0,:] = p['TwrSSSF'][1,J,0]*CoordSys['a3'] - ( p['AxRedTSS'][1,1,J]* QT[DOF_TSS2] + p['AxRedTSS'][0,1,J]* QT[DOF_TSS1] )*CoordSys['a2'] + TmpVec1 = np.cross( dat['AngVelEX'], dat['PLinVelET'][J,DOF_TFA1,0,:] ) + TmpVec2 = np.cross( dat['AngVelEX'], dat['PLinVelET'][J,DOF_TSS1,0,:] ) + TmpVec3 = np.cross( dat['AngVelEX'], dat['PLinVelET'][J,DOF_TFA2,0,:] ) + TmpVec4 = np.cross( dat['AngVelEX'], dat['PLinVelET'][J,DOF_TSS2,0,:] ) + dat['PLinVelET'][J,DOF_TFA1,1,:] = TmpVec1 - ( p['AxRedTFA'][0,0,J]*QDT[DOF_TFA1] + p['AxRedTFA'][0,1,J]*QDT[DOF_TFA2] )*CoordSys['a2'] + dat['PLinVelET'][J,DOF_TSS1,1,:] = TmpVec2 - ( p['AxRedTSS'][0,0,J]*QDT[DOF_TSS1] + p['AxRedTSS'][0,1,J]*QDT[DOF_TSS2] )*CoordSys['a2'] + dat['PLinVelET'][J,DOF_TFA2,1,:] = TmpVec3 - ( p['AxRedTFA'][1,1,J]*QDT[DOF_TFA2] + p['AxRedTFA'][0,1,J]*QDT[DOF_TFA1] )*CoordSys['a2'] + dat['PLinVelET'][J,DOF_TSS2,1,:] = TmpVec4 - ( p['AxRedTSS'][1,1,J]*QDT[DOF_TSS2] + p['AxRedTSS'][0,1,J]*QDT[DOF_TSS1] )*CoordSys['a2'] + LinVelXT = QDT[DOF_TFA1]*dat['PLinVelET'][J,DOF_TFA1,0,:] \ + + QDT[DOF_TSS1]*dat['PLinVelET'][J,DOF_TSS1,0,:] \ + + QDT[DOF_TFA2]*dat['PLinVelET'][J,DOF_TFA2,0,:] \ + + QDT[DOF_TSS2]*dat['PLinVelET'][J,DOF_TSS2,0,:] + dat['LinAccETt'][J,:] = QDT[DOF_TFA1]*dat['PLinVelET'][J,DOF_TFA1,1,:] \ + + QDT[DOF_TSS1]*dat['PLinVelET'][J,DOF_TSS1,1,:] \ + + QDT[DOF_TFA2]*dat['PLinVelET'][J,DOF_TFA2,1,:] \ + + QDT[DOF_TSS2]*dat['PLinVelET'][J,DOF_TSS2,1,:] + dat['LinVelET'][J,:] = LinVelXT + dat['LinVelEZ'] + for I in range(NPX): # Loop through all DOFs associated with the angular motion of the platform (body X) + TmpVec0 = np.cross( dat['PAngVelEX'][PX[I],0,:], dat['rZT'][J,:] ) + TmpVec1 = np.cross( dat['PAngVelEX'][PX[I],0,:], EwXXrZT + LinVelXT ) + dat['PLinVelET'][J,PX[I],0,:] = dat['PLinVelET'][J,PX[I],0,:] + TmpVec0 + dat['PLinVelET'][J,PX[I],1,:] = dat['PLinVelET'][J,PX[I],1,:] + TmpVec1 + dat['LinVelET'][ J, :] += QDT[PX[I]]*dat['PLinVelET'][J,PX[I],0,:] + dat['LinAccETt'][J, :] += QDT[PX[I]]*dat['PLinVelET'][J,PX[I],1,:] + # --- + if IEC is None: + IEC = dict() + IEC['v_F'] = EDVec2IEC(dat['LinVelEZ']) + IEC['v_N'] = EDVec2IEC(dat['LinVelEO']) + IEC['ud_N'] = EDVec2IEC(LinVelXO) # Elastic velocity of tower top + IEC['v_Gn'] = EDVec2IEC(dat['LinVelEU']) # velocity of nacelle COG + IEC['v_Ts'] = EDVec2IEC(dat['LinVelET']) # velocity of tower nodes + + return dat, IEC + + +def ED_CalcOutputs(x, p, noAxRed=False): + """ + INPUTS: + - x: states (for now structural only) + - p: parameters, e.g. as returned by p = ED_Parameters(fstSim) + OUTPUTS: + - CS: dictionary of Coordinate system info + - dat: dictionary of structural variables in "ElastoDyn internal" coordinate system + - IEC: dictionary of structural variables in OpenFAST/IEC coordinate system + + Example: + p = ED_Parameters(fstSim) + x['qDict'] = {'Sg': 10.0, 'Sw':20.0, 'Hv': 5.0, 'R':0.0, 'P':0.3, 'Y':0, 'TFA1':1.0, 'TSS1':10.0, 'Yaw':np.pi/8} + x['qdDict'] = {'Sg': 1.0, 'Sw': 2.0, 'Hv': 3.0, 'R':0.1, 'P':0.3, 'Y':0, 'TFA1':2.0, 'TSS1':4.0, 'Yaw':0.0} + + """ + if noAxRed: + p['AxRedTFA']*=0 # + p['AxRedTSS']*=0 # + qDict = x['qDict'] + qdDict = x['qdDict'] + CS = ED_CoordSys(qDict=qDict, TwrFASF=p['TwrFASF'], TwrSSSF=p['TwrSSSF']) + dat, IEC = ED_Positions(qDict=qDict, CoordSys=CS, p=p) + dat, IEC = ED_AngPosVelPAcc(qDict=qDict, qdDict=qdDict, CoordSys=CS, p=p, dat=dat, IEC=IEC) + dat, IEC = ED_LinVelPAcc (qDict=qDict, qdDict=qdDict, CoordSys=CS, p=p, dat=dat, IEC=IEC) + return CS, dat, IEC + +if __name__ == '__main__': +# EDfilename='../yams/_Jens/FEMBeam_NewFASTCoeffs/data/NREL5MW_ED_Onshore.dat' +# EDfilename='../../data/NREL5MW/onshore/NREL5MW_ED_Onshore.dat' +# bladeParameters(EDfilename) + + EDfilename = '../../data/NREL5MW/onshore/NREL5MW_ED_Onshore.dat' + FSTfilename = '../../data/NREL5MW/Main_Onshore.fst' + + fst = FASTInputFile(FSTfilename) + + p,pbld = rotorParameters(EDfilename) + + ptwr=towerParameters(EDfilename, gravity=fst['Gravity'], RotMass=p['RotMass']) + + # Calculate the turbine mass: + ptwr['TurbMass'] = ptwr['TwrTpMass'] + ptwr['TwrMass']; + + diff --git a/openfast_toolbox/modules/servodyn.py b/openfast_toolbox/modules/servodyn.py new file mode 100644 index 0000000..d6b6657 --- /dev/null +++ b/openfast_toolbox/modules/servodyn.py @@ -0,0 +1,63 @@ +import os +import ctypes +import platform + +from openfast_toolbox.tools.strings import INFO, FAIL, OK, WARN, print_bold + +def check_discon_library(libpath): + """ + Try to load a DISCON-style library and optionally call its DISCON entry point. + + Parameters + ---------- + libpath : str + Path to the shared library (.dll, .so, .dylib) + + Returns + ------- + success : bool + True if the library is suitable for the current OS and the DISCON function is callable. + msg : str + Description of the result. + """ + # Normalize path + libpath = os.path.abspath(libpath) + + # Check extension against OS + system = platform.system() + expected_ext = { "Windows": ".dll", "Linux": ".so", "Darwin": ".dylib" } + if system not in expected_ext: + WARN(f"Unsupported OS: {system}") + if not libpath.endswith(expected_ext[system]): + WARN(f"Library extension mismatch: expected {expected_ext[system]} for {system}") + + if not os.path.isfile(libpath): + FAIL(f"File not found: {libpath}") + return False + + try: + lib = ctypes.CDLL(libpath) + OK(f"Successully loaded library {libpath}") + except OSError as e: + FAIL(f"Failed to load library {libpath}\nError: {e}") + return False + + # Check if DISCON function exists + try: + discon_func = lib.DISCON + # Optionally set argument/return types (DISCON has a big Fortran-style interface) + # but for a simple check, just ensure we can obtain the symbol + except AttributeError: + FAIL(f"Library loaded but no DISCON symbol found in {libpath}") + return False + # Try a "dry call" — this will fail unless proper arguments are passed. + # We just test that the function pointer exists and is callable. + try: + _ = callable(discon_func) + OK(f'DISCON function is present in library.') + except Exception as e: + FAIL[f"DISCON symbol found but not callable:\nLibrary:{libpath}\nError: {e}"] + return False + + #OK(f'Successfully loaded {libpath} and DISCON function is present.') + return True diff --git a/openfast_toolbox/tools/grids.py b/openfast_toolbox/tools/grids.py new file mode 100644 index 0000000..a6d7c9b --- /dev/null +++ b/openfast_toolbox/tools/grids.py @@ -0,0 +1,218 @@ +import numpy as np +import matplotlib.pyplot as plt + +class BoundingBox: + def __init__(self, x, y, z=None): + """Initialize from arrays of coordinates x,y,(z).""" + self.is3D = z is not None + self.xmin, self.xmax = np.min(x), np.max(x) + self.ymin, self.ymax = np.min(y), np.max(y) + if self.is3D: + self.zmin, self.zmax = np.min(z), np.max(z) + + def lines(self): + """Return line segments (arrays of shape (N,2) or (N,3)) + for plotting the bounding box edges.""" + if not self.is3D: + # 2D rectangle (closed loop) + pts = np.array([ + [self.xmin, self.ymin], + [self.xmax, self.ymin], + [self.xmax, self.ymax], + [self.xmin, self.ymax], + [self.xmin, self.ymin] + ]) + return pts + else: + # 3D: 8 corners + corners = np.array([ + [self.xmin, self.ymin, self.zmin], + [self.xmax, self.ymin, self.zmin], + [self.xmax, self.ymax, self.zmin], + [self.xmin, self.ymax, self.zmin], + [self.xmin, self.ymin, self.zmax], + [self.xmax, self.ymin, self.zmax], + [self.xmax, self.ymax, self.zmax], + [self.xmin, self.ymax, self.zmax] + ]) + # faces as loops of 4 corners (closed) + faces = [ + [0,1,2,3,0], # bottom + [4,5,6,7,4], # top + [0,1,5,4,0], # front + [1,2,6,5,1], # right + [2,3,7,6,2], # back + [3,0,4,7,3] # left + ] + + X, Y, Z = [], [], [] + for f in faces: + X.extend(corners[f,0]); X.append(np.nan) # NaN breaks the line + Y.extend(corners[f,1]); Y.append(np.nan) + Z.extend(corners[f,2]); Z.append(np.nan) + + points = np.zeros((len(X), 3) ) + points[:,0] = X + points[:,1] = Y + points[:,2] = Z + return points + + def contains(self, other, strict: bool = False): + """Return True if self contains other bounding box. + + Parameters + ---------- + other : BoundingBox + The other bounding box to check. + strict : bool, optional + If True, requires strict containment (no shared boundaries). + Default is False. + """ + if self.is3D != other.is3D: + raise Exception('Cannot compare a 2D and 3D box') + if strict: + ok = (self.xmin < other.xmin) and (self.xmax > other.xmax) \ + and (self.ymin < other.ymin) and (self.ymax > other.ymax) + if self.is3D: + ok = ok and (self.zmin < other.zmin) and (self.zmax > other.zmax) + else: + ok = (self.xmin <= other.xmin) and (self.xmax >= other.xmax) \ + and (self.ymin <= other.ymin) and (self.ymax >= other.ymax) + if self.is3D: + ok = ok and (self.zmin <= other.zmin) and (self.zmax >= other.zmax) + return ok + + def plot(self, ax, plane=None, **kwargs): + """Plot bounding box in the chosen plane.""" + pts = self.lines() + if plane is None: + if not self.is3D: + ax.plot(pts[:,0], pts[:,1], **kwargs) + else: + ax.plot(pts[:,0], pts[:,1], pts[:,2], **kwargs) + return + if plane == "XY": + X, Y = pts[:,0],pts[:,1] + elif plane == "XZ" and self.is3D: + X, Y = pts[:,0],pts[:,2] + elif plane == "YZ" and self.is3D: + X, Y = pts[:,1],pts[:,2] + else: + raise ValueError(f"Invalid plane {plane} for this bounding box") + ax.plot(X, Y, **kwargs) + + + +class RegularGrid: + def __init__(self, x0, nx, dx, y0=None, ny=None, dy=None, z0=None, nz=None, dz=None): + """ + Define a regular grid in 2D or 3D. + + Parameters + ---------- + x0, nx, dx : float, int, float + Origin, number of points, spacing along x. + y0, ny, dy : optional + Same for y. + z0, nz, dz : optional + Same for z. + """ + self.x0, self.nx, self.dx = x0, nx, dx + self.y0, self.ny, self.dy = y0, ny, dy + self.z0, self.nz, self.dz = z0, nz, dz + self.is3D = z0 is not None and nz is not None and dz is not None + + # Coordinates + self.x = x0 + np.arange(nx) * dx + if y0 is not None and ny is not None and dy is not None: + self.y = y0 + np.arange(ny) * dy + else: + self.y = None + if self.is3D: + self.z = z0 + np.arange(nz) * dz + else: + self.z = None + + # Bounding box + if self.is3D: + self.bb = BoundingBox(self.x, self.y, self.z) + else: + self.bb = BoundingBox(self.x, self.y) + + def contains_grid(self, other, strict=False): + """Check if this grid fully contains another grid.""" + return self.bb.contains(other.bb, strict=strict) + + def contains_bb(self, other_bb, strict=False): + """Check if this grid contains a given bounding box.""" + return self.bb.contains(other_bb, strict=strict) + + def contains_p(self, p): + """Check if this grid contains a point p=(x,y) or (x,y,z).""" + if self.is3D and len(p) == 3: + x, y, z = p + return (self.bb.xmin <= x <= self.bb.xmax and + self.bb.ymin <= y <= self.bb.ymax and + self.bb.zmin <= z <= self.bb.zmax) + elif not self.is3D and len(p) == 2: + x, y = p + return (self.bb.xmin <= x <= self.bb.xmax and + self.bb.ymin <= y <= self.bb.ymax) + else: + return False + + def plot(self, ax, plane="XY", grid=True, color=(0.3,0.3,0.3), optsGd=None, optsBB=None): + """Plot grid lines and bounding box projection in a given plane.""" + optsGdLoc=dict(ls='-', color=color, lw=0.3) + optsGd=optsGdLoc if optsGd is None else optsGdLoc.update(optsGd) + optsBBLoc=dict(ls='-', color=color, lw=1.0) + optsBB=optsBBLoc if optsBB is None else optsBBLoc.update(optsBB) + + if plane == "XY": + X, Y = self.x, self.y + elif plane == "XZ" and self.is3D: + X, Y = self.x, self.z + elif plane == "YZ" and self.is3D: + X, Y = self.y, self.z + else: + raise ValueError(f"Invalid plane {plane} for this grid") + + # Grid lines + if grid: + ax.vlines(X, ymin=Y[0], ymax=Y[-1], **optsGdLoc) + ax.hlines(Y, xmin=X[0], xmax=X[-1], **optsGdLoc) + # Bounding box + self.bb.plot(ax, plane=plane, **optsBBLoc) + + +if __name__ == "__main__": + # --- 2D test --- + x = np.random.randn(20) + y = np.random.randn(20) +# bb2d = BoundingBox(x, y) +# +# fig, ax = plt.subplots() +# ax.scatter(x, y, label="points") +# bb2d.plot(ax, color='r', lw=2, label="bounding box") +# ax.legend() +# plt.title("2D BoundingBox test") +# plt.show() + + # --- 3D test --- + z = np.random.randn(20) + bb3d = BoundingBox(x, y, z) + + fig = plt.figure() + ax = fig.add_subplot(111, projection='3d') + ax.scatter(x, y, z, c='b', marker='o') + bb3d.plot(ax, color='g', lw=2) + plt.title("3D BoundingBox test") + plt.show() + + # --- containment test --- + x_small = np.random.uniform(-0.5, 0.5, 10) + y_small = np.random.uniform(-0.5, 0.5, 10) + z_small = np.random.uniform(-0.5, 0.5, 10) + small_box = BoundingBox(x_small, y_small, z_small) + print("bb3d contains small_box?", bb3d.contains(small_box)) + diff --git a/openfast_toolbox/tools/strings.py b/openfast_toolbox/tools/strings.py new file mode 100644 index 0000000..0b118a5 --- /dev/null +++ b/openfast_toolbox/tools/strings.py @@ -0,0 +1,391 @@ +import sys +import numpy as np +from html import escape + +# ---------- optional libs ---------- +try: + from termcolor import cprint as _tc_cprint + _HAS_TERMCOLOR = True +except Exception: + _HAS_TERMCOLOR = False + +try: + from IPython import get_ipython + from IPython.display import display, HTML + _IPY = get_ipython() + # ZMQInteractiveShell => Jupyter Notebook / Lab + _IN_JUPYTER = _IPY is not None and _IPY.__class__.__name__ == "ZMQInteractiveShell" +except Exception: + _IN_JUPYTER = False + +# --- HTML +_HTML_COLOR = { + 'red': '#d32f2f', 'yellow': '#f7b500', 'green': '#388e3c', + 'blue': '#1976d2', 'magenta': '#8e24aa', 'cyan': '#0097a7', None: 'inherit' +} + +# --- ASCII Codes +_ANSI_COLOR = { + 'red': '\033[91m', + 'yellow': '\033[93m', + 'green': '\033[92m', + 'blue': '\033[94m', + 'magenta':'\033[95m', + 'cyan': '\033[96m', + None: '' +} +_ATTRS_ANSI = { + 'bold': '\033[1m', + 'underline': '\033[4m' +} +_RESET = '\033[0m' + + + +def cprint_local(msg, color=None, attrs=None, file=sys.stdout, end='\n'): + color_code = _COLOR.get(color, '') + attr_code = ''.join(_ATTR.get(a, '') for a in (attrs or [])) + try: + print(f"{color_code}{attr_code}{msg}{_RESET}", file=file) + except Exception: + # Absolute last resort (no colors, never crash) + print(msg, file=file, end=end) + + + +def cprint(msg, color=None, attrs=None, file=sys.stdout, end='\n'): + """Robust colored / bold print. In Jupyter: render HTML for reliable styling. + In normal terminals: use termcolor if present, else ANSI escapes, else plain print. + `file` follows print() semantics; when in a notebook and file is stdout/stderr + the function uses rich HTML output (display).""" + attrs = attrs or [] + + # 1) Jupyter: render HTML so bold + color always show in output cells + if _IN_JUPYTER and file in (sys.stdout, sys.stderr): + try: + color_css = _HTML_COLOR.get(color, color or 'inherit') + style = '' + if color_css: + style += f'color:{color_css};' + if 'bold' in attrs: + style += 'font-weight:700;' + if 'underline' in attrs: + style += 'text-decoration:underline;' + safe = escape(msg) + html = (f"
{safe}
") + display(HTML(html)) + return + except Exception: + # fall through to other backends if display fails + pass + + # 2) termcolor if available (works well in many terminals) + if _HAS_TERMCOLOR: + try: + _tc_cprint(msg, color=color, attrs=attrs, file=file) + return + except Exception: + pass + + # 3) ANSI fallback + try: + color_code = _ANSI_COLOR.get(color, '') + attr_code = ''.join(_ATTRS_ANSI.get(a, '') for a in attrs) + trailing = _RESET if (color_code or attr_code) else '' + print(f"{color_code}{attr_code}{msg}{trailing}", file=file, end=end) + except Exception: + # 4) last resort: plain text + try: + print(msg, file=file, end=end) + except Exception: + # silence any error (we never want the logger itself to crash) + pass + + +# ------------------------------------------------------------------------- +# --- Convenient functions +# ------------------------------------------------------------------------- +def print_bold(msg, **kwargs): + cprint(msg, attrs=['bold'], **kwargs) + +def FAIL(msg, label='[FAIL] ', **kwargs): + msg = ('\n'+ ' ' * len(label)).join( (label+msg).split('\n') ) # Indending new lines + cprint(msg, color='red', attrs=['bold'], file=sys.stderr, **kwargs) + +def WARN(msg, label='[WARN] ', **kwargs): + msg = ('\n'+ ' ' * len(label)).join( (label+msg).split('\n') ) # Indending new lines + cprint(msg, color='yellow', attrs=['bold'], **kwargs) + +def OK(msg, label='[ OK ] ', **kwargs): + msg = ('\n'+ ' ' * len(label)).join( (label+msg).split('\n') ) # Indending new lines + cprint(msg, color='green', attrs=['bold'], **kwargs) + +def INFO(msg, label='[INFO] ', **kwargs): + msg = ('\n'+ ' ' * len(label)).join( (label+msg).split('\n') ) # Indending new lines + cprint(msg, **kwargs) + + +# -------------------------------------------------------------------------------- +# --- Pretty prints +# -------------------------------------------------------------------------------- +def pretty_num(x, digits=None, nchar=None, align='right', xmin=1e-16, center0=True): + """ + Printing number with "pretty" formatting, either: + - fixed number of decimals by setting digits + OR + - fixed number of characters by setting nchar + + """ + if nchar is not None and digits is not None: + method='fixed_number_of_char_and_digits' + + elif nchar is None: + nchar=7+digits + method='fixed_number_of_digits' + else: + if digits is None: + digits=int(nchar/2) + method='fixed_number_of_char' + if nchar<8: + raise Exception('nchar needs to be at least 7 to accomodate exp notation') + + try: + x = float(x) + except: + s=str(x) + if align=='right': + return s.rjust(nchar) + else: + return s.ljust(nchar) + + if np.abs(x)1e-7: + s= "{:.6f}".format(x) + else: + s= "{:.6e}".format(x) + elif digits==5: + if abs(x)<100000 and abs(x)>1e-6: + s= "{:.5f}".format(x) + else: + s= "{:.5e}".format(x) + elif digits==4: + if abs(x)<10000 and abs(x)>1e-5: + s= "{:.4f}".format(x) + else: + s= "{:.4e}".format(x) + elif digits==3: + if abs(x)<10000 and abs(x)>1e-4: + s= "{:.3f}".format(x) + else: + s= "{:.3e}".format(x) + elif digits==2: + if abs(x)<100000 and abs(x)>1e-3: + s= "{:.2f}".format(x) + else: + s= "{:.2e}".format(x) + elif digits==1: + if abs(x)<100000 and abs(x)>1e-2: + s= "{:.1f}".format(x) + else: + s= "{:.1e}".format(x) + elif digits==0: + if abs(x)<1000000 and abs(x)>1e-1: + s= "{:.0f}".format(x) + else: + s= "{:.0e}".format(x) + else: + raise NotImplementedError('digits',digits) + elif method=='fixed_number_of_char': + xlow = 10**(-(nchar-2)) + xhigh = 10**( (nchar-1)) + if type(x)==int: + raise NotImplementedError() + if abs(x)xlow: + n = int(np.log10(abs(x))) + if n<0: + sfmt='{:'+str(nchar)+'.'+str(nchar-3)+'f'+'}' + elif nchar-3-n<0: + sfmt='{:'+str(nchar-1)+'.0'+'f'+'}' + elif nchar-3-n==0: + sfmt='{:'+str(nchar-1)+'.0'+'f'+'}.' + else: + sfmt='{:'+str(nchar)+'.'+str(nchar-3-n)+'f'+'}' + else: + sfmt='{:'+str(nchar)+'.'+str(nchar-7)+'e'+'}' # Need 7 char for exp + s = sfmt.format(x) + #print(xlow, xhigh, sfmt, len(s), '>'+s+'<') + elif method=='fixed_number_of_char_and_digits': + xlow = 10**(-(nchar-2)) + xhigh = 10**( (nchar-1)) + s = f"{x:.{digits+1}g}" # general format with significant digits + if len(s) > nchar: + # fallback: scientific notation + s = f"{x:.{digits+1}e}" + # truncate or pad to exactly nchar characters + if len(s) > nchar: + s = s[:nchar] + else: + raise NotImplementedError(method) + + if align=='right': + return s.rjust(nchar) + else: + return s.ljust(nchar) + +def prettyMat(M, var=None, digits=2, nchar=None, sindent=' ', align='right', center0=True, newline=True, openChar='[',closeChar=']', sepChar=' ', xmin=1e-16): + """ + return a matrix as a string, with misc output options + INPUTS: + - M: array of float/int + - var: string + """ + s='' + if var is not None: + if not isinstance(var, str): + raise Exception() + s=var+':' + if newline: + s+='\n' + # Corner cases, being nice to user.. + if isinstance(M, str): + s+=M + return s + if not hasattr(M,'__len__'): + s+=pretty_num(M, digits=digits, nchar=nchar, align=align, center0=center0, xmin=xmin) + return s + + M=np.atleast_2d(M) + s+=sindent + for iline,line in enumerate(M): + s+= openChar+sepChar.join([pretty_num(v, digits=digits, nchar=nchar, align=align, center0=center0, xmin=xmin) for v in line ])+closeChar + if iline>> printDict TYPE', type(v)) +# sindentloc = print('{}{20s}:{}'.format(sindent, k, v) + + +def prettyVar(val, var=None, key_fmt='{:15s}', digits=2, xmin=1e-16, **kwargs): + s='' + if var is not None: + s+=key_fmt.format(var)+': ' + # Corner cases, being nice to user.. + if isinstance(val, str): + s+=val + return s + if not hasattr(val,'__len__'): + s+=pretty_num(val, digits=digits, **kwargs) + return s + +def printVar(val, var=None, key_fmt='{:15s}', digits=2, xmin=1e-16, **kwargs): + var, val = _swapArgs(var, val) + s = prettyVar(val, var=var, key_fmt=key_fmt, digits=digits, xmin=xmin, **kwargs) + print(s) + +def printVarTex(val, var=None, key_fmt='{:15s}', digits=2, xmin=1e-16, **kwargs): + var, val = _swapArgs(var, val) + s = '' + if var is not None: + var='$'+var+'$' + s += key_fmt.format(var) + '&' + if isinstance(val, str): + s+=val + elif not hasattr(val,'__len__'): + s+=pretty_num(val, digits=digits, **kwargs) + s+='\\\\' + return print(s) + + +def _swapArgs(var, val): + if var is not None: + if not isinstance(var, str): + if isinstance(val, str): + val, var = var, val # we swap + return var, val + + +# ------------------------------------------------------------------------- +# Example usage +# ------------------------------------------------------------------------- +if __name__ == "__main__": + f= 10.**np.arange(-8,8,1) + f1=10.**np.arange(-8,8,1) + f2=-f1 + f3=f1*0 + M = np.stack((f1,f2,f3,f1)) + d=3 + nc=None + d=None + nc=12 + for x in f: + print(pretty_num(x, digits=d, nchar=nc)) + for x in f: + s=pretty_num(-x, digits=d, nchar=nc) + print(s, len(s), -x) + print(pretty_num(0, digits=d, nchar=nc)) + printMat(M, 'M', digits=1, align='right') + + + FAIL("This is a failure message") + WARN("This is a warning message") + OK("This is a success message")