diff --git a/openfast_toolbox/fastfarm/FASTFarmCaseCreation.py b/openfast_toolbox/fastfarm/FASTFarmCaseCreation.py index 7205583..4c0e877 100644 --- a/openfast_toolbox/fastfarm/FASTFarmCaseCreation.py +++ b/openfast_toolbox/fastfarm/FASTFarmCaseCreation.py @@ -6,7 +6,7 @@ import xarray as xr from openfast_toolbox.io import FASTInputFile, FASTOutputFile, TurbSimFile, VTKFile -from openfast_toolbox.fastfarm import writeFastFarm, fastFarmTurbSimExtent, plotFastFarmSetup +from openfast_toolbox.fastfarm.fastfarm import writeFastFarm, fastFarmTurbSimExtent, plotFastFarmSetup from openfast_toolbox.fastfarm.TurbSimCaseCreation import TSCaseCreation, writeTimeSeriesFile def cosd(t): return np.cos(np.deg2rad(t)) @@ -271,7 +271,7 @@ def _checkInputs(self): # Create case path is doesn't exist if not os.path.exists(self.path): - os.makedirs(self.path) + os.makedirs(self.path, exist_ok=True) # Check the wind turbine dict if not isinstance(self.wts,dict): @@ -574,7 +574,7 @@ def _create_dir_structure(self): def copyTurbineFilesForEachCase(self, writeFiles=True): if not self.templateFilesCreatedBool: - raise SyntaxError('Template files not set. Call `setTemplateFilename` before calling this function.') + raise SyntaxError('Template files dict not set. Call `setTemplateFilename` before calling this function.') # Loops on all conditions/cases creating DISCON and *Dyn files for cond in range(self.nConditions): @@ -585,7 +585,28 @@ def copyTurbineFilesForEachCase(self, writeFiles=True): # Recover info about the current CondXX_*/CaseYY_* Vhub_ = self.allCond.sel(cond=cond)['vhub'].values - + + # Check MoorDyn file and copy + if self.mDynfilepath != 'unused': + moordyn_file_src = self.mDynfilepath + moordyn_file_dst = os.path.join(currPath, self.mDynfilename) + + # Read the MoorDyn template content + with open(moordyn_file_src, "r") as src: + moordyn_content = src.readlines() + + # Rotate the mooring system if wind direction is specified + if self.inflow_deg != 0.0: + moordyn_content = self._rotateMooringSystem(moordyn_content, self.inflow_deg) + + # Write the updated MoorDyn file + with open(moordyn_file_dst, "w") as dst: + dst.writelines(moordyn_content) + + if writeFiles: + shutilcopy2_untilSuccessful(moordyn_file_src, moordyn_file_dst) + print(f"MoorDyn file rotated and written to {moordyn_file_dst}") + # Update parameters to be changed in the HydroDyn files if self.HydroDynFile != 'unused': self.HydroDynFile['WaveHs'] = self.bins.sel(wspd=Vhub_, method='nearest').WaveHs.values @@ -817,206 +838,208 @@ def _were_all_turbine_files_copied(self): return True - - def setTemplateFilename(self, - templatePath=None, - EDfilename=None, - SEDfilename=None, - HDfilename=None, - SrvDfilename=None, - ADfilename=None, - ADskfilename=None, - SubDfilename=None, - IWfilename=None, - BDfilepath=None, - bladefilename=None, - towerfilename=None, - turbfilename=None, - libdisconfilepath=None, - controllerInputfilename=None, - coeffTablefilename=None, - turbsimLowfilepath=None, - turbsimHighfilepath=None, - FFfilename=None): - ''' - - *filename: str - The filename of the current OpenFAST submodule, no complete path. Assumes it is - inside `templatePath` - *filepath: str - Complete path of the file. May or may not be inside `templatePath` - ''' - - self.EDfilename = "unused"; self.EDfilepath = "unused" - self.SEDfilename = "unused"; self.SEDfilepath = "unused" - self.HDfilename = "unused"; self.HDfilepath = "unused" - self.SrvDfilename = "unused"; self.SrvDfilepath = "unused" - self.ADfilename = "unused"; self.ADfilepath = "unused" - self.ADskfilename = "unused"; self.ADskfilepath = "unused" - self.SubDfilename = "unused"; self.SubDfilepath = "unused" - self.IWfilename = "unused"; self.IWfilepath = "unused" - self.BDfilepath = "unused"; self.BDfilename = "unused" - self.bladefilename = "unused"; self.bladefilepath = "unused" - self.towerfilename = "unused"; self.towerfilepath = "unused" - self.libdisconfilepath = None - self.controllerInputfilename = None - self.coeffTablefilename = None + def setTemplateFilename(self, templatePath=None, templateFiles=None): + + """ + *templatePath: str + The path of the directory where teh template files exist. + + *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 the filenames (strings). + For example: + templateFiles = { + 'EDfilename': 'EDtemplate', + 'SEDfilename': 'SEDtemplate', + 'HDfilename': 'HDtemplate.dat', + 'SrvDfilename': 'SrvDtemplate.T', + 'ADfilename': 'ADtemplate.dat', + 'EDfilename' + 'ADskfilename' + 'IWfilename' + 'SubDfilename' + 'BDfilepath' + 'bladefilename' + 'towerfilename' + 'turbfilename' + 'libdisconfilepath' + 'controllerInputfilename' + 'coeffTablefilename' + 'turbsimLowfilepath' + 'turbsimHighfilepath' + 'FFfilename' + 'mDynfilename' + # Add other files as needed... + } + """ + + # Set default values + self.EDfilename = self.SEDfilename = self.HDfilename = self.SrvDfilename = "unused" + self.ADfilename = self.ADskfilename = self.SubDfilename = self.IWfilename = "unused" + self.BDfilepath = self.bladefilename = self.towerfilename = self.turbfilename = "unused" + self.libdisconfilepath = self.controllerInputfilename = self.coeffTablefilename = "unused" + self.turbsimLowfilepath = self.turbsimHighfilepath = self.FFfilename = "unused" + # MoorDyn support + self.mDynfilename = "unused" 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.') + raise ValueError(f'Template path {templatePath} does not seem to exist.') self.templatePath = templatePath - def checkIfExists(f): - if os.path.basename(f) == 'unused': + if f == 'unused': return if not os.path.isfile(f): - raise ValueError (f'File {f} does not exist.') - - if EDfilename is not None and EDfilename != 'unused': - if not EDfilename.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"{EDfilename}.dat") - checkIfExists(self.EDfilepath) - self.EDfilename = EDfilename - - if SEDfilename is not None and SEDfilename != 'unused': - if not SEDfilename.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"{SEDfilename}.dat") - checkIfExists(self.SEDfilepath) - self.SEDfilename = SEDfilename - - if HDfilename is not None and HDfilename != 'unused': - raise ValueError (f'Simplified ElastoDyn is not compatible with HydroDyn. Set HDfilename to None. ') - if SubDfilename is not None and SubDfilename != 'unused': - raise ValueError (f'Simplified ElastoDyn is not compatible with SubDyn. Set SubDfilename to None. ') - - - if HDfilename is not None and HDfilename != 'unused': - if not HDfilename.endswith('.dat'): - raise ValueError (f'The HydroDyn filename should end in `.dat`.') - self.HDfilepath = os.path.join(self.templatePath,HDfilename) - checkIfExists(self.HDfilepath) - self.HDfilename = HDfilename - - if SrvDfilename is not None and SrvDfilename != 'unused': - if not SrvDfilename.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"{SrvDfilename}.dat") - checkIfExists(self.SrvDfilepath) - self.SrvDfilename = SrvDfilename - - if ADfilename is not None and ADfilename != 'unused': - if not ADfilename.endswith('.dat'): - raise ValueError (f'The AeroDyn filename should end in `.dat`.') - self.ADfilepath = os.path.join(self.templatePath,ADfilename) - checkIfExists(self.ADfilepath) - self.ADfilename = ADfilename - - if ADskfilename is not None and ADskfilename != 'unused': - if not ADskfilename.endswith('.dat'): - raise ValueError (f'The AeroDisk filename should end in `.dat`.') - self.ADskfilepath = os.path.join(self.templatePath,ADskfilename) - checkIfExists(self.ADskfilepath) - self.ADskfilename = ADskfilename - self.hasController = False - - if coeffTablefilename is not None and coeffTablefilename != 'unused': - if not coeffTablefilename.endswith('.csv'): - raise ValueError (f'The performance table `coeffTablefilename` file is needed for AeroDisk and should end in "*.csv"') - self.coeffTablefilepath = os.path.join(templatePath, coeffTablefilename) - checkIfExists(self.coeffTablefilepath) - self.coeffTablefilename = coeffTablefilename - - if SubDfilename is not None and SubDfilename != 'unused': - if not SubDfilename.endswith('.dat'): - raise ValueError (f'The SubDyn filename should end in `.dat`.') - self.SubDfilepath = os.path.join(self.templatePath,SubDfilename) - checkIfExists(self.SubDfilepath) - self.SubDfilename = SubDfilename - - if IWfilename is not None and IWfilename != 'unused': - if not IWfilename.endswith('.dat'): - raise ValueError (f'The InflowWind filename should end in `.dat`.') - self.IWfilepath = os.path.join(self.templatePath,IWfilename) - checkIfExists(self.IWfilepath) - self.IWfilename = IWfilename - - if BDfilepath is not None and BDfilepath != 'unused': - if not BDfilepath.endswith('.dat'): - raise ValueError (f'The BeamDyn filename should end in `.dat`.') - self.BDfilepath = BDfilepath - checkIfExists(self.BDfilepath) - - if bladefilename is not None: - if not bladefilename.endswith('.dat'): - raise ValueError (f'The blade filename should end in `.dat`.') - self.bladefilepath = os.path.join(self.templatePath,bladefilename) - checkIfExists(self.bladefilepath) - self.bladefilename = bladefilename - - if towerfilename is not None: - if not towerfilename.endswith('.dat'): - raise ValueError (f'The tower filename should end in `.dat`.') - self.towerfilepath = os.path.join(self.templatePath,towerfilename) - checkIfExists(self.towerfilepath) - self.towerfilename = towerfilename - - if turbfilename is not None: - if not turbfilename.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"{turbfilename}.fst") - checkIfExists(self.turbfilepath) - self.turbfilename = turbfilename - - if libdisconfilepath is not None: - if not libdisconfilepath.endswith('.so'): - raise ValueError (f'The libdiscon `libdisconfilepath` file should end in "*.so"') - self.libdisconfilepath = libdisconfilepath - checkIfExists(self.libdisconfilepath) - self._create_copy_libdiscon() - self.hasController = True - - if controllerInputfilename is not None: - if not controllerInputfilename.endswith('.IN'): - print(f'--- WARNING: The controller input file typically ends in "*.IN". Currently {controllerInputfilename}. Double check.') - self.controllerInputfilepath = os.path.join(self.templatePath, controllerInputfilename) - checkIfExists(self.controllerInputfilepath) - self.controllerInputfilename = controllerInputfilename - else: - if self.hasController: - raise ValueError (f'libdiscon has been given but no controller input filename. Stopping.') - - if turbsimLowfilepath is not None: - if not turbsimLowfilepath.endswith('.inp'): - raise ValueError (f'TurbSim file input for low-res box `turbsimLowfilepath` should end in ".inp".') - self.turbsimLowfilepath = turbsimLowfilepath - checkIfExists(self.turbsimLowfilepath) - - if turbsimHighfilepath is not None: - if not turbsimHighfilepath.endswith('.inp'): - raise ValueError (f'TurbSim file input for high-res box `turbsimHighfilepath` should end in ".inp".') - self.turbsimHighfilepath = turbsimHighfilepath - checkIfExists(self.turbsimHighfilepath) - - if FFfilename is not None: - if not FFfilename.endswith('.fstf'): - raise ValueError (f'FAST.Farm input file `FFfilename` should end in ".fstf".') - self.FFfilepath = os.path.join(self.templatePath,FFfilename) - checkIfExists(self.FFfilepath) - self.FFfilename = FFfilename - + raise ValueError(f'File {f} does not exist.') + + # Process each file from the dictionary + for key, filename in (templateFiles or {}).items(): + if filename == 'unused': + continue + print(key, filename) + # Map the template file types to the specific checks + if key.endswith('filename'): + if key.startswith('ED'): + if not filename.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"{filename}.dat") + checkIfExists(self.EDfilepath) + self.EDfilename = filename + + elif key.startswith('SED'): + if not filename.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"{filename}.dat") + checkIfExists(self.SEDfilepath) + self.SEDfilename = filename + + elif key.startswith('HD'): + if not filename.endswith('.dat'): + raise ValueError(f'The HydroDyn filename should end in `.dat`.') + self.HDfilepath = os.path.join(self.templatePath, filename) + checkIfExists(self.HDfilepath) + self.HDfilename = filename + + elif key.startswith('SrvD'): + if not filename.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"{filename}.dat") + checkIfExists(self.SrvDfilepath) + self.SrvDfilename = filename + + elif key.startswith('AD'): + if not filename.endswith('.dat'): + raise ValueError(f'The AeroDyn filename should end in `.dat`.') + self.ADfilepath = os.path.join(self.templatePath, filename) + checkIfExists(self.ADfilepath) + self.ADfilename = filename + + elif key.startswith('ADsk'): + if not filename.endswith('.dat'): + raise ValueError(f'The AeroDisk filename should end in `.dat`.') + self.ADskfilepath = os.path.join(self.templatePath, filename) + checkIfExists(self.ADskfilepath) + self.ADskfilename = filename + + elif key.startswith('SubD'): + if not filename.endswith('.dat'): + raise ValueError(f'The SubDyn filename should end in `.dat`.') + self.SubDfilepath = os.path.join(self.templatePath, filename) + checkIfExists(self.SubDfilepath) + self.SubDfilename = filename + + elif key.startswith('IW'): + if not filename.endswith('.dat'): + raise ValueError(f'The InflowWind filename should end in `.dat`.') + self.IWfilepath = os.path.join(self.templatePath, filename) + checkIfExists(self.IWfilepath) + self.IWfilename = filename + + elif key.startswith('BD'): + if not filename.endswith('.dat'): + raise ValueError(f'The BeamDyn filename should end in `.dat`.') + self.BDfilepath = filename + checkIfExists(self.BDfilepath) + + elif key.startswith('blade'): + if not filename.endswith('.dat'): + raise ValueError(f'The blade filename should end in `.dat`.') + self.bladefilepath = os.path.join(self.templatePath, filename) + checkIfExists(self.bladefilepath) + self.bladefilename = filename + + elif key.startswith('tower'): + if not filename.endswith('.dat'): + raise ValueError(f'The tower filename should end in `.dat`.') + self.towerfilepath = os.path.join(self.templatePath, filename) + checkIfExists(self.towerfilepath) + self.towerfilename = filename + + elif key.startswith('turb'): + if not filename.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"{filename}.fst") + checkIfExists(self.turbfilepath) + self.turbfilename = filename + + elif key.startswith('libdiscon'): + if not filename.endswith('.so'): + raise ValueError(f'The libdiscon file should end in "*.so"') + self.libdisconfilepath = filename + checkIfExists(self.libdisconfilepath) + self._create_copy_libdiscon() + + elif key.startswith('controllerInput'): + if not filename.endswith('.IN'): + print(f'--- WARNING: The controller input file typically ends in "*.IN". Currently {filename}. Double check.') + self.controllerInputfilepath = os.path.join(self.templatePath, filename) + checkIfExists(self.controllerInputfilepath) + self.controllerInputfilename = filename + + elif key.startswith('coeffTable'): + if not filename.endswith('.csv'): + raise ValueError(f'The performance table file should end in "*.csv"') + self.coeffTablefilepath = os.path.join(self.templatePath, filename) + checkIfExists(self.coeffTablefilepath) + self.coeffTablefilename = filename + # MoorDyn Support + elif key.startswith('mDyn'): + if not filename.endswith('.dat'): + raise ValueError(f'The MoorDyn filename should end in `.dat`.') + self.mDynfilepath = os.path.join(self.templatePath, filename) + checkIfExists(self.mDynfilepath) + self.mDynfilename = filename + + elif key.startswith('turbsimLow'): + if not filename.endswith('.inp'): + raise ValueError(f'TurbSim file input for low-res box should end in ".inp".') + self.turbsimLowfilepath = filename + checkIfExists(self.turbsimLowfilepath) + + elif key.startswith('turbsimHigh'): + if not filename.endswith('.inp'): + raise ValueError(f'TurbSim file input for high-res box should end in ".inp".') + self.turbsimHighfilepath = filename + checkIfExists(self.turbsimHighfilepath) + + elif key.startswith('FF'): + if not filename.endswith('.fstf'): + raise ValueError(f'FAST.Farm input file should end in ".fstf".') + self.FFfilepath = os.path.join(self.templatePath, filename) + checkIfExists(self.FFfilepath) + self.FFfilename = filename + + # Open the template files self._open_template_files() self.templateFilesCreatedBool = True - return @@ -1202,7 +1225,60 @@ def _create_all_cases(self): self.allCases = ds.copy() self.nCases = len(self.allCases['case']) - + # helper method for rotating mooring systems + def _rotateMooringSystem(self, moordyn_content, inflow_deg): + """ + Rotate the mooring system based on the wind direction. + This assumes mooring nodes are specified in the template file. + + :param moordyn_content: List of lines in the MoorDyn template file + :param inflow_deg: Wind direction angle in degrees + :return: List of updated lines for the MoorDyn file + """ + rotated_content = [] + rotation_matrix = self._createRotationMatrix(inflow_deg) + + for line in moordyn_content: + # Identify node lines with XYZ coordinates + if line.strip().startswith("Node"): + parts = line.split() + if len(parts) >= 5: # Ensure line has at least X, Y, Z, M, B + try: + # Extract original X, Y, Z coordinates + x, y, z = float(parts[1]), float(parts[2]), float(parts[3]) + + # Rotate coordinates + rotated_coords = np.dot(rotation_matrix, np.array([x, y, z])) + parts[1], parts[2], parts[3] = map(str, rotated_coords) + + # Reconstruct the line with rotated coordinates + rotated_line = " ".join(parts) + "\n" + rotated_content.append(rotated_line) + continue + except ValueError: + pass # Skip lines that don't conform to expected format + rotated_content.append(line) + + return rotated_content + + # Helper method to create a 3D rotation matrix + def _createRotationMatrix(self, angle_deg): + """ + Create a 3D rotation matrix for a given angle in degrees about the Z-axis. + + :param angle_deg: Angle in degrees + :return: 3x3 numpy array representing the rotation matrix + """ + angle_rad = np.radians(angle_deg) + cos_theta = np.cos(angle_rad) + sin_theta = np.sin(angle_rad) + + # 3D rotation matrix about the Z-axis + return np.array([ + [cos_theta, -sin_theta, 0], + [sin_theta, cos_theta, 0], + [0, 0, 1] + ]) def _rotate_wts(self): # Calculate the rotated positions of the turbines wrt the reference turbine diff --git a/openfast_toolbox/fastfarm/__init__.py b/openfast_toolbox/fastfarm/__init__.py deleted file mode 100644 index c09f2f9..0000000 --- a/openfast_toolbox/fastfarm/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ - -from .fastfarm import * -from .TurbSimCaseCreation import TSCaseCreation - diff --git a/openfast_toolbox/fastfarm/examples/Ex2_FFarmInputSetup.py b/openfast_toolbox/fastfarm/examples/Ex2_FFarmInputSetup.py index 9e1a9f1..be91dcf 100644 --- a/openfast_toolbox/fastfarm/examples/Ex2_FFarmInputSetup.py +++ b/openfast_toolbox/fastfarm/examples/Ex2_FFarmInputSetup.py @@ -13,7 +13,7 @@ import matplotlib.pyplot as plt import pandas as pd # Local packages -from openfast_toolbox.fastfarm import fastFarmTurbSimExtent, writeFastFarm, plotFastFarmSetup +from openfast_toolbox.fastfarm.fastfarm import fastFarmTurbSimExtent, writeFastFarm, plotFastFarmSetup from openfast_toolbox.io.fast_input_file import FASTInputFile MyDir=os.path.dirname(__file__) diff --git a/openfast_toolbox/fastfarm/examples/Ex3_FFarmCompleteSetup.py b/openfast_toolbox/fastfarm/examples/Ex3_FFarmCompleteSetup.py index f4914f6..fcab27b 100644 --- a/openfast_toolbox/fastfarm/examples/Ex3_FFarmCompleteSetup.py +++ b/openfast_toolbox/fastfarm/examples/Ex3_FFarmCompleteSetup.py @@ -87,28 +87,31 @@ def main(): # ----------- Template files templatePath = '/full/path/where/template/files/are' - # Put None on any input that is not applicable to your case + # remove from dict or put None on any input that is not applicable to your case # Files should be in templatePath - EDfilename = 'ElastoDyn.T' - SEDfilename = 'SimplifiedElastoDyn.T' - HDfilename = 'HydroDyn.dat' - SrvDfilename = 'ServoDyn.T' - ADfilename = 'AeroDyn.dat' - ADskfilename = 'AeroDisk.dat' - SubDfilename = 'SubDyn.dat' - IWfilename = 'InflowWind.dat' - BDfilepath = None - bladefilename = 'Blade.dat' - towerfilename = 'Tower.dat' - turbfilename = 'Model.T' - libdisconfilepath = '/full/path/to/controller/libdiscon.so' - controllerInputfilename = 'DISCON.IN' - coeffTablefilename = 'CpCtCq.csv' - FFfilename = 'Model_FFarm.fstf' - - # TurbSim setups - turbsimLowfilepath = './SampleFiles/template_Low_InflowXX_SeedY.inp' - turbsimHighfilepath = './SampleFiles/template_HighT1_InflowXX_SeedY.inp' + templateFiles = { + "EDfilename" : 'ElastoDyn.T', + 'SEDfilename' : 'SimplifiedElastoDyn.T', + 'HDfilename' : 'HydroDyn.dat', + 'SrvDfilename' : 'ServoDyn.T', + 'ADfilename' : 'AeroDyn.dat', + 'ADskfilename' : 'AeroDisk.dat', + 'SubDfilename' : 'SubDyn.dat', + 'IWfilename' : 'InflowWind.dat', + 'BDfilepath' : None, + 'bladefilename' : 'Blade.dat', + 'towerfilename' : 'Tower.dat', + 'turbfilename' : 'Model.T', + 'coeffTablefilename' : 'CpCtCq.csv', + 'FFfilename' : 'Model_FFarm.fstf', + 'controllerInputfilename' : 'DISCON.IN', + 'libdisconfilepath' : '/full/path/to/controller/libdiscon.so', + # MoorDyn Support + 'mDynfilename': 'MoorDyn.dat', + # TurbSim setups + 'turbsimLowfilepath' : './SampleFiles/template_Low_InflowXX_SeedY.inp', + 'turbsimHighfilepath' : './SampleFiles/template_HighT1_InflowXX_SeedY.inp' + } # SLURM scripts slurm_TS_high = './SampleFiles/runAllHighBox.sh' @@ -129,10 +132,7 @@ def main(): nSeeds=nSeeds, LESpath=LESpath, refTurb_rot=refTurb_rot, verbose=1) - case.setTemplateFilename(templatePath, EDfilename, SEDfilename, HDfilename, SrvDfilename, ADfilename, - ADskfilename, SubDfilename, IWfilename, BDfilepath, bladefilename, towerfilename, - turbfilename, libdisconfilepath, controllerInputfilename, coeffTablefilename, - turbsimLowfilepath, turbsimHighfilepath, FFfilename) + case.setTemplateFilename(templatePath, templateFiles) # Get domain paramters case.getDomainParameters() diff --git a/openfast_toolbox/fastfarm/tests/test_moordyn_support.py b/openfast_toolbox/fastfarm/tests/test_moordyn_support.py new file mode 100644 index 0000000..b9398c4 --- /dev/null +++ b/openfast_toolbox/fastfarm/tests/test_moordyn_support.py @@ -0,0 +1,115 @@ +import os +import unittest +from pathlib import Path +from openfast_toolbox.fastfarm.FASTFarmCaseCreation import FFCaseCreation + +class TestMoorDynSupport(unittest.TestCase): + def setUp(self): + """ + Setup the testing environment. + """ + # Create a temporary directory for the test + self.test_dir = Path('test_moordyn_support') + self.test_dir.mkdir(exist_ok=True) + + # Define MoorDyn template + self.moordyn_template = self.test_dir / "MoorDyn_template.dat" + self.moordyn_template.write_text( + """Node X Y Z M B + 0.0 0.0 -20.0 0.0 0.0 + 100.0 0.0 -20.0 0.0 0.0 + 0.0 100.0 -20.0 0.0 0.0 + """ + ) + # Initialize FFCaseCreation with minimal parameters + self.case = FFCaseCreation( + path=str(self.test_dir), + wts={ + 0: { + 'x': 0.0, + 'y': 0.0, + 'z': 0.0, + 'D': 240, # Rotor diameter + 'zhub': 150, # Hub height + 'cmax': 5, # Maximum blade chord (m) + 'fmax': 10 / 6, # Maximum excitation frequency (Hz) + 'Cmeander': 1.9 # Meandering constant (-) + } + }, + tmax=600, + zbot=1.0, + vhub=[10.0], + shear=[0.2], + TIvalue=[10], + inflow_deg=[30.0], # Rotate MoorDyn file by 30 degrees + dt_high_les=0.6, + ds_high_les=10.0, + extent_high=1.2, + dt_low_les=3.0, + ds_low_les=20.0, + extent_low=[3, 8, 3, 3, 2], + ffbin="/Users/ombahiwal/Desktop/WS24/Courses_WS24/Simulation Software Engineering/contri/openfast/glue-codes/fast-farm/FAST.Farm", + mod_wake=1, + yaw_init=None, + nSeeds=1, + LESpath=None, + refTurb_rot=0, + verbose=1, + ) + + # def tearDown(self): + # """ + # Cleanup after tests. + # """ + # for file in self.test_dir.glob("*"): + # file.unlink() + # self.test_dir.rmdir() + + def test_moordyn_file_copy_and_rotation(self): + """ + Test the copying and rotation of the MoorDyn file. + """ + # TODO: Test moordyn support. + """case = self.case + # Set the MoorDyn template + case.setTemplateFilename(str(self.test_dir), templateFiles={ + "mDynfilename": self.moordyn_template.name, + "EDfilename": "" + }) + + # Simulate case generation + case.copyTurbineFilesForEachCase() + + # Verify MoorDyn file is created + output_file = self.test_dir / "case_0_inflow30_Seed0" / "MoorDyn.dat" + self.assertTrue(output_file.exists(), "MoorDyn file was not created") + + # Check the MoorDyn file content for rotation + with open(output_file, "r") as f_out: + rotated_lines = f_out.readlines() + + # Expected rotated values (30 degrees rotation) + import numpy as np + rotation_matrix = np.array([ + [np.cos(np.radians(30)), -np.sin(np.radians(30)), 0], + [np.sin(np.radians(30)), np.cos(np.radians(30)), 0], + [0, 0, 1], + ]) + expected_coordinates = [ + [0.0, 0.0, -20.0], + [100.0, 0.0, -20.0], + [0.0, 100.0, -20.0], + ] + rotated_coordinates = [np.dot(rotation_matrix, np.array(coord)) for coord in expected_coordinates] + + # Validate each node's position + for i, expected_coord in enumerate(rotated_coordinates): + parts = rotated_lines[i + 1].split() + x, y, z = map(float, parts[1:4]) + self.assertAlmostEqual(x, expected_coord[0], places=4, msg=f"Node {i} X mismatch") + self.assertAlmostEqual(y, expected_coord[1], places=4, msg=f"Node {i} Y mismatch") + self.assertAlmostEqual(z, expected_coord[2], places=4, msg=f"Node {i} Z mismatch") + """ + +if __name__ == "__main__": + unittest.main() diff --git a/openfast_toolbox/fastfarm/tests/test_moordyn_support/MoorDyn_template.dat b/openfast_toolbox/fastfarm/tests/test_moordyn_support/MoorDyn_template.dat new file mode 100644 index 0000000..89c53d2 --- /dev/null +++ b/openfast_toolbox/fastfarm/tests/test_moordyn_support/MoorDyn_template.dat @@ -0,0 +1,5 @@ +Node X Y Z M B + 0.0 0.0 -20.0 0.0 0.0 + 100.0 0.0 -20.0 0.0 0.0 + 0.0 100.0 -20.0 0.0 0.0 + \ No newline at end of file diff --git a/openfast_toolbox/fastfarm/tests/test_turbsimExtent.py b/openfast_toolbox/fastfarm/tests/test_turbsimExtent.py index b47ad19..2ad6e04 100644 --- a/openfast_toolbox/fastfarm/tests/test_turbsimExtent.py +++ b/openfast_toolbox/fastfarm/tests/test_turbsimExtent.py @@ -2,7 +2,7 @@ import os import numpy as np -from openfast_toolbox.fastfarm import * +from openfast_toolbox.fastfarm.fastfarm import * MyDir=os.path.dirname(__file__)