diff --git a/openfast_toolbox/fastfarm/FASTFarmCaseCreation.py b/openfast_toolbox/fastfarm/FASTFarmCaseCreation.py index 3370790..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): @@ -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 @@ -835,6 +856,21 @@ def setTemplateFilename(self, templatePath=None, templateFiles=None): '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... } """ @@ -845,6 +881,8 @@ def setTemplateFilename(self, templatePath=None, templateFiles=None): 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') @@ -865,7 +903,7 @@ def checkIfExists(f): 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'): @@ -971,6 +1009,13 @@ def checkIfExists(f): 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'): @@ -1180,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/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 2b610eb..fcab27b 100644 --- a/openfast_toolbox/fastfarm/examples/Ex3_FFarmCompleteSetup.py +++ b/openfast_toolbox/fastfarm/examples/Ex3_FFarmCompleteSetup.py @@ -106,7 +106,8 @@ def main(): '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' 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__)