diff --git a/README.md b/README.md index 545c8f3..facd684 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# pyfast +# openfast_toolbox [![Build status](https://github.com/openfast/python-toolbox/workflows/Development%20Pipeline/badge.svg)](https://github.com/OpenFAST/python-toolbox/actions?query=workflow%3A%22Development+Pipeline%22) [![Python: 3.6+](https://img.shields.io/badge/python-3.6%2B-informational)](https://www.python.org/) @@ -22,21 +22,21 @@ pytest The repository contains a set of small packages: -- input\_output: read/write OpenFAST/FAST.Farm/OLAF input and output files (see [README](pyFAST/input_output)) -- postpro: postprocess OpenFAST outputs (extract radial data, compute fatigue loads) (see [examples](pyFAST/postpro/examples)) -- linearization: tools to deal with OpenFAST linearization, e.g. generate a Campbell diagram (see [examples](pyFAST/linearization/examples/)) +- input\_output: read/write OpenFAST/FAST.Farm/OLAF input and output files (see [README](openfast_toolbox/io)) +- postpro: postprocess OpenFAST outputs (extract radial data, compute fatigue loads) (see [examples](openfast_toolbox/postpro/examples)) +- linearization: tools to deal with OpenFAST linearization, e.g. generate a Campbell diagram (see [examples](openfast_toolbox/linearization/examples/)) - aeroacoustics: tools for aeroacoustics (generate BL files and plot outputs) -- case\_generation: tools to generate and run a set of input of OpenFAST input files (see [examples](pyFAST/case_generation/examples)) +- case\_generation: tools to generate and run a set of input of OpenFAST input files (see [examples](openfast_toolbox/case_generation/examples)) ## QuickStart and main usage ### Read and write files -Find examples scripts in this [folder](pyFAST/input_output/examples) and the different fileformats [here](pyFAST/input_output). +Find examples scripts in this [folder](openfast_toolbox/io/examples) and the different fileformats [here](openfast_toolbox/io). Read an AeroDyn file (or any OpenFAST input file), modifies some values and write the modified file: ```python -from pyFAST.input_output import FASTInputFile +from openfast_toolbox.io import FASTInputFile filename = 'AeroDyn.dat' f = FASTInputFile(filename) f['TwrAero'] = True @@ -46,7 +46,7 @@ f.write('AeroDyn_Changed.dat') Read an OpenFAST binary output file and convert it to a pandas DataFrame ```python -from pyFAST.input_output import FASTOutputFile +from openfast_toolbox.io import FASTOutputFile df = FASTOutputFile('5MW.outb').toDataFrame() time = df['Time_[s]'] Omega = df['RotSpeed_[rpm]'] @@ -54,7 +54,7 @@ Omega = df['RotSpeed_[rpm]'] Read a TurbSim binary file, modify it and write it back ```python -from pyFAST.input_output import TurbSimFile +from openfast_toolbox.io import TurbSimFile ts = TurbSimFile('Turb.bts') print(ts.keys()) print(ts['u'].shape) @@ -63,27 +63,27 @@ tw.write('NewTurbulenceBox.bts') ``` ### Polar/airfoil manipulation -Find examples scripts in this [folder](pyFAST/polar/examples). +Find examples scripts in this [folder](openfast_toolbox/polar/examples). Read a CSV file with `alpha, Cl, Cd, Cm`, and write it to AeroDyn format (also computes unsteady coefficients) ```python -from pyFAST.airfoils.Polar import Polar -polar = Polar('pyFAST/airfoils/data/DU21_A17.csv', fformat='delimited') +from openfast_toolbox.airfoils.Polar import Polar +polar = Polar('openfast_toolbox/airfoils/data/DU21_A17.csv', fformat='delimited') ADpol = polar.toAeroDyn('AeroDyn_Polar_DU21_A17.dat') ``` ### Write a set of OpenFAST input file for multiple simulations -Find examples scripts in this [folder](pyFAST/case_generation/examples). +Find examples scripts in this [folder](openfast_toolbox/case_generation/examples). ### Postprocessing Below are different scripts to manipulate OpenFAST outputs: -- [Extract average radial data](pyFAST/postpro/examples/Example_RadialPostPro.py). -- [Interpolate data at different radial positions](pyFAST/postpro/examples/Example_RadialInterp.py). -- [Compute damage equivalent loads](pyFAST/postpro/examples/Example_EquivalentLoad.py). -- [Change column names and units](pyFAST/postpro/examples/Example_Remap.py). +- [Extract average radial data](openfast_toolbox/postpro/examples/Example_RadialPostPro.py). +- [Interpolate data at different radial positions](openfast_toolbox/postpro/examples/Example_RadialInterp.py). +- [Compute damage equivalent loads](openfast_toolbox/postpro/examples/Example_EquivalentLoad.py). +- [Change column names and units](openfast_toolbox/postpro/examples/Example_Remap.py). ## Future work and friend projects diff --git a/data/NREL5MW/5MW_Land_Lin_Rotating/postpro_MultiLinFiles_OneOP.py b/data/NREL5MW/5MW_Land_Lin_Rotating/postpro_MultiLinFiles_OneOP.py index 7059f85..4b301d2 100644 --- a/data/NREL5MW/5MW_Land_Lin_Rotating/postpro_MultiLinFiles_OneOP.py +++ b/data/NREL5MW/5MW_Land_Lin_Rotating/postpro_MultiLinFiles_OneOP.py @@ -2,14 +2,14 @@ Script to postprocess linearization files from OpenFAST for one operating point. Adapted from: - https://github.com/OpenFAST/python-toolbox/blob/dev/pyFAST/linearization/examples/ex2a_MultiLinFiles_OneOP.py + https://github.com/OpenFAST/python-toolbox/blob/dev/openfast_toolbox/linearization/examples/ex2a_MultiLinFiles_OneOP.py """ import os import glob import numpy as np -import pyFAST -import pyFAST.linearization as lin +import openfast_toolbox +import openfast_toolbox.linearization as lin ## Script Parameters fstFile = './Main.fst' # Main .fst file, .lin files will be sought for with same basename diff --git a/data/linearization_outputs/README.md b/data/linearization_outputs/README.md index 7438fe5..5421c58 100644 --- a/data/linearization_outputs/README.md +++ b/data/linearization_outputs/README.md @@ -1,5 +1,5 @@ The outputs in this directory should match the ones given by the example script: - pyFAST/linearization/runCampbell. + openfast_toolbox/linearization/runCampbell. Using the 5MW Land turbine, at given operating points. diff --git a/developper_notes.md b/developper_notes.md new file mode 100644 index 0000000..b1955b8 --- /dev/null +++ b/developper_notes.md @@ -0,0 +1,76 @@ +# Developer Notes + + + +## Release Steps +Typically releases are done for each new version of OpenFAST + +1. Create a pull request from main to dev +2. Make sure the input files in the `data` directory are compatible with the new OpenFAST version +3. Change the file VERSION (and/or setup.py) and push to the pull request +4. Merge pull request to main +5. Tag the commit using `git tag -a vX.X.X` and push to remote: `git push --tags``. +6. Upload to pypi and conda (see below) +7. Merge main to dev + + +## Upload a new version for pip +Detailled steps are provided further below. + +### Summary +Remember to change VERSION file and/or setup.py +```bash +python setup.py sdist +twine upload dist/* # upload to pypi +``` + + + +### (Step 0 : create an account on pypi) + +### Step 1: go to your repo +Go to folder +```bash +cd path/to/python-toolbox +``` + +### Step 2: change version in setup.py and tag it +change VERSION in setup.py +``` +git add setup.py VERSION +git commit "Version X.X.X" +git tag -a +``` + +### Step 3: Create a source distribution +```bash +python setup.py sdist +``` + +### Step 4: Install twine +```bash +pip install twine +``` + +### Step 5: Ubplot to pypi +Run twine to upload to Pypi (will ask for username and password) +```bash +twine upload dist/* +``` + +### After clone / first time +Add `.gitconfig` to your path, to apply filters on jupyter notebooks +```bash +git config --local include.path ../.gitconfig +``` + + + +## Upload a new version to Conda +TODO TODO TODO + +conda-forge, +make a pull request there. + - setup build script (https://conda-forge.org/docs/maintainer/adding_pkgs.html). + see e.g. FLORIS (https://github.com/conda-forge/floris-feedstock/blob/master/recipe/meta.yaml). + - make a pull request to https://github.com/conda-forge/staged-recipes/ diff --git a/openfast_toolbox/__init__.py b/openfast_toolbox/__init__.py new file mode 100644 index 0000000..511afef --- /dev/null +++ b/openfast_toolbox/__init__.py @@ -0,0 +1,14 @@ +"""Initialize everything""" +import os +from openfast_toolbox.common import * +# Make main io tools available +from .io import read +from .io.fast_input_file import FASTInputFile +from .io.fast_output_file import FASTOutputFile +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() + + diff --git a/pyFAST/aeroacoustics/__init__.py b/openfast_toolbox/aeroacoustics/__init__.py similarity index 100% rename from pyFAST/aeroacoustics/__init__.py rename to openfast_toolbox/aeroacoustics/__init__.py diff --git a/pyFAST/aeroacoustics/aa_tools.py b/openfast_toolbox/aeroacoustics/aa_tools.py similarity index 100% rename from pyFAST/aeroacoustics/aa_tools.py rename to openfast_toolbox/aeroacoustics/aa_tools.py diff --git a/pyFAST/aeroacoustics/examples/README.md b/openfast_toolbox/aeroacoustics/examples/README.md similarity index 100% rename from pyFAST/aeroacoustics/examples/README.md rename to openfast_toolbox/aeroacoustics/examples/README.md diff --git a/openfast_toolbox/aeroacoustics/examples/_plot_directivity.py b/openfast_toolbox/aeroacoustics/examples/_plot_directivity.py new file mode 100644 index 0000000..9501f34 --- /dev/null +++ b/openfast_toolbox/aeroacoustics/examples/_plot_directivity.py @@ -0,0 +1,86 @@ +import numpy as np +import pandas as pd +import matplotlib.pyplot as plt +from parse import * +import re, os, platform +from openfast_toolbox.io.fast_output_file import FASTOutputFile + +######################################################################################################################################### +## User inputs +# Save plot and/or data? +save_fig = False +save_data = False +fig_ext = '.png' + +# Number of revolutions (n) to average spectra +n = 1 + +######################################################################################################################################### +## Paths to files +if platform.system() == 'Windows': + FAST_directory = os.path.dirname( os.path.dirname( os.path.dirname( os.path.dirname( os.path.realpath(__file__) ) ) ) ) + os.sep + 'reg_tests' + os.sep + 'r-tests' + os.sep + 'glue-codes' + os.sep + 'openfast' + os.sep + 'IEA_LB_RWT-AeroAcoustics' +else: + FAST_directory = os.path.dirname( os.path.dirname( os.path.dirname( os.path.dirname( os.path.realpath(__file__) ) ) ) ) + os.sep + 'openfast' + os.sep + 'reg_tests' + os.sep + 'r-tests' + os.sep + 'glue-codes' + os.sep + 'openfast' + os.sep + 'IEA_LB_RWT-AeroAcoustics' +AAfilename = FAST_directory + os.sep + 'IEA_LB_RWT-AeroAcoustics_1.out' +OFfilename = FAST_directory + os.sep + 'IEA_LB_RWT-AeroAcoustics.out' +locfilename = FAST_directory + os.sep + 'AA_ObserverLocations_Map.dat' +output_dir = os.path.dirname( os.path.realpath(__file__) ) +outputfilename = output_dir + os.sep + "data_output1" + +######################################################################################################################################### +## Read in data, manipulate it, and plot it +# reads in file data +AA_1 = FASTOutputFile(AAfilename).toDataFrame() +OF = FASTOutputFile(OFfilename).toDataFrame() +location = pd.read_csv(locfilename,delimiter='\s+',skiprows=[0,1],names=['x','y','z']) + +# determine number of observers +num_obs = AA_1.shape[1]-1 + +# calculate sample time for n revolutions +rpm = OF[["RotSpeed_[rpm]"]].mean()[0] +yaw = OF[["YawPzn_[deg]"]].mean()[0] / 180. * np.pi +time_revs = n*60/rpm +tot_time = AA_1["Time_[s]"].max() +if time_revs < tot_time: + sample_time = tot_time - time_revs +else: + print("Error: Time for number of revolutions exceeds simulation time. Reduce n.") + raise SystemExit('') + +# slice AA dataframe for t > sample_time +AA_1 = AA_1[AA_1["Time_[s]"] > sample_time] +AA_1=AA_1.drop("Time_[s]",axis=1) + +# average P over rotor revolution +AA_1 = AA_1.mean() + +# merge location info with SPL info +AA_1=AA_1.reset_index() +AA_1=AA_1.drop("index",axis=1) +AA_1=pd.merge(location,AA_1,left_index=True,right_index=True) +AA_1=AA_1.rename(index=str,columns={0:"SPL"}) + +# contour plot of SPL for each location +if num_obs < 3: + print("Error: Need at least 3 observers to generate contour.") +else: + x=AA_1['x']; + y=AA_1['y']; + z=AA_1['SPL']; + fs = 10 + fig,ax=plt.subplots() + ax.set_aspect('equal') + ax.set_xlabel('x [m]', fontsize=fs+2, fontweight='bold') + ax.set_ylabel('y [m]', fontsize=fs+2, fontweight='bold') + tcf=ax.tricontourf(x,y,z, range(58, 84, 1)) + fig.colorbar(tcf,orientation="vertical").set_label(label = 'Overall SPL [dB]', fontsize=fs+2,weight='bold') + if save_fig == True: + fig_name = 'directivity_map' + fig.savefig(output_dir + os.sep + fig_name + fig_ext) + plt.show() + +# export to csv +if save_data == True: + AA_1.to_csv(r'{}-data.csv'.format(outputfilename)) + diff --git a/openfast_toolbox/aeroacoustics/examples/_plot_mechanisms.py b/openfast_toolbox/aeroacoustics/examples/_plot_mechanisms.py new file mode 100644 index 0000000..0216a0f --- /dev/null +++ b/openfast_toolbox/aeroacoustics/examples/_plot_mechanisms.py @@ -0,0 +1,112 @@ +import numpy as np +import pandas as pd +import matplotlib.pyplot as plt +from parse import * +import re, os, platform +from openfast_toolbox.io.fast_output_file import FASTOutputFile + +######################################################################################################################################### +## User inputs +# Save plot and/or data? +save_fig = False +save_data = False +fig_ext = '.png' + +# Number of revolutions (n) to average spectra +n = 1 + +######################################################################################################################################### +## Paths to files +if platform.system() == 'Windows': + FAST_directory = os.path.dirname( os.path.dirname( os.path.dirname( os.path.dirname( os.path.realpath(__file__) ) ) ) ) + os.sep + 'reg_tests' + os.sep + 'r-tests' + os.sep + 'glue-codes' + os.sep + 'openfast' + os.sep + 'IEA_LB_RWT-AeroAcoustics' +else: + FAST_directory = os.path.dirname( os.path.dirname( os.path.dirname( os.path.dirname( os.path.realpath(__file__) ) ) ) ) + os.sep + 'openfast' + os.sep + 'reg_tests' + os.sep + 'r-tests' + os.sep + 'glue-codes' + os.sep + 'openfast' + os.sep + 'IEA_LB_RWT-AeroAcoustics' +AAfilename = FAST_directory + os.sep + 'IEA_LB_RWT-AeroAcoustics_3.out' +OFfilename = FAST_directory + os.sep + 'IEA_LB_RWT-AeroAcoustics.out' +output_dir = os.path.dirname( os.path.realpath(__file__) ) +outputfilename = output_dir + os.sep + "data_output2" + +######################################################################################################################################### +## Read in data, manipulate it, and plot it + +# Read in file data +AA_3 = FASTOutputFile(AAfilename).toDataFrame() +OF = FASTOutputFile(OFfilename).toDataFrame() + +# Determine number of observers +num_obs = int((AA_3.shape[1]-1)/(7*34)) + +# Calculate sample time for n revolutions +rpm = OF[["RotSpeed_[rpm]"]].mean()[0] +time_revs = n*60/rpm +tot_time = AA_3["Time_[s]"].max() +if time_revs < tot_time: + sample_time = tot_time - time_revs +else: + print("Error: Time for number of revolutions exceeds simulation time. Reduce n.") + raise SystemExit('') + +# Slice AA dataframe for t > sample_time +AA_3 = AA_3[AA_3["Time_[s]"] > sample_time] +AA_3=AA_3.drop("Time_[s]",axis=1) + +# Average SPL for each observer +AA_3 = AA_3.mean() + +# Manipulate PD dataframes +# convert to dataframe with appropriate columns +cols = ['Observer','Mechanism','Frequency (Hz)','SPL (dB)'] +aa_3 = pd.DataFrame(columns=cols) +for i in AA_3.index: + nums = re.findall(r"[-+]?\d*\.\d+|\d+",i) + aa_3.loc[len(aa_3)] = [nums[0],nums[2],nums[1],AA_3[i]] + +AA_3 = aa_3 + +# rename mechanism for legend +for i in range(0,AA_3.last_valid_index()+1): + if AA_3.loc[i,"Mechanism"]=='1': + AA_3.loc[i,"Mechanism"]="LBL-VS" + if AA_3.loc[i,"Mechanism"]=='2': + AA_3.loc[i,"Mechanism"]="TBL-TE-PS" + if AA_3.loc[i,"Mechanism"]=='3': + AA_3.loc[i,"Mechanism"]="TBL-TE-SS" + if AA_3.loc[i,"Mechanism"]=='4': + AA_3.loc[i,"Mechanism"]="TBL-TE-AoA" + if AA_3.loc[i,"Mechanism"]=='5': + AA_3.loc[i,"Mechanism"]="TE Bluntness" + if AA_3.loc[i,"Mechanism"]=='6': + AA_3.loc[i,"Mechanism"]="Tip Vortex" + if AA_3.loc[i,"Mechanism"]=='7': + AA_3.loc[i,"Mechanism"]="TI" + +AA_3["Observer"]=AA_3["Observer"].apply(pd.to_numeric) +AA_3["Frequency (Hz)"]=AA_3["Frequency (Hz)"].apply(pd.to_numeric) +AA_3["SPL (dB)"]=AA_3["SPL (dB)"].apply(pd.to_numeric) + + +# Plot spectra +fs = 10 +for j in range(num_obs): + fig,ax=plt.subplots() + plt.xscale('log') + ax.set_xlabel('Frequency (Hz)', fontsize=fs+2, fontweight='bold') + ax.set_ylabel('SPL (dB)', fontsize=fs+2, fontweight='bold') + for i in range(7): + plt.plot(AA_3["Frequency (Hz)"][j*34*7 + i : j*34*7 + i + 34 * 7:7], AA_3["SPL (dB)"][j*34*7 + i : j*34*7 + i + 34 * 7:7], label = AA_3.loc[i,"Mechanism"]) + ax.set_title('Observer ' + str(j), fontsize=fs+2, fontweight='bold') + plt.grid(color=[0.8,0.8,0.8], linestyle='--') + ax.set_ylim(0,) + ax.legend() + if save_fig == True: + fig_name = 'spectra_Obs' + str(j) + fig_ext + fig.savefig(output_dir + os.sep + fig_name) + plt.show() + +# Export to csv +if save_data == True: + AA_3.to_csv(r'{}-data.csv'.format(outputfilename)) + + + + diff --git a/openfast_toolbox/aeroacoustics/examples/_plot_rotor_map.py b/openfast_toolbox/aeroacoustics/examples/_plot_rotor_map.py new file mode 100644 index 0000000..32d9f32 --- /dev/null +++ b/openfast_toolbox/aeroacoustics/examples/_plot_rotor_map.py @@ -0,0 +1,92 @@ +import numpy as np +import pandas as pd +import matplotlib.pyplot as plt +from parse import * +import re, os, platform +import matplotlib.colors +from openfast_toolbox.io.fast_output_file import FASTOutputFile + +######################################################################################################################################### +## User inputs +# Save plot and/or data? +save_fig = False +save_data = False +fig_ext = '.png' +R = 65. +R_hub = 2. + +# Number of revolutions (n) to average spectra +n = 1 + +######################################################################################################################################### +## Paths to files +if platform.system() == 'Windows': + FAST_directory = os.path.dirname( os.path.dirname( os.path.dirname( os.path.dirname( os.path.realpath(__file__) ) ) ) ) + os.sep + 'reg_tests' + os.sep + 'r-tests' + os.sep + 'glue-codes' + os.sep + 'openfast' + os.sep + 'IEA_LB_RWT-AeroAcoustics' +else: + FAST_directory = os.path.dirname( os.path.dirname( os.path.dirname( os.path.dirname( os.path.realpath(__file__) ) ) ) ) + os.sep + 'openfast' + os.sep + 'reg_tests' + os.sep + 'r-tests' + os.sep + 'glue-codes' + os.sep + 'openfast' + os.sep + 'IEA_LB_RWT-AeroAcoustics' +AAfilename = FAST_directory + os.sep + 'IEA_LB_RWT-AeroAcoustics_4.out' +OFfilename = FAST_directory + os.sep + 'IEA_LB_RWT-AeroAcoustics.out' +locfilename = FAST_directory + os.sep + 'AA_ObserverLocations.dat' +output_dir = os.path.dirname( os.path.realpath(__file__) ) +outputfilename = output_dir + os.sep + "data_output4" + +######################################################################################################################################### + + +location = pd.read_csv(locfilename,delimiter='\s+',skiprows=[0,1],names=['x','y','z']) + + +AA_1 = FASTOutputFile(AAfilename).toDataFrame() +OF = FASTOutputFile(OFfilename).toDataFrame() + + +with open(AAfilename, 'r') as f: + f.readline() + f.readline() + f.readline() + n_obs = int(f.readline().split()[-1]) + n_blades = int(f.readline().split()[-1]) + n_nodes = int(f.readline().split()[-1]) +f.close() + +k = np.ones(n_obs) +for i in range(n_obs): + if location['x'][i] < 0: + k[i] = -1 + +phi = OF['Azimuth_[deg]'] / 180. * np.pi +phi_interp = np.interp(AA_1['Time_[s]'], OF['Time_[s]'], phi) +index = [] +for i in range(1, len(phi_interp)): + if phi_interp[i] < phi_interp[i-1]: + index.append(i) + +y_b = np.linspace(R_hub, R, n_nodes) +x_b = np.zeros_like(y_b) + +n_pts = index[-1] - index[-2] + +for j in range(n_obs): + x = np.zeros((n_pts,n_nodes)) + y = np.zeros((n_pts,n_nodes)) + + for i in range(n_pts): + x[i,:] = x_b * np.cos(k[j]*phi_interp[i + index[-2]]) - y_b * np.sin(k[j]*phi_interp[i + index[-2]]) + y[i,:] = x_b * np.sin(k[j]*phi_interp[i + index[-2]]) + y_b * np.cos(k[j]*phi_interp[i + index[-2]]) + + z = np.array(AA_1)[index[-2]:index[-1], 1+j:1 + 30*n_obs + j:n_obs] + fs = 10 + fig,ax=plt.subplots() + ax.set_aspect('equal') + ax.set_xlabel('y [m]', fontsize=fs+2, fontweight='bold') + ax.set_ylabel('z [m]', fontsize=fs+2, fontweight='bold') + ax.set_title('Observer ' + str(j), fontsize=fs+2, fontweight='bold') + tcf=ax.tricontourf(x.flatten(),y.flatten(),z.flatten(), range(20,75)) + fig.colorbar(tcf,orientation="vertical").set_label(label = 'Overall SPL [dB]', fontsize=fs+2,weight='bold') + if save_fig == True: + fig_name = 'rotor_map_Obs' + str(j) + fig_ext + fig.savefig(output_dir + os.sep + fig_name) + plt.show() + + + diff --git a/openfast_toolbox/aeroacoustics/examples/_plot_spectra.py b/openfast_toolbox/aeroacoustics/examples/_plot_spectra.py new file mode 100644 index 0000000..7487b77 --- /dev/null +++ b/openfast_toolbox/aeroacoustics/examples/_plot_spectra.py @@ -0,0 +1,87 @@ +import numpy as np +import pandas as pd +import matplotlib.pyplot as plt +from parse import * +import re, os, platform +from openfast_toolbox.io.fast_output_file import FASTOutputFile + +######################################################################################################################################### +## User inputs +# Save plot and/or data? +save_fig = False +save_data = False +fig_ext = '.png' + +# Number of revolutions (n) to average spectra +n = 1 + +######################################################################################################################################### +## Paths to files +if platform.system() == 'Windows': + FAST_directory = os.path.dirname( os.path.dirname( os.path.dirname( os.path.dirname( os.path.realpath(__file__) ) ) ) ) + os.sep + 'reg_tests' + os.sep + 'r-tests' + os.sep + 'glue-codes' + os.sep + 'openfast' + os.sep + 'IEA_LB_RWT-AeroAcoustics' +else: + FAST_directory = os.path.dirname( os.path.dirname( os.path.dirname( os.path.dirname( os.path.realpath(__file__) ) ) ) ) + os.sep + 'openfast' + os.sep + 'reg_tests' + os.sep + 'r-tests' + os.sep + 'glue-codes' + os.sep + 'openfast' + os.sep + 'IEA_LB_RWT-AeroAcoustics' +AAfilename = FAST_directory + os.sep + 'IEA_LB_RWT-AeroAcoustics_2.out' +OFfilename = FAST_directory + os.sep + 'IEA_LB_RWT-AeroAcoustics.out' +output_dir = os.path.dirname( os.path.realpath(__file__) ) +outputfilename = output_dir + os.sep + "data_output2" + +######################################################################################################################################### +## Read in data, manipulate it, and plot it + +# Read in file data +AA_2 = FASTOutputFile(AAfilename).toDataFrame() +OF = FASTOutputFile(OFfilename).toDataFrame() + +# Determine number of observers +num_obs = int((AA_2.shape[1]-1)/34) + +# Calculate sample time for n revolutions +rpm = OF[["RotSpeed_[rpm]"]].mean()[0] +time_revs = n*60/rpm +tot_time = AA_2["Time_[s]"].max() +if time_revs < tot_time: + sample_time = tot_time - time_revs +else: + print("Error: Time for number of revolutions exceeds simulation time. Reduce n.") + raise SystemExit('') + +# Slice AA dataframe for t > sample_time +AA_2 = AA_2[AA_2["Time_[s]"] > sample_time] +AA_2=AA_2.drop("Time_[s]",axis=1) + +# Average SPL for each observer +AA_2 = AA_2.mean() + +# Manipulate PD dataframes +cols = ['Observer','Frequency (Hz)','SPL (dB)'] +aa_2 = pd.DataFrame(columns=cols) +for i in AA_2.index: + nums = re.findall(r"[-+]?\d*\.\d+|\d+",i) + aa_2.loc[len(aa_2)] = [nums[0],nums[1],AA_2[i]] +AA_2 = aa_2 +AA_2["Frequency (Hz)"]=AA_2["Frequency (Hz)"].apply(pd.to_numeric) +AA_2["SPL (dB)"]=AA_2["SPL (dB)"].apply(pd.to_numeric) + +# Plot spectra +fs = 10 +fig,ax=plt.subplots() +plt.xscale('log') +ax.set_xlabel('Frequency (Hz)', fontsize=fs+2, fontweight='bold') +ax.set_ylabel('SPL (dB)', fontsize=fs+2, fontweight='bold') +for i in range(num_obs): + plt.plot(AA_2["Frequency (Hz)"][i*34:i*34 + 34], AA_2["SPL (dB)"][i*34:i*34 + 34], label = 'Observer ' + str(i)) +plt.grid(color=[0.8,0.8,0.8], linestyle='--') +ax.legend() +if save_fig == True: + fig_name = 'spectra' + fig_ext + fig.savefig(output_dir + os.sep + fig_name) +plt.show() + +# Export to csv +if save_data == True: + AA_2.to_csv(r'{}-data.csv'.format(outputfilename)) + + + + diff --git a/openfast_toolbox/aeroacoustics/examples/_run_OF.py b/openfast_toolbox/aeroacoustics/examples/_run_OF.py new file mode 100644 index 0000000..f3c47be --- /dev/null +++ b/openfast_toolbox/aeroacoustics/examples/_run_OF.py @@ -0,0 +1,18 @@ +import os +import platform +import subprocess + +if platform.system() == 'Windows': + FAST_exe = os.path.dirname( os.path.dirname( os.path.dirname( os.path.dirname( os.path.realpath(__file__) ) ) ) ) + os.sep + 'openfast' + os.sep + 'build' + os.sep + 'glue-codes' + os.sep + 'openfast_x64.exe' + FAST_directory = os.path.dirname( os.path.dirname( os.path.dirname( os.path.dirname( os.path.realpath(__file__) ) ) ) ) + os.sep + 'openfast' + os.sep + 'reg_tests' + os.sep + 'r-tests' + os.sep + 'glue-codes' + os.sep + 'IEA_LB_RWT-AeroAcoustics' +else: + FAST_exe = os.path.dirname( os.path.dirname( os.path.dirname( os.path.dirname( os.path.realpath(__file__) ) ) ) ) + os.sep + 'openfast' + os.sep + 'build' + os.sep + 'glue-codes' + os.sep + 'openfast' + os.sep + 'openfast' + FAST_directory = os.path.dirname( os.path.dirname( os.path.dirname( os.path.dirname( os.path.realpath(__file__) ) ) ) ) + os.sep + 'openfast' + os.sep + 'reg_tests' + os.sep + 'r-tests' + os.sep + 'glue-codes' + os.sep + 'openfast' + os.sep + 'IEA_LB_RWT-AeroAcoustics' + + +FAST_InputFile = FAST_directory + os.sep + 'IEA_LB_RWT-AeroAcoustics.fst' +exec_str = [] +exec_str.append(FAST_exe) +exec_str.append(FAST_InputFile) + +subprocess.call(exec_str) \ No newline at end of file diff --git a/openfast_toolbox/aeroacoustics/examples/_write_BLfiles.py b/openfast_toolbox/aeroacoustics/examples/_write_BLfiles.py new file mode 100644 index 0000000..ba371bc --- /dev/null +++ b/openfast_toolbox/aeroacoustics/examples/_write_BLfiles.py @@ -0,0 +1,144 @@ +import numpy as np +import os, shutil, platform + +# Path to XFoil +path2xfoil = 'Xfoil/bin/xfoil' + +## inputs +if platform.system() == 'Windows': + FAST_directory = os.path.dirname( os.path.dirname( os.path.dirname( os.path.dirname( os.path.realpath(__file__) ) ) ) ) + os.sep + 'reg_tests' + os.sep + 'r-tests' + os.sep + 'glue-codes' + os.sep + 'openfast' + os.sep + 'IEA_LB_RWT-AeroAcoustics' +else: + FAST_directory = os.path.dirname( os.path.dirname( os.path.dirname( os.path.dirname( os.path.realpath(__file__) ) ) ) ) + os.sep + 'openfast' + os.sep + 'reg_tests' + os.sep + 'r-tests' + os.sep + 'glue-codes' + os.sep + 'openfast' + os.sep + 'IEA_LB_RWT-AeroAcoustics' +folder_inputs = FAST_directory + os.sep + 'Airfoils' +folder_outputs = folder_inputs +n_stations = 30 +aoa = np.linspace(-5., 25., 30) # List of angles of attack +Re = np.array([0.5e+06, 1.e+06, 5.e+06, 10.e+06]) # List of Reynolds numbers +TEoffset = 1 # Number of nodes away from the TE where the BL properties are extracted, TEoffset = 0 is prone to convergence issues +trip = 0 # Flag to set free (0) or force (1) +TEAngle = 10.*np.ones(n_stations) # Distribution of trailing edge angles for BPM trailing edge bluntness noise model +TEThick = 0.005*np.ones(n_stations) # Distribution of trailing edge angles for BPM trailing edge bluntness noise model + +## code + +inputs_list = [] +outputs_list = [] +for i in range(n_stations): + if i < 10: + inputs_list.append(folder_inputs + os.sep + 'AF0' + str(i) + '_Coords.txt') # List of files containing the airfoil coordinates + else: + inputs_list.append(folder_inputs + os.sep + 'AF' + str(i) + '_Coords.txt') # List of files containing the airfoil coordinates + outputs_list.append(folder_outputs + os.sep + 'AF' + str(i) + '_BL.txt') # List of files containing the boundary layer characteristics + + +for id in range(5,len(inputs_list)): + print('Compute BL properties station ' + str(id)) + filename = inputs_list[id] + coord = np.loadtxt(filename, skiprows = 9) + np.savetxt('airfoil.dat', coord) + Ue_Vinf_SS = np.zeros((len(aoa),len(Re))) + Ue_Vinf_PS = np.zeros((len(aoa),len(Re))) + Dstar_SS = np.zeros((len(aoa),len(Re))) + Dstar_PS = np.zeros((len(aoa),len(Re))) + Theta_SS = np.zeros((len(aoa),len(Re))) + Theta_PS = np.zeros((len(aoa),len(Re))) + Cf_SS = np.zeros((len(aoa),len(Re))) + Cf_PS = np.zeros((len(aoa),len(Re))) + H_SS = np.zeros((len(aoa),len(Re))) + H_PS = np.zeros((len(aoa),len(Re))) + + + for k in range(len(Re)): + fid = open('inputxfoil.vbs','w') + fid.write('\n') + fid.write('load airfoil.dat\n') + fid.write('pane\n') + fid.write('plop\n') + fid.write('g\n') + fid.write('%f\n' % 0.8) + fid.write('\n') + fid.write('oper\n') + fid.write('visc %1.3e\n' % (Re[k])) + fid.write('iter\n') + fid.write('%u\n' % 200) + + if trip == 1: + fid.write('vpar\n') + fid.write('xtr 0.05 0.1\n') + fid.write('\n') + + for j in range(len(aoa)): + fid.write('alfa %3.2f\n' % aoa[j]) + fid.write('dump airfoil.bl%u\n' % j) + fid.write('\n') + fid.write('quit\n') + fid.close() + + os.system(path2xfoil + ' < inputxfoil.vbs') + + os.remove('inputxfoil.vbs') + + + for j in range(len(aoa)): + s_counter=0 + i_TE_PS = [] + bl_data = np.array([]) + text_file = 'airfoil.bl' + str(j) + with open(text_file) as xfile: + xfile.readline() + for line in xfile: + data = np.array([float(x) for x in line.strip().split()]) + if s_counter==0: + bl_data = data + else: + if len(data) == 12: + bl_data = np.vstack((bl_data,data)) + elif len(data) == 8: + data = np.hstack((data, np.zeros(4))) + if i_TE_PS == []: + i_TE_PS = s_counter - 1 + else: + pass + + s_counter +=1 + + + Ue_Vinf_SS[j,k] = bl_data[0 + TEoffset , 3] + Ue_Vinf_PS[j,k] = bl_data[i_TE_PS - TEoffset , 3] + Dstar_SS[j,k] = bl_data[0 + TEoffset , 4] + Dstar_PS[j,k] = bl_data[i_TE_PS - TEoffset , 4] + Theta_SS[j,k] = bl_data[0 + TEoffset , 5] + Theta_PS[j,k] = bl_data[i_TE_PS - TEoffset , 5] + Cf_SS[j,k] = bl_data[0 + TEoffset , 6] + Cf_PS[j,k] = bl_data[i_TE_PS - TEoffset , 6] + H_SS[j,k] = bl_data[0 + TEoffset , 7] + H_PS[j,k] = bl_data[i_TE_PS - TEoffset , 7] + + os.remove('airfoil.bl' + str(j)) + + os.remove('airfoil.dat') + + # Compute Delta (nominal boundary layer thickness) from Dstar (boundary layer displacement thickness), Theta (boundary layer momentum thickness), and H (kinematic shape factor) + Delta_SS = Theta_SS*(3.15+1.72/(H_SS-1.))+Dstar_SS + Delta_PS = Theta_PS*(3.15+1.72/(H_PS-1.))+Dstar_PS + + fid=open(outputs_list[id],'w') + fid.write('! Boundary layer characteristics at the trailing edge for the airfoil coordinates of %s\n' % filename) + fid.write('! Legend: aoa - angle of attack (deg), Re - Reynolds number (-, millions), PS - pressure side, SS - suction side, Ue_Vinf - edge velocity (-), Dstar - displacement thickness (-), Delta - nominal boundary layer thickness (-) Cf - friction coefficient (-)\n') + fid.write('%u \t ReListBL - Number of Reynolds numbers (it corresponds to the number of tables)\n' % len(Re)) + fid.write('%u \t aoaListBL - Number of angles of attack (it corresponds to the number of rows in each table)\n' % len(aoa)) + for k in range(len(Re)): + fid.write('%1.2f \t - Re\n' % (Re[k]*1.e-6)) + fid.write('aoa \t \t Ue_Vinf_SS \t Ue_Vinf_PS \t Dstar_SS \t \t Dstar_PS \t \t Delta_SS \t \t Delta_PS \t \t Cf_SS \t \t Cf_PS\n') + fid.write('(deg) \t \t \t (-) \t \t \t (-) \t \t \t (-) \t \t \t (-) \t \t \t (-) \t \t \t (-) \t \t \t (-) \t \t \t (-)\n') + for j in range(len(aoa)): + fid.write('%1.5f \t %1.5e \t %1.5e \t %1.5e \t %1.5e \t %1.5e \t %1.5e \t %1.5e \t %1.5e \n' % (aoa[j], Ue_Vinf_SS[j,k], Ue_Vinf_PS[j,k], Dstar_SS[j,k], Dstar_PS[j,k], Delta_SS[j,k], Delta_PS[j,k], Cf_SS[j,k], Cf_PS[j,k])) + + fid.write('\n') + fid.write('! Inputs to trailing edge bluntness noise model\n') + fid.write('%1.5f \t TEAngle - Angle of the trailing edge (deg)\n'%TEAngle[id]) + fid.write('%1.5f \t TEThick - Finite thickness of the trailing edge (deg)\n'%TEThick[id]) + + fid.close() + + \ No newline at end of file diff --git a/pyFAST/aeroacoustics/tests/__init__.py b/openfast_toolbox/aeroacoustics/tests/__init__.py similarity index 100% rename from pyFAST/aeroacoustics/tests/__init__.py rename to openfast_toolbox/aeroacoustics/tests/__init__.py diff --git a/pyFAST/aeroacoustics/tests/test_aa_tools.py b/openfast_toolbox/aeroacoustics/tests/test_aa_tools.py similarity index 90% rename from pyFAST/aeroacoustics/tests/test_aa_tools.py rename to openfast_toolbox/aeroacoustics/tests/test_aa_tools.py index 7ad6db0..a5adea4 100644 --- a/pyFAST/aeroacoustics/tests/test_aa_tools.py +++ b/openfast_toolbox/aeroacoustics/tests/test_aa_tools.py @@ -1,7 +1,7 @@ import unittest import numpy as np import os -from pyFAST.aeroacoustics import * +from openfast_toolbox.aeroacoustics import * scriptDir = os.path.dirname(__file__) # --------------------------------------------------------------------------------} diff --git a/pyFAST/aeroacoustics/tests/test_run_Examples.py b/openfast_toolbox/aeroacoustics/tests/test_run_Examples.py similarity index 100% rename from pyFAST/aeroacoustics/tests/test_run_Examples.py rename to openfast_toolbox/aeroacoustics/tests/test_run_Examples.py diff --git a/pyFAST/airfoils/DynamicStall.py b/openfast_toolbox/airfoils/DynamicStall.py similarity index 100% rename from pyFAST/airfoils/DynamicStall.py rename to openfast_toolbox/airfoils/DynamicStall.py diff --git a/pyFAST/airfoils/Polar.py b/openfast_toolbox/airfoils/Polar.py similarity index 97% rename from pyFAST/airfoils/Polar.py rename to openfast_toolbox/airfoils/Polar.py index ce29a38..3fb7038 100644 --- a/pyFAST/airfoils/Polar.py +++ b/openfast_toolbox/airfoils/Polar.py @@ -1,1993 +1,1993 @@ -""" This module contains: - - Polar: class to represent a polar (computes steady/unsteady parameters, corrections etc.) - - blend: function to blend two polars - - thicknessinterp_from_one_set: interpolate polars at different thickeness based on one set of polars -""" - -import os -import numpy as np -from .polar_file import loadPolarFile # For IO - -class NoCrossingException(Exception): - pass -class NoStallDetectedException(Exception): - pass - - -class Polar(object): - """ - Defines section lift, drag, and pitching moment coefficients as a - function of angle of attack at a particular Reynolds number. - Different parameters may be computed and different corrections applied. - - Available routines: - - cl_interp : cl at given alpha values - - cd_interp : cd at given alpha values - - cm_interp : cm at given alpha values - - cn_interp : cn at given alpha values - - fs_interp : separation function (compared to fully separated polar) - - cl_fs_interp : cl fully separated at given alpha values - - cl_inv_interp : cl inviscid at given alpha values - - correction3D : apply 3D rotatational correction - - extrapolate : extend polar data set using Viterna's method - - unsteadyParams : computes unsteady params e.g. needed by AeroDyn15 - - unsteadyparam : same but (old) - - plot : plots the polar - - alpha0 : computes and returns alpha0, also stored in _alpha0 - - linear_region : determines the alpha and cl values in the linear region - - cl_max : cl_max - - cl_linear_slope : linear slope and the linear region - - cl_fully_separated: fully separated cl - - toAeroDyn: write AeroDyn file - """ - - def __init__(self, filename=None, alpha=None, cl=None, cd=None, cm=None, Re=None, - compute_params=False, radians=None, cl_lin_method='max', - fformat='auto', verbose=False): - """Constructor - - Parameters - ---------- - filename: string - If provided, the polar will be read from filename using fformat - Re : float - Reynolds number - alpha : ndarray (deg) - angle of attack - cl : ndarray - lift coefficient - cd : ndarray - drag coefficient - cm : ndarray - moment coefficient - fformat: string - file format to be used when filename is provided - - """ - # --- Potentially-locked properties - # Introducing locks so that some properties become readonly if prescribed by user - self._fs_lock = False - self._cl_fs_lock = False - self._cl_inv_lock = False - # TODO lock _alpha0 and cl_alpha - self.fs = None # steady separation function - self.cl_fs = None # cl_fully separated - self.cl_inv = None # cl inviscid/linear/potential flow - self._alpha0 = None - self._linear_slope = None - - # Read polar according to fileformat, if filename provided - if filename is not None: - df, Re = loadPolarFile(filename, fformat=fformat, to_radians=radians, verbose=verbose) - alpha = df['Alpha'].values - cl = df['Cl'].values - cd = df['Cd'].values - cm = df['Cm'].values - if 'fs' in df.keys(): - print('[INFO] Using separating function from input file.') - self.fs = df['fs'].values - self._fs_lock = True - if 'Cl_fs' in df.keys(): - print('[INFO] Using Cl fully separated from input file.') - self.cl_fs =df['Cl_fs'].values - self._cl_fs_lock = True - if 'Cl_inv' in df.keys(): - print('[INFO] Using Cl inviscid from input file.') - self.cl_inv = df['Cl_inv'].values - self._cl_inv_lock = True - # TODO we need a trigger if cl_inv provided, we should get alpha0 and slope from it - nLocks = sum([self._fs_lock, self._cl_fs_lock, self._cl_inv_lock]) - if nLocks>0 and nLocks<3: - raise Exception("For now, input files are assumed to have all or none of the columns: (fs, cl_fs, and cl_inv). Otherwise, we\'ll have to ensure consitency, and so far we dont...") - - self.Re = Re - self.alpha = np.array(alpha) - if cl is None: - cl = np.zeros_like(self.alpha) - if cd is None: - cd = np.zeros_like(self.alpha) - if cm is None: - cm = np.zeros_like(self.alpha) - self.cl = np.array(cl) - self.cd = np.array(cd) - self.cm = np.array(cm) - if radians is None: - # If the max alpha is above pi, most likely we are in degrees - self._radians = np.mean(np.abs(self.alpha)) <= np.pi / 2 - else: - self._radians = radians - - # NOTE: method needs to be in harmony for linear_slope and the one used in cl_fully_separated - if compute_params: - self._linear_slope, self._alpha0 = self.cl_linear_slope(method=cl_lin_method) - if not self._cl_fs_lock: - self.cl_fully_separated(method=cl_lin_method) - if not self._cl_inv_lock: - self.cl_inv = self._linear_slope * (self.alpha - self._alpha0) - - def __repr__(self): - s='<{} object>:\n'.format(type(self).__name__) - sunit = 'deg' - if self._radians: - sunit = 'rad' - s+='Parameters:\n' - s+=' - alpha, cl, cd, cm : arrays of size {}\n'.format(len(self.alpha)) - s+=' - Re : {} \n'.format(self.Re) - s+=' - _radians: {} (True if alpha in radians)\n'.format(self._radians) - s+=' - _alpha0: {} [{}]\n'.format(self._alpha0, sunit) - s+=' - _linear_slope: {} [1/{}]\n'.format(self._linear_slope, sunit) - s+='Derived parameters:\n' - s+=' * cl_inv : array of size {} \n'.format(len(self.alpha)) - s+=' * cl_fs : array of size {} \n'.format(len(self.alpha)) - s+=' * fs : array of size {} \n'.format(len(self.alpha)) - s+=' * cl_lin (UNSURE) : array of size {} \n'.format(len(self.alpha)) - s+='Functional parameters:\n' - s+=' * alpha0 : {} [{}]\n'.format(self.alpha0(),sunit) - s+=' * cl_linear_slope : {} [1/{}]\n'.format(self.cl_linear_slope()[0],sunit) - s+=' * cl_max : {} \n'.format(self.cl_max()) - s+=' * unsteadyParams : {} \n'.format(self.unsteadyParams()) - s+='Useful functions: cl_interp, cd_interp, cm_interp, fs_interp \n' - s+=' cl_fs_interp, cl_inv_interp, \n' - s+=' interpolant \n' - s+=' plot, extrapolate\n' - return s - - - # --- Potential read only properties - @property - def cl_inv(self): - if self._cl_inv is None: - self.cl_fully_separated() # computes cl_fs, cl_inv and fs - return self._cl_inv - @cl_inv.setter - def cl_inv(self, cl_inv): - if self._cl_inv_lock: - raise Exception('Cl_inv was set by user, cannot modify it') - else: - self._cl_inv = cl_inv - - @property - def cl_fs(self): - if self._cl_fs is None: - self.cl_fully_separated() # computes cl_fs, cl_inv and fs - return self._cl_fs - @cl_fs.setter - def cl_fs(self, cl_fs): - if self._cl_fs_lock: - raise Exception('cl_fs was set by user, cannot modify it') - else: - self._cl_fs = cl_fs - - @property - def fs(self): - if self._fs is None: - self.cl_fully_separated() # computes fs, cl_inv and fs - return self._fs - @fs.setter - def fs(self, fs): - if self._fs_lock: - raise Exception('fs was set by user, cannot modify it') - else: - self._fs = fs - - - # --- Interpolants - def cl_interp(self, alpha): - return np.interp(alpha, self.alpha, self.cl) - - def cd_interp(self, alpha): - return np.interp(alpha, self.alpha, self.cd) - - def cm_interp(self, alpha): - return np.interp(alpha, self.alpha, self.cm) - - def cn_interp(self, alpha): - return np.interp(alpha, self.alpha, self.cn) - - def fs_interp(self, alpha): - return np.interp(alpha, self.alpha, self.fs) - - def cl_fs_interp(self, alpha): - return np.interp(alpha, self.alpha, self.cl_fs) - - def cl_inv_interp(self, alpha): - return np.interp(alpha, self.alpha, self.cl_inv) - - def interpolant(self, variables=['cl', 'cd', 'cm'], radians=None): - """ - Create an interpolant `f` for a set of requested variables with alpha as input variable: - var_array = f(alpha) - - This is convenient to quickly interpolate multiple polar variables at once. - The interpolant returns an array corresponding to the interpolated values of the - requested `variables`, in the same order as they are requested. - - When alpha is a scalar, f(alpha) is of length nVar = len(variables) - When alpha is an array of length n, f(alpha) is of shape (nVar x n) - - INPUTS: - - variables: list of variables that will be returned by the interpolant - Allowed values: ['alpha', 'cl', 'cd', 'cm', 'fs', 'cl_inv', 'cl_fs'] - - radians: enforce whether `alpha` is in radians or degrees - - OUTPUTS: - - f: interpolant - - """ - from scipy.interpolate import interp1d - - MAP = {'alpha':self.alpha, 'cl':self.cl, 'cd':self.cd, 'cm':self.cm, - 'cl_inv':self.cl_inv, 'cl_fs':self.cl_fs, 'fs':self.fs} - - if radians is None: - radians = self._radians - - # Create a Matrix with columns requested by user - #polCols = polar.columns.values[1:] - M = self.alpha # we start by alpha for convenience - for v in variables: - v = v.lower().strip() - if v not in MAP.keys(): - raise Exception('Polar: cannot create an interpolant for variable `{}`, allowed variables: {}'.format(v, MAP.keys())) - M = np.column_stack( (M, MAP[v]) ) - # We remove alpha - M = M[:,1:] - # Determine the "x" value for the interpolant (alpha in rad or deg) - if radians == self._radians: - alpha = self.alpha # the user requested the same as what we have - else: - if radians: - alpha = np.radians(self.alpha) - else: - alpha = np.degrees(self.alpha) - # Create the interpolant for requested variables with alpha as "x" axis - f = interp1d(alpha, M.T) - return f - - @property - def cn(self): - """ returns : Cl cos(alpha) + Cd sin(alpha) - NOT: Cl cos(alpha) + (Cd-Cd0) sin(alpha) - """ - if self._radians: - return self.cl * np.cos(self.alpha) + self.cd * np.sin(self.alpha) - else: - return self.cl * np.cos(self.alpha * np.pi / 180) + self.cd * np.sin(self.alpha * np.pi / 180) - - @property - def cl_lin(self): # TODO consider removing - print('[WARN] Polar: cl_lin is a bit of a weird property. Not sure if it will be kept') - if self.cl_inv is None: - self.cl_fully_separated() # computes cl_fs, cl_inv and fs - return self.cl_inv - #if (self._linear_slope is None) and (self._alpha0 is None): - # self._linear_slope,self._alpha0=self.cl_linear_slope() - #return self._linear_slope*(self.alpha-self._alpha0) - - @classmethod - def fromfile(cls, filename, fformat='auto', compute_params=False, to_radians=False): - """Constructor based on a filename - # NOTE: this is legacy - """ - print('[WARN] Polar: "fromfile" is depreciated and will be removed in a future release') - return cls(filename, fformat=fformat, compute_params=compute_params, radians=to_radians) - - def correction3D( - self, - r_over_R, - chord_over_r, - tsr, - lift_method="DuSelig", - drag_method="None", - blending_method="linear_25_45", - max_cl_corr=0.25, - alpha_max_corr=None, - alpha_linear_min=None, - alpha_linear_max=None, - ): - """Applies 3-D corrections for rotating sections from the 2-D data. - - Parameters - ---------- - r_over_R : float - local radial position / rotor radius - chord_over_r : float - local chord length / local radial location - tsr : float - tip-speed ratio - lift_method : string, optional - flag switching between Du-Selig and Snel corrections - drag_method : string, optional - flag switching between Eggers correction and None - blending_method: string: - blending method used to blend from 3D to 2D polar. default 'linear_25_45' - max_cl_corr: float, optional - maximum correction allowed, default is 0.25. - alpha_max_corr : float, optional (deg) - maximum angle of attack to apply full correction - alpha_linear_min : float, optional (deg) - angle of attack where linear portion of lift curve slope begins - alpha_linear_max : float, optional (deg) - angle of attack where linear portion of lift curve slope ends - - Returns - ------- - polar : Polar - A new Polar object corrected for 3-D effects - - Notes - ----- - The Du-Selig method :cite:`Du1998A-3-D-stall-del` is used to correct lift, and - the Eggers method :cite:`Eggers-Jr2003An-assessment-o` is used to correct drag. - - """ - - if alpha_max_corr == None and alpha_linear_min == None and alpha_linear_max == None: - alpha_linear_region, _, cl_slope, alpha0 = self.linear_region() - alpha_linear_min = alpha_linear_region[0] - alpha_linear_max = alpha_linear_region[-1] - _, alpha_max_corr = self.cl_max() - find_linear_region = False - elif alpha_max_corr * alpha_linear_min * alpha_linear_max == None: - raise Exception( - "Define all or none of the keyword arguments alpha_max_corr, alpha_linear_min, and alpha_linear_max" - ) - else: - find_linear_region = True - - # rename and convert units for convenience - if self._radians: - alpha = alpha - else: - alpha = np.radians(self.alpha) - cl_2d = self.cl - cd_2d = self.cd - alpha_max_corr = np.radians(alpha_max_corr) - alpha_linear_min = np.radians(alpha_linear_min) - alpha_linear_max = np.radians(alpha_linear_max) - - # parameters in Du-Selig model - a = 1 - b = 1 - d = 1 - lam = tsr / (1 + tsr ** 2) ** 0.5 # modified tip speed ratio - if np.abs(r_over_R)>1e-4: - expon = d / lam / r_over_R - else: - expon = d / lam / 1e-4 - - # find linear region with numpy polyfit - if find_linear_region: - idx = np.logical_and(alpha >= alpha_linear_min, alpha <= alpha_linear_max) - p = np.polyfit(alpha[idx], cl_2d[idx], 1) - cl_slope = p[0] - alpha0 = -p[1] / cl_slope - else: - cl_slope = np.degrees(cl_slope) - alpha0 = np.radians(alpha0) - - if lift_method == "DuSelig": - # Du-Selig correction factor - if np.abs(cl_slope)>1e-4: - fcl = ( - 1.0 - / cl_slope - * (1.6 * chord_over_r / 0.1267 * (a - chord_over_r ** expon) / (b + chord_over_r ** expon) - 1) - ) - # Force fcl to stay non-negative - if fcl < 0.: - fcl = 0. - else: - fcl=0.0 - elif lift_method == "Snel": - # Snel correction - fcl = 3.0 * chord_over_r ** 2.0 - else: - raise Exception("The keyword argument lift_method (3d correction for lift) can only be DuSelig or Snel.") - - # 3D correction for lift - cl_linear = cl_slope * (alpha - alpha0) - cl_corr = fcl * (cl_linear - cl_2d) - # Bound correction +/- max_cl_corr - cl_corr = np.clip(cl_corr, -max_cl_corr, max_cl_corr) - # Blending - if blending_method == "linear_25_45": - # We adjust fully between +/- 25 deg, linearly to +/- 45 - adj_alpha = np.radians([-180, -45, -25, 25, 45, 180]) - adj_value = np.array([0, 0, 1, 1, 0, 0]) - adj = np.interp(alpha, adj_alpha, adj_value) - elif blending_method == "heaviside": - # Apply (arbitrary!) smoothing function to smoothen the 3D corrections and zero them out away from alpha_max_corr - delta_corr = 10 - y1 = 1.0 - smooth_heaviside(alpha, k=1, rng=(alpha_max_corr, alpha_max_corr + np.deg2rad(delta_corr))) - y2 = smooth_heaviside(alpha, k=1, rng=(0.0, np.deg2rad(delta_corr))) - adj = y1 * y2 - else: - raise NotImplementedError("blending :", blending_method) - cl_3d = cl_2d + cl_corr * adj - - # Eggers 2003 correction for drag - if drag_method == "Eggers": - delta_cd = cl_corr * (np.sin(alpha) - 0.12 * np.cos(alpha)) / (np.cos(alpha) + 0.12 * np.sin(alpha)) * adj - elif drag_method == "None": - delta_cd = 0.0 - else: - raise Exception("The keyword argument darg_method (3d correction for drag) can only be Eggers or None.") - - cd_3d = cd_2d + delta_cd - - return type(self)(Re=self.Re, alpha=np.degrees(alpha), cl=cl_3d, cd=cd_3d, cm=self.cm, radians=False) - - def extrapolate(self, cdmax, AR=None, cdmin=0.001, nalpha=15): - """Extrapolates force coefficients up to +/- 180 degrees using Viterna's method - :cite:`Viterna1982Theoretical-and`. - - Parameters - ---------- - cdmax : float - maximum drag coefficient - AR : float, optional - aspect ratio = (rotor radius / chord_75% radius) - if provided, cdmax is computed from AR - cdmin: float, optional - minimum drag coefficient. used to prevent negative values that can sometimes occur - with this extrapolation method - nalpha: int, optional - number of points to add in each segment of Viterna method - - Returns - ------- - polar : Polar - a new Polar object - - Notes - ----- - If the current polar already supplies data beyond 90 degrees then - this method cannot be used in its current form and will just return itself. - - If AR is provided, then the maximum drag coefficient is estimated as - - >>> cdmax = 1.11 + 0.018*AR - - - """ - - if cdmin < 0: - raise Exception("cdmin cannot be < 0") - - # lift coefficient adjustment to account for assymetry - cl_adj = 0.7 - - # estimate CD max - if AR is not None: - cdmax = 1.11 + 0.018 * AR - self.cdmax = max(max(self.cd), cdmax) - - # extract matching info from ends - alpha_high = np.radians(self.alpha[-1]) - cl_high = self.cl[-1] - cd_high = self.cd[-1] - cm_high = self.cm[-1] - - alpha_low = np.radians(self.alpha[0]) - cl_low = self.cl[0] - cd_low = self.cd[0] - - if alpha_high > np.pi / 2: - raise Exception("alpha[-1] > pi/2") - return self - if alpha_low < -np.pi / 2: - raise Exception("alpha[0] < -pi/2") - return self - - # parameters used in model - sa = np.sin(alpha_high) - ca = np.cos(alpha_high) - self.A = (cl_high - self.cdmax * sa * ca) * sa / ca ** 2 - self.B = (cd_high - self.cdmax * sa * sa) / ca - - # alpha_high <-> 90 - alpha1 = np.linspace(alpha_high, np.pi / 2, nalpha) - alpha1 = alpha1[1:] # remove first element so as not to duplicate when concatenating - cl1, cd1 = self.__Viterna(alpha1, 1.0) - - # 90 <-> 180-alpha_high - alpha2 = np.linspace(np.pi / 2, np.pi - alpha_high, nalpha) - alpha2 = alpha2[1:] - cl2, cd2 = self.__Viterna(np.pi - alpha2, -cl_adj) - - # 180-alpha_high <-> 180 - alpha3 = np.linspace(np.pi - alpha_high, np.pi, nalpha) - alpha3 = alpha3[1:] - cl3, cd3 = self.__Viterna(np.pi - alpha3, 1.0) - cl3 = (alpha3 - np.pi) / alpha_high * cl_high * cl_adj # override with linear variation - - if alpha_low <= -alpha_high: - alpha4 = [] - cl4 = [] - cd4 = [] - alpha5max = alpha_low - else: - # -alpha_high <-> alpha_low - # Note: this is done slightly differently than AirfoilPrep for better continuity - alpha4 = np.linspace(-alpha_high, alpha_low, nalpha) - alpha4 = alpha4[1:-2] # also remove last element for concatenation for this case - cl4 = -cl_high * cl_adj + (alpha4 + alpha_high) / (alpha_low + alpha_high) * (cl_low + cl_high * cl_adj) - cd4 = cd_low + (alpha4 - alpha_low) / (-alpha_high - alpha_low) * (cd_high - cd_low) - alpha5max = -alpha_high - - # -90 <-> -alpha_high - alpha5 = np.linspace(-np.pi / 2, alpha5max, nalpha) - alpha5 = alpha5[1:] - if alpha_low == -alpha_high: - alpha5 = alpha5[:-1] - cl5, cd5 = self.__Viterna(-alpha5, -cl_adj) - - # -180+alpha_high <-> -90 - alpha6 = np.linspace(-np.pi + alpha_high, -np.pi / 2, nalpha) - alpha6 = alpha6[1:] - cl6, cd6 = self.__Viterna(alpha6 + np.pi, cl_adj) - - # -180 <-> -180 + alpha_high - alpha7 = np.linspace(-np.pi, -np.pi + alpha_high, nalpha) - cl7, cd7 = self.__Viterna(alpha7 + np.pi, 1.0) - cl7 = (alpha7 + np.pi) / alpha_high * cl_high * cl_adj # linear variation - - alpha = np.concatenate((alpha7, alpha6, alpha5, alpha4, np.radians(self.alpha), alpha1, alpha2, alpha3)) - cl = np.concatenate((cl7, cl6, cl5, cl4, self.cl, cl1, cl2, cl3)) - cd = np.concatenate((cd7, cd6, cd5, cd4, self.cd, cd1, cd2, cd3)) - - cd = np.maximum(cd, cdmin) # don't allow negative drag coefficients - - # Setup alpha and cm to be used in extrapolation - cm1_alpha = np.floor(self.alpha[0] / 10.0) * 10.0 - cm2_alpha = np.ceil(self.alpha[-1] / 10.0) * 10.0 - if cm2_alpha == self.alpha[-1]: - self.alpha = self.alpha[:-1] - self.cm = self.cm[:-1] - alpha_num = abs(int((-180.0 - cm1_alpha) / 10.0 - 1)) - alpha_cm1 = np.linspace(-180.0, cm1_alpha, alpha_num) - alpha_cm2 = np.linspace(cm2_alpha, 180.0, int((180.0 - cm2_alpha) / 10.0 + 1)) - alpha_cm = np.concatenate( - (alpha_cm1, self.alpha, alpha_cm2) - ) # Specific alpha values are needed for cm function to work - cm1 = np.zeros(len(alpha_cm1)) - cm2 = np.zeros(len(alpha_cm2)) - cm_ext = np.concatenate((cm1, self.cm, cm2)) - if np.count_nonzero(self.cm) > 0: - cmCoef = self.__CMCoeff(cl_high, cd_high, cm_high) # get cm coefficient - cl_cm = np.interp(alpha_cm, np.degrees(alpha), cl) # get cl for applicable alphas - cd_cm = np.interp(alpha_cm, np.degrees(alpha), cd) # get cd for applicable alphas - alpha_low_deg = self.alpha[0] - alpha_high_deg = self.alpha[-1] - for i in range(len(alpha_cm)): - cm_new = self.__getCM(i, cmCoef, alpha_cm, cl_cm, cd_cm, alpha_low_deg, alpha_high_deg) - if cm_new is None: - pass # For when it reaches the range of cm's that the user provides - else: - cm_ext[i] = cm_new - cm = np.interp(np.degrees(alpha), alpha_cm, cm_ext) - return type(self)(self.Re, np.degrees(alpha), cl, cd, cm) - - - - - def __Viterna(self, alpha, cl_adj): - """private method to perform Viterna extrapolation""" - - alpha = np.maximum(alpha, 0.0001) # prevent divide by zero - - cl = self.cdmax / 2 * np.sin(2 * alpha) + self.A * np.cos(alpha) ** 2 / np.sin(alpha) - cl = cl * cl_adj - - cd = self.cdmax * np.sin(alpha) ** 2 + self.B * np.cos(alpha) - - return cl, cd - - def __CMCoeff(self, cl_high, cd_high, cm_high): - """private method to obtain CM0 and CMCoeff""" - - found_zero_lift = False - - for i in range(len(self.cm) - 1): - if abs(self.alpha[i]) < 20.0 and self.cl[i] <= 0 and self.cl[i + 1] >= 0: - p = -self.cl[i] / (self.cl[i + 1] - self.cl[i]) - cm0 = self.cm[i] + p * (self.cm[i + 1] - self.cm[i]) - found_zero_lift = True - break - - if not found_zero_lift: - p = -self.cl[0] / (self.cl[1] - self.cl[0]) - cm0 = self.cm[0] + p * (self.cm[1] - self.cm[0]) - self.cm0 = cm0 - alpha_high = np.radians(self.alpha[-1]) - XM = (-cm_high + cm0) / (cl_high * np.cos(alpha_high) + cd_high * np.sin(alpha_high)) - cmCoef = (XM - 0.25) / np.tan((alpha_high - np.pi / 2)) - return cmCoef - - def __getCM(self, i, cmCoef, alpha, cl_ext, cd_ext, alpha_low_deg, alpha_high_deg): - """private method to extrapolate Cm""" - - cm_new = 0 - if alpha[i] >= alpha_low_deg and alpha[i] <= alpha_high_deg: - return - if alpha[i] > -165 and alpha[i] < 165: - if abs(alpha[i]) < 0.01: - cm_new = self.cm0 - else: - if alpha[i] > 0: - x = cmCoef * np.tan(np.radians(alpha[i]) - np.pi / 2) + 0.25 - cm_new = self.cm0 - x * ( - cl_ext[i] * np.cos(np.radians(alpha[i])) + cd_ext[i] * np.sin(np.radians(alpha[i])) - ) - else: - x = cmCoef * np.tan(-np.radians(alpha[i]) - np.pi / 2) + 0.25 - cm_new = -( - self.cm0 - - x * (-cl_ext[i] * np.cos(-np.radians(alpha[i])) + cd_ext[i] * np.sin(-np.radians(alpha[i]))) - ) - else: - if alpha[i] == 165: - cm_new = -0.4 - elif alpha[i] == 170: - cm_new = -0.5 - elif alpha[i] == 175: - cm_new = -0.25 - elif alpha[i] == 180: - cm_new = 0 - elif alpha[i] == -165: - cm_new = 0.35 - elif alpha[i] == -170: - cm_new = 0.4 - elif alpha[i] == -175: - cm_new = 0.2 - elif alpha[i] == -180: - cm_new = 0 - else: - print("Angle encountered for which there is no CM table value " "(near +/-180 deg). Program will stop.") - return cm_new - - def unsteadyParams(self, window_offset=None, nMin=720): - """compute unsteady aero parameters used in AeroDyn input file - - TODO Questions to solve: - - Is alpha 0 defined at zero lift or zero Cn? - - Are Cn1 and Cn2 the stall points of Cn or the regions where Cn deviates from the linear region? - - Is Cd0 Cdmin? - - Should Cd0 be used in cn? - - Should the TSE points be used? - - If so, should we use the linear points or the points on the cn-curve - - Should we prescribe alpha0cn when determining the slope? - NOTE: - alpha0Cl and alpha0Cn are usually within 0.005 deg of each other, less thatn 0.3% difference, with alpha0Cn > alpha0Cl. The difference increase thought towards the root of the blade - - Using the f=0.7 points doesnot change much for the lower point - but it has quite an impact on the upper point - % - - Parameters - ---------- - window_dalpha0: the linear region will be looked for in the region alpha+window_offset - - Returns - ------- - alpha0 : lift or 0 cn (TODO TODO) angle of attack (deg) - alpha1 : angle of attack at f=0.7 (approximately the stall angle) for AOA>alpha0 (deg) - alpha2 : angle of attack at f=0.7 (approximately the stall angle) for AOA= alpha_linear_min, - alpha <= alpha_linear_max) - - # checks for inppropriate data (like cylinders) - if len(idx) < 10 or len(np.unique(cl)) < 10: - return 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,0.0 - - # linear fit - p = np.polyfit(alpha[idx], cn[idx], 1) - m = p[0] - alpha0 = -p[1]/m - - # find cn at "stall onset" locations, when cn deviates from the linear region - alphaUpper = np.radians(np.arange(40.0)) - alphaLower = np.radians(np.arange(5.0, -40.0, -1)) - cnUpper = np.interp(alphaUpper, alpha, cn) - cnLower = np.interp(alphaLower, alpha, cn) - cnLinearUpper = m*(alphaUpper - alpha0) - cnLinearLower = m*(alphaLower - alpha0) - deviation = 0.05 # threshold for cl in detecting stall - - alphaU = np.interp(deviation, cnLinearUpper-cnUpper, alphaUpper) - alphaL = np.interp(deviation, cnLower-cnLinearLower, alphaLower) - - # compute cn at stall according to linear fit - cnStallUpper = m*(alphaU-alpha0) - cnStallLower = m*(alphaL-alpha0) - - # find min cd - minIdx = cd.argmin() - - # return: control setting, stall angle, alpha for 0 cn, cn slope, - # cn at stall+, cn at stall-, alpha for min CD, min(CD) - return (0.0, np.degrees(alphaU), np.degrees(alpha0), m, - cnStallUpper, cnStallLower, alpha[minIdx], cd[minIdx]) - - def plot(self): - """plot cl/cd/cm polar - - Returns - ------- - figs : list of figure handles - - """ - import matplotlib.pyplot as plt - - p = self - - figs = [] - - # plot cl - fig = plt.figure() - figs.append(fig) - ax = fig.add_subplot(111) - plt.plot(p.alpha, p.cl, label="Re = " + str(p.Re / 1e6) + " million") - ax.set_xlabel("angle of attack (deg)") - ax.set_ylabel("lift coefficient") - ax.legend(loc="best") - - # plot cd - fig = plt.figure() - figs.append(fig) - ax = fig.add_subplot(111) - ax.plot(p.alpha, p.cd, label="Re = " + str(p.Re / 1e6) + " million") - ax.set_xlabel("angle of attack (deg)") - ax.set_ylabel("drag coefficient") - ax.legend(loc="best") - - # plot cm - fig = plt.figure() - figs.append(fig) - ax = fig.add_subplot(111) - ax.plot(p.alpha, p.cm, label="Re = " + str(p.Re / 1e6) + " million") - ax.set_xlabel("angle of attack (deg)") - ax.set_ylabel("moment coefficient") - ax.legend(loc="best") - - return figs - - def alpha0(self, window=None): - """ Finds alpha0, angle of zero lift """ - if window is None: - if self._radians: - window = [np.radians(-30), np.radians(30)] - else: - window = [-30, 30] - window = _alpha_window_in_bounds(self.alpha, window) - # print(window) - # print(self.alpha) - # print(self._radians) - # print(self.cl) - # print(window) - - return _find_alpha0(self.alpha, self.cl, window) - - def linear_region(self, delta_alpha0=4, method_linear_fit="max"): - cl_slope, alpha0 = self.cl_linear_slope() - alpha_linear_region = np.asarray(_find_TSE_region(self.alpha, self.cl, cl_slope, alpha0, deviation=0.05)) - cl_linear_region = (alpha_linear_region - alpha0) * cl_slope - - return alpha_linear_region, cl_linear_region, cl_slope, alpha0 - - def cl_max(self, window=None): - """ Finds cl_max , returns (Cl_max,alpha_max) """ - if window is None: - if self._radians: - window = [np.radians(-40), np.radians(40)] - else: - window = [-40, 40] - - # Constant case or only one value - if np.all(self.cl == self.cl[0]) or len(self.cl) == 1: - return self.cl, self.alpha - - # Ensuring window is within our alpha values - window = _alpha_window_in_bounds(self.alpha, window) - - # Finding max within window - iwindow = np.where((self.alpha >= window[0]) & (self.alpha <= window[1])) - alpha = self.alpha[iwindow] - cl = self.cl[iwindow] - i_max = np.argmax(cl) - if i_max == len(iwindow): - raise Exception( - "Max cl is at the window boundary ([{};{}]), increase window (TODO automatically)".format( - window[0], window[1] - ) - ) - pass - cl_max = cl[i_max] - alpha_cl_max = alpha[i_max] - # alpha_zc,i_zc = _zero_crossings(x=alpha,y=cl,direction='up') - # if len(alpha_zc)>1: - # raise Exception('Cannot find alpha0, {} zero crossings of Cl in the range of alpha values: [{} {}] '.format(len(alpha_zc),window[0],window[1])) - # elif len(alpha_zc)==0: - # raise Exception('Cannot find alpha0, no zero crossing of Cl in the range of alpha values: [{} {}] '.format(window[0],window[1])) - # - # alpha0=alpha_zc[0] - return cl_max, alpha_cl_max - - - def cl_linear_slope(self, window=None, method="optim", radians=False): - """Find slope of linear region - Outputs: a 2-tuplet of: - slope (in inverse units of alpha, or in radians-1 if radians=True) - alpha_0 in the same unit as alpha, or in radians if radians=True - """ - return cl_linear_slope(self.alpha, self.cl, window=window, method=method, inputInRadians=self._radians, radians=radians) - - def cl_fully_separated(self, method='max'): - alpha0 = self.alpha0() - cla,_, = self.cl_linear_slope(method=method) - if cla == 0: - cl_fs = self.cl # when fs ==1 - fs = self.cl * 0 - else: - cl_ratio = self.cl / (cla * (self.alpha - alpha0)) - cl_ratio[np.where(cl_ratio < 0)] = 0 - if self._fs_lock: - fs = self.fs - else: - fs = (2 * np.sqrt(cl_ratio) - 1) ** 2 - fs[np.where(fs < 1e-15)] = 0 - # Initialize to linear region (in fact only at singularity, where fs=1) - cl_fs = self.cl / 2.0 # when fs ==1 - # Region where fs<1, merge - I = np.where(fs < 1) - cl_fs[I] = (self.cl[I] - cla * (self.alpha[I] - alpha0) * fs[I]) / (1.0 - fs[I]) - # Outside region, use steady data - iHig = np.ma.argmin(np.ma.MaskedArray(fs, self.alpha < alpha0)) - iLow = np.ma.argmin(np.ma.MaskedArray(fs, self.alpha > alpha0)) - cl_fs[0 : iLow + 1] = self.cl[0 : iLow + 1] - cl_fs[iHig + 1 : -1] = self.cl[iHig + 1 : -1] - - # Ensuring everything is consistent (but we cant with user provided values..) - cl_inv = cla * (self.alpha - alpha0) - if not self._fs_lock: - fs = (self.cl - cl_fs) / (cl_inv - cl_fs + 1e-10) - fs[np.where(fs < 1e-15)] = 0 - fs[np.where(fs > 1)] = 1 - # Storing - self.fs = fs - self.cl_fs = cl_fs - if not self._cl_inv_lock: - self.cl_inv = cl_inv - return cl_fs, fs - - def dynaStallOye_DiscreteStep(self, alpha_t, tau, fs_prev, dt): - # compute aerodynamical force from aerodynamic data - # interpolation from data - fs = self.fs_interp(alpha_t) - Clinv = self.cl_inv_interp(alpha_t) - Clfs = self.cl_fs_interp(alpha_t) - # dynamic stall model - fs_dyn = fs + (fs_prev - fs) * np.exp(-dt / tau) - Cl = fs_dyn * Clinv + (1 - fs_dyn) * Clfs - return Cl, fs_dyn - - def toAeroDyn(self, filenameOut=None, templateFile=None, Re=1.0, comment=None, unsteadyParams=True): - from pyFAST.input_output.fast_input_file import ADPolarFile - cleanComments=comment is not None - # Read a template file for AeroDyn polars - if templateFile is None: - MyDir=os.path.dirname(__file__) - templateFile = os.path.join(MyDir,'../../data/NREL5MW/5MW_Baseline/Airfoils/Cylinder1.dat') - cleanComments=True - if isinstance(templateFile, ADPolarFile): - ADpol = templateFile - else: - ADpol = ADPolarFile(templateFile) - - - - # --- Updating the AD polar file - ADpol['Re'] = Re # TODO UNKNOWN - - # Compute unsteady parameters - if unsteadyParams: - (alpha0,alpha1,alpha2,cnSlope,cn1,cn2,cd0,cm0)=self.unsteadyParams() - - # Setting unsteady parameters - if np.isnan(alpha0): - ADpol['alpha0'] = 0 - else: - ADpol['alpha0'] = np.around(alpha0, 4) - ADpol['alpha1'] = np.around(alpha1, 4) # TODO approximate - ADpol['alpha2'] = np.around(alpha2, 4) # TODO approximate - ADpol['C_nalpha'] = np.around(cnSlope ,4) - ADpol['Cn1'] = np.around(cn1, 4) # TODO verify - ADpol['Cn2'] = np.around(cn2, 4) - ADpol['Cd0'] = np.around(cd0, 4) - ADpol['Cm0'] = np.around(cm0, 4) - - # Setting polar - PolarTable = np.column_stack((self.alpha, self.cl, self.cd, self.cm)) - ADpol['NumAlf'] = self.cl.shape[0] - ADpol['AFCoeff'] = np.around(PolarTable, 5) - - # --- Comment - if cleanComments: - ADpol.comment='' # remove comment from template - if comment is not None: - ADpol.comment=comment - - if filenameOut is not None: - ADpol.write(filenameOut) - return ADpol - - - - -def blend(pol1, pol2, weight): - """Blend this polar with another one with the specified weighting - - Parameters - ---------- - pol1: (class Polar or array) first polar - pol2: (class Polar or array) second polar - weight: (float) blending parameter between 0 (first polar) and 1 (second polar) - - Returns - ------- - polar : (class Polar or array) a blended Polar - """ - bReturnObject = False - if hasattr(pol1, "cl"): - bReturnObject = True - alpha1 = pol1.alpha - M1 = np.zeros((len(alpha1), 4)) - M1[:, 0] = pol1.alpha - M1[:, 1] = pol1.cl - M1[:, 2] = pol1.cd - M1[:, 3] = pol1.cm - else: - alpha1 = pol1[:, 0] - M1 = pol1 - if hasattr(pol2, "cl"): - bReturnObject = True - alpha2 = pol2.alpha - M2 = np.zeros((len(alpha2), 4)) - M2[:, 0] = pol2.alpha - M2[:, 1] = pol2.cl - M2[:, 2] = pol2.cd - M2[:, 3] = pol2.cm - else: - alpha2 = pol2[:, 0] - M2 = pol2 - # Define range of alpha, merged values and truncate if one set beyond the other range - alpha = np.union1d(alpha1, alpha2) - min_alpha = max(alpha1.min(), alpha2.min()) - max_alpha = min(alpha1.max(), alpha2.max()) - alpha = alpha[np.logical_and(alpha >= min_alpha, alpha <= max_alpha)] - # alpha = np.array([a for a in alpha if a >= min_alpha and a <= max_alpha]) - - # Creating new output matrix to store polar - M = np.zeros((len(alpha), M1.shape[1])) - M[:, 0] = alpha - - # interpolate to new alpha and linearly blend - for j in np.arange(1, M.shape[1]): - v1 = np.interp(alpha, alpha1, M1[:, j]) - v2 = np.interp(alpha, alpha2, M2[:, j]) - M[:, j] = (1 - weight) * v1 + weight * v2 - if hasattr(pol1, "Re"): - Re = pol1.Re + weight * (pol2.Re - pol1.Re) - else: - Re = np.nan - - if bReturnObject: - return type(pol1)(Re=Re, alpha=M[:, 0], cl=M[:, 1], cd=M[:, 2], cm=M[:, 3]) - else: - return M - - -def thicknessinterp_from_one_set(thickness, polarList, polarThickness): - """Returns a set of interpolated polars from one set of polars at known thicknesses and a list of thickness - The nearest polar is used when the thickness is beyond the range of values of the input polars. - """ - thickness = np.asarray(thickness) - polarThickness = np.asarray(polarThickness) - polarList = np.asarray(polarList) - tmax_in = np.max(thickness) - tmax_pol = np.max(polarThickness) - if (tmax_in > 1.2 and tmax_pol <= 1.2) or (tmax_in <= 1.2 and tmax_pol > 1.2): - raise Exception( - "Thicknesses of polars and input thickness need to be both in percent ([0-120]) or in fraction ([0-1.2])" - ) - - # sorting thickness - Isort = np.argsort(polarThickness) - polarThickness = polarThickness[Isort] - polarList = polarList[Isort] - - polars = [] - for it, t in enumerate(thickness): - ihigh = len(polarThickness) - 1 - for ip, tp in enumerate(polarThickness): - if tp > t: - ihigh = ip - break - ilow = 0 - for ip, tp in reversed(list(enumerate(polarThickness))): - if tp < t: - ilow = ip - break - - if ihigh == ilow: - polars.append(polarList[ihigh]) - print("[WARN] Using nearest polar for section {}, t={} , t_near={}".format(it, t, polarThickness[ihigh])) - else: - if (polarThickness[ilow] > t) or (polarThickness[ihigh] < t): - raise Exception("Implementation Error") - weight = (t - polarThickness[ilow]) / (polarThickness[ihigh] - polarThickness[ilow]) - # print(polarThickness[ilow],'<',t,'<',polarThickness[ihigh],'Weight',weight) - pol = blend(polarList[ilow], polarList[ihigh], weight) - polars.append(pol) - # import matplotlib.pyplot as plt - # fig=plt.figure() - # plt.plot(polarList[ilow][: ,0],polarList[ilow][: ,2],'b',label='thick'+str(polarThickness[ilow])) - # plt.plot(pol[:,0],pol[:,2],'k--',label='thick'+str(t)) - # plt.plot(polarList[ihigh][:,0],polarList[ihigh][:,2],'r',label='thick'+str(polarThickness[ihigh])) - # plt.legend() - # plt.show() - return polars - - -def _alpha_window_in_bounds(alpha, window): - """Ensures that the window of alpha values is within the bounds of alpha - Example: alpha in [-30,30], window=[-20,20] => window=[-20,20] - Example: alpha in [-10,10], window=[-20,20] => window=[-10,10] - Example: alpha in [-30,30], window=[-40,10] => window=[-40,10] - """ - IBef = np.where(alpha <= window[0])[0] - if len(IBef) > 0: - im = IBef[-1] - else: - im = 0 - IAft = np.where(alpha >= window[1])[0] - if len(IAft) > 0: - ip = IAft[0] - else: - ip = len(alpha) - 1 - window = [alpha[im], alpha[ip]] - return window - - -def _find_alpha0(alpha, coeff, window, direction='up', value_if_constant = np.nan): - """Finds the point where coeff(alpha)==0 using interpolation. - The search is narrowed to a window that can be specified by the user. The default window is yet enough for cases that make physical sense. - The angle alpha0 is found by looking at a zero up crossing in this window, and interpolation is used to find the exact location. - """ - # Constant case or only one value - if np.all(abs((coeff - coeff[0])<1e-8)) or len(coeff) == 1: - if coeff[0] == 0: - return 0 - else: - return value_if_constant - # Ensuring window is within our alpha values - window = _alpha_window_in_bounds(alpha, window) - - # Finding zero up-crossing within window - iwindow = np.where((alpha >= window[0]) & (alpha <= window[1])) - alpha = alpha[iwindow] - coeff = coeff[iwindow] - alpha_zc, i_zc, s_zc = _zero_crossings(x=alpha, y=coeff, direction=direction) - - if len(alpha_zc) > 1: - print('WARN: Cannot find alpha0, {} zero crossings of Coeff in the range of alpha values: [{} {}] '.format(len(alpha_zc),window[0],window[1])) - print('>>> Using second zero') - alpha_zc=alpha_zc[1:] - #raise Exception('Cannot find alpha0, {} zero crossings of Coeff in the range of alpha values: [{} {}] '.format(len(alpha_zc),window[0],window[1])) - elif len(alpha_zc) == 0: - raise NoCrossingException('Cannot find alpha0, no zero crossing of Coeff in the range of alpha values: [{} {}] '.format(window[0],window[1])) - - alpha0 = alpha_zc[0] - return alpha0 - - -def _find_TSE_region(alpha, coeff, slope, alpha0, deviation): - """Find the Trailing Edge Separation points, when the coefficient separates from its linear region - These points are defined as the points where the difference is equal to +/- `deviation` - Typically deviation is about 0.05 (absolute value) - The linear region is defined as coeff_lin = slope (alpha-alpha0) - - returns: - a_TSE: values of alpha at the TSE point (upper and lower) - - """ - # How off are we from the linear region - DeltaLin = slope * (alpha - alpha0) - coeff - - # Upper and lower regions - bUpp = alpha >= alpha0 - bLow = alpha <= alpha0 - - # Finding the point where the delta is equal to `deviation` - a_TSEUpp = np.interp(deviation, DeltaLin[bUpp], alpha[bUpp]) - a_TSELow = np.interp(-deviation, DeltaLin[bLow], alpha[bLow]) - return a_TSELow, a_TSEUpp - - -def _find_max_points(alpha, coeff, alpha0, method="inflections"): - """Find upper and lower max points in `coeff` vector. - if `method` is "inflection": - These point are detected from slope changes of `coeff`, positive of negative inflections - The upper stall point is the first point after alpha0 with a "hat" inflection - The lower stall point is the first point below alpha0 with a "v" inflection - """ - if method == "inflections": - dC = np.diff(coeff) - IHatInflections = np.where(np.logical_and.reduce((dC[1:] < 0, dC[0:-1] > 0, alpha[1:-1] > alpha0)))[0] - IVeeInflections = np.where(np.logical_and.reduce((dC[1:] > 0, dC[0:-1] < 0, alpha[1:-1] < alpha0)))[0] - if len(IHatInflections) <= 0: - raise NoStallDetectedException("Not able to detect upper stall point of curve") - if len(IVeeInflections) <= 0: - raise NoStallDetectedException("Not able to detect lower stall point of curve") - a_MaxUpp = alpha[IHatInflections[0] + 1] - c_MaxUpp = coeff[IHatInflections[0] + 1] - a_MaxLow = alpha[IVeeInflections[-1] + 1] - c_MaxLow = coeff[IVeeInflections[-1] + 1] - elif method == "minmax": - iMax = np.argmax(coeff) - iMin = np.argmin(coeff) - a_MaxUpp = alpha[iMax] - c_MaxUpp = coeff[iMax] - a_MaxLow = alpha[iMin] - c_MaxLow = coeff[iMin] - else: - raise NotImplementedError() - return (a_MaxUpp, c_MaxUpp, a_MaxLow, c_MaxLow) - - - - -# --------------------------------------------------------------------------------} -# --- Low-level functions -# --------------------------------------------------------------------------------{ -def fn_fullsep(alpha, dclda, alpha0, alpha_sl_neg, alpha_sl_pos, valpha, vCl): - """ Function that is zero when f=0 from the Kirchhoff theory """ - cl_linear = cl_lin(alpha, dclda, alpha0, alpha_sl_neg, alpha_sl_pos, valpha, vCl) - return cl_linear - 0.25* dclda*(alpha-alpha0) - -def cl_lin(alpha, dclda, alpha0, alpha_sl_neg, alpha_sl_pos, valpha, vCl): - """ Linear Cl """ - if alpha > alpha_sl_neg and alpha < alpha_sl_pos : - cl = dclda*(alpha-alpha0) - else: - cl=np.interp(alpha, valpha, vCl) - return cl - -def cl_fullsep(alpha, dclda, alpha0, alpha_sl_neg, alpha_sl_pos, valpha, vCl, alpha_fs_l, alpha_fs_u): - """ fully separated lift coefficient""" - cl_linear = cl_lin(alpha, dclda, alpha0, alpha_sl_neg, alpha_sl_pos, valpha, vCl) - if alpha > alpha_sl_neg and alpha < alpha_sl_pos : - cl = cl_linear*0.5 - else: - fp = f_point(alpha, dclda, alpha0, alpha_sl_neg, alpha_sl_pos, valpha, vCl, alpha_fs_l, alpha_fs_u) - cl=(cl_linear- dclda*(alpha-alpha0)*fp)/(1-fp) - return cl - -def f_point(alpha, dclda, alpha0, alpha_sl_neg, alpha_sl_pos, valpha, vCl, alpha_fs_l, alpha_fs_u): - """ separation function - # TODO harmonize with cl_fully_separated maybe? - """ - if dclda==0: - return 0 - if alpha < alpha_fs_l or alpha > alpha_fs_u: - return 0 - else: - cl_linear = cl_lin(alpha, dclda, alpha0, alpha_sl_neg, alpha_sl_pos, valpha, vCl) - xx=cl_linear/(dclda*(alpha-alpha0) + np.sign(cl_linear)*1e-15) - return (2*np.sqrt(xx)-1)**2 - - -def polar_params(alpha, cl, cd, cm): - """ - - alpha in radians - """ - # Treat zero-lift sections separately - zero_offset=1e-15 - x1 = -7*np.pi/180 - x2 = 7*np.pi/180 - y1 = np.interp(x1, alpha, cl) - y2 = np.interp(x2, alpha, cl) - - if (y1maxclp: - alpha_maxclp=alpha_sl_pos - maxclp=dclda - relerr=0. - for k in np.arange(nobs): - x1 = alpha0+(k+1)*dalpha - y1 = np.interp(x1, alpha, cl) - y2 = dclda*(x1-alpha0) - relerr=relerr+(y1-y2)/y2 - relerr=relerr/nobs - move_up= relerr>1e-2 - if move_up: - alpha_sl_pos=alpha_sl_pos-dalpha - alpha_sl_pos=max(alpha_maxclp, alpha_sl_pos) - - # Find negative angle of attack stall limit alpha_sl_neg - alpha_sl_neg=-20*np.pi/180 - move_down=True - maxclp=0. - while move_down: - dalpha=(alpha0-alpha_sl_neg)/nobs - y1=np.interp(alpha_sl_neg, alpha, cl) - dclda=y1/(alpha_sl_neg-alpha0) - if dclda>maxclp: - alpha_maxclp=alpha_sl_neg - maxclp=dclda - relerr=0. - for k in np.arange(nobs): - x1=alpha_sl_neg+(k+1)*dalpha - y1 = np.interp(x1, alpha, cl) - y2=dclda*(x1-alpha0) - relerr=relerr+(y1-y2)/y2 - relerr=relerr/nobs - move_down=relerr>1e-2 - if move_down: - alpha_sl_neg=alpha_sl_neg+dalpha - alpha_sl_neg=min(alpha_maxclp, alpha_sl_neg) - - # Compute the final alpha and dclda (linear lift coefficient slope) values - y1=np.interp(alpha_sl_neg, alpha, cl) - y2=np.interp(alpha_sl_pos, alpha, cl) - alpha0=(y1*alpha_sl_pos-y2*alpha_sl_neg)/(y1-y2) - dclda =(y1-y2)/(alpha_sl_neg-alpha_sl_pos) - - # Find Cd0 and Cm0 - Cd0 = np.interp(alpha0, alpha, cd) - Cm0 = np.interp(alpha0, alpha, cm) - # Find upper surface fully stalled angle of attack alpha_fs_u (Upper limit of the Kirchhoff flat plate solution) - y1=-1. - y2=-1. - delta = np.pi/180 - x2=alpha_sl_pos + delta - while y1*y2>0. and x2+delta0. and x2-delta>-np.pi: - x1=x2 - x2=x1-delta - y1=fn_fullsep(x1, dclda, alpha0, alpha_sl_neg, alpha_sl_pos, alpha, cl) - y2=fn_fullsep(x2, dclda, alpha0, alpha_sl_neg, alpha_sl_pos, alpha, cl) - if y1*y2<0: - alpha_fs_l=(0.-y2)/(y1-y2)*x1+(0.-y1)/(y2-y1)*x2 - else: - alpha_fs_l=-np.pi - - # --- Compute values at all angle of attack - Cl_fully_sep = np.zeros(alpha.shape) - fs = np.zeros(alpha.shape) - Cl_linear = np.zeros(alpha.shape) - for i,al in enumerate(alpha): - Cl_fully_sep[i] = cl_fullsep(al, dclda, alpha0, alpha_sl_neg, alpha_sl_pos, alpha, cl, alpha_fs_l, alpha_fs_u) - fs [i] = f_point (al, dclda, alpha0, alpha_sl_neg, alpha_sl_pos, alpha, cl, alpha_fs_l, alpha_fs_u) - Cl_linear [i] = cl_lin (al, dclda, alpha0, alpha_sl_neg, alpha_sl_pos, alpha, cl) - - p=dict() - p['alpha0'] = alpha0 - p['Cd0'] = Cd0 - p['Cm0'] = Cm0 - p['dclda'] = dclda - p['alpha_fs_l'] = alpha_fs_l - p['alpha_fs_u'] = alpha_fs_u - p['alpha_sl_neg'] = alpha_sl_neg - p['alpha_sl_pos'] = alpha_sl_pos - - return p, Cl_linear, Cl_fully_sep, fs - - - -def cl_linear_slope(alpha, cl, window=None, method="max", nInterp=721, inputInRadians=False, radians=False): - """ - Find slope of linear region - Outputs: a 2-tuplet of: - slope (in inverse units of alpha, or in radians-1 if radians=True) - alpha_0 in the same unit as alpha, or in radians if radians=True - - - INPUTS: - - alpha: angle of attack in radians - - Cl : lift coefficient - - window: [alpha_min, alpha_max]: region when linear slope is sought - - method: 'max', 'optim', 'leastsquare', 'leastsquare_constraint' - - OUTPUTS: - - Cl_alpha, alpha0: lift slope (1/rad) and angle of attack (rad) of zero lift - """ - # --- Return function - def myret(sl, a0): - # wrapper function to return degrees or radians # TODO this should be a function of self._radians - if radians: - if inputInRadians: - return sl, a0 - else: - return np.rad2deg(sl), np.deg2rad(a0) # NOTE: slope needs rad2deg, alpha needs deg2rad - else: - return sl, a0 - - # finding alpha0 # TODO TODO TODO THIS IS NOT NECESSARY - if inputInRadians: - windowAlpha0 = [np.radians(-30), np.radians(30)] - else: - windowAlpha0 = [-30, 30] - windowAlpha0 = _alpha_window_in_bounds(alpha, windowAlpha0) - alpha0 = _find_alpha0(alpha, cl, windowAlpha0) - - # Constant case or only one value - if np.all(cl == cl[0]) or len(cl) == 1: - return myret(0, alpha0) - - if window is None: - if np.nanmin(cl) > 0 or np.nanmax(cl) < 0: - window = [alpha[0], alpha[-1]] - else: - # define a window around alpha0 - if inputInRadians: - window = alpha0 + np.radians(np.array([-5, +20])) - else: - window = alpha0 + np.array([-5, +20]) - - # Ensuring window is within our alpha values - window = _alpha_window_in_bounds(alpha, window) - - if method in ["max", "leastsquare"]: - slope, off = _find_slope(alpha, cl, xi=alpha0, window=window, method=method) - - elif method == "leastsquare_constraint": - slope, off = _find_slope(alpha, cl, x0=alpha0, window=window, method="leastsquare") - - elif method == "optim": - # Selecting range of values within window - idx = np.where((alpha >= window[0]) & (alpha <= window[1]) & ~np.isnan(cl))[0] - cl, alpha = cl[idx], alpha[idx] - # Selecting within the min and max of this window to improve accuracy - imin = np.where(cl == np.min(cl))[0][-1] - idx = np.arange(imin, np.argmax(cl) + 1) - window = [alpha[imin], alpha[np.argmax(cl)]] - cl, alpha = cl[idx], alpha[idx] - # Performing minimization of slope - slope, off = _find_slope(alpha, cl, x0=alpha0, window=None, method="optim") - - else: - raise Exception("Method unknown for lift slope determination: {}".format(method)) - - # --- Safety checks - if len(cl) > 10: - # Looking at slope around alpha 0 to see if we are too far off - slope_FD, off_FD = _find_slope(alpha, cl, xi=alpha0, window=window, method="finitediff_1c") - if abs(slope - slope_FD) / slope_FD * 100 > 50: - #raise Exception('Warning: More than 20% error between estimated slope ({:.4f}) and the slope around alpha0 ({:.4f}). The window for the slope search ([{} {}]) is likely wrong.'.format(slope,slope_FD,window[0],window[-1])) - print('[WARN] More than 20% error between estimated slope ({:.4f}) and the slope around alpha0 ({:.4f}). The window for the slope search ([{} {}]) is likely wrong.'.format(slope,slope_FD,window[0],window[-1])) -# print('slope ',slope,' Alpha range: {:.3f} {:.3f} - nLin {} nMin {} nMax {}'.format(alpha[iStart],alpha[iEnd],len(alpha[iStart:iEnd+1]),nMin,len(alpha))) - return myret(slope, off) - -# --------------------------------------------------------------------------------} -# --- Generic curve handling functions -# --------------------------------------------------------------------------------{ -def _find_slope(x, y, xi=None, x0=None, window=None, method="max", opts=None, nInterp=721): - """Find the slope of a curve at x=xi based on a given method. - INPUTS: - x: array of x values - y: array of y values - xi: point where the slope is to be computed - x0: point where y(x0)=0 - if provided the constraint y(x0)=0 is added. - window: - If a `window` is provided the search is restrained to this region of x values. - Typical windows for airfoils are: window=[alpha0,Clmax], or window=[-5,5]+alpha0 - If window is None, the whole extent is used (window=[min(x),max(x)]) - - The methods available are: - 'max' : returns the maximum slope within the window. Needs `xi` - 'leastsquare': use leastsquare (or polyfit), to fit the curve within the window - 'finitediff_1c': first order centered finite difference. Needs `xi` - 'optim': find the slope by looking at all possible slope values, and try to find an optimal where the length of linear region is maximized. - - returns: - (a,x0): such that the slope is a(x-x0) - (x0=-b/a where y=ax+b) - """ - if window is not None: - x_=x - y_=y - if nInterp is not None: - x_ = np.linspace(x[0], x[-1], max(nInterp,len(x))) # using 0.5deg resolution at least - y_ = np.interp(x_, x, y) - I = np.where(np.logical_and(x_>=window[0],x_<=window[1])) - x = x_[I] - y = y_[I] - - - if len(y) <= 1: - raise Exception('Cannot find slope, two points needed ({} after window selection)'.format(len(y))) - - - if len(y)<4 and method=='optim': - method='leastsquare' - #print('[WARN] Not enought data to find slope with optim method, using leastsquare') - - - if method == "max": - if xi is not None: - I = np.nonzero(x - xi) - yi = np.interp(xi, x, y) - a = max((y[I] - yi) / (x[I] - xi)) - x0 = xi - yi / a - else: - raise Exception("For now xi needs to be set to find a slope with the max method") - - elif method == "finitediff_1c": - # First order centered finite difference - if xi is not None: - # First point strictly before xi - im = np.where(x < xi)[0][-1] - dx = x[im + 1] - x[im - 1] - if np.abs(dx) > 1e-7: - a = (y[im + 1] - y[im - 1]) / dx - yi = np.interp(xi, x, y) - x0 = xi - yi / a - else: - a = np.inf - x0 = xi - #print('a',a) - #print('x0',x0) - #print('yi',yi) - dx=(x[im+1]-x[im]) - if np.abs(dx)>1e-7: - a = ( y[im+1] - y[im] ) / dx - yi = np.interp(xi,x,y) - x0 = xi - yi/a - else: - a=np.inf - x0 = xi - #print('a',a) - #print('x0',x0) - #print('yi',yi) - else: - raise Exception("For now xi needs to be set to find a slope with the finite diff method") - - elif method == "leastsquare": - if x0 is not None: - try: - a = np.linalg.lstsq((x - x0).reshape((-1, 1)), y.reshape((-1, 1)), rcond=None)[0][0][0] - except: - a = np.linalg.lstsq((x - x0).reshape((-1, 1)), y.reshape((-1, 1)))[0][0][0] - else: - p = np.polyfit(x, y, 1) - a = p[0] - x0 = -p[1] / a - elif method == "optim": - if opts is None: - nMin = max(3, int(len(x) / 2)) - else: - nMin = opts["nMin"] - - a, x0, iStart, iEnd = _find_linear_region(x, y, nMin, x0) - - else: - raise NotImplementedError() - return a, x0 - -def _find_linear_region(x, y, nMin, x0=None): - """Find a linear region by computing all possible slopes for all possible extent. - The objective function tries to minimize the error with the linear slope - and maximize the length of the linear region. - nMin is the mimum number of points to be present in the region - If x0 is provided, the function a*(x-x0) is fitted - - returns: - slope : - offset: - iStart: index of start of linear region - iEnd : index of end of linear region - """ - if x0 is not None: - x = x.reshape((-1, 1)) - x0 - y = y.reshape((-1, 1)) - n = len(x) - nMin + 1 - err = np.zeros((n, n)) * np.nan - slp = np.zeros((n, n)) * np.nan - off = np.zeros((n, n)) * np.nan - spn = np.zeros((n, n)) * np.nan - for iStart in range(n): - for j in range(iStart, n): - iEnd = j + nMin - if x0 is not None: - sl = np.linalg.lstsq(x[iStart:iEnd], y[iStart:iEnd], rcond=None)[0][0] - slp[iStart, j] = sl - off[iStart, j] = x0 - y_lin = x[iStart:iEnd] * sl - else: - coefs = np.polyfit(x[iStart:iEnd], y[iStart:iEnd], 1) - slp[iStart, j] = coefs[0] - off[iStart, j] = -coefs[1] / coefs[0] - y_lin = x[iStart:iEnd] * coefs[0] + coefs[1] - err[iStart, j] = np.mean((y[iStart:iEnd] - y_lin) ** 2) - spn[iStart, j] = iEnd - iStart - spn = 1 / (spn - nMin + 1) - err = (err) / (np.nanmax(err)) - obj = np.multiply(spn, err) - obj = err - (iStart, j) = np.unravel_index(np.nanargmin(obj), obj.shape) - iEnd = j + nMin - 1 # note -1 since we return the index here - return slp[iStart, j], off[iStart, j], iStart, iEnd - - -def _zero_crossings(y, x=None, direction=None): - - """ - Find zero-crossing points in a discrete vector, using linear interpolation. - direction: 'up' or 'down', to select only up-crossings or down-crossings - Returns: - x values xzc such that y(yzc)==0 - indexes izc, such that the zero is between y[izc] (excluded) and y[izc+1] (included) - if direction is not provided, also returns: - sign, equal to 1 for up crossing - """ - y = np.asarray(y) - if x is None: - x = np.arange(len(y)) - - deltas = x[1:] - x[0:-1] - if np.any( deltas == 0.0): - I=np.where(deltas==0)[0] - print("[WARN] Some x values are repeated at index {}. Removing them.".format(I)) - x=np.delete(x,I) - y=np.delete(x,I) - if np.any(deltas<0): - raise Exception("x values need to be in ascending order") - - # Indices before zero-crossing - iBef = np.where(y[1:] * y[0:-1] < 0.0)[0] - - # Find the zero crossing by linear interpolation - xzc = x[iBef] - y[iBef] * (x[iBef + 1] - x[iBef]) / (y[iBef + 1] - y[iBef]) - - # Selecting points that are exactly 0 and where neighbor change sign - iZero = np.where(y == 0.0)[0] - iZero = iZero[np.where((iZero > 0) & (iZero < x.size - 1))] - iZero = iZero[np.where(y[iZero - 1] * y[iZero + 1] < 0.0)] - - # Concatenate - xzc = np.concatenate((xzc, x[iZero])) - iBef = np.concatenate((iBef, iZero)) - - # Sort - iSort = np.argsort(xzc) - xzc, iBef = xzc[iSort], iBef[iSort] - - # Return up-crossing, down crossing or both - sign = np.sign(y[iBef + 1] - y[iBef]) - if direction == "up": - I = np.where(sign == 1)[0] - return xzc[I], iBef[I], sign[I] - elif direction == "down": - I = np.where(sign == -1)[0] - return xzc[I], iBef[I], sign[I] - elif direction is not None: - raise Exception("Direction should be either `up` or `down`") - return xzc, iBef, sign - - -def _intersections(x1, y1, x2, y2, plot=False, minDist=1e-6, verbose=False): - """ - INTERSECTIONS Intersections of curves. - Computes the (x,y) locations where two curves intersect. The curves - can be broken with NaNs or have vertical segments. - - Written by: Sukhbinder, https://github.com/sukhbinder/intersection - adapted by E.Branlard to allow for minimum distance between points - License: MIT - usage: - x,y=intersection(x1,y1,x2,y2) - - Example: - a, b = 1, 2 - phi = np.linspace(3, 10, 100) - x1 = a*phi - b*np.sin(phi) - y1 = a - b*np.cos(phi) - - x2=phi - y2=np.sin(phi)+2 - x,y=intersections(x1,y1,x2,y2) - - plt.plot(x1,y1,c='r') - plt.plot(x2,y2,c='g') - plt.plot(x,y,'*k') - plt.show() - - """ - - def _rect_inter_inner(x1, x2): - n1 = x1.shape[0] - 1 - n2 = x2.shape[0] - 1 - X1 = np.c_[x1[:-1], x1[1:]] - X2 = np.c_[x2[:-1], x2[1:]] - S1 = np.tile(X1.min(axis=1), (n2, 1)).T - S2 = np.tile(X2.max(axis=1), (n1, 1)) - S3 = np.tile(X1.max(axis=1), (n2, 1)).T - S4 = np.tile(X2.min(axis=1), (n1, 1)) - return S1, S2, S3, S4 - - def _rectangle_intersection_(x1, y1, x2, y2): - S1, S2, S3, S4 = _rect_inter_inner(x1, x2) - S5, S6, S7, S8 = _rect_inter_inner(y1, y2) - - C1 = np.less_equal(S1, S2) - C2 = np.greater_equal(S3, S4) - C3 = np.less_equal(S5, S6) - C4 = np.greater_equal(S7, S8) - - ii, jj = np.nonzero(C1 & C2 & C3 & C4) - return ii, jj - - ii, jj = _rectangle_intersection_(x1, y1, x2, y2) - n = len(ii) - - dxy1 = np.diff(np.c_[x1, y1], axis=0) - dxy2 = np.diff(np.c_[x2, y2], axis=0) - - T = np.zeros((4, n)) - AA = np.zeros((4, 4, n)) - AA[0:2, 2, :] = -1 - AA[2:4, 3, :] = -1 - AA[0::2, 0, :] = dxy1[ii, :].T - AA[1::2, 1, :] = dxy2[jj, :].T - - BB = np.zeros((4, n)) - BB[0, :] = -x1[ii].ravel() - BB[1, :] = -x2[jj].ravel() - BB[2, :] = -y1[ii].ravel() - BB[3, :] = -y2[jj].ravel() - - for i in range(n): - try: - T[:, i] = np.linalg.solve(AA[:, :, i], BB[:, i]) - except: - T[:, i] = np.NaN - - in_range = (T[0, :] >= 0) & (T[1, :] >= 0) & (T[0, :] <= 1) & (T[1, :] <= 1) - - xy0 = T[2:, in_range] - xy0 = xy0.T - - x = xy0[:, 0] - y = xy0[:, 1] - - # --- Remove "duplicates" - if minDist is not None: - pointKept=[(x[0],y[0])] - pointSkipped=[] - for p in zip(x[1:],y[1:]): - distances = np.array([np.sqrt((p[0]-pk[0])**2 + (p[1]-pk[1])**2) for pk in pointKept]) - if all(distances>minDist): - pointKept.append((p[0],p[1])) - else: - pointSkipped.append((p[0],p[1])) - if verbose: - if len(pointSkipped)>0: - print('Polar:Intersection:Point Kept :', pointKept) - print('Polar:Intersection:Point Skipped:', pointSkipped) - - M = np.array(pointKept) - x = M[:,0] - y = M[:,1] - if plot: - import matplotlib.pyplot as plt - plt.plot(x1,y1,'.',c='r') - plt.plot(x2,y2,'',c='g') - plt.plot(x,y,'*k') - - return x, y - - -def smooth_heaviside(x, k=1, rng=(-np.inf, np.inf), method="exp"): - r""" - Smooth approximation of Heaviside function where the step occurs between rng[0] and rng[1]: - if rng[0]=rng[1])=1 - if rng[0]>rng[1]: then f(=rng[0])=0 - exp: - rng=(-inf,inf): H(x)=[1 + exp(-2kx) ]^-1 - rng=(-1,1): H(x)=[1 + exp(4kx/(x^2-1) ]^-1 - rng=(0,1): H(x)=[1 + exp(k(2x-1)/(x(x-1)) ]^-1 - INPUTS: - x : scalar or vector of real x values \in ]-infty; infty[ - k : float >=1, the higher k the "steeper" the heaviside function - rng: tuple of min and max value such that f(<=min)=0 and f(>=max)=1. - Reversing the range makes the Heaviside function from 1 to 0 instead of 0 to 1 - method: smooth approximation used (e.g. exp or tan) - NOTE: an epsilon is introduced in the denominator to avoid overflow of the exponentail - """ - if k < 1: - raise Exception("k needs to be >=1") - eps = 1e-2 - mn, mx = rng - x = np.asarray(x) - H = np.zeros(x.shape) - if mn < mx: - H[x <= mn] = 0 - H[x >= mx] = 1 - b = np.logical_and(x > mn, x < mx) - else: - H[x <= mx] = 1 - H[x >= mn] = 0 - b = np.logical_and(x < mn, x > mx) - x = x[b] - if method == "exp": - if np.abs(mn) == np.inf and np.abs(mx) == np.inf: - # Infinite support - x[k * x > 100] = 100.0 / k - x[k * x < -100] = -100.0 / k - if mn < mx: - H[b] = 1 / (1 + np.exp(-k * x)) - else: - H[b] = 1 / (1 + np.exp(k * x)) - elif np.abs(mn) != np.inf and np.abs(mx) != np.inf: - n = 4.0 - # Compact support - s = 2.0 / (mx - mn) * (x - (mn + mx) / 2.0) # transform compact support into ]-1,1[ - x = -n * s / (s ** 2 - 1.0) # then transform ]-1,1[ into ]-inf,inf[ - x[k * x > 100] = 100.0 / k - x[k * x < -100] = -100.0 / k - H[b] = 1.0 / (1 + np.exp(-k * x)) - else: - raise NotImplementedError("Heaviside with only one bound infinite") - else: - # TODO tan approx - raise NotImplementedError() - return H - - -if __name__ == "__main__": - pass +""" This module contains: + - Polar: class to represent a polar (computes steady/unsteady parameters, corrections etc.) + - blend: function to blend two polars + - thicknessinterp_from_one_set: interpolate polars at different thickeness based on one set of polars +""" + +import os +import numpy as np +from .polar_file import loadPolarFile # For IO + +class NoCrossingException(Exception): + pass +class NoStallDetectedException(Exception): + pass + + +class Polar(object): + """ + Defines section lift, drag, and pitching moment coefficients as a + function of angle of attack at a particular Reynolds number. + Different parameters may be computed and different corrections applied. + + Available routines: + - cl_interp : cl at given alpha values + - cd_interp : cd at given alpha values + - cm_interp : cm at given alpha values + - cn_interp : cn at given alpha values + - fs_interp : separation function (compared to fully separated polar) + - cl_fs_interp : cl fully separated at given alpha values + - cl_inv_interp : cl inviscid at given alpha values + - correction3D : apply 3D rotatational correction + - extrapolate : extend polar data set using Viterna's method + - unsteadyParams : computes unsteady params e.g. needed by AeroDyn15 + - unsteadyparam : same but (old) + - plot : plots the polar + - alpha0 : computes and returns alpha0, also stored in _alpha0 + - linear_region : determines the alpha and cl values in the linear region + - cl_max : cl_max + - cl_linear_slope : linear slope and the linear region + - cl_fully_separated: fully separated cl + - toAeroDyn: write AeroDyn file + """ + + def __init__(self, filename=None, alpha=None, cl=None, cd=None, cm=None, Re=None, + compute_params=False, radians=None, cl_lin_method='max', + fformat='auto', verbose=False): + """Constructor + + Parameters + ---------- + filename: string + If provided, the polar will be read from filename using fformat + Re : float + Reynolds number + alpha : ndarray (deg) + angle of attack + cl : ndarray + lift coefficient + cd : ndarray + drag coefficient + cm : ndarray + moment coefficient + fformat: string + file format to be used when filename is provided + + """ + # --- Potentially-locked properties + # Introducing locks so that some properties become readonly if prescribed by user + self._fs_lock = False + self._cl_fs_lock = False + self._cl_inv_lock = False + # TODO lock _alpha0 and cl_alpha + self.fs = None # steady separation function + self.cl_fs = None # cl_fully separated + self.cl_inv = None # cl inviscid/linear/potential flow + self._alpha0 = None + self._linear_slope = None + + # Read polar according to fileformat, if filename provided + if filename is not None: + df, Re = loadPolarFile(filename, fformat=fformat, to_radians=radians, verbose=verbose) + alpha = df['Alpha'].values + cl = df['Cl'].values + cd = df['Cd'].values + cm = df['Cm'].values + if 'fs' in df.keys(): + print('[INFO] Using separating function from input file.') + self.fs = df['fs'].values + self._fs_lock = True + if 'Cl_fs' in df.keys(): + print('[INFO] Using Cl fully separated from input file.') + self.cl_fs =df['Cl_fs'].values + self._cl_fs_lock = True + if 'Cl_inv' in df.keys(): + print('[INFO] Using Cl inviscid from input file.') + self.cl_inv = df['Cl_inv'].values + self._cl_inv_lock = True + # TODO we need a trigger if cl_inv provided, we should get alpha0 and slope from it + nLocks = sum([self._fs_lock, self._cl_fs_lock, self._cl_inv_lock]) + if nLocks>0 and nLocks<3: + raise Exception("For now, input files are assumed to have all or none of the columns: (fs, cl_fs, and cl_inv). Otherwise, we\'ll have to ensure consitency, and so far we dont...") + + self.Re = Re + self.alpha = np.array(alpha) + if cl is None: + cl = np.zeros_like(self.alpha) + if cd is None: + cd = np.zeros_like(self.alpha) + if cm is None: + cm = np.zeros_like(self.alpha) + self.cl = np.array(cl) + self.cd = np.array(cd) + self.cm = np.array(cm) + if radians is None: + # If the max alpha is above pi, most likely we are in degrees + self._radians = np.mean(np.abs(self.alpha)) <= np.pi / 2 + else: + self._radians = radians + + # NOTE: method needs to be in harmony for linear_slope and the one used in cl_fully_separated + if compute_params: + self._linear_slope, self._alpha0 = self.cl_linear_slope(method=cl_lin_method) + if not self._cl_fs_lock: + self.cl_fully_separated(method=cl_lin_method) + if not self._cl_inv_lock: + self.cl_inv = self._linear_slope * (self.alpha - self._alpha0) + + def __repr__(self): + s='<{} object>:\n'.format(type(self).__name__) + sunit = 'deg' + if self._radians: + sunit = 'rad' + s+='Parameters:\n' + s+=' - alpha, cl, cd, cm : arrays of size {}\n'.format(len(self.alpha)) + s+=' - Re : {} \n'.format(self.Re) + s+=' - _radians: {} (True if alpha in radians)\n'.format(self._radians) + s+=' - _alpha0: {} [{}]\n'.format(self._alpha0, sunit) + s+=' - _linear_slope: {} [1/{}]\n'.format(self._linear_slope, sunit) + s+='Derived parameters:\n' + s+=' * cl_inv : array of size {} \n'.format(len(self.alpha)) + s+=' * cl_fs : array of size {} \n'.format(len(self.alpha)) + s+=' * fs : array of size {} \n'.format(len(self.alpha)) + s+=' * cl_lin (UNSURE) : array of size {} \n'.format(len(self.alpha)) + s+='Functional parameters:\n' + s+=' * alpha0 : {} [{}]\n'.format(self.alpha0(),sunit) + s+=' * cl_linear_slope : {} [1/{}]\n'.format(self.cl_linear_slope()[0],sunit) + s+=' * cl_max : {} \n'.format(self.cl_max()) + s+=' * unsteadyParams : {} \n'.format(self.unsteadyParams()) + s+='Useful functions: cl_interp, cd_interp, cm_interp, fs_interp \n' + s+=' cl_fs_interp, cl_inv_interp, \n' + s+=' interpolant \n' + s+=' plot, extrapolate\n' + return s + + + # --- Potential read only properties + @property + def cl_inv(self): + if self._cl_inv is None: + self.cl_fully_separated() # computes cl_fs, cl_inv and fs + return self._cl_inv + @cl_inv.setter + def cl_inv(self, cl_inv): + if self._cl_inv_lock: + raise Exception('Cl_inv was set by user, cannot modify it') + else: + self._cl_inv = cl_inv + + @property + def cl_fs(self): + if self._cl_fs is None: + self.cl_fully_separated() # computes cl_fs, cl_inv and fs + return self._cl_fs + @cl_fs.setter + def cl_fs(self, cl_fs): + if self._cl_fs_lock: + raise Exception('cl_fs was set by user, cannot modify it') + else: + self._cl_fs = cl_fs + + @property + def fs(self): + if self._fs is None: + self.cl_fully_separated() # computes fs, cl_inv and fs + return self._fs + @fs.setter + def fs(self, fs): + if self._fs_lock: + raise Exception('fs was set by user, cannot modify it') + else: + self._fs = fs + + + # --- Interpolants + def cl_interp(self, alpha): + return np.interp(alpha, self.alpha, self.cl) + + def cd_interp(self, alpha): + return np.interp(alpha, self.alpha, self.cd) + + def cm_interp(self, alpha): + return np.interp(alpha, self.alpha, self.cm) + + def cn_interp(self, alpha): + return np.interp(alpha, self.alpha, self.cn) + + def fs_interp(self, alpha): + return np.interp(alpha, self.alpha, self.fs) + + def cl_fs_interp(self, alpha): + return np.interp(alpha, self.alpha, self.cl_fs) + + def cl_inv_interp(self, alpha): + return np.interp(alpha, self.alpha, self.cl_inv) + + def interpolant(self, variables=['cl', 'cd', 'cm'], radians=None): + """ + Create an interpolant `f` for a set of requested variables with alpha as input variable: + var_array = f(alpha) + + This is convenient to quickly interpolate multiple polar variables at once. + The interpolant returns an array corresponding to the interpolated values of the + requested `variables`, in the same order as they are requested. + + When alpha is a scalar, f(alpha) is of length nVar = len(variables) + When alpha is an array of length n, f(alpha) is of shape (nVar x n) + + INPUTS: + - variables: list of variables that will be returned by the interpolant + Allowed values: ['alpha', 'cl', 'cd', 'cm', 'fs', 'cl_inv', 'cl_fs'] + - radians: enforce whether `alpha` is in radians or degrees + + OUTPUTS: + - f: interpolant + + """ + from scipy.interpolate import interp1d + + MAP = {'alpha':self.alpha, 'cl':self.cl, 'cd':self.cd, 'cm':self.cm, + 'cl_inv':self.cl_inv, 'cl_fs':self.cl_fs, 'fs':self.fs} + + if radians is None: + radians = self._radians + + # Create a Matrix with columns requested by user + #polCols = polar.columns.values[1:] + M = self.alpha # we start by alpha for convenience + for v in variables: + v = v.lower().strip() + if v not in MAP.keys(): + raise Exception('Polar: cannot create an interpolant for variable `{}`, allowed variables: {}'.format(v, MAP.keys())) + M = np.column_stack( (M, MAP[v]) ) + # We remove alpha + M = M[:,1:] + # Determine the "x" value for the interpolant (alpha in rad or deg) + if radians == self._radians: + alpha = self.alpha # the user requested the same as what we have + else: + if radians: + alpha = np.radians(self.alpha) + else: + alpha = np.degrees(self.alpha) + # Create the interpolant for requested variables with alpha as "x" axis + f = interp1d(alpha, M.T) + return f + + @property + def cn(self): + """ returns : Cl cos(alpha) + Cd sin(alpha) + NOT: Cl cos(alpha) + (Cd-Cd0) sin(alpha) + """ + if self._radians: + return self.cl * np.cos(self.alpha) + self.cd * np.sin(self.alpha) + else: + return self.cl * np.cos(self.alpha * np.pi / 180) + self.cd * np.sin(self.alpha * np.pi / 180) + + @property + def cl_lin(self): # TODO consider removing + print('[WARN] Polar: cl_lin is a bit of a weird property. Not sure if it will be kept') + if self.cl_inv is None: + self.cl_fully_separated() # computes cl_fs, cl_inv and fs + return self.cl_inv + #if (self._linear_slope is None) and (self._alpha0 is None): + # self._linear_slope,self._alpha0=self.cl_linear_slope() + #return self._linear_slope*(self.alpha-self._alpha0) + + @classmethod + def fromfile(cls, filename, fformat='auto', compute_params=False, to_radians=False): + """Constructor based on a filename + # NOTE: this is legacy + """ + print('[WARN] Polar: "fromfile" is depreciated and will be removed in a future release') + return cls(filename, fformat=fformat, compute_params=compute_params, radians=to_radians) + + def correction3D( + self, + r_over_R, + chord_over_r, + tsr, + lift_method="DuSelig", + drag_method="None", + blending_method="linear_25_45", + max_cl_corr=0.25, + alpha_max_corr=None, + alpha_linear_min=None, + alpha_linear_max=None, + ): + """Applies 3-D corrections for rotating sections from the 2-D data. + + Parameters + ---------- + r_over_R : float + local radial position / rotor radius + chord_over_r : float + local chord length / local radial location + tsr : float + tip-speed ratio + lift_method : string, optional + flag switching between Du-Selig and Snel corrections + drag_method : string, optional + flag switching between Eggers correction and None + blending_method: string: + blending method used to blend from 3D to 2D polar. default 'linear_25_45' + max_cl_corr: float, optional + maximum correction allowed, default is 0.25. + alpha_max_corr : float, optional (deg) + maximum angle of attack to apply full correction + alpha_linear_min : float, optional (deg) + angle of attack where linear portion of lift curve slope begins + alpha_linear_max : float, optional (deg) + angle of attack where linear portion of lift curve slope ends + + Returns + ------- + polar : Polar + A new Polar object corrected for 3-D effects + + Notes + ----- + The Du-Selig method :cite:`Du1998A-3-D-stall-del` is used to correct lift, and + the Eggers method :cite:`Eggers-Jr2003An-assessment-o` is used to correct drag. + + """ + + if alpha_max_corr == None and alpha_linear_min == None and alpha_linear_max == None: + alpha_linear_region, _, cl_slope, alpha0 = self.linear_region() + alpha_linear_min = alpha_linear_region[0] + alpha_linear_max = alpha_linear_region[-1] + _, alpha_max_corr = self.cl_max() + find_linear_region = False + elif alpha_max_corr * alpha_linear_min * alpha_linear_max == None: + raise Exception( + "Define all or none of the keyword arguments alpha_max_corr, alpha_linear_min, and alpha_linear_max" + ) + else: + find_linear_region = True + + # rename and convert units for convenience + if self._radians: + alpha = alpha + else: + alpha = np.radians(self.alpha) + cl_2d = self.cl + cd_2d = self.cd + alpha_max_corr = np.radians(alpha_max_corr) + alpha_linear_min = np.radians(alpha_linear_min) + alpha_linear_max = np.radians(alpha_linear_max) + + # parameters in Du-Selig model + a = 1 + b = 1 + d = 1 + lam = tsr / (1 + tsr ** 2) ** 0.5 # modified tip speed ratio + if np.abs(r_over_R)>1e-4: + expon = d / lam / r_over_R + else: + expon = d / lam / 1e-4 + + # find linear region with numpy polyfit + if find_linear_region: + idx = np.logical_and(alpha >= alpha_linear_min, alpha <= alpha_linear_max) + p = np.polyfit(alpha[idx], cl_2d[idx], 1) + cl_slope = p[0] + alpha0 = -p[1] / cl_slope + else: + cl_slope = np.degrees(cl_slope) + alpha0 = np.radians(alpha0) + + if lift_method == "DuSelig": + # Du-Selig correction factor + if np.abs(cl_slope)>1e-4: + fcl = ( + 1.0 + / cl_slope + * (1.6 * chord_over_r / 0.1267 * (a - chord_over_r ** expon) / (b + chord_over_r ** expon) - 1) + ) + # Force fcl to stay non-negative + if fcl < 0.: + fcl = 0. + else: + fcl=0.0 + elif lift_method == "Snel": + # Snel correction + fcl = 3.0 * chord_over_r ** 2.0 + else: + raise Exception("The keyword argument lift_method (3d correction for lift) can only be DuSelig or Snel.") + + # 3D correction for lift + cl_linear = cl_slope * (alpha - alpha0) + cl_corr = fcl * (cl_linear - cl_2d) + # Bound correction +/- max_cl_corr + cl_corr = np.clip(cl_corr, -max_cl_corr, max_cl_corr) + # Blending + if blending_method == "linear_25_45": + # We adjust fully between +/- 25 deg, linearly to +/- 45 + adj_alpha = np.radians([-180, -45, -25, 25, 45, 180]) + adj_value = np.array([0, 0, 1, 1, 0, 0]) + adj = np.interp(alpha, adj_alpha, adj_value) + elif blending_method == "heaviside": + # Apply (arbitrary!) smoothing function to smoothen the 3D corrections and zero them out away from alpha_max_corr + delta_corr = 10 + y1 = 1.0 - smooth_heaviside(alpha, k=1, rng=(alpha_max_corr, alpha_max_corr + np.deg2rad(delta_corr))) + y2 = smooth_heaviside(alpha, k=1, rng=(0.0, np.deg2rad(delta_corr))) + adj = y1 * y2 + else: + raise NotImplementedError("blending :", blending_method) + cl_3d = cl_2d + cl_corr * adj + + # Eggers 2003 correction for drag + if drag_method == "Eggers": + delta_cd = cl_corr * (np.sin(alpha) - 0.12 * np.cos(alpha)) / (np.cos(alpha) + 0.12 * np.sin(alpha)) * adj + elif drag_method == "None": + delta_cd = 0.0 + else: + raise Exception("The keyword argument darg_method (3d correction for drag) can only be Eggers or None.") + + cd_3d = cd_2d + delta_cd + + return type(self)(Re=self.Re, alpha=np.degrees(alpha), cl=cl_3d, cd=cd_3d, cm=self.cm, radians=False) + + def extrapolate(self, cdmax, AR=None, cdmin=0.001, nalpha=15): + """Extrapolates force coefficients up to +/- 180 degrees using Viterna's method + :cite:`Viterna1982Theoretical-and`. + + Parameters + ---------- + cdmax : float + maximum drag coefficient + AR : float, optional + aspect ratio = (rotor radius / chord_75% radius) + if provided, cdmax is computed from AR + cdmin: float, optional + minimum drag coefficient. used to prevent negative values that can sometimes occur + with this extrapolation method + nalpha: int, optional + number of points to add in each segment of Viterna method + + Returns + ------- + polar : Polar + a new Polar object + + Notes + ----- + If the current polar already supplies data beyond 90 degrees then + this method cannot be used in its current form and will just return itself. + + If AR is provided, then the maximum drag coefficient is estimated as + + >>> cdmax = 1.11 + 0.018*AR + + + """ + + if cdmin < 0: + raise Exception("cdmin cannot be < 0") + + # lift coefficient adjustment to account for assymetry + cl_adj = 0.7 + + # estimate CD max + if AR is not None: + cdmax = 1.11 + 0.018 * AR + self.cdmax = max(max(self.cd), cdmax) + + # extract matching info from ends + alpha_high = np.radians(self.alpha[-1]) + cl_high = self.cl[-1] + cd_high = self.cd[-1] + cm_high = self.cm[-1] + + alpha_low = np.radians(self.alpha[0]) + cl_low = self.cl[0] + cd_low = self.cd[0] + + if alpha_high > np.pi / 2: + raise Exception("alpha[-1] > pi/2") + return self + if alpha_low < -np.pi / 2: + raise Exception("alpha[0] < -pi/2") + return self + + # parameters used in model + sa = np.sin(alpha_high) + ca = np.cos(alpha_high) + self.A = (cl_high - self.cdmax * sa * ca) * sa / ca ** 2 + self.B = (cd_high - self.cdmax * sa * sa) / ca + + # alpha_high <-> 90 + alpha1 = np.linspace(alpha_high, np.pi / 2, nalpha) + alpha1 = alpha1[1:] # remove first element so as not to duplicate when concatenating + cl1, cd1 = self.__Viterna(alpha1, 1.0) + + # 90 <-> 180-alpha_high + alpha2 = np.linspace(np.pi / 2, np.pi - alpha_high, nalpha) + alpha2 = alpha2[1:] + cl2, cd2 = self.__Viterna(np.pi - alpha2, -cl_adj) + + # 180-alpha_high <-> 180 + alpha3 = np.linspace(np.pi - alpha_high, np.pi, nalpha) + alpha3 = alpha3[1:] + cl3, cd3 = self.__Viterna(np.pi - alpha3, 1.0) + cl3 = (alpha3 - np.pi) / alpha_high * cl_high * cl_adj # override with linear variation + + if alpha_low <= -alpha_high: + alpha4 = [] + cl4 = [] + cd4 = [] + alpha5max = alpha_low + else: + # -alpha_high <-> alpha_low + # Note: this is done slightly differently than AirfoilPrep for better continuity + alpha4 = np.linspace(-alpha_high, alpha_low, nalpha) + alpha4 = alpha4[1:-2] # also remove last element for concatenation for this case + cl4 = -cl_high * cl_adj + (alpha4 + alpha_high) / (alpha_low + alpha_high) * (cl_low + cl_high * cl_adj) + cd4 = cd_low + (alpha4 - alpha_low) / (-alpha_high - alpha_low) * (cd_high - cd_low) + alpha5max = -alpha_high + + # -90 <-> -alpha_high + alpha5 = np.linspace(-np.pi / 2, alpha5max, nalpha) + alpha5 = alpha5[1:] + if alpha_low == -alpha_high: + alpha5 = alpha5[:-1] + cl5, cd5 = self.__Viterna(-alpha5, -cl_adj) + + # -180+alpha_high <-> -90 + alpha6 = np.linspace(-np.pi + alpha_high, -np.pi / 2, nalpha) + alpha6 = alpha6[1:] + cl6, cd6 = self.__Viterna(alpha6 + np.pi, cl_adj) + + # -180 <-> -180 + alpha_high + alpha7 = np.linspace(-np.pi, -np.pi + alpha_high, nalpha) + cl7, cd7 = self.__Viterna(alpha7 + np.pi, 1.0) + cl7 = (alpha7 + np.pi) / alpha_high * cl_high * cl_adj # linear variation + + alpha = np.concatenate((alpha7, alpha6, alpha5, alpha4, np.radians(self.alpha), alpha1, alpha2, alpha3)) + cl = np.concatenate((cl7, cl6, cl5, cl4, self.cl, cl1, cl2, cl3)) + cd = np.concatenate((cd7, cd6, cd5, cd4, self.cd, cd1, cd2, cd3)) + + cd = np.maximum(cd, cdmin) # don't allow negative drag coefficients + + # Setup alpha and cm to be used in extrapolation + cm1_alpha = np.floor(self.alpha[0] / 10.0) * 10.0 + cm2_alpha = np.ceil(self.alpha[-1] / 10.0) * 10.0 + if cm2_alpha == self.alpha[-1]: + self.alpha = self.alpha[:-1] + self.cm = self.cm[:-1] + alpha_num = abs(int((-180.0 - cm1_alpha) / 10.0 - 1)) + alpha_cm1 = np.linspace(-180.0, cm1_alpha, alpha_num) + alpha_cm2 = np.linspace(cm2_alpha, 180.0, int((180.0 - cm2_alpha) / 10.0 + 1)) + alpha_cm = np.concatenate( + (alpha_cm1, self.alpha, alpha_cm2) + ) # Specific alpha values are needed for cm function to work + cm1 = np.zeros(len(alpha_cm1)) + cm2 = np.zeros(len(alpha_cm2)) + cm_ext = np.concatenate((cm1, self.cm, cm2)) + if np.count_nonzero(self.cm) > 0: + cmCoef = self.__CMCoeff(cl_high, cd_high, cm_high) # get cm coefficient + cl_cm = np.interp(alpha_cm, np.degrees(alpha), cl) # get cl for applicable alphas + cd_cm = np.interp(alpha_cm, np.degrees(alpha), cd) # get cd for applicable alphas + alpha_low_deg = self.alpha[0] + alpha_high_deg = self.alpha[-1] + for i in range(len(alpha_cm)): + cm_new = self.__getCM(i, cmCoef, alpha_cm, cl_cm, cd_cm, alpha_low_deg, alpha_high_deg) + if cm_new is None: + pass # For when it reaches the range of cm's that the user provides + else: + cm_ext[i] = cm_new + cm = np.interp(np.degrees(alpha), alpha_cm, cm_ext) + return type(self)(self.Re, np.degrees(alpha), cl, cd, cm) + + + + + def __Viterna(self, alpha, cl_adj): + """private method to perform Viterna extrapolation""" + + alpha = np.maximum(alpha, 0.0001) # prevent divide by zero + + cl = self.cdmax / 2 * np.sin(2 * alpha) + self.A * np.cos(alpha) ** 2 / np.sin(alpha) + cl = cl * cl_adj + + cd = self.cdmax * np.sin(alpha) ** 2 + self.B * np.cos(alpha) + + return cl, cd + + def __CMCoeff(self, cl_high, cd_high, cm_high): + """private method to obtain CM0 and CMCoeff""" + + found_zero_lift = False + + for i in range(len(self.cm) - 1): + if abs(self.alpha[i]) < 20.0 and self.cl[i] <= 0 and self.cl[i + 1] >= 0: + p = -self.cl[i] / (self.cl[i + 1] - self.cl[i]) + cm0 = self.cm[i] + p * (self.cm[i + 1] - self.cm[i]) + found_zero_lift = True + break + + if not found_zero_lift: + p = -self.cl[0] / (self.cl[1] - self.cl[0]) + cm0 = self.cm[0] + p * (self.cm[1] - self.cm[0]) + self.cm0 = cm0 + alpha_high = np.radians(self.alpha[-1]) + XM = (-cm_high + cm0) / (cl_high * np.cos(alpha_high) + cd_high * np.sin(alpha_high)) + cmCoef = (XM - 0.25) / np.tan((alpha_high - np.pi / 2)) + return cmCoef + + def __getCM(self, i, cmCoef, alpha, cl_ext, cd_ext, alpha_low_deg, alpha_high_deg): + """private method to extrapolate Cm""" + + cm_new = 0 + if alpha[i] >= alpha_low_deg and alpha[i] <= alpha_high_deg: + return + if alpha[i] > -165 and alpha[i] < 165: + if abs(alpha[i]) < 0.01: + cm_new = self.cm0 + else: + if alpha[i] > 0: + x = cmCoef * np.tan(np.radians(alpha[i]) - np.pi / 2) + 0.25 + cm_new = self.cm0 - x * ( + cl_ext[i] * np.cos(np.radians(alpha[i])) + cd_ext[i] * np.sin(np.radians(alpha[i])) + ) + else: + x = cmCoef * np.tan(-np.radians(alpha[i]) - np.pi / 2) + 0.25 + cm_new = -( + self.cm0 + - x * (-cl_ext[i] * np.cos(-np.radians(alpha[i])) + cd_ext[i] * np.sin(-np.radians(alpha[i]))) + ) + else: + if alpha[i] == 165: + cm_new = -0.4 + elif alpha[i] == 170: + cm_new = -0.5 + elif alpha[i] == 175: + cm_new = -0.25 + elif alpha[i] == 180: + cm_new = 0 + elif alpha[i] == -165: + cm_new = 0.35 + elif alpha[i] == -170: + cm_new = 0.4 + elif alpha[i] == -175: + cm_new = 0.2 + elif alpha[i] == -180: + cm_new = 0 + else: + print("Angle encountered for which there is no CM table value " "(near +/-180 deg). Program will stop.") + return cm_new + + def unsteadyParams(self, window_offset=None, nMin=720): + """compute unsteady aero parameters used in AeroDyn input file + + TODO Questions to solve: + - Is alpha 0 defined at zero lift or zero Cn? + - Are Cn1 and Cn2 the stall points of Cn or the regions where Cn deviates from the linear region? + - Is Cd0 Cdmin? + - Should Cd0 be used in cn? + - Should the TSE points be used? + - If so, should we use the linear points or the points on the cn-curve + - Should we prescribe alpha0cn when determining the slope? + NOTE: + alpha0Cl and alpha0Cn are usually within 0.005 deg of each other, less thatn 0.3% difference, with alpha0Cn > alpha0Cl. The difference increase thought towards the root of the blade + + Using the f=0.7 points doesnot change much for the lower point + but it has quite an impact on the upper point + % + + Parameters + ---------- + window_dalpha0: the linear region will be looked for in the region alpha+window_offset + + Returns + ------- + alpha0 : lift or 0 cn (TODO TODO) angle of attack (deg) + alpha1 : angle of attack at f=0.7 (approximately the stall angle) for AOA>alpha0 (deg) + alpha2 : angle of attack at f=0.7 (approximately the stall angle) for AOA= alpha_linear_min, + alpha <= alpha_linear_max) + + # checks for inppropriate data (like cylinders) + if len(idx) < 10 or len(np.unique(cl)) < 10: + return 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,0.0 + + # linear fit + p = np.polyfit(alpha[idx], cn[idx], 1) + m = p[0] + alpha0 = -p[1]/m + + # find cn at "stall onset" locations, when cn deviates from the linear region + alphaUpper = np.radians(np.arange(40.0)) + alphaLower = np.radians(np.arange(5.0, -40.0, -1)) + cnUpper = np.interp(alphaUpper, alpha, cn) + cnLower = np.interp(alphaLower, alpha, cn) + cnLinearUpper = m*(alphaUpper - alpha0) + cnLinearLower = m*(alphaLower - alpha0) + deviation = 0.05 # threshold for cl in detecting stall + + alphaU = np.interp(deviation, cnLinearUpper-cnUpper, alphaUpper) + alphaL = np.interp(deviation, cnLower-cnLinearLower, alphaLower) + + # compute cn at stall according to linear fit + cnStallUpper = m*(alphaU-alpha0) + cnStallLower = m*(alphaL-alpha0) + + # find min cd + minIdx = cd.argmin() + + # return: control setting, stall angle, alpha for 0 cn, cn slope, + # cn at stall+, cn at stall-, alpha for min CD, min(CD) + return (0.0, np.degrees(alphaU), np.degrees(alpha0), m, + cnStallUpper, cnStallLower, alpha[minIdx], cd[minIdx]) + + def plot(self): + """plot cl/cd/cm polar + + Returns + ------- + figs : list of figure handles + + """ + import matplotlib.pyplot as plt + + p = self + + figs = [] + + # plot cl + fig = plt.figure() + figs.append(fig) + ax = fig.add_subplot(111) + plt.plot(p.alpha, p.cl, label="Re = " + str(p.Re / 1e6) + " million") + ax.set_xlabel("angle of attack (deg)") + ax.set_ylabel("lift coefficient") + ax.legend(loc="best") + + # plot cd + fig = plt.figure() + figs.append(fig) + ax = fig.add_subplot(111) + ax.plot(p.alpha, p.cd, label="Re = " + str(p.Re / 1e6) + " million") + ax.set_xlabel("angle of attack (deg)") + ax.set_ylabel("drag coefficient") + ax.legend(loc="best") + + # plot cm + fig = plt.figure() + figs.append(fig) + ax = fig.add_subplot(111) + ax.plot(p.alpha, p.cm, label="Re = " + str(p.Re / 1e6) + " million") + ax.set_xlabel("angle of attack (deg)") + ax.set_ylabel("moment coefficient") + ax.legend(loc="best") + + return figs + + def alpha0(self, window=None): + """ Finds alpha0, angle of zero lift """ + if window is None: + if self._radians: + window = [np.radians(-30), np.radians(30)] + else: + window = [-30, 30] + window = _alpha_window_in_bounds(self.alpha, window) + # print(window) + # print(self.alpha) + # print(self._radians) + # print(self.cl) + # print(window) + + return _find_alpha0(self.alpha, self.cl, window) + + def linear_region(self, delta_alpha0=4, method_linear_fit="max"): + cl_slope, alpha0 = self.cl_linear_slope() + alpha_linear_region = np.asarray(_find_TSE_region(self.alpha, self.cl, cl_slope, alpha0, deviation=0.05)) + cl_linear_region = (alpha_linear_region - alpha0) * cl_slope + + return alpha_linear_region, cl_linear_region, cl_slope, alpha0 + + def cl_max(self, window=None): + """ Finds cl_max , returns (Cl_max,alpha_max) """ + if window is None: + if self._radians: + window = [np.radians(-40), np.radians(40)] + else: + window = [-40, 40] + + # Constant case or only one value + if np.all(self.cl == self.cl[0]) or len(self.cl) == 1: + return self.cl, self.alpha + + # Ensuring window is within our alpha values + window = _alpha_window_in_bounds(self.alpha, window) + + # Finding max within window + iwindow = np.where((self.alpha >= window[0]) & (self.alpha <= window[1])) + alpha = self.alpha[iwindow] + cl = self.cl[iwindow] + i_max = np.argmax(cl) + if i_max == len(iwindow): + raise Exception( + "Max cl is at the window boundary ([{};{}]), increase window (TODO automatically)".format( + window[0], window[1] + ) + ) + pass + cl_max = cl[i_max] + alpha_cl_max = alpha[i_max] + # alpha_zc,i_zc = _zero_crossings(x=alpha,y=cl,direction='up') + # if len(alpha_zc)>1: + # raise Exception('Cannot find alpha0, {} zero crossings of Cl in the range of alpha values: [{} {}] '.format(len(alpha_zc),window[0],window[1])) + # elif len(alpha_zc)==0: + # raise Exception('Cannot find alpha0, no zero crossing of Cl in the range of alpha values: [{} {}] '.format(window[0],window[1])) + # + # alpha0=alpha_zc[0] + return cl_max, alpha_cl_max + + + def cl_linear_slope(self, window=None, method="optim", radians=False): + """Find slope of linear region + Outputs: a 2-tuplet of: + slope (in inverse units of alpha, or in radians-1 if radians=True) + alpha_0 in the same unit as alpha, or in radians if radians=True + """ + return cl_linear_slope(self.alpha, self.cl, window=window, method=method, inputInRadians=self._radians, radians=radians) + + def cl_fully_separated(self, method='max'): + alpha0 = self.alpha0() + cla,_, = self.cl_linear_slope(method=method) + if cla == 0: + cl_fs = self.cl # when fs ==1 + fs = self.cl * 0 + else: + cl_ratio = self.cl / (cla * (self.alpha - alpha0)) + cl_ratio[np.where(cl_ratio < 0)] = 0 + if self._fs_lock: + fs = self.fs + else: + fs = (2 * np.sqrt(cl_ratio) - 1) ** 2 + fs[np.where(fs < 1e-15)] = 0 + # Initialize to linear region (in fact only at singularity, where fs=1) + cl_fs = self.cl / 2.0 # when fs ==1 + # Region where fs<1, merge + I = np.where(fs < 1) + cl_fs[I] = (self.cl[I] - cla * (self.alpha[I] - alpha0) * fs[I]) / (1.0 - fs[I]) + # Outside region, use steady data + iHig = np.ma.argmin(np.ma.MaskedArray(fs, self.alpha < alpha0)) + iLow = np.ma.argmin(np.ma.MaskedArray(fs, self.alpha > alpha0)) + cl_fs[0 : iLow + 1] = self.cl[0 : iLow + 1] + cl_fs[iHig + 1 : -1] = self.cl[iHig + 1 : -1] + + # Ensuring everything is consistent (but we cant with user provided values..) + cl_inv = cla * (self.alpha - alpha0) + if not self._fs_lock: + fs = (self.cl - cl_fs) / (cl_inv - cl_fs + 1e-10) + fs[np.where(fs < 1e-15)] = 0 + fs[np.where(fs > 1)] = 1 + # Storing + self.fs = fs + self.cl_fs = cl_fs + if not self._cl_inv_lock: + self.cl_inv = cl_inv + return cl_fs, fs + + def dynaStallOye_DiscreteStep(self, alpha_t, tau, fs_prev, dt): + # compute aerodynamical force from aerodynamic data + # interpolation from data + fs = self.fs_interp(alpha_t) + Clinv = self.cl_inv_interp(alpha_t) + Clfs = self.cl_fs_interp(alpha_t) + # dynamic stall model + fs_dyn = fs + (fs_prev - fs) * np.exp(-dt / tau) + Cl = fs_dyn * Clinv + (1 - fs_dyn) * Clfs + return Cl, fs_dyn + + def toAeroDyn(self, filenameOut=None, templateFile=None, Re=1.0, comment=None, unsteadyParams=True): + from openfast_toolbox.io.fast_input_file import ADPolarFile + cleanComments=comment is not None + # Read a template file for AeroDyn polars + if templateFile is None: + MyDir=os.path.dirname(__file__) + templateFile = os.path.join(MyDir,'../../data/NREL5MW/5MW_Baseline/Airfoils/Cylinder1.dat') + cleanComments=True + if isinstance(templateFile, ADPolarFile): + ADpol = templateFile + else: + ADpol = ADPolarFile(templateFile) + + + + # --- Updating the AD polar file + ADpol['Re'] = Re # TODO UNKNOWN + + # Compute unsteady parameters + if unsteadyParams: + (alpha0,alpha1,alpha2,cnSlope,cn1,cn2,cd0,cm0)=self.unsteadyParams() + + # Setting unsteady parameters + if np.isnan(alpha0): + ADpol['alpha0'] = 0 + else: + ADpol['alpha0'] = np.around(alpha0, 4) + ADpol['alpha1'] = np.around(alpha1, 4) # TODO approximate + ADpol['alpha2'] = np.around(alpha2, 4) # TODO approximate + ADpol['C_nalpha'] = np.around(cnSlope ,4) + ADpol['Cn1'] = np.around(cn1, 4) # TODO verify + ADpol['Cn2'] = np.around(cn2, 4) + ADpol['Cd0'] = np.around(cd0, 4) + ADpol['Cm0'] = np.around(cm0, 4) + + # Setting polar + PolarTable = np.column_stack((self.alpha, self.cl, self.cd, self.cm)) + ADpol['NumAlf'] = self.cl.shape[0] + ADpol['AFCoeff'] = np.around(PolarTable, 5) + + # --- Comment + if cleanComments: + ADpol.comment='' # remove comment from template + if comment is not None: + ADpol.comment=comment + + if filenameOut is not None: + ADpol.write(filenameOut) + return ADpol + + + + +def blend(pol1, pol2, weight): + """Blend this polar with another one with the specified weighting + + Parameters + ---------- + pol1: (class Polar or array) first polar + pol2: (class Polar or array) second polar + weight: (float) blending parameter between 0 (first polar) and 1 (second polar) + + Returns + ------- + polar : (class Polar or array) a blended Polar + """ + bReturnObject = False + if hasattr(pol1, "cl"): + bReturnObject = True + alpha1 = pol1.alpha + M1 = np.zeros((len(alpha1), 4)) + M1[:, 0] = pol1.alpha + M1[:, 1] = pol1.cl + M1[:, 2] = pol1.cd + M1[:, 3] = pol1.cm + else: + alpha1 = pol1[:, 0] + M1 = pol1 + if hasattr(pol2, "cl"): + bReturnObject = True + alpha2 = pol2.alpha + M2 = np.zeros((len(alpha2), 4)) + M2[:, 0] = pol2.alpha + M2[:, 1] = pol2.cl + M2[:, 2] = pol2.cd + M2[:, 3] = pol2.cm + else: + alpha2 = pol2[:, 0] + M2 = pol2 + # Define range of alpha, merged values and truncate if one set beyond the other range + alpha = np.union1d(alpha1, alpha2) + min_alpha = max(alpha1.min(), alpha2.min()) + max_alpha = min(alpha1.max(), alpha2.max()) + alpha = alpha[np.logical_and(alpha >= min_alpha, alpha <= max_alpha)] + # alpha = np.array([a for a in alpha if a >= min_alpha and a <= max_alpha]) + + # Creating new output matrix to store polar + M = np.zeros((len(alpha), M1.shape[1])) + M[:, 0] = alpha + + # interpolate to new alpha and linearly blend + for j in np.arange(1, M.shape[1]): + v1 = np.interp(alpha, alpha1, M1[:, j]) + v2 = np.interp(alpha, alpha2, M2[:, j]) + M[:, j] = (1 - weight) * v1 + weight * v2 + if hasattr(pol1, "Re"): + Re = pol1.Re + weight * (pol2.Re - pol1.Re) + else: + Re = np.nan + + if bReturnObject: + return type(pol1)(Re=Re, alpha=M[:, 0], cl=M[:, 1], cd=M[:, 2], cm=M[:, 3]) + else: + return M + + +def thicknessinterp_from_one_set(thickness, polarList, polarThickness): + """Returns a set of interpolated polars from one set of polars at known thicknesses and a list of thickness + The nearest polar is used when the thickness is beyond the range of values of the input polars. + """ + thickness = np.asarray(thickness) + polarThickness = np.asarray(polarThickness) + polarList = np.asarray(polarList) + tmax_in = np.max(thickness) + tmax_pol = np.max(polarThickness) + if (tmax_in > 1.2 and tmax_pol <= 1.2) or (tmax_in <= 1.2 and tmax_pol > 1.2): + raise Exception( + "Thicknesses of polars and input thickness need to be both in percent ([0-120]) or in fraction ([0-1.2])" + ) + + # sorting thickness + Isort = np.argsort(polarThickness) + polarThickness = polarThickness[Isort] + polarList = polarList[Isort] + + polars = [] + for it, t in enumerate(thickness): + ihigh = len(polarThickness) - 1 + for ip, tp in enumerate(polarThickness): + if tp > t: + ihigh = ip + break + ilow = 0 + for ip, tp in reversed(list(enumerate(polarThickness))): + if tp < t: + ilow = ip + break + + if ihigh == ilow: + polars.append(polarList[ihigh]) + print("[WARN] Using nearest polar for section {}, t={} , t_near={}".format(it, t, polarThickness[ihigh])) + else: + if (polarThickness[ilow] > t) or (polarThickness[ihigh] < t): + raise Exception("Implementation Error") + weight = (t - polarThickness[ilow]) / (polarThickness[ihigh] - polarThickness[ilow]) + # print(polarThickness[ilow],'<',t,'<',polarThickness[ihigh],'Weight',weight) + pol = blend(polarList[ilow], polarList[ihigh], weight) + polars.append(pol) + # import matplotlib.pyplot as plt + # fig=plt.figure() + # plt.plot(polarList[ilow][: ,0],polarList[ilow][: ,2],'b',label='thick'+str(polarThickness[ilow])) + # plt.plot(pol[:,0],pol[:,2],'k--',label='thick'+str(t)) + # plt.plot(polarList[ihigh][:,0],polarList[ihigh][:,2],'r',label='thick'+str(polarThickness[ihigh])) + # plt.legend() + # plt.show() + return polars + + +def _alpha_window_in_bounds(alpha, window): + """Ensures that the window of alpha values is within the bounds of alpha + Example: alpha in [-30,30], window=[-20,20] => window=[-20,20] + Example: alpha in [-10,10], window=[-20,20] => window=[-10,10] + Example: alpha in [-30,30], window=[-40,10] => window=[-40,10] + """ + IBef = np.where(alpha <= window[0])[0] + if len(IBef) > 0: + im = IBef[-1] + else: + im = 0 + IAft = np.where(alpha >= window[1])[0] + if len(IAft) > 0: + ip = IAft[0] + else: + ip = len(alpha) - 1 + window = [alpha[im], alpha[ip]] + return window + + +def _find_alpha0(alpha, coeff, window, direction='up', value_if_constant = np.nan): + """Finds the point where coeff(alpha)==0 using interpolation. + The search is narrowed to a window that can be specified by the user. The default window is yet enough for cases that make physical sense. + The angle alpha0 is found by looking at a zero up crossing in this window, and interpolation is used to find the exact location. + """ + # Constant case or only one value + if np.all(abs((coeff - coeff[0])<1e-8)) or len(coeff) == 1: + if coeff[0] == 0: + return 0 + else: + return value_if_constant + # Ensuring window is within our alpha values + window = _alpha_window_in_bounds(alpha, window) + + # Finding zero up-crossing within window + iwindow = np.where((alpha >= window[0]) & (alpha <= window[1])) + alpha = alpha[iwindow] + coeff = coeff[iwindow] + alpha_zc, i_zc, s_zc = _zero_crossings(x=alpha, y=coeff, direction=direction) + + if len(alpha_zc) > 1: + print('WARN: Cannot find alpha0, {} zero crossings of Coeff in the range of alpha values: [{} {}] '.format(len(alpha_zc),window[0],window[1])) + print('>>> Using second zero') + alpha_zc=alpha_zc[1:] + #raise Exception('Cannot find alpha0, {} zero crossings of Coeff in the range of alpha values: [{} {}] '.format(len(alpha_zc),window[0],window[1])) + elif len(alpha_zc) == 0: + raise NoCrossingException('Cannot find alpha0, no zero crossing of Coeff in the range of alpha values: [{} {}] '.format(window[0],window[1])) + + alpha0 = alpha_zc[0] + return alpha0 + + +def _find_TSE_region(alpha, coeff, slope, alpha0, deviation): + """Find the Trailing Edge Separation points, when the coefficient separates from its linear region + These points are defined as the points where the difference is equal to +/- `deviation` + Typically deviation is about 0.05 (absolute value) + The linear region is defined as coeff_lin = slope (alpha-alpha0) + + returns: + a_TSE: values of alpha at the TSE point (upper and lower) + + """ + # How off are we from the linear region + DeltaLin = slope * (alpha - alpha0) - coeff + + # Upper and lower regions + bUpp = alpha >= alpha0 + bLow = alpha <= alpha0 + + # Finding the point where the delta is equal to `deviation` + a_TSEUpp = np.interp(deviation, DeltaLin[bUpp], alpha[bUpp]) + a_TSELow = np.interp(-deviation, DeltaLin[bLow], alpha[bLow]) + return a_TSELow, a_TSEUpp + + +def _find_max_points(alpha, coeff, alpha0, method="inflections"): + """Find upper and lower max points in `coeff` vector. + if `method` is "inflection": + These point are detected from slope changes of `coeff`, positive of negative inflections + The upper stall point is the first point after alpha0 with a "hat" inflection + The lower stall point is the first point below alpha0 with a "v" inflection + """ + if method == "inflections": + dC = np.diff(coeff) + IHatInflections = np.where(np.logical_and.reduce((dC[1:] < 0, dC[0:-1] > 0, alpha[1:-1] > alpha0)))[0] + IVeeInflections = np.where(np.logical_and.reduce((dC[1:] > 0, dC[0:-1] < 0, alpha[1:-1] < alpha0)))[0] + if len(IHatInflections) <= 0: + raise NoStallDetectedException("Not able to detect upper stall point of curve") + if len(IVeeInflections) <= 0: + raise NoStallDetectedException("Not able to detect lower stall point of curve") + a_MaxUpp = alpha[IHatInflections[0] + 1] + c_MaxUpp = coeff[IHatInflections[0] + 1] + a_MaxLow = alpha[IVeeInflections[-1] + 1] + c_MaxLow = coeff[IVeeInflections[-1] + 1] + elif method == "minmax": + iMax = np.argmax(coeff) + iMin = np.argmin(coeff) + a_MaxUpp = alpha[iMax] + c_MaxUpp = coeff[iMax] + a_MaxLow = alpha[iMin] + c_MaxLow = coeff[iMin] + else: + raise NotImplementedError() + return (a_MaxUpp, c_MaxUpp, a_MaxLow, c_MaxLow) + + + + +# --------------------------------------------------------------------------------} +# --- Low-level functions +# --------------------------------------------------------------------------------{ +def fn_fullsep(alpha, dclda, alpha0, alpha_sl_neg, alpha_sl_pos, valpha, vCl): + """ Function that is zero when f=0 from the Kirchhoff theory """ + cl_linear = cl_lin(alpha, dclda, alpha0, alpha_sl_neg, alpha_sl_pos, valpha, vCl) + return cl_linear - 0.25* dclda*(alpha-alpha0) + +def cl_lin(alpha, dclda, alpha0, alpha_sl_neg, alpha_sl_pos, valpha, vCl): + """ Linear Cl """ + if alpha > alpha_sl_neg and alpha < alpha_sl_pos : + cl = dclda*(alpha-alpha0) + else: + cl=np.interp(alpha, valpha, vCl) + return cl + +def cl_fullsep(alpha, dclda, alpha0, alpha_sl_neg, alpha_sl_pos, valpha, vCl, alpha_fs_l, alpha_fs_u): + """ fully separated lift coefficient""" + cl_linear = cl_lin(alpha, dclda, alpha0, alpha_sl_neg, alpha_sl_pos, valpha, vCl) + if alpha > alpha_sl_neg and alpha < alpha_sl_pos : + cl = cl_linear*0.5 + else: + fp = f_point(alpha, dclda, alpha0, alpha_sl_neg, alpha_sl_pos, valpha, vCl, alpha_fs_l, alpha_fs_u) + cl=(cl_linear- dclda*(alpha-alpha0)*fp)/(1-fp) + return cl + +def f_point(alpha, dclda, alpha0, alpha_sl_neg, alpha_sl_pos, valpha, vCl, alpha_fs_l, alpha_fs_u): + """ separation function + # TODO harmonize with cl_fully_separated maybe? + """ + if dclda==0: + return 0 + if alpha < alpha_fs_l or alpha > alpha_fs_u: + return 0 + else: + cl_linear = cl_lin(alpha, dclda, alpha0, alpha_sl_neg, alpha_sl_pos, valpha, vCl) + xx=cl_linear/(dclda*(alpha-alpha0) + np.sign(cl_linear)*1e-15) + return (2*np.sqrt(xx)-1)**2 + + +def polar_params(alpha, cl, cd, cm): + """ + - alpha in radians + """ + # Treat zero-lift sections separately + zero_offset=1e-15 + x1 = -7*np.pi/180 + x2 = 7*np.pi/180 + y1 = np.interp(x1, alpha, cl) + y2 = np.interp(x2, alpha, cl) + + if (y1maxclp: + alpha_maxclp=alpha_sl_pos + maxclp=dclda + relerr=0. + for k in np.arange(nobs): + x1 = alpha0+(k+1)*dalpha + y1 = np.interp(x1, alpha, cl) + y2 = dclda*(x1-alpha0) + relerr=relerr+(y1-y2)/y2 + relerr=relerr/nobs + move_up= relerr>1e-2 + if move_up: + alpha_sl_pos=alpha_sl_pos-dalpha + alpha_sl_pos=max(alpha_maxclp, alpha_sl_pos) + + # Find negative angle of attack stall limit alpha_sl_neg + alpha_sl_neg=-20*np.pi/180 + move_down=True + maxclp=0. + while move_down: + dalpha=(alpha0-alpha_sl_neg)/nobs + y1=np.interp(alpha_sl_neg, alpha, cl) + dclda=y1/(alpha_sl_neg-alpha0) + if dclda>maxclp: + alpha_maxclp=alpha_sl_neg + maxclp=dclda + relerr=0. + for k in np.arange(nobs): + x1=alpha_sl_neg+(k+1)*dalpha + y1 = np.interp(x1, alpha, cl) + y2=dclda*(x1-alpha0) + relerr=relerr+(y1-y2)/y2 + relerr=relerr/nobs + move_down=relerr>1e-2 + if move_down: + alpha_sl_neg=alpha_sl_neg+dalpha + alpha_sl_neg=min(alpha_maxclp, alpha_sl_neg) + + # Compute the final alpha and dclda (linear lift coefficient slope) values + y1=np.interp(alpha_sl_neg, alpha, cl) + y2=np.interp(alpha_sl_pos, alpha, cl) + alpha0=(y1*alpha_sl_pos-y2*alpha_sl_neg)/(y1-y2) + dclda =(y1-y2)/(alpha_sl_neg-alpha_sl_pos) + + # Find Cd0 and Cm0 + Cd0 = np.interp(alpha0, alpha, cd) + Cm0 = np.interp(alpha0, alpha, cm) + # Find upper surface fully stalled angle of attack alpha_fs_u (Upper limit of the Kirchhoff flat plate solution) + y1=-1. + y2=-1. + delta = np.pi/180 + x2=alpha_sl_pos + delta + while y1*y2>0. and x2+delta0. and x2-delta>-np.pi: + x1=x2 + x2=x1-delta + y1=fn_fullsep(x1, dclda, alpha0, alpha_sl_neg, alpha_sl_pos, alpha, cl) + y2=fn_fullsep(x2, dclda, alpha0, alpha_sl_neg, alpha_sl_pos, alpha, cl) + if y1*y2<0: + alpha_fs_l=(0.-y2)/(y1-y2)*x1+(0.-y1)/(y2-y1)*x2 + else: + alpha_fs_l=-np.pi + + # --- Compute values at all angle of attack + Cl_fully_sep = np.zeros(alpha.shape) + fs = np.zeros(alpha.shape) + Cl_linear = np.zeros(alpha.shape) + for i,al in enumerate(alpha): + Cl_fully_sep[i] = cl_fullsep(al, dclda, alpha0, alpha_sl_neg, alpha_sl_pos, alpha, cl, alpha_fs_l, alpha_fs_u) + fs [i] = f_point (al, dclda, alpha0, alpha_sl_neg, alpha_sl_pos, alpha, cl, alpha_fs_l, alpha_fs_u) + Cl_linear [i] = cl_lin (al, dclda, alpha0, alpha_sl_neg, alpha_sl_pos, alpha, cl) + + p=dict() + p['alpha0'] = alpha0 + p['Cd0'] = Cd0 + p['Cm0'] = Cm0 + p['dclda'] = dclda + p['alpha_fs_l'] = alpha_fs_l + p['alpha_fs_u'] = alpha_fs_u + p['alpha_sl_neg'] = alpha_sl_neg + p['alpha_sl_pos'] = alpha_sl_pos + + return p, Cl_linear, Cl_fully_sep, fs + + + +def cl_linear_slope(alpha, cl, window=None, method="max", nInterp=721, inputInRadians=False, radians=False): + """ + Find slope of linear region + Outputs: a 2-tuplet of: + slope (in inverse units of alpha, or in radians-1 if radians=True) + alpha_0 in the same unit as alpha, or in radians if radians=True + + + INPUTS: + - alpha: angle of attack in radians + - Cl : lift coefficient + - window: [alpha_min, alpha_max]: region when linear slope is sought + - method: 'max', 'optim', 'leastsquare', 'leastsquare_constraint' + + OUTPUTS: + - Cl_alpha, alpha0: lift slope (1/rad) and angle of attack (rad) of zero lift + """ + # --- Return function + def myret(sl, a0): + # wrapper function to return degrees or radians # TODO this should be a function of self._radians + if radians: + if inputInRadians: + return sl, a0 + else: + return np.rad2deg(sl), np.deg2rad(a0) # NOTE: slope needs rad2deg, alpha needs deg2rad + else: + return sl, a0 + + # finding alpha0 # TODO TODO TODO THIS IS NOT NECESSARY + if inputInRadians: + windowAlpha0 = [np.radians(-30), np.radians(30)] + else: + windowAlpha0 = [-30, 30] + windowAlpha0 = _alpha_window_in_bounds(alpha, windowAlpha0) + alpha0 = _find_alpha0(alpha, cl, windowAlpha0) + + # Constant case or only one value + if np.all(cl == cl[0]) or len(cl) == 1: + return myret(0, alpha0) + + if window is None: + if np.nanmin(cl) > 0 or np.nanmax(cl) < 0: + window = [alpha[0], alpha[-1]] + else: + # define a window around alpha0 + if inputInRadians: + window = alpha0 + np.radians(np.array([-5, +20])) + else: + window = alpha0 + np.array([-5, +20]) + + # Ensuring window is within our alpha values + window = _alpha_window_in_bounds(alpha, window) + + if method in ["max", "leastsquare"]: + slope, off = _find_slope(alpha, cl, xi=alpha0, window=window, method=method) + + elif method == "leastsquare_constraint": + slope, off = _find_slope(alpha, cl, x0=alpha0, window=window, method="leastsquare") + + elif method == "optim": + # Selecting range of values within window + idx = np.where((alpha >= window[0]) & (alpha <= window[1]) & ~np.isnan(cl))[0] + cl, alpha = cl[idx], alpha[idx] + # Selecting within the min and max of this window to improve accuracy + imin = np.where(cl == np.min(cl))[0][-1] + idx = np.arange(imin, np.argmax(cl) + 1) + window = [alpha[imin], alpha[np.argmax(cl)]] + cl, alpha = cl[idx], alpha[idx] + # Performing minimization of slope + slope, off = _find_slope(alpha, cl, x0=alpha0, window=None, method="optim") + + else: + raise Exception("Method unknown for lift slope determination: {}".format(method)) + + # --- Safety checks + if len(cl) > 10: + # Looking at slope around alpha 0 to see if we are too far off + slope_FD, off_FD = _find_slope(alpha, cl, xi=alpha0, window=window, method="finitediff_1c") + if abs(slope - slope_FD) / slope_FD * 100 > 50: + #raise Exception('Warning: More than 20% error between estimated slope ({:.4f}) and the slope around alpha0 ({:.4f}). The window for the slope search ([{} {}]) is likely wrong.'.format(slope,slope_FD,window[0],window[-1])) + print('[WARN] More than 20% error between estimated slope ({:.4f}) and the slope around alpha0 ({:.4f}). The window for the slope search ([{} {}]) is likely wrong.'.format(slope,slope_FD,window[0],window[-1])) +# print('slope ',slope,' Alpha range: {:.3f} {:.3f} - nLin {} nMin {} nMax {}'.format(alpha[iStart],alpha[iEnd],len(alpha[iStart:iEnd+1]),nMin,len(alpha))) + return myret(slope, off) + +# --------------------------------------------------------------------------------} +# --- Generic curve handling functions +# --------------------------------------------------------------------------------{ +def _find_slope(x, y, xi=None, x0=None, window=None, method="max", opts=None, nInterp=721): + """Find the slope of a curve at x=xi based on a given method. + INPUTS: + x: array of x values + y: array of y values + xi: point where the slope is to be computed + x0: point where y(x0)=0 + if provided the constraint y(x0)=0 is added. + window: + If a `window` is provided the search is restrained to this region of x values. + Typical windows for airfoils are: window=[alpha0,Clmax], or window=[-5,5]+alpha0 + If window is None, the whole extent is used (window=[min(x),max(x)]) + + The methods available are: + 'max' : returns the maximum slope within the window. Needs `xi` + 'leastsquare': use leastsquare (or polyfit), to fit the curve within the window + 'finitediff_1c': first order centered finite difference. Needs `xi` + 'optim': find the slope by looking at all possible slope values, and try to find an optimal where the length of linear region is maximized. + + returns: + (a,x0): such that the slope is a(x-x0) + (x0=-b/a where y=ax+b) + """ + if window is not None: + x_=x + y_=y + if nInterp is not None: + x_ = np.linspace(x[0], x[-1], max(nInterp,len(x))) # using 0.5deg resolution at least + y_ = np.interp(x_, x, y) + I = np.where(np.logical_and(x_>=window[0],x_<=window[1])) + x = x_[I] + y = y_[I] + + + if len(y) <= 1: + raise Exception('Cannot find slope, two points needed ({} after window selection)'.format(len(y))) + + + if len(y)<4 and method=='optim': + method='leastsquare' + #print('[WARN] Not enought data to find slope with optim method, using leastsquare') + + + if method == "max": + if xi is not None: + I = np.nonzero(x - xi) + yi = np.interp(xi, x, y) + a = max((y[I] - yi) / (x[I] - xi)) + x0 = xi - yi / a + else: + raise Exception("For now xi needs to be set to find a slope with the max method") + + elif method == "finitediff_1c": + # First order centered finite difference + if xi is not None: + # First point strictly before xi + im = np.where(x < xi)[0][-1] + dx = x[im + 1] - x[im - 1] + if np.abs(dx) > 1e-7: + a = (y[im + 1] - y[im - 1]) / dx + yi = np.interp(xi, x, y) + x0 = xi - yi / a + else: + a = np.inf + x0 = xi + #print('a',a) + #print('x0',x0) + #print('yi',yi) + dx=(x[im+1]-x[im]) + if np.abs(dx)>1e-7: + a = ( y[im+1] - y[im] ) / dx + yi = np.interp(xi,x,y) + x0 = xi - yi/a + else: + a=np.inf + x0 = xi + #print('a',a) + #print('x0',x0) + #print('yi',yi) + else: + raise Exception("For now xi needs to be set to find a slope with the finite diff method") + + elif method == "leastsquare": + if x0 is not None: + try: + a = np.linalg.lstsq((x - x0).reshape((-1, 1)), y.reshape((-1, 1)), rcond=None)[0][0][0] + except: + a = np.linalg.lstsq((x - x0).reshape((-1, 1)), y.reshape((-1, 1)))[0][0][0] + else: + p = np.polyfit(x, y, 1) + a = p[0] + x0 = -p[1] / a + elif method == "optim": + if opts is None: + nMin = max(3, int(len(x) / 2)) + else: + nMin = opts["nMin"] + + a, x0, iStart, iEnd = _find_linear_region(x, y, nMin, x0) + + else: + raise NotImplementedError() + return a, x0 + +def _find_linear_region(x, y, nMin, x0=None): + """Find a linear region by computing all possible slopes for all possible extent. + The objective function tries to minimize the error with the linear slope + and maximize the length of the linear region. + nMin is the mimum number of points to be present in the region + If x0 is provided, the function a*(x-x0) is fitted + + returns: + slope : + offset: + iStart: index of start of linear region + iEnd : index of end of linear region + """ + if x0 is not None: + x = x.reshape((-1, 1)) - x0 + y = y.reshape((-1, 1)) + n = len(x) - nMin + 1 + err = np.zeros((n, n)) * np.nan + slp = np.zeros((n, n)) * np.nan + off = np.zeros((n, n)) * np.nan + spn = np.zeros((n, n)) * np.nan + for iStart in range(n): + for j in range(iStart, n): + iEnd = j + nMin + if x0 is not None: + sl = np.linalg.lstsq(x[iStart:iEnd], y[iStart:iEnd], rcond=None)[0][0] + slp[iStart, j] = sl + off[iStart, j] = x0 + y_lin = x[iStart:iEnd] * sl + else: + coefs = np.polyfit(x[iStart:iEnd], y[iStart:iEnd], 1) + slp[iStart, j] = coefs[0] + off[iStart, j] = -coefs[1] / coefs[0] + y_lin = x[iStart:iEnd] * coefs[0] + coefs[1] + err[iStart, j] = np.mean((y[iStart:iEnd] - y_lin) ** 2) + spn[iStart, j] = iEnd - iStart + spn = 1 / (spn - nMin + 1) + err = (err) / (np.nanmax(err)) + obj = np.multiply(spn, err) + obj = err + (iStart, j) = np.unravel_index(np.nanargmin(obj), obj.shape) + iEnd = j + nMin - 1 # note -1 since we return the index here + return slp[iStart, j], off[iStart, j], iStart, iEnd + + +def _zero_crossings(y, x=None, direction=None): + + """ + Find zero-crossing points in a discrete vector, using linear interpolation. + direction: 'up' or 'down', to select only up-crossings or down-crossings + Returns: + x values xzc such that y(yzc)==0 + indexes izc, such that the zero is between y[izc] (excluded) and y[izc+1] (included) + if direction is not provided, also returns: + sign, equal to 1 for up crossing + """ + y = np.asarray(y) + if x is None: + x = np.arange(len(y)) + + deltas = x[1:] - x[0:-1] + if np.any( deltas == 0.0): + I=np.where(deltas==0)[0] + print("[WARN] Some x values are repeated at index {}. Removing them.".format(I)) + x=np.delete(x,I) + y=np.delete(x,I) + if np.any(deltas<0): + raise Exception("x values need to be in ascending order") + + # Indices before zero-crossing + iBef = np.where(y[1:] * y[0:-1] < 0.0)[0] + + # Find the zero crossing by linear interpolation + xzc = x[iBef] - y[iBef] * (x[iBef + 1] - x[iBef]) / (y[iBef + 1] - y[iBef]) + + # Selecting points that are exactly 0 and where neighbor change sign + iZero = np.where(y == 0.0)[0] + iZero = iZero[np.where((iZero > 0) & (iZero < x.size - 1))] + iZero = iZero[np.where(y[iZero - 1] * y[iZero + 1] < 0.0)] + + # Concatenate + xzc = np.concatenate((xzc, x[iZero])) + iBef = np.concatenate((iBef, iZero)) + + # Sort + iSort = np.argsort(xzc) + xzc, iBef = xzc[iSort], iBef[iSort] + + # Return up-crossing, down crossing or both + sign = np.sign(y[iBef + 1] - y[iBef]) + if direction == "up": + I = np.where(sign == 1)[0] + return xzc[I], iBef[I], sign[I] + elif direction == "down": + I = np.where(sign == -1)[0] + return xzc[I], iBef[I], sign[I] + elif direction is not None: + raise Exception("Direction should be either `up` or `down`") + return xzc, iBef, sign + + +def _intersections(x1, y1, x2, y2, plot=False, minDist=1e-6, verbose=False): + """ + INTERSECTIONS Intersections of curves. + Computes the (x,y) locations where two curves intersect. The curves + can be broken with NaNs or have vertical segments. + + Written by: Sukhbinder, https://github.com/sukhbinder/intersection + adapted by E.Branlard to allow for minimum distance between points + License: MIT + usage: + x,y=intersection(x1,y1,x2,y2) + + Example: + a, b = 1, 2 + phi = np.linspace(3, 10, 100) + x1 = a*phi - b*np.sin(phi) + y1 = a - b*np.cos(phi) + + x2=phi + y2=np.sin(phi)+2 + x,y=intersections(x1,y1,x2,y2) + + plt.plot(x1,y1,c='r') + plt.plot(x2,y2,c='g') + plt.plot(x,y,'*k') + plt.show() + + """ + + def _rect_inter_inner(x1, x2): + n1 = x1.shape[0] - 1 + n2 = x2.shape[0] - 1 + X1 = np.c_[x1[:-1], x1[1:]] + X2 = np.c_[x2[:-1], x2[1:]] + S1 = np.tile(X1.min(axis=1), (n2, 1)).T + S2 = np.tile(X2.max(axis=1), (n1, 1)) + S3 = np.tile(X1.max(axis=1), (n2, 1)).T + S4 = np.tile(X2.min(axis=1), (n1, 1)) + return S1, S2, S3, S4 + + def _rectangle_intersection_(x1, y1, x2, y2): + S1, S2, S3, S4 = _rect_inter_inner(x1, x2) + S5, S6, S7, S8 = _rect_inter_inner(y1, y2) + + C1 = np.less_equal(S1, S2) + C2 = np.greater_equal(S3, S4) + C3 = np.less_equal(S5, S6) + C4 = np.greater_equal(S7, S8) + + ii, jj = np.nonzero(C1 & C2 & C3 & C4) + return ii, jj + + ii, jj = _rectangle_intersection_(x1, y1, x2, y2) + n = len(ii) + + dxy1 = np.diff(np.c_[x1, y1], axis=0) + dxy2 = np.diff(np.c_[x2, y2], axis=0) + + T = np.zeros((4, n)) + AA = np.zeros((4, 4, n)) + AA[0:2, 2, :] = -1 + AA[2:4, 3, :] = -1 + AA[0::2, 0, :] = dxy1[ii, :].T + AA[1::2, 1, :] = dxy2[jj, :].T + + BB = np.zeros((4, n)) + BB[0, :] = -x1[ii].ravel() + BB[1, :] = -x2[jj].ravel() + BB[2, :] = -y1[ii].ravel() + BB[3, :] = -y2[jj].ravel() + + for i in range(n): + try: + T[:, i] = np.linalg.solve(AA[:, :, i], BB[:, i]) + except: + T[:, i] = np.NaN + + in_range = (T[0, :] >= 0) & (T[1, :] >= 0) & (T[0, :] <= 1) & (T[1, :] <= 1) + + xy0 = T[2:, in_range] + xy0 = xy0.T + + x = xy0[:, 0] + y = xy0[:, 1] + + # --- Remove "duplicates" + if minDist is not None: + pointKept=[(x[0],y[0])] + pointSkipped=[] + for p in zip(x[1:],y[1:]): + distances = np.array([np.sqrt((p[0]-pk[0])**2 + (p[1]-pk[1])**2) for pk in pointKept]) + if all(distances>minDist): + pointKept.append((p[0],p[1])) + else: + pointSkipped.append((p[0],p[1])) + if verbose: + if len(pointSkipped)>0: + print('Polar:Intersection:Point Kept :', pointKept) + print('Polar:Intersection:Point Skipped:', pointSkipped) + + M = np.array(pointKept) + x = M[:,0] + y = M[:,1] + if plot: + import matplotlib.pyplot as plt + plt.plot(x1,y1,'.',c='r') + plt.plot(x2,y2,'',c='g') + plt.plot(x,y,'*k') + + return x, y + + +def smooth_heaviside(x, k=1, rng=(-np.inf, np.inf), method="exp"): + r""" + Smooth approximation of Heaviside function where the step occurs between rng[0] and rng[1]: + if rng[0]=rng[1])=1 + if rng[0]>rng[1]: then f(=rng[0])=0 + exp: + rng=(-inf,inf): H(x)=[1 + exp(-2kx) ]^-1 + rng=(-1,1): H(x)=[1 + exp(4kx/(x^2-1) ]^-1 + rng=(0,1): H(x)=[1 + exp(k(2x-1)/(x(x-1)) ]^-1 + INPUTS: + x : scalar or vector of real x values \in ]-infty; infty[ + k : float >=1, the higher k the "steeper" the heaviside function + rng: tuple of min and max value such that f(<=min)=0 and f(>=max)=1. + Reversing the range makes the Heaviside function from 1 to 0 instead of 0 to 1 + method: smooth approximation used (e.g. exp or tan) + NOTE: an epsilon is introduced in the denominator to avoid overflow of the exponentail + """ + if k < 1: + raise Exception("k needs to be >=1") + eps = 1e-2 + mn, mx = rng + x = np.asarray(x) + H = np.zeros(x.shape) + if mn < mx: + H[x <= mn] = 0 + H[x >= mx] = 1 + b = np.logical_and(x > mn, x < mx) + else: + H[x <= mx] = 1 + H[x >= mn] = 0 + b = np.logical_and(x < mn, x > mx) + x = x[b] + if method == "exp": + if np.abs(mn) == np.inf and np.abs(mx) == np.inf: + # Infinite support + x[k * x > 100] = 100.0 / k + x[k * x < -100] = -100.0 / k + if mn < mx: + H[b] = 1 / (1 + np.exp(-k * x)) + else: + H[b] = 1 / (1 + np.exp(k * x)) + elif np.abs(mn) != np.inf and np.abs(mx) != np.inf: + n = 4.0 + # Compact support + s = 2.0 / (mx - mn) * (x - (mn + mx) / 2.0) # transform compact support into ]-1,1[ + x = -n * s / (s ** 2 - 1.0) # then transform ]-1,1[ into ]-inf,inf[ + x[k * x > 100] = 100.0 / k + x[k * x < -100] = -100.0 / k + H[b] = 1.0 / (1 + np.exp(-k * x)) + else: + raise NotImplementedError("Heaviside with only one bound infinite") + else: + # TODO tan approx + raise NotImplementedError() + return H + + +if __name__ == "__main__": + pass diff --git a/pyFAST/airfoils/__init__.py b/openfast_toolbox/airfoils/__init__.py similarity index 100% rename from pyFAST/airfoils/__init__.py rename to openfast_toolbox/airfoils/__init__.py diff --git a/pyFAST/airfoils/data/63-235.csv b/openfast_toolbox/airfoils/data/63-235.csv similarity index 100% rename from pyFAST/airfoils/data/63-235.csv rename to openfast_toolbox/airfoils/data/63-235.csv diff --git a/pyFAST/airfoils/data/Cylinder.csv b/openfast_toolbox/airfoils/data/Cylinder.csv similarity index 100% rename from pyFAST/airfoils/data/Cylinder.csv rename to openfast_toolbox/airfoils/data/Cylinder.csv diff --git a/pyFAST/airfoils/data/Cylinder.dat b/openfast_toolbox/airfoils/data/Cylinder.dat similarity index 100% rename from pyFAST/airfoils/data/Cylinder.dat rename to openfast_toolbox/airfoils/data/Cylinder.dat diff --git a/pyFAST/airfoils/data/DU21_A17.csv b/openfast_toolbox/airfoils/data/DU21_A17.csv similarity index 100% rename from pyFAST/airfoils/data/DU21_A17.csv rename to openfast_toolbox/airfoils/data/DU21_A17.csv diff --git a/pyFAST/airfoils/data/FFA-W3-241-Re12M.dat b/openfast_toolbox/airfoils/data/FFA-W3-241-Re12M.dat similarity index 100% rename from pyFAST/airfoils/data/FFA-W3-241-Re12M.dat rename to openfast_toolbox/airfoils/data/FFA-W3-241-Re12M.dat diff --git a/pyFAST/airfoils/examples/correction3D.py b/openfast_toolbox/airfoils/examples/correction3D.py similarity index 96% rename from pyFAST/airfoils/examples/correction3D.py rename to openfast_toolbox/airfoils/examples/correction3D.py index 6528f27..92ad0bc 100644 --- a/pyFAST/airfoils/examples/correction3D.py +++ b/openfast_toolbox/airfoils/examples/correction3D.py @@ -3,7 +3,7 @@ import pandas as pd import matplotlib.pyplot as plt # Local -from pyFAST.airfoils.Polar import Polar +from openfast_toolbox.airfoils.Polar import Polar MyDir=os.path.dirname(__file__) diff --git a/pyFAST/airfoils/examples/createADPolarFile.py b/openfast_toolbox/airfoils/examples/createADPolarFile.py similarity index 96% rename from pyFAST/airfoils/examples/createADPolarFile.py rename to openfast_toolbox/airfoils/examples/createADPolarFile.py index fe560ca..4ba296a 100644 --- a/pyFAST/airfoils/examples/createADPolarFile.py +++ b/openfast_toolbox/airfoils/examples/createADPolarFile.py @@ -6,9 +6,9 @@ import numpy as np import os -from pyFAST.airfoils.Polar import Polar -from pyFAST.input_output.fast_input_file import FASTInputFile -from pyFAST.input_output.csv_file import CSVFile +from openfast_toolbox.airfoils.Polar import Polar +from openfast_toolbox.io.fast_input_file import FASTInputFile +from openfast_toolbox.io.csv_file import CSVFile # Get current directory so this script can be called from any location scriptDir=os.path.dirname(__file__) diff --git a/pyFAST/airfoils/examples/createADPolarFile_Basic.py b/openfast_toolbox/airfoils/examples/createADPolarFile_Basic.py similarity index 95% rename from pyFAST/airfoils/examples/createADPolarFile_Basic.py rename to openfast_toolbox/airfoils/examples/createADPolarFile_Basic.py index c1b88d6..c24f9db 100644 --- a/pyFAST/airfoils/examples/createADPolarFile_Basic.py +++ b/openfast_toolbox/airfoils/examples/createADPolarFile_Basic.py @@ -10,7 +10,7 @@ scriptDir=os.path.dirname(__file__) # --- Create an AeroDyn polar from a CSV file -from pyFAST.airfoils.Polar import Polar +from openfast_toolbox.airfoils.Polar import Polar polarFile_in = os.path.join(scriptDir,'../data/DU21_A17.csv') polar = Polar(polarFile_in, fformat='delimited') ADpol = polar.toAeroDyn('_AeroDyn_Polar_DU21_A17.dat') @@ -25,7 +25,7 @@ plt.legend() # --- Optional: read the AeroDyn polar -from pyFAST.input_output import FASTInputFile +from openfast_toolbox.io import FASTInputFile ADpol = FASTInputFile('_AeroDyn_Polar_DU21_A17.dat') # --- Plot important data for unsteady aerodynamics diff --git a/pyFAST/airfoils/polar_file.py b/openfast_toolbox/airfoils/polar_file.py similarity index 98% rename from pyFAST/airfoils/polar_file.py rename to openfast_toolbox/airfoils/polar_file.py index 6d4c746..cd72f61 100644 --- a/pyFAST/airfoils/polar_file.py +++ b/openfast_toolbox/airfoils/polar_file.py @@ -11,15 +11,15 @@ # --- Welib polar readers try: - from pyFAST.input_output.csv_file import CSVFile + from openfast_toolbox.io.csv_file import CSVFile except: CSVFile=None try: - from pyFAST.input_output.fast_input_file import ADPolarFile + from openfast_toolbox.io.fast_input_file import ADPolarFile except: ADPolarFile=None try: - from pyFAST.input_output.hawc2_ae_file import HAWC2AEFile + from openfast_toolbox.io.hawc2_ae_file import HAWC2AEFile except: HAWC2AEFile=None diff --git a/pyFAST/airfoils/tests/__init__.py b/openfast_toolbox/airfoils/tests/__init__.py similarity index 100% rename from pyFAST/airfoils/tests/__init__.py rename to openfast_toolbox/airfoils/tests/__init__.py diff --git a/pyFAST/airfoils/tests/test_polar_interp.py b/openfast_toolbox/airfoils/tests/test_polar_interp.py similarity index 97% rename from pyFAST/airfoils/tests/test_polar_interp.py rename to openfast_toolbox/airfoils/tests/test_polar_interp.py index 7f2108b..1946f17 100644 --- a/pyFAST/airfoils/tests/test_polar_interp.py +++ b/openfast_toolbox/airfoils/tests/test_polar_interp.py @@ -2,7 +2,7 @@ import numpy as np import os MyDir=os.path.dirname(__file__) -from pyFAST.airfoils.Polar import * +from openfast_toolbox.airfoils.Polar import * # --------------------------------------------------------------------------------} # --- diff --git a/pyFAST/airfoils/tests/test_polar_manip.py b/openfast_toolbox/airfoils/tests/test_polar_manip.py similarity index 93% rename from pyFAST/airfoils/tests/test_polar_manip.py rename to openfast_toolbox/airfoils/tests/test_polar_manip.py index 327a23c..62690e9 100644 --- a/pyFAST/airfoils/tests/test_polar_manip.py +++ b/openfast_toolbox/airfoils/tests/test_polar_manip.py @@ -2,7 +2,7 @@ import numpy as np import os MyDir=os.path.dirname(__file__) -from pyFAST.airfoils.Polar import * +from openfast_toolbox.airfoils.Polar import * # --------------------------------------------------------------------------------} # --- diff --git a/pyFAST/airfoils/tests/test_polar_params.py b/openfast_toolbox/airfoils/tests/test_polar_params.py similarity index 99% rename from pyFAST/airfoils/tests/test_polar_params.py rename to openfast_toolbox/airfoils/tests/test_polar_params.py index e117d57..f283930 100644 --- a/pyFAST/airfoils/tests/test_polar_params.py +++ b/openfast_toolbox/airfoils/tests/test_polar_params.py @@ -2,7 +2,7 @@ import numpy as np import os MyDir=os.path.dirname(__file__) -from pyFAST.airfoils.Polar import * +from openfast_toolbox.airfoils.Polar import * # --------------------------------------------------------------------------------} # --- diff --git a/pyFAST/airfoils/tests/test_polar_subfunctions.py b/openfast_toolbox/airfoils/tests/test_polar_subfunctions.py similarity index 95% rename from pyFAST/airfoils/tests/test_polar_subfunctions.py rename to openfast_toolbox/airfoils/tests/test_polar_subfunctions.py index 1b801be..1459c94 100644 --- a/pyFAST/airfoils/tests/test_polar_subfunctions.py +++ b/openfast_toolbox/airfoils/tests/test_polar_subfunctions.py @@ -4,7 +4,7 @@ import os import matplotlib.pyplot as plt MyDir=os.path.dirname(__file__) -from pyFAST.airfoils.Polar import _intersections +from openfast_toolbox.airfoils.Polar import _intersections # --------------------------------------------------------------------------------} # --- diff --git a/pyFAST/airfoils/tests/test_run_Examples.py b/openfast_toolbox/airfoils/tests/test_run_Examples.py similarity index 100% rename from pyFAST/airfoils/tests/test_run_Examples.py rename to openfast_toolbox/airfoils/tests/test_run_Examples.py diff --git a/pyFAST/case_generation/README.rst b/openfast_toolbox/case_generation/README.rst similarity index 100% rename from pyFAST/case_generation/README.rst rename to openfast_toolbox/case_generation/README.rst diff --git a/pyFAST/case_generation/__init__.py b/openfast_toolbox/case_generation/__init__.py similarity index 100% rename from pyFAST/case_generation/__init__.py rename to openfast_toolbox/case_generation/__init__.py diff --git a/pyFAST/case_generation/case_gen.py b/openfast_toolbox/case_generation/case_gen.py similarity index 83% rename from pyFAST/case_generation/case_gen.py rename to openfast_toolbox/case_generation/case_gen.py index 512b3de..88aabf6 100644 --- a/pyFAST/case_generation/case_gen.py +++ b/openfast_toolbox/case_generation/case_gen.py @@ -1,679 +1,795 @@ -import os -try: - import collections.abc as collections -except: - import collections - -import glob -import pandas as pd -import numpy as np -import shutil -import stat -import re - -# --- Misc fast libraries -import pyFAST.input_output.fast_input_file as fi -import pyFAST.case_generation.runner as runner -import pyFAST.input_output.postpro as postpro -from pyFAST.input_output.fast_wind_file import FASTWndFile -from pyFAST.input_output.rosco_performance_file import ROSCOPerformanceFile -from pyFAST.input_output.csv_file import CSVFile -# --------------------------------------------------------------------------------} -# --- Template replace -# --------------------------------------------------------------------------------{ -def handleRemoveReadonlyWin(func, path, exc_info): - """ - Error handler for ``shutil.rmtree``. - If the error is due to an access error (read only file) - it attempts to add write permission and then retries. - Usage : ``shutil.rmtree(path, onerror=onerror)`` - """ - if not os.access(path, os.W_OK): - # Is the error an access error ? - os.chmod(path, stat.S_IWUSR) - func(path) - else: - raise - - -def forceCopyFile (sfile, dfile): - # ---- Handling error due to wrong mod - if os.path.isfile(dfile): - if not os.access(dfile, os.W_OK): - os.chmod(dfile, stat.S_IWUSR) - #print(sfile, ' > ', dfile) - shutil.copy2(sfile, dfile) - -def copyTree(src, dst): - """ - Copy a directory to another one, overwritting files if necessary. - copy_tree from distutils and copytree from shutil fail on Windows (in particular on git files) - """ - def forceMergeFlatDir(srcDir, dstDir): - if not os.path.exists(dstDir): - os.makedirs(dstDir) - for item in os.listdir(srcDir): - srcFile = os.path.join(srcDir, item) - dstFile = os.path.join(dstDir, item) - forceCopyFile(srcFile, dstFile) - - def isAFlatDir(sDir): - for item in os.listdir(sDir): - sItem = os.path.join(sDir, item) - if os.path.isdir(sItem): - return False - return True - - for item in os.listdir(src): - s = os.path.join(src, item) - d = os.path.join(dst, item) - if os.path.isfile(s): - if not os.path.exists(dst): - os.makedirs(dst) - forceCopyFile(s,d) - if os.path.isdir(s): - isRecursive = not isAFlatDir(s) - if isRecursive: - copyTree(s, d) - else: - forceMergeFlatDir(s, d) - - -def templateReplaceGeneral(PARAMS, templateDir=None, outputDir=None, main_file=None, removeAllowed=False, removeRefSubFiles=False, oneSimPerDir=False, dryRun=False): - """ Generate inputs files by replacing different parameters from a template file. - The generated files are placed in the output directory `outputDir` - The files are read and written using the library `weio`. - The template file is read and its content can be changed like a dictionary. - Each item of `PARAMS` correspond to a set of parameters that will be replaced - in the template file to generate one input file. - - For "FAST" input files, parameters can be changed recursively. - - - INPUTS: - PARAMS: list of dictionaries. Each key of the dictionary should be a key present in the - template file when read with `weio` (see: weio.read(main_file).keys() ) - - PARAMS[0]={'DT':0.1, 'EDFile|GBRatio':1, 'ServoFile|GenEff':0.8} - - templateDir: if provided, this directory and its content will be copied to `outputDir` - before doing the parametric substitution - - outputDir : directory where files will be generated. - """ - # --- Helper functions - def rebase_rel(wd,s,sid): - split = os.path.splitext(s) - return os.path.join(wd,split[0]+sid+split[1]) - - def get_strID(p) : - if '__name__' in p.keys(): - strID=p['__name__'] - else: - raise Exception('When calling `templateReplace`, provide the key `__name_` in the parameter dictionaries') - return strID - - def splitAddress(sAddress): - sp = sAddress.split('|') - if len(sp)==1: - return sp[0],[] - else: - return sp[0],sp[1:] - - def rebaseFileName(org_filename, workDir, strID): - new_filename_full = rebase_rel(workDir, org_filename,'_'+strID) - new_filename = os.path.relpath(new_filename_full,workDir).replace('\\','/') - return new_filename, new_filename_full - - def replaceRecurse(templatename_or_newname, FileKey, ParamKey, ParamValue, Files, strID, workDir, TemplateFiles): - """ - FileKey: a single key defining which file we are currently modifying e.g. :'AeroFile', 'EDFile','FVWInputFileName' - ParamKey: the address key of the parameter to be changed, relative to the current FileKey - e.g. 'EDFile|IntMethod' (if FileKey is '') - 'IntMethod' (if FileKey is 'EDFile') - ParamValue: the value to be used - Files: dict of files, as returned by weio, keys are "FileKeys" - """ - # --- Special handling for the root - if FileKey=='': - FileKey='Root' - # --- Open (or get if already open) file where a parameter needs to be changed - if FileKey in Files.keys(): - # The file was already opened, it's stored - f = Files[FileKey] - newfilename_full = f.filename - newfilename = os.path.relpath(newfilename_full,workDir).replace('\\','/') - - else: - templatefilename = templatename_or_newname - templatefilename_full = os.path.join(workDir,templatefilename) - TemplateFiles.append(templatefilename_full) - if FileKey=='Root': - # Root files, we start from strID - ext = os.path.splitext(templatefilename)[-1] - newfilename_full = os.path.join(wd,strID+ext) - newfilename = strID+ext - if dryRun: newfilename = os.path.join(workDir, newfilename) obj=type('DummyClass', (object,), {'filename':newfilename}) return newfilename, {'Root':obj} else: - newfilename, newfilename_full = rebaseFileName(templatefilename, workDir, strID) - #print('--------------------------------------------------------------') - #print('TemplateFile :', templatefilename) - #print('TemplateFileFull:', templatefilename_full) - #print('NewFile :', newfilename) - #print('NewFileFull :', newfilename_full) - shutil.copyfile(templatefilename_full, newfilename_full) - f= fi.FASTInputFile(newfilename_full) # open the template file for that filekey - Files[FileKey]=f # store it - - # --- Changing parameters in that file - NewFileKey_or_Key, ChildrenKeys = splitAddress(ParamKey) - if len(ChildrenKeys)==0: - # A simple parameter is changed - Key = NewFileKey_or_Key - #print('Setting', FileKey, '|',Key, 'to',ParamValue) - if Key=='OutList': - if len(ParamValue)>0: - if len(ParamValue[0])==0: - f[Key] = ParamValue # We replace - else: - OutList=f[Key] - f[Key] = addToOutlist(OutList, ParamValue) # we insert - else: - f[Key] = ParamValue - else: - # Parameters needs to be changed in subfiles (children) - NewFileKey = NewFileKey_or_Key - ChildrenKey = '|'.join(ChildrenKeys) - child_templatefilename = f[NewFileKey].strip('"') # old filename that will be used as a template - baseparent = os.path.dirname(newfilename) - #print('Child templatefilename:',child_templatefilename) - #print('Parent base dir :',baseparent) - workDir = os.path.join(workDir, baseparent) - - # - newchildFilename, Files = replaceRecurse(child_templatefilename, NewFileKey, ChildrenKey, ParamValue, Files, strID, workDir, TemplateFiles) - #print('Setting', FileKey, '|',NewFileKey, 'to',newchildFilename) - f[NewFileKey] = '"'+newchildFilename+'"' - - return newfilename, Files - - - # --- Safety checks - if templateDir is None and outputDir is None: - raise Exception('Provide at least a template directory OR an output directory') - - if templateDir is not None: - if not os.path.exists(templateDir): - raise Exception('Template directory does not exist: '+templateDir) - - # Default value of outputDir if not provided - if templateDir[-1]=='/' or templateDir[-1]=='\\' : - templateDir=templateDir[0:-1] - if outputDir is None: - outputDir=templateDir+'_Parametric' - - # --- Main file use as "master" - if templateDir is not None: - main_file=os.path.join(outputDir, os.path.basename(main_file)) - else: - main_file=main_file - - # Params need to be a list - if not isinstance(PARAMS,list): - PARAMS=[PARAMS] - - if oneSimPerDir: - workDirS=[os.path.join(outputDir,get_strID(p)) for p in PARAMS] - else: - workDirS=[outputDir]*len(PARAMS) - # --- Creating outputDir - Copying template folder to outputDir if necessary - # Copying template folder to workDir - for wd in list(set(workDirS)): - if removeAllowed: - removeFASTOuputs(wd) - if os.path.exists(wd) and removeAllowed: - shutil.rmtree(wd, ignore_errors=False, onerror=handleRemoveReadonlyWin) - templateDir = os.path.normpath(templateDir) - wd = os.path.normpath(wd) - # NOTE: need some special handling if path are the sames - if templateDir!=wd: - copyTree(templateDir, wd) - if removeAllowed: - removeFASTOuputs(wd) - - - TemplateFiles=[] - files=[] - nTot=len(PARAMS) - for ip,(wd,p) in enumerate(zip(workDirS,PARAMS)): - if np.mod(ip+1,1000)==0: - print('File {:d}/{:d}'.format(ip,nTot)) - if '__index__' not in p.keys(): - p['__index__']=ip - - main_file_base = os.path.basename(main_file) - strID = get_strID(p) - # --- Setting up files for this simulation - Files=dict() - for k,v in p.items(): - if k =='__index__' or k=='__name__': - continue - new_mainFile, Files = replaceRecurse(main_file_base, '', k, v, Files, strID, wd, TemplateFiles) - if dryRun: break - # --- Writting files - for k,f in Files.items(): - if k=='Root': - files.append(f.filename) - if not dryRun: f.write() - # --- Remove extra files at the end - if removeRefSubFiles: - TemplateFiles, nCounts = np.unique(TemplateFiles, return_counts=True) - if not oneSimPerDir: - # we can only detele template files that were used by ALL simulations - TemplateFiles=[t for nc,t in zip(nCounts, TemplateFiles) if nc==len(PARAMS)] - for tf in TemplateFiles: - try: - os.remove(tf) - except: - print('[FAIL] Removing '+tf) - pass - return files - -# def templateReplace(PARAMS, *args, **kwargs): -def templateReplace(PARAMS, templateDir, outputDir=None, main_file=None, removeAllowed=False, removeRefSubFiles=False, oneSimPerDir=False, dryRun=False): - """ - see templateReplaceGeneral - - Replace parameters in a fast folder using a list of dictionaries where the keys are for instance: - 'DT', 'EDFile|GBRatio', 'ServoFile|GenEff' - """ - # --- For backward compatibility, remove "FAST|" from the keys - for p in PARAMS: - old_keys=[ k for k,_ in p.items() if k.find('FAST|')==0] - for k_old in old_keys: - k_new=k_old.replace('FAST|','') - p[k_new] = p.pop(k_old) - -# return templateReplaceGeneral(PARAMS, *args, **kwargs) - return templateReplaceGeneral(PARAMS, templateDir, outputDir=outputDir, main_file=main_file, - removeAllowed=removeAllowed, removeRefSubFiles=removeRefSubFiles, oneSimPerDir=oneSimPerDir, dryRun=dryRun) - -def addToOutlist(OutList, Signals): - if not isinstance(Signals,list): - raise Exception('Signals must be a list') - if len(Signals)==0: - return - for s in Signals: - if len(s)>0: - ss=s.split()[0].strip().strip('"').strip('\'') - AlreadyIn = any([o.find(ss)==1 for o in OutList ]) - if not AlreadyIn: - OutList.append(s) - if len(OutList[0])>0: - OutList=['']+OutList # ensuring first element has zero length (fast_input_file limitation for now) - return OutList - -def removeFASTOuputs(workDir): - # Cleaning folder - for f in glob.glob(os.path.join(workDir,'*.out')): - os.remove(f) - for f in glob.glob(os.path.join(workDir,'*.outb')): - os.remove(f) - for f in glob.glob(os.path.join(workDir,'*.ech')): - os.remove(f) - for f in glob.glob(os.path.join(workDir,'*.sum')): - os.remove(f) - -# --------------------------------------------------------------------------------} -# --- Tools for template replacement -# --------------------------------------------------------------------------------{ -def paramsSteadyAero(p=None): - p = dict() if p is None else p - p['AeroFile|AFAeroMod']=1 # remove dynamic effects dynamic - p['AeroFile|WakeMod']=1 # remove dynamic inflow dynamic - p['AeroFile|TwrPotent']=0 # remove tower shadow - p['AeroFile|TwrAero']=False # remove tower shadow - return p - -def paramsNoGen(p=None): - p = dict() if p is None else p - p['EDFile|GenDOF' ] = 'False' - return p - -def paramsGen(p=None): - p = dict() if p is None else p - p['EDFile|GenDOF' ] = 'True' - return p - -def paramsNoController(p=None): - p = dict() if p is None else p - p['ServoFile|PCMode'] = 0; - p['ServoFile|VSContrl'] = 0; - p['ServoFile|YCMode'] = 0; - return p - -def paramsControllerDLL(p=None): - p = dict() if p is None else p - p['ServoFile|PCMode'] = 5; - p['ServoFile|VSContrl'] = 5; - p['ServoFile|YCMode'] = 5; - p['EDFile|GenDOF'] = 'True'; - return p - - -def paramsStiff(p=None): - p = dict() if p is None else p - p['EDFile|FlapDOF1'] = 'False' - p['EDFile|FlapDOF2'] = 'False' - p['EDFile|EdgeDOF' ] = 'False' - p['EDFile|TeetDOF' ] = 'False' - p['EDFile|DrTrDOF' ] = 'False' - p['EDFile|YawDOF' ] = 'False' - p['EDFile|TwFADOF1'] = 'False' - p['EDFile|TwFADOF2'] = 'False' - p['EDFile|TwSSDOF1'] = 'False' - p['EDFile|TwSSDOF2'] = 'False' - p['EDFile|PtfmSgDOF'] = 'False' - p['EDFile|PtfmSwDOF'] = 'False' - p['EDFile|PtfmHvDOF'] = 'False' - p['EDFile|PtfmRDOF'] = 'False' - p['EDFile|PtfmPDOF'] = 'False' - p['EDFile|PtfmYDOF'] = 'False' - return p - -def paramsWS_RPM_Pitch(WS, RPM, Pitch, baseDict=None, flatInputs=False, tMax_OneRotation=True, nPerRot=60, dtMax=0.2): - """ - Generate OpenFAST "parameters" (list of dictionaries with "address") - chaing the inputs in ElastoDyn, InflowWind for different wind speed, RPM and Pitch - """ - # --- Ensuring everythin is an iterator - def iterify(x): - if not isinstance(x, collections.Iterable): x = [x] - return x - WS = iterify(WS) - RPM = iterify(RPM) - Pitch = iterify(Pitch) - # --- If inputs are not flat but different vectors to length through, we flatten them (TODO: meshgrid and ravel?) - if not flatInputs : - WS_flat = [] - Pitch_flat = [] - RPM_flat = [] - for pitch in Pitch: - for rpm in RPM: - for ws in WS: - WS_flat.append(ws) - RPM_flat.append(rpm) - Pitch_flat.append(pitch) - else: - WS_flat, Pitch_flat, RPM_flat = WS, Pitch, RPM - - # --- Defining the parametric study - PARAMS=[] - i=0 - for ws,rpm,pitch in zip(WS_flat,RPM_flat,Pitch_flat): - if baseDict is None: - p=dict() - else: - p = baseDict.copy() - if tMax_OneRotation: - # Ensure that tMax is large enough to cover one full rotation - omega = rpm/60*2*np.pi - T = 2*np.pi/omega - dt = min(dtMax, T/nPerRot) - p['DT'] = dt - p['TMax'] = T+3*dt # small buffer - p['DT_Out'] = 'default' - - p['EDFile|RotSpeed'] = rpm - p['InflowFile|HWindSpeed'] = ws - p['InflowFile|WindType'] = 1 # Setting steady wind - p['EDFile|BlPitch(1)'] = pitch - p['EDFile|BlPitch(2)'] = pitch - p['EDFile|BlPitch(3)'] = pitch - - p['__index__'] = i - #p['__name__'] = '{:03d}_ws{:04.1f}_pt{:04.2f}_om{:04.2f}'.format(p['__index__'],p['InflowFile|HWindSpeed'],p['EDFile|BlPitch(1)'],p['EDFile|RotSpeed']) - p['__name__'] = 'ws{:04.1f}_pt{:04.2f}_om{:04.2f}'.format(p['InflowFile|HWindSpeed'],p['EDFile|BlPitch(1)'],p['EDFile|RotSpeed']) - i=i+1 - PARAMS.append(p) - return PARAMS - -def paramsLinearTrim(p=None): - p = dict() if p is None else p - - # Set a few DOFs, move this to main file - p['Linearize'] = True - p['CalcSteady'] = True - p['TrimGain'] = 1e-4 - p['TrimTol'] = 1e-5 - p['CompMooring'] = 0 - p['CompHydro'] = 0 - p['LinOutJac'] = False - p['LinOutMod'] = False - p['OutFmt'] = '"ES20.12E3"' # Important for decent resolution - - p['AeroFile|AFAeroMod'] = 1 - p['AeroFile|CavitCheck'] = 'False' - p['AeroFile|CompAA'] = 'False' - - p['ServoFile|PCMode'] = 0 - p['ServoFile|VSContrl'] = 1 - - p['ServoFile|CompNTMD'] = 'False' - p['ServoFile|CompTTMD'] = 'False' - - # Set all DOFs off, enable as desired - p['EDFile|FlapDOF1'] = 'False' - p['EDFile|FlapDOF2'] = 'False' - p['EDFile|EdgeDOF'] = 'False' - p['EDFile|TeetDOF'] = 'False' - p['EDFile|DrTrDOF'] = 'False' - p['EDFile|GenDOF'] = 'False' - p['EDFile|YawDOF'] = 'False' - p['EDFile|TwFADOF1'] = 'False' - p['EDFile|TwFADOF2'] = 'False' - p['EDFile|TwSSDOF1'] = 'False' - p['EDFile|TwSSDOF2'] = 'False' - p['EDFile|PtfmSgDOF'] = 'False' - p['EDFile|PtfmSwDOF'] = 'False' - p['EDFile|PtfmHvDOF'] = 'False' - p['EDFile|PtfmRDOF'] = 'False' - p['EDFile|PtfmPDOF'] = 'False' - p['EDFile|PtfmYDOF'] = 'False' - - - return p - -# --------------------------------------------------------------------------------} -# --- -# --------------------------------------------------------------------------------{ -def createStepWind(filename,WSstep=1,WSmin=3,WSmax=25,tstep=100,dt=0.5,tmin=0,tmax=999): - f = FASTWndFile() - Steps= np.arange(WSmin,WSmax+WSstep,WSstep) - print(Steps) - nCol = len(f.colNames) - nRow = len(Steps)*2 - M = np.zeros((nRow,nCol)); - M[0,0] = tmin - M[0,1] = WSmin - for i,s in enumerate(Steps[:-1]): - M[2*i+1,0] = tmin + (i+1)*tstep-dt - M[2*i+2,0] = tmin + (i+1)*tstep - M[2*i+1,1] = Steps[i] - if i0: - main_fastfile=os.path.basename(main_fastfile) - - # --- Reading main fast file to get rotor radius - fst = fi.FASTInputFile(os.path.join(refdir,main_fastfile)) - ed = fi.FASTInputFile(os.path.join(refdir,fst['EDFile'].replace('"',''))) - R = ed['TipRad'] - - # --- Making sure we have - if (Omega is not None): - if (Lambda is not None): - WS = np.ones(Omega.shape)*WS_default - elif (WS is not None): - if len(WS)!=len(Omega): - raise Exception('When providing Omega and WS, both vectors should have the same dimension') - else: - WS = np.ones(Omega.shape)*WS_default - else: - Omega = WS_default * Lambda/R*60/(2*np.pi) #[rpm] TODO, use more realistic combinations of WS and Omega - WS = np.ones(Omega.shape)*WS_default - - - # --- Defining flat vectors of operating conditions - WS_flat = [] - RPM_flat = [] - Pitch_flat = [] - for pitch in Pitch: - for (rpm,ws) in zip(Omega,WS): - WS_flat.append(ws) - RPM_flat.append(rpm) - Pitch_flat.append(pitch) - # --- Setting up default options - baseDict={'TMax': TMax, 'DT': 0.01, 'DT_Out': 0.1, 'OutFileFmt':2} # NOTE: Tmax should be at least 2pi/Omega - baseDict['AeroFile|OutList'] = ['', '"RtAeroCp"', '"RtAeroCt"','"RtVAvgxh"'] - baseDict['EDFile|OutList'] = ['', '"Azimuth"' ,'"RotSpeed"', '"BldPitch1"'] - baseDict['InflowFile|PLexp'] = 0 - baseDict['InflowFile|RefHt'] = 90 # Arbitrary - baseDict['InflowFile|NWindVel'] =1 - baseDict['InflowFile|WindVxiList']=0 - baseDict['InflowFile|WindVyiList']=0 - baseDict['InflowFile|WindVziList']=90 # should be same as RefHt - baseDict['InflowFile|OutList'] = ['', '"Wind1VelX"'] - - baseDict = paramsNoController(baseDict) - if bStiff: - baseDict = paramsStiff(baseDict) - if bNoGen: - baseDict = paramsNoGen(baseDict) - if bSteadyAero: - baseDict = paramsSteadyAero(baseDict) - - # --- Creating set of parameters to be changed - PARAMS = paramsWS_RPM_Pitch(WS_flat, RPM_flat, Pitch_flat, baseDict=baseDict, flatInputs=True, tMax_OneRotation=True) - - # --- Generating all files in a workDir - if workDir is None: - workDir = refdir.strip('/').strip('\\')+'_CPLambdaPitch' - print('>>> Generating {} inputs files in {}'.format(len(PARAMS), workDir)) - RemoveAllowed=reRun # If the user want to rerun, we can remove, otherwise we keep existing simulations - fastFiles=templateReplace(PARAMS, refdir, outputDir=workDir,removeRefSubFiles=True,removeAllowed=RemoveAllowed,main_file=main_fastfile, dryRun=skipWrite) - - # --- Creating a batch script just in case - batchFile = os.path.join(workDir,'_RUN_ALL.bat') - runner.writeBatch(batchFile, fastFiles, fastExe=fastExe, nBatches=nCores) - print('>>> Batch script created (if preferred):', batchFile) - - # --- Running fast simulations - print('>>> Running {} simulations...'.format(len(fastFiles))) - runner.run_fastfiles(fastFiles, showOutputs=showOutputs, fastExe=fastExe, nCores=nCores, reRun=reRun) - - # --- Postpro - Computing averages at the end of the simluation - print('>>> Postprocessing...') - outFiles = [os.path.splitext(f)[0]+'.outb' for f in fastFiles] - # outFiles = glob.glob(os.path.join(workDir,'*.outb')) - ColKeepStats = ['RotSpeed_[rpm]','BldPitch1_[deg]','RtAeroCp_[-]','RtAeroCt_[-]','Wind1VelX_[m/s]'] - ColSort='RotSpeed_[rpm]' - try: - result = postpro.averagePostPro(outFiles, avgMethod='periods', avgParam=1, ColKeep=ColKeepStats, ColSort=ColSort ) - except: - result = postpro.averagePostPro(outFiles, avgMethod='constantwindow', avgParam=None, ColKeep=ColKeepStats, ColSort=ColSort) - - # --- Adding lambda, sorting and keeping only few columns - result['lambda_[-]'] = result['RotSpeed_[rpm]']* R*2*np.pi/60/result['Wind1VelX_[m/s]'] - result.sort_values(['lambda_[-]','BldPitch1_[deg]'], ascending=[True,True], inplace=True) - ColKeepFinal = ['lambda_[-]','BldPitch1_[deg]','RtAeroCp_[-]','RtAeroCt_[-]'] - result = result[ColKeepFinal] - - # --- Converting to matrices - CP = result['RtAeroCp_[-]'].values - CT = result['RtAeroCt_[-]'].values - MCP = CP.reshape((len(Lambda),len(Pitch))) - MCT = CT.reshape((len(Lambda),len(Pitch))) - - # --- Create a ROSCO PerformanceFile for convenience - turbname = os.path.basename(exportBase) - rs = ROSCOPerformanceFile(pitch=Pitch, tsr=Lambda, CP=MCP, CT=MCT, name=turbname) - - if exportBase is not None: - if exportFmt.lower()=='rosco': - # Write a ROSCO performance file - aeroMapFile = exportBase+'_CPCTCQ.txt' - rs.write(aeroMapFile) - elif exportFmt.lower()=='csv': - # Write individual CSV files - np.savetxt(exportBase+'_Lambda.csv',Lambda,delimiter = ',') - np.savetxt(exportBase+'_Pitch.csv' ,Pitch ,delimiter = ',') - np.savetxt(exportBase+'_CP.csv' ,MCP ,delimiter = ',') - np.savetxt(exportBase+'_CT.csv' ,MCT ,delimiter = ',') - else: - raise NotImplementedError(exportFmt) - - fig = None - if plot is True: - # --- Plotting matrix of CP values - fig = rs.plotCP3D() - return rs, result, fig - - -if __name__=='__main__': - # --- Test of templateReplace - PARAMS = {} - PARAMS['TMax'] = 10 - PARAMS['__name__'] = 'MyName' - PARAMS['DT'] = 0.01 - PARAMS['DT_Out'] = 0.1 - PARAMS['EDFile|RotSpeed'] = 100 - PARAMS['EDFile|BlPitch(1)'] = 1 - PARAMS['EDFile|GBoxEff'] = 0.92 - PARAMS['ServoFile|VS_Rgn2K'] = 0.00038245 - PARAMS['ServoFile|GenEff'] = 0.95 - PARAMS['InflowFile|HWindSpeed'] = 8 - templateReplace(PARAMS,refDir,RemoveRefSubFiles=True) - +import os +try: + import collections.abc as collections +except: + import collections + +import glob +import pandas as pd +import numpy as np +import shutil +import stat +import re + +# --- Misc fast libraries +import openfast_toolbox.io.fast_input_file as fi +import openfast_toolbox.case_generation.runner as runner +import openfast_toolbox.postpro as postpro +from openfast_toolbox.io.fast_wind_file import FASTWndFile +from openfast_toolbox.io.rosco_performance_file import ROSCOPerformanceFile +from openfast_toolbox.io.csv_file import CSVFile +# --------------------------------------------------------------------------------} +# --- Template replace +# --------------------------------------------------------------------------------{ +def handleRemoveReadonlyWin(func, path, exc_info): + """ + Error handler for ``shutil.rmtree``. + If the error is due to an access error (read only file) + it attempts to add write permission and then retries. + Usage : ``shutil.rmtree(path, onerror=onerror)`` + """ + if not os.access(path, os.W_OK): + # Is the error an access error ? + os.chmod(path, stat.S_IWUSR) + func(path) + else: + raise + + +def forceCopyFile (sfile, dfile): + # ---- Handling error due to wrong mod + if os.path.isfile(dfile): + if not os.access(dfile, os.W_OK): + os.chmod(dfile, stat.S_IWUSR) + #print(sfile, ' > ', dfile) + shutil.copy2(sfile, dfile) + +def copyTree(src, dst): + """ + Copy a directory to another one, overwritting files if necessary. + copy_tree from distutils and copytree from shutil fail on Windows (in particular on git files) + """ + def forceMergeFlatDir(srcDir, dstDir): + if not os.path.exists(dstDir): + os.makedirs(dstDir) + for item in os.listdir(srcDir): + srcFile = os.path.join(srcDir, item) + dstFile = os.path.join(dstDir, item) + forceCopyFile(srcFile, dstFile) + + def isAFlatDir(sDir): + for item in os.listdir(sDir): + sItem = os.path.join(sDir, item) + if os.path.isdir(sItem): + return False + return True + + for item in os.listdir(src): + s = os.path.join(src, item) + d = os.path.join(dst, item) + if os.path.isfile(s): + if not os.path.exists(dst): + os.makedirs(dst) + forceCopyFile(s,d) + if os.path.isdir(s): + isRecursive = not isAFlatDir(s) + if isRecursive: + copyTree(s, d) + else: + forceMergeFlatDir(s, d) + + +def templateReplaceGeneral(PARAMS, templateDir=None, outputDir=None, main_file=None, removeAllowed=False, removeRefSubFiles=False, oneSimPerDir=False, dryRun=False): + + """ Generate inputs files by replacing different parameters from a template file. + The generated files are placed in the output directory `outputDir` + The files are read and written using the library `weio`. + The template file is read and its content can be changed like a dictionary. + Each item of `PARAMS` correspond to a set of parameters that will be replaced + in the template file to generate one input file. + + For "FAST" input files, parameters can be changed recursively. + + + INPUTS: + PARAMS: list of dictionaries. Each key of the dictionary should be a key present in the + template file when read with `weio` (see: weio.read(main_file).keys() ) + + PARAMS[0]={'DT':0.1, 'EDFile|GBRatio':1, 'ServoFile|GenEff':0.8} + + templateDir: if provided, this directory and its content will be copied to `outputDir` + before doing the parametric substitution + + outputDir : directory where files will be generated. + """ + # --- Helper functions + def rebase_rel(wd,s,sid): + split = os.path.splitext(s) + return os.path.join(wd,split[0]+sid+split[1]) + + def get_strID(p) : + if '__name__' in p.keys(): + strID=p['__name__'] + else: + raise Exception('When calling `templateReplace`, provide the key `__name_` in the parameter dictionaries') + return strID + + def splitAddress(sAddress): + sp = sAddress.split('|') + if len(sp)==1: + return sp[0],[] + else: + return sp[0],sp[1:] + + def rebaseFileName(org_filename, workDir, strID): + new_filename_full = rebase_rel(workDir, org_filename,'_'+strID) + new_filename = os.path.relpath(new_filename_full,workDir).replace('\\','/') + return new_filename, new_filename_full + + def replaceRecurse(templatename_or_newname, FileKey, ParamKey, ParamValue, Files, strID, workDir, TemplateFiles): + """ + FileKey: a single key defining which file we are currently modifying e.g. :'AeroFile', 'EDFile','FVWInputFileName' + ParamKey: the address key of the parameter to be changed, relative to the current FileKey + e.g. 'EDFile|IntMethod' (if FileKey is '') + 'IntMethod' (if FileKey is 'EDFile') + ParamValue: the value to be used + Files: dict of files, as returned by weio, keys are "FileKeys" + """ + # --- Special handling for the root + if FileKey=='': + FileKey='Root' + # --- Open (or get if already open) file where a parameter needs to be changed + if FileKey in Files.keys(): + # The file was already opened, it's stored + f = Files[FileKey] + newfilename_full = f.filename + newfilename = os.path.relpath(newfilename_full,workDir).replace('\\','/') + + else: + templatefilename = templatename_or_newname + templatefilename_full = os.path.join(workDir,templatefilename) + TemplateFiles.append(templatefilename_full) + if FileKey=='Root': + # Root files, we start from strID + ext = os.path.splitext(templatefilename)[-1] + newfilename_full = os.path.join(wd,strID+ext) + newfilename = strID+ext + if dryRun: + newfilename = os.path.join(workDir, newfilename) + obj=type('DummyClass', (object,), {'filename':newfilename}) + return newfilename, {'Root':obj} + else: + newfilename, newfilename_full = rebaseFileName(templatefilename, workDir, strID) + #print('--------------------------------------------------------------') + #print('TemplateFile :', templatefilename) + #print('TemplateFileFull:', templatefilename_full) + #print('NewFile :', newfilename) + #print('NewFileFull :', newfilename_full) + shutil.copyfile(templatefilename_full, newfilename_full) + f= fi.FASTInputFile(newfilename_full) # open the template file for that filekey + Files[FileKey]=f # store it + + # --- Changing parameters in that file + NewFileKey_or_Key, ChildrenKeys = splitAddress(ParamKey) + if len(ChildrenKeys)==0: + # A simple parameter is changed + Key = NewFileKey_or_Key + #print('Setting', FileKey, '|',Key, 'to',ParamValue) + if Key=='OutList': + if len(ParamValue)>0: + if len(ParamValue[0])==0: + f[Key] = ParamValue # We replace + else: + OutList=f[Key] + f[Key] = addToOutlist(OutList, ParamValue) # we insert + else: + f[Key] = ParamValue + else: + # Parameters needs to be changed in subfiles (children) + NewFileKey = NewFileKey_or_Key + ChildrenKey = '|'.join(ChildrenKeys) + child_templatefilename = f[NewFileKey].strip('"') # old filename that will be used as a template + baseparent = os.path.dirname(newfilename) + #print('Child templatefilename:',child_templatefilename) + #print('Parent base dir :',baseparent) + workDir = os.path.join(workDir, baseparent) + + # + newchildFilename, Files = replaceRecurse(child_templatefilename, NewFileKey, ChildrenKey, ParamValue, Files, strID, workDir, TemplateFiles) + #print('Setting', FileKey, '|',NewFileKey, 'to',newchildFilename) + f[NewFileKey] = '"'+newchildFilename+'"' + + return newfilename, Files + + + # --- Safety checks + if templateDir is None and outputDir is None: + raise Exception('Provide at least a template directory OR an output directory') + + if templateDir is not None: + if not os.path.exists(templateDir): + raise Exception('Template directory does not exist: '+templateDir) + + # Default value of outputDir if not provided + if templateDir[-1]=='/' or templateDir[-1]=='\\' : + templateDir=templateDir[0:-1] + if outputDir is None: + outputDir=templateDir+'_Parametric' + + # --- Main file use as "master" + if templateDir is not None: + main_file=os.path.join(outputDir, os.path.basename(main_file)) + else: + main_file=main_file + + # Params need to be a list + if not isinstance(PARAMS,list): + PARAMS=[PARAMS] + + if oneSimPerDir: + workDirS=[os.path.join(outputDir,get_strID(p)) for p in PARAMS] + else: + workDirS=[outputDir]*len(PARAMS) + # --- Creating outputDir - Copying template folder to outputDir if necessary + # Copying template folder to workDir + for wd in list(set(workDirS)): + if removeAllowed: + removeFASTOuputs(wd) + if os.path.exists(wd) and removeAllowed: + shutil.rmtree(wd, ignore_errors=False, onerror=handleRemoveReadonlyWin) + templateDir = os.path.normpath(templateDir) + wd = os.path.normpath(wd) + # NOTE: need some special handling if path are the sames + if templateDir!=wd: + copyTree(templateDir, wd) + if removeAllowed: + removeFASTOuputs(wd) + + + TemplateFiles=[] + files=[] + nTot=len(PARAMS) + for ip,(wd,p) in enumerate(zip(workDirS,PARAMS)): + if np.mod(ip+1,1000)==0: + print('File {:d}/{:d}'.format(ip,nTot)) + if '__index__' not in p.keys(): + p['__index__']=ip + + main_file_base = os.path.basename(main_file) + strID = get_strID(p) + # --- Setting up files for this simulation + Files=dict() + for k,v in p.items(): + if k =='__index__' or k=='__name__': + continue + new_mainFile, Files = replaceRecurse(main_file_base, '', k, v, Files, strID, wd, TemplateFiles) + if dryRun: + break + + # --- Writting files + for k,f in Files.items(): + if k=='Root': + files.append(f.filename) + if not dryRun: + f.write() + + # --- Remove extra files at the end + if removeRefSubFiles: + TemplateFiles, nCounts = np.unique(TemplateFiles, return_counts=True) + if not oneSimPerDir: + # we can only detele template files that were used by ALL simulations + TemplateFiles=[t for nc,t in zip(nCounts, TemplateFiles) if nc==len(PARAMS)] + for tf in TemplateFiles: + try: + os.remove(tf) + except: + print('[FAIL] Removing '+tf) + pass + return files + +# def templateReplace(PARAMS, *args, **kwargs): + +def templateReplace(PARAMS, templateDir, outputDir=None, main_file=None, removeAllowed=False, removeRefSubFiles=False, oneSimPerDir=False, dryRun=False): + + """ + + see templateReplaceGeneral + + + + Replace parameters in a fast folder using a list of dictionaries where the keys are for instance: + + 'DT', 'EDFile|GBRatio', 'ServoFile|GenEff' + """ + # --- For backward compatibility, remove "FAST|" from the keys + for p in PARAMS: + old_keys=[ k for k,_ in p.items() if k.find('FAST|')==0] + for k_old in old_keys: + k_new=k_old.replace('FAST|','') + p[k_new] = p.pop(k_old) + +# return templateReplaceGeneral(PARAMS, *args, **kwargs) + + return templateReplaceGeneral(PARAMS, templateDir, outputDir=outputDir, main_file=main_file, + removeAllowed=removeAllowed, removeRefSubFiles=removeRefSubFiles, oneSimPerDir=oneSimPerDir, dryRun=dryRun) + + +def addToOutlist(OutList, Signals): + + if not isinstance(Signals,list): + + raise Exception('Signals must be a list') + + if len(Signals)==0: + + return + + for s in Signals: + + if len(s)>0: + + ss=s.split()[0].strip().strip('"').strip('\'') + + AlreadyIn = any([o.find(ss)==1 for o in OutList ]) + + if not AlreadyIn: + + OutList.append(s) + + if len(OutList[0])>0: + + OutList=['']+OutList # ensuring first element has zero length (fast_input_file limitation for now) + + return OutList + + + +def removeFASTOuputs(workDir): + # Cleaning folder + for f in glob.glob(os.path.join(workDir,'*.out')): + os.remove(f) + for f in glob.glob(os.path.join(workDir,'*.outb')): + os.remove(f) + for f in glob.glob(os.path.join(workDir,'*.ech')): + os.remove(f) + for f in glob.glob(os.path.join(workDir,'*.sum')): + os.remove(f) + +# --------------------------------------------------------------------------------} +# --- Tools for template replacement +# --------------------------------------------------------------------------------{ +def paramsSteadyAero(p=None): + p = dict() if p is None else p + p['AeroFile|AFAeroMod']=1 # remove dynamic effects dynamic + p['AeroFile|WakeMod']=1 # remove dynamic inflow dynamic + p['AeroFile|TwrPotent']=0 # remove tower shadow + p['AeroFile|TwrAero']=False # remove tower shadow + + return p + +def paramsNoGen(p=None): + p = dict() if p is None else p + p['EDFile|GenDOF' ] = 'False' + return p + +def paramsGen(p=None): + p = dict() if p is None else p + p['EDFile|GenDOF' ] = 'True' + return p + +def paramsNoController(p=None): + p = dict() if p is None else p + p['ServoFile|PCMode'] = 0; + p['ServoFile|VSContrl'] = 0; + p['ServoFile|YCMode'] = 0; + return p + +def paramsControllerDLL(p=None): + p = dict() if p is None else p + p['ServoFile|PCMode'] = 5; + p['ServoFile|VSContrl'] = 5; + p['ServoFile|YCMode'] = 5; + p['EDFile|GenDOF'] = 'True'; + return p + + +def paramsStiff(p=None): + p = dict() if p is None else p + p['EDFile|FlapDOF1'] = 'False' + p['EDFile|FlapDOF2'] = 'False' + p['EDFile|EdgeDOF' ] = 'False' + p['EDFile|TeetDOF' ] = 'False' + p['EDFile|DrTrDOF' ] = 'False' + p['EDFile|YawDOF' ] = 'False' + p['EDFile|TwFADOF1'] = 'False' + p['EDFile|TwFADOF2'] = 'False' + p['EDFile|TwSSDOF1'] = 'False' + p['EDFile|TwSSDOF2'] = 'False' + p['EDFile|PtfmSgDOF'] = 'False' + p['EDFile|PtfmSwDOF'] = 'False' + p['EDFile|PtfmHvDOF'] = 'False' + p['EDFile|PtfmRDOF'] = 'False' + p['EDFile|PtfmPDOF'] = 'False' + p['EDFile|PtfmYDOF'] = 'False' + return p + +def paramsWS_RPM_Pitch(WS, RPM, Pitch, baseDict=None, flatInputs=False, tMax_OneRotation=True, nPerRot=60, dtMax=0.2): + + """ + Generate OpenFAST "parameters" (list of dictionaries with "address") + chaing the inputs in ElastoDyn, InflowWind for different wind speed, RPM and Pitch + """ + # --- Ensuring everythin is an iterator + def iterify(x): + if not isinstance(x, collections.Iterable): x = [x] + return x + WS = iterify(WS) + RPM = iterify(RPM) + Pitch = iterify(Pitch) + # --- If inputs are not flat but different vectors to length through, we flatten them (TODO: meshgrid and ravel?) + if not flatInputs : + WS_flat = [] + Pitch_flat = [] + RPM_flat = [] + for pitch in Pitch: + for rpm in RPM: + for ws in WS: + WS_flat.append(ws) + RPM_flat.append(rpm) + Pitch_flat.append(pitch) + else: + WS_flat, Pitch_flat, RPM_flat = WS, Pitch, RPM + + # --- Defining the parametric study + PARAMS=[] + i=0 + for ws,rpm,pitch in zip(WS_flat,RPM_flat,Pitch_flat): + if baseDict is None: + p=dict() + else: + p = baseDict.copy() + if tMax_OneRotation: + + # Ensure that tMax is large enough to cover one full rotation + + omega = rpm/60*2*np.pi + + T = 2*np.pi/omega + + dt = min(dtMax, T/nPerRot) + + p['DT'] = dt + + p['TMax'] = T+3*dt # small buffer + + p['DT_Out'] = 'default' + + + + p['EDFile|RotSpeed'] = rpm + p['InflowFile|HWindSpeed'] = ws + p['InflowFile|WindType'] = 1 # Setting steady wind + p['EDFile|BlPitch(1)'] = pitch + p['EDFile|BlPitch(2)'] = pitch + p['EDFile|BlPitch(3)'] = pitch + + p['__index__'] = i + #p['__name__'] = '{:03d}_ws{:04.1f}_pt{:04.2f}_om{:04.2f}'.format(p['__index__'],p['InflowFile|HWindSpeed'],p['EDFile|BlPitch(1)'],p['EDFile|RotSpeed']) + + p['__name__'] = 'ws{:04.1f}_pt{:04.2f}_om{:04.2f}'.format(p['InflowFile|HWindSpeed'],p['EDFile|BlPitch(1)'],p['EDFile|RotSpeed']) + + i=i+1 + PARAMS.append(p) + return PARAMS + +def paramsLinearTrim(p=None): + p = dict() if p is None else p + + # Set a few DOFs, move this to main file + p['Linearize'] = True + p['CalcSteady'] = True + p['TrimGain'] = 1e-4 + p['TrimTol'] = 1e-5 + p['CompMooring'] = 0 + p['CompHydro'] = 0 + p['LinOutJac'] = False + p['LinOutMod'] = False + p['OutFmt'] = '"ES20.12E3"' # Important for decent resolution + + p['AeroFile|AFAeroMod'] = 1 + p['AeroFile|CavitCheck'] = 'False' + p['AeroFile|CompAA'] = 'False' + + p['ServoFile|PCMode'] = 0 + p['ServoFile|VSContrl'] = 1 + + p['ServoFile|CompNTMD'] = 'False' + p['ServoFile|CompTTMD'] = 'False' + + # Set all DOFs off, enable as desired + p['EDFile|FlapDOF1'] = 'False' + p['EDFile|FlapDOF2'] = 'False' + p['EDFile|EdgeDOF'] = 'False' + p['EDFile|TeetDOF'] = 'False' + p['EDFile|DrTrDOF'] = 'False' + p['EDFile|GenDOF'] = 'False' + p['EDFile|YawDOF'] = 'False' + p['EDFile|TwFADOF1'] = 'False' + p['EDFile|TwFADOF2'] = 'False' + p['EDFile|TwSSDOF1'] = 'False' + p['EDFile|TwSSDOF2'] = 'False' + p['EDFile|PtfmSgDOF'] = 'False' + p['EDFile|PtfmSwDOF'] = 'False' + p['EDFile|PtfmHvDOF'] = 'False' + p['EDFile|PtfmRDOF'] = 'False' + p['EDFile|PtfmPDOF'] = 'False' + p['EDFile|PtfmYDOF'] = 'False' + + + return p + +# --------------------------------------------------------------------------------} +# --- +# --------------------------------------------------------------------------------{ +def createStepWind(filename,WSstep=1,WSmin=3,WSmax=25,tstep=100,dt=0.5,tmin=0,tmax=999): + f = FASTWndFile() + Steps= np.arange(WSmin,WSmax+WSstep,WSstep) + print(Steps) + nCol = len(f.colNames) + nRow = len(Steps)*2 + M = np.zeros((nRow,nCol)); + M[0,0] = tmin + M[0,1] = WSmin + for i,s in enumerate(Steps[:-1]): + M[2*i+1,0] = tmin + (i+1)*tstep-dt + M[2*i+2,0] = tmin + (i+1)*tstep + M[2*i+1,1] = Steps[i] + if i0: + main_fastfile=os.path.basename(main_fastfile) + + # --- Reading main fast file to get rotor radius + fst = fi.FASTInputFile(os.path.join(refdir,main_fastfile)) + ed = fi.FASTInputFile(os.path.join(refdir,fst['EDFile'].replace('"',''))) + R = ed['TipRad'] + + # --- Making sure we have + if (Omega is not None): + if (Lambda is not None): + WS = np.ones(Omega.shape)*WS_default + elif (WS is not None): + if len(WS)!=len(Omega): + raise Exception('When providing Omega and WS, both vectors should have the same dimension') + else: + WS = np.ones(Omega.shape)*WS_default + else: + Omega = WS_default * Lambda/R*60/(2*np.pi) #[rpm] TODO, use more realistic combinations of WS and Omega + + WS = np.ones(Omega.shape)*WS_default + + + # --- Defining flat vectors of operating conditions + WS_flat = [] + RPM_flat = [] + Pitch_flat = [] + for pitch in Pitch: + for (rpm,ws) in zip(Omega,WS): + WS_flat.append(ws) + RPM_flat.append(rpm) + Pitch_flat.append(pitch) + # --- Setting up default options + baseDict={'TMax': TMax, 'DT': 0.01, 'DT_Out': 0.1, 'OutFileFmt':2} # NOTE: Tmax should be at least 2pi/Omega + + baseDict['AeroFile|OutList'] = ['', '"RtAeroCp"', '"RtAeroCt"','"RtVAvgxh"'] + + baseDict['EDFile|OutList'] = ['', '"Azimuth"' ,'"RotSpeed"', '"BldPitch1"'] + + baseDict['InflowFile|PLexp'] = 0 + + baseDict['InflowFile|RefHt'] = 90 # Arbitrary + + baseDict['InflowFile|NWindVel'] =1 + + baseDict['InflowFile|WindVxiList']=0 + + baseDict['InflowFile|WindVyiList']=0 + + baseDict['InflowFile|WindVziList']=90 # should be same as RefHt + + baseDict['InflowFile|OutList'] = ['', '"Wind1VelX"'] + + + + baseDict = paramsNoController(baseDict) + if bStiff: + baseDict = paramsStiff(baseDict) + if bNoGen: + baseDict = paramsNoGen(baseDict) + if bSteadyAero: + baseDict = paramsSteadyAero(baseDict) + + # --- Creating set of parameters to be changed + PARAMS = paramsWS_RPM_Pitch(WS_flat, RPM_flat, Pitch_flat, baseDict=baseDict, flatInputs=True, tMax_OneRotation=True) + + + # --- Generating all files in a workDir + if workDir is None: + + workDir = refdir.strip('/').strip('\\')+'_CPLambdaPitch' + + print('>>> Generating {} inputs files in {}'.format(len(PARAMS), workDir)) + + RemoveAllowed=reRun # If the user want to rerun, we can remove, otherwise we keep existing simulations + fastFiles=templateReplace(PARAMS, refdir, outputDir=workDir,removeRefSubFiles=True,removeAllowed=RemoveAllowed,main_file=main_fastfile, dryRun=skipWrite) + + + # --- Creating a batch script just in case + + batchFile = os.path.join(workDir,'_RUN_ALL.bat') + + runner.writeBatch(batchFile, fastFiles, fastExe=fastExe, nBatches=nCores) + + print('>>> Batch script created (if preferred):', batchFile) + + + + # --- Running fast simulations + print('>>> Running {} simulations...'.format(len(fastFiles))) + runner.run_fastfiles(fastFiles, showOutputs=showOutputs, fastExe=fastExe, nCores=nCores, reRun=reRun) + + # --- Postpro - Computing averages at the end of the simluation + print('>>> Postprocessing...') + outFiles = [os.path.splitext(f)[0]+'.outb' for f in fastFiles] + # outFiles = glob.glob(os.path.join(workDir,'*.outb')) + ColKeepStats = ['RotSpeed_[rpm]','BldPitch1_[deg]','RtAeroCp_[-]','RtAeroCt_[-]','Wind1VelX_[m/s]'] + ColSort='RotSpeed_[rpm]' + + try: + + result = postpro.averagePostPro(outFiles, avgMethod='periods', avgParam=1, ColKeep=ColKeepStats, ColSort=ColSort ) + + except: + + result = postpro.averagePostPro(outFiles, avgMethod='constantwindow', avgParam=None, ColKeep=ColKeepStats, ColSort=ColSort) + + + # --- Adding lambda, sorting and keeping only few columns + result['lambda_[-]'] = result['RotSpeed_[rpm]']* R*2*np.pi/60/result['Wind1VelX_[m/s]'] + + result.sort_values(['lambda_[-]','BldPitch1_[deg]'], ascending=[True,True], inplace=True) + + ColKeepFinal = ['lambda_[-]','BldPitch1_[deg]','RtAeroCp_[-]','RtAeroCt_[-]'] + + result = result[ColKeepFinal] + + + # --- Converting to matrices + + CP = result['RtAeroCp_[-]'].values + CT = result['RtAeroCt_[-]'].values + MCP = CP.reshape((len(Lambda),len(Pitch))) + + MCT = CT.reshape((len(Lambda),len(Pitch))) + + + # --- Create a ROSCO PerformanceFile for convenience + + turbname = os.path.basename(exportBase) + + rs = ROSCOPerformanceFile(pitch=Pitch, tsr=Lambda, CP=MCP, CT=MCT, name=turbname) + + + + if exportBase is not None: + + if exportFmt.lower()=='rosco': + + # Write a ROSCO performance file + + aeroMapFile = exportBase+'_CPCTCQ.txt' + + rs.write(aeroMapFile) + + elif exportFmt.lower()=='csv': + + # Write individual CSV files + + np.savetxt(exportBase+'_Lambda.csv',Lambda,delimiter = ',') + + np.savetxt(exportBase+'_Pitch.csv' ,Pitch ,delimiter = ',') + + np.savetxt(exportBase+'_CP.csv' ,MCP ,delimiter = ',') + + np.savetxt(exportBase+'_CT.csv' ,MCT ,delimiter = ',') + + else: + + raise NotImplementedError(exportFmt) + + + + fig = None + + if plot is True: + + # --- Plotting matrix of CP values + + fig = rs.plotCP3D() + + return rs, result, fig + + + +if __name__=='__main__': + # --- Test of templateReplace + PARAMS = {} + PARAMS['TMax'] = 10 + PARAMS['__name__'] = 'MyName' + PARAMS['DT'] = 0.01 + PARAMS['DT_Out'] = 0.1 + PARAMS['EDFile|RotSpeed'] = 100 + PARAMS['EDFile|BlPitch(1)'] = 1 + PARAMS['EDFile|GBoxEff'] = 0.92 + PARAMS['ServoFile|VS_Rgn2K'] = 0.00038245 + PARAMS['ServoFile|GenEff'] = 0.95 + PARAMS['InflowFile|HWindSpeed'] = 8 + templateReplace(PARAMS,refDir,RemoveRefSubFiles=True) + + diff --git a/pyFAST/case_generation/examples/.gitignore b/openfast_toolbox/case_generation/examples/.gitignore similarity index 100% rename from pyFAST/case_generation/examples/.gitignore rename to openfast_toolbox/case_generation/examples/.gitignore diff --git a/pyFAST/case_generation/examples/Example_CPLambdaPitch.py b/openfast_toolbox/case_generation/examples/Example_CPLambdaPitch.py similarity index 94% rename from pyFAST/case_generation/examples/Example_CPLambdaPitch.py rename to openfast_toolbox/case_generation/examples/Example_CPLambdaPitch.py index a7b98b5..fb13ddd 100644 --- a/pyFAST/case_generation/examples/Example_CPLambdaPitch.py +++ b/openfast_toolbox/case_generation/examples/Example_CPLambdaPitch.py @@ -2,8 +2,8 @@ import os import matplotlib.pyplot as plt -import pyFAST.case_generation.case_gen as case_gen -import pyFAST.input_output.postpro as postpro +import openfast_toolbox.case_generation.case_gen as case_gen +import openfast_toolbox.postpro as postpro # Get current directory so this script can be called from any location MyDir=os.path.dirname(__file__) diff --git a/pyFAST/case_generation/examples/Example_ExcelFile.py b/openfast_toolbox/case_generation/examples/Example_ExcelFile.py similarity index 92% rename from pyFAST/case_generation/examples/Example_ExcelFile.py rename to openfast_toolbox/case_generation/examples/Example_ExcelFile.py index daca408..96dbdef 100644 --- a/pyFAST/case_generation/examples/Example_ExcelFile.py +++ b/openfast_toolbox/case_generation/examples/Example_ExcelFile.py @@ -4,10 +4,10 @@ import os import pandas as pd -import pyFAST.case_generation.case_gen as case_gen -import pyFAST.case_generation.runner as runner -import pyFAST.input_output.postpro as postpro -import pyFAST.input_output as io +import openfast_toolbox.case_generation.case_gen as case_gen +import openfast_toolbox.case_generation.runner as runner +import openfast_toolbox.postpro as postpro +import openfast_toolbox.io as io # Get current directory so this script can be called from any location scriptDir=os.path.dirname(__file__) diff --git a/pyFAST/case_generation/examples/Example_Parametric.py b/openfast_toolbox/case_generation/examples/Example_Parametric.py similarity index 95% rename from pyFAST/case_generation/examples/Example_Parametric.py rename to openfast_toolbox/case_generation/examples/Example_Parametric.py index 1e3e82f..cdb2ffa 100644 --- a/pyFAST/case_generation/examples/Example_Parametric.py +++ b/openfast_toolbox/case_generation/examples/Example_Parametric.py @@ -19,10 +19,10 @@ """ import numpy as np import os -import pyFAST.case_generation.case_gen as case_gen -import pyFAST.case_generation.runner as runner -import pyFAST.input_output.postpro as postpro -from pyFAST.input_output.fast_input_file import FASTInputFile +import openfast_toolbox.case_generation.case_gen as case_gen +import openfast_toolbox.case_generation.runner as runner +import openfast_toolbox.postpro as postpro +from openfast_toolbox.io.fast_input_file import FASTInputFile # Get current directory so this script can be called from any location scriptDir=os.path.dirname(__file__) diff --git a/pyFAST/case_generation/examples/Example_PowerCurve_Parametric.py b/openfast_toolbox/case_generation/examples/Example_PowerCurve_Parametric.py similarity index 97% rename from pyFAST/case_generation/examples/Example_PowerCurve_Parametric.py rename to openfast_toolbox/case_generation/examples/Example_PowerCurve_Parametric.py index 8282cfc..df009ee 100644 --- a/pyFAST/case_generation/examples/Example_PowerCurve_Parametric.py +++ b/openfast_toolbox/case_generation/examples/Example_PowerCurve_Parametric.py @@ -1,9 +1,9 @@ import numpy as np import os -import pyFAST.case_generation.case_gen as case_gen -import pyFAST.case_generation.runner as runner -import pyFAST.input_output.postpro as postpro +import openfast_toolbox.case_generation.case_gen as case_gen +import openfast_toolbox.case_generation.runner as runner +import openfast_toolbox.postpro as postpro # Get current directory so this script can be called from any location MyDir=os.path.dirname(__file__) diff --git a/pyFAST/case_generation/examples/ParametricExcel.xlsx b/openfast_toolbox/case_generation/examples/ParametricExcel.xlsx similarity index 100% rename from pyFAST/case_generation/examples/ParametricExcel.xlsx rename to openfast_toolbox/case_generation/examples/ParametricExcel.xlsx diff --git a/pyFAST/case_generation/examples/README.md b/openfast_toolbox/case_generation/examples/README.md similarity index 100% rename from pyFAST/case_generation/examples/README.md rename to openfast_toolbox/case_generation/examples/README.md diff --git a/pyFAST/case_generation/runner.py b/openfast_toolbox/case_generation/runner.py similarity index 98% rename from pyFAST/case_generation/runner.py rename to openfast_toolbox/case_generation/runner.py index c584c2c..df7ed10 100644 --- a/pyFAST/case_generation/runner.py +++ b/openfast_toolbox/case_generation/runner.py @@ -12,8 +12,8 @@ import re # --- Fast libraries -from pyFAST.input_output.fast_input_file import FASTInputFile -from pyFAST.input_output.fast_output_file import FASTOutputFile +from openfast_toolbox.io.fast_input_file import FASTInputFile +from openfast_toolbox.io.fast_output_file import FASTOutputFile FAST_EXE='openfast' diff --git a/pyFAST/case_generation/tests/__init__.py b/openfast_toolbox/case_generation/tests/__init__.py similarity index 100% rename from pyFAST/case_generation/tests/__init__.py rename to openfast_toolbox/case_generation/tests/__init__.py diff --git a/pyFAST/case_generation/tests/test_run_Examples.py b/openfast_toolbox/case_generation/tests/test_run_Examples.py similarity index 100% rename from pyFAST/case_generation/tests/test_run_Examples.py rename to openfast_toolbox/case_generation/tests/test_run_Examples.py diff --git a/pyFAST/common.py b/openfast_toolbox/common.py similarity index 100% rename from pyFAST/common.py rename to openfast_toolbox/common.py diff --git a/pyFAST/converters/.gitignore b/openfast_toolbox/converters/.gitignore similarity index 100% rename from pyFAST/converters/.gitignore rename to openfast_toolbox/converters/.gitignore diff --git a/pyFAST/converters/README.md b/openfast_toolbox/converters/README.md similarity index 100% rename from pyFAST/converters/README.md rename to openfast_toolbox/converters/README.md diff --git a/pyFAST/converters/__init__.py b/openfast_toolbox/converters/__init__.py similarity index 100% rename from pyFAST/converters/__init__.py rename to openfast_toolbox/converters/__init__.py diff --git a/pyFAST/converters/beam.py b/openfast_toolbox/converters/beam.py similarity index 100% rename from pyFAST/converters/beam.py rename to openfast_toolbox/converters/beam.py diff --git a/pyFAST/converters/beamDynToHawc2.py b/openfast_toolbox/converters/beamDynToHawc2.py similarity index 98% rename from pyFAST/converters/beamDynToHawc2.py rename to openfast_toolbox/converters/beamDynToHawc2.py index d596059..51248ff 100644 --- a/pyFAST/converters/beamDynToHawc2.py +++ b/openfast_toolbox/converters/beamDynToHawc2.py @@ -5,11 +5,11 @@ # from weio.hawc2_htc_file import HAWC2HTCFile # from weio.csv_file import CSVFile # from weio.fast_input_file import FASTInputFile -from pyFAST.input_output.hawc2_htc_file import HAWC2HTCFile -from pyFAST.input_output.csv_file import CSVFile -from pyFAST.input_output.fast_input_file import FASTInputFile -from pyFAST.converters.beam import ComputeStiffnessProps, TransformCrossSectionMatrix -from pyFAST.input_output.fast_input_deck import FASTInputDeck +from openfast_toolbox.io.hawc2_htc_file import HAWC2HTCFile +from openfast_toolbox.io.csv_file import CSVFile +from openfast_toolbox.io.fast_input_file import FASTInputFile +from openfast_toolbox.converters.beam import ComputeStiffnessProps, TransformCrossSectionMatrix +from openfast_toolbox.io.fast_input_deck import FASTInputDeck from .beam import * from .hawc2 import dfstructure2stfile diff --git a/pyFAST/converters/beamdyn.py b/openfast_toolbox/converters/beamdyn.py similarity index 100% rename from pyFAST/converters/beamdyn.py rename to openfast_toolbox/converters/beamdyn.py diff --git a/pyFAST/converters/doc/.gitignore b/openfast_toolbox/converters/doc/.gitignore similarity index 100% rename from pyFAST/converters/doc/.gitignore rename to openfast_toolbox/converters/doc/.gitignore diff --git a/pyFAST/converters/doc/BeamDynInput.tex b/openfast_toolbox/converters/doc/BeamDynInput.tex similarity index 100% rename from pyFAST/converters/doc/BeamDynInput.tex rename to openfast_toolbox/converters/doc/BeamDynInput.tex diff --git a/pyFAST/converters/doc/Bibliography.bib b/openfast_toolbox/converters/doc/Bibliography.bib similarity index 100% rename from pyFAST/converters/doc/Bibliography.bib rename to openfast_toolbox/converters/doc/Bibliography.bib diff --git a/pyFAST/converters/doc/Makefile b/openfast_toolbox/converters/doc/Makefile similarity index 100% rename from pyFAST/converters/doc/Makefile rename to openfast_toolbox/converters/doc/Makefile diff --git a/pyFAST/converters/doc/svgtex/AeroElastCodesCoordConvention.svg b/openfast_toolbox/converters/doc/svgtex/AeroElastCodesCoordConvention.svg similarity index 100% rename from pyFAST/converters/doc/svgtex/AeroElastCodesCoordConvention.svg rename to openfast_toolbox/converters/doc/svgtex/AeroElastCodesCoordConvention.svg diff --git a/pyFAST/converters/doc/svgtex/BeamDynSectionCoord.svg b/openfast_toolbox/converters/doc/svgtex/BeamDynSectionCoord.svg similarity index 100% rename from pyFAST/converters/doc/svgtex/BeamDynSectionCoord.svg rename to openfast_toolbox/converters/doc/svgtex/BeamDynSectionCoord.svg diff --git a/pyFAST/converters/doc/svgtex/BeamDynSectionCoord_Abstract.svg b/openfast_toolbox/converters/doc/svgtex/BeamDynSectionCoord_Abstract.svg similarity index 100% rename from pyFAST/converters/doc/svgtex/BeamDynSectionCoord_Abstract.svg rename to openfast_toolbox/converters/doc/svgtex/BeamDynSectionCoord_Abstract.svg diff --git a/pyFAST/converters/doc/svgtex/StiffnessMatrixAxialBending.svg b/openfast_toolbox/converters/doc/svgtex/StiffnessMatrixAxialBending.svg similarity index 100% rename from pyFAST/converters/doc/svgtex/StiffnessMatrixAxialBending.svg rename to openfast_toolbox/converters/doc/svgtex/StiffnessMatrixAxialBending.svg diff --git a/pyFAST/converters/elastodyn.py b/openfast_toolbox/converters/elastodyn.py similarity index 100% rename from pyFAST/converters/elastodyn.py rename to openfast_toolbox/converters/elastodyn.py diff --git a/pyFAST/converters/examples/.gitignore b/openfast_toolbox/converters/examples/.gitignore similarity index 100% rename from pyFAST/converters/examples/.gitignore rename to openfast_toolbox/converters/examples/.gitignore diff --git a/pyFAST/converters/examples/Main_BeamDynToHawc2.py b/openfast_toolbox/converters/examples/Main_BeamDynToHawc2.py similarity index 98% rename from pyFAST/converters/examples/Main_BeamDynToHawc2.py rename to openfast_toolbox/converters/examples/Main_BeamDynToHawc2.py index 4946652..7ed117c 100644 --- a/pyFAST/converters/examples/Main_BeamDynToHawc2.py +++ b/openfast_toolbox/converters/examples/Main_BeamDynToHawc2.py @@ -12,7 +12,7 @@ # Local from shutil import copyfile np.set_printoptions(linewidth=1500) -import pyFAST.converters.beamdyn as bd +import openfast_toolbox.converters.beamdyn as bd # Get current directory so this script can be called from any location MyDir=os.path.dirname(__file__) diff --git a/pyFAST/converters/examples/Main_Hawc2ToAeroDyn.py b/openfast_toolbox/converters/examples/Main_Hawc2ToAeroDyn.py similarity index 94% rename from pyFAST/converters/examples/Main_Hawc2ToAeroDyn.py rename to openfast_toolbox/converters/examples/Main_Hawc2ToAeroDyn.py index 5548ab7..8a227c8 100644 --- a/pyFAST/converters/examples/Main_Hawc2ToAeroDyn.py +++ b/openfast_toolbox/converters/examples/Main_Hawc2ToAeroDyn.py @@ -1,45 +1,45 @@ -""" -Convert HAWC2 aerodynamic data to AeroDyn - -NOTE: - - Position of aerodynamic center assumed to be at c/4 from c2def. TODO -""" -import os -import numpy as np -import pandas as pd - -from pyFAST.converters.hawc2ToOpenfast import hawc2toAD -import matplotlib.pyplot as plt - -# Get current directory so this script can be called from any location -MyDir=os.path.dirname(__file__) - -def main(): - np.set_printoptions(linewidth=300) - - # --- Hawc2 aero to AeroDyn - # See documentation in hawc2ToOpenfast.py - - # --- hawc2toAD - htcFilename = os.path.join(MyDir,'../../../data/NREL5MW/hawc2/NREL_5MW_reference_wind_turbine_hs2.htc') # readonly, hawc2 model file - ADbldFilename_out = '_NREL5MW_AD_bld.dat' # full path of AeroDyn blade file to be written - polarFilebase_out = '_Polars/_Polar_' # base path where polars will be written - correction3D = True # Apply 3D correction to polar data - tsr = 9 # Tip speed ratio used for 3D correction - r_AD=None # Radial position for AeroDyn. None = same as HAWC2 - #r_AD=[0.000000 , 2.625000 , 5.250000 , 7.875000 , 10.500000, 13.125000, 15.750000, 18.375000, 21.000000, 23.625000, 26.250000, 28.875000, 31.500000, 34.125000, 36.750000, 39.375000, 42.000000, 44.625000, 47.250000, 49.875000, 52.500000, 55.125000, 57.750000, 60.375000, 63.000000, 63.100000, 63.200000, 63.300000, 63.400000, 63.438000] - - # Convert - return hawc2toAD(htcFilename, r_AD=r_AD, ADbldFilename_out=ADbldFilename_out, polarFilebase_out=polarFilebase_out, correction3D=correction3D, tsr=tsr) - -if __name__=='__main__': - aeroNodes, polars, polarFilenames = main() - print('Polar files: ',polarFilenames) - print(aeroNodes) - plt.show() - -if __name__=='__test__': - main() - import shutil - os.remove('_NREL5MW_AD_bld.dat') - shutil.rmtree('_Polars') +""" +Convert HAWC2 aerodynamic data to AeroDyn + +NOTE: + - Position of aerodynamic center assumed to be at c/4 from c2def. TODO +""" +import os +import numpy as np +import pandas as pd + +from openfast_toolbox.converters.hawc2ToOpenfast import hawc2toAD +import matplotlib.pyplot as plt + +# Get current directory so this script can be called from any location +MyDir=os.path.dirname(__file__) + +def main(): + np.set_printoptions(linewidth=300) + + # --- Hawc2 aero to AeroDyn + # See documentation in hawc2ToOpenfast.py + + # --- hawc2toAD + htcFilename = os.path.join(MyDir,'../../../data/NREL5MW/hawc2/NREL_5MW_reference_wind_turbine_hs2.htc') # readonly, hawc2 model file + ADbldFilename_out = '_NREL5MW_AD_bld.dat' # full path of AeroDyn blade file to be written + polarFilebase_out = '_Polars/_Polar_' # base path where polars will be written + correction3D = True # Apply 3D correction to polar data + tsr = 9 # Tip speed ratio used for 3D correction + r_AD=None # Radial position for AeroDyn. None = same as HAWC2 + #r_AD=[0.000000 , 2.625000 , 5.250000 , 7.875000 , 10.500000, 13.125000, 15.750000, 18.375000, 21.000000, 23.625000, 26.250000, 28.875000, 31.500000, 34.125000, 36.750000, 39.375000, 42.000000, 44.625000, 47.250000, 49.875000, 52.500000, 55.125000, 57.750000, 60.375000, 63.000000, 63.100000, 63.200000, 63.300000, 63.400000, 63.438000] + + # Convert + return hawc2toAD(htcFilename, r_AD=r_AD, ADbldFilename_out=ADbldFilename_out, polarFilebase_out=polarFilebase_out, correction3D=correction3D, tsr=tsr) + +if __name__=='__main__': + aeroNodes, polars, polarFilenames = main() + print('Polar files: ',polarFilenames) + print(aeroNodes) + plt.show() + +if __name__=='__test__': + main() + import shutil + os.remove('_NREL5MW_AD_bld.dat') + shutil.rmtree('_Polars') diff --git a/pyFAST/converters/examples/Main_Hawc2ToBeamDyn.py b/openfast_toolbox/converters/examples/Main_Hawc2ToBeamDyn.py similarity index 97% rename from pyFAST/converters/examples/Main_Hawc2ToBeamDyn.py rename to openfast_toolbox/converters/examples/Main_Hawc2ToBeamDyn.py index ee5e65c..a4ec34a 100644 --- a/pyFAST/converters/examples/Main_Hawc2ToBeamDyn.py +++ b/openfast_toolbox/converters/examples/Main_Hawc2ToBeamDyn.py @@ -5,7 +5,7 @@ import numpy as np import pandas as pd -import pyFAST.converters.beamdyn as bd +import openfast_toolbox.converters.beamdyn as bd import matplotlib.pyplot as plt # Get current directory so this script can be called from any location diff --git a/pyFAST/converters/examples/Main_OpenFASTToHawc2.py b/openfast_toolbox/converters/examples/Main_OpenFASTToHawc2.py similarity index 95% rename from pyFAST/converters/examples/Main_OpenFASTToHawc2.py rename to openfast_toolbox/converters/examples/Main_OpenFASTToHawc2.py index 5afa566..3a1a688 100644 --- a/pyFAST/converters/examples/Main_OpenFASTToHawc2.py +++ b/openfast_toolbox/converters/examples/Main_OpenFASTToHawc2.py @@ -12,7 +12,7 @@ import numpy as np import pandas as pd import os -from pyFAST.converters.openfastToHawc2 import FAST2Hawc2 +from openfast_toolbox.converters.openfastToHawc2 import FAST2Hawc2 # Get current directory so this script can be called from any location MyDir=os.path.dirname(__file__) diff --git a/pyFAST/converters/hawc2.py b/openfast_toolbox/converters/hawc2.py similarity index 100% rename from pyFAST/converters/hawc2.py rename to openfast_toolbox/converters/hawc2.py diff --git a/pyFAST/converters/hawc2ToBeamDyn.py b/openfast_toolbox/converters/hawc2ToBeamDyn.py similarity index 99% rename from pyFAST/converters/hawc2ToBeamDyn.py rename to openfast_toolbox/converters/hawc2ToBeamDyn.py index 09c2705..4d7bf7a 100644 --- a/pyFAST/converters/hawc2ToBeamDyn.py +++ b/openfast_toolbox/converters/hawc2ToBeamDyn.py @@ -2,11 +2,11 @@ from numpy import cos, sin import pandas as pd import os -from pyFAST.input_output.hawc2_htc_file import HAWC2HTCFile -from pyFAST.input_output.csv_file import CSVFile -from pyFAST.input_output.fast_input_file import FASTInputFile, BDFile +from openfast_toolbox.io.hawc2_htc_file import HAWC2HTCFile +from openfast_toolbox.io.csv_file import CSVFile +from openfast_toolbox.io.fast_input_file import FASTInputFile, BDFile -from pyFAST.tools.pandalib import pd_interp1 +from openfast_toolbox.tools.pandalib import pd_interp1 from .beam import * diff --git a/pyFAST/converters/hawc2ToElastoDyn.py b/openfast_toolbox/converters/hawc2ToElastoDyn.py similarity index 93% rename from pyFAST/converters/hawc2ToElastoDyn.py rename to openfast_toolbox/converters/hawc2ToElastoDyn.py index ff68119..b463d16 100644 --- a/pyFAST/converters/hawc2ToElastoDyn.py +++ b/openfast_toolbox/converters/hawc2ToElastoDyn.py @@ -2,10 +2,10 @@ from numpy import cos, sin import pandas as pd import os -from pyFAST.input_output.hawc2_htc_file import HAWC2HTCFile -from pyFAST.input_output.csv_file import CSVFile -from pyFAST.input_output.fast_input_file import FASTInputFile, EDBladeFile,EDTowerFile -from pyFAST.tools.pandalib import pd_interp1 +from openfast_toolbox.io.hawc2_htc_file import HAWC2HTCFile +from openfast_toolbox.io.csv_file import CSVFile +from openfast_toolbox.io.fast_input_file import FASTInputFile, EDBladeFile,EDTowerFile +from openfast_toolbox.tools.pandalib import pd_interp1 def htcToElastoDyn(HTCFile, outDir='./', prefix='', suffix='', bladeBodyName='blade1', towerBodyName='tower', rBlade=None, hTower=None): diff --git a/pyFAST/converters/hawc2ToOpenfast.py b/openfast_toolbox/converters/hawc2ToOpenfast.py similarity index 96% rename from pyFAST/converters/hawc2ToOpenfast.py rename to openfast_toolbox/converters/hawc2ToOpenfast.py index c87df17..0a1ce0a 100644 --- a/pyFAST/converters/hawc2ToOpenfast.py +++ b/openfast_toolbox/converters/hawc2ToOpenfast.py @@ -9,11 +9,11 @@ import pandas as pd import os # Local -from pyFAST.input_output.hawc2_htc_file import HAWC2HTCFile -from pyFAST.input_output.hawc2_ae_file import HAWC2AEFile -from pyFAST.input_output.hawc2_pc_file import HAWC2PCFile -from pyFAST.input_output.fast_input_file import FASTInputFile, ADBladeFile, ADPolarFile -from pyFAST.airfoils.Polar import Polar +from openfast_toolbox.io.hawc2_htc_file import HAWC2HTCFile +from openfast_toolbox.io.hawc2_ae_file import HAWC2AEFile +from openfast_toolbox.io.hawc2_pc_file import HAWC2PCFile +from openfast_toolbox.io.fast_input_file import FASTInputFile, ADBladeFile, ADPolarFile +from openfast_toolbox.airfoils.Polar import Polar diff --git a/pyFAST/converters/openfastToHawc2.py b/openfast_toolbox/converters/openfastToHawc2.py similarity index 96% rename from pyFAST/converters/openfastToHawc2.py rename to openfast_toolbox/converters/openfastToHawc2.py index d045e03..393bed8 100644 --- a/pyFAST/converters/openfastToHawc2.py +++ b/openfast_toolbox/converters/openfastToHawc2.py @@ -2,16 +2,16 @@ import numpy as np import pandas as pd -from pyFAST.input_output.hawc2_htc_file import HAWC2HTCFile -from pyFAST.input_output.hawc2_ae_file import HAWC2AEFile -from pyFAST.input_output.hawc2_pc_file import HAWC2PCFile -from pyFAST.input_output.csv_file import CSVFile -from pyFAST.input_output.fast_input_file import FASTInputFile -from pyFAST.input_output.fast_input_deck import FASTInputDeck - -import pyFAST.converters.beamdyn as bd -import pyFAST.converters.elastodyn as ed -import pyFAST.converters.hawc2 as h2 +from openfast_toolbox.io.hawc2_htc_file import HAWC2HTCFile +from openfast_toolbox.io.hawc2_ae_file import HAWC2AEFile +from openfast_toolbox.io.hawc2_pc_file import HAWC2PCFile +from openfast_toolbox.io.csv_file import CSVFile +from openfast_toolbox.io.fast_input_file import FASTInputFile +from openfast_toolbox.io.fast_input_deck import FASTInputDeck + +import openfast_toolbox.converters.beamdyn as bd +import openfast_toolbox.converters.elastodyn as ed +import openfast_toolbox.converters.hawc2 as h2 def FAST2Hawc2(fstIn, htcTemplate, htcOut, OPfile=None, TwrFAFreq=0.1, TwrSSFreq=0.1, SftTorFreq=4, FPM = False, Bld_E=None, Bld_G=None, Bld_A=None, Bld_theta_p=None): diff --git a/pyFAST/converters/tests/__init__.py b/openfast_toolbox/converters/tests/__init__.py similarity index 100% rename from pyFAST/converters/tests/__init__.py rename to openfast_toolbox/converters/tests/__init__.py diff --git a/pyFAST/converters/tests/test_K_BD_H2.py b/openfast_toolbox/converters/tests/test_K_BD_H2.py similarity index 96% rename from pyFAST/converters/tests/test_K_BD_H2.py rename to openfast_toolbox/converters/tests/test_K_BD_H2.py index 1176241..4bc26da 100644 --- a/pyFAST/converters/tests/test_K_BD_H2.py +++ b/openfast_toolbox/converters/tests/test_K_BD_H2.py @@ -1,9 +1,9 @@ import unittest import os import numpy as np -from pyFAST.converters.beam import ComputeStiffnessProps, ComputeInertiaProps, TransformCrossSectionMatrix -from pyFAST.converters.beam import MM, KK -from pyFAST.converters.beam import K66toPropsDecoupled, M66toPropsDecoupled +from openfast_toolbox.converters.beam import ComputeStiffnessProps, ComputeInertiaProps, TransformCrossSectionMatrix +from openfast_toolbox.converters.beam import MM, KK +from openfast_toolbox.converters.beam import K66toPropsDecoupled, M66toPropsDecoupled class Test(unittest.TestCase): diff --git a/pyFAST/converters/tests/test_beamprops.py b/openfast_toolbox/converters/tests/test_beamprops.py similarity index 97% rename from pyFAST/converters/tests/test_beamprops.py rename to openfast_toolbox/converters/tests/test_beamprops.py index b5b87d9..9050405 100644 --- a/pyFAST/converters/tests/test_beamprops.py +++ b/openfast_toolbox/converters/tests/test_beamprops.py @@ -1,7 +1,7 @@ import unittest import os import numpy as np -from pyFAST.converters.beam import * +from openfast_toolbox.converters.beam import * class Test(unittest.TestCase): diff --git a/pyFAST/converters/tests/test_hawc2ToBeamDyn.py b/openfast_toolbox/converters/tests/test_hawc2ToBeamDyn.py similarity index 98% rename from pyFAST/converters/tests/test_hawc2ToBeamDyn.py rename to openfast_toolbox/converters/tests/test_hawc2ToBeamDyn.py index f7daf39..9f58152 100644 --- a/pyFAST/converters/tests/test_hawc2ToBeamDyn.py +++ b/openfast_toolbox/converters/tests/test_hawc2ToBeamDyn.py @@ -2,7 +2,7 @@ import os import numpy as np from shutil import copyfile -import pyFAST.converters.beamdyn as bd +import openfast_toolbox.converters.beamdyn as bd class Test(unittest.TestCase): diff --git a/pyFAST/converters/tests/test_run_Examples.py b/openfast_toolbox/converters/tests/test_run_Examples.py similarity index 100% rename from pyFAST/converters/tests/test_run_Examples.py rename to openfast_toolbox/converters/tests/test_run_Examples.py diff --git a/pyFAST/fastfarm/.gitignore b/openfast_toolbox/fastfarm/.gitignore similarity index 85% rename from pyFAST/fastfarm/.gitignore rename to openfast_toolbox/fastfarm/.gitignore index e76d5ad..305dbd7 100644 --- a/pyFAST/fastfarm/.gitignore +++ b/openfast_toolbox/fastfarm/.gitignore @@ -1,3 +1,3 @@ -*.sum -*.bts -*.pdf +*.sum +*.bts +*.pdf diff --git a/pyFAST/fastfarm/AMRWindSimulation.py b/openfast_toolbox/fastfarm/AMRWindSimulation.py similarity index 99% rename from pyFAST/fastfarm/AMRWindSimulation.py rename to openfast_toolbox/fastfarm/AMRWindSimulation.py index a171852..67e3edf 100644 --- a/pyFAST/fastfarm/AMRWindSimulation.py +++ b/openfast_toolbox/fastfarm/AMRWindSimulation.py @@ -1,7 +1,7 @@ import numpy as np import os -from pyFAST.fastfarm.FASTFarmCaseCreation import getMultipleOf +from openfast_toolbox.fastfarm.FASTFarmCaseCreation import getMultipleOf class AMRWindSimulation: ''' diff --git a/pyFAST/fastfarm/FASTFarmCaseCreation.py b/openfast_toolbox/fastfarm/FASTFarmCaseCreation.py similarity index 99% rename from pyFAST/fastfarm/FASTFarmCaseCreation.py rename to openfast_toolbox/fastfarm/FASTFarmCaseCreation.py index 54f9c81..57ad6bd 100644 --- a/pyFAST/fastfarm/FASTFarmCaseCreation.py +++ b/openfast_toolbox/fastfarm/FASTFarmCaseCreation.py @@ -5,9 +5,9 @@ import numpy as np import xarray as xr -from pyFAST.input_output import FASTInputFile, FASTOutputFile, TurbSimFile, VTKFile -from pyFAST.fastfarm import writeFastFarm, fastFarmTurbSimExtent, plotFastFarmSetup -from pyFAST.fastfarm.TurbSimCaseCreation import TSCaseCreation, writeTimeSeriesFile +from openfast_toolbox.io import FASTInputFile, FASTOutputFile, TurbSimFile, VTKFile +from openfast_toolbox.fastfarm import writeFastFarm, fastFarmTurbSimExtent, plotFastFarmSetup +from openfast_toolbox.fastfarm.TurbSimCaseCreation import TSCaseCreation, writeTimeSeriesFile def cosd(t): return np.cos(np.deg2rad(t)) def sind(t): return np.sin(np.deg2rad(t)) @@ -453,7 +453,7 @@ def _checkInputs(self): def _determine_resolutions_from_dummy_amrwind_grid(self): - from pyFAST.fastfarm.AMRWindSimulation import AMRWindSimulation + from openfast_toolbox.fastfarm.AMRWindSimulation import AMRWindSimulation # Create values and keep variable names consistent across interfaces dummy_dt = 0.1 @@ -1190,7 +1190,7 @@ def _setRotorParameters(self): def TS_low_setup(self, writeFiles=True, runOnce=False): - # Loops on all conditions/seeds creating Low-res TurbSim box (following python-toolbox/pyFAST/fastfarm/examples/Ex1_TurbSimInputSetup.py) + # Loops on all conditions/seeds creating Low-res TurbSim box (following python-toolbox/openfast_toolbox/fastfarm/examples/Ex1_TurbSimInputSetup.py) boxType='lowres' for cond in range(self.nConditions): diff --git a/pyFAST/fastfarm/README.md b/openfast_toolbox/fastfarm/README.md similarity index 96% rename from pyFAST/fastfarm/README.md rename to openfast_toolbox/fastfarm/README.md index 815483d..055d0b6 100644 --- a/pyFAST/fastfarm/README.md +++ b/openfast_toolbox/fastfarm/README.md @@ -1,8 +1,8 @@ -#Set of tools to work with FAST.Farm - - -NOTE: the interface of these scripts is still in a preliminary phase and might be updated in the future. - -Look at the folder [examples](examples) for application examples. - - +#Set of tools to work with FAST.Farm + + +NOTE: the interface of these scripts is still in a preliminary phase and might be updated in the future. + +Look at the folder [examples](examples) for application examples. + + diff --git a/pyFAST/fastfarm/TurbSimCaseCreation.py b/openfast_toolbox/fastfarm/TurbSimCaseCreation.py similarity index 100% rename from pyFAST/fastfarm/TurbSimCaseCreation.py rename to openfast_toolbox/fastfarm/TurbSimCaseCreation.py diff --git a/pyFAST/fastfarm/__init__.py b/openfast_toolbox/fastfarm/__init__.py similarity index 94% rename from pyFAST/fastfarm/__init__.py rename to openfast_toolbox/fastfarm/__init__.py index 7da8e3f..c09f2f9 100644 --- a/pyFAST/fastfarm/__init__.py +++ b/openfast_toolbox/fastfarm/__init__.py @@ -1,4 +1,4 @@ - -from .fastfarm import * -from .TurbSimCaseCreation import TSCaseCreation - + +from .fastfarm import * +from .TurbSimCaseCreation import TSCaseCreation + diff --git a/pyFAST/fastfarm/examples/Ex1_TurbSimInputSetup.py b/openfast_toolbox/fastfarm/examples/Ex1_TurbSimInputSetup.py similarity index 95% rename from pyFAST/fastfarm/examples/Ex1_TurbSimInputSetup.py rename to openfast_toolbox/fastfarm/examples/Ex1_TurbSimInputSetup.py index 4da4642..61d5f3e 100644 --- a/pyFAST/fastfarm/examples/Ex1_TurbSimInputSetup.py +++ b/openfast_toolbox/fastfarm/examples/Ex1_TurbSimInputSetup.py @@ -1,55 +1,55 @@ -""" -Create a TurbSim input file for a FAST.Farm simulation: - - The x-y extents of the box are large enough to accomodate all turbines - - The z extent is large enough to include the rotor, start from the user specified `zbot`, - and accomodates for the meandering in the vertical direction . - - The dy and dz resolution is set of the maximum chord of the turbine - - The dt resolution is set based on the maximum frequency expected to be relevant for dynamics -""" -import os, sys -import numpy as np -import pandas as pd -import matplotlib.pyplot as plt -# Local -from pyFAST.fastfarm.TurbSimCaseCreation import TSCaseCreation - -MyDir=os.path.dirname(__file__) - -# --- Define parameters necessary for this script -OldTSFile = os.path.join(MyDir, 'SampleFiles/TestCase.inp' ) # Template file used for TurbSim, need to exist -NewTSFile = os.path.join(MyDir, 'SampleFiles/_TestCase_mod.inp') # New file that will be written -D = 77.0 # Turbine diameter (m) -HubHt = 78.045 # Hub Height (m) -Vhub = 6 # mean wind speed at hub height (m/s) -TI = 10 # turbulence intensity at hub height -PLExp = 0.2 # power law exponent for shear (-) -xlocs = [0.0, 265.643] # x positions of turbines -ylocs = [0.0, 50.0 ] # y postitions of turbines - -# --- "Optional" inputs -cmax = 5 # maximum blade chord (m). Turbine specific. -fmax = 5.0 # maximum excitation frequency (Hz). Turbine specific, 5Hz is satisfactory for modern multi-MW turbine. -zbot = 1.0 # vertical start of the turbulence box (m). Depend on hub height and expected vertical meandering of wakes. -Cmeander = 1.9 # Meandering constant (-) - -# --- Use TurbSim Case Creation class to write a new TurbSim file -Case = TSCaseCreation(D, HubHt, Vhub, TI, PLExp, x=xlocs, y=ylocs, zbot=zbot, cmax=cmax, fmax=fmax, Cmeander=Cmeander) -# Rewrite TurbSim Input File -Case.writeTSFile(OldTSFile, NewTSFile, tmax=5, turb=1) -print('NOTE: run TurbSim to generate this new BTS file.') - -# --- Visualize low extent and turbine positions -fig, ax = Case.plotSetup() - - - - -if __name__ == '__main__': - plt.show() - -if __name__ == '__test__': - pass - - - - +""" +Create a TurbSim input file for a FAST.Farm simulation: + - The x-y extents of the box are large enough to accomodate all turbines + - The z extent is large enough to include the rotor, start from the user specified `zbot`, + and accomodates for the meandering in the vertical direction . + - The dy and dz resolution is set of the maximum chord of the turbine + - The dt resolution is set based on the maximum frequency expected to be relevant for dynamics +""" +import os, sys +import numpy as np +import pandas as pd +import matplotlib.pyplot as plt +# Local +from openfast_toolbox.fastfarm.TurbSimCaseCreation import TSCaseCreation + +MyDir=os.path.dirname(__file__) + +# --- Define parameters necessary for this script +OldTSFile = os.path.join(MyDir, 'SampleFiles/TestCase.inp' ) # Template file used for TurbSim, need to exist +NewTSFile = os.path.join(MyDir, 'SampleFiles/_TestCase_mod.inp') # New file that will be written +D = 77.0 # Turbine diameter (m) +HubHt = 78.045 # Hub Height (m) +Vhub = 6 # mean wind speed at hub height (m/s) +TI = 10 # turbulence intensity at hub height +PLExp = 0.2 # power law exponent for shear (-) +xlocs = [0.0, 265.643] # x positions of turbines +ylocs = [0.0, 50.0 ] # y postitions of turbines + +# --- "Optional" inputs +cmax = 5 # maximum blade chord (m). Turbine specific. +fmax = 5.0 # maximum excitation frequency (Hz). Turbine specific, 5Hz is satisfactory for modern multi-MW turbine. +zbot = 1.0 # vertical start of the turbulence box (m). Depend on hub height and expected vertical meandering of wakes. +Cmeander = 1.9 # Meandering constant (-) + +# --- Use TurbSim Case Creation class to write a new TurbSim file +Case = TSCaseCreation(D, HubHt, Vhub, TI, PLExp, x=xlocs, y=ylocs, zbot=zbot, cmax=cmax, fmax=fmax, Cmeander=Cmeander) +# Rewrite TurbSim Input File +Case.writeTSFile(OldTSFile, NewTSFile, tmax=5, turb=1) +print('NOTE: run TurbSim to generate this new BTS file.') + +# --- Visualize low extent and turbine positions +fig, ax = Case.plotSetup() + + + + +if __name__ == '__main__': + plt.show() + +if __name__ == '__test__': + pass + + + + diff --git a/pyFAST/fastfarm/examples/Ex2_FFarmInputSetup.py b/openfast_toolbox/fastfarm/examples/Ex2_FFarmInputSetup.py similarity index 93% rename from pyFAST/fastfarm/examples/Ex2_FFarmInputSetup.py rename to openfast_toolbox/fastfarm/examples/Ex2_FFarmInputSetup.py index 5ccc63e..96895c5 100644 --- a/pyFAST/fastfarm/examples/Ex2_FFarmInputSetup.py +++ b/openfast_toolbox/fastfarm/examples/Ex2_FFarmInputSetup.py @@ -1,73 +1,73 @@ -""" -Setup a FAST.Farm input file based on a TurbSim box. - -The extent of the high res and low res domain are setup according to the guidelines: - https://openfast.readthedocs.io/en/dev/source/user/fast.farm/ModelGuidance.html - -NOTE: the box SampleFiles/TestCase.bts is not provided as part of this repository - Run TurbSim on SampleFiles/TestCase.inp to generate the box before running this example. - -""" -import os, sys -import numpy as np -import matplotlib.pyplot as plt -import pandas as pd -# Local packages -from pyFAST.fastfarm import fastFarmTurbSimExtent, writeFastFarm, plotFastFarmSetup -from pyFAST.input_output.fast_input_file import FASTInputFile - -MyDir=os.path.dirname(__file__) - -if __name__ == '__main__': - # --- FAST Farm input files - templateFSTF = os.path.join(MyDir,'SampleFiles/TestCase.fstf') # template file used for FastFarm input file, need to exist - outputFSTF = os.path.join(MyDir,'SampleFiles/_TestCase_mod.fstf')# new file that will be written - # --- Parameters for TurbSim Extent - D = 77.0 # Turbine diameter (m) - hubHeight = 78.045 # Hub Height (m) - extent_X_high = 1.2 # x-extent of high res box in diamter around turbine location - extent_YZ_high = 1.2 # y-extent of high res box in diamter around turbine location - chord_max = 5 # maximum blade chord (m). Turbine specific. - Cmeander = 1.9 # Meandering constant (-) - BTSFilename = os.path.join(MyDir,'SampleFiles/TestCase.bts') # TurbSim Box to be used in FAST.Farm simulation, need to exist. - # --- Layout - xWT = [0.0, 265.] # x positions of turbines - yWT = [0.0, 50.0] # y postitions of turbines - zWT = [0.0, 0.0 ] # z postitions of turbines - # --- Output list for turbine 1 (will be replicated for other turbines) - #OutList_Sel=[ - # 'RtAxsXT1, RtAxsYT1, RtAxsZT1', - # 'RtPosXT1, RtPosYT1, RtPosZT1', - # 'RtDiamT1', - # 'YawErrT1', - # "TIAmbT1", - # 'RtVAmbT1', - # 'RtVRelT1', - # 'W1VAmbX, W1VAmbY, W1VAmbZ'] - OutList_Sel=None - - # --- Get box extents - FFTS = fastFarmTurbSimExtent(BTSFilename, hubHeight, D, xWT, yWT, Cmeander=Cmeander, chord_max=chord_max, extent_X=extent_X_high, extent_YZ=extent_YZ_high, meanUAtHubHeight=True) - - # --- Write Fast Farm file with layout and Low and High res extent - writeFastFarm(outputFSTF, templateFSTF, xWT, yWT, zWT, FFTS=FFTS, OutListT1=OutList_Sel) - print('Output file:',outputFSTF) - - # --- Visualize low&high extent and turbine positions - fig = plotFastFarmSetup(outputFSTF, grid=True, D=D, hubHeight=hubHeight, plane='XY') - fig = plotFastFarmSetup(outputFSTF, grid=True, D=D, hubHeight=hubHeight, plane='XZ') - fig = plotFastFarmSetup(outputFSTF, grid=True, D=D, hubHeight=hubHeight, plane='YZ') - - # --- Finer tuning - #fst = FASTInputFile(outputFSTF) - #fst['InflowFile']='"../Inflow/{}_IW.dat"'.format(Case) - #fst['WrDisDT']=1.0 - #fst['DT']=1.0 - #fst.write(outputFile) - plt.show() - - - -if __name__ == '__test__': - # This example cannot be run as a test case because the BTS file is not provided in the repository - pass +""" +Setup a FAST.Farm input file based on a TurbSim box. + +The extent of the high res and low res domain are setup according to the guidelines: + https://openfast.readthedocs.io/en/dev/source/user/fast.farm/ModelGuidance.html + +NOTE: the box SampleFiles/TestCase.bts is not provided as part of this repository + Run TurbSim on SampleFiles/TestCase.inp to generate the box before running this example. + +""" +import os, sys +import numpy as np +import matplotlib.pyplot as plt +import pandas as pd +# Local packages +from openfast_toolbox.fastfarm import fastFarmTurbSimExtent, writeFastFarm, plotFastFarmSetup +from openfast_toolbox.io.fast_input_file import FASTInputFile + +MyDir=os.path.dirname(__file__) + +if __name__ == '__main__': + # --- FAST Farm input files + templateFSTF = os.path.join(MyDir,'SampleFiles/TestCase.fstf') # template file used for FastFarm input file, need to exist + outputFSTF = os.path.join(MyDir,'SampleFiles/_TestCase_mod.fstf')# new file that will be written + # --- Parameters for TurbSim Extent + D = 77.0 # Turbine diameter (m) + hubHeight = 78.045 # Hub Height (m) + extent_X_high = 1.2 # x-extent of high res box in diamter around turbine location + extent_YZ_high = 1.2 # y-extent of high res box in diamter around turbine location + chord_max = 5 # maximum blade chord (m). Turbine specific. + Cmeander = 1.9 # Meandering constant (-) + BTSFilename = os.path.join(MyDir,'SampleFiles/TestCase.bts') # TurbSim Box to be used in FAST.Farm simulation, need to exist. + # --- Layout + xWT = [0.0, 265.] # x positions of turbines + yWT = [0.0, 50.0] # y postitions of turbines + zWT = [0.0, 0.0 ] # z postitions of turbines + # --- Output list for turbine 1 (will be replicated for other turbines) + #OutList_Sel=[ + # 'RtAxsXT1, RtAxsYT1, RtAxsZT1', + # 'RtPosXT1, RtPosYT1, RtPosZT1', + # 'RtDiamT1', + # 'YawErrT1', + # "TIAmbT1", + # 'RtVAmbT1', + # 'RtVRelT1', + # 'W1VAmbX, W1VAmbY, W1VAmbZ'] + OutList_Sel=None + + # --- Get box extents + FFTS = fastFarmTurbSimExtent(BTSFilename, hubHeight, D, xWT, yWT, Cmeander=Cmeander, chord_max=chord_max, extent_X=extent_X_high, extent_YZ=extent_YZ_high, meanUAtHubHeight=True) + + # --- Write Fast Farm file with layout and Low and High res extent + writeFastFarm(outputFSTF, templateFSTF, xWT, yWT, zWT, FFTS=FFTS, OutListT1=OutList_Sel) + print('Output file:',outputFSTF) + + # --- Visualize low&high extent and turbine positions + fig = plotFastFarmSetup(outputFSTF, grid=True, D=D, hubHeight=hubHeight, plane='XY') + fig = plotFastFarmSetup(outputFSTF, grid=True, D=D, hubHeight=hubHeight, plane='XZ') + fig = plotFastFarmSetup(outputFSTF, grid=True, D=D, hubHeight=hubHeight, plane='YZ') + + # --- Finer tuning + #fst = FASTInputFile(outputFSTF) + #fst['InflowFile']='"../Inflow/{}_IW.dat"'.format(Case) + #fst['WrDisDT']=1.0 + #fst['DT']=1.0 + #fst.write(outputFile) + plt.show() + + + +if __name__ == '__test__': + # This example cannot be run as a test case because the BTS file is not provided in the repository + pass diff --git a/pyFAST/fastfarm/examples/Ex3_FFarmCompleteSetup.py b/openfast_toolbox/fastfarm/examples/Ex3_FFarmCompleteSetup.py similarity index 97% rename from pyFAST/fastfarm/examples/Ex3_FFarmCompleteSetup.py rename to openfast_toolbox/fastfarm/examples/Ex3_FFarmCompleteSetup.py index 318fb72..f2903fc 100644 --- a/pyFAST/fastfarm/examples/Ex3_FFarmCompleteSetup.py +++ b/openfast_toolbox/fastfarm/examples/Ex3_FFarmCompleteSetup.py @@ -1,161 +1,161 @@ -""" -Setup a FAST.Farm suite of cases based on input parameters. - -The extent of the high res and low res domain are setup according to the guidelines: - https://openfast.readthedocs.io/en/dev/source/user/fast.farm/ModelGuidance.html - -NOTE: If driving FAST.Farm using TurbSim inflow, the resulting boxes are necessary to - build the final FAST.Farm case and are not provided as part of this repository. - If driving FAST.Farm using LES inflow, the VTK boxes are not necessary to exist. - -""" - -from pyFAST.fastfarm.FASTFarmCaseCreation import FFCaseCreation - -def main(): - - # ----------------------------------------------------------------------------- - # USER INPUT: Modify these - # For the d{t,s}_{high,low}_les paramters, use AMRWindSimulation.py - # ----------------------------------------------------------------------------- - - # ----------- Case absolute path - path = '/complete/path/of/your/case' - - # ----------- General hard-coded parameters - cmax = 5 # maximum blade chord (m) - fmax = 10/6 # maximum excitation frequency (Hz) - Cmeander = 1.9 # Meandering constant (-) - - # ----------- Wind farm - D = 240 - zhub = 150 - wts = { - 0 :{'x':0.0, 'y':0, 'z':0.0, 'D':D, 'zhub':zhub, 'cmax':cmax, 'fmax':fmax, 'Cmeander':Cmeander}, - 1 :{'x':1852.0, 'y':0, 'z':0.0, 'D':D, 'zhub':zhub, 'cmax':cmax, 'fmax':fmax, 'Cmeander':Cmeander}, - 2 :{'x':3704.0, 'y':0, 'z':0.0, 'D':D, 'zhub':zhub, 'cmax':cmax, 'fmax':fmax, 'Cmeander':Cmeander}, - 3 :{'x':5556.0, 'y':0, 'z':0.0, 'D':D, 'zhub':zhub, 'cmax':cmax, 'fmax':fmax, 'Cmeander':Cmeander}, - 4 :{'x':7408.0, 'y':0, 'z':0.0, 'D':D, 'zhub':zhub, 'cmax':cmax, 'fmax':fmax, 'Cmeander':Cmeander}, - 5 :{'x':1852.0, 'y':1852.0, 'z':0.0, 'D':D, 'zhub':zhub, 'cmax':cmax, 'fmax':fmax, 'Cmeander':Cmeander}, - 6 :{'x':3704.0, 'y':1852.0, 'z':0.0, 'D':D, 'zhub':zhub, 'cmax':cmax, 'fmax':fmax, 'Cmeander':Cmeander}, - 7 :{'x':5556.0, 'y':1852.0, 'z':0.0, 'D':D, 'zhub':zhub, 'cmax':cmax, 'fmax':fmax, 'Cmeander':Cmeander}, - 8 :{'x':7408.0, 'y':1852.0, 'z':0.0, 'D':D, 'zhub':zhub, 'cmax':cmax, 'fmax':fmax, 'Cmeander':Cmeander}, - 9 :{'x':3704.0, 'y':3704.0, 'z':0.0, 'D':D, 'zhub':zhub, 'cmax':cmax, 'fmax':fmax, 'Cmeander':Cmeander}, - 10:{'x':5556.0, 'y':3704.0, 'z':0.0, 'D':D, 'zhub':zhub, 'cmax':cmax, 'fmax':fmax, 'Cmeander':Cmeander}, - 11:{'x':7408.0, 'y':3704.0, 'z':0.0, 'D':D, 'zhub':zhub, 'cmax':cmax, 'fmax':fmax, 'Cmeander':Cmeander}, - } - refTurb_rot = 0 - - # ----------- Additional variables - tmax = 1800 # Total simulation time - nSeeds = 6 # Number of different seeds - zbot = 1 # Bottom of your domain - mod_wake = 1 # Wake model. 1: Polar, 2: Curl, 3: Cartesian - - # ----------- Desired sweeps - vhub = [10] - shear = [0.2] - TIvalue = [10] - inflow_deg = [0] - - # ----------- Turbine parameters - # Set the yaw of each turbine for wind dir. One row for each wind direction. - yaw_init = [ [0,0,0,0,0,0,0,0,0,0,0,0] ] - - # ----------- Low- and high-res boxes parameters - # Should match LES if comparisons are to be made; otherwise, set desired values - # For an automatic computation of such parameters, omit them from the call to FFCaseCreation - # High-res boxes settings - dt_high_les = 0.6 # sampling frequency of high-res files - ds_high_les = 10.0 # dx, dy, dz that you want these high-res files at - extent_high = 1.2 # high-res box extent in y and x for each turbine, in D. - # Low-res boxes settings - dt_low_les = 3 # sampling frequency of low-res files - ds_low_les = 20.0 # dx, dy, dz of low-res files - extent_low = [3, 8, 3, 3, 2] # extent in xmin, xmax, ymin, ymax, zmax, in D - - - # ----------- Execution parameters - ffbin = '/full/path/to/your/binary/.../bin/FAST.Farm' - - # ----------- LES parameters. This variable will dictate whether it is a TurbSim-driven or LES-driven case - LESpath = '/full/path/to/the/LES/case' - #LESpath = None # set as None if TurbSim-driven is desired - - - # ----------------------------------------------------------------------------- - # ----------- Template files - templatePath = '/full/path/where/template/files/are' - - # Put 'unused' to 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 = 'unused' - 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' - - # SLURM scripts - slurm_TS_high = './SampleFiles/runAllHighBox.sh' - slurm_TS_low = './SampleFiles/runAllLowBox.sh' - slurm_FF_single = './SampleFiles/runFASTFarm_cond0_case0_seed0.sh' - - - # ----------------------------------------------------------------------------- - # END OF USER INPUT - # ----------------------------------------------------------------------------- - - - # Initial setup - case = FFCaseCreation(path, wts, tmax, zbot, vhub, shear, TIvalue, inflow_deg, - dt_high_les, ds_high_les, extent_high, - dt_low_les, ds_low_les, extent_low, - ffbin, mod_wake, yaw_init, - nSeeds=nSeeds, LESpath=LESpath, - verbose=1) - - case.setTemplateFilename(templatePath, EDfilename, SEDfilename, HDfilename, SrvDfilename, ADfilename, - ADskfilename, SubDfilename, IWfilename, BDfilepath, bladefilename, towerfilename, - turbfilename, libdisconfilepath, controllerInputfilename, coeffTablefilename, - turbsimLowfilepath, turbsimHighfilepath, FFfilename) - - # Get domain paramters - case.getDomainParameters() - - # Organize file structure - case.copyTurbineFilesForEachCase() - - # TurbSim setup - if LESpath is None: - case.TS_low_setup() - case.TS_low_slurm_prepare(slurm_TS_low) - #case.TS_low_slurm_submit() - - case.TS_high_setup() - case.TS_high_slurm_prepare(slurm_TS_high) - #case.TS_high_slurm_submit() - - # Final setup - case.FF_setup() - case.FF_slurm_prepare(slurm_FF_single) - #case.FF_slurm_submit() - - -if __name__ == '__main__': - # This example cannot be fully run. - pass +""" +Setup a FAST.Farm suite of cases based on input parameters. + +The extent of the high res and low res domain are setup according to the guidelines: + https://openfast.readthedocs.io/en/dev/source/user/fast.farm/ModelGuidance.html + +NOTE: If driving FAST.Farm using TurbSim inflow, the resulting boxes are necessary to + build the final FAST.Farm case and are not provided as part of this repository. + If driving FAST.Farm using LES inflow, the VTK boxes are not necessary to exist. + +""" + +from openfast_toolbox.fastfarm.FASTFarmCaseCreation import FFCaseCreation + +def main(): + + # ----------------------------------------------------------------------------- + # USER INPUT: Modify these + # For the d{t,s}_{high,low}_les paramters, use AMRWindSimulation.py + # ----------------------------------------------------------------------------- + + # ----------- Case absolute path + path = '/complete/path/of/your/case' + + # ----------- General hard-coded parameters + cmax = 5 # maximum blade chord (m) + fmax = 10/6 # maximum excitation frequency (Hz) + Cmeander = 1.9 # Meandering constant (-) + + # ----------- Wind farm + D = 240 + zhub = 150 + wts = { + 0 :{'x':0.0, 'y':0, 'z':0.0, 'D':D, 'zhub':zhub, 'cmax':cmax, 'fmax':fmax, 'Cmeander':Cmeander}, + 1 :{'x':1852.0, 'y':0, 'z':0.0, 'D':D, 'zhub':zhub, 'cmax':cmax, 'fmax':fmax, 'Cmeander':Cmeander}, + 2 :{'x':3704.0, 'y':0, 'z':0.0, 'D':D, 'zhub':zhub, 'cmax':cmax, 'fmax':fmax, 'Cmeander':Cmeander}, + 3 :{'x':5556.0, 'y':0, 'z':0.0, 'D':D, 'zhub':zhub, 'cmax':cmax, 'fmax':fmax, 'Cmeander':Cmeander}, + 4 :{'x':7408.0, 'y':0, 'z':0.0, 'D':D, 'zhub':zhub, 'cmax':cmax, 'fmax':fmax, 'Cmeander':Cmeander}, + 5 :{'x':1852.0, 'y':1852.0, 'z':0.0, 'D':D, 'zhub':zhub, 'cmax':cmax, 'fmax':fmax, 'Cmeander':Cmeander}, + 6 :{'x':3704.0, 'y':1852.0, 'z':0.0, 'D':D, 'zhub':zhub, 'cmax':cmax, 'fmax':fmax, 'Cmeander':Cmeander}, + 7 :{'x':5556.0, 'y':1852.0, 'z':0.0, 'D':D, 'zhub':zhub, 'cmax':cmax, 'fmax':fmax, 'Cmeander':Cmeander}, + 8 :{'x':7408.0, 'y':1852.0, 'z':0.0, 'D':D, 'zhub':zhub, 'cmax':cmax, 'fmax':fmax, 'Cmeander':Cmeander}, + 9 :{'x':3704.0, 'y':3704.0, 'z':0.0, 'D':D, 'zhub':zhub, 'cmax':cmax, 'fmax':fmax, 'Cmeander':Cmeander}, + 10:{'x':5556.0, 'y':3704.0, 'z':0.0, 'D':D, 'zhub':zhub, 'cmax':cmax, 'fmax':fmax, 'Cmeander':Cmeander}, + 11:{'x':7408.0, 'y':3704.0, 'z':0.0, 'D':D, 'zhub':zhub, 'cmax':cmax, 'fmax':fmax, 'Cmeander':Cmeander}, + } + refTurb_rot = 0 + + # ----------- Additional variables + tmax = 1800 # Total simulation time + nSeeds = 6 # Number of different seeds + zbot = 1 # Bottom of your domain + mod_wake = 1 # Wake model. 1: Polar, 2: Curl, 3: Cartesian + + # ----------- Desired sweeps + vhub = [10] + shear = [0.2] + TIvalue = [10] + inflow_deg = [0] + + # ----------- Turbine parameters + # Set the yaw of each turbine for wind dir. One row for each wind direction. + yaw_init = [ [0,0,0,0,0,0,0,0,0,0,0,0] ] + + # ----------- Low- and high-res boxes parameters + # Should match LES if comparisons are to be made; otherwise, set desired values + # For an automatic computation of such parameters, omit them from the call to FFCaseCreation + # High-res boxes settings + dt_high_les = 0.6 # sampling frequency of high-res files + ds_high_les = 10.0 # dx, dy, dz that you want these high-res files at + extent_high = 1.2 # high-res box extent in y and x for each turbine, in D. + # Low-res boxes settings + dt_low_les = 3 # sampling frequency of low-res files + ds_low_les = 20.0 # dx, dy, dz of low-res files + extent_low = [3, 8, 3, 3, 2] # extent in xmin, xmax, ymin, ymax, zmax, in D + + + # ----------- Execution parameters + ffbin = '/full/path/to/your/binary/.../bin/FAST.Farm' + + # ----------- LES parameters. This variable will dictate whether it is a TurbSim-driven or LES-driven case + LESpath = '/full/path/to/the/LES/case' + #LESpath = None # set as None if TurbSim-driven is desired + + + # ----------------------------------------------------------------------------- + # ----------- Template files + templatePath = '/full/path/where/template/files/are' + + # Put 'unused' to 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 = 'unused' + 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' + + # SLURM scripts + slurm_TS_high = './SampleFiles/runAllHighBox.sh' + slurm_TS_low = './SampleFiles/runAllLowBox.sh' + slurm_FF_single = './SampleFiles/runFASTFarm_cond0_case0_seed0.sh' + + + # ----------------------------------------------------------------------------- + # END OF USER INPUT + # ----------------------------------------------------------------------------- + + + # Initial setup + case = FFCaseCreation(path, wts, tmax, zbot, vhub, shear, TIvalue, inflow_deg, + dt_high_les, ds_high_les, extent_high, + dt_low_les, ds_low_les, extent_low, + ffbin, mod_wake, yaw_init, + nSeeds=nSeeds, LESpath=LESpath, + verbose=1) + + case.setTemplateFilename(templatePath, EDfilename, SEDfilename, HDfilename, SrvDfilename, ADfilename, + ADskfilename, SubDfilename, IWfilename, BDfilepath, bladefilename, towerfilename, + turbfilename, libdisconfilepath, controllerInputfilename, coeffTablefilename, + turbsimLowfilepath, turbsimHighfilepath, FFfilename) + + # Get domain paramters + case.getDomainParameters() + + # Organize file structure + case.copyTurbineFilesForEachCase() + + # TurbSim setup + if LESpath is None: + case.TS_low_setup() + case.TS_low_slurm_prepare(slurm_TS_low) + #case.TS_low_slurm_submit() + + case.TS_high_setup() + case.TS_high_slurm_prepare(slurm_TS_high) + #case.TS_high_slurm_submit() + + # Final setup + case.FF_setup() + case.FF_slurm_prepare(slurm_FF_single) + #case.FF_slurm_submit() + + +if __name__ == '__main__': + # This example cannot be fully run. + pass diff --git a/pyFAST/fastfarm/examples/Ex4_AMRWindSamplingSetup.py b/openfast_toolbox/fastfarm/examples/Ex4_AMRWindSamplingSetup.py similarity index 96% rename from pyFAST/fastfarm/examples/Ex4_AMRWindSamplingSetup.py rename to openfast_toolbox/fastfarm/examples/Ex4_AMRWindSamplingSetup.py index 58b9ff7..3edb70e 100644 --- a/pyFAST/fastfarm/examples/Ex4_AMRWindSamplingSetup.py +++ b/openfast_toolbox/fastfarm/examples/Ex4_AMRWindSamplingSetup.py @@ -2,7 +2,7 @@ Set up sampling planes for AMR-Wind to use for inflow winds. """ -from pyFAST.fastfarm.AMRWindSimulation import AMRWindSimulation +from openfast_toolbox.fastfarm.AMRWindSimulation import AMRWindSimulation def main(): # ----------------------------------------------------------------------------- diff --git a/pyFAST/fastfarm/examples/README.md b/openfast_toolbox/fastfarm/examples/README.md similarity index 97% rename from pyFAST/fastfarm/examples/README.md rename to openfast_toolbox/fastfarm/examples/README.md index 51b5b5d..d3df9f6 100644 --- a/pyFAST/fastfarm/examples/README.md +++ b/openfast_toolbox/fastfarm/examples/README.md @@ -1,10 +1,10 @@ - - -Example 1: Create a TurbSim input file for a FAST.Farm simulation, with proper box extent parameters. - -Example 2: Setup the high and low res parameters of FAST.Farm input using a TurbSim box - -Example 3: Creation of complete setup of FAST.Farm, including TurbSim inputs for high- and low-res boxes. Combination of Examples 1 and 2 above. - - -NOTE: the interface of these scripts is still in a preliminary phase and might be updated in the future. + + +Example 1: Create a TurbSim input file for a FAST.Farm simulation, with proper box extent parameters. + +Example 2: Setup the high and low res parameters of FAST.Farm input using a TurbSim box + +Example 3: Creation of complete setup of FAST.Farm, including TurbSim inputs for high- and low-res boxes. Combination of Examples 1 and 2 above. + + +NOTE: the interface of these scripts is still in a preliminary phase and might be updated in the future. diff --git a/pyFAST/fastfarm/examples/SampleFiles/TestCase.fstf b/openfast_toolbox/fastfarm/examples/SampleFiles/TestCase.fstf similarity index 99% rename from pyFAST/fastfarm/examples/SampleFiles/TestCase.fstf rename to openfast_toolbox/fastfarm/examples/SampleFiles/TestCase.fstf index dc32ded..c65111d 100644 --- a/pyFAST/fastfarm/examples/SampleFiles/TestCase.fstf +++ b/openfast_toolbox/fastfarm/examples/SampleFiles/TestCase.fstf @@ -1,17 +1,17 @@ -FAST.Farm v1.00.* INPUT FILE -Sample FAST.Farm input file ---- SIMULATION CONTROL --- -False Echo Echo input data to .ech? (flag) -FATAL AbortLevel Error level when simulation should abort (string) {"WARNING", "SEVERE", "FATAL"} +FAST.Farm v1.00.* INPUT FILE +Sample FAST.Farm input file +--- SIMULATION CONTROL --- +False Echo Echo input data to .ech? (flag) +FATAL AbortLevel Error level when simulation should abort (string) {"WARNING", "SEVERE", "FATAL"} 2000.0 TMax Total run time (s) [>=0.0] False UseSC Use a super controller? (flag) 2 Mod_AmbWind Ambient wind model (-) (switch) {1: high-fidelity precursor in VTK format, 2: InflowWind module} --- SUPER CONTROLLER --- [used only for UseSC=True] -"SC_DLL.dll" SC_FileName Name/location of the dynamic library {.dll [Windows] or .so [Linux]} containing the Super Controller algorithms (quoated string) ---- AMBIENT WIND: PRECURSOR IN VTK FORMAT --- [used only for Mod_AmbWind=1] -2.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 wind data files from precursor (string) +"SC_DLL.dll" SC_FileName Name/location of the dynamic library {.dll [Windows] or .so [Linux]} containing the Super Controller algorithms (quoated string) +--- AMBIENT WIND: PRECURSOR IN VTK FORMAT --- [used only for Mod_AmbWind=1] +2.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 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] 2.0 DT_Low Time step for low -resolution wind data interpolation; will be used as the global FAST.Farm time step (s) [>0.0] @@ -28,85 +28,85 @@ False ChkWndFiles Check all the ambient wind files for data 29 NX_High Number of high-resolution spatial nodes in X direction for wind data interpolation (-) [>=2] 32 NY_High Number of high-resolution spatial nodes in Y direction for wind data interpolation (-) [>=2] 54 NZ_High Number of high-resolution spatial nodes in Z direction for wind data interpolation (-) [>=2] -"InflowWind.dat" InflowFile Name of file containing InflowWind module input parameters (quoted string) ---- WIND TURBINES --- +"InflowWind.dat" InflowFile Name of file containing InflowWind module input parameters (quoted string) +--- WIND TURBINES --- 2 NumTurbines Number of wind turbines (-) [>=1] [last 6 columns used only for Mod_AmbWind=2] -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) +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) 16.926 -420.606 0.0 "../turbineModel/Test18_WT1.fst" -30.0 -507.39 1.0 3.33 3.0 3.0 0.0 0.866 0.0 "../turbineModel/Test18_WT2.fst" -40.0 -45.03 1.0 3.33 3.0 3.0 ---- WAKE DYNAMICS --- -3.0 dr Radial increment of radial finite-difference grid (m) [>0.0] -50 NumRadii Number of radii in the radial finite-difference grid (-) [>=2] -44 NumPlanes Number of wake planes (-) [>=2] -DEFAULT f_c Cutoff (corner) frequency of the low-pass time-filter for the wake advection, deflection, and meandering model (Hz) [>0.0] or DEFAULT [DEFAULT=0.0007] -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.3 ] -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.004] -DEFAULT C_NearWake Calibrated parameter for the near-wake correction (-) [>1.0 and <2.5] or DEFAULT [DEFAULT=1.8] -DEFAULT k_vAmb Calibrated parameter for the influence of ambient turbulence in the eddy viscosity (-) [>=0.0] or DEFAULT [DEFAULT=0.05 ] -DEFAULT k_vShr Calibrated parameter for the influence of the shear layer in the eddy viscosity (-) [>=0.0] or DEFAULT [DEFAULT=0.016] -DEFAULT C_vAmb_DMin Calibrated parameter in the eddy viscosity filter function for ambient turbulence defining the transitional diameter fraction between the minimum and exponential regions (-) [>=0.0 ] or DEFAULT [DEFAULT= 0.0 ] -DEFAULT C_vAmb_DMax Calibrated parameter in the eddy viscosity filter function for ambient turbulence defining the transitional diameter fraction between the exponential and maximum regions (-) [> C_vAmb_DMin ] or DEFAULT [DEFAULT= 1.0 ] -DEFAULT C_vAmb_FMin Calibrated parameter in the eddy viscosity filter function for ambient turbulence defining the value in the minimum region (-) [>=0.0 and <=1.0] or DEFAULT [DEFAULT= 1.0 ] -DEFAULT C_vAmb_Exp Calibrated parameter in the eddy viscosity filter function for ambient turbulence defining the exponent in the exponential region (-) [> 0.0 ] or DEFAULT [DEFAULT= 0.01] -DEFAULT C_vShr_DMin Calibrated parameter in the eddy viscosity filter function for the shear layer defining the transitional diameter fraction between the minimum and exponential regions (-) [>=0.0 ] or DEFAULT [DEFAULT= 3.0 ] -DEFAULT C_vShr_DMax Calibrated parameter in the eddy viscosity filter function for the shear layer defining the transitional diameter fraction between the exponential and maximum regions (-) [> C_vShr_DMin ] or DEFAULT [DEFAULT=25.0 ] -DEFAULT C_vShr_FMin Calibrated parameter in the eddy viscosity filter function for the shear layer defining the value in the minimum region (-) [>=0.0 and <=1.0] or DEFAULT [DEFAULT= 0.2 ] -DEFAULT C_vShr_Exp Calibrated parameter in the eddy viscosity filter function for the shear layer defining the exponent in the exponential region (-) [> 0.0 ] or DEFAULT [DEFAULT= 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] ---- VISUALIZATION --- -False WrDisWind Write low- and high-resolution disturbed wind data to .Low.Dis.t.vtk etc.? (flag) -1 NOutDisWindXY Number of XY planes for output of disturbed wind data across the low-resolution domain to .Low.DisXY.t.vtk (-) [0 to 9] -80.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 9] -748.0, 1252.0, 1378.0, 1504.0, 1630.0, 1756.0, 1882.0, 2008.0 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 9] -0.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] -DEFAULT 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 --- -True SumPrint Print summary data to .sum? (flag) -99999.9 ChkptTime Amount of time between creating checkpoint files for potential restart (s) [>0.0] -200.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) -20 NOutRadii Number of radial nodes for wake output for an individual rotor (-) [0 to 20] -0, 1, 2, 3, 4, 5, 7, 9, 11, 13, 15, 16, 17, 18, 19, 21, 24, 28, 33, 39 OutRadii List of radial nodes for wake output for an individual rotor (-) [1 to NOutRadii] [unused for NOutRadii=0] -7 NOutDist Number of downstream distances for wake output for an individual rotor (-) [0 to 9 ] -252.0, 378.0, 504.0, 630.0, 756.0, 882.0, 1008.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] -0.0 WindVelX List of coordinates in the X direction for wind output (m) [1 to NWindVel] [unused for NWindVel=0] -0.0 WindVelY List of coordinates in the Y direction for wind output (m) [1 to NWindVel] [unused for NWindVel=0] -80.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) -"RtAxsXT1 , RtAxsYT1 , RtAxsZT1" -"RtPosXT1 , RtPosYT1 , RtPosZT1" -"YawErrT1" -"TIAmbT1" -"CtT1N01 , CtT1N02 , CtT1N03 , CtT1N04 , CtT1N05 , CtT1N06 , CtT1N07 , CtT1N08 , CtT1N09 , CtT1N10 , CtT1N11 , CtT1N12 , CtT1N13 , CtT1N14 , CtT1N15 , CtT1N16 , CtT1N17 , CtT1N18 , CtT1N19 , CtT1N20" -"WkAxsXT1D1 , WkAxsXT1D2 , WkAxsXT1D3 , WkAxsXT1D4 , WkAxsXT1D5 , WkAxsXT1D6 , WkAxsXT1D7" -"WkAxsYT1D1 , WkAxsYT1D2 , WkAxsYT1D3 , WkAxsYT1D4 , WkAxsYT1D5 , WkAxsYT1D6 , WkAxsYT1D7" -"WkAxsZT1D1 , WkAxsZT1D2 , WkAxsZT1D3 , WkAxsZT1D4 , WkAxsZT1D5 , WkAxsZT1D6 , WkAxsZT1D7" -"WkPosXT1D1 , WkPosXT1D2 , WkPosXT1D3 , WkPosXT1D4 , WkPosXT1D5 , WkPosXT1D6 , WkPosXT1D7" -"WkPosYT1D1 , WkPosYT1D2 , WkPosYT1D3 , WkPosYT1D4 , WkPosYT1D5 , WkPosYT1D6 , WkPosYT1D7" -"WkPosZT1D1 , WkPosZT1D2 , WkPosZT1D3 , WkPosZT1D4 , WkPosZT1D5 , WkPosZT1D6 , WkPosZT1D7" -"WkDfVxT1N01D1, WkDfVxT1N02D1, WkDfVxT1N03D1, WkDfVxT1N04D1, WkDfVxT1N05D1, WkDfVxT1N06D1, WkDfVxT1N07D1, WkDfVxT1N08D1, WkDfVxT1N09D1, WkDfVxT1N10D1, WkDfVxT1N11D1, WkDfVxT1N12D1, WkDfVxT1N13D1, WkDfVxT1N14D1, WkDfVxT1N15D1, WkDfVxT1N16D1, WkDfVxT1N17D1, WkDfVxT1N18D1, WkDfVxT1N19D1, WkDfVxT1N20D1" -"WkDfVxT1N01D2, WkDfVxT1N02D2, WkDfVxT1N03D2, WkDfVxT1N04D2, WkDfVxT1N05D2, WkDfVxT1N06D2, WkDfVxT1N07D2, WkDfVxT1N08D2, WkDfVxT1N09D2, WkDfVxT1N10D2, WkDfVxT1N11D2, WkDfVxT1N12D2, WkDfVxT1N13D2, WkDfVxT1N14D2, WkDfVxT1N15D2, WkDfVxT1N16D2, WkDfVxT1N17D2, WkDfVxT1N18D2, WkDfVxT1N19D2, WkDfVxT1N20D2" -"WkDfVxT1N01D3, WkDfVxT1N02D3, WkDfVxT1N03D3, WkDfVxT1N04D3, WkDfVxT1N05D3, WkDfVxT1N06D3, WkDfVxT1N07D3, WkDfVxT1N08D3, WkDfVxT1N09D3, WkDfVxT1N10D3, WkDfVxT1N11D3, WkDfVxT1N12D3, WkDfVxT1N13D3, WkDfVxT1N14D3, WkDfVxT1N15D3, WkDfVxT1N16D3, WkDfVxT1N17D3, WkDfVxT1N18D3, WkDfVxT1N19D3, WkDfVxT1N20D3" -"WkDfVxT1N01D4, WkDfVxT1N02D4, WkDfVxT1N03D4, WkDfVxT1N04D4, WkDfVxT1N05D4, WkDfVxT1N06D4, WkDfVxT1N07D4, WkDfVxT1N08D4, WkDfVxT1N09D4, WkDfVxT1N10D4, WkDfVxT1N11D4, WkDfVxT1N12D4, WkDfVxT1N13D4, WkDfVxT1N14D4, WkDfVxT1N15D4, WkDfVxT1N16D4, WkDfVxT1N17D4, WkDfVxT1N18D4, WkDfVxT1N19D4, WkDfVxT1N20D4" -"WkDfVxT1N01D5, WkDfVxT1N02D5, WkDfVxT1N03D5, WkDfVxT1N04D5, WkDfVxT1N05D5, WkDfVxT1N06D5, WkDfVxT1N07D5, WkDfVxT1N08D5, WkDfVxT1N09D5, WkDfVxT1N10D5, WkDfVxT1N11D5, WkDfVxT1N12D5, WkDfVxT1N13D5, WkDfVxT1N14D5, WkDfVxT1N15D5, WkDfVxT1N16D5, WkDfVxT1N17D5, WkDfVxT1N18D5, WkDfVxT1N19D5, WkDfVxT1N20D5" -"WkDfVxT1N01D6, WkDfVxT1N02D6, WkDfVxT1N03D6, WkDfVxT1N04D6, WkDfVxT1N05D6, WkDfVxT1N06D6, WkDfVxT1N07D6, WkDfVxT1N08D6, WkDfVxT1N09D6, WkDfVxT1N10D6, WkDfVxT1N11D6, WkDfVxT1N12D6, WkDfVxT1N13D6, WkDfVxT1N14D6, WkDfVxT1N15D6, WkDfVxT1N16D6, WkDfVxT1N17D6, WkDfVxT1N18D6, WkDfVxT1N19D6, WkDfVxT1N20D6" -"WkDfVxT1N01D7, WkDfVxT1N02D7, WkDfVxT1N03D7, WkDfVxT1N04D7, WkDfVxT1N05D7, WkDfVxT1N06D7, WkDfVxT1N07D7, WkDfVxT1N08D7, WkDfVxT1N09D7, WkDfVxT1N10D7, WkDfVxT1N11D7, WkDfVxT1N12D7, WkDfVxT1N13D7, WkDfVxT1N14D7, WkDfVxT1N15D7, WkDfVxT1N16D7, WkDfVxT1N17D7, WkDfVxT1N18D7, WkDfVxT1N19D7, WkDfVxT1N20D7" -"WkDfVrT1N01D1, WkDfVrT1N02D1, WkDfVrT1N03D1, WkDfVrT1N04D1, WkDfVrT1N05D1, WkDfVrT1N06D1, WkDfVrT1N07D1, WkDfVrT1N08D1, WkDfVrT1N09D1, WkDfVrT1N10D1, WkDfVrT1N11D1, WkDfVrT1N12D1, WkDfVrT1N13D1, WkDfVrT1N14D1, WkDfVrT1N15D1, WkDfVrT1N16D1, WkDfVrT1N17D1, WkDfVrT1N18D1, WkDfVrT1N19D1, WkDfVrT1N20D1" -"WkDfVrT1N01D2, WkDfVrT1N02D2, WkDfVrT1N03D2, WkDfVrT1N04D2, WkDfVrT1N05D2, WkDfVrT1N06D2, WkDfVrT1N07D2, WkDfVrT1N08D2, WkDfVrT1N09D2, WkDfVrT1N10D2, WkDfVrT1N11D2, WkDfVrT1N12D2, WkDfVrT1N13D2, WkDfVrT1N14D2, WkDfVrT1N15D2, WkDfVrT1N16D2, WkDfVrT1N17D2, WkDfVrT1N18D2, WkDfVrT1N19D2, WkDfVrT1N20D2" -"WkDfVrT1N01D3, WkDfVrT1N02D3, WkDfVrT1N03D3, WkDfVrT1N04D3, WkDfVrT1N05D3, WkDfVrT1N06D3, WkDfVrT1N07D3, WkDfVrT1N08D3, WkDfVrT1N09D3, WkDfVrT1N10D3, WkDfVrT1N11D3, WkDfVrT1N12D3, WkDfVrT1N13D3, WkDfVrT1N14D3, WkDfVrT1N15D3, WkDfVrT1N16D3, WkDfVrT1N17D3, WkDfVrT1N18D3, WkDfVrT1N19D3, WkDfVrT1N20D3" -"WkDfVrT1N01D4, WkDfVrT1N02D4, WkDfVrT1N03D4, WkDfVrT1N04D4, WkDfVrT1N05D4, WkDfVrT1N06D4, WkDfVrT1N07D4, WkDfVrT1N08D4, WkDfVrT1N09D4, WkDfVrT1N10D4, WkDfVrT1N11D4, WkDfVrT1N12D4, WkDfVrT1N13D4, WkDfVrT1N14D4, WkDfVrT1N15D4, WkDfVrT1N16D4, WkDfVrT1N17D4, WkDfVrT1N18D4, WkDfVrT1N19D4, WkDfVrT1N20D4" -"WkDfVrT1N01D5, WkDfVrT1N02D5, WkDfVrT1N03D5, WkDfVrT1N04D5, WkDfVrT1N05D5, WkDfVrT1N06D5, WkDfVrT1N07D5, WkDfVrT1N08D5, WkDfVrT1N09D5, WkDfVrT1N10D5, WkDfVrT1N11D5, WkDfVrT1N12D5, WkDfVrT1N13D5, WkDfVrT1N14D5, WkDfVrT1N15D5, WkDfVrT1N16D5, WkDfVrT1N17D5, WkDfVrT1N18D5, WkDfVrT1N19D5, WkDfVrT1N20D5" -"WkDfVrT1N01D6, WkDfVrT1N02D6, WkDfVrT1N03D6, WkDfVrT1N04D6, WkDfVrT1N05D6, WkDfVrT1N06D6, WkDfVrT1N07D6, WkDfVrT1N08D6, WkDfVrT1N09D6, WkDfVrT1N10D6, WkDfVrT1N11D6, WkDfVrT1N12D6, WkDfVrT1N13D6, WkDfVrT1N14D6, WkDfVrT1N15D6, WkDfVrT1N16D6, WkDfVrT1N17D6, WkDfVrT1N18D6, WkDfVrT1N19D6, WkDfVrT1N20D6" -"WkDfVrT1N01D7, WkDfVrT1N02D7, WkDfVrT1N03D7, WkDfVrT1N04D7, WkDfVrT1N05D7, WkDfVrT1N06D7, WkDfVrT1N07D7, WkDfVrT1N08D7, WkDfVrT1N09D7, WkDfVrT1N10D7, WkDfVrT1N11D7, WkDfVrT1N12D7, WkDfVrT1N13D7, WkDfVrT1N14D7, WkDfVrT1N15D7, WkDfVrT1N16D7, WkDfVrT1N17D7, WkDfVrT1N18D7, WkDfVrT1N19D7, WkDfVrT1N20D7" -END of input file (the word "END" must appear in the first 3 columns of this last OutList line) +--- WAKE DYNAMICS --- +3.0 dr Radial increment of radial finite-difference grid (m) [>0.0] +50 NumRadii Number of radii in the radial finite-difference grid (-) [>=2] +44 NumPlanes Number of wake planes (-) [>=2] +DEFAULT f_c Cutoff (corner) frequency of the low-pass time-filter for the wake advection, deflection, and meandering model (Hz) [>0.0] or DEFAULT [DEFAULT=0.0007] +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.3 ] +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.004] +DEFAULT C_NearWake Calibrated parameter for the near-wake correction (-) [>1.0 and <2.5] or DEFAULT [DEFAULT=1.8] +DEFAULT k_vAmb Calibrated parameter for the influence of ambient turbulence in the eddy viscosity (-) [>=0.0] or DEFAULT [DEFAULT=0.05 ] +DEFAULT k_vShr Calibrated parameter for the influence of the shear layer in the eddy viscosity (-) [>=0.0] or DEFAULT [DEFAULT=0.016] +DEFAULT C_vAmb_DMin Calibrated parameter in the eddy viscosity filter function for ambient turbulence defining the transitional diameter fraction between the minimum and exponential regions (-) [>=0.0 ] or DEFAULT [DEFAULT= 0.0 ] +DEFAULT C_vAmb_DMax Calibrated parameter in the eddy viscosity filter function for ambient turbulence defining the transitional diameter fraction between the exponential and maximum regions (-) [> C_vAmb_DMin ] or DEFAULT [DEFAULT= 1.0 ] +DEFAULT C_vAmb_FMin Calibrated parameter in the eddy viscosity filter function for ambient turbulence defining the value in the minimum region (-) [>=0.0 and <=1.0] or DEFAULT [DEFAULT= 1.0 ] +DEFAULT C_vAmb_Exp Calibrated parameter in the eddy viscosity filter function for ambient turbulence defining the exponent in the exponential region (-) [> 0.0 ] or DEFAULT [DEFAULT= 0.01] +DEFAULT C_vShr_DMin Calibrated parameter in the eddy viscosity filter function for the shear layer defining the transitional diameter fraction between the minimum and exponential regions (-) [>=0.0 ] or DEFAULT [DEFAULT= 3.0 ] +DEFAULT C_vShr_DMax Calibrated parameter in the eddy viscosity filter function for the shear layer defining the transitional diameter fraction between the exponential and maximum regions (-) [> C_vShr_DMin ] or DEFAULT [DEFAULT=25.0 ] +DEFAULT C_vShr_FMin Calibrated parameter in the eddy viscosity filter function for the shear layer defining the value in the minimum region (-) [>=0.0 and <=1.0] or DEFAULT [DEFAULT= 0.2 ] +DEFAULT C_vShr_Exp Calibrated parameter in the eddy viscosity filter function for the shear layer defining the exponent in the exponential region (-) [> 0.0 ] or DEFAULT [DEFAULT= 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] +--- VISUALIZATION --- +False WrDisWind Write low- and high-resolution disturbed wind data to .Low.Dis.t.vtk etc.? (flag) +1 NOutDisWindXY Number of XY planes for output of disturbed wind data across the low-resolution domain to .Low.DisXY.t.vtk (-) [0 to 9] +80.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 9] +748.0, 1252.0, 1378.0, 1504.0, 1630.0, 1756.0, 1882.0, 2008.0 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 9] +0.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] +DEFAULT 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 --- +True SumPrint Print summary data to .sum? (flag) +99999.9 ChkptTime Amount of time between creating checkpoint files for potential restart (s) [>0.0] +200.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) +20 NOutRadii Number of radial nodes for wake output for an individual rotor (-) [0 to 20] +0, 1, 2, 3, 4, 5, 7, 9, 11, 13, 15, 16, 17, 18, 19, 21, 24, 28, 33, 39 OutRadii List of radial nodes for wake output for an individual rotor (-) [1 to NOutRadii] [unused for NOutRadii=0] +7 NOutDist Number of downstream distances for wake output for an individual rotor (-) [0 to 9 ] +252.0, 378.0, 504.0, 630.0, 756.0, 882.0, 1008.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] +0.0 WindVelX List of coordinates in the X direction for wind output (m) [1 to NWindVel] [unused for NWindVel=0] +0.0 WindVelY List of coordinates in the Y direction for wind output (m) [1 to NWindVel] [unused for NWindVel=0] +80.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) +"RtAxsXT1 , RtAxsYT1 , RtAxsZT1" +"RtPosXT1 , RtPosYT1 , RtPosZT1" +"YawErrT1" +"TIAmbT1" +"CtT1N01 , CtT1N02 , CtT1N03 , CtT1N04 , CtT1N05 , CtT1N06 , CtT1N07 , CtT1N08 , CtT1N09 , CtT1N10 , CtT1N11 , CtT1N12 , CtT1N13 , CtT1N14 , CtT1N15 , CtT1N16 , CtT1N17 , CtT1N18 , CtT1N19 , CtT1N20" +"WkAxsXT1D1 , WkAxsXT1D2 , WkAxsXT1D3 , WkAxsXT1D4 , WkAxsXT1D5 , WkAxsXT1D6 , WkAxsXT1D7" +"WkAxsYT1D1 , WkAxsYT1D2 , WkAxsYT1D3 , WkAxsYT1D4 , WkAxsYT1D5 , WkAxsYT1D6 , WkAxsYT1D7" +"WkAxsZT1D1 , WkAxsZT1D2 , WkAxsZT1D3 , WkAxsZT1D4 , WkAxsZT1D5 , WkAxsZT1D6 , WkAxsZT1D7" +"WkPosXT1D1 , WkPosXT1D2 , WkPosXT1D3 , WkPosXT1D4 , WkPosXT1D5 , WkPosXT1D6 , WkPosXT1D7" +"WkPosYT1D1 , WkPosYT1D2 , WkPosYT1D3 , WkPosYT1D4 , WkPosYT1D5 , WkPosYT1D6 , WkPosYT1D7" +"WkPosZT1D1 , WkPosZT1D2 , WkPosZT1D3 , WkPosZT1D4 , WkPosZT1D5 , WkPosZT1D6 , WkPosZT1D7" +"WkDfVxT1N01D1, WkDfVxT1N02D1, WkDfVxT1N03D1, WkDfVxT1N04D1, WkDfVxT1N05D1, WkDfVxT1N06D1, WkDfVxT1N07D1, WkDfVxT1N08D1, WkDfVxT1N09D1, WkDfVxT1N10D1, WkDfVxT1N11D1, WkDfVxT1N12D1, WkDfVxT1N13D1, WkDfVxT1N14D1, WkDfVxT1N15D1, WkDfVxT1N16D1, WkDfVxT1N17D1, WkDfVxT1N18D1, WkDfVxT1N19D1, WkDfVxT1N20D1" +"WkDfVxT1N01D2, WkDfVxT1N02D2, WkDfVxT1N03D2, WkDfVxT1N04D2, WkDfVxT1N05D2, WkDfVxT1N06D2, WkDfVxT1N07D2, WkDfVxT1N08D2, WkDfVxT1N09D2, WkDfVxT1N10D2, WkDfVxT1N11D2, WkDfVxT1N12D2, WkDfVxT1N13D2, WkDfVxT1N14D2, WkDfVxT1N15D2, WkDfVxT1N16D2, WkDfVxT1N17D2, WkDfVxT1N18D2, WkDfVxT1N19D2, WkDfVxT1N20D2" +"WkDfVxT1N01D3, WkDfVxT1N02D3, WkDfVxT1N03D3, WkDfVxT1N04D3, WkDfVxT1N05D3, WkDfVxT1N06D3, WkDfVxT1N07D3, WkDfVxT1N08D3, WkDfVxT1N09D3, WkDfVxT1N10D3, WkDfVxT1N11D3, WkDfVxT1N12D3, WkDfVxT1N13D3, WkDfVxT1N14D3, WkDfVxT1N15D3, WkDfVxT1N16D3, WkDfVxT1N17D3, WkDfVxT1N18D3, WkDfVxT1N19D3, WkDfVxT1N20D3" +"WkDfVxT1N01D4, WkDfVxT1N02D4, WkDfVxT1N03D4, WkDfVxT1N04D4, WkDfVxT1N05D4, WkDfVxT1N06D4, WkDfVxT1N07D4, WkDfVxT1N08D4, WkDfVxT1N09D4, WkDfVxT1N10D4, WkDfVxT1N11D4, WkDfVxT1N12D4, WkDfVxT1N13D4, WkDfVxT1N14D4, WkDfVxT1N15D4, WkDfVxT1N16D4, WkDfVxT1N17D4, WkDfVxT1N18D4, WkDfVxT1N19D4, WkDfVxT1N20D4" +"WkDfVxT1N01D5, WkDfVxT1N02D5, WkDfVxT1N03D5, WkDfVxT1N04D5, WkDfVxT1N05D5, WkDfVxT1N06D5, WkDfVxT1N07D5, WkDfVxT1N08D5, WkDfVxT1N09D5, WkDfVxT1N10D5, WkDfVxT1N11D5, WkDfVxT1N12D5, WkDfVxT1N13D5, WkDfVxT1N14D5, WkDfVxT1N15D5, WkDfVxT1N16D5, WkDfVxT1N17D5, WkDfVxT1N18D5, WkDfVxT1N19D5, WkDfVxT1N20D5" +"WkDfVxT1N01D6, WkDfVxT1N02D6, WkDfVxT1N03D6, WkDfVxT1N04D6, WkDfVxT1N05D6, WkDfVxT1N06D6, WkDfVxT1N07D6, WkDfVxT1N08D6, WkDfVxT1N09D6, WkDfVxT1N10D6, WkDfVxT1N11D6, WkDfVxT1N12D6, WkDfVxT1N13D6, WkDfVxT1N14D6, WkDfVxT1N15D6, WkDfVxT1N16D6, WkDfVxT1N17D6, WkDfVxT1N18D6, WkDfVxT1N19D6, WkDfVxT1N20D6" +"WkDfVxT1N01D7, WkDfVxT1N02D7, WkDfVxT1N03D7, WkDfVxT1N04D7, WkDfVxT1N05D7, WkDfVxT1N06D7, WkDfVxT1N07D7, WkDfVxT1N08D7, WkDfVxT1N09D7, WkDfVxT1N10D7, WkDfVxT1N11D7, WkDfVxT1N12D7, WkDfVxT1N13D7, WkDfVxT1N14D7, WkDfVxT1N15D7, WkDfVxT1N16D7, WkDfVxT1N17D7, WkDfVxT1N18D7, WkDfVxT1N19D7, WkDfVxT1N20D7" +"WkDfVrT1N01D1, WkDfVrT1N02D1, WkDfVrT1N03D1, WkDfVrT1N04D1, WkDfVrT1N05D1, WkDfVrT1N06D1, WkDfVrT1N07D1, WkDfVrT1N08D1, WkDfVrT1N09D1, WkDfVrT1N10D1, WkDfVrT1N11D1, WkDfVrT1N12D1, WkDfVrT1N13D1, WkDfVrT1N14D1, WkDfVrT1N15D1, WkDfVrT1N16D1, WkDfVrT1N17D1, WkDfVrT1N18D1, WkDfVrT1N19D1, WkDfVrT1N20D1" +"WkDfVrT1N01D2, WkDfVrT1N02D2, WkDfVrT1N03D2, WkDfVrT1N04D2, WkDfVrT1N05D2, WkDfVrT1N06D2, WkDfVrT1N07D2, WkDfVrT1N08D2, WkDfVrT1N09D2, WkDfVrT1N10D2, WkDfVrT1N11D2, WkDfVrT1N12D2, WkDfVrT1N13D2, WkDfVrT1N14D2, WkDfVrT1N15D2, WkDfVrT1N16D2, WkDfVrT1N17D2, WkDfVrT1N18D2, WkDfVrT1N19D2, WkDfVrT1N20D2" +"WkDfVrT1N01D3, WkDfVrT1N02D3, WkDfVrT1N03D3, WkDfVrT1N04D3, WkDfVrT1N05D3, WkDfVrT1N06D3, WkDfVrT1N07D3, WkDfVrT1N08D3, WkDfVrT1N09D3, WkDfVrT1N10D3, WkDfVrT1N11D3, WkDfVrT1N12D3, WkDfVrT1N13D3, WkDfVrT1N14D3, WkDfVrT1N15D3, WkDfVrT1N16D3, WkDfVrT1N17D3, WkDfVrT1N18D3, WkDfVrT1N19D3, WkDfVrT1N20D3" +"WkDfVrT1N01D4, WkDfVrT1N02D4, WkDfVrT1N03D4, WkDfVrT1N04D4, WkDfVrT1N05D4, WkDfVrT1N06D4, WkDfVrT1N07D4, WkDfVrT1N08D4, WkDfVrT1N09D4, WkDfVrT1N10D4, WkDfVrT1N11D4, WkDfVrT1N12D4, WkDfVrT1N13D4, WkDfVrT1N14D4, WkDfVrT1N15D4, WkDfVrT1N16D4, WkDfVrT1N17D4, WkDfVrT1N18D4, WkDfVrT1N19D4, WkDfVrT1N20D4" +"WkDfVrT1N01D5, WkDfVrT1N02D5, WkDfVrT1N03D5, WkDfVrT1N04D5, WkDfVrT1N05D5, WkDfVrT1N06D5, WkDfVrT1N07D5, WkDfVrT1N08D5, WkDfVrT1N09D5, WkDfVrT1N10D5, WkDfVrT1N11D5, WkDfVrT1N12D5, WkDfVrT1N13D5, WkDfVrT1N14D5, WkDfVrT1N15D5, WkDfVrT1N16D5, WkDfVrT1N17D5, WkDfVrT1N18D5, WkDfVrT1N19D5, WkDfVrT1N20D5" +"WkDfVrT1N01D6, WkDfVrT1N02D6, WkDfVrT1N03D6, WkDfVrT1N04D6, WkDfVrT1N05D6, WkDfVrT1N06D6, WkDfVrT1N07D6, WkDfVrT1N08D6, WkDfVrT1N09D6, WkDfVrT1N10D6, WkDfVrT1N11D6, WkDfVrT1N12D6, WkDfVrT1N13D6, WkDfVrT1N14D6, WkDfVrT1N15D6, WkDfVrT1N16D6, WkDfVrT1N17D6, WkDfVrT1N18D6, WkDfVrT1N19D6, WkDfVrT1N20D6" +"WkDfVrT1N01D7, WkDfVrT1N02D7, WkDfVrT1N03D7, WkDfVrT1N04D7, WkDfVrT1N05D7, WkDfVrT1N06D7, WkDfVrT1N07D7, WkDfVrT1N08D7, WkDfVrT1N09D7, WkDfVrT1N10D7, WkDfVrT1N11D7, WkDfVrT1N12D7, WkDfVrT1N13D7, WkDfVrT1N14D7, WkDfVrT1N15D7, WkDfVrT1N16D7, WkDfVrT1N17D7, WkDfVrT1N18D7, WkDfVrT1N19D7, WkDfVrT1N20D7" +END of input file (the word "END" must appear in the first 3 columns of this last OutList line) diff --git a/pyFAST/fastfarm/examples/SampleFiles/TestCase.inp b/openfast_toolbox/fastfarm/examples/SampleFiles/TestCase.inp similarity index 100% rename from pyFAST/fastfarm/examples/SampleFiles/TestCase.inp rename to openfast_toolbox/fastfarm/examples/SampleFiles/TestCase.inp diff --git a/pyFAST/fastfarm/examples/SampleFiles/runAllHighBox.sh b/openfast_toolbox/fastfarm/examples/SampleFiles/runAllHighBox.sh similarity index 100% rename from pyFAST/fastfarm/examples/SampleFiles/runAllHighBox.sh rename to openfast_toolbox/fastfarm/examples/SampleFiles/runAllHighBox.sh diff --git a/pyFAST/fastfarm/examples/SampleFiles/runAllLowBox.sh b/openfast_toolbox/fastfarm/examples/SampleFiles/runAllLowBox.sh similarity index 100% rename from pyFAST/fastfarm/examples/SampleFiles/runAllLowBox.sh rename to openfast_toolbox/fastfarm/examples/SampleFiles/runAllLowBox.sh diff --git a/pyFAST/fastfarm/examples/SampleFiles/runFASTFarm_cond0_case0_seed0.sh b/openfast_toolbox/fastfarm/examples/SampleFiles/runFASTFarm_cond0_case0_seed0.sh similarity index 100% rename from pyFAST/fastfarm/examples/SampleFiles/runFASTFarm_cond0_case0_seed0.sh rename to openfast_toolbox/fastfarm/examples/SampleFiles/runFASTFarm_cond0_case0_seed0.sh diff --git a/pyFAST/fastfarm/examples/SampleFiles/template_HighT1_InflowXX_SeedY.inp b/openfast_toolbox/fastfarm/examples/SampleFiles/template_HighT1_InflowXX_SeedY.inp similarity index 100% rename from pyFAST/fastfarm/examples/SampleFiles/template_HighT1_InflowXX_SeedY.inp rename to openfast_toolbox/fastfarm/examples/SampleFiles/template_HighT1_InflowXX_SeedY.inp diff --git a/pyFAST/fastfarm/examples/SampleFiles/template_Low_InflowXX_SeedY.inp b/openfast_toolbox/fastfarm/examples/SampleFiles/template_Low_InflowXX_SeedY.inp similarity index 100% rename from pyFAST/fastfarm/examples/SampleFiles/template_Low_InflowXX_SeedY.inp rename to openfast_toolbox/fastfarm/examples/SampleFiles/template_Low_InflowXX_SeedY.inp diff --git a/pyFAST/fastfarm/fastfarm.py b/openfast_toolbox/fastfarm/fastfarm.py similarity index 96% rename from pyFAST/fastfarm/fastfarm.py rename to openfast_toolbox/fastfarm/fastfarm.py index 926e438..9ac4b85 100644 --- a/pyFAST/fastfarm/fastfarm.py +++ b/openfast_toolbox/fastfarm/fastfarm.py @@ -1,648 +1,648 @@ -import os -import glob -import numpy as np -import pandas as pd -from pyFAST.input_output.fast_input_file import FASTInputFile -from pyFAST.input_output.fast_output_file import FASTOutputFile -from pyFAST.input_output.turbsim_file import TurbSimFile -import pyFAST.postpro as fastlib - -# --------------------------------------------------------------------------------} -# --- Small helper functions -# --------------------------------------------------------------------------------{ -def insertTN(s,i,nWT=1000, noLeadingZero=False): - """ insert turbine number in name """ - if nWT<10: - fmt='{:d}' - elif nWT<100: - fmt='{:02d}' - else: - fmt='{:03d}' - - if noLeadingZero: - fmt='{:d}' - - if s.find('T1')>=0: - s=s.replace('T1','T'+fmt.format(i)) - elif s.find('T0')>=0: - print('this should not be printed') - s=s.replace('T0','T'+fmt.format(i)) - else: - sp=os.path.splitext(s) - s=sp[0]+'_T'+fmt.format(i)+sp[1] - return s -def forceCopyFile (sfile, dfile): - # ---- Handling error due to wrong mod - if os.path.isfile(dfile): - if not os.access(dfile, os.W_OK): - os.chmod(dfile, stat.S_IWUSR) - #print(sfile, ' > ', dfile) - shutil.copy2(sfile, dfile) - -# --------------------------------------------------------------------------------} -# --- Tools to create fast farm simulations -# --------------------------------------------------------------------------------{ -def writeFSTandDLL(FstT1Name, nWT): - """ - Write FST files for each turbine, with different ServoDyn files and DLL - FST files, ServoFiles, and DLL files will be written next to their turbine 1 - files, with name Ti. - - FstT1Name: absolute or relative path to the Turbine FST file - """ - - FstT1Full = os.path.abspath(FstT1Name).replace('\\','/') - FstDir = os.path.dirname(FstT1Full) - - fst=FASTInputFile(FstT1Name) - SrvT1Name = fst['ServoFile'].strip('"') - SrvT1Full = os.path.join(FstDir, SrvT1Name).replace('\\','/') - SrvDir = os.path.dirname(SrvT1Full) - SrvT1RelFst = os.path.relpath(SrvT1Full,FstDir) - if os.path.exists(SrvT1Full): - srv=FASTInputFile(SrvT1Full) - DLLT1Name = srv['DLL_FileName'].strip('"') - DLLT1Full = os.path.join(SrvDir, DLLT1Name) - if os.path.exists(DLLT1Full): - servo=True - else: - print('[Info] DLL file not found, not copying servo and dll files ({})'.format(DLLT1Full)) - servo=False - else: - print('[Info] ServoDyn file not found, not copying servo and dll files ({})'.format(SrvT1Full)) - servo=False - - #print(FstDir) - #print(FstT1Full) - #print(SrvT1Name) - #print(SrvT1Full) - #print(SrvT1RelFst) - - for i in np.arange(2,nWT+1): - FstName = insertTN(FstT1Name,i,nWT) - if servo: - # TODO handle the case where T1 not present - SrvName = insertTN(SrvT1Name,i,nWT) - DLLName = insertTN(DLLT1Name,i,nWT) - DLLFullName = os.path.join(SrvDir, DLLName) - - print('') - print('FstName: ',FstName) - if servo: - print('SrvName: ',SrvName) - print('DLLName: ',DLLName) - print('DLLFull: ',DLLFullName) - - # Changing main file - if servo: - fst['ServoFile']='"'+SrvName+'"' - fst.write(FstName) - if servo: - # Changing servo file - srv['DLL_FileName']='"'+DLLName+'"' - srv.write(SrvName) - # Copying dll - forceCopyFile(DLLT1Full, DLLFullName) - - - -def rectangularLayoutSubDomains(D,Lx,Ly): - """ Retuns position of turbines in a rectangular layout - TODO, unfinished function parameters - """ - # --- Parameters - D = 112 # turbine diameter [m] - Lx = 3840 # x dimension of precusor - Ly = 3840 # y dimension of precusor - Height = 0 # Height above ground, likely 0 [m] - nDomains_x = 2 # number of domains in x - nDomains_y = 2 # number of domains in y - # --- 36 WT - nx = 3 # number of turbines to be placed along x in one precursor domain - ny = 3 # number of turbines to be placed along y in one precursor domain - StartX = 1/2 # How close do we start from the x boundary - StartY = 1/2 # How close do we start from the y boundary - # --- Derived parameters - Lx_Domain = Lx * nDomains_x # Full domain size - Ly_Domain = Ly * nDomains_y - DeltaX = Lx / (nx) # Turbine spacing - DeltaY = Ly / (ny) - xWT = np.arange(DeltaX*StartX,Lx_Domain,DeltaX) # Turbine positions - yWT = np.arange(DeltaY*StartY,Ly_Domain,DeltaY) - - print('Full domain size [D] : {:.2f} x {:.2f} '.format(Lx_Domain/D, Ly_Domain/D)) - print('Turbine spacing [D] : {:.2f} x {:.2f} '.format(DeltaX/D,DeltaX/D)) - print('Number of turbines : {:d} x {:d} = {:d}'.format(len(xWT),len(yWT),len(xWT)*len(yWT))) - - XWT,YWT=np.meshgrid(xWT,yWT) - ZWT=XWT*0+Height - - # --- Export coordinates only - M=np.column_stack((XWT.ravel(),YWT.ravel(),ZWT.ravel())) - np.savetxt('Farm_Coordinates.csv', M, delimiter=',',header='X_[m], Y_[m], Z_[m]') - print(M) - - return XWT, YWT, ZWT - - -def fastFarmTurbSimExtent(TurbSimFilename, hubHeight, D, xWT, yWT, Cmeander=1.9, chord_max=3, extent_X=1.1, extent_YZ=1.1, meanUAtHubHeight=False): - """ - Determines "Ambient Wind" box parametesr for FastFarm, based on a TurbSimFile ('bts') - - Implements the guidelines listed here: - https://openfast.readthedocs.io/en/dev/source/user/fast.farm/ModelGuidance.html - - INPUTS: - - TurbSimFilename: name of the BTS file used in the FAST.Farm simulation - - hubHeight : Hub height [m] - - D : turbine diameter [m] - - xWT : vector of x positions of the wind turbines (e.g. [0,300,600]) - - yWT : vector of y positions of the wind turbines (e.g. [0,0,0]) - - Cmeander : parameter for meandering used in FAST.Farm [-] - - chord_max : maximum chord of the wind turbine blade. Used to determine the high resolution - - extent_X : x-extent of high res box in diamter around turbine location - - extent_YZ : y-extent of high res box in diamter around turbine location - - """ - # --- TurbSim data - ts = TurbSimFile(TurbSimFilename) - - if meanUAtHubHeight: - # Use Hub Height to determine convection velocity - iy,iz = ts.closestPoint(y=0,z=hubHeight) - meanU = ts['u'][0,:,iy,iz].mean() - else: - # Use middle of the box to determine convection velocity - zMid, meanU = ts.midValues() - - return fastFarmBoxExtent(ts.y, ts.z, ts.t, meanU, hubHeight, D, xWT, yWT, Cmeander=Cmeander, chord_max=chord_max, extent_X=extent_X, extent_YZ=extent_YZ) - -def fastFarmBoxExtent(yBox, zBox, tBox, meanU, hubHeight, D, xWT, yWT, - Cmeander=1.9, chord_max=3, extent_X=1.1, extent_YZ=1.1, - extent_wake=8, LES=False): - """ - Determines "Ambient Wind" box parametesr for FastFarm, based on turbulence box parameters - INPUTS: - - yBox : y vector of grid points of the box - - zBox : z vector of grid points of the box - - tBox : time vector of the box - - meanU : mean velocity used to convect the box - - hubHeight : Hub height [m] - - D : turbine diameter [m] - - xWT : vector of x positions of the wind turbines (e.g. [0,300,600]) - - yWT : vector of y positions of the wind turbines (e.g. [0,0,0]) - - Cmeander : parameter for meandering used in FAST.Farm [-] - - chord_max : maximum chord of the wind turbine blade. Used to determine the high resolution - - extent_X : x-extent of high-res box (in diameter) around turbine location - - extent_YZ : y-extent of high-res box (in diameter) around turbine location - - extent_wake : extent of low-res box (in diameter) to add beyond the "last" wind turbine - - LES: False for TurbSim box, true for LES. Perform additional checks for LES. - """ - if LES: - raise NotImplementedError() - # --- Box resolution and extents - dY_Box = yBox[1]-yBox[0] - dZ_Box = zBox[1]-zBox[0] - dT_Box = tBox[1]-tBox[0] - dX_Box = dT_Box * meanU - Z0_Box = zBox[0] - LY_Box = yBox[-1]-yBox[0] - LZ_Box = zBox[-1]-zBox[0] - LT_Box = tBox[-1]-tBox[0] - LX_Box = LT_Box * meanU - - # --- Desired resolution, rules of thumb - dX_High_desired = chord_max - dX_Low_desired = Cmeander*D*meanU/150.0 - dY_Low_desired = dX_Low_desired - dZ_Low_desired = dX_Low_desired - dT_Low_desired = Cmeander*D/(10.0*meanU) - - # --- Suitable resolution for high res - dX_High = int(dX_High_desired/dX_Box)*dX_Box - if dX_High==0: raise Exception('The x-resolution of the box ({}) is too large and cannot satisfy the requirements for the high-res domain of dX~{} (based on chord_max). Reduce DX (or DT) of the box.'.format(dX_Box, dX_High_desired)) - dY_High = dY_Box # TODO? - dZ_High = dZ_Box # TODO? - dT_High = dT_Box # TODO? - - # --- Suitable resolution for Low res - dT_Low = int(dT_Low_desired/dT_Box )*dT_Box - dX_Low = int(dX_Low_desired/dX_High)*dX_High - dY_Low = int(dY_Low_desired/dY_High)*dY_High - dZ_Low = int(dZ_Low_desired/dZ_High)*dZ_High - if dT_Low==0: raise Exception('The time-resolution of the box ({}) is too large and cannot satisfy the requirements for the low-res domain of dT~{} (based on D & U). Reduce the DT of the box.'.format(dT_Box, dT_Low_desired)) - if dX_Low==0: raise Exception('The X-resolution of the box ({}) is too large and cannot satisfy the requirements for the low-res domain of dX~{} (based on D & U). Reduce the DX of the box.'.format(dX_Box, dX_Low_desired)) - if dY_Low==0: raise Exception('The Y-resolution of the box ({}) is too large and cannot satisfy the requirements for the low-res domain of dY~{} (based on D & U). Reduce the DY of the box.'.format(dY_Box, dY_Low_desired)) - if dZ_Low==0: raise Exception('The Z-resolution of the box ({}) is too large and cannot satisfy the requirements for the low-res domain of dZ~{} (based on D & U). Reduce the DZ of the box.'.format(dZ_Box, dZ_Low_desired)) - - # --- Low-res domain - # NOTE: more work is needed to make sure the domain encompass the turbines - # Also, we need to know the main flow direction to add a buffere with extent_wake - # Origin - nD_Before = extent_X/2 # Diameters before the first turbine to start the domain - X0_Low = np.floor( (min(xWT)-nD_Before*D-dX_Low)) # Starting on integer value for esthetics. With a dX_Low margin. - Y0_Low = np.floor( -LY_Box/2 ) # Starting on integer value for esthetics - Z0_Low = zBox[0] # we start at lowest to include tower - if LES: - if Y0_Low > min(yWT)-3*D: - Y0_Low = np.floor(min(yWT)-3*D) - # Extent NOTE: this assumes main flow about x. Might need to be changed - - XMax_Low = max(xWT) + extent_wake*D - LX_Low = XMax_Low-X0_Low - LY_Low = LY_Box - LZ_Low = LZ_Box - # Number of points - nX_Low = int(np.ceil(LX_Low/dX_Low)) - nY_Low = int(np.ceil(LY_Low/dY_Low)) - nZ_Low = int(np.ceil(LZ_Low/dZ_Low)) - # Make sure we don't exceed box in Y and Z #rt: this essentially gives us 1 less grid point than what is on the inp/bst files - if (nY_Low*dY_Low>LY_Box): nY_Low=nY_Low-1 - if (nZ_Low*dZ_Low>LZ_Box): nZ_Low=nZ_Low-1 - - # --- High-res domain extent and number of points - ZMax_High = hubHeight+extent_YZ*D/2 - Z0_High = zBox[0] # we start at lowest to include tower - LX_High = extent_X*D - LY_High = min(LY_Box, extent_YZ*D ) # Bounding to not exceed the box dimension - LZ_High = min(LZ_Box, ZMax_High-Z0_High) # Bounding to not exceed the box dimension - nX_High = int(np.ceil(LX_High/dX_High)) - nY_High = int(np.ceil(LY_High/dY_High)) - nZ_High = int(np.ceil(LZ_High/dZ_High)) - # Make sure we don't exceed box in Y and Z - if (nY_High*dY_High>LY_Box): nY_High=nY_High-1 - if (nZ_High*dZ_High>LZ_Box): nZ_High=nZ_High-1 - - # --- High-res location per turbine - X0_desired = np.asarray(xWT)-LX_High/2 # high-res is centered on turbine location - Y0_desired = np.asarray(yWT)-LY_High/2 # high-res is centered on turbine location - X0_High = X0_Low + np.floor((X0_desired-X0_Low)/dX_High)*dX_High - Y0_High = Y0_Low + np.floor((Y0_desired-Y0_Low)/dY_High)*dY_High - - d = dict() - d['DT_Low'] = np.around(dT_Low ,4) - d['DT_High'] = np.around(dT_High,4) - d['NX_Low'] = nX_Low - d['NY_Low'] = nY_Low - d['NZ_Low'] = nZ_Low - d['X0_Low'] = np.around(X0_Low,4) - d['Y0_Low'] = np.around(Y0_Low,4) - d['Z0_Low'] = np.around(Z0_Low,4) - d['dX_Low'] = np.around(dX_Low,4) - d['dY_Low'] = np.around(dY_Low,4) - d['dZ_Low'] = np.around(dZ_Low,4) - d['NX_High'] = nX_High - d['NY_High'] = nY_High - d['NZ_High'] = nZ_High - # --- High extent info for turbine outputs - d['dX_High'] = np.around(dX_High,4) - d['dY_High'] = np.around(dY_High,4) - d['dZ_High'] = np.around(dZ_High,4) - d['X0_High'] = np.around(X0_High,4) - d['Y0_High'] = np.around(Y0_High,4) - d['Z0_High'] = np.around(Z0_High,4) - # --- Misc - d['dX_des_High'] = dX_High_desired - d['dX_des_Low'] = dX_Low_desired - d['DT_des'] = dT_Low_desired - d['U_mean'] = meanU - - # --- Sanity check: check that the high res is at "almost" an integer location - X_rel = (np.array(d['X0_High'])-d['X0_Low'])/d['dX_High'] - Y_rel = (np.array(d['Y0_High'])-d['Y0_Low'])/d['dY_High'] - dX = X_rel - np.round(X_rel) # Should be close to zero - dY = Y_rel - np.round(Y_rel) # Should be close to zero - if any(abs(dX)>1e-3): - print('Deltas:',dX) - print('Exception has been raise. I put this print statement instead. Check with EB.') - print('Exception: Some X0_High are not on an integer multiple of the high-res grid') - #raise Exception('Some X0_High are not on an integer multiple of the high-res grid') - if any(abs(dY)>1e-3): - print('Deltas:',dY) - print('Exception has been raise. I put this print statement instead. Check with EB.') - print('Exception: Some Y0_High are not on an integer multiple of the high-res grid') - #raise Exception('Some Y0_High are not on an integer multiple of the high-res grid') - - return d - - -def writeFastFarm(outputFile, templateFile, xWT, yWT, zWT, FFTS=None, OutListT1=None, noLeadingZero=False): - """ Write FastFarm input file based on a template, a TurbSimFile and the Layout - - outputFile: .fstf file to be written - templateFile: .fstf file that will be used to generate the output_file - XWT,YWT,ZWT: positions of turbines - FFTS: FastFarm TurbSim parameters as returned by fastFarmTurbSimExtent - """ - # --- Read template fast farm file - fst=FASTInputFile(templateFile) - # --- Replace box extent values - if FFTS is not None: - fst['Mod_AmbWind'] = 2 - ModVars = ['DT_Low', 'DT_High', 'NX_Low', 'NY_Low', 'NZ_Low', 'X0_Low', 'Y0_Low', 'Z0_Low', 'dX_Low', 'dY_Low', 'dZ_Low', 'NX_High', 'NY_High', 'NZ_High'] - for k in ModVars: - if isinstance(FFTS[k],int): - fst[k] = FFTS[k] - else: - fst[k] = np.around(FFTS[k],3) - fst['WrDisDT'] = FFTS['DT_Low'] - - # --- Set turbine names, position, and box extent - nWT = len(xWT) - fst['NumTurbines'] = nWT - if FFTS is not None: - nCol= 10 - else: - nCol = 4 - ref_path = fst['WindTurbines'][0,3] - WT = np.array(['']*nWT*nCol,dtype='object').reshape((nWT,nCol)) - for iWT,(x,y,z) in enumerate(zip(xWT,yWT,zWT)): - WT[iWT,0]=x - WT[iWT,1]=y - WT[iWT,2]=z - WT[iWT,3]=insertTN(ref_path,iWT+1,nWT,noLeadingZero=noLeadingZero) - if FFTS is not None: - WT[iWT,4]=FFTS['X0_High'][iWT] - WT[iWT,5]=FFTS['Y0_High'][iWT] - WT[iWT,6]=FFTS['Z0_High'] - WT[iWT,7]=FFTS['dX_High'] - WT[iWT,8]=FFTS['dY_High'] - WT[iWT,9]=FFTS['dZ_High'] - fst['WindTurbines']=WT - - fst.write(outputFile) - if OutListT1 is not None: - setFastFarmOutputs(outputFile, OutListT1) - -def setFastFarmOutputs(fastFarmFile, OutListT1): - """ Duplicate the output list, by replacing "T1" with T1->Tn """ - fst = FASTInputFile(fastFarmFile) - nWTOut = min(fst['NumTurbines'],9) # Limited to 9 turbines - OutList=[''] - for s in OutListT1: - s=s.strip('"') - if 'T1' in s: - OutList+=['"'+s.replace('T1','T{:d}'.format(iWT+1))+'"' for iWT in np.arange(nWTOut) ] - elif 'W1VAmb' in s: # special case for ambient wind - OutList+=['"'+s.replace('1','{:d}'.format(iWT+1))+'"' for iWT in np.arange(nWTOut) ] - elif 'W1VDis' in s: # special case for disturbed wind - OutList+=['"'+s.replace('1','{:d}'.format(iWT+1))+'"' for iWT in np.arange(nWTOut) ] - else: - OutList+='"'+s+'"' - fst['OutList']=OutList - fst.write(fastFarmFile) - - -def plotFastFarmSetup(fastFarmFile, grid=True, fig=None, D=None, plane='XY', hubHeight=None, showLegend=True): - """ """ - import matplotlib.pyplot as plt - - 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 - - - # --- Read FAST.Farm input file - fst=FASTInputFile(fastFarmFile) - - if fig is None: - fig = plt.figure(figsize=(13.5,8)) - 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) - - return fig - -# --------------------------------------------------------------------------------} -# --- Tools for postpro -# --------------------------------------------------------------------------------{ - -def spanwiseColFastFarm(Cols, nWT=9, nD=9): - """ Return column info, available columns and indices that contain AD spanwise data""" - FFSpanMap=dict() - for i in np.arange(nWT): - FFSpanMap[r'^CtT{:d}N(\d*)_\[-\]'.format(i+1)]='CtT{:d}_[-]'.format(i+1) - for i in np.arange(nWT): - for k in np.arange(nD): - FFSpanMap[r'^WkDfVxT{:d}N(\d*)D{:d}_\[m/s\]'.format(i+1,k+1) ]='WkDfVxT{:d}D{:d}_[m/s]'.format(i+1, k+1) - for i in np.arange(nWT): - for k in np.arange(nD): - FFSpanMap[r'^WkDfVrT{:d}N(\d*)D{:d}_\[m/s\]'.format(i+1,k+1) ]='WkDfVrT{:d}D{:d}_[m/s]'.format(i+1, k+1) - - return fastlib.find_matching_columns(Cols, FFSpanMap) - -def diameterwiseColFastFarm(Cols, nWT=9): - """ Return column info, available columns and indices that contain AD spanwise data""" - FFDiamMap=dict() - for i in np.arange(nWT): - for x in ['X','Y','Z']: - FFDiamMap[r'^WkAxs{}T{:d}D(\d*)_\[-\]'.format(x,i+1)] ='WkAxs{}T{:d}_[-]'.format(x,i+1) - for i in np.arange(nWT): - for x in ['X','Y','Z']: - FFDiamMap[r'^WkPos{}T{:d}D(\d*)_\[m\]'.format(x,i+1)] ='WkPos{}T{:d}_[m]'.format(x,i+1) - for i in np.arange(nWT): - for x in ['X','Y','Z']: - FFDiamMap[r'^WkVel{}T{:d}D(\d*)_\[m/s\]'.format(x,i+1)] ='WkVel{}T{:d}_[m/s]'.format(x,i+1) - for i in np.arange(nWT): - for x in ['X','Y','Z']: - FFDiamMap[r'^WkDiam{}T{:d}D(\d*)_\[m\]'.format(x,i+1)] ='WkDiam{}T{:d}_[m]'.format(x,i+1) - return fastlib.find_matching_columns(Cols, FFDiamMap) - -def SensorsFARMRadial(nWT=3,nD=10,nR=30,signals=None): - """ Returns a list of FASTFarm sensors that are used for the radial distribution - of quantities (e.g. Ct, Wake Deficits). - If `signals` is provided, the output is the list of sensors within the list `signals`. - """ - WT = np.arange(nWT) - r = np.arange(nR) - D = np.arange(nD) - sens=[] - sens+=['CtT{:d}N{:02d}_[-]'.format(i+1,j+1) for i in WT for j in r] - sens+=['WkDfVxT{:d}N{:02d}D{:d}_[m/s]'.format(i+1,j+1,k+1) for i in WT for j in r for k in D] - sens+=['WkDfVrT{:d}N{:02d}D{:d}_[m/s]'.format(i+1,j+1,k+1) for i in WT for j in r for k in D] - if signals is not None: - sens = [c for c in sens if c in signals] - return sens - -def SensorsFARMDiam(nWT,nD): - """ Returns a list of FASTFarm sensors that contain quantities at different downstream diameters - (e.g. WkAxs, WkPos, WkVel, WkDiam) - If `signals` is provided, the output is the list of sensors within the list `signals`. - """ - WT = np.arange(nWT) - D = np.arange(nD) - XYZ = ['X','Y','Z'] - sens=[] - sens+=['WkAxs{}T{:d}D{:d}_[-]'.format(x,i+1,j+1) for x in XYZ for i in WT for j in D] - sens+=['WkPos{}T{:d}D{:d}_[m]'.format(x,i+1,j+1) for x in XYZ for i in WT for j in D] - sens+=['WkVel{}T{:d}D{:d}_[m/s]'.format(x,i+1,j+1) for x in XYZ for i in WT for j in D] - sens+=['WkDiam{}T{:d}D{:d}_[m]'.format(x,i+1,j+1) for x in XYZ for i in WT for j in D] - if signals is not None: - sens = [c for c in sens if c in signals] - return sens - - -def extractFFRadialData(fastfarm_out,fastfarm_input,avgMethod='constantwindow',avgParam=30,D=1,df=None): - # LEGACY - return spanwisePostProFF(fastfarm_input,avgMethod=avgMethod,avgParam=avgParam,D=D,df=df,fastfarm_out=fastfarm_out) - - -def spanwisePostProFF(fastfarm_input,avgMethod='constantwindow',avgParam=30,D=1,df=None,fastfarm_out=None): - """ - Opens a FASTFarm output file, extract the radial data, average them and returns spanwise data - - D: diameter TODO, extract it from the main file - - See faslibt.averageDF for `avgMethod` and `avgParam`. - """ - # --- Opening ouputfile - if df is None: - df=FASTOutputFile(fastfarm_out).toDataFrame() - - # --- Opening input file and extracting inportant variables - if fastfarm_input is None: - # We don't have an input file, guess numbers of turbine, diameters, Nodes... - cols, sIdx = fastlib.find_matching_pattern(df.columns.values, r'T(\d+)') - nWT = np.array(sIdx).astype(int).max() - cols, sIdx = fastlib.find_matching_pattern(df.columns.values, r'D(\d+)') - nD = np.array(sIdx).astype(int).max() - cols, sIdx = fastlib.find_matching_pattern(df.columns.values, r'N(\d+)') - nr = np.array(sIdx).astype(int).max() - vr=None - vD=None - D=0 - main = None - else: - main=FASTInputFile(fastfarm_input) - iOut = main['OutRadii'] - dr = main['dr'] # Radial increment of radial finite-difference grid (m) - OutDist = main['OutDist'] # List of downstream distances for wake output for an individual rotor - WT = main['WindTurbines'] - nWT = len(WT) - vr = dr*np.array(iOut) - vD = np.array(OutDist) - nr=len(iOut) - nD=len(vD) - - - # --- Extracting time series of radial data only - colRadial = SensorsFARMRadial(nWT=nWT,nD=nD,nR=nr,signals=df.columns.values) - colRadial=['Time_[s]']+colRadial - dfRadialTime = df[colRadial] # TODO try to do some magic with it, display it with a slider - - # --- Averaging data - dfAvg = fastlib.averageDF(df,avgMethod=avgMethod,avgParam=avgParam) - - # --- Extract radial data - ColsInfo, nrMax = spanwiseColFastFarm(df.columns.values, nWT=nWT, nD=nD) - dfRad = fastlib.extract_spanwise_data(ColsInfo, nrMax, df=None, ts=dfAvg.iloc[0]) - #dfRad = fastlib.insert_radial_columns(dfRad, vr) - if dfRad is not None: - dfRad.insert(0, 'i_[#]', np.arange(nrMax)+1) # For all, to ease comparison - if vr is not None: - dfRad.insert(0, 'r_[m]', vr[:nrMax]) # give priority to r_[m] when available - dfRad['i/n_[-]']=np.arange(nrMax)/nrMax - - # --- Extract downstream data - ColsInfo, nDMax = diameterwiseColFastFarm(df.columns.values, nWT=nWT) - dfDiam = fastlib.extract_spanwise_data(ColsInfo, nDMax, df=None, ts=dfAvg.iloc[0]) - if dfDiam is not None: - dfDiam.insert(0, 'i_[#]', np.arange(nDMax)+1) # For all, to ease comparison - if vD is not None: - dfDiam.insert(0, 'x_[m]', vD[:nDMax]) - dfDiam['i/n_[-]'] = np.arange(nDMax)/nDMax - return dfRad, dfRadialTime, dfDiam - +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.turbsim_file import TurbSimFile +import openfast_toolbox.postpro as fastlib + +# --------------------------------------------------------------------------------} +# --- Small helper functions +# --------------------------------------------------------------------------------{ +def insertTN(s,i,nWT=1000, noLeadingZero=False): + """ insert turbine number in name """ + if nWT<10: + fmt='{:d}' + elif nWT<100: + fmt='{:02d}' + else: + fmt='{:03d}' + + if noLeadingZero: + fmt='{:d}' + + if s.find('T1')>=0: + s=s.replace('T1','T'+fmt.format(i)) + elif s.find('T0')>=0: + print('this should not be printed') + s=s.replace('T0','T'+fmt.format(i)) + else: + sp=os.path.splitext(s) + s=sp[0]+'_T'+fmt.format(i)+sp[1] + return s +def forceCopyFile (sfile, dfile): + # ---- Handling error due to wrong mod + if os.path.isfile(dfile): + if not os.access(dfile, os.W_OK): + os.chmod(dfile, stat.S_IWUSR) + #print(sfile, ' > ', dfile) + shutil.copy2(sfile, dfile) + +# --------------------------------------------------------------------------------} +# --- Tools to create fast farm simulations +# --------------------------------------------------------------------------------{ +def writeFSTandDLL(FstT1Name, nWT): + """ + Write FST files for each turbine, with different ServoDyn files and DLL + FST files, ServoFiles, and DLL files will be written next to their turbine 1 + files, with name Ti. + + FstT1Name: absolute or relative path to the Turbine FST file + """ + + FstT1Full = os.path.abspath(FstT1Name).replace('\\','/') + FstDir = os.path.dirname(FstT1Full) + + fst=FASTInputFile(FstT1Name) + SrvT1Name = fst['ServoFile'].strip('"') + SrvT1Full = os.path.join(FstDir, SrvT1Name).replace('\\','/') + SrvDir = os.path.dirname(SrvT1Full) + SrvT1RelFst = os.path.relpath(SrvT1Full,FstDir) + if os.path.exists(SrvT1Full): + srv=FASTInputFile(SrvT1Full) + DLLT1Name = srv['DLL_FileName'].strip('"') + DLLT1Full = os.path.join(SrvDir, DLLT1Name) + if os.path.exists(DLLT1Full): + servo=True + else: + print('[Info] DLL file not found, not copying servo and dll files ({})'.format(DLLT1Full)) + servo=False + else: + print('[Info] ServoDyn file not found, not copying servo and dll files ({})'.format(SrvT1Full)) + servo=False + + #print(FstDir) + #print(FstT1Full) + #print(SrvT1Name) + #print(SrvT1Full) + #print(SrvT1RelFst) + + for i in np.arange(2,nWT+1): + FstName = insertTN(FstT1Name,i,nWT) + if servo: + # TODO handle the case where T1 not present + SrvName = insertTN(SrvT1Name,i,nWT) + DLLName = insertTN(DLLT1Name,i,nWT) + DLLFullName = os.path.join(SrvDir, DLLName) + + print('') + print('FstName: ',FstName) + if servo: + print('SrvName: ',SrvName) + print('DLLName: ',DLLName) + print('DLLFull: ',DLLFullName) + + # Changing main file + if servo: + fst['ServoFile']='"'+SrvName+'"' + fst.write(FstName) + if servo: + # Changing servo file + srv['DLL_FileName']='"'+DLLName+'"' + srv.write(SrvName) + # Copying dll + forceCopyFile(DLLT1Full, DLLFullName) + + + +def rectangularLayoutSubDomains(D,Lx,Ly): + """ Retuns position of turbines in a rectangular layout + TODO, unfinished function parameters + """ + # --- Parameters + D = 112 # turbine diameter [m] + Lx = 3840 # x dimension of precusor + Ly = 3840 # y dimension of precusor + Height = 0 # Height above ground, likely 0 [m] + nDomains_x = 2 # number of domains in x + nDomains_y = 2 # number of domains in y + # --- 36 WT + nx = 3 # number of turbines to be placed along x in one precursor domain + ny = 3 # number of turbines to be placed along y in one precursor domain + StartX = 1/2 # How close do we start from the x boundary + StartY = 1/2 # How close do we start from the y boundary + # --- Derived parameters + Lx_Domain = Lx * nDomains_x # Full domain size + Ly_Domain = Ly * nDomains_y + DeltaX = Lx / (nx) # Turbine spacing + DeltaY = Ly / (ny) + xWT = np.arange(DeltaX*StartX,Lx_Domain,DeltaX) # Turbine positions + yWT = np.arange(DeltaY*StartY,Ly_Domain,DeltaY) + + print('Full domain size [D] : {:.2f} x {:.2f} '.format(Lx_Domain/D, Ly_Domain/D)) + print('Turbine spacing [D] : {:.2f} x {:.2f} '.format(DeltaX/D,DeltaX/D)) + print('Number of turbines : {:d} x {:d} = {:d}'.format(len(xWT),len(yWT),len(xWT)*len(yWT))) + + XWT,YWT=np.meshgrid(xWT,yWT) + ZWT=XWT*0+Height + + # --- Export coordinates only + M=np.column_stack((XWT.ravel(),YWT.ravel(),ZWT.ravel())) + np.savetxt('Farm_Coordinates.csv', M, delimiter=',',header='X_[m], Y_[m], Z_[m]') + print(M) + + return XWT, YWT, ZWT + + +def fastFarmTurbSimExtent(TurbSimFilename, hubHeight, D, xWT, yWT, Cmeander=1.9, chord_max=3, extent_X=1.1, extent_YZ=1.1, meanUAtHubHeight=False): + """ + Determines "Ambient Wind" box parametesr for FastFarm, based on a TurbSimFile ('bts') + + Implements the guidelines listed here: + https://openfast.readthedocs.io/en/dev/source/user/fast.farm/ModelGuidance.html + + INPUTS: + - TurbSimFilename: name of the BTS file used in the FAST.Farm simulation + - hubHeight : Hub height [m] + - D : turbine diameter [m] + - xWT : vector of x positions of the wind turbines (e.g. [0,300,600]) + - yWT : vector of y positions of the wind turbines (e.g. [0,0,0]) + - Cmeander : parameter for meandering used in FAST.Farm [-] + - chord_max : maximum chord of the wind turbine blade. Used to determine the high resolution + - extent_X : x-extent of high res box in diamter around turbine location + - extent_YZ : y-extent of high res box in diamter around turbine location + + """ + # --- TurbSim data + ts = TurbSimFile(TurbSimFilename) + + if meanUAtHubHeight: + # Use Hub Height to determine convection velocity + iy,iz = ts.closestPoint(y=0,z=hubHeight) + meanU = ts['u'][0,:,iy,iz].mean() + else: + # Use middle of the box to determine convection velocity + zMid, meanU = ts.midValues() + + return fastFarmBoxExtent(ts.y, ts.z, ts.t, meanU, hubHeight, D, xWT, yWT, Cmeander=Cmeander, chord_max=chord_max, extent_X=extent_X, extent_YZ=extent_YZ) + +def fastFarmBoxExtent(yBox, zBox, tBox, meanU, hubHeight, D, xWT, yWT, + Cmeander=1.9, chord_max=3, extent_X=1.1, extent_YZ=1.1, + extent_wake=8, LES=False): + """ + Determines "Ambient Wind" box parametesr for FastFarm, based on turbulence box parameters + INPUTS: + - yBox : y vector of grid points of the box + - zBox : z vector of grid points of the box + - tBox : time vector of the box + - meanU : mean velocity used to convect the box + - hubHeight : Hub height [m] + - D : turbine diameter [m] + - xWT : vector of x positions of the wind turbines (e.g. [0,300,600]) + - yWT : vector of y positions of the wind turbines (e.g. [0,0,0]) + - Cmeander : parameter for meandering used in FAST.Farm [-] + - chord_max : maximum chord of the wind turbine blade. Used to determine the high resolution + - extent_X : x-extent of high-res box (in diameter) around turbine location + - extent_YZ : y-extent of high-res box (in diameter) around turbine location + - extent_wake : extent of low-res box (in diameter) to add beyond the "last" wind turbine + - LES: False for TurbSim box, true for LES. Perform additional checks for LES. + """ + if LES: + raise NotImplementedError() + # --- Box resolution and extents + dY_Box = yBox[1]-yBox[0] + dZ_Box = zBox[1]-zBox[0] + dT_Box = tBox[1]-tBox[0] + dX_Box = dT_Box * meanU + Z0_Box = zBox[0] + LY_Box = yBox[-1]-yBox[0] + LZ_Box = zBox[-1]-zBox[0] + LT_Box = tBox[-1]-tBox[0] + LX_Box = LT_Box * meanU + + # --- Desired resolution, rules of thumb + dX_High_desired = chord_max + dX_Low_desired = Cmeander*D*meanU/150.0 + dY_Low_desired = dX_Low_desired + dZ_Low_desired = dX_Low_desired + dT_Low_desired = Cmeander*D/(10.0*meanU) + + # --- Suitable resolution for high res + dX_High = int(dX_High_desired/dX_Box)*dX_Box + if dX_High==0: raise Exception('The x-resolution of the box ({}) is too large and cannot satisfy the requirements for the high-res domain of dX~{} (based on chord_max). Reduce DX (or DT) of the box.'.format(dX_Box, dX_High_desired)) + dY_High = dY_Box # TODO? + dZ_High = dZ_Box # TODO? + dT_High = dT_Box # TODO? + + # --- Suitable resolution for Low res + dT_Low = int(dT_Low_desired/dT_Box )*dT_Box + dX_Low = int(dX_Low_desired/dX_High)*dX_High + dY_Low = int(dY_Low_desired/dY_High)*dY_High + dZ_Low = int(dZ_Low_desired/dZ_High)*dZ_High + if dT_Low==0: raise Exception('The time-resolution of the box ({}) is too large and cannot satisfy the requirements for the low-res domain of dT~{} (based on D & U). Reduce the DT of the box.'.format(dT_Box, dT_Low_desired)) + if dX_Low==0: raise Exception('The X-resolution of the box ({}) is too large and cannot satisfy the requirements for the low-res domain of dX~{} (based on D & U). Reduce the DX of the box.'.format(dX_Box, dX_Low_desired)) + if dY_Low==0: raise Exception('The Y-resolution of the box ({}) is too large and cannot satisfy the requirements for the low-res domain of dY~{} (based on D & U). Reduce the DY of the box.'.format(dY_Box, dY_Low_desired)) + if dZ_Low==0: raise Exception('The Z-resolution of the box ({}) is too large and cannot satisfy the requirements for the low-res domain of dZ~{} (based on D & U). Reduce the DZ of the box.'.format(dZ_Box, dZ_Low_desired)) + + # --- Low-res domain + # NOTE: more work is needed to make sure the domain encompass the turbines + # Also, we need to know the main flow direction to add a buffere with extent_wake + # Origin + nD_Before = extent_X/2 # Diameters before the first turbine to start the domain + X0_Low = np.floor( (min(xWT)-nD_Before*D-dX_Low)) # Starting on integer value for esthetics. With a dX_Low margin. + Y0_Low = np.floor( -LY_Box/2 ) # Starting on integer value for esthetics + Z0_Low = zBox[0] # we start at lowest to include tower + if LES: + if Y0_Low > min(yWT)-3*D: + Y0_Low = np.floor(min(yWT)-3*D) + # Extent NOTE: this assumes main flow about x. Might need to be changed + + XMax_Low = max(xWT) + extent_wake*D + LX_Low = XMax_Low-X0_Low + LY_Low = LY_Box + LZ_Low = LZ_Box + # Number of points + nX_Low = int(np.ceil(LX_Low/dX_Low)) + nY_Low = int(np.ceil(LY_Low/dY_Low)) + nZ_Low = int(np.ceil(LZ_Low/dZ_Low)) + # Make sure we don't exceed box in Y and Z #rt: this essentially gives us 1 less grid point than what is on the inp/bst files + if (nY_Low*dY_Low>LY_Box): nY_Low=nY_Low-1 + if (nZ_Low*dZ_Low>LZ_Box): nZ_Low=nZ_Low-1 + + # --- High-res domain extent and number of points + ZMax_High = hubHeight+extent_YZ*D/2 + Z0_High = zBox[0] # we start at lowest to include tower + LX_High = extent_X*D + LY_High = min(LY_Box, extent_YZ*D ) # Bounding to not exceed the box dimension + LZ_High = min(LZ_Box, ZMax_High-Z0_High) # Bounding to not exceed the box dimension + nX_High = int(np.ceil(LX_High/dX_High)) + nY_High = int(np.ceil(LY_High/dY_High)) + nZ_High = int(np.ceil(LZ_High/dZ_High)) + # Make sure we don't exceed box in Y and Z + if (nY_High*dY_High>LY_Box): nY_High=nY_High-1 + if (nZ_High*dZ_High>LZ_Box): nZ_High=nZ_High-1 + + # --- High-res location per turbine + X0_desired = np.asarray(xWT)-LX_High/2 # high-res is centered on turbine location + Y0_desired = np.asarray(yWT)-LY_High/2 # high-res is centered on turbine location + X0_High = X0_Low + np.floor((X0_desired-X0_Low)/dX_High)*dX_High + Y0_High = Y0_Low + np.floor((Y0_desired-Y0_Low)/dY_High)*dY_High + + d = dict() + d['DT_Low'] = np.around(dT_Low ,4) + d['DT_High'] = np.around(dT_High,4) + d['NX_Low'] = nX_Low + d['NY_Low'] = nY_Low + d['NZ_Low'] = nZ_Low + d['X0_Low'] = np.around(X0_Low,4) + d['Y0_Low'] = np.around(Y0_Low,4) + d['Z0_Low'] = np.around(Z0_Low,4) + d['dX_Low'] = np.around(dX_Low,4) + d['dY_Low'] = np.around(dY_Low,4) + d['dZ_Low'] = np.around(dZ_Low,4) + d['NX_High'] = nX_High + d['NY_High'] = nY_High + d['NZ_High'] = nZ_High + # --- High extent info for turbine outputs + d['dX_High'] = np.around(dX_High,4) + d['dY_High'] = np.around(dY_High,4) + d['dZ_High'] = np.around(dZ_High,4) + d['X0_High'] = np.around(X0_High,4) + d['Y0_High'] = np.around(Y0_High,4) + d['Z0_High'] = np.around(Z0_High,4) + # --- Misc + d['dX_des_High'] = dX_High_desired + d['dX_des_Low'] = dX_Low_desired + d['DT_des'] = dT_Low_desired + d['U_mean'] = meanU + + # --- Sanity check: check that the high res is at "almost" an integer location + X_rel = (np.array(d['X0_High'])-d['X0_Low'])/d['dX_High'] + Y_rel = (np.array(d['Y0_High'])-d['Y0_Low'])/d['dY_High'] + dX = X_rel - np.round(X_rel) # Should be close to zero + dY = Y_rel - np.round(Y_rel) # Should be close to zero + if any(abs(dX)>1e-3): + print('Deltas:',dX) + print('Exception has been raise. I put this print statement instead. Check with EB.') + print('Exception: Some X0_High are not on an integer multiple of the high-res grid') + #raise Exception('Some X0_High are not on an integer multiple of the high-res grid') + if any(abs(dY)>1e-3): + print('Deltas:',dY) + print('Exception has been raise. I put this print statement instead. Check with EB.') + print('Exception: Some Y0_High are not on an integer multiple of the high-res grid') + #raise Exception('Some Y0_High are not on an integer multiple of the high-res grid') + + return d + + +def writeFastFarm(outputFile, templateFile, xWT, yWT, zWT, FFTS=None, OutListT1=None, noLeadingZero=False): + """ Write FastFarm input file based on a template, a TurbSimFile and the Layout + + outputFile: .fstf file to be written + templateFile: .fstf file that will be used to generate the output_file + XWT,YWT,ZWT: positions of turbines + FFTS: FastFarm TurbSim parameters as returned by fastFarmTurbSimExtent + """ + # --- Read template fast farm file + fst=FASTInputFile(templateFile) + # --- Replace box extent values + if FFTS is not None: + fst['Mod_AmbWind'] = 2 + ModVars = ['DT_Low', 'DT_High', 'NX_Low', 'NY_Low', 'NZ_Low', 'X0_Low', 'Y0_Low', 'Z0_Low', 'dX_Low', 'dY_Low', 'dZ_Low', 'NX_High', 'NY_High', 'NZ_High'] + for k in ModVars: + if isinstance(FFTS[k],int): + fst[k] = FFTS[k] + else: + fst[k] = np.around(FFTS[k],3) + fst['WrDisDT'] = FFTS['DT_Low'] + + # --- Set turbine names, position, and box extent + nWT = len(xWT) + fst['NumTurbines'] = nWT + if FFTS is not None: + nCol= 10 + else: + nCol = 4 + ref_path = fst['WindTurbines'][0,3] + WT = np.array(['']*nWT*nCol,dtype='object').reshape((nWT,nCol)) + for iWT,(x,y,z) in enumerate(zip(xWT,yWT,zWT)): + WT[iWT,0]=x + WT[iWT,1]=y + WT[iWT,2]=z + WT[iWT,3]=insertTN(ref_path,iWT+1,nWT,noLeadingZero=noLeadingZero) + if FFTS is not None: + WT[iWT,4]=FFTS['X0_High'][iWT] + WT[iWT,5]=FFTS['Y0_High'][iWT] + WT[iWT,6]=FFTS['Z0_High'] + WT[iWT,7]=FFTS['dX_High'] + WT[iWT,8]=FFTS['dY_High'] + WT[iWT,9]=FFTS['dZ_High'] + fst['WindTurbines']=WT + + fst.write(outputFile) + if OutListT1 is not None: + setFastFarmOutputs(outputFile, OutListT1) + +def setFastFarmOutputs(fastFarmFile, OutListT1): + """ Duplicate the output list, by replacing "T1" with T1->Tn """ + fst = FASTInputFile(fastFarmFile) + nWTOut = min(fst['NumTurbines'],9) # Limited to 9 turbines + OutList=[''] + for s in OutListT1: + s=s.strip('"') + if 'T1' in s: + OutList+=['"'+s.replace('T1','T{:d}'.format(iWT+1))+'"' for iWT in np.arange(nWTOut) ] + elif 'W1VAmb' in s: # special case for ambient wind + OutList+=['"'+s.replace('1','{:d}'.format(iWT+1))+'"' for iWT in np.arange(nWTOut) ] + elif 'W1VDis' in s: # special case for disturbed wind + OutList+=['"'+s.replace('1','{:d}'.format(iWT+1))+'"' for iWT in np.arange(nWTOut) ] + else: + OutList+='"'+s+'"' + fst['OutList']=OutList + fst.write(fastFarmFile) + + +def plotFastFarmSetup(fastFarmFile, grid=True, fig=None, D=None, plane='XY', hubHeight=None, showLegend=True): + """ """ + import matplotlib.pyplot as plt + + 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 + + + # --- Read FAST.Farm input file + fst=FASTInputFile(fastFarmFile) + + if fig is None: + fig = plt.figure(figsize=(13.5,8)) + 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) + + return fig + +# --------------------------------------------------------------------------------} +# --- Tools for postpro +# --------------------------------------------------------------------------------{ + +def spanwiseColFastFarm(Cols, nWT=9, nD=9): + """ Return column info, available columns and indices that contain AD spanwise data""" + FFSpanMap=dict() + for i in np.arange(nWT): + FFSpanMap[r'^CtT{:d}N(\d*)_\[-\]'.format(i+1)]='CtT{:d}_[-]'.format(i+1) + for i in np.arange(nWT): + for k in np.arange(nD): + FFSpanMap[r'^WkDfVxT{:d}N(\d*)D{:d}_\[m/s\]'.format(i+1,k+1) ]='WkDfVxT{:d}D{:d}_[m/s]'.format(i+1, k+1) + for i in np.arange(nWT): + for k in np.arange(nD): + FFSpanMap[r'^WkDfVrT{:d}N(\d*)D{:d}_\[m/s\]'.format(i+1,k+1) ]='WkDfVrT{:d}D{:d}_[m/s]'.format(i+1, k+1) + + return fastlib.find_matching_columns(Cols, FFSpanMap) + +def diameterwiseColFastFarm(Cols, nWT=9): + """ Return column info, available columns and indices that contain AD spanwise data""" + FFDiamMap=dict() + for i in np.arange(nWT): + for x in ['X','Y','Z']: + FFDiamMap[r'^WkAxs{}T{:d}D(\d*)_\[-\]'.format(x,i+1)] ='WkAxs{}T{:d}_[-]'.format(x,i+1) + for i in np.arange(nWT): + for x in ['X','Y','Z']: + FFDiamMap[r'^WkPos{}T{:d}D(\d*)_\[m\]'.format(x,i+1)] ='WkPos{}T{:d}_[m]'.format(x,i+1) + for i in np.arange(nWT): + for x in ['X','Y','Z']: + FFDiamMap[r'^WkVel{}T{:d}D(\d*)_\[m/s\]'.format(x,i+1)] ='WkVel{}T{:d}_[m/s]'.format(x,i+1) + for i in np.arange(nWT): + for x in ['X','Y','Z']: + FFDiamMap[r'^WkDiam{}T{:d}D(\d*)_\[m\]'.format(x,i+1)] ='WkDiam{}T{:d}_[m]'.format(x,i+1) + return fastlib.find_matching_columns(Cols, FFDiamMap) + +def SensorsFARMRadial(nWT=3,nD=10,nR=30,signals=None): + """ Returns a list of FASTFarm sensors that are used for the radial distribution + of quantities (e.g. Ct, Wake Deficits). + If `signals` is provided, the output is the list of sensors within the list `signals`. + """ + WT = np.arange(nWT) + r = np.arange(nR) + D = np.arange(nD) + sens=[] + sens+=['CtT{:d}N{:02d}_[-]'.format(i+1,j+1) for i in WT for j in r] + sens+=['WkDfVxT{:d}N{:02d}D{:d}_[m/s]'.format(i+1,j+1,k+1) for i in WT for j in r for k in D] + sens+=['WkDfVrT{:d}N{:02d}D{:d}_[m/s]'.format(i+1,j+1,k+1) for i in WT for j in r for k in D] + if signals is not None: + sens = [c for c in sens if c in signals] + return sens + +def SensorsFARMDiam(nWT,nD): + """ Returns a list of FASTFarm sensors that contain quantities at different downstream diameters + (e.g. WkAxs, WkPos, WkVel, WkDiam) + If `signals` is provided, the output is the list of sensors within the list `signals`. + """ + WT = np.arange(nWT) + D = np.arange(nD) + XYZ = ['X','Y','Z'] + sens=[] + sens+=['WkAxs{}T{:d}D{:d}_[-]'.format(x,i+1,j+1) for x in XYZ for i in WT for j in D] + sens+=['WkPos{}T{:d}D{:d}_[m]'.format(x,i+1,j+1) for x in XYZ for i in WT for j in D] + sens+=['WkVel{}T{:d}D{:d}_[m/s]'.format(x,i+1,j+1) for x in XYZ for i in WT for j in D] + sens+=['WkDiam{}T{:d}D{:d}_[m]'.format(x,i+1,j+1) for x in XYZ for i in WT for j in D] + if signals is not None: + sens = [c for c in sens if c in signals] + return sens + + +def extractFFRadialData(fastfarm_out,fastfarm_input,avgMethod='constantwindow',avgParam=30,D=1,df=None): + # LEGACY + return spanwisePostProFF(fastfarm_input,avgMethod=avgMethod,avgParam=avgParam,D=D,df=df,fastfarm_out=fastfarm_out) + + +def spanwisePostProFF(fastfarm_input,avgMethod='constantwindow',avgParam=30,D=1,df=None,fastfarm_out=None): + """ + Opens a FASTFarm output file, extract the radial data, average them and returns spanwise data + + D: diameter TODO, extract it from the main file + + See faslibt.averageDF for `avgMethod` and `avgParam`. + """ + # --- Opening ouputfile + if df is None: + df=FASTOutputFile(fastfarm_out).toDataFrame() + + # --- Opening input file and extracting inportant variables + if fastfarm_input is None: + # We don't have an input file, guess numbers of turbine, diameters, Nodes... + cols, sIdx = fastlib.find_matching_pattern(df.columns.values, r'T(\d+)') + nWT = np.array(sIdx).astype(int).max() + cols, sIdx = fastlib.find_matching_pattern(df.columns.values, r'D(\d+)') + nD = np.array(sIdx).astype(int).max() + cols, sIdx = fastlib.find_matching_pattern(df.columns.values, r'N(\d+)') + nr = np.array(sIdx).astype(int).max() + vr=None + vD=None + D=0 + main = None + else: + main=FASTInputFile(fastfarm_input) + iOut = main['OutRadii'] + dr = main['dr'] # Radial increment of radial finite-difference grid (m) + OutDist = main['OutDist'] # List of downstream distances for wake output for an individual rotor + WT = main['WindTurbines'] + nWT = len(WT) + vr = dr*np.array(iOut) + vD = np.array(OutDist) + nr=len(iOut) + nD=len(vD) + + + # --- Extracting time series of radial data only + colRadial = SensorsFARMRadial(nWT=nWT,nD=nD,nR=nr,signals=df.columns.values) + colRadial=['Time_[s]']+colRadial + dfRadialTime = df[colRadial] # TODO try to do some magic with it, display it with a slider + + # --- Averaging data + dfAvg = fastlib.averageDF(df,avgMethod=avgMethod,avgParam=avgParam) + + # --- Extract radial data + ColsInfo, nrMax = spanwiseColFastFarm(df.columns.values, nWT=nWT, nD=nD) + dfRad = fastlib.extract_spanwise_data(ColsInfo, nrMax, df=None, ts=dfAvg.iloc[0]) + #dfRad = fastlib.insert_radial_columns(dfRad, vr) + if dfRad is not None: + dfRad.insert(0, 'i_[#]', np.arange(nrMax)+1) # For all, to ease comparison + if vr is not None: + dfRad.insert(0, 'r_[m]', vr[:nrMax]) # give priority to r_[m] when available + dfRad['i/n_[-]']=np.arange(nrMax)/nrMax + + # --- Extract downstream data + ColsInfo, nDMax = diameterwiseColFastFarm(df.columns.values, nWT=nWT) + dfDiam = fastlib.extract_spanwise_data(ColsInfo, nDMax, df=None, ts=dfAvg.iloc[0]) + if dfDiam is not None: + dfDiam.insert(0, 'i_[#]', np.arange(nDMax)+1) # For all, to ease comparison + if vD is not None: + dfDiam.insert(0, 'x_[m]', vD[:nDMax]) + dfDiam['i/n_[-]'] = np.arange(nDMax)/nDMax + return dfRad, dfRadialTime, dfDiam + diff --git a/pyFAST/fastfarm/tests/__init__.py b/openfast_toolbox/fastfarm/tests/__init__.py similarity index 100% rename from pyFAST/fastfarm/tests/__init__.py rename to openfast_toolbox/fastfarm/tests/__init__.py diff --git a/pyFAST/fastfarm/tests/test_run_Examples.py b/openfast_toolbox/fastfarm/tests/test_run_Examples.py similarity index 97% rename from pyFAST/fastfarm/tests/test_run_Examples.py rename to openfast_toolbox/fastfarm/tests/test_run_Examples.py index 45b2490..1c5f9b4 100644 --- a/pyFAST/fastfarm/tests/test_run_Examples.py +++ b/openfast_toolbox/fastfarm/tests/test_run_Examples.py @@ -1,33 +1,33 @@ -import unittest -import numpy as np -import glob -import os - -def execfile(filepath, globals=None, locals=None): - """ Execute a given python file """ - if globals is None: - globals = {"__name__": "__main__"} - globals.update({ - "__file__": filepath, - }) - with open(filepath, 'rb') as file: - exec(compile(file.read(), filepath, 'exec'), globals, locals) - -class TestExamples(unittest.TestCase): - def test_run_examples(self): - exclude_list=[] - # Add tests to class - MyDir=os.path.dirname(__file__) - files = glob.glob(os.path.join(MyDir,'../examples/[A-Za-z][_-a-zA-Z0-9]*.py')) - import matplotlib.pyplot as plt - print('\n--------------------------------------------------------------') - for f in files: - print('Running example script: {}'.format(f)) - if hasattr(self,'subTest'): - with self.subTest(filename=os.path.basename(f)): - execfile(f, {'__name__': '__test__', 'print': lambda *_:None}) - plt.close('all') - - -if __name__ == '__main__': - unittest.main() +import unittest +import numpy as np +import glob +import os + +def execfile(filepath, globals=None, locals=None): + """ Execute a given python file """ + if globals is None: + globals = {"__name__": "__main__"} + globals.update({ + "__file__": filepath, + }) + with open(filepath, 'rb') as file: + exec(compile(file.read(), filepath, 'exec'), globals, locals) + +class TestExamples(unittest.TestCase): + def test_run_examples(self): + exclude_list=[] + # Add tests to class + MyDir=os.path.dirname(__file__) + files = glob.glob(os.path.join(MyDir,'../examples/[A-Za-z][_-a-zA-Z0-9]*.py')) + import matplotlib.pyplot as plt + print('\n--------------------------------------------------------------') + for f in files: + print('Running example script: {}'.format(f)) + if hasattr(self,'subTest'): + with self.subTest(filename=os.path.basename(f)): + execfile(f, {'__name__': '__test__', 'print': lambda *_:None}) + plt.close('all') + + +if __name__ == '__main__': + unittest.main() diff --git a/pyFAST/fastfarm/tests/test_turbsimExtent.py b/openfast_toolbox/fastfarm/tests/test_turbsimExtent.py similarity index 97% rename from pyFAST/fastfarm/tests/test_turbsimExtent.py rename to openfast_toolbox/fastfarm/tests/test_turbsimExtent.py index 5fe6a01..b47ad19 100644 --- a/pyFAST/fastfarm/tests/test_turbsimExtent.py +++ b/openfast_toolbox/fastfarm/tests/test_turbsimExtent.py @@ -1,79 +1,79 @@ -import unittest -import os -import numpy as np - -from pyFAST.fastfarm import * - -MyDir=os.path.dirname(__file__) - -class Test(unittest.TestCase): - - def test_box_extent(self): - # Test the turbulence box extent function - - # --- TurbSim Box paarameters - yBox = np.arange(-187.5,188 ,5 ) - zBox = np.arange(1 ,282 ,5 ) - tBox = np.arange(0 ,4.901,0.1) - meanU = 5.968230471458111 - # --- Parameters for TurbSim Extent - D = 77.0 # Turbine diameter (m) - hubHeight = 78.045 # Hub Height (m) - extent_X_high = 1.2 # x-extent of high res box in diamter around turbine location - extent_Y_high = 1.2 # y-extent of high res box in diamter around turbine location - chord_max = 5 # maximum blade chord (m). Turbine specific. - Cmeander = 1.9 # Meandering constant (-) - # --- Layout - xWT = [0.0, 265.] # x positions of turbines - yWT = [0.0, 50.0] # y postitions of turbines - zWT = [0.0, 0.0 ] # z postitions of turbines - - # --- Determine Box extent for FAST>Farm - FFTS = fastFarmBoxExtent(yBox, zBox, tBox, meanU, hubHeight, D, xWT, yWT, Cmeander=Cmeander, chord_max=chord_max, extent_X=extent_X_high, extent_YZ=extent_Y_high) - - # --- Test values - #print(FFTS) - np.testing.assert_almost_equal(FFTS['DT_Low'] , 2.4 , 5) - np.testing.assert_almost_equal(FFTS['DT_High'] , 0.1 , 5) - np.testing.assert_almost_equal(FFTS['NX_Low'] , 196 , 5) - np.testing.assert_almost_equal(FFTS['NY_Low'] , 75 , 5) - np.testing.assert_almost_equal(FFTS['NZ_Low'] , 56 , 5) - np.testing.assert_almost_equal(FFTS['X0_Low'] ,-51 , 5) - np.testing.assert_almost_equal(FFTS['Y0_Low'] ,-188 , 5) - np.testing.assert_almost_equal(FFTS['Z0_Low'] , 1 , 5) - np.testing.assert_almost_equal(FFTS['dX_Low'] , 4.7746 , 5) - np.testing.assert_almost_equal(FFTS['dY_Low'] , 5.0 , 5) - np.testing.assert_almost_equal(FFTS['dZ_Low'] , 5.0 , 5) - np.testing.assert_almost_equal(FFTS['NX_High'] , 20 , 5) - np.testing.assert_almost_equal(FFTS['NY_High'] , 19 , 5) - np.testing.assert_almost_equal(FFTS['NZ_High'] , 25 , 5) - np.testing.assert_almost_equal(FFTS['dX_High'] , 4.7746, 5) - np.testing.assert_almost_equal(FFTS['dY_High'] , 5 , 5) - np.testing.assert_almost_equal(FFTS['dZ_High'] , 5 , 5) - np.testing.assert_almost_equal(FFTS['X0_High'] , [-46.2254, 216.3767], 5) - 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 - 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 - #plotFastFarmSetup(outputFSTF, grid=True) - #plt.show() - - # --- Check that locations are at integer locations - X_rel = (np.array(FFTS['X0_High'])-FFTS['X0_Low'])/FFTS['dX_High'] - Y_rel = (np.array(FFTS['Y0_High'])-FFTS['Y0_Low'])/FFTS['dY_High'] - dX = X_rel - np.round(X_rel) - dY = Y_rel - np.round(Y_rel) - np.testing.assert_almost_equal(dX, [0]*len(dX), 3) - np.testing.assert_almost_equal(dY, [0]*len(dY), 3) - - - -if __name__ == '__main__': - unittest.main() - -if __name__ == '__main__': - unittest.main() - +import unittest +import os +import numpy as np + +from openfast_toolbox.fastfarm import * + +MyDir=os.path.dirname(__file__) + +class Test(unittest.TestCase): + + def test_box_extent(self): + # Test the turbulence box extent function + + # --- TurbSim Box paarameters + yBox = np.arange(-187.5,188 ,5 ) + zBox = np.arange(1 ,282 ,5 ) + tBox = np.arange(0 ,4.901,0.1) + meanU = 5.968230471458111 + # --- Parameters for TurbSim Extent + D = 77.0 # Turbine diameter (m) + hubHeight = 78.045 # Hub Height (m) + extent_X_high = 1.2 # x-extent of high res box in diamter around turbine location + extent_Y_high = 1.2 # y-extent of high res box in diamter around turbine location + chord_max = 5 # maximum blade chord (m). Turbine specific. + Cmeander = 1.9 # Meandering constant (-) + # --- Layout + xWT = [0.0, 265.] # x positions of turbines + yWT = [0.0, 50.0] # y postitions of turbines + zWT = [0.0, 0.0 ] # z postitions of turbines + + # --- Determine Box extent for FAST>Farm + FFTS = fastFarmBoxExtent(yBox, zBox, tBox, meanU, hubHeight, D, xWT, yWT, Cmeander=Cmeander, chord_max=chord_max, extent_X=extent_X_high, extent_YZ=extent_Y_high) + + # --- Test values + #print(FFTS) + np.testing.assert_almost_equal(FFTS['DT_Low'] , 2.4 , 5) + np.testing.assert_almost_equal(FFTS['DT_High'] , 0.1 , 5) + np.testing.assert_almost_equal(FFTS['NX_Low'] , 196 , 5) + np.testing.assert_almost_equal(FFTS['NY_Low'] , 75 , 5) + np.testing.assert_almost_equal(FFTS['NZ_Low'] , 56 , 5) + np.testing.assert_almost_equal(FFTS['X0_Low'] ,-51 , 5) + np.testing.assert_almost_equal(FFTS['Y0_Low'] ,-188 , 5) + np.testing.assert_almost_equal(FFTS['Z0_Low'] , 1 , 5) + np.testing.assert_almost_equal(FFTS['dX_Low'] , 4.7746 , 5) + np.testing.assert_almost_equal(FFTS['dY_Low'] , 5.0 , 5) + np.testing.assert_almost_equal(FFTS['dZ_Low'] , 5.0 , 5) + np.testing.assert_almost_equal(FFTS['NX_High'] , 20 , 5) + np.testing.assert_almost_equal(FFTS['NY_High'] , 19 , 5) + np.testing.assert_almost_equal(FFTS['NZ_High'] , 25 , 5) + np.testing.assert_almost_equal(FFTS['dX_High'] , 4.7746, 5) + np.testing.assert_almost_equal(FFTS['dY_High'] , 5 , 5) + np.testing.assert_almost_equal(FFTS['dZ_High'] , 5 , 5) + np.testing.assert_almost_equal(FFTS['X0_High'] , [-46.2254, 216.3767], 5) + 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 + 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 + #plotFastFarmSetup(outputFSTF, grid=True) + #plt.show() + + # --- Check that locations are at integer locations + X_rel = (np.array(FFTS['X0_High'])-FFTS['X0_Low'])/FFTS['dX_High'] + Y_rel = (np.array(FFTS['Y0_High'])-FFTS['Y0_Low'])/FFTS['dY_High'] + dX = X_rel - np.round(X_rel) + dY = Y_rel - np.round(Y_rel) + np.testing.assert_almost_equal(dX, [0]*len(dX), 3) + np.testing.assert_almost_equal(dY, [0]*len(dY), 3) + + + +if __name__ == '__main__': + unittest.main() + +if __name__ == '__main__': + unittest.main() + diff --git a/pyFAST/input_output/README.md b/openfast_toolbox/io/README.md similarity index 91% rename from pyFAST/input_output/README.md rename to openfast_toolbox/io/README.md index e8e689c..efc5c2c 100644 --- a/pyFAST/input_output/README.md +++ b/openfast_toolbox/io/README.md @@ -1,63 +1,63 @@ - -# Input output file readers - -This package contains readers and writers for typical files used in OpenFAST simulations: -- FAST input file (`.fst, .dat, .txt`), `Class: FASTInputFile, file: fast_input_file.py`. -- FAST output file (`.out, .outb, .elev`), `Class: FASTOutputFile, file: fast_output_file.py`. -- FAST linearization file (`.lin`), `Class: FASTLinearizationFile, file: fast_linearization_file.py`. -- FAST summary file (`.sum.yaml`), `Class: FASTSummaryFile, file: fast_summary_file.py`. -- TurbSim binary file (`.bts`), `Class: TurbSimFile, file: turbsim_file.py`. -- CSV file (`.csv, .dat, .txt`), `Class: CSVFile, file: csv_file.py`. - - -## Main architecture and interface - -A separate python file and class is used for each file format. -The classes inherit from the standard `File` class, present in the file `file.py`. - -The object returned by each class is (or behaves as) a dictionary. -The main methods are: -- `object = class()` : create an instance of a file object -- `object = class(filename)`: create an instance of a file object, and read a given file -- `object.read(filename)`: read the given file -- `object.write(filename)`: write the object to a file (may overwrite) -- `object.toDataFrame()`: attempts to convert object to a pandas DataFrame - -Additional methods may be present depending on the file format. - - -## Examples -Examples scripts are found in this [folder](examples). -Below are simple examples to get started: - - -Read an AeroDyn file, modifies some values and write the modified file: -```python -from pyFAST.input_output import FASTInputFile -filename = 'AeroDyn.dat' -f = FASTInputFile(filename) -f['TwrAero'] = True -f['AirDens'] = 1.225 -f.write('AeroDyn_Changed.dat') -``` - -Read an OpenFAST binary output file and convert it to a pandas DataFrame -```python -from pyFAST.input_output import FASTOutputFile -df = FASTOutputFile('5MW.outb').toDataFrame() -time = df['Time_[s]'] -Omega = df['RotSpeed_[rpm]'] -``` - -Read a TurbSim binary file -```python -from pyFAST.input_output import TurbSimFile -ts = TurbSimFile('Turb.bts') -print(ts.keys()) -print(ts['u'].shape) -``` -For more examples on how to manipulate TurbSim files see -see [examples](examples/Example_TurbSimBox.py) - -TurbSim input file processing (file modification, run and result export) -see [examples](examples/Example_TurbSim_Processing.py). + +# Input output file readers + +This package contains readers and writers for typical files used in OpenFAST simulations: +- FAST input file (`.fst, .dat, .txt`), `Class: FASTInputFile, file: fast_input_file.py`. +- FAST output file (`.out, .outb, .elev`), `Class: FASTOutputFile, file: fast_output_file.py`. +- FAST linearization file (`.lin`), `Class: FASTLinearizationFile, file: fast_linearization_file.py`. +- FAST summary file (`.sum.yaml`), `Class: FASTSummaryFile, file: fast_summary_file.py`. +- TurbSim binary file (`.bts`), `Class: TurbSimFile, file: turbsim_file.py`. +- CSV file (`.csv, .dat, .txt`), `Class: CSVFile, file: csv_file.py`. + + +## Main architecture and interface + +A separate python file and class is used for each file format. +The classes inherit from the standard `File` class, present in the file `file.py`. + +The object returned by each class is (or behaves as) a dictionary. +The main methods are: +- `object = class()` : create an instance of a file object +- `object = class(filename)`: create an instance of a file object, and read a given file +- `object.read(filename)`: read the given file +- `object.write(filename)`: write the object to a file (may overwrite) +- `object.toDataFrame()`: attempts to convert object to a pandas DataFrame + +Additional methods may be present depending on the file format. + + +## Examples +Examples scripts are found in this [folder](examples). +Below are simple examples to get started: + + +Read an AeroDyn file, modifies some values and write the modified file: +```python +from openfast_toolbox.input_output import FASTInputFile +filename = 'AeroDyn.dat' +f = FASTInputFile(filename) +f['TwrAero'] = True +f['AirDens'] = 1.225 +f.write('AeroDyn_Changed.dat') +``` + +Read an OpenFAST binary output file and convert it to a pandas DataFrame +```python +from openfast_toolbox.input_output import FASTOutputFile +df = FASTOutputFile('5MW.outb').toDataFrame() +time = df['Time_[s]'] +Omega = df['RotSpeed_[rpm]'] +``` + +Read a TurbSim binary file +```python +from openfast_toolbox.input_output import TurbSimFile +ts = TurbSimFile('Turb.bts') +print(ts.keys()) +print(ts['u'].shape) +``` +For more examples on how to manipulate TurbSim files see +see [examples](examples/Example_TurbSimBox.py) + +TurbSim input file processing (file modification, run and result export) +see [examples](examples/Example_TurbSim_Processing.py). diff --git a/pyFAST/input_output/__init__.py b/openfast_toolbox/io/__init__.py similarity index 100% rename from pyFAST/input_output/__init__.py rename to openfast_toolbox/io/__init__.py diff --git a/pyFAST/input_output/amrwind_file.py b/openfast_toolbox/io/amrwind_file.py similarity index 100% rename from pyFAST/input_output/amrwind_file.py rename to openfast_toolbox/io/amrwind_file.py diff --git a/pyFAST/input_output/bladed_out_file.py b/openfast_toolbox/io/bladed_out_file.py similarity index 100% rename from pyFAST/input_output/bladed_out_file.py rename to openfast_toolbox/io/bladed_out_file.py diff --git a/pyFAST/input_output/bmodes_out_file.py b/openfast_toolbox/io/bmodes_out_file.py similarity index 100% rename from pyFAST/input_output/bmodes_out_file.py rename to openfast_toolbox/io/bmodes_out_file.py diff --git a/pyFAST/input_output/cactus_element_file.py b/openfast_toolbox/io/cactus_element_file.py similarity index 100% rename from pyFAST/input_output/cactus_element_file.py rename to openfast_toolbox/io/cactus_element_file.py diff --git a/pyFAST/input_output/cactus_file.py b/openfast_toolbox/io/cactus_file.py similarity index 100% rename from pyFAST/input_output/cactus_file.py rename to openfast_toolbox/io/cactus_file.py diff --git a/pyFAST/input_output/converters.py b/openfast_toolbox/io/converters.py similarity index 100% rename from pyFAST/input_output/converters.py rename to openfast_toolbox/io/converters.py diff --git a/pyFAST/input_output/csv_file.py b/openfast_toolbox/io/csv_file.py similarity index 100% rename from pyFAST/input_output/csv_file.py rename to openfast_toolbox/io/csv_file.py diff --git a/pyFAST/input_output/examples/.gitignore b/openfast_toolbox/io/examples/.gitignore similarity index 100% rename from pyFAST/input_output/examples/.gitignore rename to openfast_toolbox/io/examples/.gitignore diff --git a/pyFAST/input_output/examples/Example_ChangeAeroDyn.py b/openfast_toolbox/io/examples/Example_ChangeAeroDyn.py similarity index 89% rename from pyFAST/input_output/examples/Example_ChangeAeroDyn.py rename to openfast_toolbox/io/examples/Example_ChangeAeroDyn.py index 2663a80..fa41b23 100644 --- a/pyFAST/input_output/examples/Example_ChangeAeroDyn.py +++ b/openfast_toolbox/io/examples/Example_ChangeAeroDyn.py @@ -3,7 +3,7 @@ """ import os -from pyFAST.input_output import FASTInputFile +from openfast_toolbox.io import FASTInputFile # Get current directory so this script can be called from any location scriptDir = os.path.dirname(__file__) diff --git a/pyFAST/input_output/examples/Example_EditOpenFASTModel.py b/openfast_toolbox/io/examples/Example_EditOpenFASTModel.py similarity index 98% rename from pyFAST/input_output/examples/Example_EditOpenFASTModel.py rename to openfast_toolbox/io/examples/Example_EditOpenFASTModel.py index 21e75f1..aa61a1c 100644 --- a/pyFAST/input_output/examples/Example_EditOpenFASTModel.py +++ b/openfast_toolbox/io/examples/Example_EditOpenFASTModel.py @@ -13,7 +13,7 @@ """ import os import numpy as np -from pyFAST.input_output import FASTInputFile +from openfast_toolbox.io import FASTInputFile import matplotlib.pyplot as plt diff --git a/pyFAST/input_output/examples/Example_MannBox.py b/openfast_toolbox/io/examples/Example_MannBox.py similarity index 97% rename from pyFAST/input_output/examples/Example_MannBox.py rename to openfast_toolbox/io/examples/Example_MannBox.py index 380841b..e99a5ed 100644 --- a/pyFAST/input_output/examples/Example_MannBox.py +++ b/openfast_toolbox/io/examples/Example_MannBox.py @@ -5,7 +5,7 @@ import os import numpy as np import matplotlib.pyplot as plt -from pyFAST.input_output.mannbox_file import MannBoxFile +from openfast_toolbox.io.mannbox_file import MannBoxFile scriptDir = os.path.dirname(__file__) diff --git a/pyFAST/input_output/examples/Example_PlotBinary.py b/openfast_toolbox/io/examples/Example_PlotBinary.py similarity index 92% rename from pyFAST/input_output/examples/Example_PlotBinary.py rename to openfast_toolbox/io/examples/Example_PlotBinary.py index f4107e9..9d16da2 100644 --- a/pyFAST/input_output/examples/Example_PlotBinary.py +++ b/openfast_toolbox/io/examples/Example_PlotBinary.py @@ -5,7 +5,7 @@ """ import os import matplotlib.pyplot as plt -from pyFAST.input_output import FASTOutputFile +from openfast_toolbox.io import FASTOutputFile # Get current directory so this script can be called from any location scriptDir = os.path.dirname(__file__) diff --git a/pyFAST/input_output/examples/Example_PlotBlade.py b/openfast_toolbox/io/examples/Example_PlotBlade.py similarity index 91% rename from pyFAST/input_output/examples/Example_PlotBlade.py rename to openfast_toolbox/io/examples/Example_PlotBlade.py index 3679600..5fa02c9 100644 --- a/pyFAST/input_output/examples/Example_PlotBlade.py +++ b/openfast_toolbox/io/examples/Example_PlotBlade.py @@ -5,7 +5,7 @@ import os import matplotlib.pyplot as plt -from pyFAST.input_output import FASTInputFile +from openfast_toolbox.io import FASTInputFile # Get current directory so this script can be called from any location scriptDir = os.path.dirname(__file__) diff --git a/pyFAST/input_output/examples/Example_TurbSimBox.py b/openfast_toolbox/io/examples/Example_TurbSimBox.py similarity index 98% rename from pyFAST/input_output/examples/Example_TurbSimBox.py rename to openfast_toolbox/io/examples/Example_TurbSimBox.py index 7e04d17..8160790 100644 --- a/pyFAST/input_output/examples/Example_TurbSimBox.py +++ b/openfast_toolbox/io/examples/Example_TurbSimBox.py @@ -19,7 +19,7 @@ import pandas as pd import matplotlib.pyplot as plt -from pyFAST.input_output import TurbSimFile +from openfast_toolbox.io import TurbSimFile def main(): MyDir = os.path.dirname(__file__) diff --git a/pyFAST/input_output/examples/Example_TurbSim_Processing.py b/openfast_toolbox/io/examples/Example_TurbSim_Processing.py similarity index 90% rename from pyFAST/input_output/examples/Example_TurbSim_Processing.py rename to openfast_toolbox/io/examples/Example_TurbSim_Processing.py index 3ce0bc4..44df8ad 100644 --- a/pyFAST/input_output/examples/Example_TurbSim_Processing.py +++ b/openfast_toolbox/io/examples/Example_TurbSim_Processing.py @@ -1,38 +1,38 @@ -from pyFAST.input_output import FASTInputFile -from pyFAST.case_generation import runner -from pyFAST.input_output import TurbSimFile -import pandas as pd -import os - -def main(): - """Modify TurbSim parameters and write""" - MyDir = os.path.dirname(__file__) - filename = os.path.join(MyDir, '../tests/example_files/FASTIn_TurbSim.dat') # Name of TurbSim's input file - f = FASTInputFile(filename) - f['WrBLFF'] = False - f['WrADFF'] = True - f['WrADTWR'] = True - f['NumGrid_Z'] = 15 - f['NumGrid_Y'] = 15 - f['AnalysisTime'] = 600 - f.write(os.path.join(MyDir, '../tests/example_files/FASTIn_TurbSim_change.dat')) # Modified file name - - """Run TurbSim""" - TurbSim_FILE = os.path.join(MyDir, '../tests/example_files/FASTIn_TurbSim_change.dat') # Input file - Turbsim_EXE = os.path.join(MyDir, '../../../../openfast/build/bin/TurbSim_x64.exe') # Change to the path of the TurbSim executable - runner.run_cmd(TurbSim_FILE, Turbsim_EXE, wait=True, showOutputs=False, showCommand=True) - - """Open the turbulence box, containing the wind speed in 3 directions""" - # NOTE: See Example_TurbSimBox for more use cases of the TurbSimFile class - ts = TurbSimFile(os.path.join(MyDir, '../tests/example_files/FASTIn_TurbSim_change.bts')) # Output file - print(ts.keys()) - print(ts['info']) - print(ts['u'].shape) - - """Save wind speed data in CSV format""" - fgp_data = pd.DataFrame(ts['u'][:,:,0,0]).T # 3D turbulence data for the first grid point - fgp_data.columns = ['streamwise wind speed', 'transverse wind speed', 'vertical wind speed'] # add columns name - fgp_data.to_csv(os.path.join(MyDir, '../tests/example_files/wind_data_of_first_grid_point.csv')) # save data in CSV format - -if __name__ == "__main__": - main() +from openfast_toolbox.io import FASTInputFile +from openfast_toolbox.case_generation import runner +from openfast_toolbox.io import TurbSimFile +import pandas as pd +import os + +def main(): + """Modify TurbSim parameters and write""" + MyDir = os.path.dirname(__file__) + filename = os.path.join(MyDir, '../tests/example_files/FASTIn_TurbSim.dat') # Name of TurbSim's input file + f = FASTInputFile(filename) + f['WrBLFF'] = False + f['WrADFF'] = True + f['WrADTWR'] = True + f['NumGrid_Z'] = 15 + f['NumGrid_Y'] = 15 + f['AnalysisTime'] = 600 + f.write(os.path.join(MyDir, '../tests/example_files/FASTIn_TurbSim_change.dat')) # Modified file name + + """Run TurbSim""" + TurbSim_FILE = os.path.join(MyDir, '../tests/example_files/FASTIn_TurbSim_change.dat') # Input file + Turbsim_EXE = os.path.join(MyDir, '../../../../openfast/build/bin/TurbSim_x64.exe') # Change to the path of the TurbSim executable + runner.run_cmd(TurbSim_FILE, Turbsim_EXE, wait=True, showOutputs=False, showCommand=True) + + """Open the turbulence box, containing the wind speed in 3 directions""" + # NOTE: See Example_TurbSimBox for more use cases of the TurbSimFile class + ts = TurbSimFile(os.path.join(MyDir, '../tests/example_files/FASTIn_TurbSim_change.bts')) # Output file + print(ts.keys()) + print(ts['info']) + print(ts['u'].shape) + + """Save wind speed data in CSV format""" + fgp_data = pd.DataFrame(ts['u'][:,:,0,0]).T # 3D turbulence data for the first grid point + fgp_data.columns = ['streamwise wind speed', 'transverse wind speed', 'vertical wind speed'] # add columns name + fgp_data.to_csv(os.path.join(MyDir, '../tests/example_files/wind_data_of_first_grid_point.csv')) # save data in CSV format + +if __name__ == "__main__": + main() diff --git a/pyFAST/input_output/examples/Example_VTKPlanesGrid.py b/openfast_toolbox/io/examples/Example_VTKPlanesGrid.py similarity index 94% rename from pyFAST/input_output/examples/Example_VTKPlanesGrid.py rename to openfast_toolbox/io/examples/Example_VTKPlanesGrid.py index 2ec04aa..c608073 100644 --- a/pyFAST/input_output/examples/Example_VTKPlanesGrid.py +++ b/openfast_toolbox/io/examples/Example_VTKPlanesGrid.py @@ -7,8 +7,8 @@ import matplotlib.pyplot as plt import os # Local -import pyFAST.input_output as io -import pyFAST.input_output.vtk_file +import openfast_toolbox.io as io +import openfast_toolbox.io.vtk_file # Get current directory so this script can be called from any location MyDir=os.path.dirname(__file__) diff --git a/pyFAST/input_output/examples/README.md b/openfast_toolbox/io/examples/README.md similarity index 100% rename from pyFAST/input_output/examples/README.md rename to openfast_toolbox/io/examples/README.md diff --git a/pyFAST/input_output/examples/SubDynModes.py b/openfast_toolbox/io/examples/SubDynModes.py similarity index 95% rename from pyFAST/input_output/examples/SubDynModes.py rename to openfast_toolbox/io/examples/SubDynModes.py index 7fd79ad..7791847 100644 --- a/pyFAST/input_output/examples/SubDynModes.py +++ b/openfast_toolbox/io/examples/SubDynModes.py @@ -7,7 +7,7 @@ import matplotlib.pyplot as plt import os # Local -from pyFAST.input_output.fast_summary_file import FASTSummaryFile +from openfast_toolbox.io.fast_summary_file import FASTSummaryFile # Get current directory so this script can be called from any location scriptDir = os.path.dirname(__file__) diff --git a/pyFAST/input_output/excel_file.py b/openfast_toolbox/io/excel_file.py similarity index 100% rename from pyFAST/input_output/excel_file.py rename to openfast_toolbox/io/excel_file.py diff --git a/pyFAST/input_output/fast_input_deck.py b/openfast_toolbox/io/fast_input_deck.py similarity index 100% rename from pyFAST/input_output/fast_input_deck.py rename to openfast_toolbox/io/fast_input_deck.py diff --git a/pyFAST/input_output/fast_input_file.py b/openfast_toolbox/io/fast_input_file.py similarity index 98% rename from pyFAST/input_output/fast_input_file.py rename to openfast_toolbox/io/fast_input_file.py index b02e4fe..97ff369 100644 --- a/pyFAST/input_output/fast_input_file.py +++ b/openfast_toolbox/io/fast_input_file.py @@ -1,2104 +1,2104 @@ -import numpy as np -import os -import pandas as pd -import re -try: - from .file import File, WrongFormatError, BrokenFormatError -except: - File = dict - class WrongFormatError(Exception): pass - class BrokenFormatError(Exception): pass - -__all__ = ['FASTInputFile'] - -TABTYPE_NOT_A_TAB = 0 -TABTYPE_NUM_WITH_HEADER = 1 -TABTYPE_NUM_WITH_HEADERCOM = 2 -TABTYPE_NUM_NO_HEADER = 4 -TABTYPE_NUM_BEAMDYN = 5 -TABTYPE_NUM_SUBDYNOUT = 7 -TABTYPE_MIX_WITH_HEADER = 6 -TABTYPE_FIL = 3 -TABTYPE_FMT = 9999 # TODO - - - -class FASTInputFile(File): - """ - Read/write an OpenFAST input file. The object behaves like a dictionary. - A generic reader/writer is used at first. - If a dedicated OpenFAST input file is detected, additional functionalities are added. - See at the end of this file for dedicated class that can be used instead of this generic reader. - - Main methods - ------------ - - read, write, toDataFrame, keys, toGraph - - - Return an object which inherits from FASTInputFileBase - - The generic file reader is run first - - If a specific file format/module is detected, a fixed file format object is returned - The fixed file format have additional outputs, sanity checks and methods - """ - - @staticmethod - def defaultExtensions(): - return ['.dat','.fst','.txt','.fstf','.dvr'] - - @staticmethod - def formatName(): - return 'FAST input file' - - def __init__(self, filename=None, **kwargs): - self._fixedfile = None - self.basefile = FASTInputFileBase(filename, **kwargs) # Generic fileformat - - @property - def fixedfile(self): - if self._fixedfile is not None: - return self._fixedfile - elif len(self.basefile.data)>0: - self._fixedfile=self.fixedFormat() - return self._fixedfile - else: - return self.basefile - - @property - def module(self): - if self._fixedfile is None: - return self.basefile.module - else: - return self._fixedfile.module - - @property - def hasNodal(self): - if self._fixedfile is None: - return self.basefile.hasNodal - else: - return self._fixedfile.hasNodal - - def getID(self, label): - return self.basefile.getID(label) - - @property - def data(self): - return self.basefile.data - - def fixedFormat(self): - # --- Creating a dedicated Child - KEYS = list(self.basefile.keys()) - if 'NumBlNds' in KEYS: - return ADBladeFile.from_fast_input_file(self.basefile) - elif 'rhoinf' in KEYS: - return BDFile.from_fast_input_file(self.basefile) - elif 'NBlInpSt' in KEYS: - return EDBladeFile.from_fast_input_file(self.basefile) - elif 'NTwInpSt' in KEYS: - return EDTowerFile.from_fast_input_file(self.basefile) - elif 'MassMatrix' in KEYS and self.module == 'ExtPtfm': - return ExtPtfmFile.from_fast_input_file(self.basefile) - elif 'NumCoords' in KEYS and 'InterpOrd' in KEYS: - return ADPolarFile.from_fast_input_file(self.basefile) - else: - # TODO: HD, SD, SvD, ED, AD, EDbld, BD, - #print('>>>>>>>>>>>> NO FILEFORMAT', KEYS) - return self.basefile - - def read(self, filename=None): - return self.fixedfile.read(filename) - - def write(self, filename=None): - return self.fixedfile.write(filename) - - def toDataFrame(self): - return self.fixedfile.toDataFrame() - - def toString(self): - return self.fixedfile.toString() - - def keys(self): - return self.fixedfile.keys() - - def toGraph(self, **kwargs): - return self.fixedfile.toGraph(**kwargs) - - @property - def filename(self): - return self.fixedfile.filename - - @property - def comment(self): - return self.fixedfile.comment - - @comment.setter - def comment(self,comment): - self.fixedfile.comment = comment - - def __iter__(self): - return self.fixedfile.__iter__() - - def __next__(self): - return self.fixedfile.__next__() - - def __setitem__(self,key,item): - return self.fixedfile.__setitem__(key,item) - - def __getitem__(self,key): - return self.fixedfile.__getitem__(key) - - def __repr__(self): - return self.fixedfile.__repr__() - #s ='Fast input file: {}\n'.format(self.filename) - #return s+'\n'.join(['{:15s}: {}'.format(d['label'],d['value']) for i,d in enumerate(self.data)]) - - -# --------------------------------------------------------------------------------} -# --- BASE INPUT FILE -# --------------------------------------------------------------------------------{ -class FASTInputFileBase(File): - """ - Read/write an OpenFAST input file. The object behaves like a dictionary. - - Main methods - ------------ - - read, write, toDataFrame, keys - - Main keys - --------- - The keys correspond to the keys used in the file. For instance for a .fst file: 'DT','TMax' - - Examples - -------- - - filename = 'AeroDyn.dat' - f = FASTInputFile(filename) - f['TwrAero'] = True - f['AirDens'] = 1.225 - f.write('AeroDyn_Changed.dat') - - """ - @staticmethod - def defaultExtensions(): - return ['.dat','.fst','.txt','.fstf','.dvr'] - - @staticmethod - def formatName(): - return 'FAST input file Base' - - def __init__(self, filename=None, **kwargs): - self._size=None - self.setData() # Init data - if filename: - self.filename = filename - self.read() - - def setData(self, filename=None, data=None, hasNodal=False, module=None): - """ Set the data of this object. This object shouldn't store anything else. """ - if data is None: - self.data = [] - else: - self.data = data - self.hasNodal = hasNodal - self.module = module - self.filename = filename - - def keys(self): - self.labels = [ d['label'] for i,d in enumerate(self.data) if (not d['isComment']) and (i not in self._IComment)] - return self.labels - - def getID(self,label): - i=self.getIDSafe(label) - if i<0: - raise KeyError('Variable `'+ label+'` not found in FAST file:'+self.filename) - else: - return i - def getIDs(self,label): - I=[] - # brute force search - for i in range(len(self.data)): - d = self.data[i] - if d['label'].lower()==label.lower(): - I.append(i) - if len(I)<0: - raise KeyError('Variable `'+ label+'` not found in FAST file:'+self.filename) - else: - return I - - def getIDSafe(self,label): - # brute force search - for i in range(len(self.data)): - d = self.data[i] - if d['label'].lower()==label.lower(): - return i - return -1 - - # Making object an iterator - def __iter__(self): - self.iCurrent=-1 - self.iMax=len(self.data)-1 - return self - - def __next__(self): # Python 2: def next(self) - if self.iCurrent > self.iMax: - raise StopIteration - else: - self.iCurrent += 1 - return self.data[self.iCurrent] - - # Making it behave like a dictionary - def __setitem__(self, key, item): - I = self.getIDs(key) - for i in I: - if self.data[i]['tabType'] != TABTYPE_NOT_A_TAB: - # For tables, we automatically update variable that stores the dimension - nRows = len(item) - if 'tabDimVar' in self.data[i].keys(): - dimVar = self.data[i]['tabDimVar'] - iDimVar = self.getID(dimVar) - self.data[iDimVar]['value'] = nRows # Avoiding a recursive call to __setitem__ here - else: - pass - self.data[i]['value'] = item - - def __getitem__(self,key): - i = self.getID(key) - return self.data[i]['value'] - - def __repr__(self): - s ='Fast input file base: {}\n'.format(self.filename) - return s+'\n'.join(['{:15s}: {}'.format(d['label'],d['value']) for i,d in enumerate(self.data)]) - - def addKeyVal(self, key, val, descr=None): - i=self.getIDSafe(key) - if i<0: - d = getDict() - else: - d = self.data[i] - d['label']=key - d['value']=val - if descr is not None: - d['descr']=descr - if i<0: - self.data.append(d) - - def addValKey(self,val,key,descr=None): - self.addKeyVal(key, val, descr) - - def addComment(self, comment='!'): - d=getDict() - d['isComment'] = True - d['value'] = comment - self.data.append(d) - - def addTable(self, label, tab, cols=None, units=None, tabType=1, tabDimVar=None): - d=getDict() - d['label'] = label - d['value'] = tab - d['tabType'] = tabType - d['tabDimVar'] = tabDimVar - d['tabColumnNames'] = cols - d['tabUnits'] = units - self.data.append(d) - - @property - def comment(self): - return '\n'.join([self.data[i]['value'] for i in self._IComment]) - - @comment.setter - def comment(self, comment): - splits = comment.split('\n') - for i,com in zip(self._IComment, splits): - self.data[i]['value'] = com - self.data[i]['label'] = '' - self.data[i]['descr'] = '' - self.data[i]['isComment'] = True - - @property - def _IComment(self): - """ return indices of comment line""" - return [1] # Typical OpenFAST files have comment on second line [1] - - - def read(self, filename=None): - if filename: - self.filename = filename - if self.filename: - if not os.path.isfile(self.filename): - raise OSError(2,'File not found:',self.filename) - if os.stat(self.filename).st_size == 0: - raise EmptyFileError('File is empty:',self.filename) - self._read() - else: - raise Exception('No filename provided') - - def _read(self): - - # --- Tables that can be detected based on the "Value" (first entry on line) - # TODO members for BeamDyn with mutliple key point ####### TODO PropSetID is Duplicate SubDyn and used in HydroDyn - NUMTAB_FROM_VAL_DETECT = ['HtFract' , 'TwrElev' , 'BlFract' , 'Genspd_TLU' , 'BlSpn' , 'HvCoefID' , 'AxCoefID' , 'JointID' , 'Dpth' , 'FillNumM' , 'MGDpth' , 'SimplCd' , 'RNodes' , 'kp_xr' , 'mu1' , 'TwrHtFr' , 'TwrRe' , 'WT_X'] - NUMTAB_FROM_VAL_DIM_VAR = ['NTwInpSt' , 'NumTwrNds' , 'NBlInpSt' , 'DLL_NumTrq' , 'NumBlNds' , 'NHvCoef' , 'NAxCoef' , 'NJoints' , 'NCoefDpth' , 'NFillGroups' , 'NMGDepths' , 1 , 'BldNodes' , 'kp_total' , 1 , 'NTwrHt' , 'NTwrRe' , 'NumTurbines'] - NUMTAB_FROM_VAL_VARNAME = ['TowProp' , 'TowProp' , 'BldProp' , 'DLLProp' , 'BldAeroNodes' , 'HvCoefs' , 'AxCoefs' , 'Joints' , 'DpthProp' , 'FillGroups' , 'MGProp' , 'SmplProp' , 'BldAeroNodes' , 'MemberGeom' , 'DampingCoeffs' , 'TowerProp' , 'TowerRe', 'WindTurbines'] - NUMTAB_FROM_VAL_NHEADER = [2 , 2 , 2 , 2 , 2 , 2 , 2 , 2 , 2 , 2 , 2 , 2 , 1 , 2 , 2 , 1 , 1 , 2 ] - NUMTAB_FROM_VAL_TYPE = ['num' , 'num' , 'num' , 'num' , 'num' , 'num' , 'num' , 'num' , 'num' , 'num' , 'num' , 'num' , 'mix' , 'num' , 'num' , 'num' , 'num' , 'mix'] - # SubDyn - NUMTAB_FROM_VAL_DETECT += [ 'RJointID' , 'IJointID' , 'COSMID' , 'CMJointID' ] - NUMTAB_FROM_VAL_DIM_VAR += [ 'NReact' , 'NInterf' , 'NCOSMs' , 'NCmass' ] - NUMTAB_FROM_VAL_VARNAME += [ 'BaseJoints' , 'InterfaceJoints' , 'MemberCosineMatrix' , 'ConcentratedMasses'] - NUMTAB_FROM_VAL_NHEADER += [ 2 , 2 , 2 , 2 ] - NUMTAB_FROM_VAL_TYPE += [ 'mix' , 'num' , 'num' , 'num' ] - # AD Driver old and new - NUMTAB_FROM_VAL_DETECT += [ 'WndSpeed' , 'HWndSpeed' ] - NUMTAB_FROM_VAL_DIM_VAR += [ 'NumCases' , 'NumCases' ] - NUMTAB_FROM_VAL_VARNAME += [ 'Cases' , 'Cases' ] - NUMTAB_FROM_VAL_NHEADER += [ 2 , 2 ] - NUMTAB_FROM_VAL_TYPE += [ 'num' , 'num' ] - - # --- Tables that can be detected based on the "Label" (second entry on line) - # NOTE: MJointID1, used by SubDyn and HydroDyn - NUMTAB_FROM_LAB_DETECT = ['NumAlf' , 'F_X' , 'MemberCd1' , 'MJointID1' , 'NOutLoc' , 'NOutCnt' , 'PropD' ] - NUMTAB_FROM_LAB_DIM_VAR = ['NumAlf' , 'NKInpSt' , 'NCoefMembers' , 'NMembers' , 'NMOutputs' , 'NMOutputs' , 'NPropSets' ] - NUMTAB_FROM_LAB_VARNAME = ['AFCoeff' , 'TMDspProp' , 'MemberProp' , 'Members' , 'MemberOuts' , 'MemberOuts' , 'SectionProp' ] - NUMTAB_FROM_LAB_NHEADER = [2 , 2 , 2 , 2 , 2 , 2 , 2 ] - NUMTAB_FROM_LAB_NOFFSET = [0 , 0 , 0 , 0 , 0 , 0 , 0 ] - NUMTAB_FROM_LAB_TYPE = ['num' , 'num' , 'num' , 'mix' , 'num' , 'sdout' , 'num' ] - # MoorDyn Version 1 and 2 (with AUTO for LAB_DIM_VAR) - NUMTAB_FROM_LAB_DETECT += ['Diam' ,'Type' ,'LineType' , 'Attachment'] - NUMTAB_FROM_LAB_DIM_VAR += ['NTypes:AUTO','NConnects' ,'NLines:AUTO' , 'AUTO'] - NUMTAB_FROM_LAB_VARNAME += ['LineTypes' ,'ConnectionProp' ,'LineProp' , 'Points'] - NUMTAB_FROM_LAB_NHEADER += [ 2 , 2 , 2 , 2 ] - NUMTAB_FROM_LAB_NOFFSET += [ 0 , 0 , 0 , 0 ] - NUMTAB_FROM_LAB_TYPE += ['mix' ,'mix' ,'mix' , 'mix'] - # SubDyn - NUMTAB_FROM_LAB_DETECT += ['GuyanDampSize' , 'YoungE' , 'YoungE' , 'EA' , 'MatDens' ] - NUMTAB_FROM_LAB_DIM_VAR += [6 , 'NPropSets', 'NXPropSets', 'NCablePropSets' , 'NRigidPropSets'] - NUMTAB_FROM_LAB_VARNAME += ['GuyanDampMatrix' , 'BeamProp' , 'BeamPropX' , 'CableProp' , 'RigidProp' ] - NUMTAB_FROM_LAB_NHEADER += [0 , 2 , 2 , 2 , 2 ] - NUMTAB_FROM_LAB_NOFFSET += [1 , 0 , 0 , 0 , 0 ] - NUMTAB_FROM_LAB_TYPE += ['num' , 'num' , 'num' , 'num' , 'num' ] - # OLAF - NUMTAB_FROM_LAB_DETECT += ['GridName' ] - NUMTAB_FROM_LAB_DIM_VAR += ['nGridOut' ] - NUMTAB_FROM_LAB_VARNAME += ['GridOutputs'] - NUMTAB_FROM_LAB_NHEADER += [0 ] - NUMTAB_FROM_LAB_NOFFSET += [2 ] - NUMTAB_FROM_LAB_TYPE += ['mix' ] - - FILTAB_FROM_LAB_DETECT = ['FoilNm' ,'AFNames'] - FILTAB_FROM_LAB_DIM_VAR = ['NumFoil','NumAFfiles'] - FILTAB_FROM_LAB_VARNAME = ['FoilNm' ,'AFNames'] - - # Using lower case to be more tolerant.. - NUMTAB_FROM_VAL_DETECT_L = [s.lower() for s in NUMTAB_FROM_VAL_DETECT] - NUMTAB_FROM_LAB_DETECT_L = [s.lower() for s in NUMTAB_FROM_LAB_DETECT] - FILTAB_FROM_LAB_DETECT_L = [s.lower() for s in FILTAB_FROM_LAB_DETECT] - - # Reset data - self.data = [] - self.hasNodal=False - self.module = None - #with open(self.filename, 'r', errors="surrogateescape") as f: - with open(self.filename, 'r', errors="surrogateescape") as f: - lines=f.read().splitlines() - # IF NEEDED> DO THE FOLLOWING FORMATTING: - #lines = [str(l).encode('utf-8').decode('ascii','ignore') for l in f.read().splitlines()] - - # Fast files start with ! or - - #if lines[0][0]!='!' and lines[0][0]!='-': - # raise Exception('Fast file do not start with ! or -, is it the right format?') - - # Special filetypes - if detectAndReadExtPtfmSE(self, lines): - return - if self.detectAndReadAirfoilAD14(lines): - return - - # Parsing line by line, storing each line into a dictionary - i=0 - nComments = 0 - nWrongLabels = 0 - allowSpaceSeparatedList=False - iTab = 0 - - labOffset='' - while i0 \ - or line.upper().find('MESH-BASED OUTPUTS')>0 \ - or line.upper().find('OUTPUT CHANNELS' )>0: # "OutList - The next line(s) contains a list of output parameters. See OutListParameters.xlsx for a listing of available output channels, (-)'" - # TODO, lazy implementation so far, MAKE SUB FUNCTION - parts = re.match(r'^\W*\w+', line) - if parts: - firstword = parts.group(0).strip() - else: - raise NotImplementedError - remainer = re.sub(r'^\W*\w+\W*', '', line) - # Parsing outlist, and then we continue at a new "i" (to read END etc.) - OutList,i = parseFASTOutList(lines,i+1) - d = getDict() - if self.hasNodal and not firstword.endswith('_Nodal'): - d['label'] = firstword+'_Nodal' - else: - d['label'] = firstword - d['descr'] = remainer - d['tabType'] = TABTYPE_FIL # TODO - d['value'] = ['']+OutList - self.data.append(d) - if i>=len(lines): - break - - # --- Here we cheat and force an exit of the input file - # The reason for this is that some files have a lot of things after the END, which will result in the file being intepreted as a wrong format due to too many comments - if i+20 or lines[i+2].lower().find('bldnd_bloutnd')>0): - self.hasNodal=True - else: - self.data.append(parseFASTInputLine('END of input file (the word "END" must appear in the first 3 columns of this last OutList line)',i+1)) - self.data.append(parseFASTInputLine('---------------------------------------------------------------------------------------',i+2)) - break - elif line.upper().find('SSOUTLIST' )>0 or line.upper().find('SDOUTLIST' )>0: - # SUBDYN Outlist doesn not follow regular format - self.data.append(parseFASTInputLine(line,i)) - # OUTLIST Exception for BeamDyn - OutList,i = parseFASTOutList(lines,i+1) - # TODO - for o in OutList: - d = getDict() - d['isComment'] = True - d['value']=o - self.data.append(d) - # --- Here we cheat and force an exit of the input file - self.data.append(parseFASTInputLine('END of input file (the word "END" must appear in the first 3 columns of this last OutList line)',i+1)) - self.data.append(parseFASTInputLine('---------------------------------------------------------------------------------------',i+2)) - break - elif line.upper().find('ADDITIONAL STIFFNESS')>0: - # TODO, lazy implementation so far, MAKE SUB FUNCTION - self.data.append(parseFASTInputLine(line,i)) - i +=1 - KDAdd = [] - for _ in range(19): - KDAdd.append(lines[i]) - i +=1 - d = getDict() - d['label'] = 'KDAdd' # TODO - d['tabType'] = TABTYPE_FIL # TODO - d['value'] = KDAdd - self.data.append(d) - if i>=len(lines): - break - elif line.upper().find('DISTRIBUTED PROPERTIES')>0: - self.data.append(parseFASTInputLine(line,i)); - i+=1; - self.readBeamDynProps(lines,i) - return - elif line.upper().find('OUTPUTS')>0: - if 'Points' in self.keys() and 'dtM' in self.keys(): - OutList,i = parseFASTOutList(lines,i+1) - d = getDict() - d['label'] = 'Outlist' - d['descr'] = '' - d['tabType'] = TABTYPE_FIL # TODO - d['value'] = OutList - self.addComment('------------------------ OUTPUTS --------------------------------------------') - self.data.append(d) - self.addComment('END') - self.addComment('------------------------- need this line --------------------------------------') - return - - # --- Parsing of standard lines: value(s) key comment - line = lines[i] - d = parseFASTInputLine(line,i,allowSpaceSeparatedList) - labelRaw =d['label'].lower() - d['label']+=labOffset - - # --- Handling of special files - if labelRaw=='kp_total': - # BeamDyn has weird space speparated list around keypoint definition - allowSpaceSeparatedList=True - elif labelRaw=='numcoords': - # TODO, lazy implementation so far, MAKE SUB FUNCTION - if isStr(d['value']): - if d['value'][0]=='@': - # it's a ref to the airfoil coord file - pass - else: - if not strIsInt(d['value']): - raise WrongFormatError('Wrong value of NumCoords') - if int(d['value'])<=0: - pass - else: - self.data.append(d); i+=1; - # 3 comment lines - self.data.append(parseFASTInputLine(lines[i],i)); i+=1; - self.data.append(parseFASTInputLine(lines[i],i)); i+=1; - self.data.append(parseFASTInputLine(lines[i],i)); i+=1; - splits=cleanAfterChar(cleanLine(lines[i]),'!').split() - # Airfoil ref point - try: - pos=[float(splits[0]), float(splits[1])] - except: - raise WrongFormatError('Wrong format while reading coordinates of airfoil reference') - i+=1 - d = getDict() - d['label'] = 'AirfoilRefPoint' - d['value'] = pos - self.data.append(d) - # 2 comment lines - self.data.append(parseFASTInputLine(lines[i],i)); i+=1; - self.data.append(parseFASTInputLine(lines[i],i)); i+=1; - # Table of coordinats itself - d = getDict() - d['label'] = 'AirfoilCoord' - d['tabDimVar'] = 'NumCoords' - d['tabType'] = TABTYPE_NUM_WITH_HEADERCOM - nTabLines = self[d['tabDimVar']]-1 # SOMEHOW ONE DATA POINT LESS - d['value'], d['tabColumnNames'],_ = parseFASTNumTable(self.filename,lines[i:i+nTabLines+1],nTabLines,i,1) - d['tabUnits'] = ['(-)','(-)'] - self.data.append(d) - break - - elif labelRaw=='re': - try: - nAirfoilTab = self['NumTabs'] - iTab +=1 - if nAirfoilTab>1: - labOffset ='_'+str(iTab) - d['label']=labelRaw+labOffset - except: - # Unsteady driver input file... - pass - - - #print('label>',d['label'],'<',type(d['label'])); - #print('value>',d['value'],'<',type(d['value'])); - #print(isStr(d['value'])) - #if isStr(d['value']): - # print(d['value'].lower() in NUMTAB_FROM_VAL_DETECT_L) - - - # --- Handling of tables - if isStr(d['value']) and d['value'].lower() in NUMTAB_FROM_VAL_DETECT_L: - # Table with numerical values, - ii = NUMTAB_FROM_VAL_DETECT_L.index(d['value'].lower()) - tab_type = NUMTAB_FROM_VAL_TYPE[ii] - if tab_type=='num': - d['tabType'] = TABTYPE_NUM_WITH_HEADER - else: - d['tabType'] = TABTYPE_MIX_WITH_HEADER - d['label'] = NUMTAB_FROM_VAL_VARNAME[ii]+labOffset - d['tabDimVar'] = NUMTAB_FROM_VAL_DIM_VAR[ii] - nHeaders = NUMTAB_FROM_VAL_NHEADER[ii] - nTabLines=0 - if isinstance(d['tabDimVar'],int): - nTabLines = d['tabDimVar'] - 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']) - i += nTabLines+nHeaders-1 - - # --- Temporary hack for e.g. SubDyn, that has duplicate table, impossible to detect in the current way... - # So we remove the element form the list one read - del NUMTAB_FROM_VAL_DETECT[ii] - del NUMTAB_FROM_VAL_DIM_VAR[ii] - del NUMTAB_FROM_VAL_VARNAME[ii] - del NUMTAB_FROM_VAL_NHEADER[ii] - del NUMTAB_FROM_VAL_TYPE [ii] - del NUMTAB_FROM_VAL_DETECT_L[ii] - - elif isStr(labelRaw) and labelRaw in NUMTAB_FROM_LAB_DETECT_L: - ii = NUMTAB_FROM_LAB_DETECT_L.index(labelRaw) - tab_type = NUMTAB_FROM_LAB_TYPE[ii] - # Special case for airfoil data, the table follows NumAlf, so we add d first - doDelete =True - if labelRaw=='numalf': - doDelete =False - d['tabType']=TABTYPE_NOT_A_TAB - self.data.append(d) - # Creating a new dictionary for the table - d = {'value':None, 'label':'NumAlf'+labOffset, 'isComment':False, 'descr':'', 'tabType':None} - i += 1 - nHeaders = NUMTAB_FROM_LAB_NHEADER[ii] - nOffset = NUMTAB_FROM_LAB_NOFFSET[ii] - if nOffset>0: - # Creating a dictionary for that entry - dd = {'value':d['value'], 'label':d['label']+labOffset, 'isComment':False, 'descr':d['descr'], 'tabType':TABTYPE_NOT_A_TAB} - self.data.append(dd) - - d['label'] = NUMTAB_FROM_LAB_VARNAME[ii] - if d['label'].lower()=='afcoeff' : - d['tabType'] = TABTYPE_NUM_WITH_HEADERCOM - else: - if tab_type=='num': - d['tabType'] = TABTYPE_NUM_WITH_HEADER - elif tab_type=='sdout': - d['tabType'] = TABTYPE_NUM_SUBDYNOUT - else: - d['tabType'] = TABTYPE_MIX_WITH_HEADER - # Finding table dimension (number of lines) - tabDimVar = NUMTAB_FROM_LAB_DIM_VAR[ii] - if isinstance(tabDimVar, int): # dimension hardcoded - d['tabDimVar'] = tabDimVar - nTabLines = d['tabDimVar'] - else: - # We either use a variable name or "AUTO" to find the number of rows - tabDimVars = tabDimVar.split(':') - for tabDimVar in tabDimVars: - d['tabDimVar'] = tabDimVar - if tabDimVar=='AUTO': - # Determine table dimension automatically - nTabLines = findNumberOfTableLines(lines[i+nHeaders:], break_chars=['---','!','#']) - break - else: - try: - nTabLines = self[tabDimVar+labOffset] - break - except KeyError: - #print('Cannot determine table dimension using {}'.format(tabDimVar)) - # Hopefully this table has AUTO as well - pass - - d['label'] += labOffset - #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+nOffset],nTabLines,i, nHeaders, tableType=tab_type, nOffset=nOffset, varNumLines=d['tabDimVar']) - i += nTabLines+1-nOffset - - # --- Temporary hack for e.g. SubDyn, that has duplicate table, impossible to detect in the current way... - # So we remove the element form the list one read - if doDelete: - del NUMTAB_FROM_LAB_DETECT[ii] - del NUMTAB_FROM_LAB_DIM_VAR[ii] - del NUMTAB_FROM_LAB_VARNAME[ii] - del NUMTAB_FROM_LAB_NHEADER[ii] - del NUMTAB_FROM_LAB_NOFFSET[ii] - del NUMTAB_FROM_LAB_TYPE [ii] - del NUMTAB_FROM_LAB_DETECT_L[ii] - - elif isStr(d['label']) and d['label'].lower() in FILTAB_FROM_LAB_DETECT_L: - ii = FILTAB_FROM_LAB_DETECT_L.index(d['label'].lower()) - d['label'] = FILTAB_FROM_LAB_VARNAME[ii]+labOffset - d['tabDimVar'] = FILTAB_FROM_LAB_DIM_VAR[ii] - d['tabType'] = TABTYPE_FIL - nTabLines = self[d['tabDimVar']] - #print('Reading table {} Dimension {} (based on {})'.format(d['label'],nTabLines,d['tabDimVar'])); - d['value'] = parseFASTFilTable(lines[i:i+nTabLines],nTabLines,i) - i += nTabLines-1 - - - - self.data.append(d) - i += 1 - # --- Safety checks - if d['isComment']: - #print(line) - nComments +=1 - else: - if hasSpecialChars(d['label']): - nWrongLabels +=1 - #print('label>',d['label'],'<',type(d['label']),line); - if i>3: # first few lines may be comments, we allow it - #print('Line',i,'Label:',d['label']) - raise WrongFormatError('Special Character found in Label: `{}`, for line: `{}`'.format(d['label'],line)) - if len(d['label'])==0: - nWrongLabels +=1 - if nComments>len(lines)*0.35: - #print('Comment fail',nComments,len(lines),self.filename) - raise WrongFormatError('Most lines were read as comments, probably not a FAST Input File: {}'.format(self.filename)) - if nWrongLabels>len(lines)*0.10: - #print('Label fail',nWrongLabels,len(lines),self.filename) - raise WrongFormatError('Too many lines with wrong labels, probably not a FAST Input File {}:'.format(self.filename)) - - # --- END OF FOR LOOP ON LINES - - # --- PostReading checks - labels = self.keys() - duplicates = set([x for x in labels if (labels.count(x) > 1) and x!='OutList' and x.strip()!='-']) - if len(duplicates)>0: - print('[WARN] Duplicate labels found in file: '+self.filename) - print(' Duplicates: '+', '.join(duplicates)) - print(' It\'s strongly recommended to make them unique! ') -# except WrongFormatError as e: -# raise WrongFormatError('Fast File {}: '.format(self.filename)+'\n'+e.args[0]) -# except Exception as e: -# raise e -# # print(e) -# raise Exception('Fast File {}: '.format(self.filename)+'\n'+e.args[0]) - self._lines = lines - - - def toString(self): - s='' - # Special file formats, TODO subclass - def toStringVLD(val,lab,descr): - val='{}'.format(val) - lab='{}'.format(lab) - if len(val)<13: - val='{:13s}'.format(val) - if len(lab)<13: - lab='{:13s}'.format(lab) - return val+' '+lab+' - '+descr.strip().lstrip('-').lstrip() - - def toStringIntFloatStr(x): - try: - if int(x)==x: - s='{:15.0f}'.format(x) - else: - s='{:15.8e}'.format(x) - except: - s=x - return s - - def beamdyn_section_mat_tostring(x,K,M): - def mat_tostring(M,fmt='24.16e'): - return '\n'.join([' '+' '.join(['{:24.16E}'.format(m) for m in M[i,:]]) for i in range(np.size(M,1))]) - s='' - s+='{:.6f}\n'.format(x) - s+=mat_tostring(K) - #s+=np.array2string(K) - s+='\n' - s+='\n' - s+=mat_tostring(M) - #s+=np.array2string(M) - s+='\n' - s+='\n' - return s - - for i in range(len(self.data)): - d=self.data[i] - if d['isComment']: - s+='{}'.format(d['value']) - elif d['tabType']==TABTYPE_NOT_A_TAB: - if isinstance(d['value'], list): - sList=', '.join([str(x) for x in d['value']]) - s+=toStringVLD(sList, d['label'], d['descr']) - else: - s+=toStringVLD(d['value'],d['label'],d['descr']) - elif d['tabType']==TABTYPE_NUM_WITH_HEADER: - if d['tabColumnNames'] is not None: - s+='{}'.format(' '.join(['{:15s}'.format(s) for s in d['tabColumnNames']])) - #s+=d['descr'] # Not ready for that - if d['tabUnits'] is not None: - s+='\n' - s+='{}'.format(' '.join(['{:15s}'.format(s) for s in d['tabUnits']])) - newline='\n' - else: - newline='' - if np.size(d['value'],0) > 0 : - s+=newline - s+='\n'.join('\t'.join( ('{:15.0f}'.format(x) if int(x)==x else '{:15.8e}'.format(x) ) for x in y) for y in d['value']) - elif d['tabType']==TABTYPE_MIX_WITH_HEADER: - s+='{}'.format(' '.join(['{:15s}'.format(s) for s in d['tabColumnNames']])) - if d['tabUnits'] is not None: - s+='\n' - s+='{}'.format(' '.join(['{:15s}'.format(s) for s in d['tabUnits']])) - if np.size(d['value'],0) > 0 : - s+='\n' - s+='\n'.join('\t'.join(toStringIntFloatStr(x) for x in y) for y in d['value']) - elif d['tabType']==TABTYPE_NUM_WITH_HEADERCOM: - s+='! {}\n'.format(' '.join(['{:15s}'.format(s) for s in d['tabColumnNames']])) - s+='! {}\n'.format(' '.join(['{:15s}'.format(s) for s in d['tabUnits']])) - s+='\n'.join('\t'.join('{:15.8e}'.format(x) for x in y) for y in d['value']) - elif d['tabType']==TABTYPE_FIL: - #f.write('{} {} {}\n'.format(d['value'][0],d['tabDetect'],d['descr'])) - label = d['label'] - if 'kbot' in self.keys(): # Moordyn has no 'OutList' label.. - label='' - if len(d['value'])==1: - s+='{} {} {}'.format(d['value'][0], label, d['descr']) # TODO? - else: - s+='{} {} {}\n'.format(d['value'][0], label, d['descr']) # TODO? - s+='\n'.join(fil for fil in d['value'][1:]) - elif d['tabType']==TABTYPE_NUM_BEAMDYN: - # TODO use dedicated sub-class - data = d['value'] - Cols =['Span'] - Cols+=['K{}{}'.format(i+1,j+1) for i in range(6) for j in range(6)] - Cols+=['M{}{}'.format(i+1,j+1) for i in range(6) for j in range(6)] - for i in np.arange(len(data['span'])): - x = data['span'][i] - K = data['K'][i] - M = data['M'][i] - s += beamdyn_section_mat_tostring(x,K,M) - elif d['tabType']==TABTYPE_NUM_SUBDYNOUT: - data = d['value'] - s+='{}\n'.format(' '.join(['{:15s}'.format(s) for s in d['tabColumnNames']])) - s+='{}'.format(' '.join(['{:15s}'.format(s) for s in d['tabUnits']])) - if np.size(d['value'],0) > 0 : - s+='\n' - s+='\n'.join('\t'.join('{:15.0f}'.format(x) for x in y) for y in data) - else: - raise Exception('Unknown table type for variable {}'.format(d)) - if i0: - print('[WARN] Creating directory: ',dirname) - os.makedirs(dirname) - - self._write() - else: - raise Exception('No filename provided') - - def _writeSanityChecks(self): - """ Sanity checks before write""" - pass - - def _write(self): - self._writeSanityChecks() - with open(self.filename,'w') as f: - f.write(self.toString()) - - def toDataFrame(self): - return self._toDataFrame() - - def _toDataFrame(self): - dfs={} - - for i in range(len(self.data)): - d=self.data[i] - if d['tabType'] in [TABTYPE_NUM_WITH_HEADER, TABTYPE_NUM_WITH_HEADERCOM, TABTYPE_NUM_NO_HEADER, TABTYPE_MIX_WITH_HEADER]: - Val= d['value'] - if d['tabUnits'] is None: - Cols=d['tabColumnNames'] - else: - Cols=['{}_{}'.format(c,u.replace('(','[').replace(')',']')) for c,u in zip(d['tabColumnNames'],d['tabUnits'])] - #print(Val) - #print(Cols) - - # --- Adding some useful tabulated data for some files (Shapefunctions, polar) - - if self.getIDSafe('TwFAM1Sh(2)')>0: - # Hack for tower files, we add the modes - # NOTE: we provide interpolated shape function just in case the resolution of the input file is low.. - x=Val[:,0] - Modes=np.zeros((x.shape[0],4)) - Modes[:,0] = x**2 * self['TwFAM1Sh(2)'] + x**3 * self['TwFAM1Sh(3)'] + x**4 * self['TwFAM1Sh(4)'] + x**5 * self['TwFAM1Sh(5)'] + x**6 * self['TwFAM1Sh(6)'] - Modes[:,1] = x**2 * self['TwFAM2Sh(2)'] + x**3 * self['TwFAM2Sh(3)'] + x**4 * self['TwFAM2Sh(4)'] + x**5 * self['TwFAM2Sh(5)'] + x**6 * self['TwFAM2Sh(6)'] - Modes[:,2] = x**2 * self['TwSSM1Sh(2)'] + x**3 * self['TwSSM1Sh(3)'] + x**4 * self['TwSSM1Sh(4)'] + x**5 * self['TwSSM1Sh(5)'] + x**6 * self['TwSSM1Sh(6)'] - Modes[:,3] = x**2 * self['TwSSM2Sh(2)'] + x**3 * self['TwSSM2Sh(3)'] + x**4 * self['TwSSM2Sh(4)'] + x**5 * self['TwSSM2Sh(5)'] + x**6 * self['TwSSM2Sh(6)'] - Val = np.hstack((Val,Modes)) - ShapeCols = [c+'_[-]' for c in ['ShapeForeAft1','ShapeForeAft2','ShapeSideSide1','ShapeSideSide2']] - Cols = Cols + ShapeCols - - name=d['label'] - - if name=='DampingCoeffs': - pass - else: - dfs[name]=pd.DataFrame(data=Val,columns=Cols) - elif d['tabType'] in [TABTYPE_NUM_BEAMDYN]: - span = d['value']['span'] - M = d['value']['M'] - K = d['value']['K'] - nSpan=len(span) - MM=np.zeros((nSpan,1+36+36)) - MM[:,0] = span - MM[:,1:37] = K.reshape(nSpan,36) - MM[:,37:] = M.reshape(nSpan,36) - Cols =['Span'] - Cols+=['K{}{}'.format(i+1,j+1) for i in range(6) for j in range(6)] - Cols+=['M{}{}'.format(i+1,j+1) for i in range(6) for j in range(6)] - # Putting the main terms first - IAll = range(1+36+36) - IMain= [0] + [i*6+i+1 for i in range(6)] + [i*6+i+37 for i in range(6)] - IOrg = IMain + [i for i in range(1+36+36) if i not in IMain] - Cols = [Cols[i] for i in IOrg] - data = MM[:,IOrg] - name=d['label'] - dfs[name]=pd.DataFrame(data=data,columns=Cols) - if len(dfs)==1: - dfs=dfs[list(dfs.keys())[0]] - return dfs - - def toGraph(self, **kwargs): - from .fast_input_file_graph import fastToGraph - return fastToGraph(self, **kwargs) - - - -# --------------------------------------------------------------------------------} -# --- SubReaders /detectors -# --------------------------------------------------------------------------------{ - - - def detectAndReadAirfoilAD14(self,lines): - if len(lines)<14: - return False - # Reading number of tables - L3 = lines[2].strip().split() - if len(L3)<=0: - return False - if not strIsInt(L3[0]): - return False - nTables=int(L3[0]) - # Reading table ID - L4 = lines[3].strip().split() - if len(L4)<=nTables: - return False - TableID=L4[:nTables] - if nTables==1: - TableID=[''] - # Keywords for file format - KW1=lines[12].strip().split() - KW2=lines[13].strip().split() - if len(KW1)>nTables and len(KW2)>nTables: - if KW1[nTables].lower()=='angle' and KW2[nTables].lower()=='minimum': - d = getDict(); d['isComment'] = True; d['value'] = lines[0]; self.data.append(d); - d = getDict(); d['isComment'] = True; d['value'] = lines[1]; self.data.append(d); - for i in range(2,14): - splits = lines[i].split() - #print(splits) - d = getDict() - d['label'] = ' '.join(splits[1:]) # TODO - d['descr'] = ' '.join(splits[1:]) # TODO - d['value'] = float(splits[0]) - self.data.append(d) - #pass - #for i in range(2,14): - nTabLines=0 - while 14+nTabLines0 : - nTabLines +=1 - #data = np.array([lines[i].strip().split() for i in range(14,len(lines)) if len(lines[i])>0]).astype(float) - #data = np.array([lines[i].strip().split() for i in takewhile(lambda x: len(lines[i].strip())>0, range(14,len(lines)-1))]).astype(float) - data = np.array([lines[i].strip().split() for i in range(14,nTabLines+14)]).astype(float) - #print(data) - d = getDict() - d['label'] = 'Polar' - d['tabDimVar'] = nTabLines - d['tabType'] = TABTYPE_NUM_NO_HEADER - d['value'] = data - if np.size(data,1)==1+nTables*3: - d['tabColumnNames'] = ['Alpha']+[n+l for l in TableID for n in ['Cl','Cd','Cm']] - d['tabUnits'] = ['(deg)']+['(-)' , '(-)' , '(-)']*nTables - elif np.size(data,1)==1+nTables*2: - d['tabColumnNames'] = ['Alpha']+[n+l for l in TableID for n in ['Cl','Cd']] - d['tabUnits'] = ['(deg)']+['(-)' , '(-)']*nTables - else: - d['tabColumnNames'] = ['col{}'.format(j) for j in range(np.size(data,1))] - self.data.append(d) - return True - - def readBeamDynProps(self,lines,iStart): - nStations=self['station_total'] - #M=np.zeros((nStations,1+36+36)) - M = np.zeros((nStations,6,6)) - K = np.zeros((nStations,6,6)) - span = np.zeros(nStations) - i=iStart; - try: - for j in range(nStations): - # Read span location - span[j]=float(lines[i]); i+=1; - # Read stiffness matrix - K[j,:,:]=np.array((' '.join(lines[i:i+6])).split()).astype(float).reshape(6,6) - i+=7 - # Read mass matrix - M[j,:,:]=np.array((' '.join(lines[i:i+6])).split()).astype(float).reshape(6,6) - i+=7 - except: - raise WrongFormatError('An error occured while reading section {}/{}'.format(j+1,nStations)) - d = getDict() - d['label'] = 'BeamProperties' - d['descr'] = '' - d['tabType'] = TABTYPE_NUM_BEAMDYN - d['value'] = {'span':span, 'K':K, 'M':M} - self.data.append(d) - - -# --------------------------------------------------------------------------------} -# --- Helper functions -# --------------------------------------------------------------------------------{ -def isStr(s): - return isinstance(s, str) - -def strIsFloat(s): - #return s.replace('.',',1').isdigit() - try: - float(s) - return True - except: - return False - -def strIsBool(s): - return s.lower() in ['true','false','t','f'] - -def strIsInt(s): - s = str(s) - if s[0] in ('-', '+'): - return s[1:].isdigit() - return s.isdigit() - -def strToBool(s): - return s.lower() in ['true','t'] - -def hasSpecialChars(s): - # fast allows for parenthesis - # For now we allow for - but that's because of BeamDyn geometry members - return not re.match("^[\"\'a-zA-Z0-9_()-]*$", s) - -def cleanLine(l): - # makes a string single space separated - l = l.replace('\t',' ') - l = ' '.join(l.split()) - l = l.strip() - return l - -def cleanAfterChar(l,c): - # remove whats after a character - n = l.find(c); - if n>0: - return l[:n] - else: - return l - -def getDict(): - return {'value':None, 'label':'', 'isComment':False, 'descr':'', 'tabType':TABTYPE_NOT_A_TAB} - -def _merge_value(splits): - - merged = splits.pop(0) - if merged[0] == '"': - while merged[-1] != '"': - merged += " "+splits.pop(0) - splits.insert(0, merged) - - - - -def parseFASTInputLine(line_raw,i,allowSpaceSeparatedList=False): - d = getDict() - #print(line_raw) - try: - # preliminary cleaning (Note: loss of formatting) - line = cleanLine(line_raw) - # Comment - if any(line.startswith(c) for c in ['#','!','--','==']) or len(line)==0: - d['isComment']=True - d['value']=line_raw - return d - if line.lower().startswith('end'): - sp =line.split() - if len(sp)>2 and sp[1]=='of': - d['isComment']=True - d['value']=line_raw - - # Detecting lists - List=[]; - iComma=line.find(',') - if iComma>0 and iComma<30: - fakeline=line.replace(' ',',') - fakeline=re.sub(',+',',',fakeline) - csplits=fakeline.split(',') - # Splitting based on comma and looping while it's numbers of booleans - ii=0 - s=csplits[ii] - #print(csplits) - while strIsFloat(s) or strIsBool(s) and ii=len(csplits): - raise WrongFormatError('Wrong number of list values') - s = csplits[ii] - #print('[INFO] Line {}: Found list: '.format(i),List) - # Defining value and remaining splits - if len(List)>=2: - d['value']=List - line_remaining=line - # eating line, removing each values - for iii in range(ii): - sValue=csplits[iii] - ipos=line_remaining.find(sValue) - line_remaining = line_remaining[ipos+len(sValue):] - splits=line_remaining.split() - iNext=0 - else: - # It's not a list, we just use space as separators - splits=line.split(' ') - _merge_value(splits) - s=splits[0] - - if strIsInt(s): - d['value']=int(s) - if allowSpaceSeparatedList and len(splits)>1: - if strIsInt(splits[1]): - d['value']=splits[0]+ ' '+splits[1] - elif strIsFloat(s): - d['value']=float(s) - elif strIsBool(s): - d['value']=strToBool(s) - else: - d['value']=s - iNext=1 - - # Extracting label (TODO, for now only second split) - bOK=False - while (not bOK) and iNext comment assumed'.format(i+1)) - d['isComment']=True - d['value']=line_raw - iNext = len(splits)+1 - - # Recombining description - if len(splits)>=iNext+1: - d['descr']=' '.join(splits[iNext:]) - except WrongFormatError as e: - raise WrongFormatError('Line {}: '.format(i+1)+e.args[0]) - except Exception as e: - raise Exception('Line {}: '.format(i+1)+e.args[0]) - - return d - -def parseFASTOutList(lines,iStart): - OutList=[] - i = iStart - while i=len(lines): - print('[WARN] End of file reached while reading Outlist') - #i=min(i+1,len(lines)) - return OutList,iStart+len(OutList) - - -def extractWithinParenthesis(s): - mo = re.search(r'\((.*)\)', s) - if mo: - return mo.group(1) - return '' - -def extractWithinBrackets(s): - mo = re.search(r'\((.*)\)', s) - if mo: - return mo.group(1) - return '' - -def detectUnits(s,nRef): - nPOpen=s.count('(') - nPClos=s.count(')') - nBOpen=s.count('[') - nBClos=s.count(']') - - sep='!#@#!' - if (nPOpen == nPClos) and (nPOpen>=nRef): - #that's pretty good - Units=s.replace('(','').replace(')',sep).split(sep)[:-1] - elif (nBOpen == nBClos) and (nBOpen>=nRef): - Units=s.replace('[','').replace(']',sep).split(sep)[:-1] - else: - Units=s.split() - return Units - - -def findNumberOfTableLines(lines, break_chars): - """ Loop through lines until a one of the "break character is found""" - for i, l in enumerate(lines): - for bc in break_chars: - if l.startswith(bc): - return i - # Not found - print('[FAIL] end of table not found') - return len(lines) - - -def parseFASTNumTable(filename,lines,n,iStart,nHeaders=2,tableType='num',nOffset=0, varNumLines=''): - """ - First lines of data starts at: nHeaders+nOffset - - """ - Tab = None - ColNames = None - Units = None - - - if len(lines)!=n+nHeaders+nOffset: - raise BrokenFormatError('Not enough lines in table: {} lines instead of {}\nFile:{}'.format(len(lines)-nHeaders,n,filename)) - try: - if nHeaders==0: - # Extract number of values from number of numerical values on first line - numeric_const_pattern = r'[-+]? (?: (?: \d* \. \d+ ) | (?: \d+ \.? ) )(?: [Ee] [+-]? \d+ ) ?' - rx = re.compile(numeric_const_pattern, re.VERBOSE) - header = cleanAfterChar(lines[nOffset], '!') - if tableType=='num': - dat= np.array(rx.findall(header)).astype(float) - ColNames=['C{}'.format(j) for j in range(len(dat))] - else: - raise NotImplementedError('Reading FAST tables with no headers for type different than num not implemented yet') - - elif nHeaders>=1: - # Extract column names - i = 0 - sTmp = cleanLine(lines[i]) - sTmp = cleanAfterChar(sTmp,'[') - sTmp = cleanAfterChar(sTmp,'(') - sTmp = cleanAfterChar(sTmp,'!') - sTmp = cleanAfterChar(sTmp,'#') - if sTmp.startswith('!'): - sTmp=sTmp[1:].strip() - ColNames=sTmp.split() - if nHeaders>=2: - # Extract units - i = 1 - sTmp = cleanLine(lines[i]) - sTmp = cleanAfterChar(sTmp,'!') - sTmp = cleanAfterChar(sTmp,'#') - if sTmp.startswith('!'): - sTmp=sTmp[1:].strip() - - Units = detectUnits(sTmp,len(ColNames)) - Units = ['({})'.format(u.strip()) for u in Units] - # Forcing user to match number of units and column names - if len(ColNames) != len(Units): - print(ColNames) - print(Units) - print('[WARN] {}: Line {}: Number of column names different from number of units in table'.format(filename, iStart+i+1)) - - nCols=len(ColNames) - - if tableType=='num': - if n==0: - Tab = np.zeros((n, nCols)) - for i in range(nHeaders+nOffset,n+nHeaders+nOffset): - l = cleanAfterChar(lines[i].lower(),'!') - l = cleanAfterChar(l,'#') - v = l.split() - if len(v) != nCols: - # Discarding SubDyn special cases - if ColNames[-1].lower() not in ['nodecnt']: - print('[WARN] {}: Line {}: number of data different from number of column names. ColumnNames: {}'.format(filename, iStart+i+1, ColNames)) - if i==nHeaders+nOffset: - # Node Cnt - if len(v) != nCols: - if ColNames[-1].lower()== 'nodecnt': - ColNames = ColNames+['Col']*(len(v)-nCols) - Units = Units+['Col']*(len(v)-nCols) - - nCols=len(v) - Tab = np.zeros((n, nCols)) - # Accounting for TRUE FALSE and converting to float - v = [s.replace('true','1').replace('false','0').replace('noprint','0').replace('print','1') for s in v] - v = [float(s) for s in v[0:nCols]] - if len(v) < nCols: - raise Exception('Number of data is lower than number of column names') - Tab[i-nHeaders-nOffset,:] = v - elif tableType=='mix': - # a mix table contains a mixed of strings and floats - # For now, we are being a bit more relaxed about the number of columns - if n==0: - Tab = np.zeros((n, nCols)).astype(object) - for i in range(nHeaders+nOffset,n+nHeaders+nOffset): - l = lines[i] - l = cleanAfterChar(l,'!') - l = cleanAfterChar(l,'#') - v = l.split() - if l.startswith('---'): - raise BrokenFormatError('Error reading line {} while reading table. Is the variable `{}` set correctly?'.format(iStart+i+1, varNumLines)) - if len(v) != nCols: - # Discarding SubDyn special cases - if ColNames[-1].lower() not in ['cosmid', 'ssifile']: - print('[WARN] {}: Line {}: Number of data is different than number of column names. Column Names: {}'.format(filename,iStart+1+i, ColNames)) - if i==nHeaders+nOffset: - if len(v)>nCols: - ColNames = ColNames+['Col']*(len(v)-nCols) - Units = Units+['Col']*(len(v)-nCols) - nCols=len(v) - Tab = np.zeros((n, nCols)).astype(object) - v=v[0:min(len(v),nCols)] - Tab[i-nHeaders-nOffset,0:len(v)] = v - # If all values are float, we convert to float - if all([strIsFloat(x) for x in Tab.ravel()]): - Tab=Tab.astype(float) - elif tableType=='sdout': - header = lines[0] - units = lines[1] - Tab=[] - for i in range(nHeaders+nOffset,n+nHeaders+nOffset): - l = cleanAfterChar(lines[i].lower(),'!') - Tab.append( np.array(l.split()).astype(int)) - else: - raise Exception('Unknown table type') - - ColNames = ColNames[0:nCols] - if Units is not None: - Units = Units[0:nCols] - Units = ['('+u.replace('(','').replace(')','')+')' for u in Units] - if nHeaders==0: - ColNames=None - - except Exception as e: - raise BrokenFormatError('Line {}: {}'.format(iStart+i+1,e.args[0])) - return Tab, ColNames, Units - - -def parseFASTFilTable(lines,n,iStart): - Tab = [] - try: - i=0 - if len(lines)!=n: - raise WrongFormatError('Not enough lines in table: {} lines instead of {}'.format(len(lines),n)) - for i in range(n): - l = lines[i].split() - #print(l[0].strip()) - Tab.append(l[0].strip()) - - except Exception as e: - raise Exception('Line {}: '.format(iStart+i+1)+e.args[0]) - return Tab - - - -# --------------------------------------------------------------------------------} -# --------------------------------------------------------------------------------} -# --------------------------------------------------------------------------------} -# --- Predefined types (may change with OpenFAST version..) -# --------------------------------------------------------------------------------{ -# --------------------------------------------------------------------------------{ -# --------------------------------------------------------------------------------{ - - -# --------------------------------------------------------------------------------} -# --- BeamDyn -# --------------------------------------------------------------------------------{ -class BDFile(FASTInputFileBase): - @classmethod - def from_fast_input_file(cls, parent): - self = cls() - self.setData(filename=parent.filename, data=parent.data, hasNodal=parent.hasNodal, module='BD') - return self - - def __init__(self, filename=None, **kwargs): - FASTInputFileBase.__init__(self, filename, **kwargs) - if filename is None: - # Define a prototype for this file format - self.addComment('--------- BEAMDYN with OpenFAST INPUT FILE -------------------------------------------') - self.addComment('BeamDyn input file, written by BDFile') - self.addComment('---------------------- SIMULATION CONTROL --------------------------------------') - self.addValKey(False , 'Echo' , 'Echo input data to ".ech"? (flag)') - self.addValKey(True , 'QuasiStaticInit' , 'Use quasi-static pre-conditioning with centripetal accelerations in initialization? (flag) [dynamic solve only]') - self.addValKey( 0 , 'rhoinf' , 'Numerical damping parameter for generalized-alpha integrator') - self.addValKey( 2 , 'quadrature' , 'Quadrature method: 1=Gaussian; 2=Trapezoidal (switch)') - self.addValKey("DEFAULT" , 'refine' , 'Refinement factor for trapezoidal quadrature (-) [DEFAULT = 1; used only when quadrature=2]') - self.addValKey("DEFAULT" , 'n_fact' , 'Factorization frequency for the Jacobian in N-R iteration(-) [DEFAULT = 5]') - self.addValKey("DEFAULT" , 'DTBeam' , 'Time step size (s)') - self.addValKey("DEFAULT" , 'load_retries' , 'Number of factored load retries before quitting the simulation [DEFAULT = 20]') - self.addValKey("DEFAULT" , 'NRMax' , 'Max number of iterations in Newton-Raphson algorithm (-) [DEFAULT = 10]') - self.addValKey("DEFAULT" , 'stop_tol' , 'Tolerance for stopping criterion (-) [DEFAULT = 1E-5]') - self.addValKey("DEFAULT" , 'tngt_stf_fd' , 'Use finite differenced tangent stiffness matrix? (flag)') - self.addValKey("DEFAULT" , 'tngt_stf_comp' , 'Compare analytical finite differenced tangent stiffness matrix? (flag)') - self.addValKey("DEFAULT" , 'tngt_stf_pert' , 'Perturbation size for finite differencing (-) [DEFAULT = 1E-6]') - self.addValKey("DEFAULT" , 'tngt_stf_difftol', 'Maximum allowable relative difference between analytical and fd tangent stiffness (-); [DEFAULT = 0.1]') - self.addValKey(True , 'RotStates' , 'Orient states in the rotating frame during linearization? (flag) [used only when linearizing] ') - self.addComment('---------------------- GEOMETRY PARAMETER --------------------------------------') - self.addValKey( 1 , 'member_total' , 'Total number of members (-)') - self.addValKey( 0 , 'kp_total' , 'Total number of key points (-) [must be at least 3]') - self.addValKey( [1, 0] , 'kp_per_member' , 'Member number; Number of key points in this member') - self.addTable('MemberGeom', np.zeros((0,4)), tabType=1, tabDimVar='kp_total', - cols=['kp_xr', 'kp_yr', 'kp_zr', 'initial_twist'], - units=['(m)', '(m)', '(m)', '(deg)']) - self.addComment('---------------------- MESH PARAMETER ------------------------------------------') - self.addValKey( 5 , 'order_elem' , 'Order of interpolation (basis) function (-)') - self.addComment('---------------------- MATERIAL PARAMETER --------------------------------------') - self.addValKey('"undefined"', 'BldFile' , 'Name of file containing properties for blade (quoted string)') - self.addComment('---------------------- PITCH ACTUATOR PARAMETERS -------------------------------') - self.addValKey(False , 'UsePitchAct' , 'Whether a pitch actuator should be used (flag)') - self.addValKey( 1 , 'PitchJ' , 'Pitch actuator inertia (kg-m^2) [used only when UsePitchAct is true]') - self.addValKey( 0 , 'PitchK' , 'Pitch actuator stiffness (kg-m^2/s^2) [used only when UsePitchAct is true]') - self.addValKey( 0 , 'PitchC' , 'Pitch actuator damping (kg-m^2/s) [used only when UsePitchAct is true]') - self.addComment('---------------------- OUTPUTS -------------------------------------------------') - self.addValKey(False , 'SumPrint' , 'Print summary data to ".sum" (flag)') - self.addValKey('"ES10.3E2"' , 'OutFmt' , 'Format used for text tabular output, excluding the time channel.') - self.addValKey( 0 , 'NNodeOuts' , 'Number of nodes to output to file [0 - 9] (-)') - self.addValKey( [1] , 'OutNd' , 'Nodes whose values will be output (-)') - self.addValKey( [''] , 'OutList' , 'The next line(s) contains a list of output parameters. See OutListParameters.xlsx, BeamDyn tab for a listing of available output channels, (-)') - self.addComment('END of OutList (the word "END" must appear in the first 3 columns of this last OutList line)') - self.addComment('---------------------- NODE OUTPUTS --------------------------------------------') - self.addValKey( 99 , 'BldNd_BlOutNd' , 'Blade nodes on each blade (currently unused)') - self.addValKey( [''] , 'OutList_Nodal' , 'The next line(s) contains a list of output parameters. See OutListParameters.xlsx, BeamDyn_Nodes tab for a listing of available output channels, (-)') - self.addComment('END of input file (the word "END" must appear in the first 3 columns of this last OutList line)') - self.addComment('--------------------------------------------------------------------------------') - self.hasNodal=True - #"RootFxr, RootFyr, RootFzr" - #"RootMxr, RootMyr, RootMzr" - #"TipTDxr, TipTDyr, TipTDzr" - #"TipRDxr, TipRDyr, TipRDzr" - - else: - # fix some stuff that generic reader fail at - self.data[1] = {'value':self._lines[1], 'label':'', 'isComment':True, 'descr':'', 'tabType':0} - i = self.getID('kp_total') - listval = [int(v) for v in str(self.data[i+1]['value']).split()] - self.data[i+1]['value']=listval - self.data[i+1]['label']='kp_per_member' - self.data[i+1]['isComment']=False - self.module='BD' - - def _writeSanityChecks(self): - """ Sanity checks before write """ - self['kp_total']=self['MemberGeom'].shape[0] - i = self.getID('kp_total') - self.data[i+1]['value']=[1, self['MemberGeom'].shape[0]] # kp_per_member - self.data[i+1]['label']='kp_per_member' - # Could check length of OutNd - - def _toDataFrame(self): - df = FASTInputFileBase._toDataFrame(self) - # TODO add quadrature points based on trapz/gauss - return df - - @property - def _IComment(self): return [1] - -# --------------------------------------------------------------------------------} -# --- ElastoDyn Blade -# --------------------------------------------------------------------------------{ -class EDBladeFile(FASTInputFileBase): - @classmethod - def from_fast_input_file(cls, parent): - self = cls() - self.setData(filename=parent.filename, data=parent.data, hasNodal=parent.hasNodal, module='EDBlade') - return self - - def __init__(self, filename=None, **kwargs): - FASTInputFileBase.__init__(self, filename, **kwargs) - if filename is None: - # Define a prototype for this file format - self.addComment('------- ELASTODYN V1.00.* INDIVIDUAL BLADE INPUT FILE --------------------------') - self.addComment('ElastoDyn blade definition, written by EDBladeFile.') - self.addComment('---------------------- BLADE PARAMETERS ----------------------------------------') - self.addValKey( 0 , 'NBlInpSt' , 'Number of blade input stations (-)') - self.addValKey( 1. , 'BldFlDmp(1)', 'Blade flap mode #1 structural damping in percent of critical (%)') - self.addValKey( 1. , 'BldFlDmp(2)', 'Blade flap mode #2 structural damping in percent of critical (%)') - self.addValKey( 1. , 'BldEdDmp(1)', 'Blade edge mode #1 structural damping in percent of critical (%)') - self.addComment('---------------------- BLADE ADJUSTMENT FACTORS --------------------------------') - self.addValKey( 1. , 'FlStTunr(1)', 'Blade flapwise modal stiffness tuner, 1st mode (-)') - self.addValKey( 1. , 'FlStTunr(2)', 'Blade flapwise modal stiffness tuner, 2nd mode (-)') - self.addValKey( 1. , 'AdjBlMs' , 'Factor to adjust blade mass density (-)') - self.addValKey( 1. , 'AdjFlSt' , 'Factor to adjust blade flap stiffness (-)') - self.addValKey( 1. , 'AdjEdSt' , 'Factor to adjust blade edge stiffness (-)') - self.addComment('---------------------- DISTRIBUTED BLADE PROPERTIES ----------------------------') - self.addTable('BldProp', np.zeros((0,6)), tabType=1, tabDimVar='NBlInpSt', cols=['BlFract', 'PitchAxis', 'StrcTwst', 'BMassDen', 'FlpStff', 'EdgStff'], units=['(-)', '(-)', '(deg)', '(kg/m)', '(Nm^2)', '(Nm^2)']) - self.addComment('---------------------- BLADE MODE SHAPES ---------------------------------------') - self.addValKey( 1.0 , 'BldFl1Sh(2)', 'Flap mode 1, coeff of x^2') - self.addValKey( 0.0 , 'BldFl1Sh(3)', ' , coeff of x^3') - self.addValKey( 0.0 , 'BldFl1Sh(4)', ' , coeff of x^4') - self.addValKey( 0.0 , 'BldFl1Sh(5)', ' , coeff of x^5') - self.addValKey( 0.0 , 'BldFl1Sh(6)', ' , coeff of x^6') - self.addValKey( 0.0 , 'BldFl2Sh(2)', 'Flap mode 2, coeff of x^2') # NOTE: using something not too bad just incase user uses these as is.. - self.addValKey( 0.0 , 'BldFl2Sh(3)', ' , coeff of x^3') - self.addValKey( -13.0 , 'BldFl2Sh(4)', ' , coeff of x^4') - self.addValKey( 27.0 , 'BldFl2Sh(5)', ' , coeff of x^5') - self.addValKey( -13.0 , 'BldFl2Sh(6)', ' , coeff of x^6') - self.addValKey( 1.0 , 'BldEdgSh(2)', 'Edge mode 1, coeff of x^2') - self.addValKey( 0.0 , 'BldEdgSh(3)', ' , coeff of x^3') - self.addValKey( 0.0 , 'BldEdgSh(4)', ' , coeff of x^4') - self.addValKey( 0.0 , 'BldEdgSh(5)', ' , coeff of x^5') - self.addValKey( 0.0 , 'BldEdgSh(6)', ' , coeff of x^6') - else: - # fix some stuff that generic reader fail at - self.data[1] = {'value':self._lines[1], 'label':'', 'isComment':True, 'descr':'', 'tabType':0} - self.module='EDBlade' - - def _writeSanityChecks(self): - """ Sanity checks before write """ - self['NBlInpSt']=self['BldProp'].shape[0] - # Sum of Coeffs should be 1 - for s in ['BldFl1Sh','BldFl2Sh','BldEdgSh']: - sumcoeff=np.sum([self[s+'('+str(i)+')'] for i in [2,3,4,5,6] ]) - if np.abs(sumcoeff-1)>1e-4: - print('[WARN] Sum of coefficients for polynomial {} not equal to 1 ({}). File: {}'.format(s, sumcoeff, self.filename)) - - def _toDataFrame(self): - df = FASTInputFileBase._toDataFrame(self) - # We add the shape functions for EDBladeFile - x=df['BlFract_[-]'].values - Modes=np.zeros((x.shape[0],3)) - Modes[:,0] = x**2 * self['BldFl1Sh(2)'] + x**3 * self['BldFl1Sh(3)'] + x**4 * self['BldFl1Sh(4)'] + x**5 * self['BldFl1Sh(5)'] + x**6 * self['BldFl1Sh(6)'] - Modes[:,1] = x**2 * self['BldFl2Sh(2)'] + x**3 * self['BldFl2Sh(3)'] + x**4 * self['BldFl2Sh(4)'] + x**5 * self['BldFl2Sh(5)'] + x**6 * self['BldFl2Sh(6)'] - Modes[:,2] = x**2 * self['BldEdgSh(2)'] + x**3 * self['BldEdgSh(3)'] + x**4 * self['BldEdgSh(4)'] + x**5 * self['BldEdgSh(5)'] + x**6 * self['BldEdgSh(6)'] - df[['ShapeFlap1_[-]','ShapeFlap2_[-]','ShapeEdge1_[-]']]=Modes - return df - - @property - def _IComment(self): return [1] - -# --------------------------------------------------------------------------------} -# --- ElastoDyn Tower -# --------------------------------------------------------------------------------{ -class EDTowerFile(FASTInputFileBase): - @classmethod - def from_fast_input_file(cls, parent): - self = cls() - self.setData(filename=parent.filename, data=parent.data, hasNodal=parent.hasNodal, module='EDTower') - return self - - def __init__(self, filename=None, **kwargs): - FASTInputFileBase.__init__(self, filename, **kwargs) - if filename is None: - # Define a prototype for this file format - self.addComment('------- ELASTODYN V1.00.* TOWER INPUT FILE -------------------------------------') - self.addComment('ElastoDyn tower definition, written by EDTowerFile.') - self.addComment('---------------------- TOWER PARAMETERS ----------------------------------------') - self.addValKey( 0 , 'NTwInpSt' , 'Number of blade input stations (-)') - self.addValKey( 1. , 'TwrFADmp(1)' , 'Tower 1st fore-aft mode structural damping ratio (%)') - self.addValKey( 1. , 'TwrFADmp(2)' , 'Tower 2nd fore-aft mode structural damping ratio (%)') - self.addValKey( 1. , 'TwrSSDmp(1)' , 'Tower 1st side-to-side mode structural damping ratio (%)') - self.addValKey( 1. , 'TwrSSDmp(2)' , 'Tower 2nd side-to-side mode structural damping ratio (%)') - self.addComment('---------------------- TOWER ADJUSTMENT FACTORS --------------------------------') - self.addValKey( 1. , 'FAStTunr(1)' , 'Tower fore-aft modal stiffness tuner, 1st mode (-)') - self.addValKey( 1. , 'FAStTunr(2)' , 'Tower fore-aft modal stiffness tuner, 2nd mode (-)') - self.addValKey( 1. , 'SSStTunr(1)' , 'Tower side-to-side stiffness tuner, 1st mode (-)') - self.addValKey( 1. , 'SSStTunr(2)' , 'Tower side-to-side stiffness tuner, 2nd mode (-)') - self.addValKey( 1. , 'AdjTwMa' , 'Factor to adjust tower mass density (-)') - self.addValKey( 1. , 'AdjFASt' , 'Factor to adjust tower fore-aft stiffness (-)') - self.addValKey( 1. , 'AdjSSSt' , 'Factor to adjust tower side-to-side stiffness (-)') - self.addComment('---------------------- DISTRIBUTED TOWER PROPERTIES ----------------------------') - self.addTable('TowProp', np.zeros((0,6)), tabType=1, tabDimVar='NTwInpSt', - cols=['HtFract','TMassDen','TwFAStif','TwSSStif'], - units=['(-)', '(kg/m)', '(Nm^2)', '(Nm^2)']) - self.addComment('---------------------- TOWER FORE-AFT MODE SHAPES ------------------------------') - self.addValKey( 1.0 , 'TwFAM1Sh(2)', 'Mode 1, coefficient of x^2 term') - self.addValKey( 0.0 , 'TwFAM1Sh(3)', ' , coefficient of x^3 term') - self.addValKey( 0.0 , 'TwFAM1Sh(4)', ' , coefficient of x^4 term') - self.addValKey( 0.0 , 'TwFAM1Sh(5)', ' , coefficient of x^5 term') - self.addValKey( 0.0 , 'TwFAM1Sh(6)', ' , coefficient of x^6 term') - self.addValKey( -26. , 'TwFAM2Sh(2)', 'Mode 2, coefficient of x^2 term') # NOTE: using something not too bad just incase user uses these as is.. - self.addValKey( 0.0 , 'TwFAM2Sh(3)', ' , coefficient of x^3 term') - self.addValKey( 27. , 'TwFAM2Sh(4)', ' , coefficient of x^4 term') - self.addValKey( 0.0 , 'TwFAM2Sh(5)', ' , coefficient of x^5 term') - self.addValKey( 0.0 , 'TwFAM2Sh(6)', ' , coefficient of x^6 term') - self.addComment('---------------------- TOWER SIDE-TO-SIDE MODE SHAPES --------------------------') - self.addValKey( 1.0 , 'TwSSM1Sh(2)', 'Mode 1, coefficient of x^2 term') - self.addValKey( 0.0 , 'TwSSM1Sh(3)', ' , coefficient of x^3 term') - self.addValKey( 0.0 , 'TwSSM1Sh(4)', ' , coefficient of x^4 term') - self.addValKey( 0.0 , 'TwSSM1Sh(5)', ' , coefficient of x^5 term') - self.addValKey( 0.0 , 'TwSSM1Sh(6)', ' , coefficient of x^6 term') - self.addValKey( -26. , 'TwSSM2Sh(2)', 'Mode 2, coefficient of x^2 term') # NOTE: using something not too bad just incase user uses these as is.. - self.addValKey( 0.0 , 'TwSSM2Sh(3)', ' , coefficient of x^3 term') - self.addValKey( 27. , 'TwSSM2Sh(4)', ' , coefficient of x^4 term') - self.addValKey( 0.0 , 'TwSSM2Sh(5)', ' , coefficient of x^5 term') - self.addValKey( 0.0 , 'TwSSM2Sh(6)', ' , coefficient of x^6 term') - else: - # fix some stuff that generic reader fail at - self.data[1] = {'value':self._lines[1], 'label':'', 'isComment':True, 'descr':'', 'tabType':0} - self.module='EDTower' - - def _writeSanityChecks(self): - """ Sanity checks before write """ - self['NTwInpSt']=self['TowProp'].shape[0] - # Sum of Coeffs should be 1 - for s in ['TwFAM1Sh','TwFAM2Sh','TwSSM1Sh','TwSSM2Sh']: - sumcoeff=np.sum([self[s+'('+str(i)+')'] for i in [2,3,4,5,6] ]) - if np.abs(sumcoeff-1)>1e-4: - print('[WARN] Sum of coefficients for polynomial {} not equal to 1 ({}). File: {}'.format(s, sumcoeff, self.filename)) - - def _toDataFrame(self): - df = FASTInputFileBase._toDataFrame(self) - # We add the shape functions for EDBladeFile - # NOTE: we provide interpolated shape function just in case the resolution of the input file is low.. - x = df['HtFract_[-]'].values - Modes=np.zeros((x.shape[0],4)) - Modes[:,0] = x**2 * self['TwFAM1Sh(2)'] + x**3 * self['TwFAM1Sh(3)'] + x**4 * self['TwFAM1Sh(4)'] + x**5 * self['TwFAM1Sh(5)'] + x**6 * self['TwFAM1Sh(6)'] - Modes[:,1] = x**2 * self['TwFAM2Sh(2)'] + x**3 * self['TwFAM2Sh(3)'] + x**4 * self['TwFAM2Sh(4)'] + x**5 * self['TwFAM2Sh(5)'] + x**6 * self['TwFAM2Sh(6)'] - Modes[:,2] = x**2 * self['TwSSM1Sh(2)'] + x**3 * self['TwSSM1Sh(3)'] + x**4 * self['TwSSM1Sh(4)'] + x**5 * self['TwSSM1Sh(5)'] + x**6 * self['TwSSM1Sh(6)'] - Modes[:,3] = x**2 * self['TwSSM2Sh(2)'] + x**3 * self['TwSSM2Sh(3)'] + x**4 * self['TwSSM2Sh(4)'] + x**5 * self['TwSSM2Sh(5)'] + x**6 * self['TwSSM2Sh(6)'] - ShapeCols = [c+'_[-]' for c in ['ShapeForeAft1','ShapeForeAft2','ShapeSideSide1','ShapeSideSide2']] - df[ShapeCols]=Modes - return df - - @property - def _IComment(self): return [1] - -# --------------------------------------------------------------------------------} -# --- AeroDyn Blade -# --------------------------------------------------------------------------------{ -class ADBladeFile(FASTInputFileBase): - @classmethod - def from_fast_input_file(cls, parent): - self = cls() - self.setData(filename=parent.filename, data=parent.data, hasNodal=parent.hasNodal, module='ADBlade') - return self - - def __init__(self, filename=None, **kwargs): - FASTInputFileBase.__init__(self, filename, **kwargs) - if filename is None: - # Define a prototype for this file format - self.addComment('------- AERODYN BLADE DEFINITION INPUT FILE ----------------------------------------------') - self.addComment('Aerodynamic blade definition, written by ADBladeFile') - self.addComment('====== Blade Properties =================================================================') - self.addKeyVal('NumBlNds', 0, 'Number of blade nodes used in the analysis (-)') - self.addTable('BldAeroNodes', np.zeros((0,7)), tabType=1, tabDimVar='NumBlNds', cols=['BlSpn', 'BlCrvAC', 'BlSwpAC', 'BlCrvAng', 'BlTwist', 'BlChord', 'BlAFID'], units=['(m)', '(m)', '(m)', '(deg)', '(deg)', '(m)', '(-)']) - self.module='ADBlade' - - def _writeSanityChecks(self): - """ Sanity checks before write""" - self['NumBlNds']=self['BldAeroNodes'].shape[0] - aeroNodes = self['BldAeroNodes'] - # TODO double check this calculation with gradient - dr = np.gradient(aeroNodes[:,0]) - dx = np.gradient(aeroNodes[:,1]) - crvAng = np.degrees(np.arctan2(dx,dr)) - if np.mean(np.abs(crvAng-aeroNodes[:,3]))>0.1: - print('[WARN] BlCrvAng might not be computed correctly') - - def _toDataFrame(self): - df = FASTInputFileBase._toDataFrame(self) - aeroNodes = self['BldAeroNodes'] - r = aeroNodes[:,0] - chord = aeroNodes[:,5] - twist = aeroNodes[:,4]*np.pi/180 - prebendAC = aeroNodes[:,1] - sweepAC = aeroNodes[:,2] - - # --- IEA 15 - ##'le_location: 'Leading-edge positions from a reference blade axis (usually blade pitch axis). Locations are normalized by the local chord length. Positive in -x direction for airfoil-aligned coordinate system') - ## pitch_axis - ##'1D array of the chordwise position of the pitch axis (0-LE, 1-TE), defined along blade span.') - #grid = [0.0, 0.02040816326530612, 0.04081632653061224, 0.061224489795918366, 0.08163265306122448, 0.1020408163265306, 0.12244897959183673, 0.14285714285714285, 0.16326530612244897, 0.18367346938775508, 0.2040816326530612, 0.22448979591836732, 0.24489795918367346, 0.26530612244897955, 0.2857142857142857, 0.3061224489795918, 0.32653061224489793, 0.3469387755102041, 0.36734693877551017, 0.3877551020408163, 0.4081632653061224, 0.42857142857142855, 0.44897959183673464, 0.4693877551020408, 0.4897959183673469, 0.5102040816326531, 0.5306122448979591, 0.5510204081632653, 0.5714285714285714, 0.5918367346938775, 0.6122448979591836, 0.6326530612244897, 0.6530612244897959, 0.673469387755102, 0.6938775510204082, 0.7142857142857142, 0.7346938775510203, 0.7551020408163265, 0.7755102040816326, 0.7959183673469387, 0.8163265306122448, 0.836734693877551, 0.8571428571428571, 0.8775510204081632, 0.8979591836734693, 0.9183673469387754, 0.9387755102040816, 0.9591836734693877, 0.9795918367346939, 1.0] - #values = [0.5045454545454545, 0.4900186808012221, 0.47270018284548393, 0.4540147730610375, 0.434647782591965, 0.4156278851950606, 0.3979378721273935, 0.38129960745617403, 0.3654920515699109, 0.35160780834472827, 0.34008443128769117, 0.3310670675965599, 0.3241031342163746, 0.3188472934612394, 0.3146895762675238, 0.311488897995355, 0.3088429219529899, 0.3066054031112312, 0.3043613335231313, 0.3018756624023877, 0.2992017656131912, 0.29648581499532917, 0.29397119399704474, 0.2918571873240831, 0.2901098902886204, 0.28880659979944606, 0.28802634398115073, 0.28784151044623507, 0.28794253614539367, 0.28852264941156663, 0.28957685074559625, 0.2911108045758606, 0.2930139151081327, 0.2952412111444283, 0.2977841397364215, 0.300565286724993, 0.3035753776130124, 0.30670446458784534, 0.30988253764299156, 0.3130107259708016, 0.31639042766652853, 0.32021109189825026, 0.32462311714967124, 0.329454188784972, 0.33463306413024474, 0.3401190402144396, 0.3460555975714659, 0.3527211856428439, 0.3600890296396286, 0.36818181818181805] - ##'ref_axis_blade' desc='2D array of the coordinates (x,y,z) of the blade reference axis, defined along blade span. The coordinate system is the one of BeamDyn: it is placed at blade root with x pointing the suction side of the blade, y pointing the trailing edge and z along the blade span. A standard configuration will have negative x values (prebend), if swept positive y values, and positive z values.') - #x_grid = [0.0, 0.02040816326530612, 0.04081632653061224, 0.061224489795918366, 0.08163265306122448, 0.1020408163265306, 0.12244897959183673, 0.14285714285714285, 0.16326530612244897, 0.18367346938775508, 0.2040816326530612, 0.22448979591836732, 0.24489795918367346, 0.26530612244897955, 0.2857142857142857, 0.3061224489795918, 0.32653061224489793, 0.3469387755102041, 0.36734693877551017, 0.3877551020408163, 0.4081632653061224, 0.42857142857142855, 0.44897959183673464, 0.4693877551020408, 0.4897959183673469, 0.5102040816326531, 0.5306122448979591, 0.5510204081632653, 0.5714285714285714, 0.5918367346938775, 0.6122448979591836, 0.6326530612244897, 0.6530612244897959, 0.673469387755102, 0.6938775510204082, 0.7142857142857142, 0.7346938775510203, 0.7551020408163265, 0.7755102040816326, 0.7959183673469387, 0.8163265306122448, 0.836734693877551, 0.8571428571428571, 0.8775510204081632, 0.8979591836734693, 0.9183673469387754, 0.9387755102040816, 0.9591836734693877, 0.9795918367346939, 1.0] - #x_values = [0.0, 0.018400065266506227, 0.04225083661157623, 0.0713435070518306, 0.1036164118664373, 0.13698065932882636, 0.16947761902506267, 0.19850810716711273, 0.22314347791028566, 0.24053558565655847, 0.24886598803245524, 0.2502470372487695, 0.24941257744761433, 0.24756615214432298, 0.24481686563607896, 0.24130290560673967, 0.23698965095246982, 0.23242285078249267, 0.22531163517427788, 0.2110134548882222, 0.18623119147117725, 0.1479307251853749, 0.09847131457569316, 0.04111540547132665, -0.02233952894219675, -0.08884150619038655, -0.15891966620096387, -0.2407441175807782, -0.3366430472730907, -0.44693576549987823, -0.5680658106768092, -0.6975208703059096, -0.8321262196998409, -0.9699653368698024, -1.1090930486685822, -1.255144506570033, -1.4103667735456449, -1.5733007007462756, -1.7434963771088456, -1.9194542609028804, -2.1000907378795275, -2.285501961499942, -2.4756894577736315, -2.6734165188032692, -2.8782701025304545, -3.090085737186208, -3.308459127246535, -3.533712868740941, -3.7641269864926348, -4.0] - #y_grid = [0.0, 1.0] - #y_values = [0.0, 0.0] - #z_grid = [0.0, 0.02040816326530612, 0.04081632653061224, 0.061224489795918366, 0.08163265306122448, 0.1020408163265306, 0.12244897959183673, 0.14285714285714285, 0.16326530612244897, 0.18367346938775508, 0.2040816326530612, 0.22448979591836732, 0.24489795918367346, 0.26530612244897955, 0.2857142857142857, 0.3061224489795918, 0.32653061224489793, 0.3469387755102041, 0.36734693877551017, 0.3877551020408163, 0.4081632653061224, 0.42857142857142855, 0.44897959183673464, 0.4693877551020408, 0.4897959183673469, 0.5102040816326531, 0.5306122448979591, 0.5510204081632653, 0.5714285714285714, 0.5918367346938775, 0.6122448979591836, 0.6326530612244897, 0.6530612244897959, 0.673469387755102, 0.6938775510204082, 0.7142857142857142, 0.7346938775510203, 0.7551020408163265, 0.7755102040816326, 0.7959183673469387, 0.8163265306122448, 0.836734693877551, 0.8571428571428571, 0.8775510204081632, 0.8979591836734693, 0.9183673469387754, 0.9387755102040816, 0.9591836734693877, 0.9795918367346939, 1.0] - #z_values = [0.0, 2.387755102040816, 4.775510204081632, 7.163265306122448, 9.551020408163264, 11.938775510204081, 14.326530612244898, 16.714285714285715, 19.10204081632653, 21.489795918367346, 23.877551020408163, 26.265306122448976, 28.653061224489797, 31.04081632653061, 33.42857142857143, 35.81632653061224, 38.20408163265306, 40.59183673469388, 42.979591836734684, 45.36734693877551, 47.75510204081632, 50.14285714285714, 52.53061224489795, 54.91836734693877, 57.30612244897959, 59.69387755102041, 62.08163265306122, 64.46938775510203, 66.85714285714285, 69.24489795918367, 71.63265306122447, 74.0204081632653, 76.40816326530611, 78.79591836734693, 81.18367346938776, 83.57142857142857, 85.95918367346938, 88.3469387755102, 90.73469387755102, 93.12244897959182, 95.51020408163265, 97.89795918367345, 100.28571428571428, 102.6734693877551, 105.0612244897959, 107.44897959183673, 109.83673469387753, 112.22448979591836, 114.61224489795919, 117.0] - #r_ = [0.0, 0.02, 0.15, 0.245170, 1.0] - #ac = [0.5, 0.5, 0.316, 0.25, 0.25] - #r0 = r/r[-1] - #z = np.interp(r0, z_grid, z_values) - #x = np.interp(r0, x_grid, x_values) - #y = np.interp(r0, y_grid, y_values) - #xp = np.interp(r0, grid, values) - #df['z'] = z - #df['x'] = x - #df['y'] = y - #df['xp'] = xp - #ACloc = np.interp(r0, r_,ac) - - ## Get the absolute offset between pitch axis (rotation center) and aerodynamic center - #ch_offset = inputs['chord'] * (inputs['ac'] - inputs['le_location']) - ## Rotate it by the twist using the AD15 coordinate system - #x , y = util.rotate(0., 0., 0., ch_offset, -np.deg2rad(inputs['theta'])) - ## Apply offset to determine the AC axis - #BlCrvAC = inputs['ref_axis_blade'][:,0] + x - #BlSwpAC = inputs['ref_axis_blade'][:,1] + y - - # --- Adding C2 axis - ACloc = r*0 + 0.25 # distance (in chord) from leading edge to aero center - n=int(len(r)*0.15) # 15% span - ACloc[:n]=np.linspace(0.5,0.25, n) # Root is at 0 - - dx = chord*(0.5-ACloc) * np.sin(twist) # Should be mostly >0 - dy = chord*(0.5-ACloc) * np.cos(twist) # Should be mostly >0 - prebend = prebendAC + dx - sweep = sweepAC + dy - df['c2_Crv_Approx_[m]'] = prebend - df['c2_Swp_Approx_[m]'] = sweep - df['AC_Approx_[-]'] = ACloc - # --- Calc CvrAng - dr = np.gradient(aeroNodes[:,0]) - dx = np.gradient(aeroNodes[:,1]) - df['CrvAng_Calc_[-]'] = np.degrees(np.arctan2(dx,dr)) - return df - - @property - def _IComment(self): return [1] - - -# --------------------------------------------------------------------------------} -# --- AeroDyn Polar -# --------------------------------------------------------------------------------{ -class ADPolarFile(FASTInputFileBase): - @staticmethod - def formatName(): - return 'FAST AeroDyn polar file' - - @classmethod - def from_fast_input_file(cls, parent): - self = cls() - self.setData(filename=parent.filename, data=parent.data, hasNodal=parent.hasNodal, module='ADPolar') - return self - - def __init__(self, filename=None, hasUA=True, numTabs=1, **kwargs): - FASTInputFileBase.__init__(self, filename, **kwargs) - if filename is None: - # Define a prototype for this file format - self.addComment('! ------------ AirfoilInfo Input File ------------------------------------------') - self.addComment('! Airfoil definition, written by ADPolarFile') - self.addComment('! ') - self.addComment('! ') - self.addComment('! ------------------------------------------------------------------------------') - self.addValKey("DEFAULT", 'InterpOrd' , 'Interpolation order to use for quasi-steady table lookup {1=linear; 3=cubic spline; "default"} [default=3]') - self.addValKey( 1, 'NonDimArea', 'The non-dimensional area of the airfoil (area/chord^2) (set to 1.0 if unsure or unneeded)') - self.addValKey( 0, 'NumCoords' , 'The number of coordinates in the airfoil shape file. Set to zero if coordinates not included.') - self.addValKey( numTabs , 'NumTabs' , 'Number of airfoil tables in this file. Each table must have lines for Re and Ctrl.') - # TODO multiple tables - for iTab in range(numTabs): - if numTabs==1: - labOffset ='' - else: - labOffset ='_'+str(iTab+1) - self.addComment('! ------------------------------------------------------------------------------') - self.addComment('! data for table {}'.format(iTab+1)) - self.addComment('! ------------------------------------------------------------------------------') - self.addValKey( 1.0 , 'Re' +labOffset , 'Reynolds number in millions') - self.addValKey( 0 , 'Ctrl'+labOffset , 'Control setting') - if hasUA: - self.addValKey(True , 'InclUAdata', 'Is unsteady aerodynamics data included in this table? If TRUE, then include 30 UA coefficients below this line') - self.addComment('!........................................') - self.addValKey( np.nan , 'alpha0' + labOffset, r"0-lift angle of attack, depends on airfoil.") - self.addValKey( np.nan , 'alpha1' + labOffset, r"Angle of attack at f=0.7, (approximately the stall angle) for AOA>alpha0. (deg)") - self.addValKey( np.nan , 'alpha2' + labOffset, r"Angle of attack at f=0.7, (approximately the stall angle) for AOA1]") - self.addValKey( 0 , 'S2' + labOffset, r"Constant in the f curve best-fit for AOA> alpha1; by definition it depends on the airfoil. [ignored if UAMod<>1]") - self.addValKey( 0 , 'S3' + labOffset, r"Constant in the f curve best-fit for alpha2<=AOA< alpha0; by definition it depends on the airfoil. [ignored if UAMod<>1]") - self.addValKey( 0 , 'S4' + labOffset, r"Constant in the f curve best-fit for AOA< alpha2; by definition it depends on the airfoil. [ignored if UAMod<>1]") - self.addValKey( np.nan , 'Cn1' + labOffset, r"Critical value of C0n at leading edge separation. It should be extracted from airfoil data at a given Mach and Reynolds number. It can be calculated from the static value of Cn at either the break in the pitching moment or the loss of chord force at the onset of stall. It is close to the condition of maximum lift of the airfoil at low Mach numbers.") - self.addValKey( np.nan , 'Cn2' + labOffset, r"As Cn1 for negative AOAs.") - self.addValKey( "DEFAULT" , 'St_sh' + labOffset, r"Strouhal's shedding frequency constant. [default = 0.19]") - self.addValKey( np.nan , 'Cd0' + labOffset, r"2D drag coefficient value at 0-lift.") - self.addValKey( np.nan , 'Cm0' + labOffset, r"2D pitching moment coefficient about 1/4-chord location, at 0-lift, positive if nose up. [If the aerodynamics coefficients table does not include a column for Cm, this needs to be set to 0.0]") - self.addValKey( 0 , 'k0' + labOffset, r"Constant in the \hat(x)_cp curve best-fit; = (\hat(x)_AC-0.25). [ignored if UAMod<>1]") - self.addValKey( 0 , 'k1' + labOffset, r"Constant in the \hat(x)_cp curve best-fit. [ignored if UAMod<>1]") - self.addValKey( 0 , 'k2' + labOffset, r"Constant in the \hat(x)_cp curve best-fit. [ignored if UAMod<>1]") - self.addValKey( 0 , 'k3' + labOffset, r"Constant in the \hat(x)_cp curve best-fit. [ignored if UAMod<>1]") - self.addValKey( 0 , 'k1_hat' + labOffset, r"Constant in the expression of Cc due to leading edge vortex effects. [ignored if UAMod<>1]") - self.addValKey( "DEFAULT" , 'x_cp_bar' + labOffset, r"Constant in the expression of \hat(x)_cp^v. [ignored if UAMod<>1, default = 0.2]") - self.addValKey( "DEFAULT" , 'UACutout' + labOffset, r"Angle of attack above which unsteady aerodynamics are disabled (deg). [Specifying the string 'Default' sets UACutout to 45 degrees]") - self.addValKey( "DEFAULT" , 'filtCutOff'+ labOffset, r"Reduced frequency cut-off for low-pass filtering the AoA input to UA, as well as the 1st and 2nd derivatives (-) [default = 0.5]") - self.addComment('!........................................') - else: - self.addValKey(False , 'InclUAdata'+labOffset, 'Is unsteady aerodynamics data included in this table? If TRUE, then include 30 UA coefficients below this line') - self.addComment('! Table of aerodynamics coefficients') - self.addValKey(0 , 'NumAlf'+labOffset, '! Number of data lines in the following table') - self.addTable('AFCoeff'+labOffset, np.zeros((0,4)), tabType=2, tabDimVar='NumAlf', cols=['Alpha', 'Cl', 'Cd', 'Cm'], units=['(deg)', '(-)', '(-)', '(-)']) - self.module='ADPolar' - - def _writeSanityChecks(self): - """ Sanity checks before write""" - nTabs = self['NumTabs'] - if nTabs==1: - self['NumAlf']=self['AFCoeff'].shape[0] - else: - for iTab in range(nTabs): - labOffset='_{}'.format(iTab+1) - self['NumAlf'+labOffset] = self['AFCoeff'+labOffset].shape[0] - # Potentially compute unsteady params here - - def _write(self): - nTabs = self['NumTabs'] - if nTabs==1: - FASTInputFileBase._write(self) - else: - self._writeSanityChecks() - Labs=['Re','Ctrl','UserProp','alpha0','alpha1','alpha2','eta_e','C_nalpha','T_f0','T_V0','T_p','T_VL','b1','b2','b5','A1','A2','A5','S1','S2','S3','S4','Cn1','Cn2','St_sh','Cd0','Cm0','k0','k1','k2','k3','k1_hat','x_cp_bar','UACutout','filtCutOff','InclUAdata','NumAlf','AFCoeff'] - # Store all labels - AllLabels=[self.data[i]['label'] for i in range(len(self.data))] - # Removing lab Offset - TODO TEMPORARY HACK - for iTab in range(nTabs): - labOffset='_{}'.format(iTab+1) - for labRaw in Labs: - i = self.getIDSafe(labRaw+labOffset) - if i>0: - self.data[i]['label'] = labRaw - # Write - with open(self.filename,'w') as f: - f.write(self.toString()) - # Restore labels - for i,labFull in enumerate(AllLabels): - self.data[i]['label'] = labFull - - def _toDataFrame(self): - dfs = FASTInputFileBase._toDataFrame(self) - if not isinstance(dfs, dict): - dfs={'AFCoeff':dfs} - - for k,df in dfs.items(): - sp = k.split('_') - 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 - - # Cn with Cd0 - try: - Cd0 = self['Cd0'+labOffset] - # Cn (with or without Cd0) - Cn1 = Cl*np.cos(alpha)+ (Cd-Cd0)*np.sin(alpha) - df['Cn_Cd0off_[-]'] = Cn1 - except: - pass - - # Regular Cn - Cn = Cl*np.cos(alpha)+ Cd*np.sin(alpha) - df['Cn_[-]'] = Cn - - # Linear Cn - try: - CnLin_ = self['C_nalpha'+labOffset]*(alpha-self['alpha0'+labOffset]*np.pi/180.) - CnLin = CnLin_.copy() - CnLin[alpha<-20*np.pi/180]=np.nan - CnLin[alpha> 30*np.pi/180]=np.nan - df['Cn_pot_[-]'] = CnLin - except: - pass - - # Highlighting points surrounding 0 1 2 Cn points - CnPoints = Cn*np.nan - try: - iBef2 = np.where(alpha0: - if l[0]=='!': - if l.find('!dimension')==0: - self.addKeyVal('nDOF',int(l.split(':')[1])) - nDOFCommon=self['nDOF'] - elif l.find('!time increment')==0: - self.addKeyVal('dt',float(l.split(':')[1])) - elif l.find('!total simulation time')==0: - self.addKeyVal('T',float(l.split(':')[1])) - elif len(l.strip())==0: - pass - else: - raise BrokenFormatError('Unexcepted content found on line {}'.format(i)) - i+=1 - except BrokenFormatError as e: - raise e - except: - raise - - return True - -class ExtPtfmFile(FASTInputFileBase): - @classmethod - def from_fast_input_file(cls, parent): - self = cls() - self.setData(filename=parent.filename, data=parent.data, hasNodal=parent.hasNodal, module='ExtPtfm') - return self - - def __init__(self, filename=None, **kwargs): - FASTInputFileBase.__init__(self, filename, **kwargs) - if filename is None: - # Define a prototype for this file format - self.addValKey(0 , 'nDOF', '') - self.addValKey(1 , 'dt' , '') - self.addValKey(0 , 'T' , '') - self.addTable('MassMatrix' , np.zeros((0,0)), tabType=0) - self.addTable('StiffnessMatrix', np.zeros((0,0)), tabType=0) - self.addTable('DampingMatrix' , np.zeros((0,0)), tabType=0) - self.addTable('Loading' , np.zeros((0,0)), tabType=0) - self.comment='' - self.module='ExtPtfm' - - - def _read(self): - with open(self.filename, 'r', errors="surrogateescape") as f: - lines=f.read().splitlines() - detectAndReadExtPtfmSE(self, lines) - - def toString(self): - s='' - s+='!Comment\n' - s+='!Comment Flex 5 Format\n' - s+='!Dimension: {}\n'.format(self['nDOF']) - s+='!Time increment in simulation: {}\n'.format(self['dt']) - s+='!Total simulation time in file: {}\n'.format(self['T']) - - s+='\n!Mass Matrix\n' - s+='!Dimension: {}\n'.format(self['nDOF']) - s+='\n'.join(''.join('{:16.8e}'.format(x) for x in y) for y in self['MassMatrix']) - - s+='\n\n!Stiffness Matrix\n' - s+='!Dimension: {}\n'.format(self['nDOF']) - s+='\n'.join(''.join('{:16.8e}'.format(x) for x in y) for y in self['StiffnessMatrix']) - - s+='\n\n!Damping Matrix\n' - s+='!Dimension: {}\n'.format(self['nDOF']) - s+='\n'.join(''.join('{:16.8e}'.format(x) for x in y) for y in self['DampingMatrix']) - - s+='\n\n!Loading and Wave Elevation\n' - s+='!Dimension: 1 time column - {} force columns\n'.format(self['nDOF']) - s+='\n'.join(''.join('{:16.8e}'.format(x) for x in y) for y in self['Loading']) - return s - - def _writeSanityChecks(self): - """ Sanity checks before write""" - assert self['MassMatrix'].shape[0] == self['nDOF'] - assert self['StiffnessMatrix'].shape[0] == self['nDOF'] - assert self['DampingMatrix'].shape[0] == self['nDOF'] - assert self['MassMatrix'].shape[0] == self['MassMatrix'].shape[1] - assert self['StiffnessMatrix'].shape[0] == self['StiffnessMatrix'].shape[1] - assert self['DampingMatrix'].shape[0] == self['DampingMatrix'].shape[1] - # if self['T']>0: - # assert self['Loading'].shape[0] == (int(self['T']/self['dT'])+1 - - def _toDataFrame(self): - # Special types, TODO Subclass - nDOF=self['nDOF'] - Cols=['Time_[s]','InpF_Fx_[N]', 'InpF_Fy_[N]', 'InpF_Fz_[N]', 'InpF_Mx_[Nm]', 'InpF_My_[Nm]', 'InpF_Mz_[Nm]'] - Cols+=['CBF_{:03d}_[-]'.format(iDOF+1) for iDOF in np.arange(nDOF)] - Cols=Cols[:nDOF+1] - #dfs['Loading'] = pd.DataFrame(data = self['Loading'],columns = Cols) - dfs = pd.DataFrame(data = self['Loading'],columns = Cols) - - #Cols=['SurgeAcc_[m/s]', 'SwayAcc_[m/s]', 'HeaveAcc_[m/s]', 'RollAcc_[rad/s]', 'PitchAcc_[rad/s]', 'YawAcc_[rad/s]'] - #Cols+=['CBQD_{:03d}_[-]'.format(iDOF+1) for iDOF in np.arange(nDOF)] - #Cols=Cols[:nDOF] - #dfs['MassMatrix'] = pd.DataFrame(data = self['MassMatrix'], columns=Cols) - - #Cols=['SurgeVel_[m/s]', 'SwayVel_[m/s]', 'HeaveVel_[m/s]', 'RollVel_[rad/s]', 'PitchVel_[rad/s]', 'YawVel_[rad/s]'] - #Cols+=['CBQD_{:03d}_[-]'.format(iDOF+1) for iDOF in np.arange(nDOF)] - #Cols=Cols[:nDOF] - #dfs['DampingMatrix'] = pd.DataFrame(data = self['DampingMatrix'], columns=Cols) - - #Cols=['Surge_[m]', 'Sway_[m]', 'Heave_[m]', 'Roll_[rad]', 'Pitch_[rad]', 'Yaw_[rad]'] - #Cols+=['CBQ_{:03d}_[-]'.format(iDOF+1) for iDOF in np.arange(nDOF)] - #Cols=Cols[:nDOF] - #dfs['StiffnessMatrix'] = pd.DataFrame(data = self['StiffnessMatrix'], columns=Cols) - return dfs - - - -if __name__ == "__main__": - f = FASTInputFile('tests/example_files/FASTIn_HD_SeaState.dat') - print(f) - pass - #B=FASTIn('Turbine.outb') - - - +import numpy as np +import os +import pandas as pd +import re +try: + from .file import File, WrongFormatError, BrokenFormatError +except: + File = dict + class WrongFormatError(Exception): pass + class BrokenFormatError(Exception): pass + +__all__ = ['FASTInputFile'] + +TABTYPE_NOT_A_TAB = 0 +TABTYPE_NUM_WITH_HEADER = 1 +TABTYPE_NUM_WITH_HEADERCOM = 2 +TABTYPE_NUM_NO_HEADER = 4 +TABTYPE_NUM_BEAMDYN = 5 +TABTYPE_NUM_SUBDYNOUT = 7 +TABTYPE_MIX_WITH_HEADER = 6 +TABTYPE_FIL = 3 +TABTYPE_FMT = 9999 # TODO + + + +class FASTInputFile(File): + """ + Read/write an OpenFAST input file. The object behaves like a dictionary. + A generic reader/writer is used at first. + If a dedicated OpenFAST input file is detected, additional functionalities are added. + See at the end of this file for dedicated class that can be used instead of this generic reader. + + Main methods + ------------ + - read, write, toDataFrame, keys, toGraph + + + Return an object which inherits from FASTInputFileBase + - The generic file reader is run first + - If a specific file format/module is detected, a fixed file format object is returned + The fixed file format have additional outputs, sanity checks and methods + """ + + @staticmethod + def defaultExtensions(): + return ['.dat','.fst','.txt','.fstf','.dvr'] + + @staticmethod + def formatName(): + return 'FAST input file' + + def __init__(self, filename=None, **kwargs): + self._fixedfile = None + self.basefile = FASTInputFileBase(filename, **kwargs) # Generic fileformat + + @property + def fixedfile(self): + if self._fixedfile is not None: + return self._fixedfile + elif len(self.basefile.data)>0: + self._fixedfile=self.fixedFormat() + return self._fixedfile + else: + return self.basefile + + @property + def module(self): + if self._fixedfile is None: + return self.basefile.module + else: + return self._fixedfile.module + + @property + def hasNodal(self): + if self._fixedfile is None: + return self.basefile.hasNodal + else: + return self._fixedfile.hasNodal + + def getID(self, label): + return self.basefile.getID(label) + + @property + def data(self): + return self.basefile.data + + def fixedFormat(self): + # --- Creating a dedicated Child + KEYS = list(self.basefile.keys()) + if 'NumBlNds' in KEYS: + return ADBladeFile.from_fast_input_file(self.basefile) + elif 'rhoinf' in KEYS: + return BDFile.from_fast_input_file(self.basefile) + elif 'NBlInpSt' in KEYS: + return EDBladeFile.from_fast_input_file(self.basefile) + elif 'NTwInpSt' in KEYS: + return EDTowerFile.from_fast_input_file(self.basefile) + elif 'MassMatrix' in KEYS and self.module == 'ExtPtfm': + return ExtPtfmFile.from_fast_input_file(self.basefile) + elif 'NumCoords' in KEYS and 'InterpOrd' in KEYS: + return ADPolarFile.from_fast_input_file(self.basefile) + else: + # TODO: HD, SD, SvD, ED, AD, EDbld, BD, + #print('>>>>>>>>>>>> NO FILEFORMAT', KEYS) + return self.basefile + + def read(self, filename=None): + return self.fixedfile.read(filename) + + def write(self, filename=None): + return self.fixedfile.write(filename) + + def toDataFrame(self): + return self.fixedfile.toDataFrame() + + def toString(self): + return self.fixedfile.toString() + + def keys(self): + return self.fixedfile.keys() + + def toGraph(self, **kwargs): + return self.fixedfile.toGraph(**kwargs) + + @property + def filename(self): + return self.fixedfile.filename + + @property + def comment(self): + return self.fixedfile.comment + + @comment.setter + def comment(self,comment): + self.fixedfile.comment = comment + + def __iter__(self): + return self.fixedfile.__iter__() + + def __next__(self): + return self.fixedfile.__next__() + + def __setitem__(self,key,item): + return self.fixedfile.__setitem__(key,item) + + def __getitem__(self,key): + return self.fixedfile.__getitem__(key) + + def __repr__(self): + return self.fixedfile.__repr__() + #s ='Fast input file: {}\n'.format(self.filename) + #return s+'\n'.join(['{:15s}: {}'.format(d['label'],d['value']) for i,d in enumerate(self.data)]) + + +# --------------------------------------------------------------------------------} +# --- BASE INPUT FILE +# --------------------------------------------------------------------------------{ +class FASTInputFileBase(File): + """ + Read/write an OpenFAST input file. The object behaves like a dictionary. + + Main methods + ------------ + - read, write, toDataFrame, keys + + Main keys + --------- + The keys correspond to the keys used in the file. For instance for a .fst file: 'DT','TMax' + + Examples + -------- + + filename = 'AeroDyn.dat' + f = FASTInputFile(filename) + f['TwrAero'] = True + f['AirDens'] = 1.225 + f.write('AeroDyn_Changed.dat') + + """ + @staticmethod + def defaultExtensions(): + return ['.dat','.fst','.txt','.fstf','.dvr'] + + @staticmethod + def formatName(): + return 'FAST input file Base' + + def __init__(self, filename=None, **kwargs): + self._size=None + self.setData() # Init data + if filename: + self.filename = filename + self.read() + + def setData(self, filename=None, data=None, hasNodal=False, module=None): + """ Set the data of this object. This object shouldn't store anything else. """ + if data is None: + self.data = [] + else: + self.data = data + self.hasNodal = hasNodal + self.module = module + self.filename = filename + + def keys(self): + self.labels = [ d['label'] for i,d in enumerate(self.data) if (not d['isComment']) and (i not in self._IComment)] + return self.labels + + def getID(self,label): + i=self.getIDSafe(label) + if i<0: + raise KeyError('Variable `'+ label+'` not found in FAST file:'+self.filename) + else: + return i + def getIDs(self,label): + I=[] + # brute force search + for i in range(len(self.data)): + d = self.data[i] + if d['label'].lower()==label.lower(): + I.append(i) + if len(I)<0: + raise KeyError('Variable `'+ label+'` not found in FAST file:'+self.filename) + else: + return I + + def getIDSafe(self,label): + # brute force search + for i in range(len(self.data)): + d = self.data[i] + if d['label'].lower()==label.lower(): + return i + return -1 + + # Making object an iterator + def __iter__(self): + self.iCurrent=-1 + self.iMax=len(self.data)-1 + return self + + def __next__(self): # Python 2: def next(self) + if self.iCurrent > self.iMax: + raise StopIteration + else: + self.iCurrent += 1 + return self.data[self.iCurrent] + + # Making it behave like a dictionary + def __setitem__(self, key, item): + I = self.getIDs(key) + for i in I: + if self.data[i]['tabType'] != TABTYPE_NOT_A_TAB: + # For tables, we automatically update variable that stores the dimension + nRows = len(item) + if 'tabDimVar' in self.data[i].keys(): + dimVar = self.data[i]['tabDimVar'] + iDimVar = self.getID(dimVar) + self.data[iDimVar]['value'] = nRows # Avoiding a recursive call to __setitem__ here + else: + pass + self.data[i]['value'] = item + + def __getitem__(self,key): + i = self.getID(key) + return self.data[i]['value'] + + def __repr__(self): + s ='Fast input file base: {}\n'.format(self.filename) + return s+'\n'.join(['{:15s}: {}'.format(d['label'],d['value']) for i,d in enumerate(self.data)]) + + def addKeyVal(self, key, val, descr=None): + i=self.getIDSafe(key) + if i<0: + d = getDict() + else: + d = self.data[i] + d['label']=key + d['value']=val + if descr is not None: + d['descr']=descr + if i<0: + self.data.append(d) + + def addValKey(self,val,key,descr=None): + self.addKeyVal(key, val, descr) + + def addComment(self, comment='!'): + d=getDict() + d['isComment'] = True + d['value'] = comment + self.data.append(d) + + def addTable(self, label, tab, cols=None, units=None, tabType=1, tabDimVar=None): + d=getDict() + d['label'] = label + d['value'] = tab + d['tabType'] = tabType + d['tabDimVar'] = tabDimVar + d['tabColumnNames'] = cols + d['tabUnits'] = units + self.data.append(d) + + @property + def comment(self): + return '\n'.join([self.data[i]['value'] for i in self._IComment]) + + @comment.setter + def comment(self, comment): + splits = comment.split('\n') + for i,com in zip(self._IComment, splits): + self.data[i]['value'] = com + self.data[i]['label'] = '' + self.data[i]['descr'] = '' + self.data[i]['isComment'] = True + + @property + def _IComment(self): + """ return indices of comment line""" + return [1] # Typical OpenFAST files have comment on second line [1] + + + def read(self, filename=None): + if filename: + self.filename = filename + if self.filename: + if not os.path.isfile(self.filename): + raise OSError(2,'File not found:',self.filename) + if os.stat(self.filename).st_size == 0: + raise EmptyFileError('File is empty:',self.filename) + self._read() + else: + raise Exception('No filename provided') + + def _read(self): + + # --- Tables that can be detected based on the "Value" (first entry on line) + # TODO members for BeamDyn with mutliple key point ####### TODO PropSetID is Duplicate SubDyn and used in HydroDyn + NUMTAB_FROM_VAL_DETECT = ['HtFract' , 'TwrElev' , 'BlFract' , 'Genspd_TLU' , 'BlSpn' , 'HvCoefID' , 'AxCoefID' , 'JointID' , 'Dpth' , 'FillNumM' , 'MGDpth' , 'SimplCd' , 'RNodes' , 'kp_xr' , 'mu1' , 'TwrHtFr' , 'TwrRe' , 'WT_X'] + NUMTAB_FROM_VAL_DIM_VAR = ['NTwInpSt' , 'NumTwrNds' , 'NBlInpSt' , 'DLL_NumTrq' , 'NumBlNds' , 'NHvCoef' , 'NAxCoef' , 'NJoints' , 'NCoefDpth' , 'NFillGroups' , 'NMGDepths' , 1 , 'BldNodes' , 'kp_total' , 1 , 'NTwrHt' , 'NTwrRe' , 'NumTurbines'] + NUMTAB_FROM_VAL_VARNAME = ['TowProp' , 'TowProp' , 'BldProp' , 'DLLProp' , 'BldAeroNodes' , 'HvCoefs' , 'AxCoefs' , 'Joints' , 'DpthProp' , 'FillGroups' , 'MGProp' , 'SmplProp' , 'BldAeroNodes' , 'MemberGeom' , 'DampingCoeffs' , 'TowerProp' , 'TowerRe', 'WindTurbines'] + NUMTAB_FROM_VAL_NHEADER = [2 , 2 , 2 , 2 , 2 , 2 , 2 , 2 , 2 , 2 , 2 , 2 , 1 , 2 , 2 , 1 , 1 , 2 ] + NUMTAB_FROM_VAL_TYPE = ['num' , 'num' , 'num' , 'num' , 'num' , 'num' , 'num' , 'num' , 'num' , 'num' , 'num' , 'num' , 'mix' , 'num' , 'num' , 'num' , 'num' , 'mix'] + # SubDyn + NUMTAB_FROM_VAL_DETECT += [ 'RJointID' , 'IJointID' , 'COSMID' , 'CMJointID' ] + NUMTAB_FROM_VAL_DIM_VAR += [ 'NReact' , 'NInterf' , 'NCOSMs' , 'NCmass' ] + NUMTAB_FROM_VAL_VARNAME += [ 'BaseJoints' , 'InterfaceJoints' , 'MemberCosineMatrix' , 'ConcentratedMasses'] + NUMTAB_FROM_VAL_NHEADER += [ 2 , 2 , 2 , 2 ] + NUMTAB_FROM_VAL_TYPE += [ 'mix' , 'num' , 'num' , 'num' ] + # AD Driver old and new + NUMTAB_FROM_VAL_DETECT += [ 'WndSpeed' , 'HWndSpeed' ] + NUMTAB_FROM_VAL_DIM_VAR += [ 'NumCases' , 'NumCases' ] + NUMTAB_FROM_VAL_VARNAME += [ 'Cases' , 'Cases' ] + NUMTAB_FROM_VAL_NHEADER += [ 2 , 2 ] + NUMTAB_FROM_VAL_TYPE += [ 'num' , 'num' ] + + # --- Tables that can be detected based on the "Label" (second entry on line) + # NOTE: MJointID1, used by SubDyn and HydroDyn + NUMTAB_FROM_LAB_DETECT = ['NumAlf' , 'F_X' , 'MemberCd1' , 'MJointID1' , 'NOutLoc' , 'NOutCnt' , 'PropD' ] + NUMTAB_FROM_LAB_DIM_VAR = ['NumAlf' , 'NKInpSt' , 'NCoefMembers' , 'NMembers' , 'NMOutputs' , 'NMOutputs' , 'NPropSets' ] + NUMTAB_FROM_LAB_VARNAME = ['AFCoeff' , 'TMDspProp' , 'MemberProp' , 'Members' , 'MemberOuts' , 'MemberOuts' , 'SectionProp' ] + NUMTAB_FROM_LAB_NHEADER = [2 , 2 , 2 , 2 , 2 , 2 , 2 ] + NUMTAB_FROM_LAB_NOFFSET = [0 , 0 , 0 , 0 , 0 , 0 , 0 ] + NUMTAB_FROM_LAB_TYPE = ['num' , 'num' , 'num' , 'mix' , 'num' , 'sdout' , 'num' ] + # MoorDyn Version 1 and 2 (with AUTO for LAB_DIM_VAR) + NUMTAB_FROM_LAB_DETECT += ['Diam' ,'Type' ,'LineType' , 'Attachment'] + NUMTAB_FROM_LAB_DIM_VAR += ['NTypes:AUTO','NConnects' ,'NLines:AUTO' , 'AUTO'] + NUMTAB_FROM_LAB_VARNAME += ['LineTypes' ,'ConnectionProp' ,'LineProp' , 'Points'] + NUMTAB_FROM_LAB_NHEADER += [ 2 , 2 , 2 , 2 ] + NUMTAB_FROM_LAB_NOFFSET += [ 0 , 0 , 0 , 0 ] + NUMTAB_FROM_LAB_TYPE += ['mix' ,'mix' ,'mix' , 'mix'] + # SubDyn + NUMTAB_FROM_LAB_DETECT += ['GuyanDampSize' , 'YoungE' , 'YoungE' , 'EA' , 'MatDens' ] + NUMTAB_FROM_LAB_DIM_VAR += [6 , 'NPropSets', 'NXPropSets', 'NCablePropSets' , 'NRigidPropSets'] + NUMTAB_FROM_LAB_VARNAME += ['GuyanDampMatrix' , 'BeamProp' , 'BeamPropX' , 'CableProp' , 'RigidProp' ] + NUMTAB_FROM_LAB_NHEADER += [0 , 2 , 2 , 2 , 2 ] + NUMTAB_FROM_LAB_NOFFSET += [1 , 0 , 0 , 0 , 0 ] + NUMTAB_FROM_LAB_TYPE += ['num' , 'num' , 'num' , 'num' , 'num' ] + # OLAF + NUMTAB_FROM_LAB_DETECT += ['GridName' ] + NUMTAB_FROM_LAB_DIM_VAR += ['nGridOut' ] + NUMTAB_FROM_LAB_VARNAME += ['GridOutputs'] + NUMTAB_FROM_LAB_NHEADER += [0 ] + NUMTAB_FROM_LAB_NOFFSET += [2 ] + NUMTAB_FROM_LAB_TYPE += ['mix' ] + + FILTAB_FROM_LAB_DETECT = ['FoilNm' ,'AFNames'] + FILTAB_FROM_LAB_DIM_VAR = ['NumFoil','NumAFfiles'] + FILTAB_FROM_LAB_VARNAME = ['FoilNm' ,'AFNames'] + + # Using lower case to be more tolerant.. + NUMTAB_FROM_VAL_DETECT_L = [s.lower() for s in NUMTAB_FROM_VAL_DETECT] + NUMTAB_FROM_LAB_DETECT_L = [s.lower() for s in NUMTAB_FROM_LAB_DETECT] + FILTAB_FROM_LAB_DETECT_L = [s.lower() for s in FILTAB_FROM_LAB_DETECT] + + # Reset data + self.data = [] + self.hasNodal=False + self.module = None + #with open(self.filename, 'r', errors="surrogateescape") as f: + with open(self.filename, 'r', errors="surrogateescape") as f: + lines=f.read().splitlines() + # IF NEEDED> DO THE FOLLOWING FORMATTING: + #lines = [str(l).encode('utf-8').decode('ascii','ignore') for l in f.read().splitlines()] + + # Fast files start with ! or - + #if lines[0][0]!='!' and lines[0][0]!='-': + # raise Exception('Fast file do not start with ! or -, is it the right format?') + + # Special filetypes + if detectAndReadExtPtfmSE(self, lines): + return + if self.detectAndReadAirfoilAD14(lines): + return + + # Parsing line by line, storing each line into a dictionary + i=0 + nComments = 0 + nWrongLabels = 0 + allowSpaceSeparatedList=False + iTab = 0 + + labOffset='' + while i0 \ + or line.upper().find('MESH-BASED OUTPUTS')>0 \ + or line.upper().find('OUTPUT CHANNELS' )>0: # "OutList - The next line(s) contains a list of output parameters. See OutListParameters.xlsx for a listing of available output channels, (-)'" + # TODO, lazy implementation so far, MAKE SUB FUNCTION + parts = re.match(r'^\W*\w+', line) + if parts: + firstword = parts.group(0).strip() + else: + raise NotImplementedError + remainer = re.sub(r'^\W*\w+\W*', '', line) + # Parsing outlist, and then we continue at a new "i" (to read END etc.) + OutList,i = parseFASTOutList(lines,i+1) + d = getDict() + if self.hasNodal and not firstword.endswith('_Nodal'): + d['label'] = firstword+'_Nodal' + else: + d['label'] = firstword + d['descr'] = remainer + d['tabType'] = TABTYPE_FIL # TODO + d['value'] = ['']+OutList + self.data.append(d) + if i>=len(lines): + break + + # --- Here we cheat and force an exit of the input file + # The reason for this is that some files have a lot of things after the END, which will result in the file being intepreted as a wrong format due to too many comments + if i+20 or lines[i+2].lower().find('bldnd_bloutnd')>0): + self.hasNodal=True + else: + self.data.append(parseFASTInputLine('END of input file (the word "END" must appear in the first 3 columns of this last OutList line)',i+1)) + self.data.append(parseFASTInputLine('---------------------------------------------------------------------------------------',i+2)) + break + elif line.upper().find('SSOUTLIST' )>0 or line.upper().find('SDOUTLIST' )>0: + # SUBDYN Outlist doesn not follow regular format + self.data.append(parseFASTInputLine(line,i)) + # OUTLIST Exception for BeamDyn + OutList,i = parseFASTOutList(lines,i+1) + # TODO + for o in OutList: + d = getDict() + d['isComment'] = True + d['value']=o + self.data.append(d) + # --- Here we cheat and force an exit of the input file + self.data.append(parseFASTInputLine('END of input file (the word "END" must appear in the first 3 columns of this last OutList line)',i+1)) + self.data.append(parseFASTInputLine('---------------------------------------------------------------------------------------',i+2)) + break + elif line.upper().find('ADDITIONAL STIFFNESS')>0: + # TODO, lazy implementation so far, MAKE SUB FUNCTION + self.data.append(parseFASTInputLine(line,i)) + i +=1 + KDAdd = [] + for _ in range(19): + KDAdd.append(lines[i]) + i +=1 + d = getDict() + d['label'] = 'KDAdd' # TODO + d['tabType'] = TABTYPE_FIL # TODO + d['value'] = KDAdd + self.data.append(d) + if i>=len(lines): + break + elif line.upper().find('DISTRIBUTED PROPERTIES')>0: + self.data.append(parseFASTInputLine(line,i)); + i+=1; + self.readBeamDynProps(lines,i) + return + elif line.upper().find('OUTPUTS')>0: + if 'Points' in self.keys() and 'dtM' in self.keys(): + OutList,i = parseFASTOutList(lines,i+1) + d = getDict() + d['label'] = 'Outlist' + d['descr'] = '' + d['tabType'] = TABTYPE_FIL # TODO + d['value'] = OutList + self.addComment('------------------------ OUTPUTS --------------------------------------------') + self.data.append(d) + self.addComment('END') + self.addComment('------------------------- need this line --------------------------------------') + return + + # --- Parsing of standard lines: value(s) key comment + line = lines[i] + d = parseFASTInputLine(line,i,allowSpaceSeparatedList) + labelRaw =d['label'].lower() + d['label']+=labOffset + + # --- Handling of special files + if labelRaw=='kp_total': + # BeamDyn has weird space speparated list around keypoint definition + allowSpaceSeparatedList=True + elif labelRaw=='numcoords': + # TODO, lazy implementation so far, MAKE SUB FUNCTION + if isStr(d['value']): + if d['value'][0]=='@': + # it's a ref to the airfoil coord file + pass + else: + if not strIsInt(d['value']): + raise WrongFormatError('Wrong value of NumCoords') + if int(d['value'])<=0: + pass + else: + self.data.append(d); i+=1; + # 3 comment lines + self.data.append(parseFASTInputLine(lines[i],i)); i+=1; + self.data.append(parseFASTInputLine(lines[i],i)); i+=1; + self.data.append(parseFASTInputLine(lines[i],i)); i+=1; + splits=cleanAfterChar(cleanLine(lines[i]),'!').split() + # Airfoil ref point + try: + pos=[float(splits[0]), float(splits[1])] + except: + raise WrongFormatError('Wrong format while reading coordinates of airfoil reference') + i+=1 + d = getDict() + d['label'] = 'AirfoilRefPoint' + d['value'] = pos + self.data.append(d) + # 2 comment lines + self.data.append(parseFASTInputLine(lines[i],i)); i+=1; + self.data.append(parseFASTInputLine(lines[i],i)); i+=1; + # Table of coordinats itself + d = getDict() + d['label'] = 'AirfoilCoord' + d['tabDimVar'] = 'NumCoords' + d['tabType'] = TABTYPE_NUM_WITH_HEADERCOM + nTabLines = self[d['tabDimVar']]-1 # SOMEHOW ONE DATA POINT LESS + d['value'], d['tabColumnNames'],_ = parseFASTNumTable(self.filename,lines[i:i+nTabLines+1],nTabLines,i,1) + d['tabUnits'] = ['(-)','(-)'] + self.data.append(d) + break + + elif labelRaw=='re': + try: + nAirfoilTab = self['NumTabs'] + iTab +=1 + if nAirfoilTab>1: + labOffset ='_'+str(iTab) + d['label']=labelRaw+labOffset + except: + # Unsteady driver input file... + pass + + + #print('label>',d['label'],'<',type(d['label'])); + #print('value>',d['value'],'<',type(d['value'])); + #print(isStr(d['value'])) + #if isStr(d['value']): + # print(d['value'].lower() in NUMTAB_FROM_VAL_DETECT_L) + + + # --- Handling of tables + if isStr(d['value']) and d['value'].lower() in NUMTAB_FROM_VAL_DETECT_L: + # Table with numerical values, + ii = NUMTAB_FROM_VAL_DETECT_L.index(d['value'].lower()) + tab_type = NUMTAB_FROM_VAL_TYPE[ii] + if tab_type=='num': + d['tabType'] = TABTYPE_NUM_WITH_HEADER + else: + d['tabType'] = TABTYPE_MIX_WITH_HEADER + d['label'] = NUMTAB_FROM_VAL_VARNAME[ii]+labOffset + d['tabDimVar'] = NUMTAB_FROM_VAL_DIM_VAR[ii] + nHeaders = NUMTAB_FROM_VAL_NHEADER[ii] + nTabLines=0 + if isinstance(d['tabDimVar'],int): + nTabLines = d['tabDimVar'] + 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']) + i += nTabLines+nHeaders-1 + + # --- Temporary hack for e.g. SubDyn, that has duplicate table, impossible to detect in the current way... + # So we remove the element form the list one read + del NUMTAB_FROM_VAL_DETECT[ii] + del NUMTAB_FROM_VAL_DIM_VAR[ii] + del NUMTAB_FROM_VAL_VARNAME[ii] + del NUMTAB_FROM_VAL_NHEADER[ii] + del NUMTAB_FROM_VAL_TYPE [ii] + del NUMTAB_FROM_VAL_DETECT_L[ii] + + elif isStr(labelRaw) and labelRaw in NUMTAB_FROM_LAB_DETECT_L: + ii = NUMTAB_FROM_LAB_DETECT_L.index(labelRaw) + tab_type = NUMTAB_FROM_LAB_TYPE[ii] + # Special case for airfoil data, the table follows NumAlf, so we add d first + doDelete =True + if labelRaw=='numalf': + doDelete =False + d['tabType']=TABTYPE_NOT_A_TAB + self.data.append(d) + # Creating a new dictionary for the table + d = {'value':None, 'label':'NumAlf'+labOffset, 'isComment':False, 'descr':'', 'tabType':None} + i += 1 + nHeaders = NUMTAB_FROM_LAB_NHEADER[ii] + nOffset = NUMTAB_FROM_LAB_NOFFSET[ii] + if nOffset>0: + # Creating a dictionary for that entry + dd = {'value':d['value'], 'label':d['label']+labOffset, 'isComment':False, 'descr':d['descr'], 'tabType':TABTYPE_NOT_A_TAB} + self.data.append(dd) + + d['label'] = NUMTAB_FROM_LAB_VARNAME[ii] + if d['label'].lower()=='afcoeff' : + d['tabType'] = TABTYPE_NUM_WITH_HEADERCOM + else: + if tab_type=='num': + d['tabType'] = TABTYPE_NUM_WITH_HEADER + elif tab_type=='sdout': + d['tabType'] = TABTYPE_NUM_SUBDYNOUT + else: + d['tabType'] = TABTYPE_MIX_WITH_HEADER + # Finding table dimension (number of lines) + tabDimVar = NUMTAB_FROM_LAB_DIM_VAR[ii] + if isinstance(tabDimVar, int): # dimension hardcoded + d['tabDimVar'] = tabDimVar + nTabLines = d['tabDimVar'] + else: + # We either use a variable name or "AUTO" to find the number of rows + tabDimVars = tabDimVar.split(':') + for tabDimVar in tabDimVars: + d['tabDimVar'] = tabDimVar + if tabDimVar=='AUTO': + # Determine table dimension automatically + nTabLines = findNumberOfTableLines(lines[i+nHeaders:], break_chars=['---','!','#']) + break + else: + try: + nTabLines = self[tabDimVar+labOffset] + break + except KeyError: + #print('Cannot determine table dimension using {}'.format(tabDimVar)) + # Hopefully this table has AUTO as well + pass + + d['label'] += labOffset + #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+nOffset],nTabLines,i, nHeaders, tableType=tab_type, nOffset=nOffset, varNumLines=d['tabDimVar']) + i += nTabLines+1-nOffset + + # --- Temporary hack for e.g. SubDyn, that has duplicate table, impossible to detect in the current way... + # So we remove the element form the list one read + if doDelete: + del NUMTAB_FROM_LAB_DETECT[ii] + del NUMTAB_FROM_LAB_DIM_VAR[ii] + del NUMTAB_FROM_LAB_VARNAME[ii] + del NUMTAB_FROM_LAB_NHEADER[ii] + del NUMTAB_FROM_LAB_NOFFSET[ii] + del NUMTAB_FROM_LAB_TYPE [ii] + del NUMTAB_FROM_LAB_DETECT_L[ii] + + elif isStr(d['label']) and d['label'].lower() in FILTAB_FROM_LAB_DETECT_L: + ii = FILTAB_FROM_LAB_DETECT_L.index(d['label'].lower()) + d['label'] = FILTAB_FROM_LAB_VARNAME[ii]+labOffset + d['tabDimVar'] = FILTAB_FROM_LAB_DIM_VAR[ii] + d['tabType'] = TABTYPE_FIL + nTabLines = self[d['tabDimVar']] + #print('Reading table {} Dimension {} (based on {})'.format(d['label'],nTabLines,d['tabDimVar'])); + d['value'] = parseFASTFilTable(lines[i:i+nTabLines],nTabLines,i) + i += nTabLines-1 + + + + self.data.append(d) + i += 1 + # --- Safety checks + if d['isComment']: + #print(line) + nComments +=1 + else: + if hasSpecialChars(d['label']): + nWrongLabels +=1 + #print('label>',d['label'],'<',type(d['label']),line); + if i>3: # first few lines may be comments, we allow it + #print('Line',i,'Label:',d['label']) + raise WrongFormatError('Special Character found in Label: `{}`, for line: `{}`'.format(d['label'],line)) + if len(d['label'])==0: + nWrongLabels +=1 + if nComments>len(lines)*0.35: + #print('Comment fail',nComments,len(lines),self.filename) + raise WrongFormatError('Most lines were read as comments, probably not a FAST Input File: {}'.format(self.filename)) + if nWrongLabels>len(lines)*0.10: + #print('Label fail',nWrongLabels,len(lines),self.filename) + raise WrongFormatError('Too many lines with wrong labels, probably not a FAST Input File {}:'.format(self.filename)) + + # --- END OF FOR LOOP ON LINES + + # --- PostReading checks + labels = self.keys() + duplicates = set([x for x in labels if (labels.count(x) > 1) and x!='OutList' and x.strip()!='-']) + if len(duplicates)>0: + print('[WARN] Duplicate labels found in file: '+self.filename) + print(' Duplicates: '+', '.join(duplicates)) + print(' It\'s strongly recommended to make them unique! ') +# except WrongFormatError as e: +# raise WrongFormatError('Fast File {}: '.format(self.filename)+'\n'+e.args[0]) +# except Exception as e: +# raise e +# # print(e) +# raise Exception('Fast File {}: '.format(self.filename)+'\n'+e.args[0]) + self._lines = lines + + + def toString(self): + s='' + # Special file formats, TODO subclass + def toStringVLD(val,lab,descr): + val='{}'.format(val) + lab='{}'.format(lab) + if len(val)<13: + val='{:13s}'.format(val) + if len(lab)<13: + lab='{:13s}'.format(lab) + return val+' '+lab+' - '+descr.strip().lstrip('-').lstrip() + + def toStringIntFloatStr(x): + try: + if int(x)==x: + s='{:15.0f}'.format(x) + else: + s='{:15.8e}'.format(x) + except: + s=x + return s + + def beamdyn_section_mat_tostring(x,K,M): + def mat_tostring(M,fmt='24.16e'): + return '\n'.join([' '+' '.join(['{:24.16E}'.format(m) for m in M[i,:]]) for i in range(np.size(M,1))]) + s='' + s+='{:.6f}\n'.format(x) + s+=mat_tostring(K) + #s+=np.array2string(K) + s+='\n' + s+='\n' + s+=mat_tostring(M) + #s+=np.array2string(M) + s+='\n' + s+='\n' + return s + + for i in range(len(self.data)): + d=self.data[i] + if d['isComment']: + s+='{}'.format(d['value']) + elif d['tabType']==TABTYPE_NOT_A_TAB: + if isinstance(d['value'], list): + sList=', '.join([str(x) for x in d['value']]) + s+=toStringVLD(sList, d['label'], d['descr']) + else: + s+=toStringVLD(d['value'],d['label'],d['descr']) + elif d['tabType']==TABTYPE_NUM_WITH_HEADER: + if d['tabColumnNames'] is not None: + s+='{}'.format(' '.join(['{:15s}'.format(s) for s in d['tabColumnNames']])) + #s+=d['descr'] # Not ready for that + if d['tabUnits'] is not None: + s+='\n' + s+='{}'.format(' '.join(['{:15s}'.format(s) for s in d['tabUnits']])) + newline='\n' + else: + newline='' + if np.size(d['value'],0) > 0 : + s+=newline + s+='\n'.join('\t'.join( ('{:15.0f}'.format(x) if int(x)==x else '{:15.8e}'.format(x) ) for x in y) for y in d['value']) + elif d['tabType']==TABTYPE_MIX_WITH_HEADER: + s+='{}'.format(' '.join(['{:15s}'.format(s) for s in d['tabColumnNames']])) + if d['tabUnits'] is not None: + s+='\n' + s+='{}'.format(' '.join(['{:15s}'.format(s) for s in d['tabUnits']])) + if np.size(d['value'],0) > 0 : + s+='\n' + s+='\n'.join('\t'.join(toStringIntFloatStr(x) for x in y) for y in d['value']) + elif d['tabType']==TABTYPE_NUM_WITH_HEADERCOM: + s+='! {}\n'.format(' '.join(['{:15s}'.format(s) for s in d['tabColumnNames']])) + s+='! {}\n'.format(' '.join(['{:15s}'.format(s) for s in d['tabUnits']])) + s+='\n'.join('\t'.join('{:15.8e}'.format(x) for x in y) for y in d['value']) + elif d['tabType']==TABTYPE_FIL: + #f.write('{} {} {}\n'.format(d['value'][0],d['tabDetect'],d['descr'])) + label = d['label'] + if 'kbot' in self.keys(): # Moordyn has no 'OutList' label.. + label='' + if len(d['value'])==1: + s+='{} {} {}'.format(d['value'][0], label, d['descr']) # TODO? + else: + s+='{} {} {}\n'.format(d['value'][0], label, d['descr']) # TODO? + s+='\n'.join(fil for fil in d['value'][1:]) + elif d['tabType']==TABTYPE_NUM_BEAMDYN: + # TODO use dedicated sub-class + data = d['value'] + Cols =['Span'] + Cols+=['K{}{}'.format(i+1,j+1) for i in range(6) for j in range(6)] + Cols+=['M{}{}'.format(i+1,j+1) for i in range(6) for j in range(6)] + for i in np.arange(len(data['span'])): + x = data['span'][i] + K = data['K'][i] + M = data['M'][i] + s += beamdyn_section_mat_tostring(x,K,M) + elif d['tabType']==TABTYPE_NUM_SUBDYNOUT: + data = d['value'] + s+='{}\n'.format(' '.join(['{:15s}'.format(s) for s in d['tabColumnNames']])) + s+='{}'.format(' '.join(['{:15s}'.format(s) for s in d['tabUnits']])) + if np.size(d['value'],0) > 0 : + s+='\n' + s+='\n'.join('\t'.join('{:15.0f}'.format(x) for x in y) for y in data) + else: + raise Exception('Unknown table type for variable {}'.format(d)) + if i0: + print('[WARN] Creating directory: ',dirname) + os.makedirs(dirname) + + self._write() + else: + raise Exception('No filename provided') + + def _writeSanityChecks(self): + """ Sanity checks before write""" + pass + + def _write(self): + self._writeSanityChecks() + with open(self.filename,'w') as f: + f.write(self.toString()) + + def toDataFrame(self): + return self._toDataFrame() + + def _toDataFrame(self): + dfs={} + + for i in range(len(self.data)): + d=self.data[i] + if d['tabType'] in [TABTYPE_NUM_WITH_HEADER, TABTYPE_NUM_WITH_HEADERCOM, TABTYPE_NUM_NO_HEADER, TABTYPE_MIX_WITH_HEADER]: + Val= d['value'] + if d['tabUnits'] is None: + Cols=d['tabColumnNames'] + else: + Cols=['{}_{}'.format(c,u.replace('(','[').replace(')',']')) for c,u in zip(d['tabColumnNames'],d['tabUnits'])] + #print(Val) + #print(Cols) + + # --- Adding some useful tabulated data for some files (Shapefunctions, polar) + + if self.getIDSafe('TwFAM1Sh(2)')>0: + # Hack for tower files, we add the modes + # NOTE: we provide interpolated shape function just in case the resolution of the input file is low.. + x=Val[:,0] + Modes=np.zeros((x.shape[0],4)) + Modes[:,0] = x**2 * self['TwFAM1Sh(2)'] + x**3 * self['TwFAM1Sh(3)'] + x**4 * self['TwFAM1Sh(4)'] + x**5 * self['TwFAM1Sh(5)'] + x**6 * self['TwFAM1Sh(6)'] + Modes[:,1] = x**2 * self['TwFAM2Sh(2)'] + x**3 * self['TwFAM2Sh(3)'] + x**4 * self['TwFAM2Sh(4)'] + x**5 * self['TwFAM2Sh(5)'] + x**6 * self['TwFAM2Sh(6)'] + Modes[:,2] = x**2 * self['TwSSM1Sh(2)'] + x**3 * self['TwSSM1Sh(3)'] + x**4 * self['TwSSM1Sh(4)'] + x**5 * self['TwSSM1Sh(5)'] + x**6 * self['TwSSM1Sh(6)'] + Modes[:,3] = x**2 * self['TwSSM2Sh(2)'] + x**3 * self['TwSSM2Sh(3)'] + x**4 * self['TwSSM2Sh(4)'] + x**5 * self['TwSSM2Sh(5)'] + x**6 * self['TwSSM2Sh(6)'] + Val = np.hstack((Val,Modes)) + ShapeCols = [c+'_[-]' for c in ['ShapeForeAft1','ShapeForeAft2','ShapeSideSide1','ShapeSideSide2']] + Cols = Cols + ShapeCols + + name=d['label'] + + if name=='DampingCoeffs': + pass + else: + dfs[name]=pd.DataFrame(data=Val,columns=Cols) + elif d['tabType'] in [TABTYPE_NUM_BEAMDYN]: + span = d['value']['span'] + M = d['value']['M'] + K = d['value']['K'] + nSpan=len(span) + MM=np.zeros((nSpan,1+36+36)) + MM[:,0] = span + MM[:,1:37] = K.reshape(nSpan,36) + MM[:,37:] = M.reshape(nSpan,36) + Cols =['Span'] + Cols+=['K{}{}'.format(i+1,j+1) for i in range(6) for j in range(6)] + Cols+=['M{}{}'.format(i+1,j+1) for i in range(6) for j in range(6)] + # Putting the main terms first + IAll = range(1+36+36) + IMain= [0] + [i*6+i+1 for i in range(6)] + [i*6+i+37 for i in range(6)] + IOrg = IMain + [i for i in range(1+36+36) if i not in IMain] + Cols = [Cols[i] for i in IOrg] + data = MM[:,IOrg] + name=d['label'] + dfs[name]=pd.DataFrame(data=data,columns=Cols) + if len(dfs)==1: + dfs=dfs[list(dfs.keys())[0]] + return dfs + + def toGraph(self, **kwargs): + from .fast_input_file_graph import fastToGraph + return fastToGraph(self, **kwargs) + + + +# --------------------------------------------------------------------------------} +# --- SubReaders /detectors +# --------------------------------------------------------------------------------{ + + + def detectAndReadAirfoilAD14(self,lines): + if len(lines)<14: + return False + # Reading number of tables + L3 = lines[2].strip().split() + if len(L3)<=0: + return False + if not strIsInt(L3[0]): + return False + nTables=int(L3[0]) + # Reading table ID + L4 = lines[3].strip().split() + if len(L4)<=nTables: + return False + TableID=L4[:nTables] + if nTables==1: + TableID=[''] + # Keywords for file format + KW1=lines[12].strip().split() + KW2=lines[13].strip().split() + if len(KW1)>nTables and len(KW2)>nTables: + if KW1[nTables].lower()=='angle' and KW2[nTables].lower()=='minimum': + d = getDict(); d['isComment'] = True; d['value'] = lines[0]; self.data.append(d); + d = getDict(); d['isComment'] = True; d['value'] = lines[1]; self.data.append(d); + for i in range(2,14): + splits = lines[i].split() + #print(splits) + d = getDict() + d['label'] = ' '.join(splits[1:]) # TODO + d['descr'] = ' '.join(splits[1:]) # TODO + d['value'] = float(splits[0]) + self.data.append(d) + #pass + #for i in range(2,14): + nTabLines=0 + while 14+nTabLines0 : + nTabLines +=1 + #data = np.array([lines[i].strip().split() for i in range(14,len(lines)) if len(lines[i])>0]).astype(float) + #data = np.array([lines[i].strip().split() for i in takewhile(lambda x: len(lines[i].strip())>0, range(14,len(lines)-1))]).astype(float) + data = np.array([lines[i].strip().split() for i in range(14,nTabLines+14)]).astype(float) + #print(data) + d = getDict() + d['label'] = 'Polar' + d['tabDimVar'] = nTabLines + d['tabType'] = TABTYPE_NUM_NO_HEADER + d['value'] = data + if np.size(data,1)==1+nTables*3: + d['tabColumnNames'] = ['Alpha']+[n+l for l in TableID for n in ['Cl','Cd','Cm']] + d['tabUnits'] = ['(deg)']+['(-)' , '(-)' , '(-)']*nTables + elif np.size(data,1)==1+nTables*2: + d['tabColumnNames'] = ['Alpha']+[n+l for l in TableID for n in ['Cl','Cd']] + d['tabUnits'] = ['(deg)']+['(-)' , '(-)']*nTables + else: + d['tabColumnNames'] = ['col{}'.format(j) for j in range(np.size(data,1))] + self.data.append(d) + return True + + def readBeamDynProps(self,lines,iStart): + nStations=self['station_total'] + #M=np.zeros((nStations,1+36+36)) + M = np.zeros((nStations,6,6)) + K = np.zeros((nStations,6,6)) + span = np.zeros(nStations) + i=iStart; + try: + for j in range(nStations): + # Read span location + span[j]=float(lines[i]); i+=1; + # Read stiffness matrix + K[j,:,:]=np.array((' '.join(lines[i:i+6])).split()).astype(float).reshape(6,6) + i+=7 + # Read mass matrix + M[j,:,:]=np.array((' '.join(lines[i:i+6])).split()).astype(float).reshape(6,6) + i+=7 + except: + raise WrongFormatError('An error occured while reading section {}/{}'.format(j+1,nStations)) + d = getDict() + d['label'] = 'BeamProperties' + d['descr'] = '' + d['tabType'] = TABTYPE_NUM_BEAMDYN + d['value'] = {'span':span, 'K':K, 'M':M} + self.data.append(d) + + +# --------------------------------------------------------------------------------} +# --- Helper functions +# --------------------------------------------------------------------------------{ +def isStr(s): + return isinstance(s, str) + +def strIsFloat(s): + #return s.replace('.',',1').isdigit() + try: + float(s) + return True + except: + return False + +def strIsBool(s): + return s.lower() in ['true','false','t','f'] + +def strIsInt(s): + s = str(s) + if s[0] in ('-', '+'): + return s[1:].isdigit() + return s.isdigit() + +def strToBool(s): + return s.lower() in ['true','t'] + +def hasSpecialChars(s): + # fast allows for parenthesis + # For now we allow for - but that's because of BeamDyn geometry members + return not re.match("^[\"\'a-zA-Z0-9_()-]*$", s) + +def cleanLine(l): + # makes a string single space separated + l = l.replace('\t',' ') + l = ' '.join(l.split()) + l = l.strip() + return l + +def cleanAfterChar(l,c): + # remove whats after a character + n = l.find(c); + if n>0: + return l[:n] + else: + return l + +def getDict(): + return {'value':None, 'label':'', 'isComment':False, 'descr':'', 'tabType':TABTYPE_NOT_A_TAB} + +def _merge_value(splits): + + merged = splits.pop(0) + if merged[0] == '"': + while merged[-1] != '"': + merged += " "+splits.pop(0) + splits.insert(0, merged) + + + + +def parseFASTInputLine(line_raw,i,allowSpaceSeparatedList=False): + d = getDict() + #print(line_raw) + try: + # preliminary cleaning (Note: loss of formatting) + line = cleanLine(line_raw) + # Comment + if any(line.startswith(c) for c in ['#','!','--','==']) or len(line)==0: + d['isComment']=True + d['value']=line_raw + return d + if line.lower().startswith('end'): + sp =line.split() + if len(sp)>2 and sp[1]=='of': + d['isComment']=True + d['value']=line_raw + + # Detecting lists + List=[]; + iComma=line.find(',') + if iComma>0 and iComma<30: + fakeline=line.replace(' ',',') + fakeline=re.sub(',+',',',fakeline) + csplits=fakeline.split(',') + # Splitting based on comma and looping while it's numbers of booleans + ii=0 + s=csplits[ii] + #print(csplits) + while strIsFloat(s) or strIsBool(s) and ii=len(csplits): + raise WrongFormatError('Wrong number of list values') + s = csplits[ii] + #print('[INFO] Line {}: Found list: '.format(i),List) + # Defining value and remaining splits + if len(List)>=2: + d['value']=List + line_remaining=line + # eating line, removing each values + for iii in range(ii): + sValue=csplits[iii] + ipos=line_remaining.find(sValue) + line_remaining = line_remaining[ipos+len(sValue):] + splits=line_remaining.split() + iNext=0 + else: + # It's not a list, we just use space as separators + splits=line.split(' ') + _merge_value(splits) + s=splits[0] + + if strIsInt(s): + d['value']=int(s) + if allowSpaceSeparatedList and len(splits)>1: + if strIsInt(splits[1]): + d['value']=splits[0]+ ' '+splits[1] + elif strIsFloat(s): + d['value']=float(s) + elif strIsBool(s): + d['value']=strToBool(s) + else: + d['value']=s + iNext=1 + + # Extracting label (TODO, for now only second split) + bOK=False + while (not bOK) and iNext comment assumed'.format(i+1)) + d['isComment']=True + d['value']=line_raw + iNext = len(splits)+1 + + # Recombining description + if len(splits)>=iNext+1: + d['descr']=' '.join(splits[iNext:]) + except WrongFormatError as e: + raise WrongFormatError('Line {}: '.format(i+1)+e.args[0]) + except Exception as e: + raise Exception('Line {}: '.format(i+1)+e.args[0]) + + return d + +def parseFASTOutList(lines,iStart): + OutList=[] + i = iStart + while i=len(lines): + print('[WARN] End of file reached while reading Outlist') + #i=min(i+1,len(lines)) + return OutList,iStart+len(OutList) + + +def extractWithinParenthesis(s): + mo = re.search(r'\((.*)\)', s) + if mo: + return mo.group(1) + return '' + +def extractWithinBrackets(s): + mo = re.search(r'\((.*)\)', s) + if mo: + return mo.group(1) + return '' + +def detectUnits(s,nRef): + nPOpen=s.count('(') + nPClos=s.count(')') + nBOpen=s.count('[') + nBClos=s.count(']') + + sep='!#@#!' + if (nPOpen == nPClos) and (nPOpen>=nRef): + #that's pretty good + Units=s.replace('(','').replace(')',sep).split(sep)[:-1] + elif (nBOpen == nBClos) and (nBOpen>=nRef): + Units=s.replace('[','').replace(']',sep).split(sep)[:-1] + else: + Units=s.split() + return Units + + +def findNumberOfTableLines(lines, break_chars): + """ Loop through lines until a one of the "break character is found""" + for i, l in enumerate(lines): + for bc in break_chars: + if l.startswith(bc): + return i + # Not found + print('[FAIL] end of table not found') + return len(lines) + + +def parseFASTNumTable(filename,lines,n,iStart,nHeaders=2,tableType='num',nOffset=0, varNumLines=''): + """ + First lines of data starts at: nHeaders+nOffset + + """ + Tab = None + ColNames = None + Units = None + + + if len(lines)!=n+nHeaders+nOffset: + raise BrokenFormatError('Not enough lines in table: {} lines instead of {}\nFile:{}'.format(len(lines)-nHeaders,n,filename)) + try: + if nHeaders==0: + # Extract number of values from number of numerical values on first line + numeric_const_pattern = r'[-+]? (?: (?: \d* \. \d+ ) | (?: \d+ \.? ) )(?: [Ee] [+-]? \d+ ) ?' + rx = re.compile(numeric_const_pattern, re.VERBOSE) + header = cleanAfterChar(lines[nOffset], '!') + if tableType=='num': + dat= np.array(rx.findall(header)).astype(float) + ColNames=['C{}'.format(j) for j in range(len(dat))] + else: + raise NotImplementedError('Reading FAST tables with no headers for type different than num not implemented yet') + + elif nHeaders>=1: + # Extract column names + i = 0 + sTmp = cleanLine(lines[i]) + sTmp = cleanAfterChar(sTmp,'[') + sTmp = cleanAfterChar(sTmp,'(') + sTmp = cleanAfterChar(sTmp,'!') + sTmp = cleanAfterChar(sTmp,'#') + if sTmp.startswith('!'): + sTmp=sTmp[1:].strip() + ColNames=sTmp.split() + if nHeaders>=2: + # Extract units + i = 1 + sTmp = cleanLine(lines[i]) + sTmp = cleanAfterChar(sTmp,'!') + sTmp = cleanAfterChar(sTmp,'#') + if sTmp.startswith('!'): + sTmp=sTmp[1:].strip() + + Units = detectUnits(sTmp,len(ColNames)) + Units = ['({})'.format(u.strip()) for u in Units] + # Forcing user to match number of units and column names + if len(ColNames) != len(Units): + print(ColNames) + print(Units) + print('[WARN] {}: Line {}: Number of column names different from number of units in table'.format(filename, iStart+i+1)) + + nCols=len(ColNames) + + if tableType=='num': + if n==0: + Tab = np.zeros((n, nCols)) + for i in range(nHeaders+nOffset,n+nHeaders+nOffset): + l = cleanAfterChar(lines[i].lower(),'!') + l = cleanAfterChar(l,'#') + v = l.split() + if len(v) != nCols: + # Discarding SubDyn special cases + if ColNames[-1].lower() not in ['nodecnt']: + print('[WARN] {}: Line {}: number of data different from number of column names. ColumnNames: {}'.format(filename, iStart+i+1, ColNames)) + if i==nHeaders+nOffset: + # Node Cnt + if len(v) != nCols: + if ColNames[-1].lower()== 'nodecnt': + ColNames = ColNames+['Col']*(len(v)-nCols) + Units = Units+['Col']*(len(v)-nCols) + + nCols=len(v) + Tab = np.zeros((n, nCols)) + # Accounting for TRUE FALSE and converting to float + v = [s.replace('true','1').replace('false','0').replace('noprint','0').replace('print','1') for s in v] + v = [float(s) for s in v[0:nCols]] + if len(v) < nCols: + raise Exception('Number of data is lower than number of column names') + Tab[i-nHeaders-nOffset,:] = v + elif tableType=='mix': + # a mix table contains a mixed of strings and floats + # For now, we are being a bit more relaxed about the number of columns + if n==0: + Tab = np.zeros((n, nCols)).astype(object) + for i in range(nHeaders+nOffset,n+nHeaders+nOffset): + l = lines[i] + l = cleanAfterChar(l,'!') + l = cleanAfterChar(l,'#') + v = l.split() + if l.startswith('---'): + raise BrokenFormatError('Error reading line {} while reading table. Is the variable `{}` set correctly?'.format(iStart+i+1, varNumLines)) + if len(v) != nCols: + # Discarding SubDyn special cases + if ColNames[-1].lower() not in ['cosmid', 'ssifile']: + print('[WARN] {}: Line {}: Number of data is different than number of column names. Column Names: {}'.format(filename,iStart+1+i, ColNames)) + if i==nHeaders+nOffset: + if len(v)>nCols: + ColNames = ColNames+['Col']*(len(v)-nCols) + Units = Units+['Col']*(len(v)-nCols) + nCols=len(v) + Tab = np.zeros((n, nCols)).astype(object) + v=v[0:min(len(v),nCols)] + Tab[i-nHeaders-nOffset,0:len(v)] = v + # If all values are float, we convert to float + if all([strIsFloat(x) for x in Tab.ravel()]): + Tab=Tab.astype(float) + elif tableType=='sdout': + header = lines[0] + units = lines[1] + Tab=[] + for i in range(nHeaders+nOffset,n+nHeaders+nOffset): + l = cleanAfterChar(lines[i].lower(),'!') + Tab.append( np.array(l.split()).astype(int)) + else: + raise Exception('Unknown table type') + + ColNames = ColNames[0:nCols] + if Units is not None: + Units = Units[0:nCols] + Units = ['('+u.replace('(','').replace(')','')+')' for u in Units] + if nHeaders==0: + ColNames=None + + except Exception as e: + raise BrokenFormatError('Line {}: {}'.format(iStart+i+1,e.args[0])) + return Tab, ColNames, Units + + +def parseFASTFilTable(lines,n,iStart): + Tab = [] + try: + i=0 + if len(lines)!=n: + raise WrongFormatError('Not enough lines in table: {} lines instead of {}'.format(len(lines),n)) + for i in range(n): + l = lines[i].split() + #print(l[0].strip()) + Tab.append(l[0].strip()) + + except Exception as e: + raise Exception('Line {}: '.format(iStart+i+1)+e.args[0]) + return Tab + + + +# --------------------------------------------------------------------------------} +# --------------------------------------------------------------------------------} +# --------------------------------------------------------------------------------} +# --- Predefined types (may change with OpenFAST version..) +# --------------------------------------------------------------------------------{ +# --------------------------------------------------------------------------------{ +# --------------------------------------------------------------------------------{ + + +# --------------------------------------------------------------------------------} +# --- BeamDyn +# --------------------------------------------------------------------------------{ +class BDFile(FASTInputFileBase): + @classmethod + def from_fast_input_file(cls, parent): + self = cls() + self.setData(filename=parent.filename, data=parent.data, hasNodal=parent.hasNodal, module='BD') + return self + + def __init__(self, filename=None, **kwargs): + FASTInputFileBase.__init__(self, filename, **kwargs) + if filename is None: + # Define a prototype for this file format + self.addComment('--------- BEAMDYN with OpenFAST INPUT FILE -------------------------------------------') + self.addComment('BeamDyn input file, written by BDFile') + self.addComment('---------------------- SIMULATION CONTROL --------------------------------------') + self.addValKey(False , 'Echo' , 'Echo input data to ".ech"? (flag)') + self.addValKey(True , 'QuasiStaticInit' , 'Use quasi-static pre-conditioning with centripetal accelerations in initialization? (flag) [dynamic solve only]') + self.addValKey( 0 , 'rhoinf' , 'Numerical damping parameter for generalized-alpha integrator') + self.addValKey( 2 , 'quadrature' , 'Quadrature method: 1=Gaussian; 2=Trapezoidal (switch)') + self.addValKey("DEFAULT" , 'refine' , 'Refinement factor for trapezoidal quadrature (-) [DEFAULT = 1; used only when quadrature=2]') + self.addValKey("DEFAULT" , 'n_fact' , 'Factorization frequency for the Jacobian in N-R iteration(-) [DEFAULT = 5]') + self.addValKey("DEFAULT" , 'DTBeam' , 'Time step size (s)') + self.addValKey("DEFAULT" , 'load_retries' , 'Number of factored load retries before quitting the simulation [DEFAULT = 20]') + self.addValKey("DEFAULT" , 'NRMax' , 'Max number of iterations in Newton-Raphson algorithm (-) [DEFAULT = 10]') + self.addValKey("DEFAULT" , 'stop_tol' , 'Tolerance for stopping criterion (-) [DEFAULT = 1E-5]') + self.addValKey("DEFAULT" , 'tngt_stf_fd' , 'Use finite differenced tangent stiffness matrix? (flag)') + self.addValKey("DEFAULT" , 'tngt_stf_comp' , 'Compare analytical finite differenced tangent stiffness matrix? (flag)') + self.addValKey("DEFAULT" , 'tngt_stf_pert' , 'Perturbation size for finite differencing (-) [DEFAULT = 1E-6]') + self.addValKey("DEFAULT" , 'tngt_stf_difftol', 'Maximum allowable relative difference between analytical and fd tangent stiffness (-); [DEFAULT = 0.1]') + self.addValKey(True , 'RotStates' , 'Orient states in the rotating frame during linearization? (flag) [used only when linearizing] ') + self.addComment('---------------------- GEOMETRY PARAMETER --------------------------------------') + self.addValKey( 1 , 'member_total' , 'Total number of members (-)') + self.addValKey( 0 , 'kp_total' , 'Total number of key points (-) [must be at least 3]') + self.addValKey( [1, 0] , 'kp_per_member' , 'Member number; Number of key points in this member') + self.addTable('MemberGeom', np.zeros((0,4)), tabType=1, tabDimVar='kp_total', + cols=['kp_xr', 'kp_yr', 'kp_zr', 'initial_twist'], + units=['(m)', '(m)', '(m)', '(deg)']) + self.addComment('---------------------- MESH PARAMETER ------------------------------------------') + self.addValKey( 5 , 'order_elem' , 'Order of interpolation (basis) function (-)') + self.addComment('---------------------- MATERIAL PARAMETER --------------------------------------') + self.addValKey('"undefined"', 'BldFile' , 'Name of file containing properties for blade (quoted string)') + self.addComment('---------------------- PITCH ACTUATOR PARAMETERS -------------------------------') + self.addValKey(False , 'UsePitchAct' , 'Whether a pitch actuator should be used (flag)') + self.addValKey( 1 , 'PitchJ' , 'Pitch actuator inertia (kg-m^2) [used only when UsePitchAct is true]') + self.addValKey( 0 , 'PitchK' , 'Pitch actuator stiffness (kg-m^2/s^2) [used only when UsePitchAct is true]') + self.addValKey( 0 , 'PitchC' , 'Pitch actuator damping (kg-m^2/s) [used only when UsePitchAct is true]') + self.addComment('---------------------- OUTPUTS -------------------------------------------------') + self.addValKey(False , 'SumPrint' , 'Print summary data to ".sum" (flag)') + self.addValKey('"ES10.3E2"' , 'OutFmt' , 'Format used for text tabular output, excluding the time channel.') + self.addValKey( 0 , 'NNodeOuts' , 'Number of nodes to output to file [0 - 9] (-)') + self.addValKey( [1] , 'OutNd' , 'Nodes whose values will be output (-)') + self.addValKey( [''] , 'OutList' , 'The next line(s) contains a list of output parameters. See OutListParameters.xlsx, BeamDyn tab for a listing of available output channels, (-)') + self.addComment('END of OutList (the word "END" must appear in the first 3 columns of this last OutList line)') + self.addComment('---------------------- NODE OUTPUTS --------------------------------------------') + self.addValKey( 99 , 'BldNd_BlOutNd' , 'Blade nodes on each blade (currently unused)') + self.addValKey( [''] , 'OutList_Nodal' , 'The next line(s) contains a list of output parameters. See OutListParameters.xlsx, BeamDyn_Nodes tab for a listing of available output channels, (-)') + self.addComment('END of input file (the word "END" must appear in the first 3 columns of this last OutList line)') + self.addComment('--------------------------------------------------------------------------------') + self.hasNodal=True + #"RootFxr, RootFyr, RootFzr" + #"RootMxr, RootMyr, RootMzr" + #"TipTDxr, TipTDyr, TipTDzr" + #"TipRDxr, TipRDyr, TipRDzr" + + else: + # fix some stuff that generic reader fail at + self.data[1] = {'value':self._lines[1], 'label':'', 'isComment':True, 'descr':'', 'tabType':0} + i = self.getID('kp_total') + listval = [int(v) for v in str(self.data[i+1]['value']).split()] + self.data[i+1]['value']=listval + self.data[i+1]['label']='kp_per_member' + self.data[i+1]['isComment']=False + self.module='BD' + + def _writeSanityChecks(self): + """ Sanity checks before write """ + self['kp_total']=self['MemberGeom'].shape[0] + i = self.getID('kp_total') + self.data[i+1]['value']=[1, self['MemberGeom'].shape[0]] # kp_per_member + self.data[i+1]['label']='kp_per_member' + # Could check length of OutNd + + def _toDataFrame(self): + df = FASTInputFileBase._toDataFrame(self) + # TODO add quadrature points based on trapz/gauss + return df + + @property + def _IComment(self): return [1] + +# --------------------------------------------------------------------------------} +# --- ElastoDyn Blade +# --------------------------------------------------------------------------------{ +class EDBladeFile(FASTInputFileBase): + @classmethod + def from_fast_input_file(cls, parent): + self = cls() + self.setData(filename=parent.filename, data=parent.data, hasNodal=parent.hasNodal, module='EDBlade') + return self + + def __init__(self, filename=None, **kwargs): + FASTInputFileBase.__init__(self, filename, **kwargs) + if filename is None: + # Define a prototype for this file format + self.addComment('------- ELASTODYN V1.00.* INDIVIDUAL BLADE INPUT FILE --------------------------') + self.addComment('ElastoDyn blade definition, written by EDBladeFile.') + self.addComment('---------------------- BLADE PARAMETERS ----------------------------------------') + self.addValKey( 0 , 'NBlInpSt' , 'Number of blade input stations (-)') + self.addValKey( 1. , 'BldFlDmp(1)', 'Blade flap mode #1 structural damping in percent of critical (%)') + self.addValKey( 1. , 'BldFlDmp(2)', 'Blade flap mode #2 structural damping in percent of critical (%)') + self.addValKey( 1. , 'BldEdDmp(1)', 'Blade edge mode #1 structural damping in percent of critical (%)') + self.addComment('---------------------- BLADE ADJUSTMENT FACTORS --------------------------------') + self.addValKey( 1. , 'FlStTunr(1)', 'Blade flapwise modal stiffness tuner, 1st mode (-)') + self.addValKey( 1. , 'FlStTunr(2)', 'Blade flapwise modal stiffness tuner, 2nd mode (-)') + self.addValKey( 1. , 'AdjBlMs' , 'Factor to adjust blade mass density (-)') + self.addValKey( 1. , 'AdjFlSt' , 'Factor to adjust blade flap stiffness (-)') + self.addValKey( 1. , 'AdjEdSt' , 'Factor to adjust blade edge stiffness (-)') + self.addComment('---------------------- DISTRIBUTED BLADE PROPERTIES ----------------------------') + self.addTable('BldProp', np.zeros((0,6)), tabType=1, tabDimVar='NBlInpSt', cols=['BlFract', 'PitchAxis', 'StrcTwst', 'BMassDen', 'FlpStff', 'EdgStff'], units=['(-)', '(-)', '(deg)', '(kg/m)', '(Nm^2)', '(Nm^2)']) + self.addComment('---------------------- BLADE MODE SHAPES ---------------------------------------') + self.addValKey( 1.0 , 'BldFl1Sh(2)', 'Flap mode 1, coeff of x^2') + self.addValKey( 0.0 , 'BldFl1Sh(3)', ' , coeff of x^3') + self.addValKey( 0.0 , 'BldFl1Sh(4)', ' , coeff of x^4') + self.addValKey( 0.0 , 'BldFl1Sh(5)', ' , coeff of x^5') + self.addValKey( 0.0 , 'BldFl1Sh(6)', ' , coeff of x^6') + self.addValKey( 0.0 , 'BldFl2Sh(2)', 'Flap mode 2, coeff of x^2') # NOTE: using something not too bad just incase user uses these as is.. + self.addValKey( 0.0 , 'BldFl2Sh(3)', ' , coeff of x^3') + self.addValKey( -13.0 , 'BldFl2Sh(4)', ' , coeff of x^4') + self.addValKey( 27.0 , 'BldFl2Sh(5)', ' , coeff of x^5') + self.addValKey( -13.0 , 'BldFl2Sh(6)', ' , coeff of x^6') + self.addValKey( 1.0 , 'BldEdgSh(2)', 'Edge mode 1, coeff of x^2') + self.addValKey( 0.0 , 'BldEdgSh(3)', ' , coeff of x^3') + self.addValKey( 0.0 , 'BldEdgSh(4)', ' , coeff of x^4') + self.addValKey( 0.0 , 'BldEdgSh(5)', ' , coeff of x^5') + self.addValKey( 0.0 , 'BldEdgSh(6)', ' , coeff of x^6') + else: + # fix some stuff that generic reader fail at + self.data[1] = {'value':self._lines[1], 'label':'', 'isComment':True, 'descr':'', 'tabType':0} + self.module='EDBlade' + + def _writeSanityChecks(self): + """ Sanity checks before write """ + self['NBlInpSt']=self['BldProp'].shape[0] + # Sum of Coeffs should be 1 + for s in ['BldFl1Sh','BldFl2Sh','BldEdgSh']: + sumcoeff=np.sum([self[s+'('+str(i)+')'] for i in [2,3,4,5,6] ]) + if np.abs(sumcoeff-1)>1e-4: + print('[WARN] Sum of coefficients for polynomial {} not equal to 1 ({}). File: {}'.format(s, sumcoeff, self.filename)) + + def _toDataFrame(self): + df = FASTInputFileBase._toDataFrame(self) + # We add the shape functions for EDBladeFile + x=df['BlFract_[-]'].values + Modes=np.zeros((x.shape[0],3)) + Modes[:,0] = x**2 * self['BldFl1Sh(2)'] + x**3 * self['BldFl1Sh(3)'] + x**4 * self['BldFl1Sh(4)'] + x**5 * self['BldFl1Sh(5)'] + x**6 * self['BldFl1Sh(6)'] + Modes[:,1] = x**2 * self['BldFl2Sh(2)'] + x**3 * self['BldFl2Sh(3)'] + x**4 * self['BldFl2Sh(4)'] + x**5 * self['BldFl2Sh(5)'] + x**6 * self['BldFl2Sh(6)'] + Modes[:,2] = x**2 * self['BldEdgSh(2)'] + x**3 * self['BldEdgSh(3)'] + x**4 * self['BldEdgSh(4)'] + x**5 * self['BldEdgSh(5)'] + x**6 * self['BldEdgSh(6)'] + df[['ShapeFlap1_[-]','ShapeFlap2_[-]','ShapeEdge1_[-]']]=Modes + return df + + @property + def _IComment(self): return [1] + +# --------------------------------------------------------------------------------} +# --- ElastoDyn Tower +# --------------------------------------------------------------------------------{ +class EDTowerFile(FASTInputFileBase): + @classmethod + def from_fast_input_file(cls, parent): + self = cls() + self.setData(filename=parent.filename, data=parent.data, hasNodal=parent.hasNodal, module='EDTower') + return self + + def __init__(self, filename=None, **kwargs): + FASTInputFileBase.__init__(self, filename, **kwargs) + if filename is None: + # Define a prototype for this file format + self.addComment('------- ELASTODYN V1.00.* TOWER INPUT FILE -------------------------------------') + self.addComment('ElastoDyn tower definition, written by EDTowerFile.') + self.addComment('---------------------- TOWER PARAMETERS ----------------------------------------') + self.addValKey( 0 , 'NTwInpSt' , 'Number of blade input stations (-)') + self.addValKey( 1. , 'TwrFADmp(1)' , 'Tower 1st fore-aft mode structural damping ratio (%)') + self.addValKey( 1. , 'TwrFADmp(2)' , 'Tower 2nd fore-aft mode structural damping ratio (%)') + self.addValKey( 1. , 'TwrSSDmp(1)' , 'Tower 1st side-to-side mode structural damping ratio (%)') + self.addValKey( 1. , 'TwrSSDmp(2)' , 'Tower 2nd side-to-side mode structural damping ratio (%)') + self.addComment('---------------------- TOWER ADJUSTMENT FACTORS --------------------------------') + self.addValKey( 1. , 'FAStTunr(1)' , 'Tower fore-aft modal stiffness tuner, 1st mode (-)') + self.addValKey( 1. , 'FAStTunr(2)' , 'Tower fore-aft modal stiffness tuner, 2nd mode (-)') + self.addValKey( 1. , 'SSStTunr(1)' , 'Tower side-to-side stiffness tuner, 1st mode (-)') + self.addValKey( 1. , 'SSStTunr(2)' , 'Tower side-to-side stiffness tuner, 2nd mode (-)') + self.addValKey( 1. , 'AdjTwMa' , 'Factor to adjust tower mass density (-)') + self.addValKey( 1. , 'AdjFASt' , 'Factor to adjust tower fore-aft stiffness (-)') + self.addValKey( 1. , 'AdjSSSt' , 'Factor to adjust tower side-to-side stiffness (-)') + self.addComment('---------------------- DISTRIBUTED TOWER PROPERTIES ----------------------------') + self.addTable('TowProp', np.zeros((0,6)), tabType=1, tabDimVar='NTwInpSt', + cols=['HtFract','TMassDen','TwFAStif','TwSSStif'], + units=['(-)', '(kg/m)', '(Nm^2)', '(Nm^2)']) + self.addComment('---------------------- TOWER FORE-AFT MODE SHAPES ------------------------------') + self.addValKey( 1.0 , 'TwFAM1Sh(2)', 'Mode 1, coefficient of x^2 term') + self.addValKey( 0.0 , 'TwFAM1Sh(3)', ' , coefficient of x^3 term') + self.addValKey( 0.0 , 'TwFAM1Sh(4)', ' , coefficient of x^4 term') + self.addValKey( 0.0 , 'TwFAM1Sh(5)', ' , coefficient of x^5 term') + self.addValKey( 0.0 , 'TwFAM1Sh(6)', ' , coefficient of x^6 term') + self.addValKey( -26. , 'TwFAM2Sh(2)', 'Mode 2, coefficient of x^2 term') # NOTE: using something not too bad just incase user uses these as is.. + self.addValKey( 0.0 , 'TwFAM2Sh(3)', ' , coefficient of x^3 term') + self.addValKey( 27. , 'TwFAM2Sh(4)', ' , coefficient of x^4 term') + self.addValKey( 0.0 , 'TwFAM2Sh(5)', ' , coefficient of x^5 term') + self.addValKey( 0.0 , 'TwFAM2Sh(6)', ' , coefficient of x^6 term') + self.addComment('---------------------- TOWER SIDE-TO-SIDE MODE SHAPES --------------------------') + self.addValKey( 1.0 , 'TwSSM1Sh(2)', 'Mode 1, coefficient of x^2 term') + self.addValKey( 0.0 , 'TwSSM1Sh(3)', ' , coefficient of x^3 term') + self.addValKey( 0.0 , 'TwSSM1Sh(4)', ' , coefficient of x^4 term') + self.addValKey( 0.0 , 'TwSSM1Sh(5)', ' , coefficient of x^5 term') + self.addValKey( 0.0 , 'TwSSM1Sh(6)', ' , coefficient of x^6 term') + self.addValKey( -26. , 'TwSSM2Sh(2)', 'Mode 2, coefficient of x^2 term') # NOTE: using something not too bad just incase user uses these as is.. + self.addValKey( 0.0 , 'TwSSM2Sh(3)', ' , coefficient of x^3 term') + self.addValKey( 27. , 'TwSSM2Sh(4)', ' , coefficient of x^4 term') + self.addValKey( 0.0 , 'TwSSM2Sh(5)', ' , coefficient of x^5 term') + self.addValKey( 0.0 , 'TwSSM2Sh(6)', ' , coefficient of x^6 term') + else: + # fix some stuff that generic reader fail at + self.data[1] = {'value':self._lines[1], 'label':'', 'isComment':True, 'descr':'', 'tabType':0} + self.module='EDTower' + + def _writeSanityChecks(self): + """ Sanity checks before write """ + self['NTwInpSt']=self['TowProp'].shape[0] + # Sum of Coeffs should be 1 + for s in ['TwFAM1Sh','TwFAM2Sh','TwSSM1Sh','TwSSM2Sh']: + sumcoeff=np.sum([self[s+'('+str(i)+')'] for i in [2,3,4,5,6] ]) + if np.abs(sumcoeff-1)>1e-4: + print('[WARN] Sum of coefficients for polynomial {} not equal to 1 ({}). File: {}'.format(s, sumcoeff, self.filename)) + + def _toDataFrame(self): + df = FASTInputFileBase._toDataFrame(self) + # We add the shape functions for EDBladeFile + # NOTE: we provide interpolated shape function just in case the resolution of the input file is low.. + x = df['HtFract_[-]'].values + Modes=np.zeros((x.shape[0],4)) + Modes[:,0] = x**2 * self['TwFAM1Sh(2)'] + x**3 * self['TwFAM1Sh(3)'] + x**4 * self['TwFAM1Sh(4)'] + x**5 * self['TwFAM1Sh(5)'] + x**6 * self['TwFAM1Sh(6)'] + Modes[:,1] = x**2 * self['TwFAM2Sh(2)'] + x**3 * self['TwFAM2Sh(3)'] + x**4 * self['TwFAM2Sh(4)'] + x**5 * self['TwFAM2Sh(5)'] + x**6 * self['TwFAM2Sh(6)'] + Modes[:,2] = x**2 * self['TwSSM1Sh(2)'] + x**3 * self['TwSSM1Sh(3)'] + x**4 * self['TwSSM1Sh(4)'] + x**5 * self['TwSSM1Sh(5)'] + x**6 * self['TwSSM1Sh(6)'] + Modes[:,3] = x**2 * self['TwSSM2Sh(2)'] + x**3 * self['TwSSM2Sh(3)'] + x**4 * self['TwSSM2Sh(4)'] + x**5 * self['TwSSM2Sh(5)'] + x**6 * self['TwSSM2Sh(6)'] + ShapeCols = [c+'_[-]' for c in ['ShapeForeAft1','ShapeForeAft2','ShapeSideSide1','ShapeSideSide2']] + df[ShapeCols]=Modes + return df + + @property + def _IComment(self): return [1] + +# --------------------------------------------------------------------------------} +# --- AeroDyn Blade +# --------------------------------------------------------------------------------{ +class ADBladeFile(FASTInputFileBase): + @classmethod + def from_fast_input_file(cls, parent): + self = cls() + self.setData(filename=parent.filename, data=parent.data, hasNodal=parent.hasNodal, module='ADBlade') + return self + + def __init__(self, filename=None, **kwargs): + FASTInputFileBase.__init__(self, filename, **kwargs) + if filename is None: + # Define a prototype for this file format + self.addComment('------- AERODYN BLADE DEFINITION INPUT FILE ----------------------------------------------') + self.addComment('Aerodynamic blade definition, written by ADBladeFile') + self.addComment('====== Blade Properties =================================================================') + self.addKeyVal('NumBlNds', 0, 'Number of blade nodes used in the analysis (-)') + self.addTable('BldAeroNodes', np.zeros((0,7)), tabType=1, tabDimVar='NumBlNds', cols=['BlSpn', 'BlCrvAC', 'BlSwpAC', 'BlCrvAng', 'BlTwist', 'BlChord', 'BlAFID'], units=['(m)', '(m)', '(m)', '(deg)', '(deg)', '(m)', '(-)']) + self.module='ADBlade' + + def _writeSanityChecks(self): + """ Sanity checks before write""" + self['NumBlNds']=self['BldAeroNodes'].shape[0] + aeroNodes = self['BldAeroNodes'] + # TODO double check this calculation with gradient + dr = np.gradient(aeroNodes[:,0]) + dx = np.gradient(aeroNodes[:,1]) + crvAng = np.degrees(np.arctan2(dx,dr)) + if np.mean(np.abs(crvAng-aeroNodes[:,3]))>0.1: + print('[WARN] BlCrvAng might not be computed correctly') + + def _toDataFrame(self): + df = FASTInputFileBase._toDataFrame(self) + aeroNodes = self['BldAeroNodes'] + r = aeroNodes[:,0] + chord = aeroNodes[:,5] + twist = aeroNodes[:,4]*np.pi/180 + prebendAC = aeroNodes[:,1] + sweepAC = aeroNodes[:,2] + + # --- IEA 15 + ##'le_location: 'Leading-edge positions from a reference blade axis (usually blade pitch axis). Locations are normalized by the local chord length. Positive in -x direction for airfoil-aligned coordinate system') + ## pitch_axis + ##'1D array of the chordwise position of the pitch axis (0-LE, 1-TE), defined along blade span.') + #grid = [0.0, 0.02040816326530612, 0.04081632653061224, 0.061224489795918366, 0.08163265306122448, 0.1020408163265306, 0.12244897959183673, 0.14285714285714285, 0.16326530612244897, 0.18367346938775508, 0.2040816326530612, 0.22448979591836732, 0.24489795918367346, 0.26530612244897955, 0.2857142857142857, 0.3061224489795918, 0.32653061224489793, 0.3469387755102041, 0.36734693877551017, 0.3877551020408163, 0.4081632653061224, 0.42857142857142855, 0.44897959183673464, 0.4693877551020408, 0.4897959183673469, 0.5102040816326531, 0.5306122448979591, 0.5510204081632653, 0.5714285714285714, 0.5918367346938775, 0.6122448979591836, 0.6326530612244897, 0.6530612244897959, 0.673469387755102, 0.6938775510204082, 0.7142857142857142, 0.7346938775510203, 0.7551020408163265, 0.7755102040816326, 0.7959183673469387, 0.8163265306122448, 0.836734693877551, 0.8571428571428571, 0.8775510204081632, 0.8979591836734693, 0.9183673469387754, 0.9387755102040816, 0.9591836734693877, 0.9795918367346939, 1.0] + #values = [0.5045454545454545, 0.4900186808012221, 0.47270018284548393, 0.4540147730610375, 0.434647782591965, 0.4156278851950606, 0.3979378721273935, 0.38129960745617403, 0.3654920515699109, 0.35160780834472827, 0.34008443128769117, 0.3310670675965599, 0.3241031342163746, 0.3188472934612394, 0.3146895762675238, 0.311488897995355, 0.3088429219529899, 0.3066054031112312, 0.3043613335231313, 0.3018756624023877, 0.2992017656131912, 0.29648581499532917, 0.29397119399704474, 0.2918571873240831, 0.2901098902886204, 0.28880659979944606, 0.28802634398115073, 0.28784151044623507, 0.28794253614539367, 0.28852264941156663, 0.28957685074559625, 0.2911108045758606, 0.2930139151081327, 0.2952412111444283, 0.2977841397364215, 0.300565286724993, 0.3035753776130124, 0.30670446458784534, 0.30988253764299156, 0.3130107259708016, 0.31639042766652853, 0.32021109189825026, 0.32462311714967124, 0.329454188784972, 0.33463306413024474, 0.3401190402144396, 0.3460555975714659, 0.3527211856428439, 0.3600890296396286, 0.36818181818181805] + ##'ref_axis_blade' desc='2D array of the coordinates (x,y,z) of the blade reference axis, defined along blade span. The coordinate system is the one of BeamDyn: it is placed at blade root with x pointing the suction side of the blade, y pointing the trailing edge and z along the blade span. A standard configuration will have negative x values (prebend), if swept positive y values, and positive z values.') + #x_grid = [0.0, 0.02040816326530612, 0.04081632653061224, 0.061224489795918366, 0.08163265306122448, 0.1020408163265306, 0.12244897959183673, 0.14285714285714285, 0.16326530612244897, 0.18367346938775508, 0.2040816326530612, 0.22448979591836732, 0.24489795918367346, 0.26530612244897955, 0.2857142857142857, 0.3061224489795918, 0.32653061224489793, 0.3469387755102041, 0.36734693877551017, 0.3877551020408163, 0.4081632653061224, 0.42857142857142855, 0.44897959183673464, 0.4693877551020408, 0.4897959183673469, 0.5102040816326531, 0.5306122448979591, 0.5510204081632653, 0.5714285714285714, 0.5918367346938775, 0.6122448979591836, 0.6326530612244897, 0.6530612244897959, 0.673469387755102, 0.6938775510204082, 0.7142857142857142, 0.7346938775510203, 0.7551020408163265, 0.7755102040816326, 0.7959183673469387, 0.8163265306122448, 0.836734693877551, 0.8571428571428571, 0.8775510204081632, 0.8979591836734693, 0.9183673469387754, 0.9387755102040816, 0.9591836734693877, 0.9795918367346939, 1.0] + #x_values = [0.0, 0.018400065266506227, 0.04225083661157623, 0.0713435070518306, 0.1036164118664373, 0.13698065932882636, 0.16947761902506267, 0.19850810716711273, 0.22314347791028566, 0.24053558565655847, 0.24886598803245524, 0.2502470372487695, 0.24941257744761433, 0.24756615214432298, 0.24481686563607896, 0.24130290560673967, 0.23698965095246982, 0.23242285078249267, 0.22531163517427788, 0.2110134548882222, 0.18623119147117725, 0.1479307251853749, 0.09847131457569316, 0.04111540547132665, -0.02233952894219675, -0.08884150619038655, -0.15891966620096387, -0.2407441175807782, -0.3366430472730907, -0.44693576549987823, -0.5680658106768092, -0.6975208703059096, -0.8321262196998409, -0.9699653368698024, -1.1090930486685822, -1.255144506570033, -1.4103667735456449, -1.5733007007462756, -1.7434963771088456, -1.9194542609028804, -2.1000907378795275, -2.285501961499942, -2.4756894577736315, -2.6734165188032692, -2.8782701025304545, -3.090085737186208, -3.308459127246535, -3.533712868740941, -3.7641269864926348, -4.0] + #y_grid = [0.0, 1.0] + #y_values = [0.0, 0.0] + #z_grid = [0.0, 0.02040816326530612, 0.04081632653061224, 0.061224489795918366, 0.08163265306122448, 0.1020408163265306, 0.12244897959183673, 0.14285714285714285, 0.16326530612244897, 0.18367346938775508, 0.2040816326530612, 0.22448979591836732, 0.24489795918367346, 0.26530612244897955, 0.2857142857142857, 0.3061224489795918, 0.32653061224489793, 0.3469387755102041, 0.36734693877551017, 0.3877551020408163, 0.4081632653061224, 0.42857142857142855, 0.44897959183673464, 0.4693877551020408, 0.4897959183673469, 0.5102040816326531, 0.5306122448979591, 0.5510204081632653, 0.5714285714285714, 0.5918367346938775, 0.6122448979591836, 0.6326530612244897, 0.6530612244897959, 0.673469387755102, 0.6938775510204082, 0.7142857142857142, 0.7346938775510203, 0.7551020408163265, 0.7755102040816326, 0.7959183673469387, 0.8163265306122448, 0.836734693877551, 0.8571428571428571, 0.8775510204081632, 0.8979591836734693, 0.9183673469387754, 0.9387755102040816, 0.9591836734693877, 0.9795918367346939, 1.0] + #z_values = [0.0, 2.387755102040816, 4.775510204081632, 7.163265306122448, 9.551020408163264, 11.938775510204081, 14.326530612244898, 16.714285714285715, 19.10204081632653, 21.489795918367346, 23.877551020408163, 26.265306122448976, 28.653061224489797, 31.04081632653061, 33.42857142857143, 35.81632653061224, 38.20408163265306, 40.59183673469388, 42.979591836734684, 45.36734693877551, 47.75510204081632, 50.14285714285714, 52.53061224489795, 54.91836734693877, 57.30612244897959, 59.69387755102041, 62.08163265306122, 64.46938775510203, 66.85714285714285, 69.24489795918367, 71.63265306122447, 74.0204081632653, 76.40816326530611, 78.79591836734693, 81.18367346938776, 83.57142857142857, 85.95918367346938, 88.3469387755102, 90.73469387755102, 93.12244897959182, 95.51020408163265, 97.89795918367345, 100.28571428571428, 102.6734693877551, 105.0612244897959, 107.44897959183673, 109.83673469387753, 112.22448979591836, 114.61224489795919, 117.0] + #r_ = [0.0, 0.02, 0.15, 0.245170, 1.0] + #ac = [0.5, 0.5, 0.316, 0.25, 0.25] + #r0 = r/r[-1] + #z = np.interp(r0, z_grid, z_values) + #x = np.interp(r0, x_grid, x_values) + #y = np.interp(r0, y_grid, y_values) + #xp = np.interp(r0, grid, values) + #df['z'] = z + #df['x'] = x + #df['y'] = y + #df['xp'] = xp + #ACloc = np.interp(r0, r_,ac) + + ## Get the absolute offset between pitch axis (rotation center) and aerodynamic center + #ch_offset = inputs['chord'] * (inputs['ac'] - inputs['le_location']) + ## Rotate it by the twist using the AD15 coordinate system + #x , y = util.rotate(0., 0., 0., ch_offset, -np.deg2rad(inputs['theta'])) + ## Apply offset to determine the AC axis + #BlCrvAC = inputs['ref_axis_blade'][:,0] + x + #BlSwpAC = inputs['ref_axis_blade'][:,1] + y + + # --- Adding C2 axis + ACloc = r*0 + 0.25 # distance (in chord) from leading edge to aero center + n=int(len(r)*0.15) # 15% span + ACloc[:n]=np.linspace(0.5,0.25, n) # Root is at 0 + + dx = chord*(0.5-ACloc) * np.sin(twist) # Should be mostly >0 + dy = chord*(0.5-ACloc) * np.cos(twist) # Should be mostly >0 + prebend = prebendAC + dx + sweep = sweepAC + dy + df['c2_Crv_Approx_[m]'] = prebend + df['c2_Swp_Approx_[m]'] = sweep + df['AC_Approx_[-]'] = ACloc + # --- Calc CvrAng + dr = np.gradient(aeroNodes[:,0]) + dx = np.gradient(aeroNodes[:,1]) + df['CrvAng_Calc_[-]'] = np.degrees(np.arctan2(dx,dr)) + return df + + @property + def _IComment(self): return [1] + + +# --------------------------------------------------------------------------------} +# --- AeroDyn Polar +# --------------------------------------------------------------------------------{ +class ADPolarFile(FASTInputFileBase): + @staticmethod + def formatName(): + return 'FAST AeroDyn polar file' + + @classmethod + def from_fast_input_file(cls, parent): + self = cls() + self.setData(filename=parent.filename, data=parent.data, hasNodal=parent.hasNodal, module='ADPolar') + return self + + def __init__(self, filename=None, hasUA=True, numTabs=1, **kwargs): + FASTInputFileBase.__init__(self, filename, **kwargs) + if filename is None: + # Define a prototype for this file format + self.addComment('! ------------ AirfoilInfo Input File ------------------------------------------') + self.addComment('! Airfoil definition, written by ADPolarFile') + self.addComment('! ') + self.addComment('! ') + self.addComment('! ------------------------------------------------------------------------------') + self.addValKey("DEFAULT", 'InterpOrd' , 'Interpolation order to use for quasi-steady table lookup {1=linear; 3=cubic spline; "default"} [default=3]') + self.addValKey( 1, 'NonDimArea', 'The non-dimensional area of the airfoil (area/chord^2) (set to 1.0 if unsure or unneeded)') + self.addValKey( 0, 'NumCoords' , 'The number of coordinates in the airfoil shape file. Set to zero if coordinates not included.') + self.addValKey( numTabs , 'NumTabs' , 'Number of airfoil tables in this file. Each table must have lines for Re and Ctrl.') + # TODO multiple tables + for iTab in range(numTabs): + if numTabs==1: + labOffset ='' + else: + labOffset ='_'+str(iTab+1) + self.addComment('! ------------------------------------------------------------------------------') + self.addComment('! data for table {}'.format(iTab+1)) + self.addComment('! ------------------------------------------------------------------------------') + self.addValKey( 1.0 , 'Re' +labOffset , 'Reynolds number in millions') + self.addValKey( 0 , 'Ctrl'+labOffset , 'Control setting') + if hasUA: + self.addValKey(True , 'InclUAdata', 'Is unsteady aerodynamics data included in this table? If TRUE, then include 30 UA coefficients below this line') + self.addComment('!........................................') + self.addValKey( np.nan , 'alpha0' + labOffset, r"0-lift angle of attack, depends on airfoil.") + self.addValKey( np.nan , 'alpha1' + labOffset, r"Angle of attack at f=0.7, (approximately the stall angle) for AOA>alpha0. (deg)") + self.addValKey( np.nan , 'alpha2' + labOffset, r"Angle of attack at f=0.7, (approximately the stall angle) for AOA1]") + self.addValKey( 0 , 'S2' + labOffset, r"Constant in the f curve best-fit for AOA> alpha1; by definition it depends on the airfoil. [ignored if UAMod<>1]") + self.addValKey( 0 , 'S3' + labOffset, r"Constant in the f curve best-fit for alpha2<=AOA< alpha0; by definition it depends on the airfoil. [ignored if UAMod<>1]") + self.addValKey( 0 , 'S4' + labOffset, r"Constant in the f curve best-fit for AOA< alpha2; by definition it depends on the airfoil. [ignored if UAMod<>1]") + self.addValKey( np.nan , 'Cn1' + labOffset, r"Critical value of C0n at leading edge separation. It should be extracted from airfoil data at a given Mach and Reynolds number. It can be calculated from the static value of Cn at either the break in the pitching moment or the loss of chord force at the onset of stall. It is close to the condition of maximum lift of the airfoil at low Mach numbers.") + self.addValKey( np.nan , 'Cn2' + labOffset, r"As Cn1 for negative AOAs.") + self.addValKey( "DEFAULT" , 'St_sh' + labOffset, r"Strouhal's shedding frequency constant. [default = 0.19]") + self.addValKey( np.nan , 'Cd0' + labOffset, r"2D drag coefficient value at 0-lift.") + self.addValKey( np.nan , 'Cm0' + labOffset, r"2D pitching moment coefficient about 1/4-chord location, at 0-lift, positive if nose up. [If the aerodynamics coefficients table does not include a column for Cm, this needs to be set to 0.0]") + self.addValKey( 0 , 'k0' + labOffset, r"Constant in the \hat(x)_cp curve best-fit; = (\hat(x)_AC-0.25). [ignored if UAMod<>1]") + self.addValKey( 0 , 'k1' + labOffset, r"Constant in the \hat(x)_cp curve best-fit. [ignored if UAMod<>1]") + self.addValKey( 0 , 'k2' + labOffset, r"Constant in the \hat(x)_cp curve best-fit. [ignored if UAMod<>1]") + self.addValKey( 0 , 'k3' + labOffset, r"Constant in the \hat(x)_cp curve best-fit. [ignored if UAMod<>1]") + self.addValKey( 0 , 'k1_hat' + labOffset, r"Constant in the expression of Cc due to leading edge vortex effects. [ignored if UAMod<>1]") + self.addValKey( "DEFAULT" , 'x_cp_bar' + labOffset, r"Constant in the expression of \hat(x)_cp^v. [ignored if UAMod<>1, default = 0.2]") + self.addValKey( "DEFAULT" , 'UACutout' + labOffset, r"Angle of attack above which unsteady aerodynamics are disabled (deg). [Specifying the string 'Default' sets UACutout to 45 degrees]") + self.addValKey( "DEFAULT" , 'filtCutOff'+ labOffset, r"Reduced frequency cut-off for low-pass filtering the AoA input to UA, as well as the 1st and 2nd derivatives (-) [default = 0.5]") + self.addComment('!........................................') + else: + self.addValKey(False , 'InclUAdata'+labOffset, 'Is unsteady aerodynamics data included in this table? If TRUE, then include 30 UA coefficients below this line') + self.addComment('! Table of aerodynamics coefficients') + self.addValKey(0 , 'NumAlf'+labOffset, '! Number of data lines in the following table') + self.addTable('AFCoeff'+labOffset, np.zeros((0,4)), tabType=2, tabDimVar='NumAlf', cols=['Alpha', 'Cl', 'Cd', 'Cm'], units=['(deg)', '(-)', '(-)', '(-)']) + self.module='ADPolar' + + def _writeSanityChecks(self): + """ Sanity checks before write""" + nTabs = self['NumTabs'] + if nTabs==1: + self['NumAlf']=self['AFCoeff'].shape[0] + else: + for iTab in range(nTabs): + labOffset='_{}'.format(iTab+1) + self['NumAlf'+labOffset] = self['AFCoeff'+labOffset].shape[0] + # Potentially compute unsteady params here + + def _write(self): + nTabs = self['NumTabs'] + if nTabs==1: + FASTInputFileBase._write(self) + else: + self._writeSanityChecks() + Labs=['Re','Ctrl','UserProp','alpha0','alpha1','alpha2','eta_e','C_nalpha','T_f0','T_V0','T_p','T_VL','b1','b2','b5','A1','A2','A5','S1','S2','S3','S4','Cn1','Cn2','St_sh','Cd0','Cm0','k0','k1','k2','k3','k1_hat','x_cp_bar','UACutout','filtCutOff','InclUAdata','NumAlf','AFCoeff'] + # Store all labels + AllLabels=[self.data[i]['label'] for i in range(len(self.data))] + # Removing lab Offset - TODO TEMPORARY HACK + for iTab in range(nTabs): + labOffset='_{}'.format(iTab+1) + for labRaw in Labs: + i = self.getIDSafe(labRaw+labOffset) + if i>0: + self.data[i]['label'] = labRaw + # Write + with open(self.filename,'w') as f: + f.write(self.toString()) + # Restore labels + for i,labFull in enumerate(AllLabels): + self.data[i]['label'] = labFull + + def _toDataFrame(self): + dfs = FASTInputFileBase._toDataFrame(self) + if not isinstance(dfs, dict): + dfs={'AFCoeff':dfs} + + for k,df in dfs.items(): + sp = k.split('_') + 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 + + # Cn with Cd0 + try: + Cd0 = self['Cd0'+labOffset] + # Cn (with or without Cd0) + Cn1 = Cl*np.cos(alpha)+ (Cd-Cd0)*np.sin(alpha) + df['Cn_Cd0off_[-]'] = Cn1 + except: + pass + + # Regular Cn + Cn = Cl*np.cos(alpha)+ Cd*np.sin(alpha) + df['Cn_[-]'] = Cn + + # Linear Cn + try: + CnLin_ = self['C_nalpha'+labOffset]*(alpha-self['alpha0'+labOffset]*np.pi/180.) + CnLin = CnLin_.copy() + CnLin[alpha<-20*np.pi/180]=np.nan + CnLin[alpha> 30*np.pi/180]=np.nan + df['Cn_pot_[-]'] = CnLin + except: + pass + + # Highlighting points surrounding 0 1 2 Cn points + CnPoints = Cn*np.nan + try: + iBef2 = np.where(alpha0: + if l[0]=='!': + if l.find('!dimension')==0: + self.addKeyVal('nDOF',int(l.split(':')[1])) + nDOFCommon=self['nDOF'] + elif l.find('!time increment')==0: + self.addKeyVal('dt',float(l.split(':')[1])) + elif l.find('!total simulation time')==0: + self.addKeyVal('T',float(l.split(':')[1])) + elif len(l.strip())==0: + pass + else: + raise BrokenFormatError('Unexcepted content found on line {}'.format(i)) + i+=1 + except BrokenFormatError as e: + raise e + except: + raise + + return True + +class ExtPtfmFile(FASTInputFileBase): + @classmethod + def from_fast_input_file(cls, parent): + self = cls() + self.setData(filename=parent.filename, data=parent.data, hasNodal=parent.hasNodal, module='ExtPtfm') + return self + + def __init__(self, filename=None, **kwargs): + FASTInputFileBase.__init__(self, filename, **kwargs) + if filename is None: + # Define a prototype for this file format + self.addValKey(0 , 'nDOF', '') + self.addValKey(1 , 'dt' , '') + self.addValKey(0 , 'T' , '') + self.addTable('MassMatrix' , np.zeros((0,0)), tabType=0) + self.addTable('StiffnessMatrix', np.zeros((0,0)), tabType=0) + self.addTable('DampingMatrix' , np.zeros((0,0)), tabType=0) + self.addTable('Loading' , np.zeros((0,0)), tabType=0) + self.comment='' + self.module='ExtPtfm' + + + def _read(self): + with open(self.filename, 'r', errors="surrogateescape") as f: + lines=f.read().splitlines() + detectAndReadExtPtfmSE(self, lines) + + def toString(self): + s='' + s+='!Comment\n' + s+='!Comment Flex 5 Format\n' + s+='!Dimension: {}\n'.format(self['nDOF']) + s+='!Time increment in simulation: {}\n'.format(self['dt']) + s+='!Total simulation time in file: {}\n'.format(self['T']) + + s+='\n!Mass Matrix\n' + s+='!Dimension: {}\n'.format(self['nDOF']) + s+='\n'.join(''.join('{:16.8e}'.format(x) for x in y) for y in self['MassMatrix']) + + s+='\n\n!Stiffness Matrix\n' + s+='!Dimension: {}\n'.format(self['nDOF']) + s+='\n'.join(''.join('{:16.8e}'.format(x) for x in y) for y in self['StiffnessMatrix']) + + s+='\n\n!Damping Matrix\n' + s+='!Dimension: {}\n'.format(self['nDOF']) + s+='\n'.join(''.join('{:16.8e}'.format(x) for x in y) for y in self['DampingMatrix']) + + s+='\n\n!Loading and Wave Elevation\n' + s+='!Dimension: 1 time column - {} force columns\n'.format(self['nDOF']) + s+='\n'.join(''.join('{:16.8e}'.format(x) for x in y) for y in self['Loading']) + return s + + def _writeSanityChecks(self): + """ Sanity checks before write""" + assert self['MassMatrix'].shape[0] == self['nDOF'] + assert self['StiffnessMatrix'].shape[0] == self['nDOF'] + assert self['DampingMatrix'].shape[0] == self['nDOF'] + assert self['MassMatrix'].shape[0] == self['MassMatrix'].shape[1] + assert self['StiffnessMatrix'].shape[0] == self['StiffnessMatrix'].shape[1] + assert self['DampingMatrix'].shape[0] == self['DampingMatrix'].shape[1] + # if self['T']>0: + # assert self['Loading'].shape[0] == (int(self['T']/self['dT'])+1 + + def _toDataFrame(self): + # Special types, TODO Subclass + nDOF=self['nDOF'] + Cols=['Time_[s]','InpF_Fx_[N]', 'InpF_Fy_[N]', 'InpF_Fz_[N]', 'InpF_Mx_[Nm]', 'InpF_My_[Nm]', 'InpF_Mz_[Nm]'] + Cols+=['CBF_{:03d}_[-]'.format(iDOF+1) for iDOF in np.arange(nDOF)] + Cols=Cols[:nDOF+1] + #dfs['Loading'] = pd.DataFrame(data = self['Loading'],columns = Cols) + dfs = pd.DataFrame(data = self['Loading'],columns = Cols) + + #Cols=['SurgeAcc_[m/s]', 'SwayAcc_[m/s]', 'HeaveAcc_[m/s]', 'RollAcc_[rad/s]', 'PitchAcc_[rad/s]', 'YawAcc_[rad/s]'] + #Cols+=['CBQD_{:03d}_[-]'.format(iDOF+1) for iDOF in np.arange(nDOF)] + #Cols=Cols[:nDOF] + #dfs['MassMatrix'] = pd.DataFrame(data = self['MassMatrix'], columns=Cols) + + #Cols=['SurgeVel_[m/s]', 'SwayVel_[m/s]', 'HeaveVel_[m/s]', 'RollVel_[rad/s]', 'PitchVel_[rad/s]', 'YawVel_[rad/s]'] + #Cols+=['CBQD_{:03d}_[-]'.format(iDOF+1) for iDOF in np.arange(nDOF)] + #Cols=Cols[:nDOF] + #dfs['DampingMatrix'] = pd.DataFrame(data = self['DampingMatrix'], columns=Cols) + + #Cols=['Surge_[m]', 'Sway_[m]', 'Heave_[m]', 'Roll_[rad]', 'Pitch_[rad]', 'Yaw_[rad]'] + #Cols+=['CBQ_{:03d}_[-]'.format(iDOF+1) for iDOF in np.arange(nDOF)] + #Cols=Cols[:nDOF] + #dfs['StiffnessMatrix'] = pd.DataFrame(data = self['StiffnessMatrix'], columns=Cols) + return dfs + + + +if __name__ == "__main__": + f = FASTInputFile('tests/example_files/FASTIn_HD_SeaState.dat') + print(f) + pass + #B=FASTIn('Turbine.outb') + + + diff --git a/pyFAST/input_output/fast_input_file_graph.py b/openfast_toolbox/io/fast_input_file_graph.py similarity index 99% rename from pyFAST/input_output/fast_input_file_graph.py rename to openfast_toolbox/io/fast_input_file_graph.py index f38708e..9d2c848 100644 --- a/pyFAST/input_output/fast_input_file_graph.py +++ b/openfast_toolbox/io/fast_input_file_graph.py @@ -4,7 +4,7 @@ # Local # try: -from pyFAST.input_output.tools.graph import * +from openfast_toolbox.io.tools.graph import * # except ImportError: #from welib.FEM.graph import * diff --git a/pyFAST/input_output/fast_linearization_file.py b/openfast_toolbox/io/fast_linearization_file.py similarity index 100% rename from pyFAST/input_output/fast_linearization_file.py rename to openfast_toolbox/io/fast_linearization_file.py diff --git a/pyFAST/input_output/fast_output_file.py b/openfast_toolbox/io/fast_output_file.py similarity index 100% rename from pyFAST/input_output/fast_output_file.py rename to openfast_toolbox/io/fast_output_file.py diff --git a/pyFAST/input_output/fast_summary_file.py b/openfast_toolbox/io/fast_summary_file.py similarity index 100% rename from pyFAST/input_output/fast_summary_file.py rename to openfast_toolbox/io/fast_summary_file.py diff --git a/pyFAST/input_output/fast_wind_file.py b/openfast_toolbox/io/fast_wind_file.py similarity index 97% rename from pyFAST/input_output/fast_wind_file.py rename to openfast_toolbox/io/fast_wind_file.py index 7c46c6e..e2a75c5 100644 --- a/pyFAST/input_output/fast_wind_file.py +++ b/openfast_toolbox/io/fast_wind_file.py @@ -1,72 +1,72 @@ -import numpy as np -import pandas as pd -from .csv_file import CSVFile -from .file import isBinary, WrongFormatError - -class FASTWndFile(CSVFile): - - @staticmethod - def defaultExtensions(): - return ['.wnd'] - - @staticmethod - def formatName(): - return 'FAST determ. wind file' - - def __init__(self, *args, **kwargs): - self.colNames=['Time','WindSpeed','WindDir','VertSpeed','HorizShear','VertShear','LinVShear','GustSpeed'] - self.units=['[s]','[m/s]','[deg]','[m/s]','[-]','[-]','[-]','[m/s]'] - Cols=['{}_{}'.format(c,u) for c,u in zip(self.colNames,self.units)] - - header=[] - header+=['!Wind file.'] - header+=['!Time Wind Wind Vert. Horiz. Vert. LinV Gust'] - header+=['! Speed Dir Speed Shear Shear Shear Speed'] - - super(FASTWndFile, self).__init__(sep=' ',commentChar='!',colNames=Cols, header=header, *args, **kwargs) - - def _read(self, *args, **kwargs): - if isBinary(self.filename): - raise WrongFormatError('This is a binary file (turbulence file?) not a FAST ascii determinisctic wind file') - super(FASTWndFile, self)._read(*args, **kwargs) - - def _write(self, *args, **kwargs): - super(FASTWndFile, self)._write(*args, **kwargs) - - - def _toDataFrame(self): - return self.data - - -# --------------------------------------------------------------------------------} -# --- Functions specific to file type -# --------------------------------------------------------------------------------{ - def stepWind(self,WSstep=1,WSmin=3,WSmax=25,tstep=100,dt=0.5,tmin=0,tmax=999): - """ Set the wind file to a step wind - tstep: can be an array of size 2 [tstepmax tstepmin] - - - """ - - Steps= np.arange(WSmin,WSmax+WSstep,WSstep) - if hasattr(tstep,'__len__'): - tstep = np.around(np.linspace(tstep[0], tstep[1], len(Steps)),0) - else: - tstep = len(Steps)*[tstep] - nCol = len(self.colNames) - nRow = len(Steps)*2 - M = np.zeros((nRow,nCol)); - M[0,0] = tmin - M[0,1] = WSmin - for i,s in enumerate(Steps[:-1]): - M[2*i+1,0] = tmin + tstep[i]-dt - M[2*i+2,0] = tmin + tstep[i] - tmin +=tstep[i] - M[2*i+1,1] = Steps[i] - if i14 and code<31): - return True - return False - except UnicodeDecodeError: - return True - - -def numberOfLines(filename, method=1): - - if method==1: - return sum(1 for i in open(filename, 'rb')) - - elif method==2: - def blocks(files, size=65536): - while True: - b = files.read(size) - if not b: break - yield b - with open(filename, "r",encoding="utf-8",errors='ignore') as f: - return sum(bl.count("\n") for bl in blocks(f)) - else: - raise NotImplementedError() - -def ascii_comp(file1,file2,bDelete=False): - """ Compares two ascii files line by line. - Comparison is done ignoring multiple white spaces for now""" - # --- Read original as ascii - with open(file1, 'r') as f1: - lines1 = f1.read().splitlines(); - lines1 = '|'.join([l.replace('\t',' ').strip() for l in lines1]) - lines1 = ' '.join(lines1.split()) - # --- Read second file as ascii - with open(file2, 'r') as f2: - lines2 = f2.read().splitlines(); - lines2 = '|'.join([l.replace('\t',' ').strip() for l in lines2]) - lines2 = ' '.join(lines2.split()) - - if lines1 == lines2: - if bDelete: - os.remove(file2) - return True - else: - return False +import os +from collections import OrderedDict + +class WrongFormatError(Exception): + pass + +class EmptyFileError(Exception): + pass + +class BrokenFormatError(Exception): + pass + +class BrokenReaderError(Exception): + pass + +class OptionalImportError(Exception): + pass + +try: #Python3 + FileNotFoundError=FileNotFoundError +except NameError: # Python2 + FileNotFoundError = IOError + +class File(OrderedDict): + def __init__(self,filename=None,**kwargs): + if filename: + self.read(filename, **kwargs) + else: + self.filename = None + + def read(self, filename=None, **kwargs): + if filename: + self.filename = filename + if not self.filename: + raise Exception('No filename provided') + if not os.path.isfile(self.filename): + raise OSError(2,'File not found:',self.filename) + if os.stat(self.filename).st_size == 0: + raise EmptyFileError('File is empty:',self.filename) + # Calling children function + self._read(**kwargs) + + def write(self, filename=None, **kwargs): + if filename: + self.filename = filename + if not self.filename: + raise Exception('No filename provided') + # Calling children function + self._write(**kwargs) + + def toDataFrame(self): + return self._toDataFrame() + + # -------------------------------------------------------------------------------- + # --- Properties + # -------------------------------------------------------------------------------- + @property + def size(self): + return os.path.getsize(self.filename) + + @property + def encoding(self): + import codecs + import chardet + """ Detects encoding""" + try: + byts = min(32, self.size) + except TypeError: + return None + with open(self.filename, 'rb') as f: + raw = f.read(byts) + if raw.startswith(codecs.BOM_UTF8): + return 'utf-8-sig' + else: + result = chardet.detect(raw) + return result['encoding'] + + + # --------------------------------------------------------------------------------} + # --- Conversions + # --------------------------------------------------------------------------------{ + def toCSV(self, filename=None, extension='.csv', **kwargs): + """ By default, writes dataframes to CSV + Keywords arguments are the same as pandas DataFrame to_csv: + - sep: separator + - index: write index or not + - etc. + """ + from .converters import writeFileDataFrames + from .converters import dataFrameToCSV + writeFileDataFrames(self, dataFrameToCSV, filename=filename, extension=extension, **kwargs) + + def toOUTB(self, filename=None, extension='.outb', **kwargs): + """ write to OUTB""" + from .converters import writeFileDataFrames + from .converters import dataFrameToOUTB + writeFileDataFrames(self, dataFrameToOUTB, filename=filename, extension=extension, **kwargs) + + + def toParquet(self, filename=None, extension='.parquet', **kwargs): + """ write to OUTB""" + from .converters import writeFileDataFrames + from .converters import dataFrameToParquet + writeFileDataFrames(self, dataFrameToParquet, filename=filename, extension=extension, **kwargs) + + + # -------------------------------------------------------------------------------- + # --- Sub class methods + # -------------------------------------------------------------------------------- + def _read(self,**kwargs): + raise NotImplementedError("Method must be implemented in the subclass") + + def _write(self): + raise NotImplementedError("Method must be implemented in the subclass") + + def _toDataFrame(self): + raise NotImplementedError("Method must be implemented in the subclass") + + def _fromDataFrame(self): + raise NotImplementedError("Method must be implemented in the subclass") + + def _fromDictionary(self): + raise NotImplementedError("Method must be implemented in the subclass") + + def _fromFile(self): + raise NotImplementedError("Method must be implemented in the subclass") + + # -------------------------------------------------------------------------------- + # --- Static methods + # -------------------------------------------------------------------------------- + @staticmethod + def defaultExtension(): + raise NotImplementedError("Method must be implemented in the subclass") + + @staticmethod + def formatName(): + raise NotImplementedError("Method must be implemented in the subclass") + + def test_write_read(self,bDelete=False): + """ Test that we can write and then read what we wrote + NOTE: this does not check that what we read is the same.. + """ + # --- First, test write function (assuming read) + try: + f,ext=os.path.splitext(self.filename) + filename_out = f+'_TMP'+ext + self.write(filename_out) + except Exception as e: + raise Exception('Error writing what we read\n'+e.args[0]) + # --- Second, re-read what we wrote + try: + self.read(filename_out) + except Exception as e: + raise Exception('Error reading what we wrote\n'+e.args[0]) + if bDelete: + os.remove(filename_out) + return filename_out + + def test_ascii(self,bCompareWritesOnly=False,bDelete=True): + # compare ourselves (assuming read has occured) with what we write + + f,ext=os.path.splitext(self.filename) + # --- Perform a simple write/read test + filename_out=self.test_write_read() + + # --- Perform ascii comparison (and delete if success) + if bCompareWritesOnly: + f1 = filename_out + f2 = f+'_TMP2'+ext + self.write(f2) + else: + f1 = f+ext + f2 = filename_out + bStat=ascii_comp(f1,f2,bDelete=bDelete) + + if bStat: + if bCompareWritesOnly and bDelete: + os.remove(f1) + else: + raise Exception('The ascii content of {} and {} are different'.format(f1,f2)) + +# --------------------------------------------------------------------------------} +# --- Helper functions +# --------------------------------------------------------------------------------{ +def isBinary(filename): + with open(filename, 'r') as f: + try: + # first try to read as string + l = f.readline() + # then look for weird characters + for c in l: + code = ord(c) + if code<10 or (code>14 and code<31): + return True + return False + except UnicodeDecodeError: + return True + + +def numberOfLines(filename, method=1): + + if method==1: + return sum(1 for i in open(filename, 'rb')) + + elif method==2: + def blocks(files, size=65536): + while True: + b = files.read(size) + if not b: break + yield b + with open(filename, "r",encoding="utf-8",errors='ignore') as f: + return sum(bl.count("\n") for bl in blocks(f)) + else: + raise NotImplementedError() + +def ascii_comp(file1,file2,bDelete=False): + """ Compares two ascii files line by line. + Comparison is done ignoring multiple white spaces for now""" + # --- Read original as ascii + with open(file1, 'r') as f1: + lines1 = f1.read().splitlines(); + lines1 = '|'.join([l.replace('\t',' ').strip() for l in lines1]) + lines1 = ' '.join(lines1.split()) + # --- Read second file as ascii + with open(file2, 'r') as f2: + lines2 = f2.read().splitlines(); + lines2 = '|'.join([l.replace('\t',' ').strip() for l in lines2]) + lines2 = ' '.join(lines2.split()) + + if lines1 == lines2: + if bDelete: + os.remove(file2) + return True + else: + return False diff --git a/pyFAST/input_output/file_formats.py b/openfast_toolbox/io/file_formats.py similarity index 100% rename from pyFAST/input_output/file_formats.py rename to openfast_toolbox/io/file_formats.py diff --git a/pyFAST/input_output/flex_blade_file.py b/openfast_toolbox/io/flex_blade_file.py similarity index 100% rename from pyFAST/input_output/flex_blade_file.py rename to openfast_toolbox/io/flex_blade_file.py diff --git a/pyFAST/input_output/flex_doc_file.py b/openfast_toolbox/io/flex_doc_file.py similarity index 100% rename from pyFAST/input_output/flex_doc_file.py rename to openfast_toolbox/io/flex_doc_file.py diff --git a/pyFAST/input_output/flex_out_file.py b/openfast_toolbox/io/flex_out_file.py similarity index 100% rename from pyFAST/input_output/flex_out_file.py rename to openfast_toolbox/io/flex_out_file.py diff --git a/pyFAST/input_output/flex_profile_file.py b/openfast_toolbox/io/flex_profile_file.py similarity index 100% rename from pyFAST/input_output/flex_profile_file.py rename to openfast_toolbox/io/flex_profile_file.py diff --git a/pyFAST/input_output/flex_wavekin_file.py b/openfast_toolbox/io/flex_wavekin_file.py similarity index 100% rename from pyFAST/input_output/flex_wavekin_file.py rename to openfast_toolbox/io/flex_wavekin_file.py diff --git a/pyFAST/input_output/hawc2_ae_file.py b/openfast_toolbox/io/hawc2_ae_file.py similarity index 100% rename from pyFAST/input_output/hawc2_ae_file.py rename to openfast_toolbox/io/hawc2_ae_file.py diff --git a/pyFAST/input_output/hawc2_dat_file.py b/openfast_toolbox/io/hawc2_dat_file.py similarity index 100% rename from pyFAST/input_output/hawc2_dat_file.py rename to openfast_toolbox/io/hawc2_dat_file.py diff --git a/pyFAST/input_output/hawc2_htc_file.py b/openfast_toolbox/io/hawc2_htc_file.py similarity index 100% rename from pyFAST/input_output/hawc2_htc_file.py rename to openfast_toolbox/io/hawc2_htc_file.py diff --git a/pyFAST/input_output/hawc2_pc_file.py b/openfast_toolbox/io/hawc2_pc_file.py similarity index 100% rename from pyFAST/input_output/hawc2_pc_file.py rename to openfast_toolbox/io/hawc2_pc_file.py diff --git a/pyFAST/input_output/hawc2_st_file.py b/openfast_toolbox/io/hawc2_st_file.py similarity index 100% rename from pyFAST/input_output/hawc2_st_file.py rename to openfast_toolbox/io/hawc2_st_file.py diff --git a/pyFAST/input_output/hawcstab2_cmb_file.py b/openfast_toolbox/io/hawcstab2_cmb_file.py similarity index 100% rename from pyFAST/input_output/hawcstab2_cmb_file.py rename to openfast_toolbox/io/hawcstab2_cmb_file.py diff --git a/pyFAST/input_output/hawcstab2_ind_file.py b/openfast_toolbox/io/hawcstab2_ind_file.py similarity index 100% rename from pyFAST/input_output/hawcstab2_ind_file.py rename to openfast_toolbox/io/hawcstab2_ind_file.py diff --git a/pyFAST/input_output/hawcstab2_pwr_file.py b/openfast_toolbox/io/hawcstab2_pwr_file.py similarity index 100% rename from pyFAST/input_output/hawcstab2_pwr_file.py rename to openfast_toolbox/io/hawcstab2_pwr_file.py diff --git a/pyFAST/input_output/mannbox_file.py b/openfast_toolbox/io/mannbox_file.py similarity index 100% rename from pyFAST/input_output/mannbox_file.py rename to openfast_toolbox/io/mannbox_file.py diff --git a/pyFAST/input_output/mannbox_input_file.py b/openfast_toolbox/io/mannbox_input_file.py similarity index 100% rename from pyFAST/input_output/mannbox_input_file.py rename to openfast_toolbox/io/mannbox_input_file.py diff --git a/pyFAST/input_output/matlabmat_file.py b/openfast_toolbox/io/matlabmat_file.py similarity index 100% rename from pyFAST/input_output/matlabmat_file.py rename to openfast_toolbox/io/matlabmat_file.py diff --git a/pyFAST/input_output/mini_yaml.py b/openfast_toolbox/io/mini_yaml.py similarity index 100% rename from pyFAST/input_output/mini_yaml.py rename to openfast_toolbox/io/mini_yaml.py diff --git a/pyFAST/input_output/netcdf_file.py b/openfast_toolbox/io/netcdf_file.py similarity index 100% rename from pyFAST/input_output/netcdf_file.py rename to openfast_toolbox/io/netcdf_file.py diff --git a/pyFAST/input_output/parquet_file.py b/openfast_toolbox/io/parquet_file.py similarity index 100% rename from pyFAST/input_output/parquet_file.py rename to openfast_toolbox/io/parquet_file.py diff --git a/pyFAST/input_output/pickle_file.py b/openfast_toolbox/io/pickle_file.py similarity index 100% rename from pyFAST/input_output/pickle_file.py rename to openfast_toolbox/io/pickle_file.py diff --git a/pyFAST/input_output/raawmat_file.py b/openfast_toolbox/io/raawmat_file.py similarity index 97% rename from pyFAST/input_output/raawmat_file.py rename to openfast_toolbox/io/raawmat_file.py index c2e832d..fc5661e 100644 --- a/pyFAST/input_output/raawmat_file.py +++ b/openfast_toolbox/io/raawmat_file.py @@ -1,211 +1,211 @@ -# imports -import pandas as pd -import numpy as np -import os - - -def matfile(path_file,output='pandas',path_key=None): - ''' Function to parse RAAW .mat historical files with specific data structure - - Parameter - --------- - path_file : str - File path to .mat file to read - output : str - Desired output format of 'pandas' or 'xarray' - path_key : str (optional) - File path to .csv channel_key for extra checks and consistent formatting - - Return - ------ - ds : dict - Dict with keys 'data50hz' and 'data1hz' with specified output format - ''' - from scipy.io import loadmat - - # load in the mat file - mfile = loadmat(path_file) - - # flatten the mat file - data = [[row.flat[0] for row in line] for line in mfile['DataCell']] - - # create time column for easy comparison to simulations (0-600 seconds) - time50hz = np.arange(600,step=0.02) - time1hz = np.arange(600,step=1) - names_50hz = ['time'] - names_1hz = ['time'] - dat_50hz = [time50hz] - dat_1hz = [time1hz] - # start data reorganization into lists to be converted into pandas - allnames = [] - for signal in range(mfile['DataCell'].size): - name = data[signal][0][1][0] # get signal name - allnames.append(name) - if data[signal][0][3][0][0] == 0.02: - names_50hz.append(name) - dat_50hz.append(data[signal][0][6].squeeze()) - elif data[signal][0][3][0][0] == 1: - names_1hz.append(name) - dat_1hz.append(data[signal][0][6].squeeze()) - else: - print('ERROR: '+name+' ignored!') - - # create time index for each dataframe - tstamp = np.array2string(data[signal][0][5][0], separator=',') - tstamp = tstamp.replace(' ','').replace('[','').replace(']','') - t0 = pd.to_datetime(tstamp,format='%Y,%m,%d,%H,%M,%S') - tstamp50hz = pd.date_range(start=t0,periods=30000,freq='20ms') - tstamp1hz = pd.date_range(start=t0,periods=600,freq='1s') - - if not path_key==None: - # load in list of channels - channel_key = pd.read_csv(path_key) - list_50hz = channel_key['50hz'].to_list() - list_1hz = channel_key['1hz'].dropna().to_list() - # check if file size matches channel list size - if mfile['DataCell'].size > (len(list_50hz)+len(list_1hz)): - print('WARNING: channel size in file is greater than size of channel names lists!') - # find any missing channels from file - missing50hz = list(set(list_50hz)-set(names_50hz)) - missing1hz = list(set(list_1hz)-set(names_1hz)) - - if output == 'xarray': - import xarray as xr - # add missing columns filled with NaNs - nan50 = np.empty(30000) - nan50[:] = np.nan - nan1 = np.empty(600) - nan1[:] = np.nan - try: - if missing50hz: - for x in missing50hz: dat_50hz.append(nan50) - names_50hz.append(missing50hz) - if missing1hz: - for y in missing1hz: dat_1hz.append(nan1) - names_1hz.append(missing1hz) - except: - pass - # convert lists into xarray - df50hz = xr.DataArray(np.transpose(dat_50hz), dims=('timestamp','channel'),coords={'timestamp':tstamp50hz,'channel':names_50hz}) - df1hz = xr.DataArray(np.transpose(dat_1hz), dims=('timestamp','channel'),coords={'timestamp':tstamp1hz,'channel':names_1hz}) - # store in dict - ds = {'data50hz': df50hz, 'data1hz': df1hz} - return ds - - if output == 'pandas': - # convert lists into pandas - df_50hz = pd.DataFrame(dat_50hz).T - df_50hz.columns = names_50hz - df_1hz = pd.DataFrame(dat_1hz).T - df_1hz.columns = names_1hz - df_50hz.index = tstamp50hz - df_1hz.index = tstamp1hz - try: - # add columns with missing channels filled with NaNs - for x in missing50hz: df_50hz[x] = np.nan - for y in missing1hz: df_1hz[y] = np.nan - except: - pass - # store in dict - ds = {'data50hz': df_50hz, 'data1hz': df_1hz} - return ds - -try: - from .file import File, WrongFormatError, BrokenFormatError -except: - EmptyFileError = type('EmptyFileError', (Exception,),{}) - WrongFormatError = type('WrongFormatError', (Exception,),{}) - BrokenFormatError = type('BrokenFormatError', (Exception,),{}) - File=dict - -class RAAWMatFile(File): - """ - Read a RAAW .mat file. The object behaves as a dictionary. - - Main methods - ------------ - - read, toDataFrame, keys - - Examples - -------- - f = RAAWMatFile('file.mat') - print(f.keys()) - print(f.toDataFrame().columns) - - """ - - @staticmethod - def defaultExtensions(): - """ List of file extensions expected for this fileformat""" - return ['.mat'] - - @staticmethod - def formatName(): - """ Short string (~100 char) identifying the file format""" - return 'RAAW .mat file' - - def __init__(self, filename=None, **kwargs): - """ Class constructor. If a `filename` is given, the file is read. """ - self.filename = filename - if filename: - self.read(**kwargs) - - def read(self, filename=None, **kwargs): - """ Reads the file self.filename, or `filename` if provided """ - - # --- Standard tests and exceptions (generic code) - if filename: - self.filename = filename - if not self.filename: - raise Exception('No filename provided') - if not os.path.isfile(self.filename): - raise OSError(2,'File not found:',self.filename) - if os.stat(self.filename).st_size == 0: - raise EmptyFileError('File is empty:',self.filename) - # --- Calling (children) function to read - self._read(**kwargs) - - def write(self, filename=None): - """ Rewrite object to file, or write object to `filename` if provided """ - if filename: - self.filename = filename - if not self.filename: - raise Exception('No filename provided') - # Calling (children) function to write - self._write() - - def _read(self): - """ Reads self.filename and stores data into self. Self is (or behaves like) a dictionary""" - self['data'] = matfile(self.filename,output='pandas') - - def _write(self): - """ Writes to self.filename""" - # --- Example: - #with open(self.filename,'w') as f: - # f.write(self.toString) - raise NotImplementedError() - - def toDataFrame(self): - """ Returns object into one DataFrame, or a dictionary of DataFrames""" - return self['data'] - - # --- Optional functions - def __repr__(self): - """ String that is written to screen when the user calls `print()` on the object. - Provide short and relevant information to save time for the user. - """ - s='<{} object>:\n'.format(type(self).__name__) - s+='|Main attributes:\n' - s+='| - filename: {}\n'.format(self.filename) - # --- Example printing some relevant information for user - #s+='|Main keys:\n' - #s+='| - ID: {}\n'.format(self['ID']) - #s+='| - data : shape {}\n'.format(self['data'].shape) - s+='|Main methods:\n' - s+='| - read, toDataFrame, keys\n' - s+='|Info:\n' - d1=self['data']['data1hz'] - d5=self['data']['data50hz'] - s+='| - data1hz : {} to {} (n:{}, T:{}, dt:{})\n'.format(d1.index[0], d1.index[-1], len(d1), (d1.index[-1]-d1.index[0]).total_seconds(), (d1.index[1]-d1.index[0]).total_seconds()) - s+='| - data50hz: {} to {} (n:{}, T:{}, dt:{})\n'.format(d5.index[0], d5.index[-1], len(d5), (d5.index[-1]-d5.index[0]).total_seconds(), (d5.index[1]-d5.index[0]).total_seconds()) - return s +# imports +import pandas as pd +import numpy as np +import os + + +def matfile(path_file,output='pandas',path_key=None): + ''' Function to parse RAAW .mat historical files with specific data structure + + Parameter + --------- + path_file : str + File path to .mat file to read + output : str + Desired output format of 'pandas' or 'xarray' + path_key : str (optional) + File path to .csv channel_key for extra checks and consistent formatting + + Return + ------ + ds : dict + Dict with keys 'data50hz' and 'data1hz' with specified output format + ''' + from scipy.io import loadmat + + # load in the mat file + mfile = loadmat(path_file) + + # flatten the mat file + data = [[row.flat[0] for row in line] for line in mfile['DataCell']] + + # create time column for easy comparison to simulations (0-600 seconds) + time50hz = np.arange(600,step=0.02) + time1hz = np.arange(600,step=1) + names_50hz = ['time'] + names_1hz = ['time'] + dat_50hz = [time50hz] + dat_1hz = [time1hz] + # start data reorganization into lists to be converted into pandas + allnames = [] + for signal in range(mfile['DataCell'].size): + name = data[signal][0][1][0] # get signal name + allnames.append(name) + if data[signal][0][3][0][0] == 0.02: + names_50hz.append(name) + dat_50hz.append(data[signal][0][6].squeeze()) + elif data[signal][0][3][0][0] == 1: + names_1hz.append(name) + dat_1hz.append(data[signal][0][6].squeeze()) + else: + print('ERROR: '+name+' ignored!') + + # create time index for each dataframe + tstamp = np.array2string(data[signal][0][5][0], separator=',') + tstamp = tstamp.replace(' ','').replace('[','').replace(']','') + t0 = pd.to_datetime(tstamp,format='%Y,%m,%d,%H,%M,%S') + tstamp50hz = pd.date_range(start=t0,periods=30000,freq='20ms') + tstamp1hz = pd.date_range(start=t0,periods=600,freq='1s') + + if not path_key==None: + # load in list of channels + channel_key = pd.read_csv(path_key) + list_50hz = channel_key['50hz'].to_list() + list_1hz = channel_key['1hz'].dropna().to_list() + # check if file size matches channel list size + if mfile['DataCell'].size > (len(list_50hz)+len(list_1hz)): + print('WARNING: channel size in file is greater than size of channel names lists!') + # find any missing channels from file + missing50hz = list(set(list_50hz)-set(names_50hz)) + missing1hz = list(set(list_1hz)-set(names_1hz)) + + if output == 'xarray': + import xarray as xr + # add missing columns filled with NaNs + nan50 = np.empty(30000) + nan50[:] = np.nan + nan1 = np.empty(600) + nan1[:] = np.nan + try: + if missing50hz: + for x in missing50hz: dat_50hz.append(nan50) + names_50hz.append(missing50hz) + if missing1hz: + for y in missing1hz: dat_1hz.append(nan1) + names_1hz.append(missing1hz) + except: + pass + # convert lists into xarray + df50hz = xr.DataArray(np.transpose(dat_50hz), dims=('timestamp','channel'),coords={'timestamp':tstamp50hz,'channel':names_50hz}) + df1hz = xr.DataArray(np.transpose(dat_1hz), dims=('timestamp','channel'),coords={'timestamp':tstamp1hz,'channel':names_1hz}) + # store in dict + ds = {'data50hz': df50hz, 'data1hz': df1hz} + return ds + + if output == 'pandas': + # convert lists into pandas + df_50hz = pd.DataFrame(dat_50hz).T + df_50hz.columns = names_50hz + df_1hz = pd.DataFrame(dat_1hz).T + df_1hz.columns = names_1hz + df_50hz.index = tstamp50hz + df_1hz.index = tstamp1hz + try: + # add columns with missing channels filled with NaNs + for x in missing50hz: df_50hz[x] = np.nan + for y in missing1hz: df_1hz[y] = np.nan + except: + pass + # store in dict + ds = {'data50hz': df_50hz, 'data1hz': df_1hz} + return ds + +try: + from .file import File, WrongFormatError, BrokenFormatError +except: + EmptyFileError = type('EmptyFileError', (Exception,),{}) + WrongFormatError = type('WrongFormatError', (Exception,),{}) + BrokenFormatError = type('BrokenFormatError', (Exception,),{}) + File=dict + +class RAAWMatFile(File): + """ + Read a RAAW .mat file. The object behaves as a dictionary. + + Main methods + ------------ + - read, toDataFrame, keys + + Examples + -------- + f = RAAWMatFile('file.mat') + print(f.keys()) + print(f.toDataFrame().columns) + + """ + + @staticmethod + def defaultExtensions(): + """ List of file extensions expected for this fileformat""" + return ['.mat'] + + @staticmethod + def formatName(): + """ Short string (~100 char) identifying the file format""" + return 'RAAW .mat file' + + def __init__(self, filename=None, **kwargs): + """ Class constructor. If a `filename` is given, the file is read. """ + self.filename = filename + if filename: + self.read(**kwargs) + + def read(self, filename=None, **kwargs): + """ Reads the file self.filename, or `filename` if provided """ + + # --- Standard tests and exceptions (generic code) + if filename: + self.filename = filename + if not self.filename: + raise Exception('No filename provided') + if not os.path.isfile(self.filename): + raise OSError(2,'File not found:',self.filename) + if os.stat(self.filename).st_size == 0: + raise EmptyFileError('File is empty:',self.filename) + # --- Calling (children) function to read + self._read(**kwargs) + + def write(self, filename=None): + """ Rewrite object to file, or write object to `filename` if provided """ + if filename: + self.filename = filename + if not self.filename: + raise Exception('No filename provided') + # Calling (children) function to write + self._write() + + def _read(self): + """ Reads self.filename and stores data into self. Self is (or behaves like) a dictionary""" + self['data'] = matfile(self.filename,output='pandas') + + def _write(self): + """ Writes to self.filename""" + # --- Example: + #with open(self.filename,'w') as f: + # f.write(self.toString) + raise NotImplementedError() + + def toDataFrame(self): + """ Returns object into one DataFrame, or a dictionary of DataFrames""" + return self['data'] + + # --- Optional functions + def __repr__(self): + """ String that is written to screen when the user calls `print()` on the object. + Provide short and relevant information to save time for the user. + """ + s='<{} object>:\n'.format(type(self).__name__) + s+='|Main attributes:\n' + s+='| - filename: {}\n'.format(self.filename) + # --- Example printing some relevant information for user + #s+='|Main keys:\n' + #s+='| - ID: {}\n'.format(self['ID']) + #s+='| - data : shape {}\n'.format(self['data'].shape) + s+='|Main methods:\n' + s+='| - read, toDataFrame, keys\n' + s+='|Info:\n' + d1=self['data']['data1hz'] + d5=self['data']['data50hz'] + s+='| - data1hz : {} to {} (n:{}, T:{}, dt:{})\n'.format(d1.index[0], d1.index[-1], len(d1), (d1.index[-1]-d1.index[0]).total_seconds(), (d1.index[1]-d1.index[0]).total_seconds()) + s+='| - data50hz: {} to {} (n:{}, T:{}, dt:{})\n'.format(d5.index[0], d5.index[-1], len(d5), (d5.index[-1]-d5.index[0]).total_seconds(), (d5.index[1]-d5.index[0]).total_seconds()) + return s diff --git a/pyFAST/input_output/rosco_discon_file.py b/openfast_toolbox/io/rosco_discon_file.py similarity index 100% rename from pyFAST/input_output/rosco_discon_file.py rename to openfast_toolbox/io/rosco_discon_file.py diff --git a/pyFAST/input_output/rosco_performance_file.py b/openfast_toolbox/io/rosco_performance_file.py similarity index 100% rename from pyFAST/input_output/rosco_performance_file.py rename to openfast_toolbox/io/rosco_performance_file.py diff --git a/pyFAST/input_output/tdms_file.py b/openfast_toolbox/io/tdms_file.py similarity index 100% rename from pyFAST/input_output/tdms_file.py rename to openfast_toolbox/io/tdms_file.py diff --git a/pyFAST/input_output/tecplot_file.py b/openfast_toolbox/io/tecplot_file.py similarity index 100% rename from pyFAST/input_output/tecplot_file.py rename to openfast_toolbox/io/tecplot_file.py diff --git a/openfast_toolbox/io/tests/.gitignore b/openfast_toolbox/io/tests/.gitignore new file mode 100644 index 0000000..dfb5f73 --- /dev/null +++ b/openfast_toolbox/io/tests/.gitignore @@ -0,0 +1,2 @@ +!*.out +!*.outb diff --git a/pyFAST/input_output/tests/__init__.py b/openfast_toolbox/io/tests/__init__.py similarity index 100% rename from pyFAST/input_output/tests/__init__.py rename to openfast_toolbox/io/tests/__init__.py diff --git a/pyFAST/input_output/tests/example_files/BHAWC_out_ascii.dat b/openfast_toolbox/io/tests/example_files/BHAWC_out_ascii.dat similarity index 100% rename from pyFAST/input_output/tests/example_files/BHAWC_out_ascii.dat rename to openfast_toolbox/io/tests/example_files/BHAWC_out_ascii.dat diff --git a/pyFAST/input_output/tests/example_files/BHAWC_out_ascii.sel b/openfast_toolbox/io/tests/example_files/BHAWC_out_ascii.sel similarity index 100% rename from pyFAST/input_output/tests/example_files/BHAWC_out_ascii.sel rename to openfast_toolbox/io/tests/example_files/BHAWC_out_ascii.sel diff --git a/pyFAST/input_output/tests/example_files/BHAWC_out_ascii1.dat b/openfast_toolbox/io/tests/example_files/BHAWC_out_ascii1.dat similarity index 100% rename from pyFAST/input_output/tests/example_files/BHAWC_out_ascii1.dat rename to openfast_toolbox/io/tests/example_files/BHAWC_out_ascii1.dat diff --git a/pyFAST/input_output/tests/example_files/BHAWC_out_ascii1.sel b/openfast_toolbox/io/tests/example_files/BHAWC_out_ascii1.sel similarity index 100% rename from pyFAST/input_output/tests/example_files/BHAWC_out_ascii1.sel rename to openfast_toolbox/io/tests/example_files/BHAWC_out_ascii1.sel diff --git a/openfast_toolbox/io/tests/example_files/BModesOut.out b/openfast_toolbox/io/tests/example_files/BModesOut.out new file mode 100644 index 0000000..4ae1eae --- /dev/null +++ b/openfast_toolbox/io/tests/example_files/BModesOut.out @@ -0,0 +1,73 @@ +Results generated by BModes (v1.03.01, 25-Sept-2007, compiled using double precision) on 24-Dec-2021 at 12:22:43. +RAAW-GE Tower +================================================================================= + + tower frequencies & mode shapes + --- only first 6 modes printed + + + -------- Mode No. 1 (freq = 0.230788E+00 Hz) + +span_loc s-s disp s-s slope f-a disp f-a slope twist + + 0.0000 0.000000 0.000000 0.000000 0.000000 0.000000 + 0.2500 0.018789 0.143625 0.000000 0.000000 -0.024014 + 0.5000 0.075868 0.302878 0.000000 0.000000 -0.048028 + 0.7500 0.179896 0.509195 0.000000 0.000000 -0.072042 + 1.0000 0.331690 0.671161 0.000000 0.000000 -0.096056 + + + -------- Mode No. 2 (freq = 0.233628E+00 Hz) + +span_loc s-s disp s-s slope f-a disp f-a slope twist + + 0.0000 0.000000 0.000000 0.000000 0.000000 0.000000 + 0.2500 0.000000 0.000000 0.019206 0.146763 0.000000 + 0.5000 0.000000 0.000000 0.077483 0.309025 0.000000 + 0.7500 0.000000 0.000000 0.183450 0.517989 0.000000 + 1.0000 0.000000 0.000000 0.337416 0.678804 0.000000 + + + -------- Mode No. 3 (freq = 0.876550E+00 Hz) + +span_loc s-s disp s-s slope f-a disp f-a slope twist + + 0.0000 0.000000 0.000000 0.000000 0.000000 0.000000 + 0.2500 -0.001697 -0.012726 0.000000 0.000000 -0.139550 + 0.5000 -0.006539 -0.024897 0.000000 0.000000 -0.279100 + 0.7500 -0.014695 -0.039065 0.000000 0.000000 -0.418650 + 1.0000 -0.026919 -0.059332 0.000000 0.000000 -0.558200 + + + -------- Mode No. 4 (freq = 0.114618E+01 Hz) + +span_loc s-s disp s-s slope f-a disp f-a slope twist + + 0.0000 0.000000 0.000000 0.000000 0.000000 0.000000 + 0.2500 -0.031715 -0.223564 0.000000 0.000000 -0.004169 + 0.5000 -0.102597 -0.302834 0.000000 0.000000 -0.008338 + 0.7500 -0.152479 -0.038769 0.000000 0.000000 -0.012507 + 1.0000 -0.051019 0.904730 0.000000 0.000000 -0.016676 + + + -------- Mode No. 5 (freq = 0.122086E+01 Hz) + +span_loc s-s disp s-s slope f-a disp f-a slope twist + + 0.0000 0.000000 0.000000 0.000000 0.000000 0.000000 + 0.2500 0.000000 0.000000 -0.032003 -0.224781 0.000000 + 0.5000 0.000000 0.000000 -0.102534 -0.297969 0.000000 + 0.7500 0.000000 0.000000 -0.149375 -0.021083 0.000000 + 1.0000 0.000000 0.000000 -0.044203 0.907986 0.000000 + + + -------- Mode No. 6 (freq = 0.338282E+01 Hz) + +span_loc s-s disp s-s slope f-a disp f-a slope twist + + 0.0000 0.000000 0.000000 0.000000 0.000000 0.000000 + 0.2500 0.081670 0.500635 0.000000 0.000000 -0.020201 + 0.5000 0.179307 0.164971 0.000000 0.000000 -0.040402 + 0.7500 0.086541 -0.757018 0.000000 0.000000 -0.060604 + 1.0000 -0.041198 0.283290 0.000000 0.000000 -0.080805 +================================================================================= diff --git a/pyFAST/input_output/tests/example_files/Bladed_out_ascii.$04 b/openfast_toolbox/io/tests/example_files/Bladed_out_ascii.$04 similarity index 100% rename from pyFAST/input_output/tests/example_files/Bladed_out_ascii.$04 rename to openfast_toolbox/io/tests/example_files/Bladed_out_ascii.$04 diff --git a/pyFAST/input_output/tests/example_files/Bladed_out_ascii.$41 b/openfast_toolbox/io/tests/example_files/Bladed_out_ascii.$41 similarity index 100% rename from pyFAST/input_output/tests/example_files/Bladed_out_ascii.$41 rename to openfast_toolbox/io/tests/example_files/Bladed_out_ascii.$41 diff --git a/pyFAST/input_output/tests/example_files/Bladed_out_ascii.%04 b/openfast_toolbox/io/tests/example_files/Bladed_out_ascii.%04 similarity index 100% rename from pyFAST/input_output/tests/example_files/Bladed_out_ascii.%04 rename to openfast_toolbox/io/tests/example_files/Bladed_out_ascii.%04 diff --git a/pyFAST/input_output/tests/example_files/Bladed_out_ascii.%41 b/openfast_toolbox/io/tests/example_files/Bladed_out_ascii.%41 similarity index 100% rename from pyFAST/input_output/tests/example_files/Bladed_out_ascii.%41 rename to openfast_toolbox/io/tests/example_files/Bladed_out_ascii.%41 diff --git a/pyFAST/input_output/tests/example_files/Bladed_out_binary.$04 b/openfast_toolbox/io/tests/example_files/Bladed_out_binary.$04 similarity index 100% rename from pyFAST/input_output/tests/example_files/Bladed_out_binary.$04 rename to openfast_toolbox/io/tests/example_files/Bladed_out_binary.$04 diff --git a/pyFAST/input_output/tests/example_files/Bladed_out_binary.$41 b/openfast_toolbox/io/tests/example_files/Bladed_out_binary.$41 similarity index 100% rename from pyFAST/input_output/tests/example_files/Bladed_out_binary.$41 rename to openfast_toolbox/io/tests/example_files/Bladed_out_binary.$41 diff --git a/pyFAST/input_output/tests/example_files/Bladed_out_binary.%04 b/openfast_toolbox/io/tests/example_files/Bladed_out_binary.%04 similarity index 100% rename from pyFAST/input_output/tests/example_files/Bladed_out_binary.%04 rename to openfast_toolbox/io/tests/example_files/Bladed_out_binary.%04 diff --git a/pyFAST/input_output/tests/example_files/Bladed_out_binary.%41 b/openfast_toolbox/io/tests/example_files/Bladed_out_binary.%41 similarity index 100% rename from pyFAST/input_output/tests/example_files/Bladed_out_binary.%41 rename to openfast_toolbox/io/tests/example_files/Bladed_out_binary.%41 diff --git a/pyFAST/input_output/tests/example_files/Bladed_out_binary_case2.$04 b/openfast_toolbox/io/tests/example_files/Bladed_out_binary_case2.$04 similarity index 100% rename from pyFAST/input_output/tests/example_files/Bladed_out_binary_case2.$04 rename to openfast_toolbox/io/tests/example_files/Bladed_out_binary_case2.$04 diff --git a/pyFAST/input_output/tests/example_files/Bladed_out_binary_case2.$06 b/openfast_toolbox/io/tests/example_files/Bladed_out_binary_case2.$06 similarity index 100% rename from pyFAST/input_output/tests/example_files/Bladed_out_binary_case2.$06 rename to openfast_toolbox/io/tests/example_files/Bladed_out_binary_case2.$06 diff --git a/pyFAST/input_output/tests/example_files/Bladed_out_binary_case2.$09 b/openfast_toolbox/io/tests/example_files/Bladed_out_binary_case2.$09 similarity index 100% rename from pyFAST/input_output/tests/example_files/Bladed_out_binary_case2.$09 rename to openfast_toolbox/io/tests/example_files/Bladed_out_binary_case2.$09 diff --git a/pyFAST/input_output/tests/example_files/Bladed_out_binary_case2.$12 b/openfast_toolbox/io/tests/example_files/Bladed_out_binary_case2.$12 similarity index 100% rename from pyFAST/input_output/tests/example_files/Bladed_out_binary_case2.$12 rename to openfast_toolbox/io/tests/example_files/Bladed_out_binary_case2.$12 diff --git a/pyFAST/input_output/tests/example_files/Bladed_out_binary_case2.$23 b/openfast_toolbox/io/tests/example_files/Bladed_out_binary_case2.$23 similarity index 100% rename from pyFAST/input_output/tests/example_files/Bladed_out_binary_case2.$23 rename to openfast_toolbox/io/tests/example_files/Bladed_out_binary_case2.$23 diff --git a/pyFAST/input_output/tests/example_files/Bladed_out_binary_case2.$25 b/openfast_toolbox/io/tests/example_files/Bladed_out_binary_case2.$25 similarity index 100% rename from pyFAST/input_output/tests/example_files/Bladed_out_binary_case2.$25 rename to openfast_toolbox/io/tests/example_files/Bladed_out_binary_case2.$25 diff --git a/pyFAST/input_output/tests/example_files/Bladed_out_binary_case2.$31 b/openfast_toolbox/io/tests/example_files/Bladed_out_binary_case2.$31 similarity index 100% rename from pyFAST/input_output/tests/example_files/Bladed_out_binary_case2.$31 rename to openfast_toolbox/io/tests/example_files/Bladed_out_binary_case2.$31 diff --git a/pyFAST/input_output/tests/example_files/Bladed_out_binary_case2.$37 b/openfast_toolbox/io/tests/example_files/Bladed_out_binary_case2.$37 similarity index 100% rename from pyFAST/input_output/tests/example_files/Bladed_out_binary_case2.$37 rename to openfast_toolbox/io/tests/example_files/Bladed_out_binary_case2.$37 diff --git a/pyFAST/input_output/tests/example_files/Bladed_out_binary_case2.$46 b/openfast_toolbox/io/tests/example_files/Bladed_out_binary_case2.$46 similarity index 100% rename from pyFAST/input_output/tests/example_files/Bladed_out_binary_case2.$46 rename to openfast_toolbox/io/tests/example_files/Bladed_out_binary_case2.$46 diff --git a/pyFAST/input_output/tests/example_files/Bladed_out_binary_case2.$55 b/openfast_toolbox/io/tests/example_files/Bladed_out_binary_case2.$55 similarity index 100% rename from pyFAST/input_output/tests/example_files/Bladed_out_binary_case2.$55 rename to openfast_toolbox/io/tests/example_files/Bladed_out_binary_case2.$55 diff --git a/pyFAST/input_output/tests/example_files/Bladed_out_binary_case2.$69 b/openfast_toolbox/io/tests/example_files/Bladed_out_binary_case2.$69 similarity index 100% rename from pyFAST/input_output/tests/example_files/Bladed_out_binary_case2.$69 rename to openfast_toolbox/io/tests/example_files/Bladed_out_binary_case2.$69 diff --git a/pyFAST/input_output/tests/example_files/Bladed_out_binary_case2.$PJ b/openfast_toolbox/io/tests/example_files/Bladed_out_binary_case2.$PJ similarity index 100% rename from pyFAST/input_output/tests/example_files/Bladed_out_binary_case2.$PJ rename to openfast_toolbox/io/tests/example_files/Bladed_out_binary_case2.$PJ diff --git a/pyFAST/input_output/tests/example_files/Bladed_out_binary_case2.%04 b/openfast_toolbox/io/tests/example_files/Bladed_out_binary_case2.%04 similarity index 100% rename from pyFAST/input_output/tests/example_files/Bladed_out_binary_case2.%04 rename to openfast_toolbox/io/tests/example_files/Bladed_out_binary_case2.%04 diff --git a/pyFAST/input_output/tests/example_files/Bladed_out_binary_case2.%06 b/openfast_toolbox/io/tests/example_files/Bladed_out_binary_case2.%06 similarity index 100% rename from pyFAST/input_output/tests/example_files/Bladed_out_binary_case2.%06 rename to openfast_toolbox/io/tests/example_files/Bladed_out_binary_case2.%06 diff --git a/pyFAST/input_output/tests/example_files/Bladed_out_binary_case2.%09 b/openfast_toolbox/io/tests/example_files/Bladed_out_binary_case2.%09 similarity index 100% rename from pyFAST/input_output/tests/example_files/Bladed_out_binary_case2.%09 rename to openfast_toolbox/io/tests/example_files/Bladed_out_binary_case2.%09 diff --git a/pyFAST/input_output/tests/example_files/Bladed_out_binary_case2.%12 b/openfast_toolbox/io/tests/example_files/Bladed_out_binary_case2.%12 similarity index 100% rename from pyFAST/input_output/tests/example_files/Bladed_out_binary_case2.%12 rename to openfast_toolbox/io/tests/example_files/Bladed_out_binary_case2.%12 diff --git a/pyFAST/input_output/tests/example_files/Bladed_out_binary_case2.%23 b/openfast_toolbox/io/tests/example_files/Bladed_out_binary_case2.%23 similarity index 100% rename from pyFAST/input_output/tests/example_files/Bladed_out_binary_case2.%23 rename to openfast_toolbox/io/tests/example_files/Bladed_out_binary_case2.%23 diff --git a/pyFAST/input_output/tests/example_files/Bladed_out_binary_case2.%25 b/openfast_toolbox/io/tests/example_files/Bladed_out_binary_case2.%25 similarity index 100% rename from pyFAST/input_output/tests/example_files/Bladed_out_binary_case2.%25 rename to openfast_toolbox/io/tests/example_files/Bladed_out_binary_case2.%25 diff --git a/pyFAST/input_output/tests/example_files/Bladed_out_binary_case2.%31 b/openfast_toolbox/io/tests/example_files/Bladed_out_binary_case2.%31 similarity index 100% rename from pyFAST/input_output/tests/example_files/Bladed_out_binary_case2.%31 rename to openfast_toolbox/io/tests/example_files/Bladed_out_binary_case2.%31 diff --git a/pyFAST/input_output/tests/example_files/Bladed_out_binary_case2.%37 b/openfast_toolbox/io/tests/example_files/Bladed_out_binary_case2.%37 similarity index 100% rename from pyFAST/input_output/tests/example_files/Bladed_out_binary_case2.%37 rename to openfast_toolbox/io/tests/example_files/Bladed_out_binary_case2.%37 diff --git a/pyFAST/input_output/tests/example_files/Bladed_out_binary_case2.%46 b/openfast_toolbox/io/tests/example_files/Bladed_out_binary_case2.%46 similarity index 100% rename from pyFAST/input_output/tests/example_files/Bladed_out_binary_case2.%46 rename to openfast_toolbox/io/tests/example_files/Bladed_out_binary_case2.%46 diff --git a/pyFAST/input_output/tests/example_files/Bladed_out_binary_case2.%55 b/openfast_toolbox/io/tests/example_files/Bladed_out_binary_case2.%55 similarity index 100% rename from pyFAST/input_output/tests/example_files/Bladed_out_binary_case2.%55 rename to openfast_toolbox/io/tests/example_files/Bladed_out_binary_case2.%55 diff --git a/pyFAST/input_output/tests/example_files/Bladed_out_binary_case2.%69 b/openfast_toolbox/io/tests/example_files/Bladed_out_binary_case2.%69 similarity index 100% rename from pyFAST/input_output/tests/example_files/Bladed_out_binary_case2.%69 rename to openfast_toolbox/io/tests/example_files/Bladed_out_binary_case2.%69 diff --git a/pyFAST/input_output/tests/example_files/Bladed_out_binary_case2_fail.$55 b/openfast_toolbox/io/tests/example_files/Bladed_out_binary_case2_fail.$55 similarity index 100% rename from pyFAST/input_output/tests/example_files/Bladed_out_binary_case2_fail.$55 rename to openfast_toolbox/io/tests/example_files/Bladed_out_binary_case2_fail.$55 diff --git a/pyFAST/input_output/tests/example_files/Bladed_out_binary_case2_fail.%55 b/openfast_toolbox/io/tests/example_files/Bladed_out_binary_case2_fail.%55 similarity index 100% rename from pyFAST/input_output/tests/example_files/Bladed_out_binary_case2_fail.%55 rename to openfast_toolbox/io/tests/example_files/Bladed_out_binary_case2_fail.%55 diff --git a/pyFAST/input_output/tests/example_files/CSVAutoCommentChar.txt b/openfast_toolbox/io/tests/example_files/CSVAutoCommentChar.txt similarity index 100% rename from pyFAST/input_output/tests/example_files/CSVAutoCommentChar.txt rename to openfast_toolbox/io/tests/example_files/CSVAutoCommentChar.txt diff --git a/pyFAST/input_output/tests/example_files/CSVColInHeader.csv b/openfast_toolbox/io/tests/example_files/CSVColInHeader.csv similarity index 100% rename from pyFAST/input_output/tests/example_files/CSVColInHeader.csv rename to openfast_toolbox/io/tests/example_files/CSVColInHeader.csv diff --git a/pyFAST/input_output/tests/example_files/CSVColInHeader2.csv b/openfast_toolbox/io/tests/example_files/CSVColInHeader2.csv similarity index 100% rename from pyFAST/input_output/tests/example_files/CSVColInHeader2.csv rename to openfast_toolbox/io/tests/example_files/CSVColInHeader2.csv diff --git a/pyFAST/input_output/tests/example_files/CSVColInHeader3.csv b/openfast_toolbox/io/tests/example_files/CSVColInHeader3.csv similarity index 100% rename from pyFAST/input_output/tests/example_files/CSVColInHeader3.csv rename to openfast_toolbox/io/tests/example_files/CSVColInHeader3.csv diff --git a/pyFAST/input_output/tests/example_files/CSVComma.csv b/openfast_toolbox/io/tests/example_files/CSVComma.csv similarity index 100% rename from pyFAST/input_output/tests/example_files/CSVComma.csv rename to openfast_toolbox/io/tests/example_files/CSVComma.csv diff --git a/pyFAST/input_output/tests/example_files/CSVDateNaN.csv b/openfast_toolbox/io/tests/example_files/CSVDateNaN.csv similarity index 100% rename from pyFAST/input_output/tests/example_files/CSVDateNaN.csv rename to openfast_toolbox/io/tests/example_files/CSVDateNaN.csv diff --git a/pyFAST/input_output/tests/example_files/CSVNoHeader.csv b/openfast_toolbox/io/tests/example_files/CSVNoHeader.csv similarity index 100% rename from pyFAST/input_output/tests/example_files/CSVNoHeader.csv rename to openfast_toolbox/io/tests/example_files/CSVNoHeader.csv diff --git a/pyFAST/input_output/tests/example_files/CSVSemi.csv b/openfast_toolbox/io/tests/example_files/CSVSemi.csv similarity index 100% rename from pyFAST/input_output/tests/example_files/CSVSemi.csv rename to openfast_toolbox/io/tests/example_files/CSVSemi.csv diff --git a/pyFAST/input_output/tests/example_files/CSVSpace_ExtraCol.csv b/openfast_toolbox/io/tests/example_files/CSVSpace_ExtraCol.csv similarity index 100% rename from pyFAST/input_output/tests/example_files/CSVSpace_ExtraCol.csv rename to openfast_toolbox/io/tests/example_files/CSVSpace_ExtraCol.csv diff --git a/pyFAST/input_output/tests/example_files/CSVTab.csv b/openfast_toolbox/io/tests/example_files/CSVTab.csv similarity index 100% rename from pyFAST/input_output/tests/example_files/CSVTab.csv rename to openfast_toolbox/io/tests/example_files/CSVTab.csv diff --git a/pyFAST/input_output/tests/example_files/CSVTwoLinesHeaders.txt b/openfast_toolbox/io/tests/example_files/CSVTwoLinesHeaders.txt similarity index 100% rename from pyFAST/input_output/tests/example_files/CSVTwoLinesHeaders.txt rename to openfast_toolbox/io/tests/example_files/CSVTwoLinesHeaders.txt diff --git a/pyFAST/input_output/tests/example_files/CSVxIsString.csv b/openfast_toolbox/io/tests/example_files/CSVxIsString.csv similarity index 100% rename from pyFAST/input_output/tests/example_files/CSVxIsString.csv rename to openfast_toolbox/io/tests/example_files/CSVxIsString.csv diff --git a/pyFAST/input_output/tests/example_files/ExcelFile_OneSheet.xlsx b/openfast_toolbox/io/tests/example_files/ExcelFile_OneSheet.xlsx similarity index 100% rename from pyFAST/input_output/tests/example_files/ExcelFile_OneSheet.xlsx rename to openfast_toolbox/io/tests/example_files/ExcelFile_OneSheet.xlsx diff --git a/pyFAST/input_output/tests/example_files/FASTIn_AD14.dat b/openfast_toolbox/io/tests/example_files/FASTIn_AD14.dat similarity index 100% rename from pyFAST/input_output/tests/example_files/FASTIn_AD14.dat rename to openfast_toolbox/io/tests/example_files/FASTIn_AD14.dat diff --git a/pyFAST/input_output/tests/example_files/FASTIn_AD14_arf.dat b/openfast_toolbox/io/tests/example_files/FASTIn_AD14_arf.dat similarity index 100% rename from pyFAST/input_output/tests/example_files/FASTIn_AD14_arf.dat rename to openfast_toolbox/io/tests/example_files/FASTIn_AD14_arf.dat diff --git a/pyFAST/input_output/tests/example_files/FASTIn_AD14_arf_3col.dat b/openfast_toolbox/io/tests/example_files/FASTIn_AD14_arf_3col.dat similarity index 100% rename from pyFAST/input_output/tests/example_files/FASTIn_AD14_arf_3col.dat rename to openfast_toolbox/io/tests/example_files/FASTIn_AD14_arf_3col.dat diff --git a/pyFAST/input_output/tests/example_files/FASTIn_AD14_arf_Re.dat b/openfast_toolbox/io/tests/example_files/FASTIn_AD14_arf_Re.dat similarity index 100% rename from pyFAST/input_output/tests/example_files/FASTIn_AD14_arf_Re.dat rename to openfast_toolbox/io/tests/example_files/FASTIn_AD14_arf_Re.dat diff --git a/pyFAST/input_output/tests/example_files/FASTIn_AD15.dat b/openfast_toolbox/io/tests/example_files/FASTIn_AD15.dat similarity index 100% rename from pyFAST/input_output/tests/example_files/FASTIn_AD15.dat rename to openfast_toolbox/io/tests/example_files/FASTIn_AD15.dat diff --git a/pyFAST/input_output/tests/example_files/FASTIn_AD15_arf_multitabs.dat b/openfast_toolbox/io/tests/example_files/FASTIn_AD15_arf_multitabs.dat similarity index 100% rename from pyFAST/input_output/tests/example_files/FASTIn_AD15_arf_multitabs.dat rename to openfast_toolbox/io/tests/example_files/FASTIn_AD15_arf_multitabs.dat diff --git a/pyFAST/input_output/tests/example_files/FASTIn_AD15_arfl.dat b/openfast_toolbox/io/tests/example_files/FASTIn_AD15_arfl.dat similarity index 100% rename from pyFAST/input_output/tests/example_files/FASTIn_AD15_arfl.dat rename to openfast_toolbox/io/tests/example_files/FASTIn_AD15_arfl.dat diff --git a/pyFAST/input_output/tests/example_files/FASTIn_AD15_arfl0.dat b/openfast_toolbox/io/tests/example_files/FASTIn_AD15_arfl0.dat similarity index 100% rename from pyFAST/input_output/tests/example_files/FASTIn_AD15_arfl0.dat rename to openfast_toolbox/io/tests/example_files/FASTIn_AD15_arfl0.dat diff --git a/pyFAST/input_output/tests/example_files/FASTIn_AD15_bld.dat b/openfast_toolbox/io/tests/example_files/FASTIn_AD15_bld.dat similarity index 100% rename from pyFAST/input_output/tests/example_files/FASTIn_AD15_bld.dat rename to openfast_toolbox/io/tests/example_files/FASTIn_AD15_bld.dat diff --git a/pyFAST/input_output/tests/example_files/FASTIn_AD15_latin.dat b/openfast_toolbox/io/tests/example_files/FASTIn_AD15_latin.dat similarity index 100% rename from pyFAST/input_output/tests/example_files/FASTIn_AD15_latin.dat rename to openfast_toolbox/io/tests/example_files/FASTIn_AD15_latin.dat diff --git a/pyFAST/input_output/tests/example_files/FASTIn_AD15_of34.dat b/openfast_toolbox/io/tests/example_files/FASTIn_AD15_of34.dat similarity index 100% rename from pyFAST/input_output/tests/example_files/FASTIn_AD15_of34.dat rename to openfast_toolbox/io/tests/example_files/FASTIn_AD15_of34.dat diff --git a/pyFAST/input_output/tests/example_files/FASTIn_AD_F7.dat b/openfast_toolbox/io/tests/example_files/FASTIn_AD_F7.dat similarity index 100% rename from pyFAST/input_output/tests/example_files/FASTIn_AD_F7.dat rename to openfast_toolbox/io/tests/example_files/FASTIn_AD_F7.dat diff --git a/pyFAST/input_output/tests/example_files/FASTIn_AD_twr.dat b/openfast_toolbox/io/tests/example_files/FASTIn_AD_twr.dat similarity index 100% rename from pyFAST/input_output/tests/example_files/FASTIn_AD_twr.dat rename to openfast_toolbox/io/tests/example_files/FASTIn_AD_twr.dat diff --git a/pyFAST/input_output/tests/example_files/FASTIn_BD.dat b/openfast_toolbox/io/tests/example_files/FASTIn_BD.dat similarity index 100% rename from pyFAST/input_output/tests/example_files/FASTIn_BD.dat rename to openfast_toolbox/io/tests/example_files/FASTIn_BD.dat diff --git a/pyFAST/input_output/tests/example_files/FASTIn_BD_bld.dat b/openfast_toolbox/io/tests/example_files/FASTIn_BD_bld.dat similarity index 100% rename from pyFAST/input_output/tests/example_files/FASTIn_BD_bld.dat rename to openfast_toolbox/io/tests/example_files/FASTIn_BD_bld.dat diff --git a/pyFAST/input_output/tests/example_files/FASTIn_BD_bld_2.dat b/openfast_toolbox/io/tests/example_files/FASTIn_BD_bld_2.dat similarity index 100% rename from pyFAST/input_output/tests/example_files/FASTIn_BD_bld_2.dat rename to openfast_toolbox/io/tests/example_files/FASTIn_BD_bld_2.dat diff --git a/pyFAST/input_output/tests/example_files/FASTIn_Blade.dat b/openfast_toolbox/io/tests/example_files/FASTIn_Blade.dat similarity index 100% rename from pyFAST/input_output/tests/example_files/FASTIn_Blade.dat rename to openfast_toolbox/io/tests/example_files/FASTIn_Blade.dat diff --git a/pyFAST/input_output/tests/example_files/FASTIn_ED.dat b/openfast_toolbox/io/tests/example_files/FASTIn_ED.dat similarity index 100% rename from pyFAST/input_output/tests/example_files/FASTIn_ED.dat rename to openfast_toolbox/io/tests/example_files/FASTIn_ED.dat diff --git a/pyFAST/input_output/tests/example_files/FASTIn_ED_bld.dat b/openfast_toolbox/io/tests/example_files/FASTIn_ED_bld.dat similarity index 100% rename from pyFAST/input_output/tests/example_files/FASTIn_ED_bld.dat rename to openfast_toolbox/io/tests/example_files/FASTIn_ED_bld.dat diff --git a/pyFAST/input_output/tests/example_files/FASTIn_ED_bld_F7.dat b/openfast_toolbox/io/tests/example_files/FASTIn_ED_bld_F7.dat similarity index 100% rename from pyFAST/input_output/tests/example_files/FASTIn_ED_bld_F7.dat rename to openfast_toolbox/io/tests/example_files/FASTIn_ED_bld_F7.dat diff --git a/pyFAST/input_output/tests/example_files/FASTIn_ED_twr.dat b/openfast_toolbox/io/tests/example_files/FASTIn_ED_twr.dat similarity index 100% rename from pyFAST/input_output/tests/example_files/FASTIn_ED_twr.dat rename to openfast_toolbox/io/tests/example_files/FASTIn_ED_twr.dat diff --git a/pyFAST/input_output/tests/example_files/FASTIn_ED_twr_F7.dat b/openfast_toolbox/io/tests/example_files/FASTIn_ED_twr_F7.dat similarity index 100% rename from pyFAST/input_output/tests/example_files/FASTIn_ED_twr_F7.dat rename to openfast_toolbox/io/tests/example_files/FASTIn_ED_twr_F7.dat diff --git a/pyFAST/input_output/tests/example_files/FASTIn_ExtPtfm_SubSef.dat b/openfast_toolbox/io/tests/example_files/FASTIn_ExtPtfm_SubSef.dat similarity index 100% rename from pyFAST/input_output/tests/example_files/FASTIn_ExtPtfm_SubSef.dat rename to openfast_toolbox/io/tests/example_files/FASTIn_ExtPtfm_SubSef.dat diff --git a/pyFAST/input_output/tests/example_files/FASTIn_HD.dat b/openfast_toolbox/io/tests/example_files/FASTIn_HD.dat similarity index 100% rename from pyFAST/input_output/tests/example_files/FASTIn_HD.dat rename to openfast_toolbox/io/tests/example_files/FASTIn_HD.dat diff --git a/pyFAST/input_output/tests/example_files/FASTIn_HD2.dat b/openfast_toolbox/io/tests/example_files/FASTIn_HD2.dat similarity index 100% rename from pyFAST/input_output/tests/example_files/FASTIn_HD2.dat rename to openfast_toolbox/io/tests/example_files/FASTIn_HD2.dat diff --git a/pyFAST/input_output/tests/example_files/FASTIn_HD_SeaState.dat b/openfast_toolbox/io/tests/example_files/FASTIn_HD_SeaState.dat similarity index 100% rename from pyFAST/input_output/tests/example_files/FASTIn_HD_SeaState.dat rename to openfast_toolbox/io/tests/example_files/FASTIn_HD_SeaState.dat diff --git a/pyFAST/input_output/tests/example_files/FASTIn_HD_driver.dvr b/openfast_toolbox/io/tests/example_files/FASTIn_HD_driver.dvr similarity index 100% rename from pyFAST/input_output/tests/example_files/FASTIn_HD_driver.dvr rename to openfast_toolbox/io/tests/example_files/FASTIn_HD_driver.dvr diff --git a/pyFAST/input_output/tests/example_files/FASTIn_IF_NoHead.dat b/openfast_toolbox/io/tests/example_files/FASTIn_IF_NoHead.dat similarity index 100% rename from pyFAST/input_output/tests/example_files/FASTIn_IF_NoHead.dat rename to openfast_toolbox/io/tests/example_files/FASTIn_IF_NoHead.dat diff --git a/pyFAST/input_output/tests/example_files/FASTIn_MD-v1.dat b/openfast_toolbox/io/tests/example_files/FASTIn_MD-v1.dat similarity index 100% rename from pyFAST/input_output/tests/example_files/FASTIn_MD-v1.dat rename to openfast_toolbox/io/tests/example_files/FASTIn_MD-v1.dat diff --git a/pyFAST/input_output/tests/example_files/FASTIn_MD-v2.dat b/openfast_toolbox/io/tests/example_files/FASTIn_MD-v2.dat similarity index 100% rename from pyFAST/input_output/tests/example_files/FASTIn_MD-v2.dat rename to openfast_toolbox/io/tests/example_files/FASTIn_MD-v2.dat diff --git a/pyFAST/input_output/tests/example_files/FASTIn_SD.dat b/openfast_toolbox/io/tests/example_files/FASTIn_SD.dat similarity index 100% rename from pyFAST/input_output/tests/example_files/FASTIn_SD.dat rename to openfast_toolbox/io/tests/example_files/FASTIn_SD.dat diff --git a/pyFAST/input_output/tests/example_files/FASTIn_SbD.dat b/openfast_toolbox/io/tests/example_files/FASTIn_SbD.dat similarity index 100% rename from pyFAST/input_output/tests/example_files/FASTIn_SbD.dat rename to openfast_toolbox/io/tests/example_files/FASTIn_SbD.dat diff --git a/pyFAST/input_output/tests/example_files/FASTIn_SbD_comments.dat b/openfast_toolbox/io/tests/example_files/FASTIn_SbD_comments.dat similarity index 100% rename from pyFAST/input_output/tests/example_files/FASTIn_SbD_comments.dat rename to openfast_toolbox/io/tests/example_files/FASTIn_SbD_comments.dat diff --git a/pyFAST/input_output/tests/example_files/FASTIn_TurbSim.dat b/openfast_toolbox/io/tests/example_files/FASTIn_TurbSim.dat similarity index 100% rename from pyFAST/input_output/tests/example_files/FASTIn_TurbSim.dat rename to openfast_toolbox/io/tests/example_files/FASTIn_TurbSim.dat diff --git a/pyFAST/input_output/tests/example_files/FASTIn_arf_coords.txt b/openfast_toolbox/io/tests/example_files/FASTIn_arf_coords.txt similarity index 100% rename from pyFAST/input_output/tests/example_files/FASTIn_arf_coords.txt rename to openfast_toolbox/io/tests/example_files/FASTIn_arf_coords.txt diff --git a/pyFAST/input_output/tests/example_files/FASTLin.lin b/openfast_toolbox/io/tests/example_files/FASTLin.lin similarity index 100% rename from pyFAST/input_output/tests/example_files/FASTLin.lin rename to openfast_toolbox/io/tests/example_files/FASTLin.lin diff --git a/pyFAST/input_output/tests/example_files/FASTLin_EDM.lin b/openfast_toolbox/io/tests/example_files/FASTLin_EDM.lin similarity index 100% rename from pyFAST/input_output/tests/example_files/FASTLin_EDM.lin rename to openfast_toolbox/io/tests/example_files/FASTLin_EDM.lin diff --git a/pyFAST/input_output/tests/example_files/FASTOut.out b/openfast_toolbox/io/tests/example_files/FASTOut.out similarity index 100% rename from pyFAST/input_output/tests/example_files/FASTOut.out rename to openfast_toolbox/io/tests/example_files/FASTOut.out diff --git a/pyFAST/input_output/tests/example_files/FASTOutBin.outb b/openfast_toolbox/io/tests/example_files/FASTOutBin.outb similarity index 100% rename from pyFAST/input_output/tests/example_files/FASTOutBin.outb rename to openfast_toolbox/io/tests/example_files/FASTOutBin.outb diff --git a/pyFAST/input_output/tests/example_files/FASTOutBin_ID4.outb b/openfast_toolbox/io/tests/example_files/FASTOutBin_ID4.outb similarity index 100% rename from pyFAST/input_output/tests/example_files/FASTOutBin_ID4.outb rename to openfast_toolbox/io/tests/example_files/FASTOutBin_ID4.outb diff --git a/pyFAST/input_output/tests/example_files/FASTOut_HD.elev b/openfast_toolbox/io/tests/example_files/FASTOut_HD.elev similarity index 100% rename from pyFAST/input_output/tests/example_files/FASTOut_HD.elev rename to openfast_toolbox/io/tests/example_files/FASTOut_HD.elev diff --git a/pyFAST/input_output/tests/example_files/FASTOut_Hydro.out b/openfast_toolbox/io/tests/example_files/FASTOut_Hydro.out similarity index 100% rename from pyFAST/input_output/tests/example_files/FASTOut_Hydro.out rename to openfast_toolbox/io/tests/example_files/FASTOut_Hydro.out diff --git a/pyFAST/input_output/tests/example_files/FASTOut_V7.elm b/openfast_toolbox/io/tests/example_files/FASTOut_V7.elm similarity index 100% rename from pyFAST/input_output/tests/example_files/FASTOut_V7.elm rename to openfast_toolbox/io/tests/example_files/FASTOut_V7.elm diff --git a/pyFAST/input_output/tests/example_files/FASTSum_Pendulum.SD.sum.yaml b/openfast_toolbox/io/tests/example_files/FASTSum_Pendulum.SD.sum.yaml similarity index 100% rename from pyFAST/input_output/tests/example_files/FASTSum_Pendulum.SD.sum.yaml rename to openfast_toolbox/io/tests/example_files/FASTSum_Pendulum.SD.sum.yaml diff --git a/pyFAST/input_output/tests/example_files/FASTWnd.wnd b/openfast_toolbox/io/tests/example_files/FASTWnd.wnd similarity index 100% rename from pyFAST/input_output/tests/example_files/FASTWnd.wnd rename to openfast_toolbox/io/tests/example_files/FASTWnd.wnd diff --git a/pyFAST/input_output/tests/example_files/FLEXBlade000.bla b/openfast_toolbox/io/tests/example_files/FLEXBlade000.bla similarity index 100% rename from pyFAST/input_output/tests/example_files/FLEXBlade000.bla rename to openfast_toolbox/io/tests/example_files/FLEXBlade000.bla diff --git a/pyFAST/input_output/tests/example_files/FLEXBlade001.bld b/openfast_toolbox/io/tests/example_files/FLEXBlade001.bld similarity index 100% rename from pyFAST/input_output/tests/example_files/FLEXBlade001.bld rename to openfast_toolbox/io/tests/example_files/FLEXBlade001.bld diff --git a/pyFAST/input_output/tests/example_files/FLEXBlade002.bld b/openfast_toolbox/io/tests/example_files/FLEXBlade002.bld similarity index 100% rename from pyFAST/input_output/tests/example_files/FLEXBlade002.bld rename to openfast_toolbox/io/tests/example_files/FLEXBlade002.bld diff --git a/pyFAST/input_output/tests/example_files/FLEXBlade003.bld b/openfast_toolbox/io/tests/example_files/FLEXBlade003.bld similarity index 100% rename from pyFAST/input_output/tests/example_files/FLEXBlade003.bld rename to openfast_toolbox/io/tests/example_files/FLEXBlade003.bld diff --git a/pyFAST/input_output/tests/example_files/FLEXDocFile.out b/openfast_toolbox/io/tests/example_files/FLEXDocFile.out similarity index 100% rename from pyFAST/input_output/tests/example_files/FLEXDocFile.out rename to openfast_toolbox/io/tests/example_files/FLEXDocFile.out diff --git a/pyFAST/input_output/tests/example_files/FLEXOutBinV0.int b/openfast_toolbox/io/tests/example_files/FLEXOutBinV0.int similarity index 100% rename from pyFAST/input_output/tests/example_files/FLEXOutBinV0.int rename to openfast_toolbox/io/tests/example_files/FLEXOutBinV0.int diff --git a/pyFAST/input_output/tests/example_files/FLEXOutBinV3.res b/openfast_toolbox/io/tests/example_files/FLEXOutBinV3.res similarity index 100% rename from pyFAST/input_output/tests/example_files/FLEXOutBinV3.res rename to openfast_toolbox/io/tests/example_files/FLEXOutBinV3.res diff --git a/pyFAST/input_output/tests/example_files/FLEXProfile.pro b/openfast_toolbox/io/tests/example_files/FLEXProfile.pro similarity index 100% rename from pyFAST/input_output/tests/example_files/FLEXProfile.pro rename to openfast_toolbox/io/tests/example_files/FLEXProfile.pro diff --git a/pyFAST/input_output/tests/example_files/FLEXWaveKin.wko b/openfast_toolbox/io/tests/example_files/FLEXWaveKin.wko similarity index 100% rename from pyFAST/input_output/tests/example_files/FLEXWaveKin.wko rename to openfast_toolbox/io/tests/example_files/FLEXWaveKin.wko diff --git a/pyFAST/input_output/tests/example_files/HAWC2_ae.dat b/openfast_toolbox/io/tests/example_files/HAWC2_ae.dat similarity index 100% rename from pyFAST/input_output/tests/example_files/HAWC2_ae.dat rename to openfast_toolbox/io/tests/example_files/HAWC2_ae.dat diff --git a/pyFAST/input_output/tests/example_files/HAWC2_out_ascii.dat b/openfast_toolbox/io/tests/example_files/HAWC2_out_ascii.dat similarity index 100% rename from pyFAST/input_output/tests/example_files/HAWC2_out_ascii.dat rename to openfast_toolbox/io/tests/example_files/HAWC2_out_ascii.dat diff --git a/pyFAST/input_output/tests/example_files/HAWC2_out_ascii.sel b/openfast_toolbox/io/tests/example_files/HAWC2_out_ascii.sel similarity index 100% rename from pyFAST/input_output/tests/example_files/HAWC2_out_ascii.sel rename to openfast_toolbox/io/tests/example_files/HAWC2_out_ascii.sel diff --git a/pyFAST/input_output/tests/example_files/HAWC2_out_bin.dat b/openfast_toolbox/io/tests/example_files/HAWC2_out_bin.dat similarity index 100% rename from pyFAST/input_output/tests/example_files/HAWC2_out_bin.dat rename to openfast_toolbox/io/tests/example_files/HAWC2_out_bin.dat diff --git a/pyFAST/input_output/tests/example_files/HAWC2_out_bin.sel b/openfast_toolbox/io/tests/example_files/HAWC2_out_bin.sel similarity index 100% rename from pyFAST/input_output/tests/example_files/HAWC2_out_bin.sel rename to openfast_toolbox/io/tests/example_files/HAWC2_out_bin.sel diff --git a/pyFAST/input_output/tests/example_files/HAWC2_pc.dat b/openfast_toolbox/io/tests/example_files/HAWC2_pc.dat similarity index 100% rename from pyFAST/input_output/tests/example_files/HAWC2_pc.dat rename to openfast_toolbox/io/tests/example_files/HAWC2_pc.dat diff --git a/pyFAST/input_output/tests/example_files/HAWC2_st.dat b/openfast_toolbox/io/tests/example_files/HAWC2_st.dat similarity index 100% rename from pyFAST/input_output/tests/example_files/HAWC2_st.dat rename to openfast_toolbox/io/tests/example_files/HAWC2_st.dat diff --git a/pyFAST/input_output/tests/example_files/HAWC2_st.st b/openfast_toolbox/io/tests/example_files/HAWC2_st.st similarity index 100% rename from pyFAST/input_output/tests/example_files/HAWC2_st.st rename to openfast_toolbox/io/tests/example_files/HAWC2_st.st diff --git a/pyFAST/input_output/tests/example_files/HAWC2_st_fpm.st b/openfast_toolbox/io/tests/example_files/HAWC2_st_fpm.st similarity index 100% rename from pyFAST/input_output/tests/example_files/HAWC2_st_fpm.st rename to openfast_toolbox/io/tests/example_files/HAWC2_st_fpm.st diff --git a/pyFAST/input_output/tests/example_files/HAWCStab2.pwr b/openfast_toolbox/io/tests/example_files/HAWCStab2.pwr similarity index 100% rename from pyFAST/input_output/tests/example_files/HAWCStab2.pwr rename to openfast_toolbox/io/tests/example_files/HAWCStab2.pwr diff --git a/pyFAST/input_output/tests/example_files/HAWCStab2_defl_u3000.ind b/openfast_toolbox/io/tests/example_files/HAWCStab2_defl_u3000.ind similarity index 100% rename from pyFAST/input_output/tests/example_files/HAWCStab2_defl_u3000.ind rename to openfast_toolbox/io/tests/example_files/HAWCStab2_defl_u3000.ind diff --git a/pyFAST/input_output/tests/example_files/HAWCStab2_fext_u3000.ind b/openfast_toolbox/io/tests/example_files/HAWCStab2_fext_u3000.ind similarity index 100% rename from pyFAST/input_output/tests/example_files/HAWCStab2_fext_u3000.ind rename to openfast_toolbox/io/tests/example_files/HAWCStab2_fext_u3000.ind diff --git a/pyFAST/input_output/tests/example_files/HAWCStab2_u3000.ind b/openfast_toolbox/io/tests/example_files/HAWCStab2_u3000.ind similarity index 100% rename from pyFAST/input_output/tests/example_files/HAWCStab2_u3000.ind rename to openfast_toolbox/io/tests/example_files/HAWCStab2_u3000.ind diff --git a/pyFAST/input_output/tests/example_files/MannBox_2x4x8.bin b/openfast_toolbox/io/tests/example_files/MannBox_2x4x8.bin similarity index 100% rename from pyFAST/input_output/tests/example_files/MannBox_2x4x8.bin rename to openfast_toolbox/io/tests/example_files/MannBox_2x4x8.bin diff --git a/pyFAST/input_output/tests/example_files/ParquetFile_test.parquet b/openfast_toolbox/io/tests/example_files/ParquetFile_test.parquet similarity index 100% rename from pyFAST/input_output/tests/example_files/ParquetFile_test.parquet rename to openfast_toolbox/io/tests/example_files/ParquetFile_test.parquet diff --git a/pyFAST/input_output/tests/example_files/RoscoDISCON_PowerTracking.in b/openfast_toolbox/io/tests/example_files/RoscoDISCON_PowerTracking.in similarity index 100% rename from pyFAST/input_output/tests/example_files/RoscoDISCON_PowerTracking.in rename to openfast_toolbox/io/tests/example_files/RoscoDISCON_PowerTracking.in diff --git a/pyFAST/input_output/tests/example_files/RoscoPerformance_CpCtCq.txt b/openfast_toolbox/io/tests/example_files/RoscoPerformance_CpCtCq.txt similarity index 100% rename from pyFAST/input_output/tests/example_files/RoscoPerformance_CpCtCq.txt rename to openfast_toolbox/io/tests/example_files/RoscoPerformance_CpCtCq.txt diff --git a/pyFAST/input_output/tests/example_files/TDMS_1Grp2Chan_TimeTrack.tdms b/openfast_toolbox/io/tests/example_files/TDMS_1Grp2Chan_TimeTrack.tdms similarity index 100% rename from pyFAST/input_output/tests/example_files/TDMS_1Grp2Chan_TimeTrack.tdms rename to openfast_toolbox/io/tests/example_files/TDMS_1Grp2Chan_TimeTrack.tdms diff --git a/pyFAST/input_output/tests/example_files/TDMS_2Grp2Chan.tdms b/openfast_toolbox/io/tests/example_files/TDMS_2Grp2Chan.tdms similarity index 100% rename from pyFAST/input_output/tests/example_files/TDMS_2Grp2Chan.tdms rename to openfast_toolbox/io/tests/example_files/TDMS_2Grp2Chan.tdms diff --git a/pyFAST/input_output/tests/example_files/TecplotASCII_1.dat b/openfast_toolbox/io/tests/example_files/TecplotASCII_1.dat similarity index 100% rename from pyFAST/input_output/tests/example_files/TecplotASCII_1.dat rename to openfast_toolbox/io/tests/example_files/TecplotASCII_1.dat diff --git a/pyFAST/input_output/tests/example_files/TecplotASCII_2.dat b/openfast_toolbox/io/tests/example_files/TecplotASCII_2.dat similarity index 100% rename from pyFAST/input_output/tests/example_files/TecplotASCII_2.dat rename to openfast_toolbox/io/tests/example_files/TecplotASCII_2.dat diff --git a/pyFAST/input_output/tests/example_files/TurbSimTS.txt b/openfast_toolbox/io/tests/example_files/TurbSimTS.txt similarity index 100% rename from pyFAST/input_output/tests/example_files/TurbSimTS.txt rename to openfast_toolbox/io/tests/example_files/TurbSimTS.txt diff --git a/pyFAST/input_output/tests/example_files/TurbSim_FAST.bts b/openfast_toolbox/io/tests/example_files/TurbSim_FAST.bts similarity index 100% rename from pyFAST/input_output/tests/example_files/TurbSim_FAST.bts rename to openfast_toolbox/io/tests/example_files/TurbSim_FAST.bts diff --git a/pyFAST/input_output/tests/example_files/TurbSim_NoTwr.bts b/openfast_toolbox/io/tests/example_files/TurbSim_NoTwr.bts similarity index 100% rename from pyFAST/input_output/tests/example_files/TurbSim_NoTwr.bts rename to openfast_toolbox/io/tests/example_files/TurbSim_NoTwr.bts diff --git a/pyFAST/input_output/tests/example_files/TurbSim_WithTwr.bts b/openfast_toolbox/io/tests/example_files/TurbSim_WithTwr.bts similarity index 100% rename from pyFAST/input_output/tests/example_files/TurbSim_WithTwr.bts rename to openfast_toolbox/io/tests/example_files/TurbSim_WithTwr.bts diff --git a/pyFAST/input_output/tests/example_files/VTKStructuredPointsPointData.vtk b/openfast_toolbox/io/tests/example_files/VTKStructuredPointsPointData.vtk similarity index 100% rename from pyFAST/input_output/tests/example_files/VTKStructuredPointsPointData.vtk rename to openfast_toolbox/io/tests/example_files/VTKStructuredPointsPointData.vtk diff --git a/pyFAST/input_output/tests/example_files/input_decks/Elliptic_AD15_40.dat b/openfast_toolbox/io/tests/example_files/input_decks/Elliptic_AD15_40.dat similarity index 100% rename from pyFAST/input_output/tests/example_files/input_decks/Elliptic_AD15_40.dat rename to openfast_toolbox/io/tests/example_files/input_decks/Elliptic_AD15_40.dat diff --git a/pyFAST/input_output/tests/example_files/input_decks/Elliptic_AD15_blade_40_cos.dat b/openfast_toolbox/io/tests/example_files/input_decks/Elliptic_AD15_blade_40_cos.dat similarity index 100% rename from pyFAST/input_output/tests/example_files/input_decks/Elliptic_AD15_blade_40_cos.dat rename to openfast_toolbox/io/tests/example_files/input_decks/Elliptic_AD15_blade_40_cos.dat diff --git a/pyFAST/input_output/tests/example_files/input_decks/Elliptic_IW.dat b/openfast_toolbox/io/tests/example_files/input_decks/Elliptic_IW.dat similarity index 100% rename from pyFAST/input_output/tests/example_files/input_decks/Elliptic_IW.dat rename to openfast_toolbox/io/tests/example_files/input_decks/Elliptic_IW.dat diff --git a/pyFAST/input_output/tests/example_files/input_decks/Elliptic_OLAF.dat b/openfast_toolbox/io/tests/example_files/input_decks/Elliptic_OLAF.dat similarity index 100% rename from pyFAST/input_output/tests/example_files/input_decks/Elliptic_OLAF.dat rename to openfast_toolbox/io/tests/example_files/input_decks/Elliptic_OLAF.dat diff --git a/pyFAST/input_output/tests/example_files/input_decks/Elliptic_Wind.wnd b/openfast_toolbox/io/tests/example_files/input_decks/Elliptic_Wind.wnd similarity index 100% rename from pyFAST/input_output/tests/example_files/input_decks/Elliptic_Wind.wnd rename to openfast_toolbox/io/tests/example_files/input_decks/Elliptic_Wind.wnd diff --git a/pyFAST/input_output/tests/example_files/input_decks/Main_EllipticalWingInf_OLAF.dvr b/openfast_toolbox/io/tests/example_files/input_decks/Main_EllipticalWingInf_OLAF.dvr similarity index 100% rename from pyFAST/input_output/tests/example_files/input_decks/Main_EllipticalWingInf_OLAF.dvr rename to openfast_toolbox/io/tests/example_files/input_decks/Main_EllipticalWingInf_OLAF.dvr diff --git a/pyFAST/input_output/tests/example_files/input_decks/Polar2PiAlpha_AD15.dat b/openfast_toolbox/io/tests/example_files/input_decks/Polar2PiAlpha_AD15.dat similarity index 100% rename from pyFAST/input_output/tests/example_files/input_decks/Polar2PiAlpha_AD15.dat rename to openfast_toolbox/io/tests/example_files/input_decks/Polar2PiAlpha_AD15.dat diff --git a/pyFAST/input_output/tests/helpers_for_test.py b/openfast_toolbox/io/tests/helpers_for_test.py similarity index 100% rename from pyFAST/input_output/tests/helpers_for_test.py rename to openfast_toolbox/io/tests/helpers_for_test.py diff --git a/pyFAST/input_output/tests/test_bladed.py b/openfast_toolbox/io/tests/test_bladed.py similarity index 98% rename from pyFAST/input_output/tests/test_bladed.py rename to openfast_toolbox/io/tests/test_bladed.py index 3f56ef4..fb29693 100644 --- a/pyFAST/input_output/tests/test_bladed.py +++ b/openfast_toolbox/io/tests/test_bladed.py @@ -5,7 +5,7 @@ import unittest from .helpers_for_test import MyDir, reading_test -from pyFAST.input_output.bladed_out_file import BladedFile +from openfast_toolbox.io.bladed_out_file import BladedFile diff --git a/pyFAST/input_output/tests/test_csv.py b/openfast_toolbox/io/tests/test_csv.py similarity index 94% rename from pyFAST/input_output/tests/test_csv.py rename to openfast_toolbox/io/tests/test_csv.py index 7170a17..c7344a4 100644 --- a/pyFAST/input_output/tests/test_csv.py +++ b/openfast_toolbox/io/tests/test_csv.py @@ -1,8 +1,8 @@ import unittest import os import numpy as np -from pyFAST.input_output.tests.helpers_for_test import MyDir, reading_test -from pyFAST.input_output import CSVFile +from openfast_toolbox.io.tests.helpers_for_test import MyDir, reading_test +from openfast_toolbox.io import CSVFile class Test(unittest.TestCase): diff --git a/pyFAST/input_output/tests/test_fast_input.py b/openfast_toolbox/io/tests/test_fast_input.py similarity index 95% rename from pyFAST/input_output/tests/test_fast_input.py rename to openfast_toolbox/io/tests/test_fast_input.py index 731104f..21be156 100644 --- a/pyFAST/input_output/tests/test_fast_input.py +++ b/openfast_toolbox/io/tests/test_fast_input.py @@ -1,12 +1,12 @@ import unittest import os import numpy as np -from pyFAST.input_output.tests.helpers_for_test import MyDir, reading_test -from pyFAST.input_output.fast_input_file import FASTInputFile -from pyFAST.input_output.fast_input_file import ExtPtfmFile -from pyFAST.input_output.fast_input_file import ADPolarFile -from pyFAST.input_output.fast_input_file import EDBladeFile -from pyFAST.input_output.fast_wind_file import FASTWndFile +from openfast_toolbox.io.tests.helpers_for_test import MyDir, reading_test +from openfast_toolbox.io.fast_input_file import FASTInputFile +from openfast_toolbox.io.fast_input_file import ExtPtfmFile +from openfast_toolbox.io.fast_input_file import ADPolarFile +from openfast_toolbox.io.fast_input_file import EDBladeFile +from openfast_toolbox.io.fast_wind_file import FASTWndFile class Test(unittest.TestCase): diff --git a/pyFAST/input_output/tests/test_fast_input_deck.py b/openfast_toolbox/io/tests/test_fast_input_deck.py similarity index 84% rename from pyFAST/input_output/tests/test_fast_input_deck.py rename to openfast_toolbox/io/tests/test_fast_input_deck.py index 60e2439..8644633 100644 --- a/pyFAST/input_output/tests/test_fast_input_deck.py +++ b/openfast_toolbox/io/tests/test_fast_input_deck.py @@ -1,8 +1,8 @@ import unittest import os import numpy as np -from pyFAST.input_output.tests.helpers_for_test import MyDir, reading_test -from pyFAST.input_output.fast_input_deck import FASTInputDeck +from openfast_toolbox.io.tests.helpers_for_test import MyDir, reading_test +from openfast_toolbox.io.fast_input_deck import FASTInputDeck class Test(unittest.TestCase): diff --git a/pyFAST/input_output/tests/test_fast_linearization.py b/openfast_toolbox/io/tests/test_fast_linearization.py similarity index 94% rename from pyFAST/input_output/tests/test_fast_linearization.py rename to openfast_toolbox/io/tests/test_fast_linearization.py index 4c607ac..3b7bb58 100644 --- a/pyFAST/input_output/tests/test_fast_linearization.py +++ b/openfast_toolbox/io/tests/test_fast_linearization.py @@ -1,8 +1,8 @@ import unittest import os import numpy as np -from pyFAST.input_output.tests.helpers_for_test import MyDir, reading_test -from pyFAST.input_output import FASTLinearizationFile +from openfast_toolbox.io.tests.helpers_for_test import MyDir, reading_test +from openfast_toolbox.io import FASTLinearizationFile class Test(unittest.TestCase): diff --git a/pyFAST/input_output/tests/test_fast_output.py b/openfast_toolbox/io/tests/test_fast_output.py similarity index 92% rename from pyFAST/input_output/tests/test_fast_output.py rename to openfast_toolbox/io/tests/test_fast_output.py index 81b94ed..91d14e7 100644 --- a/pyFAST/input_output/tests/test_fast_output.py +++ b/openfast_toolbox/io/tests/test_fast_output.py @@ -1,8 +1,8 @@ import unittest import os import numpy as np -from pyFAST.input_output.tests.helpers_for_test import MyDir, reading_test -from pyFAST.input_output import FASTOutputFile +from openfast_toolbox.io.tests.helpers_for_test import MyDir, reading_test +from openfast_toolbox.io import FASTOutputFile class Test(unittest.TestCase): diff --git a/pyFAST/input_output/tests/test_fast_summary.py b/openfast_toolbox/io/tests/test_fast_summary.py similarity index 92% rename from pyFAST/input_output/tests/test_fast_summary.py rename to openfast_toolbox/io/tests/test_fast_summary.py index 803fa30..8988d84 100644 --- a/pyFAST/input_output/tests/test_fast_summary.py +++ b/openfast_toolbox/io/tests/test_fast_summary.py @@ -1,8 +1,8 @@ import unittest import os import numpy as np -from pyFAST.input_output.tests.helpers_for_test import MyDir, reading_test -from pyFAST.input_output import FASTSummaryFile +from openfast_toolbox.io.tests.helpers_for_test import MyDir, reading_test +from openfast_toolbox.io import FASTSummaryFile class Test(unittest.TestCase): diff --git a/pyFAST/input_output/tests/test_flex.py b/openfast_toolbox/io/tests/test_flex.py similarity index 88% rename from pyFAST/input_output/tests/test_flex.py rename to openfast_toolbox/io/tests/test_flex.py index 6a31e27..5bb316c 100644 --- a/pyFAST/input_output/tests/test_flex.py +++ b/openfast_toolbox/io/tests/test_flex.py @@ -1,11 +1,11 @@ import unittest import os import numpy as np -from pyFAST.input_output.tests.helpers_for_test import MyDir, reading_test -from pyFAST.input_output.flex_profile_file import FLEXProfileFile -from pyFAST.input_output.flex_blade_file import FLEXBladeFile -from pyFAST.input_output.flex_wavekin_file import FLEXWaveKinFile -from pyFAST.input_output.flex_doc_file import FLEXDocFile +from openfast_toolbox.io.tests.helpers_for_test import MyDir, reading_test +from openfast_toolbox.io.flex_profile_file import FLEXProfileFile +from openfast_toolbox.io.flex_blade_file import FLEXBladeFile +from openfast_toolbox.io.flex_wavekin_file import FLEXWaveKinFile +from openfast_toolbox.io.flex_doc_file import FLEXDocFile import pandas as pd diff --git a/pyFAST/input_output/tests/test_hawc.py b/openfast_toolbox/io/tests/test_hawc.py similarity index 90% rename from pyFAST/input_output/tests/test_hawc.py rename to openfast_toolbox/io/tests/test_hawc.py index 3c98772..5f33eab 100644 --- a/pyFAST/input_output/tests/test_hawc.py +++ b/openfast_toolbox/io/tests/test_hawc.py @@ -1,14 +1,14 @@ import unittest import os import numpy as np -import pyFAST.input_output as weio -from pyFAST.input_output.tests.helpers_for_test import MyDir, reading_test -from pyFAST.input_output.hawc2_dat_file import HAWC2DatFile -from pyFAST.input_output.hawc2_ae_file import HAWC2AEFile -from pyFAST.input_output.hawc2_pc_file import HAWC2PCFile -from pyFAST.input_output.hawc2_st_file import HAWC2StFile -from pyFAST.input_output.hawcstab2_ind_file import HAWCStab2IndFile -from pyFAST.input_output.hawcstab2_pwr_file import HAWCStab2PwrFile +import openfast_toolbox.io as weio +from openfast_toolbox.io.tests.helpers_for_test import MyDir, reading_test +from openfast_toolbox.io.hawc2_dat_file import HAWC2DatFile +from openfast_toolbox.io.hawc2_ae_file import HAWC2AEFile +from openfast_toolbox.io.hawc2_pc_file import HAWC2PCFile +from openfast_toolbox.io.hawc2_st_file import HAWC2StFile +from openfast_toolbox.io.hawcstab2_ind_file import HAWCStab2IndFile +from openfast_toolbox.io.hawcstab2_pwr_file import HAWCStab2PwrFile class Test(unittest.TestCase): diff --git a/pyFAST/input_output/tests/test_mannbox.py b/openfast_toolbox/io/tests/test_mannbox.py similarity index 86% rename from pyFAST/input_output/tests/test_mannbox.py rename to openfast_toolbox/io/tests/test_mannbox.py index f8a6712..8c77b30 100644 --- a/pyFAST/input_output/tests/test_mannbox.py +++ b/openfast_toolbox/io/tests/test_mannbox.py @@ -1,8 +1,8 @@ import unittest import os import numpy as np -from pyFAST.input_output.tests.helpers_for_test import MyDir, reading_test -from pyFAST.input_output.mannbox_file import MannBoxFile +from openfast_toolbox.io.tests.helpers_for_test import MyDir, reading_test +from openfast_toolbox.io.mannbox_file import MannBoxFile class Test(unittest.TestCase): diff --git a/pyFAST/input_output/tests/test_parquet.py b/openfast_toolbox/io/tests/test_parquet.py similarity index 85% rename from pyFAST/input_output/tests/test_parquet.py rename to openfast_toolbox/io/tests/test_parquet.py index 42d4428..6a6f31b 100644 --- a/pyFAST/input_output/tests/test_parquet.py +++ b/openfast_toolbox/io/tests/test_parquet.py @@ -1,7 +1,7 @@ import unittest import os -from pyFAST.input_output.tests.helpers_for_test import MyDir, reading_test -from pyFAST.input_output.parquet_file import ParquetFile +from openfast_toolbox.io.tests.helpers_for_test import MyDir, reading_test +from openfast_toolbox.io.parquet_file import ParquetFile class Test(unittest.TestCase): diff --git a/pyFAST/input_output/tests/test_run_Examples.py b/openfast_toolbox/io/tests/test_run_Examples.py similarity index 100% rename from pyFAST/input_output/tests/test_run_Examples.py rename to openfast_toolbox/io/tests/test_run_Examples.py diff --git a/pyFAST/input_output/tests/test_turbsim.py b/openfast_toolbox/io/tests/test_turbsim.py similarity index 93% rename from pyFAST/input_output/tests/test_turbsim.py rename to openfast_toolbox/io/tests/test_turbsim.py index 897f3c3..7a5345b 100644 --- a/pyFAST/input_output/tests/test_turbsim.py +++ b/openfast_toolbox/io/tests/test_turbsim.py @@ -1,8 +1,8 @@ import unittest import os import numpy as np -from pyFAST.input_output.tests.helpers_for_test import MyDir, reading_test -from pyFAST.input_output import TurbSimFile +from openfast_toolbox.io.tests.helpers_for_test import MyDir, reading_test +from openfast_toolbox.io import TurbSimFile class Test(unittest.TestCase): diff --git a/pyFAST/input_output/tests/test_vtk.py b/openfast_toolbox/io/tests/test_vtk.py similarity index 83% rename from pyFAST/input_output/tests/test_vtk.py rename to openfast_toolbox/io/tests/test_vtk.py index 7b52f71..e0bbf89 100644 --- a/pyFAST/input_output/tests/test_vtk.py +++ b/openfast_toolbox/io/tests/test_vtk.py @@ -1,8 +1,8 @@ import unittest import os import numpy as np -from pyFAST.input_output.tests.helpers_for_test import MyDir, reading_test -from pyFAST.input_output.vtk_file import VTKFile +from openfast_toolbox.io.tests.helpers_for_test import MyDir, reading_test +from openfast_toolbox.io.vtk_file import VTKFile diff --git a/pyFAST/input_output/tests/test_yaml.py b/openfast_toolbox/io/tests/test_yaml.py similarity index 98% rename from pyFAST/input_output/tests/test_yaml.py rename to openfast_toolbox/io/tests/test_yaml.py index d221975..d14d99d 100644 --- a/pyFAST/input_output/tests/test_yaml.py +++ b/openfast_toolbox/io/tests/test_yaml.py @@ -1,7 +1,7 @@ import unittest import numpy as np import os as os -from pyFAST.input_output.mini_yaml import * +from openfast_toolbox.io.mini_yaml import * class Test(unittest.TestCase): diff --git a/pyFAST/input_output/tools/__init__.py b/openfast_toolbox/io/tools/__init__.py similarity index 100% rename from pyFAST/input_output/tools/__init__.py rename to openfast_toolbox/io/tools/__init__.py diff --git a/pyFAST/input_output/tools/graph.py b/openfast_toolbox/io/tools/graph.py similarity index 100% rename from pyFAST/input_output/tools/graph.py rename to openfast_toolbox/io/tools/graph.py diff --git a/pyFAST/input_output/turbsim_file.py b/openfast_toolbox/io/turbsim_file.py similarity index 97% rename from pyFAST/input_output/turbsim_file.py rename to openfast_toolbox/io/turbsim_file.py index 098c05e..0259c7d 100644 --- a/pyFAST/input_output/turbsim_file.py +++ b/openfast_toolbox/io/turbsim_file.py @@ -1,1091 +1,1091 @@ -"""Read/Write TurbSim File - -Part of weio library: https://github.com/ebranlard/weio - -""" -import pandas as pd -import numpy as np -import os -import struct -import time - -try: - from .file import File, EmptyFileError -except: - EmptyFileError = type('EmptyFileError', (Exception,),{}) - File=dict - -class TurbSimFile(File): - """ - Read/write a TurbSim turbulence file (.bts). The object behaves as a dictionary. - - Main keys - --------- - - 'u': velocity field, shape (3 x nt x ny x nz) - - 'y', 'z', 't': space and time coordinates - - 'dt', 'ID', 'info' - - 'zTwr', 'uTwr': tower coordinates and field if present (3 x nt x nTwr) - - 'zRef', 'uRef': height and velocity at a reference point (usually not hub) - - Main methods - ------------ - - read, write, toDataFrame, keys - - valuesAt, vertProfile, horizontalPlane, verticalPlane, closestPoint - - fitPowerLaw - - makePeriodic, checkPeriodic - - Examples - -------- - - ts = TurbSimFile('Turb.bts') - print(ts.keys()) - print(ts['u'].shape) - u,v,w = ts.valuesAt(y=10.5, z=90) - - - """ - - @staticmethod - def defaultExtensions(): - return ['.bts'] - - @staticmethod - def formatName(): - return 'TurbSim binary' - - def __init__(self, filename=None, **kwargs): - self.filename = None - if filename: - self.read(filename, **kwargs) - - def read(self, filename=None, header_only=False, tdecimals=8): - """ read BTS file, with field: - u (3 x nt x ny x nz) - uTwr (3 x nt x nTwr) - """ - if filename: - self.filename = filename - if not self.filename: - raise Exception('No filename provided') - if not os.path.isfile(self.filename): - raise OSError(2,'File not found:',self.filename) - if os.stat(self.filename).st_size == 0: - raise EmptyFileError('File is empty:',self.filename) - - scl = np.zeros(3, np.float32); off = np.zeros(3, np.float32) - with open(self.filename, mode='rb') as f: - # Reading header info - ID, nz, ny, nTwr, nt = struct.unpack('0) - for it in range(nt): - Buffer = np.frombuffer(f.read(2*3*ny*nz), dtype=np.int16).astype(np.float32).reshape([3, ny, nz], order='F') - u[:,it,:,:]=Buffer - Buffer = np.frombuffer(f.read(2*3*nTwr), dtype=np.int16).astype(np.float32).reshape([3, nTwr], order='F') - uTwr[:,it,:]=Buffer - u -= off[:, None, None, None] - u /= scl[:, None, None, None] - self['u'] = u - uTwr -= off[:, None, None] - uTwr /= scl[:, None, None] - self['uTwr'] = uTwr - self['info'] = info - self['ID'] = ID - self['dt'] = np.round(dt, tdecimals) # dt is stored in single precision in the TurbSim output - self['y'] = np.arange(ny)*dy - self['y'] -= np.mean(self['y']) # y always centered on 0 - self['z'] = np.arange(nz)*dz +zBottom - self['t'] = np.round(np.arange(nt)*dt, tdecimals) - self['zTwr'] =-np.arange(nTwr)*dz + zBottom - self['zRef'] = zHub - self['uRef'] = uHub - - def write(self, filename=None): - """ - write a BTS file, using the following keys: 'u','z','y','t','uTwr' - u (3 x nt x ny x nz) - uTwr (3 x nt x nTwr) - """ - if filename: - self.filename = filename - if not self.filename: - raise Exception('No filename provided') - - nDim, nt, ny, nz = self['u'].shape - if 'uTwr' not in self.keys() : - self['uTwr']=np.zeros((3,nt,0)) - if 'ID' not in self.keys() : - self['ID']=7 - - _, _, nTwr = self['uTwr'].shape - tsTwr = self['uTwr'] - ts = self['u'] - intmin = -32768 - intrng = 65535 - off = np.empty((3), dtype = np.float32) - scl = np.empty((3), dtype = np.float32) - info = 'Generated by TurbSimFile on {:s}.'.format(time.strftime('%d-%b-%Y at %H:%M:%S', time.localtime())) - # Calculate scaling, offsets and scaling data - out = np.empty(ts.shape, dtype=np.int16) - outTwr = np.empty(tsTwr.shape, dtype=np.int16) - for k in range(3): - all_min, all_max = ts[k].min(), ts[k].max() - if nTwr>0: - all_min=min(all_min, tsTwr[k].min()) - all_max=max(all_max, tsTwr[k].max()) - if all_min == all_max: - scl[k] = 1 - else: - scl[k] = intrng / (all_max-all_min) - off[k] = intmin - scl[k] * all_min - out[k] = (ts[k] * scl[k] + off[k]).astype(np.int16) - outTwr[k] = (tsTwr[k] * scl[k] + off[k]).astype(np.int16) - z0 = self['z'][0] - dz = self['z'][1]- self['z'][0] - dy = self['y'][1]- self['y'][0] - dt = self['t'][1]- self['t'][0] - - # Providing estimates of uHub and zHub even if these fields are not used - zHub,uHub, bHub = self.hubValues() - - with open(self.filename, mode='wb') as f: - f.write(struct.pack('0: - s+=' - zTwr: [{} ... {}], dz: {}, n: {} \n'.format(self['zTwr'][0],self['zTwr'][-1],self['zTwr'][1]-self['zTwr'][0],len(self['zTwr'])) - if 'uTwr' in self.keys() and self['uTwr'].shape[2]>0: - s+=' - uTwr: ({} x {} x {} ) \n'.format(*(self['uTwr'].shape)) - ux,uy,uz=self['uTwr'][0], self['uTwr'][1], self['uTwr'][2] - s+=' ux: min: {}, max: {}, mean: {} \n'.format(np.min(ux), np.max(ux), np.mean(ux)) - s+=' uy: min: {}, max: {}, mean: {} \n'.format(np.min(uy), np.max(uy), np.mean(uy)) - s+=' uz: min: {}, max: {}, mean: {} \n'.format(np.min(uz), np.max(uz), np.mean(uz)) - s += ' Useful methods:\n' - s += ' - read, write, toDataFrame, keys\n' - s += ' - valuesAt, vertProfile, horizontalPlane, verticalPlane, closestPoint\n' - s += ' - fitPowerLaw\n' - s += ' - makePeriodic, checkPeriodic\n' - return s - - def toDataFrame(self): - dfs={} - - ny = len(self['y']) - nz = len(self['y']) - # Index at mid box - iy,iz = self.iMid - - # Mean vertical profile - z, m, s = self.vertProfile() - ti = s/m*100 - cols=['z_[m]','u_[m/s]','v_[m/s]','w_[m/s]','sigma_u_[m/s]','sigma_v_[m/s]','sigma_w_[m/s]','TI_[%]'] - data = np.column_stack((z, m[0,:],m[1,:],m[2,:],s[0,:],s[1,:],s[2,:],ti[0,:])) - dfs['VertProfile'] = pd.DataFrame(data = data ,columns = cols) - - # Mid time series - u = self['u'][:,:,iy,iz] - cols=['t_[s]','u_[m/s]','v_[m/s]','w_[m/s]'] - data = np.column_stack((self['t'],u[0,:],u[1,:],u[2,:])) - dfs['ZMidLine'] = pd.DataFrame(data = data ,columns = cols) - - - # ZMid YStart time series - u = self['u'][:,:,0,iz] - cols=['t_[s]','u_[m/s]','v_[m/s]','w_[m/s]'] - data = np.column_stack((self['t'],u[0,:],u[1,:],u[2,:])) - dfs['ZMidYStartLine'] = pd.DataFrame(data = data ,columns = cols) - - # ZMid YEnd time series - u = self['u'][:,:,-1,iz] - cols=['t_[s]','u_[m/s]','v_[m/s]','w_[m/s]'] - data = np.column_stack((self['t'],u[0,:],u[1,:],u[2,:])) - dfs['ZMidYEndLine'] = pd.DataFrame(data = data ,columns = cols) - - # Mid crosscorr y - y, rho_uu_y, rho_vv_y, rho_ww_y = self.crosscorr_y() - cols = ['y_[m]', 'rho_uu_[-]','rho_vv_[-]','rho_ww_[-]'] - data = np.column_stack((y, rho_uu_y, rho_vv_y, rho_ww_y)) - dfs['Mid_xcorr_y'] = pd.DataFrame(data = data ,columns = cols) - - # Mid crosscorr z - z, rho_uu_z, rho_vv_z, rho_ww_z = self.crosscorr_z() - cols = ['z_[m]', 'rho_uu_[-]','rho_vv_[-]','rho_ww_[-]'] - data = np.column_stack((z, rho_uu_z, rho_vv_z, rho_ww_z)) - dfs['Mid_xcorr_z'] = pd.DataFrame(data = data ,columns = cols) - - # Mid csd - try: - fc, chi_uu, chi_vv, chi_ww = self.csd_longi() - cols = ['f_[Hz]','chi_uu_[-]', 'chi_vv_[-]','chi_ww_[-]'] - data = np.column_stack((fc, chi_uu, chi_vv, chi_ww)) - dfs['Mid_csd_longi'] = pd.DataFrame(data = data ,columns = cols) - - # Mid csd - fc, chi_uu, chi_vv, chi_ww = self.csd_lat() - cols = ['f_[Hz]','chi_uu_[-]', 'chi_vv_[-]','chi_ww_[-]'] - data = np.column_stack((fc, chi_uu, chi_vv, chi_ww)) - dfs['Mid_csd_lat'] = pd.DataFrame(data = data ,columns = cols) - - # Mid csd - fc, chi_uu, chi_vv, chi_ww = self.csd_vert() - cols = ['f_[Hz]','chi_uu_[-]', 'chi_vv_[-]','chi_ww_[-]'] - data = np.column_stack((fc, chi_uu, chi_vv, chi_ww)) - dfs['Mid_csd_vert'] = pd.DataFrame(data = data ,columns = cols) - except ModuleNotFoundError: - print('Module scipy.signal not available') - except ImportError: - print('Likely issue with fftpack') - - - # Hub time series - #try: - # zHub = self['zHub'] - # iz = np.argmin(np.abs(self['z']-zHub)) - # u = self['u'][:,:,iy,iz] - # Cols=['t_[s]','u_[m/s]','v_[m/s]','w_[m/s]'] - # data = np.column_stack((self['t'],u[0,:],u[1,:],u[2,:])) - # dfs['TSHubLine'] = pd.DataFrame(data = data ,columns = Cols) - #except: - # pass - return dfs - - def toDataset(self): - """ - Convert the data that was read in into a xarray Dataset - - # TODO SORT OUT THE DIFFERENCE WITH toDataSet - """ - from xarray import IndexVariable, DataArray, Dataset - - print('[TODO] pyFAST.input_output.turbsim_file.toDataset: merge with function toDataSet') - - y = IndexVariable("y", self.y, attrs={"description":"lateral coordinate","units":"m"}) - zround = np.asarray([np.round(zz,6) for zz in self.z]) #the open function here returns something like *.0000000001 which is annoying - z = IndexVariable("z", zround, attrs={"description":"vertical coordinate","units":"m"}) - time = IndexVariable("time", self.t, attrs={"description":"time since start of simulation","units":"s"}) - - da = {} - for component,direction,velname in zip([0,1,2],["x","y","z"],["u","v","w"]): - # the dataset produced here has y/z axes swapped relative to data stored in original object - velocity = np.swapaxes(self["u"][component,...],1,2) - da[velname] = DataArray(velocity, - coords={"time":time,"y":y,"z":z}, - dims=["time","y","z"], - name="velocity", - attrs={"description":"velocity along {0}".format(direction),"units":"m/s"}) - - return Dataset(data_vars=da, coords={"time":time,"y":y,"z":z}) - - def toDataSet(self, datetime=False): - """ - Convert the data that was read in into a xarray Dataset - - # TODO SORT OUT THE DIFFERENCE WITH toDataset - """ - import xarray as xr - - print('[TODO] pyFAST.input_output.turbsim_file.toDataSet: should be discontinued') - print('[TODO] pyFAST.input_output.turbsim_file.toDataSet: merge with function toDataset') - - if datetime: - timearray = pd.to_datetime(self['t'], unit='s', origin=pd.to_datetime('2000-01-01 00:00:00')) - timestr = 'datetime' - else: - timearray = self['t'] - timestr = 'time' - - ds = xr.Dataset( - data_vars=dict( - u=([timestr,'y','z'], self['u'][0,:,:,:]), - v=([timestr,'y','z'], self['u'][1,:,:,:]), - w=([timestr,'y','z'], self['u'][2,:,:,:]), - ), - coords={ - timestr : timearray, - 'y' : self['y'], - 'z' : self['z'], - }, - ) - - # Add mean computations - ds['up'] = ds['u'] - ds['u'].mean(dim=timestr) - ds['vp'] = ds['v'] - ds['v'].mean(dim=timestr) - ds['wp'] = ds['w'] - ds['w'].mean(dim=timestr) - - if datetime: - # Add time (in s) to the variable list - ds['time'] = (('datetime'), self['t']) - - return ds - - # Useful converters - def fromAMRWind(self, filename, timestep, output_frequency, sampling_identifier, verbose=1, fileout=None, zref=None, xloc=None): - """ - Reads a AMRWind netcdf file, grabs a group of sampling planes (e.g. p_slice), - return an instance of TurbSimFile, optionally write turbsim file to disk - - - Parameters - ---------- - filename : str, - full path to netcdf file generated by amrwind - timestep : float, - amr-wind code timestep (time.fixed_dt) - output_frequency : int, - frequency chosen for sampling output in amrwind input file (sampling.output_frequency) - sampling_identifier : str, - identifier of the sampling being requested (an entry of sampling.labels in amrwind input file) - zref : float, - height to be written to turbsim as the reference height. if none is given, it is taken as the vertical centerpoint of the slice - """ - try: - from pyFAST.input_output.amrwind_file import AMRWindFile - except: - try: - from .amrwind_file import AMRWindFile - except: - from amrwind_file import AMRWindFile - - obj = AMRWindFile(filename,timestep,output_frequency, group_name=sampling_identifier) - - self["u"] = np.ndarray((3,obj.nt,obj.ny,obj.nz)) - - xloc = float(obj.data.x[0]) if xloc is None else xloc - if verbose: - print("Grabbing the slice at x={0} m".format(xloc)) - self['u'][0,:,:,:] = np.swapaxes(obj.data.u.sel(x=xloc).values,1,2) - self['u'][1,:,:,:] = np.swapaxes(obj.data.v.sel(x=xloc).values,1,2) - self['u'][2,:,:,:] = np.swapaxes(obj.data.w.sel(x=xloc).values,1,2) - self['t'] = obj.data.t.values - - self['y'] = obj.data.y.values - self['z'] = obj.data.z.values - self['dt'] = obj.output_dt - - self['ID'] = 7 - ltime = time.strftime('%d-%b-%Y at %H:%M:%S', time.localtime()) - self['info'] = 'Converted from AMRWind output file {0} {1:s}.'.format(filename,ltime) - - iz = int(obj.nz/2) - self['zRef'] = float(obj.data.z[iz]) if zref is None else zref - if verbose: - print("Setting the TurbSim file reference height to z={0} m".format(self["zRef"])) - - self['uRef'] = float(obj.data.u.sel(x=xloc).sel(y=0).sel(z=self["zRef"]).mean().values) - self['zRef'], self['uRef'], bHub = self.hubValues() - - if fileout is not None: - filebase = os.path.splitext(filename)[1] - fileout = filebase+".bts" - if verbose: - print("===> {0}".format(fileout)) - self.write(fileout) - - - def fromAMRWind_legacy(self, filename, dt, nt, y, z, sampling_identifier='p_sw2'): - """ - Convert current TurbSim file into one generated from AMR-Wind LES sampling data in .nc format - Assumes: - -- u, v, w (nt, nx * ny * nz) - -- u is aligned with x-axis (flow is not rotated) - this consideration needs to be added - - - INPUTS: - - filename: (string) full path to .nc sampling data file - - sampling_identifier: (string) name of sampling plane group from .inp file (e.g. "p_sw2") - - dt: timestep size [s] - - nt: number of timesteps (sequential) you want to read in, starting at the first timestep available - INPUTS: TODO - - y: user-defined vector of coordinate positions in y - - z: user-defined vector of coordinate positions in z - - uref: (float) reference mean velocity (e.g. 8.0 hub height mean velocity from input file) - - zref: (float) hub height (e.t. 150.0) - """ - import xarray as xr - - print('[TODO] fromAMRWind_legacy: function might be unfinished. Merge with fromAMRWind') - print('[TODO] fromAMRWind_legacy: figure out y, and z from data (see fromAMRWind)') - - # read in sampling data plane - ds = xr.open_dataset(filename, - engine='netcdf4', - group=sampling_identifier) - ny, nz, _ = ds.attrs['ijk_dims'] - noffsets = len(ds.attrs['offsets']) - t = np.arange(0, dt*(nt-0.5), dt) - print('max time [s] = ', t[-1]) - - self['u']=np.ndarray((3,nt,ny,nz)) #, buffer=shm.buf) - # read in AMRWind velocity data - self['u'][0,:,:,:] = ds['velocityx'].isel(num_time_steps=slice(0,nt)).values.reshape(nt,noffsets,ny,nz)[:,1,:,:] # last index = 1 refers to 2nd offset plane at -1200 m - self['u'][1,:,:,:] = ds['velocityy'].isel(num_time_steps=slice(0,nt)).values.reshape(nt,noffsets,ny,nz)[:,1,:,:] - self['u'][2,:,:,:] = ds['velocityz'].isel(num_time_steps=slice(0,nt)).values.reshape(nt,noffsets,ny,nz)[:,1,:,:] - self['t'] = t - self['y'] = y - self['z'] = z - self['dt'] = dt - # TODO - self['ID'] = 7 # ... - self['info'] = 'Converted from AMRWind fields {:s}.'.format(time.strftime('%d-%b-%Y at %H:%M:%S', time.localtime())) -# self['zTwr'] = np.array([]) -# self['uTwr'] = np.array([]) - self['zRef'] = zref #None - self['uRef'] = uref #None - self['zRef'], self['uRef'], bHub = self.hubValues() - - def fromMannBox(self, u, v, w, dx, U, y, z, addU=None): - """ - Convert current TurbSim file into one generated from MannBox - Assumes: - u, v, w (nt x ny x nz) - - y: goes from -ly/2 to ly/2 this is an IMPORTANT subtlety - The field u needs to respect this convention! - (fields from weio.mannbox_file do respect this convention - but when exported to binary files, the y axis is flipped again) - - INPUTS: - - u, v, w : mann box fields - - dx: axial spacing of mann box (to compute time) - - U: reference speed of mann box (to compute time) - - y: y coords of mann box - - z: z coords of mann box - """ - nt,ny,nz = u.shape - dt = dx/U - t = np.arange(0, dt*(nt-0.5), dt) - nt = len(t) - if y[0]>y[-1]: - raise Exception('y is assumed to go from - to +') - - self['u']=np.zeros((3, nt, ny, nz)) - self['u'][0,:,:,:] = u - self['u'][1,:,:,:] = v - self['u'][2,:,:,:] = w - if addU is not None: - self['u'][0,:,:,:] += addU - self['t'] = t - self['y'] = y - self['z'] = z - self['dt'] = dt - # TODO - self['ID'] = 7 # ... - self['info'] = 'Converted from MannBox fields {:s}.'.format(time.strftime('%d-%b-%Y at %H:%M:%S', time.localtime())) -# self['zTwr'] = np.array([]) -# self['uTwr'] = np.array([]) - self['zRef'] = None - self['uRef'] = None - self['zRef'], self['uRef'], bHub = self.hubValues() - - def toMannBox(self, base=None, removeUConstant=None, removeAllUMean=False): - """ - removeUConstant: float, will be removed from all values of the U box - removeAllUMean: If true, the time-average of each y-z points will be substracted - """ - try: - from weio.mannbox_file import MannBoxFile - except: - try: - from .mannbox_file import MannBoxFile - except: - from mannbox_file import MannBoxFile - # filename - if base is None: - base = os.path.splitext(self.filename)[0] - base = base+'_{}x{}x{}'.format(*self['u'].shape[1:]) - - mn = MannBoxFile() - mn.fromTurbSim(self['u'], 0, removeConstant=removeUConstant, removeAllMean=removeAllUMean) - mn.write(base+'.u') - - mn.fromTurbSim(self['u'], 1) - mn.write(base+'.v') - - mn.fromTurbSim(self['u'], 2) - mn.write(base+'.w') - - # --- Useful IO - def writeInfo(ts, filename): - """ Write info to txt """ - infofile = filename - with open(filename,'w') as f: - f.write(str(ts)) - zMid =(ts['z'][0]+ts['z'][-1])/2 - f.write('Middle height of box: {:.3f}\n'.format(zMid)) - y_fit, pfit, model, _ = ts.fitPowerLaw(z_ref=zMid, y_span='mid', U_guess=10, alpha_guess=0.1) - f.write('Power law: alpha={:.5f} - u={:.5f} at z={:.5f}\n'.format(pfit[1],pfit[0],zMid)) - f.write('Periodic: {}\n'.format(ts.checkPeriodic(sigmaTol=1.5, aTol=0.5))) - - - - def writeProbes(ts, probefile, yProbe, zProbe): - """ Create a CSV file with wind speed data at given probe locations - defined by the vectors yProbe and zProbe. All combinations of y and z are extracted. - INPUTS: - - probefile: filename of CSV file to be written - - yProbe: array like of y locations - - zProbe: array like of z locations - """ - Columns=['Time_[s]'] - Data = ts['t'] - for y in yProbe: - for z in zProbe: - iy = np.argmin(np.abs(ts['y']-y)) - iz = np.argmin(np.abs(ts['z']-z)) - lbl = '_y{:.0f}_z{:.0f}'.format(ts['y'][iy], ts['z'][iz]) - Columns+=['{}{}_[m/s]'.format(c,lbl) for c in['u','v','w']] - DataSub = np.column_stack((ts['u'][0,:,iy,iz],ts['u'][1,:,iy,iz],ts['u'][2,:,iy,iz])) - Data = np.column_stack((Data, DataSub)) - np.savetxt(probefile, Data, header=','.join(Columns), delimiter=',') - - def fitPowerLaw(ts, z_ref=None, y_span='full', U_guess=10, alpha_guess=0.1): - """ - Fit power law to vertical profile - INPUTS: - - z_ref: reference height used to define the "U_ref" - - y_span: if 'full', average the vertical profile accross all y-values - if 'mid', average the vertical profile at the middle y value - """ - if z_ref is None: - # use mid height for z_ref - z_ref =(ts['z'][0]+ts['z'][-1])/2 - # Average time series - z, u, _ = ts.vertProfile(y_span=y_span) - u = u[0,:] - u_fit, pfit, model = fit_powerlaw_u_alpha(z, u, z_ref=z_ref, p0=(U_guess, alpha_guess)) - return u_fit, pfit, model, z_ref - -# Functions from BTS_File.py to be ported here -# def TI(self,y=None,z=None,j=None,k=None): -# """ -# If no argument is given, compute TI over entire grid and return array of size (ny,nz). Else, compute TI at the specified point. -# -# Parameters -# ---------- -# y : float, -# cross-stream position [m] -# z : float, -# vertical position AGL [m] -# j : int, -# grid index along cross-stream -# k : int, -# grid index along vertical -# """ -# if ((y==None) & (j==None)): -# return np.std(self.U,axis=0) / np.mean(self.U,axis=0) -# if ((y==None) & (j!=None)): -# return (np.std(self.U[:,j,k])/np.mean(self.U[:,j,k])) -# if ((y!=None) & (j==None)): -# uSeries = self.U[:,self.y2j(y),self.z2k(z)] -# return np.std(uSeries)/np.mean(uSeries) -# -# def visualize(self,component='U',time=0): -# """ -# Quick peak at the data for a given component, at a specific time. -# """ -# data = getattr(self,component)[time,:,:] -# plt.figure() ; -# plt.imshow(data) ; -# plt.colorbar() -# plt.show() -# -# def spectrum(self,component='u',y=None,z=None): -# """ -# Calculate spectrum of a specific component, given time series at ~ hub. -# -# Parameters -# ---------- -# component : string, -# which component to use -# y : float, -# y coordinate [m] of specific location -# z : float, -# z coordinate [m] of specific location -# -# """ -# if y==None: -# k = self.kHub -# j = self.jHub -# data = getattr(self,component) -# data = data[:,j,k] -# N = data.size -# freqs = fftpack.fftfreq(N,self.dT)[1:N/2] -# psd = (np.abs(fftpack.fft(data,N)[1:N/2]))**2 -# return [freqs, psd] -# -# def getRotorPoints(self): -# """ -# In the square y-z slice, return which points are at the edge of the rotor in the horizontal and vertical directions. -# -# Returns -# ------- -# jLeft : int, -# index for grid point that matches the left side of the rotor (when looking towards upstream) -# jRight : int, -# index for grid point that matches the right side of the rotor (when looking towards upstream) -# kBot : int, -# index for grid point that matches the bottom of the rotor -# kTop : int, -# index for grid point that matches the top of the rotor -# """ -# self.zBotRotor = self.zHub - self.R -# self.zTopRotor = self.zHub + self.R -# self.yLeftRotor = self.yHub - self.R -# self.yRightRotor = self.yHub + self.R -# self.jLeftRotor = self.y2j(self.yLeftRotor) -# self.jRightRotor = self.y2j(self.yRightRotor) -# self.kBotRotor = self.z2k(self.zBotRotor) -# self.kTopRotor = self.z2k(self.zTopRotor) -# - - - -def fit_powerlaw_u_alpha(x, y, z_ref=100, p0=(10,0.1)): - """ - p[0] : u_ref - p[1] : alpha - """ - import scipy.optimize as so - pfit, _ = so.curve_fit(lambda x, *p : p[0] * (x / z_ref) ** p[1], x, y, p0=p0) - y_fit = pfit[0] * (x / z_ref) ** pfit[1] - coeffs_dict={'u_ref':pfit[0],'alpha':pfit[1]} - formula = '{u_ref} * (z / {z_ref}) ** {alpha}' - fitted_fun = lambda xx: pfit[0] * (xx / z_ref) ** pfit[1] - return y_fit, pfit, {'coeffs':coeffs_dict,'formula':formula,'fitted_function':fitted_fun} - -if __name__=='__main__': - ts = TurbSimFile('../_tests/TurbSim.bts') - - +"""Read/Write TurbSim File + +Part of weio library: https://github.com/ebranlard/weio + +""" +import pandas as pd +import numpy as np +import os +import struct +import time + +try: + from .file import File, EmptyFileError +except: + EmptyFileError = type('EmptyFileError', (Exception,),{}) + File=dict + +class TurbSimFile(File): + """ + Read/write a TurbSim turbulence file (.bts). The object behaves as a dictionary. + + Main keys + --------- + - 'u': velocity field, shape (3 x nt x ny x nz) + - 'y', 'z', 't': space and time coordinates + - 'dt', 'ID', 'info' + - 'zTwr', 'uTwr': tower coordinates and field if present (3 x nt x nTwr) + - 'zRef', 'uRef': height and velocity at a reference point (usually not hub) + + Main methods + ------------ + - read, write, toDataFrame, keys + - valuesAt, vertProfile, horizontalPlane, verticalPlane, closestPoint + - fitPowerLaw + - makePeriodic, checkPeriodic + + Examples + -------- + + ts = TurbSimFile('Turb.bts') + print(ts.keys()) + print(ts['u'].shape) + u,v,w = ts.valuesAt(y=10.5, z=90) + + + """ + + @staticmethod + def defaultExtensions(): + return ['.bts'] + + @staticmethod + def formatName(): + return 'TurbSim binary' + + def __init__(self, filename=None, **kwargs): + self.filename = None + if filename: + self.read(filename, **kwargs) + + def read(self, filename=None, header_only=False, tdecimals=8): + """ read BTS file, with field: + u (3 x nt x ny x nz) + uTwr (3 x nt x nTwr) + """ + if filename: + self.filename = filename + if not self.filename: + raise Exception('No filename provided') + if not os.path.isfile(self.filename): + raise OSError(2,'File not found:',self.filename) + if os.stat(self.filename).st_size == 0: + raise EmptyFileError('File is empty:',self.filename) + + scl = np.zeros(3, np.float32); off = np.zeros(3, np.float32) + with open(self.filename, mode='rb') as f: + # Reading header info + ID, nz, ny, nTwr, nt = struct.unpack('0) + for it in range(nt): + Buffer = np.frombuffer(f.read(2*3*ny*nz), dtype=np.int16).astype(np.float32).reshape([3, ny, nz], order='F') + u[:,it,:,:]=Buffer + Buffer = np.frombuffer(f.read(2*3*nTwr), dtype=np.int16).astype(np.float32).reshape([3, nTwr], order='F') + uTwr[:,it,:]=Buffer + u -= off[:, None, None, None] + u /= scl[:, None, None, None] + self['u'] = u + uTwr -= off[:, None, None] + uTwr /= scl[:, None, None] + self['uTwr'] = uTwr + self['info'] = info + self['ID'] = ID + self['dt'] = np.round(dt, tdecimals) # dt is stored in single precision in the TurbSim output + self['y'] = np.arange(ny)*dy + self['y'] -= np.mean(self['y']) # y always centered on 0 + self['z'] = np.arange(nz)*dz +zBottom + self['t'] = np.round(np.arange(nt)*dt, tdecimals) + self['zTwr'] =-np.arange(nTwr)*dz + zBottom + self['zRef'] = zHub + self['uRef'] = uHub + + def write(self, filename=None): + """ + write a BTS file, using the following keys: 'u','z','y','t','uTwr' + u (3 x nt x ny x nz) + uTwr (3 x nt x nTwr) + """ + if filename: + self.filename = filename + if not self.filename: + raise Exception('No filename provided') + + nDim, nt, ny, nz = self['u'].shape + if 'uTwr' not in self.keys() : + self['uTwr']=np.zeros((3,nt,0)) + if 'ID' not in self.keys() : + self['ID']=7 + + _, _, nTwr = self['uTwr'].shape + tsTwr = self['uTwr'] + ts = self['u'] + intmin = -32768 + intrng = 65535 + off = np.empty((3), dtype = np.float32) + scl = np.empty((3), dtype = np.float32) + info = 'Generated by TurbSimFile on {:s}.'.format(time.strftime('%d-%b-%Y at %H:%M:%S', time.localtime())) + # Calculate scaling, offsets and scaling data + out = np.empty(ts.shape, dtype=np.int16) + outTwr = np.empty(tsTwr.shape, dtype=np.int16) + for k in range(3): + all_min, all_max = ts[k].min(), ts[k].max() + if nTwr>0: + all_min=min(all_min, tsTwr[k].min()) + all_max=max(all_max, tsTwr[k].max()) + if all_min == all_max: + scl[k] = 1 + else: + scl[k] = intrng / (all_max-all_min) + off[k] = intmin - scl[k] * all_min + out[k] = (ts[k] * scl[k] + off[k]).astype(np.int16) + outTwr[k] = (tsTwr[k] * scl[k] + off[k]).astype(np.int16) + z0 = self['z'][0] + dz = self['z'][1]- self['z'][0] + dy = self['y'][1]- self['y'][0] + dt = self['t'][1]- self['t'][0] + + # Providing estimates of uHub and zHub even if these fields are not used + zHub,uHub, bHub = self.hubValues() + + with open(self.filename, mode='wb') as f: + f.write(struct.pack('0: + s+=' - zTwr: [{} ... {}], dz: {}, n: {} \n'.format(self['zTwr'][0],self['zTwr'][-1],self['zTwr'][1]-self['zTwr'][0],len(self['zTwr'])) + if 'uTwr' in self.keys() and self['uTwr'].shape[2]>0: + s+=' - uTwr: ({} x {} x {} ) \n'.format(*(self['uTwr'].shape)) + ux,uy,uz=self['uTwr'][0], self['uTwr'][1], self['uTwr'][2] + s+=' ux: min: {}, max: {}, mean: {} \n'.format(np.min(ux), np.max(ux), np.mean(ux)) + s+=' uy: min: {}, max: {}, mean: {} \n'.format(np.min(uy), np.max(uy), np.mean(uy)) + s+=' uz: min: {}, max: {}, mean: {} \n'.format(np.min(uz), np.max(uz), np.mean(uz)) + s += ' Useful methods:\n' + s += ' - read, write, toDataFrame, keys\n' + s += ' - valuesAt, vertProfile, horizontalPlane, verticalPlane, closestPoint\n' + s += ' - fitPowerLaw\n' + s += ' - makePeriodic, checkPeriodic\n' + return s + + def toDataFrame(self): + dfs={} + + ny = len(self['y']) + nz = len(self['y']) + # Index at mid box + iy,iz = self.iMid + + # Mean vertical profile + z, m, s = self.vertProfile() + ti = s/m*100 + cols=['z_[m]','u_[m/s]','v_[m/s]','w_[m/s]','sigma_u_[m/s]','sigma_v_[m/s]','sigma_w_[m/s]','TI_[%]'] + data = np.column_stack((z, m[0,:],m[1,:],m[2,:],s[0,:],s[1,:],s[2,:],ti[0,:])) + dfs['VertProfile'] = pd.DataFrame(data = data ,columns = cols) + + # Mid time series + u = self['u'][:,:,iy,iz] + cols=['t_[s]','u_[m/s]','v_[m/s]','w_[m/s]'] + data = np.column_stack((self['t'],u[0,:],u[1,:],u[2,:])) + dfs['ZMidLine'] = pd.DataFrame(data = data ,columns = cols) + + + # ZMid YStart time series + u = self['u'][:,:,0,iz] + cols=['t_[s]','u_[m/s]','v_[m/s]','w_[m/s]'] + data = np.column_stack((self['t'],u[0,:],u[1,:],u[2,:])) + dfs['ZMidYStartLine'] = pd.DataFrame(data = data ,columns = cols) + + # ZMid YEnd time series + u = self['u'][:,:,-1,iz] + cols=['t_[s]','u_[m/s]','v_[m/s]','w_[m/s]'] + data = np.column_stack((self['t'],u[0,:],u[1,:],u[2,:])) + dfs['ZMidYEndLine'] = pd.DataFrame(data = data ,columns = cols) + + # Mid crosscorr y + y, rho_uu_y, rho_vv_y, rho_ww_y = self.crosscorr_y() + cols = ['y_[m]', 'rho_uu_[-]','rho_vv_[-]','rho_ww_[-]'] + data = np.column_stack((y, rho_uu_y, rho_vv_y, rho_ww_y)) + dfs['Mid_xcorr_y'] = pd.DataFrame(data = data ,columns = cols) + + # Mid crosscorr z + z, rho_uu_z, rho_vv_z, rho_ww_z = self.crosscorr_z() + cols = ['z_[m]', 'rho_uu_[-]','rho_vv_[-]','rho_ww_[-]'] + data = np.column_stack((z, rho_uu_z, rho_vv_z, rho_ww_z)) + dfs['Mid_xcorr_z'] = pd.DataFrame(data = data ,columns = cols) + + # Mid csd + try: + fc, chi_uu, chi_vv, chi_ww = self.csd_longi() + cols = ['f_[Hz]','chi_uu_[-]', 'chi_vv_[-]','chi_ww_[-]'] + data = np.column_stack((fc, chi_uu, chi_vv, chi_ww)) + dfs['Mid_csd_longi'] = pd.DataFrame(data = data ,columns = cols) + + # Mid csd + fc, chi_uu, chi_vv, chi_ww = self.csd_lat() + cols = ['f_[Hz]','chi_uu_[-]', 'chi_vv_[-]','chi_ww_[-]'] + data = np.column_stack((fc, chi_uu, chi_vv, chi_ww)) + dfs['Mid_csd_lat'] = pd.DataFrame(data = data ,columns = cols) + + # Mid csd + fc, chi_uu, chi_vv, chi_ww = self.csd_vert() + cols = ['f_[Hz]','chi_uu_[-]', 'chi_vv_[-]','chi_ww_[-]'] + data = np.column_stack((fc, chi_uu, chi_vv, chi_ww)) + dfs['Mid_csd_vert'] = pd.DataFrame(data = data ,columns = cols) + except ModuleNotFoundError: + print('Module scipy.signal not available') + except ImportError: + print('Likely issue with fftpack') + + + # Hub time series + #try: + # zHub = self['zHub'] + # iz = np.argmin(np.abs(self['z']-zHub)) + # u = self['u'][:,:,iy,iz] + # Cols=['t_[s]','u_[m/s]','v_[m/s]','w_[m/s]'] + # data = np.column_stack((self['t'],u[0,:],u[1,:],u[2,:])) + # dfs['TSHubLine'] = pd.DataFrame(data = data ,columns = Cols) + #except: + # pass + return dfs + + def toDataset(self): + """ + Convert the data that was read in into a xarray Dataset + + # TODO SORT OUT THE DIFFERENCE WITH toDataSet + """ + from xarray import IndexVariable, DataArray, Dataset + + print('[TODO] openfast_toolbox.io.turbsim_file.toDataset: merge with function toDataSet') + + y = IndexVariable("y", self.y, attrs={"description":"lateral coordinate","units":"m"}) + zround = np.asarray([np.round(zz,6) for zz in self.z]) #the open function here returns something like *.0000000001 which is annoying + z = IndexVariable("z", zround, attrs={"description":"vertical coordinate","units":"m"}) + time = IndexVariable("time", self.t, attrs={"description":"time since start of simulation","units":"s"}) + + da = {} + for component,direction,velname in zip([0,1,2],["x","y","z"],["u","v","w"]): + # the dataset produced here has y/z axes swapped relative to data stored in original object + velocity = np.swapaxes(self["u"][component,...],1,2) + da[velname] = DataArray(velocity, + coords={"time":time,"y":y,"z":z}, + dims=["time","y","z"], + name="velocity", + attrs={"description":"velocity along {0}".format(direction),"units":"m/s"}) + + return Dataset(data_vars=da, coords={"time":time,"y":y,"z":z}) + + def toDataSet(self, datetime=False): + """ + Convert the data that was read in into a xarray Dataset + + # TODO SORT OUT THE DIFFERENCE WITH toDataset + """ + import xarray as xr + + print('[TODO] openfast_toolbox.io.turbsim_file.toDataSet: should be discontinued') + print('[TODO] openfast_toolbox.io.turbsim_file.toDataSet: merge with function toDataset') + + if datetime: + timearray = pd.to_datetime(self['t'], unit='s', origin=pd.to_datetime('2000-01-01 00:00:00')) + timestr = 'datetime' + else: + timearray = self['t'] + timestr = 'time' + + ds = xr.Dataset( + data_vars=dict( + u=([timestr,'y','z'], self['u'][0,:,:,:]), + v=([timestr,'y','z'], self['u'][1,:,:,:]), + w=([timestr,'y','z'], self['u'][2,:,:,:]), + ), + coords={ + timestr : timearray, + 'y' : self['y'], + 'z' : self['z'], + }, + ) + + # Add mean computations + ds['up'] = ds['u'] - ds['u'].mean(dim=timestr) + ds['vp'] = ds['v'] - ds['v'].mean(dim=timestr) + ds['wp'] = ds['w'] - ds['w'].mean(dim=timestr) + + if datetime: + # Add time (in s) to the variable list + ds['time'] = (('datetime'), self['t']) + + return ds + + # Useful converters + def fromAMRWind(self, filename, timestep, output_frequency, sampling_identifier, verbose=1, fileout=None, zref=None, xloc=None): + """ + Reads a AMRWind netcdf file, grabs a group of sampling planes (e.g. p_slice), + return an instance of TurbSimFile, optionally write turbsim file to disk + + + Parameters + ---------- + filename : str, + full path to netcdf file generated by amrwind + timestep : float, + amr-wind code timestep (time.fixed_dt) + output_frequency : int, + frequency chosen for sampling output in amrwind input file (sampling.output_frequency) + sampling_identifier : str, + identifier of the sampling being requested (an entry of sampling.labels in amrwind input file) + zref : float, + height to be written to turbsim as the reference height. if none is given, it is taken as the vertical centerpoint of the slice + """ + try: + from openfast_toolbox.io.amrwind_file import AMRWindFile + except: + try: + from .amrwind_file import AMRWindFile + except: + from amrwind_file import AMRWindFile + + obj = AMRWindFile(filename,timestep,output_frequency, group_name=sampling_identifier) + + self["u"] = np.ndarray((3,obj.nt,obj.ny,obj.nz)) + + xloc = float(obj.data.x[0]) if xloc is None else xloc + if verbose: + print("Grabbing the slice at x={0} m".format(xloc)) + self['u'][0,:,:,:] = np.swapaxes(obj.data.u.sel(x=xloc).values,1,2) + self['u'][1,:,:,:] = np.swapaxes(obj.data.v.sel(x=xloc).values,1,2) + self['u'][2,:,:,:] = np.swapaxes(obj.data.w.sel(x=xloc).values,1,2) + self['t'] = obj.data.t.values + + self['y'] = obj.data.y.values + self['z'] = obj.data.z.values + self['dt'] = obj.output_dt + + self['ID'] = 7 + ltime = time.strftime('%d-%b-%Y at %H:%M:%S', time.localtime()) + self['info'] = 'Converted from AMRWind output file {0} {1:s}.'.format(filename,ltime) + + iz = int(obj.nz/2) + self['zRef'] = float(obj.data.z[iz]) if zref is None else zref + if verbose: + print("Setting the TurbSim file reference height to z={0} m".format(self["zRef"])) + + self['uRef'] = float(obj.data.u.sel(x=xloc).sel(y=0).sel(z=self["zRef"]).mean().values) + self['zRef'], self['uRef'], bHub = self.hubValues() + + if fileout is not None: + filebase = os.path.splitext(filename)[1] + fileout = filebase+".bts" + if verbose: + print("===> {0}".format(fileout)) + self.write(fileout) + + + def fromAMRWind_legacy(self, filename, dt, nt, y, z, sampling_identifier='p_sw2'): + """ + Convert current TurbSim file into one generated from AMR-Wind LES sampling data in .nc format + Assumes: + -- u, v, w (nt, nx * ny * nz) + -- u is aligned with x-axis (flow is not rotated) - this consideration needs to be added + + + INPUTS: + - filename: (string) full path to .nc sampling data file + - sampling_identifier: (string) name of sampling plane group from .inp file (e.g. "p_sw2") + - dt: timestep size [s] + - nt: number of timesteps (sequential) you want to read in, starting at the first timestep available + INPUTS: TODO + - y: user-defined vector of coordinate positions in y + - z: user-defined vector of coordinate positions in z + - uref: (float) reference mean velocity (e.g. 8.0 hub height mean velocity from input file) + - zref: (float) hub height (e.t. 150.0) + """ + import xarray as xr + + print('[TODO] fromAMRWind_legacy: function might be unfinished. Merge with fromAMRWind') + print('[TODO] fromAMRWind_legacy: figure out y, and z from data (see fromAMRWind)') + + # read in sampling data plane + ds = xr.open_dataset(filename, + engine='netcdf4', + group=sampling_identifier) + ny, nz, _ = ds.attrs['ijk_dims'] + noffsets = len(ds.attrs['offsets']) + t = np.arange(0, dt*(nt-0.5), dt) + print('max time [s] = ', t[-1]) + + self['u']=np.ndarray((3,nt,ny,nz)) #, buffer=shm.buf) + # read in AMRWind velocity data + self['u'][0,:,:,:] = ds['velocityx'].isel(num_time_steps=slice(0,nt)).values.reshape(nt,noffsets,ny,nz)[:,1,:,:] # last index = 1 refers to 2nd offset plane at -1200 m + self['u'][1,:,:,:] = ds['velocityy'].isel(num_time_steps=slice(0,nt)).values.reshape(nt,noffsets,ny,nz)[:,1,:,:] + self['u'][2,:,:,:] = ds['velocityz'].isel(num_time_steps=slice(0,nt)).values.reshape(nt,noffsets,ny,nz)[:,1,:,:] + self['t'] = t + self['y'] = y + self['z'] = z + self['dt'] = dt + # TODO + self['ID'] = 7 # ... + self['info'] = 'Converted from AMRWind fields {:s}.'.format(time.strftime('%d-%b-%Y at %H:%M:%S', time.localtime())) +# self['zTwr'] = np.array([]) +# self['uTwr'] = np.array([]) + self['zRef'] = zref #None + self['uRef'] = uref #None + self['zRef'], self['uRef'], bHub = self.hubValues() + + def fromMannBox(self, u, v, w, dx, U, y, z, addU=None): + """ + Convert current TurbSim file into one generated from MannBox + Assumes: + u, v, w (nt x ny x nz) + + y: goes from -ly/2 to ly/2 this is an IMPORTANT subtlety + The field u needs to respect this convention! + (fields from weio.mannbox_file do respect this convention + but when exported to binary files, the y axis is flipped again) + + INPUTS: + - u, v, w : mann box fields + - dx: axial spacing of mann box (to compute time) + - U: reference speed of mann box (to compute time) + - y: y coords of mann box + - z: z coords of mann box + """ + nt,ny,nz = u.shape + dt = dx/U + t = np.arange(0, dt*(nt-0.5), dt) + nt = len(t) + if y[0]>y[-1]: + raise Exception('y is assumed to go from - to +') + + self['u']=np.zeros((3, nt, ny, nz)) + self['u'][0,:,:,:] = u + self['u'][1,:,:,:] = v + self['u'][2,:,:,:] = w + if addU is not None: + self['u'][0,:,:,:] += addU + self['t'] = t + self['y'] = y + self['z'] = z + self['dt'] = dt + # TODO + self['ID'] = 7 # ... + self['info'] = 'Converted from MannBox fields {:s}.'.format(time.strftime('%d-%b-%Y at %H:%M:%S', time.localtime())) +# self['zTwr'] = np.array([]) +# self['uTwr'] = np.array([]) + self['zRef'] = None + self['uRef'] = None + self['zRef'], self['uRef'], bHub = self.hubValues() + + def toMannBox(self, base=None, removeUConstant=None, removeAllUMean=False): + """ + removeUConstant: float, will be removed from all values of the U box + removeAllUMean: If true, the time-average of each y-z points will be substracted + """ + try: + from weio.mannbox_file import MannBoxFile + except: + try: + from .mannbox_file import MannBoxFile + except: + from mannbox_file import MannBoxFile + # filename + if base is None: + base = os.path.splitext(self.filename)[0] + base = base+'_{}x{}x{}'.format(*self['u'].shape[1:]) + + mn = MannBoxFile() + mn.fromTurbSim(self['u'], 0, removeConstant=removeUConstant, removeAllMean=removeAllUMean) + mn.write(base+'.u') + + mn.fromTurbSim(self['u'], 1) + mn.write(base+'.v') + + mn.fromTurbSim(self['u'], 2) + mn.write(base+'.w') + + # --- Useful IO + def writeInfo(ts, filename): + """ Write info to txt """ + infofile = filename + with open(filename,'w') as f: + f.write(str(ts)) + zMid =(ts['z'][0]+ts['z'][-1])/2 + f.write('Middle height of box: {:.3f}\n'.format(zMid)) + y_fit, pfit, model, _ = ts.fitPowerLaw(z_ref=zMid, y_span='mid', U_guess=10, alpha_guess=0.1) + f.write('Power law: alpha={:.5f} - u={:.5f} at z={:.5f}\n'.format(pfit[1],pfit[0],zMid)) + f.write('Periodic: {}\n'.format(ts.checkPeriodic(sigmaTol=1.5, aTol=0.5))) + + + + def writeProbes(ts, probefile, yProbe, zProbe): + """ Create a CSV file with wind speed data at given probe locations + defined by the vectors yProbe and zProbe. All combinations of y and z are extracted. + INPUTS: + - probefile: filename of CSV file to be written + - yProbe: array like of y locations + - zProbe: array like of z locations + """ + Columns=['Time_[s]'] + Data = ts['t'] + for y in yProbe: + for z in zProbe: + iy = np.argmin(np.abs(ts['y']-y)) + iz = np.argmin(np.abs(ts['z']-z)) + lbl = '_y{:.0f}_z{:.0f}'.format(ts['y'][iy], ts['z'][iz]) + Columns+=['{}{}_[m/s]'.format(c,lbl) for c in['u','v','w']] + DataSub = np.column_stack((ts['u'][0,:,iy,iz],ts['u'][1,:,iy,iz],ts['u'][2,:,iy,iz])) + Data = np.column_stack((Data, DataSub)) + np.savetxt(probefile, Data, header=','.join(Columns), delimiter=',') + + def fitPowerLaw(ts, z_ref=None, y_span='full', U_guess=10, alpha_guess=0.1): + """ + Fit power law to vertical profile + INPUTS: + - z_ref: reference height used to define the "U_ref" + - y_span: if 'full', average the vertical profile accross all y-values + if 'mid', average the vertical profile at the middle y value + """ + if z_ref is None: + # use mid height for z_ref + z_ref =(ts['z'][0]+ts['z'][-1])/2 + # Average time series + z, u, _ = ts.vertProfile(y_span=y_span) + u = u[0,:] + u_fit, pfit, model = fit_powerlaw_u_alpha(z, u, z_ref=z_ref, p0=(U_guess, alpha_guess)) + return u_fit, pfit, model, z_ref + +# Functions from BTS_File.py to be ported here +# def TI(self,y=None,z=None,j=None,k=None): +# """ +# If no argument is given, compute TI over entire grid and return array of size (ny,nz). Else, compute TI at the specified point. +# +# Parameters +# ---------- +# y : float, +# cross-stream position [m] +# z : float, +# vertical position AGL [m] +# j : int, +# grid index along cross-stream +# k : int, +# grid index along vertical +# """ +# if ((y==None) & (j==None)): +# return np.std(self.U,axis=0) / np.mean(self.U,axis=0) +# if ((y==None) & (j!=None)): +# return (np.std(self.U[:,j,k])/np.mean(self.U[:,j,k])) +# if ((y!=None) & (j==None)): +# uSeries = self.U[:,self.y2j(y),self.z2k(z)] +# return np.std(uSeries)/np.mean(uSeries) +# +# def visualize(self,component='U',time=0): +# """ +# Quick peak at the data for a given component, at a specific time. +# """ +# data = getattr(self,component)[time,:,:] +# plt.figure() ; +# plt.imshow(data) ; +# plt.colorbar() +# plt.show() +# +# def spectrum(self,component='u',y=None,z=None): +# """ +# Calculate spectrum of a specific component, given time series at ~ hub. +# +# Parameters +# ---------- +# component : string, +# which component to use +# y : float, +# y coordinate [m] of specific location +# z : float, +# z coordinate [m] of specific location +# +# """ +# if y==None: +# k = self.kHub +# j = self.jHub +# data = getattr(self,component) +# data = data[:,j,k] +# N = data.size +# freqs = fftpack.fftfreq(N,self.dT)[1:N/2] +# psd = (np.abs(fftpack.fft(data,N)[1:N/2]))**2 +# return [freqs, psd] +# +# def getRotorPoints(self): +# """ +# In the square y-z slice, return which points are at the edge of the rotor in the horizontal and vertical directions. +# +# Returns +# ------- +# jLeft : int, +# index for grid point that matches the left side of the rotor (when looking towards upstream) +# jRight : int, +# index for grid point that matches the right side of the rotor (when looking towards upstream) +# kBot : int, +# index for grid point that matches the bottom of the rotor +# kTop : int, +# index for grid point that matches the top of the rotor +# """ +# self.zBotRotor = self.zHub - self.R +# self.zTopRotor = self.zHub + self.R +# self.yLeftRotor = self.yHub - self.R +# self.yRightRotor = self.yHub + self.R +# self.jLeftRotor = self.y2j(self.yLeftRotor) +# self.jRightRotor = self.y2j(self.yRightRotor) +# self.kBotRotor = self.z2k(self.zBotRotor) +# self.kTopRotor = self.z2k(self.zTopRotor) +# + + + +def fit_powerlaw_u_alpha(x, y, z_ref=100, p0=(10,0.1)): + """ + p[0] : u_ref + p[1] : alpha + """ + import scipy.optimize as so + pfit, _ = so.curve_fit(lambda x, *p : p[0] * (x / z_ref) ** p[1], x, y, p0=p0) + y_fit = pfit[0] * (x / z_ref) ** pfit[1] + coeffs_dict={'u_ref':pfit[0],'alpha':pfit[1]} + formula = '{u_ref} * (z / {z_ref}) ** {alpha}' + fitted_fun = lambda xx: pfit[0] * (xx / z_ref) ** pfit[1] + return y_fit, pfit, {'coeffs':coeffs_dict,'formula':formula,'fitted_function':fitted_fun} + +if __name__=='__main__': + ts = TurbSimFile('../_tests/TurbSim.bts') + + diff --git a/pyFAST/input_output/turbsim_ts_file.py b/openfast_toolbox/io/turbsim_ts_file.py similarity index 100% rename from pyFAST/input_output/turbsim_ts_file.py rename to openfast_toolbox/io/turbsim_ts_file.py diff --git a/pyFAST/input_output/vtk_file.py b/openfast_toolbox/io/vtk_file.py similarity index 100% rename from pyFAST/input_output/vtk_file.py rename to openfast_toolbox/io/vtk_file.py diff --git a/pyFAST/input_output/wetb/__init__.py b/openfast_toolbox/io/wetb/__init__.py similarity index 100% rename from pyFAST/input_output/wetb/__init__.py rename to openfast_toolbox/io/wetb/__init__.py diff --git a/pyFAST/input_output/wetb/hawc2/Hawc2io.py b/openfast_toolbox/io/wetb/hawc2/Hawc2io.py similarity index 97% rename from pyFAST/input_output/wetb/hawc2/Hawc2io.py rename to openfast_toolbox/io/wetb/hawc2/Hawc2io.py index 4484bae..186ab8b 100644 --- a/pyFAST/input_output/wetb/hawc2/Hawc2io.py +++ b/openfast_toolbox/io/wetb/hawc2/Hawc2io.py @@ -1,343 +1,343 @@ -# -*- coding: utf-8 -*- -""" -Author: - Bjarne S. Kallesoee - - -Description: - Reads all HAWC2 output data formats, HAWC2 ascii, HAWC2 binary and FLEX - -call ex.: - # creat data file object, call without extension, but with parth - file = ReadHawc2("HAWC2ex/tests") - # if called with ReadOnly = 1 as - file = ReadHawc2("HAWC2ex/tests",ReadOnly=1) - # no channels a stored in memory, otherwise read channels are stored for reuse - - # channels are called by a list - file([0,2,1,1]) => channels 1,3,2,2 - # if empty all channels are returned - file() => all channels as 1,2,3,... - file.t => time vector - -1. version: 19/4-2011 -2. version: 5/11-2015 fixed columns to get description right, fixed time vector (mmpe@dtu.dk) - -Need to be done: - * add error handling for allmost every thing - -""" -import numpy as np -import os - -#from wetb import gtsdf - -# FIXME: numpy doesn't like io.open binary fid in PY27, why is that? As a hack -# workaround, use opent for PY23 compatibility when handling text files, -# and default open for binary - -################################################################################ -################################################################################ -################################################################################ -# Read HAWC2 class -################################################################################ -class ReadHawc2(object): - """ - """ -################################################################################ -# read *.sel file - def _ReadSelFile(self): - """ - Some title - ========== - - Using docstrings formatted according to the reStructuredText specs - can be used for automated documentation generation with for instance - Sphinx: http://sphinx.pocoo.org/. - - Parameters - ---------- - signal : ndarray - some description - - Returns - ------- - output : int - describe variable - """ - - # read *.sel hawc2 output file for result info - if self.FileName.lower().endswith('.sel'): - self.FileName = self.FileName[:-4] - fid = open(self.FileName + '.sel', 'r') - Lines = fid.readlines() - fid.close() - if Lines[0].lower().find('bhawc')>=0: - # --- Find line with scan info - iLine=0 - for i in np.arange(5,10): - if Lines[i].lower().find('scans')>=0: - iLine=i+1 - if iLine==0: - raise Exception('Cannot find the keyword "scans"') - temp = Lines[iLine].split() - self.NrSc = int(temp[0]) - self.NrCh = int(temp[1]) - self.Time = float(temp[2]) - self.Freq = self.NrSc / self.Time - self.t = np.linspace(0, self.Time, self.NrSc + 1)[1:] - # --- Find line with channel info - iLine=0 - for i in np.arange(5,13): - if Lines[i].lower().find('channel')>=0: - iLine=i+1 - if iLine==0: - raise Exception('Cannot find the keyword "Channel"') - - # reads channel info (name, unit and description) - Name = []; Unit = []; Description = []; - for i in range(0, self.NrCh+1): - if (i+iLine)>=len(Lines): - break - line = Lines[i + iLine].strip() - if len(line)==0: - continue - # --- removing number and unit - sp=[sp.strip() for sp in line.split() if len(sp.strip())>0] - num = sp[0] - iNum = line.find(num) - line = line[iNum+len(num)+1:] - unit = sp[-1] - iUnit = line.find(unit) - line = line[:iUnit] - # --- Splitting to find label and description - sp=[sp.strip() for sp in line.split('\t') if len(sp.strip())>0] - if len(sp)!=2: - for nSpaces in np.arange(2,15): - sp=[sp.strip() for sp in line.split(' '*nSpaces) if len(sp.strip())>0] - if len(sp)==2: - break - if len(sp)!=2: - raise Exception('Dont know how to split the input of the sel file into 4 columns') - - Unit.append(unit) - Description.append(sp[0]) - Name.append(sp[1]) - - self.ChInfo = [Name, Unit, Description] - self.FileFormat = 'BHAWC_ASCII' - else: - - # findes general result info (number of scans, number of channels, - # simulation time and file format) - temp = Lines[8].split() - self.NrSc = int(temp[0]) - self.NrCh = int(temp[1]) - self.Time = float(temp[2]) - self.Freq = self.NrSc / self.Time - self.t = np.linspace(0, self.Time, self.NrSc + 1)[1:] - Format = temp[3] - # reads channel info (name, unit and description) - Name = []; Unit = []; Description = []; - for i in range(0, self.NrCh): - temp = str(Lines[i + 12][12:43]); Name.append(temp.strip()) - temp = str(Lines[i + 12][43:54]); Unit.append(temp.strip()) - temp = str(Lines[i + 12][54:-1]); Description.append(temp.strip()) - self.ChInfo = [Name, Unit, Description] - # if binary file format, scaling factors are read - if Format.lower() == 'binary': - self.ScaleFactor = np.zeros(self.NrCh) - self.FileFormat = 'HAWC2_BINARY' - for i in range(0, self.NrCh): - self.ScaleFactor[i] = float(Lines[i + 12 + self.NrCh + 2]) - else: - self.FileFormat = 'HAWC2_ASCII' -################################################################################ -# read sensor file for FLEX format - def _ReadSensorFile(self): - # read sensor file used if results are saved in FLEX format - DirName = os.path.dirname(self.FileName) - try: - fid = opent(DirName + r"\sensor ", 'r') - except IOError: - print("can't finde sensor file for FLEX format") - return - Lines = fid.readlines() - fid.close() - # reads channel info (name, unit and description) - self.NrCh = 0 - Name = [] - Unit = [] - Description = [] - for i in range(2, len(Lines)): - temp = Lines[i] - if not temp.strip(): - break - self.NrCh += 1 - temp = str(Lines[i][38:45]) - Unit.append(temp.strip()) - temp = str(Lines[i][45:53]) - Name.append(temp.strip()) - temp = str(Lines[i][53:]) - Description.append(temp.strip()) - self.ChInfo = [Name, Unit, Description] - # read general info from *.int file - fid = open(self.FileName, 'rb') - fid.seek(4 * 19) - if not np.fromfile(fid, 'int32', 1) == self.NrCh: - print("number of sensors in sensor file and data file are not consisten") - fid.seek(4 * (self.NrCh) + 4, 1) - self.Version = np.fromfile(fid, 'int32',1)[0] - time_start, time_step = np.fromfile(fid, 'f', 2) - self.Freq = 1 / time_step - self.ScaleFactor = np.fromfile(fid, 'f', self.NrCh) - fid.seek(2 * 4 * self.NrCh + 48 * 2) - self.NrSc = int(len(np.fromfile(fid, 'int16')) / self.NrCh) - self.Time = self.NrSc * time_step - self.t = np.arange(0, self.Time, time_step) + time_start - fid.close() -################################################################################ -# init function, load channel and other general result file info - def __init__(self, FileName, ReadOnly=0): - self.FileName = FileName - self.ReadOnly = ReadOnly - self.Iknown = [] # to keep track of what has been read all ready - self.Data = np.zeros(0) - if FileName.lower().endswith('.sel') or os.path.isfile(FileName + ".sel"): - self._ReadSelFile() - elif FileName.lower().endswith('.dat') and os.path.isfile(os.path.splitext(FileName)[0] + ".sel"): - self.FileName = os.path.splitext(FileName)[0] - self._ReadSelFile() - elif FileName.lower().endswith('.int') or FileName.lower().endswith('.res'): - self.FileFormat = 'FLEX' - self._ReadSensorFile() - elif os.path.isfile(self.FileName + ".int"): - self.FileName = self.FileName + ".int" - self.FileFormat = 'FLEX' - self._ReadSensorFile() - elif os.path.isfile(self.FileName + ".res"): - self.FileName = self.FileName + ".res" - self.FileFormat = 'FLEX' - self._ReadSensorFile() - elif FileName.lower().endswith('.hdf5') or os.path.isfile(self.FileName + ".hdf5"): - self.FileFormat = 'GTSDF' - self.ReadGtsdf() - else: - raise Exception("unknown file: " + FileName) -################################################################################ -# Read results in binary format - def ReadBinary(self, ChVec=None): - ChVec = [] if ChVec is None else ChVec - if not ChVec: - ChVec = range(0, self.NrCh) - with open(self.FileName + '.dat', 'rb') as fid: - data = np.zeros((self.NrSc, len(ChVec))) - j = 0 - for i in ChVec: - fid.seek(i * self.NrSc * 2, 0) - data[:, j] = np.fromfile(fid, 'int16', self.NrSc) * self.ScaleFactor[i] - j += 1 - return data -################################################################################ -# Read results in ASCII format - def ReadAscii(self, ChVec=None): - ChVec = [] if ChVec is None else ChVec - if not ChVec: - ChVec = range(0, self.NrCh) - temp = np.loadtxt(self.FileName + '.dat', usecols=ChVec) - return temp.reshape((self.NrSc, len(ChVec))) -################################################################################ -# Read results in FLEX format - def ReadFLEX(self, ChVec=None): - ChVec = [] if ChVec is None else ChVec - if not ChVec: - ChVec = range(1, self.NrCh) - fid = open(self.FileName, 'rb') - fid.seek(2 * 4 * self.NrCh + 48 * 2) - temp = np.fromfile(fid, 'int16') - if self.Version==3: - temp = temp.reshape(self.NrCh, self.NrSc).transpose() - else: - temp = temp.reshape(self.NrSc, self.NrCh) - fid.close() - return np.dot(temp[:, ChVec], np.diag(self.ScaleFactor[ChVec])) -################################################################################ -# Read results in GTSD format - def ReadGtsdf(self): - raise NotImplementedError - #self.t, data, info = gtsdf.load(self.FileName + '.hdf5') - #self.Time = self.t - #self.ChInfo = [['Time'] + info['attribute_names'], - # ['s'] + info['attribute_units'], - # ['Time'] + info['attribute_descriptions']] - #self.NrCh = data.shape[1] + 1 - #self.NrSc = data.shape[0] - #self.Freq = self.NrSc / self.Time - #self.FileFormat = 'GTSDF' - #self.gtsdf_description = info['description'] - #data = np.hstack([self.Time[:,np.newaxis], data]) - #return data -################################################################################ -# One stop call for reading all data formats - def ReadAll(self, ChVec=None): - ChVec = [] if ChVec is None else ChVec - if not ChVec and not self.FileFormat == 'GTSDF': - ChVec = range(0, self.NrCh) - if self.FileFormat == 'HAWC2_BINARY': - return self.ReadBinary(ChVec) - elif self.FileFormat == 'HAWC2_ASCII' or self.FileFormat == 'BHAWC_ASCII': - return self.ReadAscii(ChVec) - elif self.FileFormat == 'GTSDF': - return self.ReadGtsdf() - elif self.FileFormat == 'FLEX': - return self.ReadFLEX(ChVec) - else: - raise Exception('Unknown file format {} for hawc2 out file'.format(self.FileFormat)) - -################################################################################ -# Main read data call, read, save and sort data - def __call__(self, ChVec=None): - ChVec = [] if ChVec is None else ChVec - if not ChVec: - ChVec = range(0, self.NrCh) - elif max(ChVec) >= self.NrCh: - print("to high channel number") - return - # if ReadOnly, read data but no storeing in memory - if self.ReadOnly: - return self.ReadAll(ChVec) - # if not ReadOnly, sort in known and new channels, read new channels - # and return all requested channels - else: - # sort into known channels and channels to be read - I1 = [] - I2 = [] # I1=Channel mapping, I2=Channels to be read - for i in ChVec: - try: - I1.append(self.Iknown.index(i)) - except: - self.Iknown.append(i) - I2.append(i) - I1.append(len(I1)) - # read new channels - if I2: - temp = self.ReadAll(I2) - # add new channels to Data - if self.Data.any(): - self.Data = np.append(self.Data, temp, axis=1) - # if first call, so Daata is empty - else: - self.Data = temp - return self.Data[:, tuple(I1)] - - -################################################################################ -################################################################################ -################################################################################ -# write HAWC2 class, to be implemented -################################################################################ - -if __name__ == '__main__': - res_file = ReadHawc2('structure_wind') - results = res_file.ReadAscii() - channelinfo = res_file.ChInfo +# -*- coding: utf-8 -*- +""" +Author: + Bjarne S. Kallesoee + + +Description: + Reads all HAWC2 output data formats, HAWC2 ascii, HAWC2 binary and FLEX + +call ex.: + # creat data file object, call without extension, but with parth + file = ReadHawc2("HAWC2ex/tests") + # if called with ReadOnly = 1 as + file = ReadHawc2("HAWC2ex/tests",ReadOnly=1) + # no channels a stored in memory, otherwise read channels are stored for reuse + + # channels are called by a list + file([0,2,1,1]) => channels 1,3,2,2 + # if empty all channels are returned + file() => all channels as 1,2,3,... + file.t => time vector + +1. version: 19/4-2011 +2. version: 5/11-2015 fixed columns to get description right, fixed time vector (mmpe@dtu.dk) + +Need to be done: + * add error handling for allmost every thing + +""" +import numpy as np +import os + +#from wetb import gtsdf + +# FIXME: numpy doesn't like io.open binary fid in PY27, why is that? As a hack +# workaround, use opent for PY23 compatibility when handling text files, +# and default open for binary + +################################################################################ +################################################################################ +################################################################################ +# Read HAWC2 class +################################################################################ +class ReadHawc2(object): + """ + """ +################################################################################ +# read *.sel file + def _ReadSelFile(self): + """ + Some title + ========== + + Using docstrings formatted according to the reStructuredText specs + can be used for automated documentation generation with for instance + Sphinx: http://sphinx.pocoo.org/. + + Parameters + ---------- + signal : ndarray + some description + + Returns + ------- + output : int + describe variable + """ + + # read *.sel hawc2 output file for result info + if self.FileName.lower().endswith('.sel'): + self.FileName = self.FileName[:-4] + fid = open(self.FileName + '.sel', 'r') + Lines = fid.readlines() + fid.close() + if Lines[0].lower().find('bhawc')>=0: + # --- Find line with scan info + iLine=0 + for i in np.arange(5,10): + if Lines[i].lower().find('scans')>=0: + iLine=i+1 + if iLine==0: + raise Exception('Cannot find the keyword "scans"') + temp = Lines[iLine].split() + self.NrSc = int(temp[0]) + self.NrCh = int(temp[1]) + self.Time = float(temp[2]) + self.Freq = self.NrSc / self.Time + self.t = np.linspace(0, self.Time, self.NrSc + 1)[1:] + # --- Find line with channel info + iLine=0 + for i in np.arange(5,13): + if Lines[i].lower().find('channel')>=0: + iLine=i+1 + if iLine==0: + raise Exception('Cannot find the keyword "Channel"') + + # reads channel info (name, unit and description) + Name = []; Unit = []; Description = []; + for i in range(0, self.NrCh+1): + if (i+iLine)>=len(Lines): + break + line = Lines[i + iLine].strip() + if len(line)==0: + continue + # --- removing number and unit + sp=[sp.strip() for sp in line.split() if len(sp.strip())>0] + num = sp[0] + iNum = line.find(num) + line = line[iNum+len(num)+1:] + unit = sp[-1] + iUnit = line.find(unit) + line = line[:iUnit] + # --- Splitting to find label and description + sp=[sp.strip() for sp in line.split('\t') if len(sp.strip())>0] + if len(sp)!=2: + for nSpaces in np.arange(2,15): + sp=[sp.strip() for sp in line.split(' '*nSpaces) if len(sp.strip())>0] + if len(sp)==2: + break + if len(sp)!=2: + raise Exception('Dont know how to split the input of the sel file into 4 columns') + + Unit.append(unit) + Description.append(sp[0]) + Name.append(sp[1]) + + self.ChInfo = [Name, Unit, Description] + self.FileFormat = 'BHAWC_ASCII' + else: + + # findes general result info (number of scans, number of channels, + # simulation time and file format) + temp = Lines[8].split() + self.NrSc = int(temp[0]) + self.NrCh = int(temp[1]) + self.Time = float(temp[2]) + self.Freq = self.NrSc / self.Time + self.t = np.linspace(0, self.Time, self.NrSc + 1)[1:] + Format = temp[3] + # reads channel info (name, unit and description) + Name = []; Unit = []; Description = []; + for i in range(0, self.NrCh): + temp = str(Lines[i + 12][12:43]); Name.append(temp.strip()) + temp = str(Lines[i + 12][43:54]); Unit.append(temp.strip()) + temp = str(Lines[i + 12][54:-1]); Description.append(temp.strip()) + self.ChInfo = [Name, Unit, Description] + # if binary file format, scaling factors are read + if Format.lower() == 'binary': + self.ScaleFactor = np.zeros(self.NrCh) + self.FileFormat = 'HAWC2_BINARY' + for i in range(0, self.NrCh): + self.ScaleFactor[i] = float(Lines[i + 12 + self.NrCh + 2]) + else: + self.FileFormat = 'HAWC2_ASCII' +################################################################################ +# read sensor file for FLEX format + def _ReadSensorFile(self): + # read sensor file used if results are saved in FLEX format + DirName = os.path.dirname(self.FileName) + try: + fid = opent(DirName + r"\sensor ", 'r') + except IOError: + print("can't finde sensor file for FLEX format") + return + Lines = fid.readlines() + fid.close() + # reads channel info (name, unit and description) + self.NrCh = 0 + Name = [] + Unit = [] + Description = [] + for i in range(2, len(Lines)): + temp = Lines[i] + if not temp.strip(): + break + self.NrCh += 1 + temp = str(Lines[i][38:45]) + Unit.append(temp.strip()) + temp = str(Lines[i][45:53]) + Name.append(temp.strip()) + temp = str(Lines[i][53:]) + Description.append(temp.strip()) + self.ChInfo = [Name, Unit, Description] + # read general info from *.int file + fid = open(self.FileName, 'rb') + fid.seek(4 * 19) + if not np.fromfile(fid, 'int32', 1) == self.NrCh: + print("number of sensors in sensor file and data file are not consisten") + fid.seek(4 * (self.NrCh) + 4, 1) + self.Version = np.fromfile(fid, 'int32',1)[0] + time_start, time_step = np.fromfile(fid, 'f', 2) + self.Freq = 1 / time_step + self.ScaleFactor = np.fromfile(fid, 'f', self.NrCh) + fid.seek(2 * 4 * self.NrCh + 48 * 2) + self.NrSc = int(len(np.fromfile(fid, 'int16')) / self.NrCh) + self.Time = self.NrSc * time_step + self.t = np.arange(0, self.Time, time_step) + time_start + fid.close() +################################################################################ +# init function, load channel and other general result file info + def __init__(self, FileName, ReadOnly=0): + self.FileName = FileName + self.ReadOnly = ReadOnly + self.Iknown = [] # to keep track of what has been read all ready + self.Data = np.zeros(0) + if FileName.lower().endswith('.sel') or os.path.isfile(FileName + ".sel"): + self._ReadSelFile() + elif FileName.lower().endswith('.dat') and os.path.isfile(os.path.splitext(FileName)[0] + ".sel"): + self.FileName = os.path.splitext(FileName)[0] + self._ReadSelFile() + elif FileName.lower().endswith('.int') or FileName.lower().endswith('.res'): + self.FileFormat = 'FLEX' + self._ReadSensorFile() + elif os.path.isfile(self.FileName + ".int"): + self.FileName = self.FileName + ".int" + self.FileFormat = 'FLEX' + self._ReadSensorFile() + elif os.path.isfile(self.FileName + ".res"): + self.FileName = self.FileName + ".res" + self.FileFormat = 'FLEX' + self._ReadSensorFile() + elif FileName.lower().endswith('.hdf5') or os.path.isfile(self.FileName + ".hdf5"): + self.FileFormat = 'GTSDF' + self.ReadGtsdf() + else: + raise Exception("unknown file: " + FileName) +################################################################################ +# Read results in binary format + def ReadBinary(self, ChVec=None): + ChVec = [] if ChVec is None else ChVec + if not ChVec: + ChVec = range(0, self.NrCh) + with open(self.FileName + '.dat', 'rb') as fid: + data = np.zeros((self.NrSc, len(ChVec))) + j = 0 + for i in ChVec: + fid.seek(i * self.NrSc * 2, 0) + data[:, j] = np.fromfile(fid, 'int16', self.NrSc) * self.ScaleFactor[i] + j += 1 + return data +################################################################################ +# Read results in ASCII format + def ReadAscii(self, ChVec=None): + ChVec = [] if ChVec is None else ChVec + if not ChVec: + ChVec = range(0, self.NrCh) + temp = np.loadtxt(self.FileName + '.dat', usecols=ChVec) + return temp.reshape((self.NrSc, len(ChVec))) +################################################################################ +# Read results in FLEX format + def ReadFLEX(self, ChVec=None): + ChVec = [] if ChVec is None else ChVec + if not ChVec: + ChVec = range(1, self.NrCh) + fid = open(self.FileName, 'rb') + fid.seek(2 * 4 * self.NrCh + 48 * 2) + temp = np.fromfile(fid, 'int16') + if self.Version==3: + temp = temp.reshape(self.NrCh, self.NrSc).transpose() + else: + temp = temp.reshape(self.NrSc, self.NrCh) + fid.close() + return np.dot(temp[:, ChVec], np.diag(self.ScaleFactor[ChVec])) +################################################################################ +# Read results in GTSD format + def ReadGtsdf(self): + raise NotImplementedError + #self.t, data, info = gtsdf.load(self.FileName + '.hdf5') + #self.Time = self.t + #self.ChInfo = [['Time'] + info['attribute_names'], + # ['s'] + info['attribute_units'], + # ['Time'] + info['attribute_descriptions']] + #self.NrCh = data.shape[1] + 1 + #self.NrSc = data.shape[0] + #self.Freq = self.NrSc / self.Time + #self.FileFormat = 'GTSDF' + #self.gtsdf_description = info['description'] + #data = np.hstack([self.Time[:,np.newaxis], data]) + #return data +################################################################################ +# One stop call for reading all data formats + def ReadAll(self, ChVec=None): + ChVec = [] if ChVec is None else ChVec + if not ChVec and not self.FileFormat == 'GTSDF': + ChVec = range(0, self.NrCh) + if self.FileFormat == 'HAWC2_BINARY': + return self.ReadBinary(ChVec) + elif self.FileFormat == 'HAWC2_ASCII' or self.FileFormat == 'BHAWC_ASCII': + return self.ReadAscii(ChVec) + elif self.FileFormat == 'GTSDF': + return self.ReadGtsdf() + elif self.FileFormat == 'FLEX': + return self.ReadFLEX(ChVec) + else: + raise Exception('Unknown file format {} for hawc2 out file'.format(self.FileFormat)) + +################################################################################ +# Main read data call, read, save and sort data + def __call__(self, ChVec=None): + ChVec = [] if ChVec is None else ChVec + if not ChVec: + ChVec = range(0, self.NrCh) + elif max(ChVec) >= self.NrCh: + print("to high channel number") + return + # if ReadOnly, read data but no storeing in memory + if self.ReadOnly: + return self.ReadAll(ChVec) + # if not ReadOnly, sort in known and new channels, read new channels + # and return all requested channels + else: + # sort into known channels and channels to be read + I1 = [] + I2 = [] # I1=Channel mapping, I2=Channels to be read + for i in ChVec: + try: + I1.append(self.Iknown.index(i)) + except: + self.Iknown.append(i) + I2.append(i) + I1.append(len(I1)) + # read new channels + if I2: + temp = self.ReadAll(I2) + # add new channels to Data + if self.Data.any(): + self.Data = np.append(self.Data, temp, axis=1) + # if first call, so Daata is empty + else: + self.Data = temp + return self.Data[:, tuple(I1)] + + +################################################################################ +################################################################################ +################################################################################ +# write HAWC2 class, to be implemented +################################################################################ + +if __name__ == '__main__': + res_file = ReadHawc2('structure_wind') + results = res_file.ReadAscii() + channelinfo = res_file.ChInfo diff --git a/pyFAST/input_output/wetb/hawc2/__init__.py b/openfast_toolbox/io/wetb/hawc2/__init__.py similarity index 100% rename from pyFAST/input_output/wetb/hawc2/__init__.py rename to openfast_toolbox/io/wetb/hawc2/__init__.py diff --git a/pyFAST/input_output/wetb/hawc2/ae_file.py b/openfast_toolbox/io/wetb/hawc2/ae_file.py similarity index 100% rename from pyFAST/input_output/wetb/hawc2/ae_file.py rename to openfast_toolbox/io/wetb/hawc2/ae_file.py diff --git a/pyFAST/input_output/wetb/hawc2/htc_contents.py b/openfast_toolbox/io/wetb/hawc2/htc_contents.py similarity index 100% rename from pyFAST/input_output/wetb/hawc2/htc_contents.py rename to openfast_toolbox/io/wetb/hawc2/htc_contents.py diff --git a/pyFAST/input_output/wetb/hawc2/htc_extensions.py b/openfast_toolbox/io/wetb/hawc2/htc_extensions.py similarity index 100% rename from pyFAST/input_output/wetb/hawc2/htc_extensions.py rename to openfast_toolbox/io/wetb/hawc2/htc_extensions.py diff --git a/pyFAST/input_output/wetb/hawc2/htc_file.py b/openfast_toolbox/io/wetb/hawc2/htc_file.py similarity index 97% rename from pyFAST/input_output/wetb/hawc2/htc_file.py rename to openfast_toolbox/io/wetb/hawc2/htc_file.py index ca3237c..c2b859b 100644 --- a/pyFAST/input_output/wetb/hawc2/htc_file.py +++ b/openfast_toolbox/io/wetb/hawc2/htc_file.py @@ -1,600 +1,600 @@ -''' -Created on 20/01/2014 - -See documentation of HTCFile below - -''' -# from wetb.utils.process_exec import pexec -# from wetb.hawc2.hawc2_pbs_file import HAWC2PBSFile -# import jinja2 -# from wetb.utils.cluster_tools.os_path import fixcase, abspath, pjoin - -from collections import OrderedDict -from .htc_contents import HTCContents, HTCSection, HTCLine -from .htc_extensions import HTCDefaults, HTCExtensions -import os - -# --- cluster_tools/os_path -def fmt_path(path): - return path.lower().replace("\\", "/") - -def repl(path): - return path.replace("\\", "/") - -def abspath(path): - return repl(os.path.abspath(path)) - -def relpath(path, start=None): - return repl(os.path.relpath(path, start)) - -def realpath(path): - return repl(os.path.realpath(path)) - -def pjoin(*path): - return repl(os.path.join(*path)) - -def fixcase(path): - path = realpath(str(path)).replace("\\", "/") - p, rest = os.path.splitdrive(path) - p += "/" - for f in rest[1:].split("/"): - f_lst = [f_ for f_ in os.listdir(p) if f_.lower() == f.lower()] - if len(f_lst) > 1: - # use the case sensitive match - f_lst = [f_ for f_ in f_lst if f_ == f] - if len(f_lst) == 0: - raise IOError("'%s' not found in '%s'" % (f, p)) - # Use matched folder - p = pjoin(p, f_lst[0]) - return p -# --- end os_path - -class HTCFile(HTCContents, HTCDefaults, HTCExtensions): - """Wrapper for HTC files - - Examples: - --------- - >>> htcfile = HTCFile('htc/test.htc') - >>> htcfile.wind.wsp = 10 - >>> htcfile.save() - - #--------------------------------------------- - >>> htc = HTCFile(filename=None, modelpath=None) # create minimal htcfile - - #Add section - >>> htc.add_section('hydro') - - #Add subsection - >>> htc.hydro.add_section("hydro_element") - - #Set values - >>> htc.hydro.hydro_element.wave_breaking = [2, 6.28, 1] # or - >>> htc.hydro.hydro_element.wave_breaking = 2, 6.28, 1 - - #Set comments - >>> htc.hydro.hydro_element.wave_breaking.comments = "This is a comment" - - #Access section - >>> hydro_element = htc.hydro.hydro_element #or - >>> hydro_element = htc['hydro.hydro_element'] # or - >>> hydro_element = htc['hydro/hydro_element'] # or - >>> print (hydro_element.wave_breaking) #string represenation - wave_breaking 2 6.28 1; This is a comment - >>> print (hydro_element.wave_breaking.name_) # command - wave_breaking - >>> print (hydro_element.wave_breaking.values) # values - [2, 6.28, 1 - >>> print (hydro_element.wave_breaking.comments) # comments - This is a comment - >>> print (hydro_element.wave_breaking[0]) # first value - 2 - - #Delete element - htc.simulation.logfile.delete() - #or - del htc.simulation.logfile #Delete logfile line. Raise keyerror if not exists - - """ - - filename = None - jinja_tags = {} - htc_inputfiles = [] - level = 0 - modelpath = "../" - initial_comments = None - - def __init__(self, filename=None, modelpath=None, jinja_tags={}): - """ - Parameters - --------- - filename : str - Absolute filename of htc file - modelpath : str - Model path relative to htc file - """ - if filename is not None: - try: - filename = fixcase(abspath(filename)) - with self.open(str(filename)): - pass - except Exception: - pass - - self.filename = filename - - self.jinja_tags = jinja_tags - self.modelpath = modelpath or self.auto_detect_modelpath() - - if filename and self.modelpath != "unknown" and not os.path.isabs(self.modelpath): - drive, p = os.path.splitdrive(os.path.join(os.path.dirname(str(self.filename)), self.modelpath)) - self.modelpath = os.path.join(drive, os.path.splitdrive(os.path.realpath(p))[1]).replace("\\", "/") - if self.modelpath != 'unknown' and self.modelpath[-1] != '/': - self.modelpath += "/" - - self.load() - - def auto_detect_modelpath(self): - if self.filename is None: - return "../" - - #print (["../"*i for i in range(3)]) - import numpy as np - input_files = HTCFile(self.filename, 'unknown').input_files() - if len(input_files) == 1: # only input file is the htc file - return "../" - rel_input_files = [f for f in input_files if not os.path.isabs(f)] - - def isfile_case_insensitive(f): - try: - f = fixcase(f) # raises exception if not existing - return os.path.isfile(f) - except IOError: - return False - found = ([np.sum([isfile_case_insensitive(os.path.join(os.path.dirname(self.filename), "../" * i, f)) - for f in rel_input_files]) for i in range(4)]) - - if max(found) > 0: - relpath = "../" * np.argmax(found) - return abspath(pjoin(os.path.dirname(self.filename), relpath)) - else: - print("Modelpath cannot be autodetected for '%s'.\nInput files not found near htc file" % self.filename) - return 'unknown' - - def load(self): - self.contents = OrderedDict() - self.initial_comments = [] - self.htc_inputfiles = [] - if self.filename is None: - lines = self.empty_htc.split("\n") - else: - lines = self.readlines(self.filename) - - lines = [l.strip() for l in lines] - - #lines = copy(self.lines) - while lines: - if lines[0].startswith(";"): - self.initial_comments.append(lines.pop(0).strip() + "\n") - elif lines[0].lower().startswith("begin"): - self._add_contents(HTCSection.from_lines(lines)) - else: - line = HTCLine.from_lines(lines) - if line.name_ == "exit": - break - self._add_contents(line) - - def readfilelines(self, filename): - with self.open(self.unix_path(os.path.abspath(filename.replace('\\', '/'))), encoding='cp1252') as fid: - txt = fid.read() - if txt[:10].encode().startswith(b'\xc3\xaf\xc2\xbb\xc2\xbf'): - txt = txt[3:] - if self.jinja_tags: - template = jinja2.Template(txt) - txt = template.render(**self.jinja_tags) - return txt.replace("\r", "").split("\n") - - def readlines(self, filename): - if filename != self.filename: # self.filename may be changed by set_name/save. Added it when needed instead - self.htc_inputfiles.append(filename) - htc_lines = [] - lines = self.readfilelines(filename) - for l in lines: - if l.lower().lstrip().startswith('continue_in_file'): - filename = l.lstrip().split(";")[0][len("continue_in_file"):].strip().lower() - - if self.modelpath == 'unknown': - p = os.path.dirname(self.filename) - try: - lu = [os.path.isfile(os.path.abspath(os.path.join(p, "../" * i, filename.replace("\\", "/")))) - for i in range(4)].index(True) - filename = os.path.join(p, "../" * lu, filename) - except ValueError: - print('[FAIL] Cannot continue in file: {}'.format(filename)) - filename = None - else: - filename = os.path.join(self.modelpath, filename) - if not os.path.isfile(filename): - print('[FAIL] Cannot continue in file: {}'.format(filename)) - filename=None - if filename is not None: - #print('[INFO] Continuing in file: {}'.format(filename)) - for line in self.readlines(filename): - if line.lstrip().lower().startswith('exit'): - break - htc_lines.append(line) - else: - htc_lines.append(l) - return htc_lines - - def __setitem__(self, key, value): - self.contents[key] = value - - def __str__(self): - self.contents # load - return "".join(self.initial_comments + [c.__str__(1) for c in self] + ["exit;"]) - - def save(self, filename=None): - """Saves the htc object to an htc file. - - Args: - filename (str, optional): Specifies the filename of the htc file to be saved. - If the value is none, the filename attribute of the object will be used as the filename. - Defaults to None. - """ - self.contents # load if not loaded - if filename is None: - filename = self.filename - else: - self.filename = filename - # exist_ok does not exist in Python27 - if not os.path.exists(os.path.dirname(filename)) and os.path.dirname(filename) != "": - os.makedirs(os.path.dirname(filename)) # , exist_ok=True) - with self.open(filename, 'w', encoding='cp1252') as fid: - fid.write(str(self)) - - def set_name(self, name, subfolder=''): - """Sets the base filename of the simulation files. - - Args: - name (str): Specifies name of the log file, dat file (for animation), hdf5 file (for visualization) and htc file. - subfolder (str, optional): Specifies the name of a subfolder to place the files in. - If the value is an empty string, no subfolders will be created. - Defaults to ''. - - Returns: - None - """ - # if os.path.isabs(folder) is False and os.path.relpath(folder).startswith("htc" + os.path.sep): - self.contents # load if not loaded - - def fmt_folder(folder, subfolder): return "./" + \ - os.path.relpath(os.path.join(folder, subfolder)).replace("\\", "/") - - self.filename = os.path.abspath(os.path.join(self.modelpath, fmt_folder( - 'htc', subfolder), "%s.htc" % name)).replace("\\", "/") - if 'simulation' in self and 'logfile' in self.simulation: - self.simulation.logfile = os.path.join(fmt_folder('log', subfolder), "%s.log" % name).replace("\\", "/") - if 'animation' in self.simulation: - self.simulation.animation = os.path.join(fmt_folder( - 'animation', subfolder), "%s.dat" % name).replace("\\", "/") - if 'visualization' in self.simulation: - f = os.path.join(fmt_folder('visualization', subfolder), "%s.hdf5" % name).replace("\\", "/") - self.simulation.visualization[0] = f - elif 'test_structure' in self and 'logfile' in self.test_structure: # hawc2aero - self.test_structure.logfile = os.path.join(fmt_folder('log', subfolder), "%s.log" % name).replace("\\", "/") - if 'output' in self: - self.output.filename = os.path.join(fmt_folder('res', subfolder), "%s" % name).replace("\\", "/") - - def set_time(self, start=None, stop=None, step=None): - self.contents # load if not loaded - if stop is not None: - self.simulation.time_stop = stop - else: - stop = self.simulation.time_stop[0] - if step is not None: - self.simulation.newmark.deltat = step - if start is not None: - self.output.time = start, stop - if "wind" in self: # and self.wind.turb_format[0] > 0: - self.wind.scale_time_start = start - - def expected_simulation_time(self): - return 600 - - def pbs_file(self, hawc2_path, hawc2_cmd, queue='workq', walltime=None, - input_files=None, output_files=None, copy_turb=(True, True)): - walltime = walltime or self.expected_simulation_time() * 2 - if len(copy_turb) == 1: - copy_turb_fwd, copy_turb_back = copy_turb, copy_turb - else: - copy_turb_fwd, copy_turb_back = copy_turb - - input_files = input_files or self.input_files() - if copy_turb_fwd: - input_files += [f for f in self.turbulence_files() if os.path.isfile(f)] - - output_files = output_files or self.output_files() - if copy_turb_back: - output_files += self.turbulence_files() - - return HAWC2PBSFile(hawc2_path, hawc2_cmd, self.filename, self.modelpath, - input_files, output_files, - queue, walltime) - - def input_files(self): - self.contents # load if not loaded - if self.modelpath == "unknown": - files = [str(f).replace("\\", "/") for f in [self.filename] + self.htc_inputfiles] - else: - files = [os.path.abspath(str(f)).replace("\\", "/") for f in [self.filename] + self.htc_inputfiles] - if 'new_htc_structure' in self: - for mb in [self.new_htc_structure[mb] - for mb in self.new_htc_structure.keys() if mb.startswith('main_body')]: - if "timoschenko_input" in mb: - files.append(mb.timoschenko_input.filename[0]) - files.append(mb.get('external_bladedata_dll', [None, None, None])[2]) - if 'aero' in self: - files.append(self.aero.ae_filename[0]) - files.append(self.aero.pc_filename[0]) - files.append(self.aero.get('external_bladedata_dll', [None, None, None])[2]) - files.append(self.aero.get('output_profile_coef_filename', [None])[0]) - if 'dynstall_ateflap' in self.aero: - files.append(self.aero.dynstall_ateflap.get('flap', [None] * 3)[2]) - if 'bemwake_method' in self.aero: - files.append(self.aero.bemwake_method.get('a-ct-filename', [None] * 3)[0]) - for dll in [self.dll[dll] for dll in self.get('dll', {}).keys() if 'filename' in self.dll[dll]]: - files.append(dll.filename[0]) - f, ext = os.path.splitext(dll.filename[0]) - files.append(f + "_64" + ext) - if 'wind' in self: - files.append(self.wind.get('user_defined_shear', [None])[0]) - files.append(self.wind.get('user_defined_shear_turbulence', [None])[0]) - files.append(self.wind.get('met_mast_wind', [None])[0]) - if 'wakes' in self: - files.append(self.wind.get('use_specific_deficit_file', [None])[0]) - files.append(self.wind.get('write_ct_cq_file', [None])[0]) - files.append(self.wind.get('write_final_deficits', [None])[0]) - if 'hydro' in self: - if 'water_properties' in self.hydro: - files.append(self.hydro.water_properties.get('water_kinematics_dll', [None])[0]) - files.append(self.hydro.water_properties.get('water_kinematics_dll', [None, None])[1]) - if 'soil' in self: - if 'soil_element' in self.soil: - files.append(self.soil.soil_element.get('datafile', [None])[0]) - try: - dtu_we_controller = self.dll.get_subsection_by_name('dtu_we_controller') - theta_min = dtu_we_controller.init.constant__5[1] - if theta_min >= 90: - files.append(os.path.join(os.path.dirname( - dtu_we_controller.filename[0]), "wpdata.%d" % theta_min).replace("\\", "/")) - except Exception: - pass - - try: - files.append(self.force.dll.dll[0]) - except Exception: - pass - - def fix_path_case(f): - if os.path.isabs(f): - return self.unix_path(f) - elif self.modelpath != "unknown": - try: - return "./" + os.path.relpath(self.unix_path(os.path.join(self.modelpath, f)), - self.modelpath).replace("\\", "/") - except IOError: - return f - else: - return f - return [fix_path_case(f) for f in set(files) if f] - - def output_files(self): - self.contents # load if not loaded - files = [] - for k, index in [('simulation/logfile', 0), - ('simulation/animation', 0), - ('simulation/visualization', 0), - ('new_htc_structure/beam_output_file_name', 0), - ('new_htc_structure/body_output_file_name', 0), - ('new_htc_structure/struct_inertia_output_file_name', 0), - ('new_htc_structure/body_eigenanalysis_file_name', 0), - ('new_htc_structure/constraint_output_file_name', 0), - ('wind/turb_export/filename_u', 0), - ('wind/turb_export/filename_v', 0), - ('wind/turb_export/filename_w', 0)]: - line = self.get(k) - if line: - files.append(line[index]) - if 'new_htc_structure' in self: - if 'system_eigenanalysis' in self.new_htc_structure: - f = self.new_htc_structure.system_eigenanalysis[0] - files.append(f) - files.append(os.path.join(os.path.dirname(f), 'mode*.dat').replace("\\", "/")) - if 'structure_eigenanalysis_file_name' in self.new_htc_structure: - f = self.new_htc_structure.structure_eigenanalysis_file_name[0] - files.append(f) - files.append(os.path.join(os.path.dirname(f), 'mode*.dat').replace("\\", "/")) - files.extend(self.res_file_lst()) - - for key in [k for k in self.contents.keys() if k.startswith("output_at_time")]: - files.append(self[key]['filename'][0] + ".dat") - return [f.lower() for f in files if f] - - def turbulence_files(self): - self.contents # load if not loaded - if 'wind' not in self.contents.keys() or self.wind.turb_format[0] == 0: - return [] - elif self.wind.turb_format[0] == 1: - files = [self.get('wind.mann.filename_%s' % comp, [None])[0] for comp in ['u', 'v', 'w']] - elif self.wind.turb_format[0] == 2: - files = [self.get('wind.flex.filename_%s' % comp, [None])[0] for comp in ['u', 'v', 'w']] - return [f for f in files if f] - - def res_file_lst(self): - self.contents # load if not loaded - res = [] - for output in [self[k] for k in self.keys() - if self[k].name_.startswith("output") and not self[k].name_.startswith("output_at_time")]: - dataformat = output.get('data_format', 'hawc_ascii') - res_filename = output.filename[0] - if dataformat[0] == "gtsdf" or dataformat[0] == "gtsdf64": - res.append(res_filename + ".hdf5") - elif dataformat[0] == "flex_int": - res.extend([res_filename + ".int", os.path.join(os.path.dirname(res_filename), 'sensor')]) - else: - res.extend([res_filename + ".sel", res_filename + ".dat"]) - return res - - def _simulate(self, exe, skip_if_up_to_date=False): - self.contents # load if not loaded - if skip_if_up_to_date: - from os.path import isfile, getmtime, isabs - res_file = os.path.join(self.modelpath, self.res_file_lst()[0]) - htc_file = os.path.join(self.modelpath, self.filename) - if isabs(exe): - exe_file = exe - else: - exe_file = os.path.join(self.modelpath, exe) - #print (from_unix(getmtime(res_file)), from_unix(getmtime(htc_file))) - if (isfile(htc_file) and isfile(res_file) and isfile(exe_file) and - str(HTCFile(htc_file)) == str(self) and - getmtime(res_file) > getmtime(htc_file) and getmtime(res_file) > getmtime(exe_file)): - if "".join(self.readfilelines(htc_file)) == str(self): - return - - self.save() - htcfile = os.path.relpath(self.filename, self.modelpath) - assert any([os.path.isfile(os.path.join(f, exe)) for f in [''] + os.environ['PATH'].split(";")]), exe - return pexec([exe, htcfile], self.modelpath) - - def simulate(self, exe, skip_if_up_to_date=False): - errorcode, stdout, stderr, cmd = self._simulate(exe, skip_if_up_to_date) - if ('simulation' in self.keys() and "logfile" in self.simulation and - os.path.isfile(os.path.join(self.modelpath, self.simulation.logfile[0]))): - with self.open(os.path.join(self.modelpath, self.simulation.logfile[0])) as fid: - log = fid.read() - else: - log = "%s\n%s" % (str(stdout), str(stderr)) - - if errorcode or 'Elapsed time' not in log: - log_lines = log.split("\n") - error_lines = [i for i, l in enumerate(log_lines) if 'error' in l.lower()] - if error_lines: - import numpy as np - line_i = np.r_[np.array([error_lines + i for i in np.arange(-3, 4)]).flatten(), - np.arange(-5, 0) + len(log_lines)] - line_i = sorted(np.unique(np.maximum(np.minimum(line_i, len(log_lines) - 1), 0))) - - lines = ["%04d %s" % (i, log_lines[i]) for i in line_i] - for jump in np.where(np.diff(line_i) > 1)[0]: - lines.insert(jump, "...") - - error_log = "\n".join(lines) - else: - error_log = log - raise Exception("\nError code: %s\nstdout:\n%s\n--------------\nstderr:\n%s\n--------------\nlog:\n%s\n--------------\ncmd:\n%s" % - (errorcode, str(stdout), str(stderr), error_log, cmd)) - return str(stdout) + str(stderr), log - - def simulate_hawc2stab2(self, exe): - errorcode, stdout, stderr, cmd = self._simulate(exe, skip_if_up_to_date=False) - - if errorcode: - raise Exception("\nstdout:\n%s\n--------------\nstderr:\n%s\n--------------\ncmd:\n%s" % - (str(stdout), str(stderr), cmd)) - return str(stdout) + str(stderr) - - def deltat(self): - return self.simulation.newmark.deltat[0] - - def compare(self, other): - if isinstance(other, str): - other = HTCFile(other) - return HTCContents.compare(self, other) - - @property - def open(self): - return open - - def unix_path(self, filename): - filename = os.path.realpath(str(filename)).replace("\\", "/") - ufn, rest = os.path.splitdrive(filename) - ufn += "/" - for f in rest[1:].split("/"): - f_lst = [f_ for f_ in os.listdir(ufn) if f_.lower() == f.lower()] - if len(f_lst) > 1: - # use the case sensitive match - f_lst = [f_ for f_ in f_lst if f_ == f] - if len(f_lst) == 0: - raise IOError("'%s' not found in '%s'" % (f, ufn)) - else: # one match found - ufn = os.path.join(ufn, f_lst[0]) - return ufn.replace("\\", "/") - - -# -# def get_body(self, name): -# lst = [b for b in self.new_htc_structure if b.name_=="main_body" and b.name[0]==name] -# if len(lst)==1: -# return lst[0] -# else: -# if len(lst)==0: -# raise ValueError("Body '%s' not found"%name) -# else: -# raise NotImplementedError() -# - -class H2aeroHTCFile(HTCFile): - def __init__(self, filename=None, modelpath=None): - HTCFile.__init__(self, filename=filename, modelpath=modelpath) - - @property - def simulation(self): - return self.test_structure - - def set_time(self, start=None, stop=None, step=None): - if stop is not None: - self.test_structure.time_stop = stop - else: - stop = self.simulation.time_stop[0] - if step is not None: - self.test_structure.deltat = step - if start is not None: - self.output.time = start, stop - if "wind" in self and self.wind.turb_format[0] > 0: - self.wind.scale_time_start = start - - -class SSH_HTCFile(HTCFile): - def __init__(self, ssh, filename=None, modelpath=None): - object.__setattr__(self, 'ssh', ssh) - HTCFile.__init__(self, filename=filename, modelpath=modelpath) - - @property - def open(self): - return self.ssh.open - - def unix_path(self, filename): - rel_filename = os.path.relpath(filename, self.modelpath).replace("\\", "/") - _, out, _ = self.ssh.execute("find -ipath ./%s" % rel_filename, cwd=self.modelpath) - out = out.strip() - if out == "": - raise IOError("'%s' not found in '%s'" % (rel_filename, self.modelpath)) - elif "\n" in out: - raise IOError("Multiple '%s' found in '%s' (due to case senitivity)" % (rel_filename, self.modelpath)) - else: - drive, path = os.path.splitdrive(os.path.join(self.modelpath, out)) - path = os.path.realpath(path).replace("\\", "/") - return os.path.join(drive, os.path.splitdrive(path)[1]) - - -if "__main__" == __name__: - f = HTCFile(r"C:/Work/BAR-Local/Hawc2ToBeamDyn/sim.htc", ".") - print(f.input_files()) -# f.save(r"C:\mmpe\HAWC2\models\DTU10MWRef6.0\htc\DTU_10MW_RWT_power_curve.htc") -# -# f = HTCFile(r"C:\mmpe\HAWC2\models\DTU10MWRef6.0\htc\DTU_10MW_RWT.htc", "../") -# f.set_time = 0, 1, .1 -# print(f.simulate(r"C:\mmpe\HAWC2\bin\HAWC2_12.8\hawc2mb.exe")) -# -# f.save(r"C:\mmpe\HAWC2\models\DTU10MWRef6.0\htc\DTU_10MW_RWT.htc") +''' +Created on 20/01/2014 + +See documentation of HTCFile below + +''' +# from wetb.utils.process_exec import pexec +# from wetb.hawc2.hawc2_pbs_file import HAWC2PBSFile +# import jinja2 +# from wetb.utils.cluster_tools.os_path import fixcase, abspath, pjoin + +from collections import OrderedDict +from .htc_contents import HTCContents, HTCSection, HTCLine +from .htc_extensions import HTCDefaults, HTCExtensions +import os + +# --- cluster_tools/os_path +def fmt_path(path): + return path.lower().replace("\\", "/") + +def repl(path): + return path.replace("\\", "/") + +def abspath(path): + return repl(os.path.abspath(path)) + +def relpath(path, start=None): + return repl(os.path.relpath(path, start)) + +def realpath(path): + return repl(os.path.realpath(path)) + +def pjoin(*path): + return repl(os.path.join(*path)) + +def fixcase(path): + path = realpath(str(path)).replace("\\", "/") + p, rest = os.path.splitdrive(path) + p += "/" + for f in rest[1:].split("/"): + f_lst = [f_ for f_ in os.listdir(p) if f_.lower() == f.lower()] + if len(f_lst) > 1: + # use the case sensitive match + f_lst = [f_ for f_ in f_lst if f_ == f] + if len(f_lst) == 0: + raise IOError("'%s' not found in '%s'" % (f, p)) + # Use matched folder + p = pjoin(p, f_lst[0]) + return p +# --- end os_path + +class HTCFile(HTCContents, HTCDefaults, HTCExtensions): + """Wrapper for HTC files + + Examples: + --------- + >>> htcfile = HTCFile('htc/test.htc') + >>> htcfile.wind.wsp = 10 + >>> htcfile.save() + + #--------------------------------------------- + >>> htc = HTCFile(filename=None, modelpath=None) # create minimal htcfile + + #Add section + >>> htc.add_section('hydro') + + #Add subsection + >>> htc.hydro.add_section("hydro_element") + + #Set values + >>> htc.hydro.hydro_element.wave_breaking = [2, 6.28, 1] # or + >>> htc.hydro.hydro_element.wave_breaking = 2, 6.28, 1 + + #Set comments + >>> htc.hydro.hydro_element.wave_breaking.comments = "This is a comment" + + #Access section + >>> hydro_element = htc.hydro.hydro_element #or + >>> hydro_element = htc['hydro.hydro_element'] # or + >>> hydro_element = htc['hydro/hydro_element'] # or + >>> print (hydro_element.wave_breaking) #string represenation + wave_breaking 2 6.28 1; This is a comment + >>> print (hydro_element.wave_breaking.name_) # command + wave_breaking + >>> print (hydro_element.wave_breaking.values) # values + [2, 6.28, 1 + >>> print (hydro_element.wave_breaking.comments) # comments + This is a comment + >>> print (hydro_element.wave_breaking[0]) # first value + 2 + + #Delete element + htc.simulation.logfile.delete() + #or + del htc.simulation.logfile #Delete logfile line. Raise keyerror if not exists + + """ + + filename = None + jinja_tags = {} + htc_inputfiles = [] + level = 0 + modelpath = "../" + initial_comments = None + + def __init__(self, filename=None, modelpath=None, jinja_tags={}): + """ + Parameters + --------- + filename : str + Absolute filename of htc file + modelpath : str + Model path relative to htc file + """ + if filename is not None: + try: + filename = fixcase(abspath(filename)) + with self.open(str(filename)): + pass + except Exception: + pass + + self.filename = filename + + self.jinja_tags = jinja_tags + self.modelpath = modelpath or self.auto_detect_modelpath() + + if filename and self.modelpath != "unknown" and not os.path.isabs(self.modelpath): + drive, p = os.path.splitdrive(os.path.join(os.path.dirname(str(self.filename)), self.modelpath)) + self.modelpath = os.path.join(drive, os.path.splitdrive(os.path.realpath(p))[1]).replace("\\", "/") + if self.modelpath != 'unknown' and self.modelpath[-1] != '/': + self.modelpath += "/" + + self.load() + + def auto_detect_modelpath(self): + if self.filename is None: + return "../" + + #print (["../"*i for i in range(3)]) + import numpy as np + input_files = HTCFile(self.filename, 'unknown').input_files() + if len(input_files) == 1: # only input file is the htc file + return "../" + rel_input_files = [f for f in input_files if not os.path.isabs(f)] + + def isfile_case_insensitive(f): + try: + f = fixcase(f) # raises exception if not existing + return os.path.isfile(f) + except IOError: + return False + found = ([np.sum([isfile_case_insensitive(os.path.join(os.path.dirname(self.filename), "../" * i, f)) + for f in rel_input_files]) for i in range(4)]) + + if max(found) > 0: + relpath = "../" * np.argmax(found) + return abspath(pjoin(os.path.dirname(self.filename), relpath)) + else: + print("Modelpath cannot be autodetected for '%s'.\nInput files not found near htc file" % self.filename) + return 'unknown' + + def load(self): + self.contents = OrderedDict() + self.initial_comments = [] + self.htc_inputfiles = [] + if self.filename is None: + lines = self.empty_htc.split("\n") + else: + lines = self.readlines(self.filename) + + lines = [l.strip() for l in lines] + + #lines = copy(self.lines) + while lines: + if lines[0].startswith(";"): + self.initial_comments.append(lines.pop(0).strip() + "\n") + elif lines[0].lower().startswith("begin"): + self._add_contents(HTCSection.from_lines(lines)) + else: + line = HTCLine.from_lines(lines) + if line.name_ == "exit": + break + self._add_contents(line) + + def readfilelines(self, filename): + with self.open(self.unix_path(os.path.abspath(filename.replace('\\', '/'))), encoding='cp1252') as fid: + txt = fid.read() + if txt[:10].encode().startswith(b'\xc3\xaf\xc2\xbb\xc2\xbf'): + txt = txt[3:] + if self.jinja_tags: + template = jinja2.Template(txt) + txt = template.render(**self.jinja_tags) + return txt.replace("\r", "").split("\n") + + def readlines(self, filename): + if filename != self.filename: # self.filename may be changed by set_name/save. Added it when needed instead + self.htc_inputfiles.append(filename) + htc_lines = [] + lines = self.readfilelines(filename) + for l in lines: + if l.lower().lstrip().startswith('continue_in_file'): + filename = l.lstrip().split(";")[0][len("continue_in_file"):].strip().lower() + + if self.modelpath == 'unknown': + p = os.path.dirname(self.filename) + try: + lu = [os.path.isfile(os.path.abspath(os.path.join(p, "../" * i, filename.replace("\\", "/")))) + for i in range(4)].index(True) + filename = os.path.join(p, "../" * lu, filename) + except ValueError: + print('[FAIL] Cannot continue in file: {}'.format(filename)) + filename = None + else: + filename = os.path.join(self.modelpath, filename) + if not os.path.isfile(filename): + print('[FAIL] Cannot continue in file: {}'.format(filename)) + filename=None + if filename is not None: + #print('[INFO] Continuing in file: {}'.format(filename)) + for line in self.readlines(filename): + if line.lstrip().lower().startswith('exit'): + break + htc_lines.append(line) + else: + htc_lines.append(l) + return htc_lines + + def __setitem__(self, key, value): + self.contents[key] = value + + def __str__(self): + self.contents # load + return "".join(self.initial_comments + [c.__str__(1) for c in self] + ["exit;"]) + + def save(self, filename=None): + """Saves the htc object to an htc file. + + Args: + filename (str, optional): Specifies the filename of the htc file to be saved. + If the value is none, the filename attribute of the object will be used as the filename. + Defaults to None. + """ + self.contents # load if not loaded + if filename is None: + filename = self.filename + else: + self.filename = filename + # exist_ok does not exist in Python27 + if not os.path.exists(os.path.dirname(filename)) and os.path.dirname(filename) != "": + os.makedirs(os.path.dirname(filename)) # , exist_ok=True) + with self.open(filename, 'w', encoding='cp1252') as fid: + fid.write(str(self)) + + def set_name(self, name, subfolder=''): + """Sets the base filename of the simulation files. + + Args: + name (str): Specifies name of the log file, dat file (for animation), hdf5 file (for visualization) and htc file. + subfolder (str, optional): Specifies the name of a subfolder to place the files in. + If the value is an empty string, no subfolders will be created. + Defaults to ''. + + Returns: + None + """ + # if os.path.isabs(folder) is False and os.path.relpath(folder).startswith("htc" + os.path.sep): + self.contents # load if not loaded + + def fmt_folder(folder, subfolder): return "./" + \ + os.path.relpath(os.path.join(folder, subfolder)).replace("\\", "/") + + self.filename = os.path.abspath(os.path.join(self.modelpath, fmt_folder( + 'htc', subfolder), "%s.htc" % name)).replace("\\", "/") + if 'simulation' in self and 'logfile' in self.simulation: + self.simulation.logfile = os.path.join(fmt_folder('log', subfolder), "%s.log" % name).replace("\\", "/") + if 'animation' in self.simulation: + self.simulation.animation = os.path.join(fmt_folder( + 'animation', subfolder), "%s.dat" % name).replace("\\", "/") + if 'visualization' in self.simulation: + f = os.path.join(fmt_folder('visualization', subfolder), "%s.hdf5" % name).replace("\\", "/") + self.simulation.visualization[0] = f + elif 'test_structure' in self and 'logfile' in self.test_structure: # hawc2aero + self.test_structure.logfile = os.path.join(fmt_folder('log', subfolder), "%s.log" % name).replace("\\", "/") + if 'output' in self: + self.output.filename = os.path.join(fmt_folder('res', subfolder), "%s" % name).replace("\\", "/") + + def set_time(self, start=None, stop=None, step=None): + self.contents # load if not loaded + if stop is not None: + self.simulation.time_stop = stop + else: + stop = self.simulation.time_stop[0] + if step is not None: + self.simulation.newmark.deltat = step + if start is not None: + self.output.time = start, stop + if "wind" in self: # and self.wind.turb_format[0] > 0: + self.wind.scale_time_start = start + + def expected_simulation_time(self): + return 600 + + def pbs_file(self, hawc2_path, hawc2_cmd, queue='workq', walltime=None, + input_files=None, output_files=None, copy_turb=(True, True)): + walltime = walltime or self.expected_simulation_time() * 2 + if len(copy_turb) == 1: + copy_turb_fwd, copy_turb_back = copy_turb, copy_turb + else: + copy_turb_fwd, copy_turb_back = copy_turb + + input_files = input_files or self.input_files() + if copy_turb_fwd: + input_files += [f for f in self.turbulence_files() if os.path.isfile(f)] + + output_files = output_files or self.output_files() + if copy_turb_back: + output_files += self.turbulence_files() + + return HAWC2PBSFile(hawc2_path, hawc2_cmd, self.filename, self.modelpath, + input_files, output_files, + queue, walltime) + + def input_files(self): + self.contents # load if not loaded + if self.modelpath == "unknown": + files = [str(f).replace("\\", "/") for f in [self.filename] + self.htc_inputfiles] + else: + files = [os.path.abspath(str(f)).replace("\\", "/") for f in [self.filename] + self.htc_inputfiles] + if 'new_htc_structure' in self: + for mb in [self.new_htc_structure[mb] + for mb in self.new_htc_structure.keys() if mb.startswith('main_body')]: + if "timoschenko_input" in mb: + files.append(mb.timoschenko_input.filename[0]) + files.append(mb.get('external_bladedata_dll', [None, None, None])[2]) + if 'aero' in self: + files.append(self.aero.ae_filename[0]) + files.append(self.aero.pc_filename[0]) + files.append(self.aero.get('external_bladedata_dll', [None, None, None])[2]) + files.append(self.aero.get('output_profile_coef_filename', [None])[0]) + if 'dynstall_ateflap' in self.aero: + files.append(self.aero.dynstall_ateflap.get('flap', [None] * 3)[2]) + if 'bemwake_method' in self.aero: + files.append(self.aero.bemwake_method.get('a-ct-filename', [None] * 3)[0]) + for dll in [self.dll[dll] for dll in self.get('dll', {}).keys() if 'filename' in self.dll[dll]]: + files.append(dll.filename[0]) + f, ext = os.path.splitext(dll.filename[0]) + files.append(f + "_64" + ext) + if 'wind' in self: + files.append(self.wind.get('user_defined_shear', [None])[0]) + files.append(self.wind.get('user_defined_shear_turbulence', [None])[0]) + files.append(self.wind.get('met_mast_wind', [None])[0]) + if 'wakes' in self: + files.append(self.wind.get('use_specific_deficit_file', [None])[0]) + files.append(self.wind.get('write_ct_cq_file', [None])[0]) + files.append(self.wind.get('write_final_deficits', [None])[0]) + if 'hydro' in self: + if 'water_properties' in self.hydro: + files.append(self.hydro.water_properties.get('water_kinematics_dll', [None])[0]) + files.append(self.hydro.water_properties.get('water_kinematics_dll', [None, None])[1]) + if 'soil' in self: + if 'soil_element' in self.soil: + files.append(self.soil.soil_element.get('datafile', [None])[0]) + try: + dtu_we_controller = self.dll.get_subsection_by_name('dtu_we_controller') + theta_min = dtu_we_controller.init.constant__5[1] + if theta_min >= 90: + files.append(os.path.join(os.path.dirname( + dtu_we_controller.filename[0]), "wpdata.%d" % theta_min).replace("\\", "/")) + except Exception: + pass + + try: + files.append(self.force.dll.dll[0]) + except Exception: + pass + + def fix_path_case(f): + if os.path.isabs(f): + return self.unix_path(f) + elif self.modelpath != "unknown": + try: + return "./" + os.path.relpath(self.unix_path(os.path.join(self.modelpath, f)), + self.modelpath).replace("\\", "/") + except IOError: + return f + else: + return f + return [fix_path_case(f) for f in set(files) if f] + + def output_files(self): + self.contents # load if not loaded + files = [] + for k, index in [('simulation/logfile', 0), + ('simulation/animation', 0), + ('simulation/visualization', 0), + ('new_htc_structure/beam_output_file_name', 0), + ('new_htc_structure/body_output_file_name', 0), + ('new_htc_structure/struct_inertia_output_file_name', 0), + ('new_htc_structure/body_eigenanalysis_file_name', 0), + ('new_htc_structure/constraint_output_file_name', 0), + ('wind/turb_export/filename_u', 0), + ('wind/turb_export/filename_v', 0), + ('wind/turb_export/filename_w', 0)]: + line = self.get(k) + if line: + files.append(line[index]) + if 'new_htc_structure' in self: + if 'system_eigenanalysis' in self.new_htc_structure: + f = self.new_htc_structure.system_eigenanalysis[0] + files.append(f) + files.append(os.path.join(os.path.dirname(f), 'mode*.dat').replace("\\", "/")) + if 'structure_eigenanalysis_file_name' in self.new_htc_structure: + f = self.new_htc_structure.structure_eigenanalysis_file_name[0] + files.append(f) + files.append(os.path.join(os.path.dirname(f), 'mode*.dat').replace("\\", "/")) + files.extend(self.res_file_lst()) + + for key in [k for k in self.contents.keys() if k.startswith("output_at_time")]: + files.append(self[key]['filename'][0] + ".dat") + return [f.lower() for f in files if f] + + def turbulence_files(self): + self.contents # load if not loaded + if 'wind' not in self.contents.keys() or self.wind.turb_format[0] == 0: + return [] + elif self.wind.turb_format[0] == 1: + files = [self.get('wind.mann.filename_%s' % comp, [None])[0] for comp in ['u', 'v', 'w']] + elif self.wind.turb_format[0] == 2: + files = [self.get('wind.flex.filename_%s' % comp, [None])[0] for comp in ['u', 'v', 'w']] + return [f for f in files if f] + + def res_file_lst(self): + self.contents # load if not loaded + res = [] + for output in [self[k] for k in self.keys() + if self[k].name_.startswith("output") and not self[k].name_.startswith("output_at_time")]: + dataformat = output.get('data_format', 'hawc_ascii') + res_filename = output.filename[0] + if dataformat[0] == "gtsdf" or dataformat[0] == "gtsdf64": + res.append(res_filename + ".hdf5") + elif dataformat[0] == "flex_int": + res.extend([res_filename + ".int", os.path.join(os.path.dirname(res_filename), 'sensor')]) + else: + res.extend([res_filename + ".sel", res_filename + ".dat"]) + return res + + def _simulate(self, exe, skip_if_up_to_date=False): + self.contents # load if not loaded + if skip_if_up_to_date: + from os.path import isfile, getmtime, isabs + res_file = os.path.join(self.modelpath, self.res_file_lst()[0]) + htc_file = os.path.join(self.modelpath, self.filename) + if isabs(exe): + exe_file = exe + else: + exe_file = os.path.join(self.modelpath, exe) + #print (from_unix(getmtime(res_file)), from_unix(getmtime(htc_file))) + if (isfile(htc_file) and isfile(res_file) and isfile(exe_file) and + str(HTCFile(htc_file)) == str(self) and + getmtime(res_file) > getmtime(htc_file) and getmtime(res_file) > getmtime(exe_file)): + if "".join(self.readfilelines(htc_file)) == str(self): + return + + self.save() + htcfile = os.path.relpath(self.filename, self.modelpath) + assert any([os.path.isfile(os.path.join(f, exe)) for f in [''] + os.environ['PATH'].split(";")]), exe + return pexec([exe, htcfile], self.modelpath) + + def simulate(self, exe, skip_if_up_to_date=False): + errorcode, stdout, stderr, cmd = self._simulate(exe, skip_if_up_to_date) + if ('simulation' in self.keys() and "logfile" in self.simulation and + os.path.isfile(os.path.join(self.modelpath, self.simulation.logfile[0]))): + with self.open(os.path.join(self.modelpath, self.simulation.logfile[0])) as fid: + log = fid.read() + else: + log = "%s\n%s" % (str(stdout), str(stderr)) + + if errorcode or 'Elapsed time' not in log: + log_lines = log.split("\n") + error_lines = [i for i, l in enumerate(log_lines) if 'error' in l.lower()] + if error_lines: + import numpy as np + line_i = np.r_[np.array([error_lines + i for i in np.arange(-3, 4)]).flatten(), + np.arange(-5, 0) + len(log_lines)] + line_i = sorted(np.unique(np.maximum(np.minimum(line_i, len(log_lines) - 1), 0))) + + lines = ["%04d %s" % (i, log_lines[i]) for i in line_i] + for jump in np.where(np.diff(line_i) > 1)[0]: + lines.insert(jump, "...") + + error_log = "\n".join(lines) + else: + error_log = log + raise Exception("\nError code: %s\nstdout:\n%s\n--------------\nstderr:\n%s\n--------------\nlog:\n%s\n--------------\ncmd:\n%s" % + (errorcode, str(stdout), str(stderr), error_log, cmd)) + return str(stdout) + str(stderr), log + + def simulate_hawc2stab2(self, exe): + errorcode, stdout, stderr, cmd = self._simulate(exe, skip_if_up_to_date=False) + + if errorcode: + raise Exception("\nstdout:\n%s\n--------------\nstderr:\n%s\n--------------\ncmd:\n%s" % + (str(stdout), str(stderr), cmd)) + return str(stdout) + str(stderr) + + def deltat(self): + return self.simulation.newmark.deltat[0] + + def compare(self, other): + if isinstance(other, str): + other = HTCFile(other) + return HTCContents.compare(self, other) + + @property + def open(self): + return open + + def unix_path(self, filename): + filename = os.path.realpath(str(filename)).replace("\\", "/") + ufn, rest = os.path.splitdrive(filename) + ufn += "/" + for f in rest[1:].split("/"): + f_lst = [f_ for f_ in os.listdir(ufn) if f_.lower() == f.lower()] + if len(f_lst) > 1: + # use the case sensitive match + f_lst = [f_ for f_ in f_lst if f_ == f] + if len(f_lst) == 0: + raise IOError("'%s' not found in '%s'" % (f, ufn)) + else: # one match found + ufn = os.path.join(ufn, f_lst[0]) + return ufn.replace("\\", "/") + + +# +# def get_body(self, name): +# lst = [b for b in self.new_htc_structure if b.name_=="main_body" and b.name[0]==name] +# if len(lst)==1: +# return lst[0] +# else: +# if len(lst)==0: +# raise ValueError("Body '%s' not found"%name) +# else: +# raise NotImplementedError() +# + +class H2aeroHTCFile(HTCFile): + def __init__(self, filename=None, modelpath=None): + HTCFile.__init__(self, filename=filename, modelpath=modelpath) + + @property + def simulation(self): + return self.test_structure + + def set_time(self, start=None, stop=None, step=None): + if stop is not None: + self.test_structure.time_stop = stop + else: + stop = self.simulation.time_stop[0] + if step is not None: + self.test_structure.deltat = step + if start is not None: + self.output.time = start, stop + if "wind" in self and self.wind.turb_format[0] > 0: + self.wind.scale_time_start = start + + +class SSH_HTCFile(HTCFile): + def __init__(self, ssh, filename=None, modelpath=None): + object.__setattr__(self, 'ssh', ssh) + HTCFile.__init__(self, filename=filename, modelpath=modelpath) + + @property + def open(self): + return self.ssh.open + + def unix_path(self, filename): + rel_filename = os.path.relpath(filename, self.modelpath).replace("\\", "/") + _, out, _ = self.ssh.execute("find -ipath ./%s" % rel_filename, cwd=self.modelpath) + out = out.strip() + if out == "": + raise IOError("'%s' not found in '%s'" % (rel_filename, self.modelpath)) + elif "\n" in out: + raise IOError("Multiple '%s' found in '%s' (due to case senitivity)" % (rel_filename, self.modelpath)) + else: + drive, path = os.path.splitdrive(os.path.join(self.modelpath, out)) + path = os.path.realpath(path).replace("\\", "/") + return os.path.join(drive, os.path.splitdrive(path)[1]) + + +if "__main__" == __name__: + f = HTCFile(r"C:/Work/BAR-Local/Hawc2ToBeamDyn/sim.htc", ".") + print(f.input_files()) +# f.save(r"C:\mmpe\HAWC2\models\DTU10MWRef6.0\htc\DTU_10MW_RWT_power_curve.htc") +# +# f = HTCFile(r"C:\mmpe\HAWC2\models\DTU10MWRef6.0\htc\DTU_10MW_RWT.htc", "../") +# f.set_time = 0, 1, .1 +# print(f.simulate(r"C:\mmpe\HAWC2\bin\HAWC2_12.8\hawc2mb.exe")) +# +# f.save(r"C:\mmpe\HAWC2\models\DTU10MWRef6.0\htc\DTU_10MW_RWT.htc") diff --git a/pyFAST/input_output/wetb/hawc2/htc_file_set.py b/openfast_toolbox/io/wetb/hawc2/htc_file_set.py similarity index 97% rename from pyFAST/input_output/wetb/hawc2/htc_file_set.py rename to openfast_toolbox/io/wetb/hawc2/htc_file_set.py index 7294185..990f52b 100644 --- a/pyFAST/input_output/wetb/hawc2/htc_file_set.py +++ b/openfast_toolbox/io/wetb/hawc2/htc_file_set.py @@ -1,55 +1,55 @@ -import glob -import os -import copy -from wetb.hawc2.hawc2_pbs_file import JESS_WINE32_HAWC2MB -from wetb.hawc2.htc_file import HTCFile -from wetb.utils.cluster_tools.pbsfile import PBSMultiRunner - - -class HTCFileSet(): - def __init__(self, model_path, htc_lst="**/*.htc"): - self.model_path = model_path - - if not isinstance(htc_lst, list): - htc_lst = [htc_lst] - - self.htc_files = [] - for htc_path in htc_lst: - if os.path.isfile(htc_path): - self.htc_files.append(htc_path) - else: - if not os.path.isabs(htc_path): - htc_path = os.path.join(model_path, htc_path) - for filename in glob.iglob(htc_path, recursive=True): - self.htc_files.append(filename) - - def pbs_files(self, hawc2_path, hawc2_cmd, queue='workq', walltime=None, - input_files=None, output_files=None, copy_turb=(True, True)): - - return (HTCFile(htc).pbs_file(hawc2_path, hawc2_cmd, queue=queue, walltime=walltime, - input_files=copy.copy(input_files), - output_files=copy.copy(output_files), - copy_turb=copy_turb) for htc in self.htc_files) - - def save_pbs_files(self, hawc2_path=None, hawc2_cmd=JESS_WINE32_HAWC2MB, queue='workq', walltime=None, - input_files=None, output_files=None, copy_turb=(True, True)): - for pbs in self.pbs_files(hawc2_path, hawc2_cmd, queue=queue, walltime=walltime, - input_files=input_files, output_files=output_files, - copy_turb=copy_turb): - pbs.save(self.model_path) - - -if __name__ == '__main__': - #model_path = r'R:\HAWC2_tests\v12.6_mmpe3\win32\simple1' - model_path = "w:/simple1" - pbs_files = HTCFileSet(model_path).pbs_files( - hawc2_path=r"R:\HAWC2_tests\v12.6_mmpe3\hawc2\win32", hawc2_cmd=JESS_WINE32_HAWC2MB, input_files=['data/*']) - import pandas as pd - time_overview = pd.read_excel( - r'C:\mmpe\programming\Fortran\HAWC2_git\HAWC2\pytest_hawc2\release_tests\Time_overview.xlsx') - for pbs in pbs_files: - f = pbs.filename - - pbs.walltime = time_overview.loc[f[:-3].replace("pbs_in/", 'simple1/')]['mean'] * 24 * 3600 - pbs.save(model_path) - PBSMultiRunner(model_path, nodes=1, ppn=10).save() +import glob +import os +import copy +from wetb.hawc2.hawc2_pbs_file import JESS_WINE32_HAWC2MB +from wetb.hawc2.htc_file import HTCFile +from wetb.utils.cluster_tools.pbsfile import PBSMultiRunner + + +class HTCFileSet(): + def __init__(self, model_path, htc_lst="**/*.htc"): + self.model_path = model_path + + if not isinstance(htc_lst, list): + htc_lst = [htc_lst] + + self.htc_files = [] + for htc_path in htc_lst: + if os.path.isfile(htc_path): + self.htc_files.append(htc_path) + else: + if not os.path.isabs(htc_path): + htc_path = os.path.join(model_path, htc_path) + for filename in glob.iglob(htc_path, recursive=True): + self.htc_files.append(filename) + + def pbs_files(self, hawc2_path, hawc2_cmd, queue='workq', walltime=None, + input_files=None, output_files=None, copy_turb=(True, True)): + + return (HTCFile(htc).pbs_file(hawc2_path, hawc2_cmd, queue=queue, walltime=walltime, + input_files=copy.copy(input_files), + output_files=copy.copy(output_files), + copy_turb=copy_turb) for htc in self.htc_files) + + def save_pbs_files(self, hawc2_path=None, hawc2_cmd=JESS_WINE32_HAWC2MB, queue='workq', walltime=None, + input_files=None, output_files=None, copy_turb=(True, True)): + for pbs in self.pbs_files(hawc2_path, hawc2_cmd, queue=queue, walltime=walltime, + input_files=input_files, output_files=output_files, + copy_turb=copy_turb): + pbs.save(self.model_path) + + +if __name__ == '__main__': + #model_path = r'R:\HAWC2_tests\v12.6_mmpe3\win32\simple1' + model_path = "w:/simple1" + pbs_files = HTCFileSet(model_path).pbs_files( + hawc2_path=r"R:\HAWC2_tests\v12.6_mmpe3\hawc2\win32", hawc2_cmd=JESS_WINE32_HAWC2MB, input_files=['data/*']) + import pandas as pd + time_overview = pd.read_excel( + r'C:\mmpe\programming\Fortran\HAWC2_git\HAWC2\pytest_hawc2\release_tests\Time_overview.xlsx') + for pbs in pbs_files: + f = pbs.filename + + pbs.walltime = time_overview.loc[f[:-3].replace("pbs_in/", 'simple1/')]['mean'] * 24 * 3600 + pbs.save(model_path) + PBSMultiRunner(model_path, nodes=1, ppn=10).save() diff --git a/pyFAST/input_output/wetb/hawc2/pc_file.py b/openfast_toolbox/io/wetb/hawc2/pc_file.py similarity index 100% rename from pyFAST/input_output/wetb/hawc2/pc_file.py rename to openfast_toolbox/io/wetb/hawc2/pc_file.py diff --git a/pyFAST/input_output/wetb/hawc2/st_file.py b/openfast_toolbox/io/wetb/hawc2/st_file.py similarity index 100% rename from pyFAST/input_output/wetb/hawc2/st_file.py rename to openfast_toolbox/io/wetb/hawc2/st_file.py diff --git a/pyFAST/linearization/README.rst b/openfast_toolbox/linearization/README.rst similarity index 100% rename from pyFAST/linearization/README.rst rename to openfast_toolbox/linearization/README.rst diff --git a/openfast_toolbox/linearization/__init__.py b/openfast_toolbox/linearization/__init__.py new file mode 100644 index 0000000..71ed960 --- /dev/null +++ b/openfast_toolbox/linearization/__init__.py @@ -0,0 +1,18 @@ + +# NOTE: we make the main functions available here, so that we can change the interface in the future. +from openfast_toolbox.linearization.tools import getMBCOP, getCampbellDataOP +from openfast_toolbox.linearization.tools import writeModesForViz +from openfast_toolbox.linearization.tools import readModesForViz +from openfast_toolbox.linearization.tools import writeVizFile +from openfast_toolbox.linearization.tools import writeVizFiles + +from openfast_toolbox.linearization.mbc import fx_mbc3 +from openfast_toolbox.linearization.campbell import postproCampbell, plotCampbell, plotCampbellDataFile +from openfast_toolbox.linearization.campbell_data import IdentifyModes +from openfast_toolbox.linearization.campbell_data import IdentifiedModesDict +from openfast_toolbox.linearization.campbell_data import printCampbellDataOP +from openfast_toolbox.linearization.campbell_data import campbellData2TXT +from openfast_toolbox.linearization.campbell_data import extractShortModeDescription +from openfast_toolbox.linearization.campbell_data import campbell_diagram_data_oneOP + +from openfast_toolbox.linearization.linearization import writeLinearizationFiles diff --git a/pyFAST/linearization/campbell.py b/openfast_toolbox/linearization/campbell.py similarity index 96% rename from pyFAST/linearization/campbell.py rename to openfast_toolbox/linearization/campbell.py index 44512a7..4b17a3f 100644 --- a/pyFAST/linearization/campbell.py +++ b/openfast_toolbox/linearization/campbell.py @@ -1,515 +1,515 @@ -""" - -Generic functions to help setup a Campbell diagram with OpenFAST - -These are high level functions for manipulate multiple operating points (multiple .fst files). - -For more granularity, see: - - pyFAST.linearization.tools.py - - pyFAST.linearization.campbell_data.py - -Main functions: - -- postproMBC(xlsFile=None, csvBase=None, sortedSuffix=None, csvModesID=None, xlssheet=None): - Generate Cambell diagram data from an xls file, or a set of csv files - - -""" - -import os -import pandas as pd -# import re -import numpy as np -from pyFAST.linearization.tools import getMBCOPs -from pyFAST.linearization.tools import getCampbellDataOPs -from pyFAST.linearization.tools import estimateLengths - -from pyFAST.linearization.campbell_data import campbell_diagram_data_oneOP, campbellData2CSV, campbellData2TXT -from pyFAST.linearization.campbell_data import IdentifyModes - - -def postproCampbell(fstFiles, BladeLen=None, TowerLen=None, verbose=True, - WS_legacy=None, - nFreqOut=500, freqRange=None, posDampRange=None, # Options for TXT output - removeTwrAzimuth=False, starSub=None, removeStatesPattern=None, # Options for A matrix selection - writeModes=None, **kwargs # Options for .viz files - ): - """ - Postprocess linearization files to extract Campbell diagram (linearization at different Operating points) - - Run MBC - - Postprocess to put into "CampbellData" matlab form - - Perform mode identification (work in progress) - - Export to disk - - INPUTS: - - fstFiles: list of fst files - - INPUTS (related to A matrices): - - removeTwrAzimuth: if False do nothing - otherwise discard lin files where azimuth in [60, 180, 300]+/-4deg (close to tower). - - starSub: if None, raise an error if `****` are present - otherwise replace *** with `starSub` (e.g. 0) - see FASTLinearizationFile. - - removeStatesPattern: remove states matching a giving description pattern. - e.g: 'tower|Drivetrain' or '^AD' - see FASTLinearizationFile. - - INPUTS (related to Campbell_Summary.txt output): - - nFreqOut: maximum number of frequencies to write to Campbell_Summary.txt file - - freqRange: range in which frequencies are "accepted", if None: [-np.inf, np.inf] - - posDampRange: range in which damping are "accepted' , if None: [1e-5, 0.96] - - INPUTS (related to .viz files): - - writeModes: if True, a binary file and a .viz file is written to disk for OpenFAST VTK visualization. - if None, the binary file is written only if a checkpoint file is present. - For instance, if the main file is : 'main.fst', - the binary file will be : 'main.ModeShapeVTK.pyPostMBC' - the viz file will be : 'main.ModeShapeVTK.viz' - the checkpoint file is expected to be : 'main.ModeShapeVTK.chkp' - - **kwargs: list of key/values to be passed to writeVizFile (see function below) - VTKLinModes=15, VTKLinScale=10, VTKLinTim=1, VTKLinTimes1=True, VTKLinPhase=0, VTKModes=None - - OUTPUTS: - - OP: dataframe of operating points - - Freq: dataframe of frequencies for each OP and identified mode - - Damp: dataframe of dampings for each OP and identified mode - - UnMapped: - - ModeData: all mode data all mode data - """ - if len(fstFiles)==0: - raise Exception('postproCampbell requires a list of at least one .fst') - - if len(fstFiles)==1 and os.path.splitext(fstFiles[0])[1]=='.pkl': - # Temporary hack for debugging, using a pickle file - import pickle - CD = pickle.load(open(fstFiles[0],'rb')) - else: - # --- Attemps to extract Blade Length and TowerLen from first file... - CD, MBC = getCampbellDataOPs(fstFiles, writeModes=writeModes, BladeLen=BladeLen, TowerLen=TowerLen, - removeTwrAzimuth=removeTwrAzimuth, starSub=starSub, removeStatesPattern=removeStatesPattern, verbose=verbose, **kwargs) - - # --- Identify modes - modeID_table,modesDesc=IdentifyModes(CD) - - # --- Write files to disk - # Write csv file for manual identification step.. - baseName = os.path.join(os.path.dirname(fstFiles[0]), 'Campbell') - modeID_file = campbellData2CSV(baseName, CD, modeID_table, modesDesc) - # Write summary txt file to help manual identification step.. - txtFileName = baseName+'_Summary.txt' - campbellData2TXT(CD, txtFileName=txtFileName, nFreqOut=nFreqOut, freqRange=freqRange, posDampRange=posDampRange) - - # --- Return nice dataframes (assuming the identification is correct) - # TODO, for now we reread the files... - OP, Freq, Damp, UnMapped, ModeData = postproMBC(csvModesIDFile=modeID_file, verbose=verbose, WS_legacy=WS_legacy) - - return OP, Freq, Damp, UnMapped, ModeData, modeID_file - -# --------------------------------------------------------------------------------} -# --- Postprocessing -# --------------------------------------------------------------------------------{ -def postproMBC(xlsFile=None, csvModesIDFile=None, xlssheet=None, verbose=True, WS_legacy=None, suffix=''): - """ - Generate Cambell diagram data from an xls file, or a set of csv files - INPUTS: - - xlsFile: path to an excel file, or, basename for a set of csv files generated by campbellFolderPostPro - - csvModesIDFile: filename of csv file containing mode identification table. - Its parent directory is noted csvBase. - Other csv files will be asssumed to located in the same folder: - - csvBase + 'Campbell_OP.csv' - - csvBase + 'Campbell_PointI.csv' I=1..n_Op - - (csvBase + 'Campbell_ModesID.csv') - - vebose: more outputs to screen - - WS_legacy: for OpenFAST 2.3, wind speed is unknown (NaN), a vector of operating point WS is needed - OUTPUTS: - - Freq: dataframe with columns [WS, RPM, Freq_Mode1,.., Freq_ModeN] for the N identified modes - - Damp: dataframe with columns [WS, RPM, Damp_Mode1,.., Damp_ModeN] (damping ratios), for the N identified modes - - UnMapped: dataframe with columns [WS, RPM, Freq, Damp] for all unidenfied modes - - ModesData: low-level data, dictionaries for each OP with frequencies and damping - """ - rpmSweep=False - if xlsFile is not None: - from pandas import ExcelFile - # --- Excel file reading - OPFileName=xlsFile; - IDFileName=xlsFile; - sheets=dict() - # Reading all sheets - if verbose: - print('Reading Excel file: ',xlsFile) - xls = pd.ExcelFile(xlsFile) - dfs = {} - for sheet_name in xls.sheet_names: - # Reading sheet - df = xls.parse(sheet_name, header=None) - if df.shape[0]>0: - sheets[sheet_name]=df - OP = sheets['OP'] - WS = OP.iloc[1,1:].values - RPM = OP.iloc[2,1:].values - ID = None - if any([s.find('mps')>0 for s in sheets.keys()]): - rpmSweep = False - sweepVar = WS - sweepUnit = 'mps' - xlssheet='WS_ModesID' if xlssheet is None else xlssheet - else: - rpmSweep = True - sweepVar = RPM - sweepUnit = 'rpm' - xlssheet ='ModesID' if xlssheet is None else xlssheet - if xlssheet not in sheets.keys(): - raise Exception('Mode identification sheet {} not found in excel file'.format(xlssheet)) - ID = sheets[xlssheet] - # Storing data for each points, we try a bunch of keys since matlab script uses a loose num2str for now - Points=dict() - for i,v in enumerate(sweepVar): - keys=[('{:.'+str(ires)+'f} {:s}').format(v,sweepUnit) for ires in [0,1,2,3,4]] - for k in keys: - try: - Points[i] = sheets[k] - except: - pass - if i not in Points.keys(): - raise Exception('Couldnf find sheet for operating point {:d}'.format(i)) - elif csvModesIDFile is not None: - # --- csv file reading - IDFileName=csvModesIDFile - csvBase=os.path.join(os.path.dirname(csvModesIDFile),'') - OPFileName=csvBase+'Campbell_OP{:}.csv'.format(suffix) - if verbose: - print('Reading csv file: ',OPFileName) - OP = pd.read_csv(OPFileName, sep = ',') - if verbose: - print('Reading csv file: ',IDFileName) - ID = pd.read_csv(IDFileName, sep = ',',header=None) - nCol = OP.shape[1]-1 - naCount = OP.isna().sum(axis=1) - WS = OP.iloc[0,1:].values - RPM = OP.iloc[1,1:].values - del OP - # Storing data for each points into a dict - Points=dict() - for i,v in enumerate(WS): - OPFile = csvBase+'Campbell_Point{:02d}{:}.csv'.format(i+1,suffix) - #print(OPFile, WS[i], RPM[i]) - Points[i] = pd.read_csv(OPFile, sep = ',', header=None, dtype='object') - else: - raise Exception('Provide either an Excel file or a csv (ModesID) file') - # --- Mode Identification - ID.iloc[:,0].fillna('Unknown', inplace=True) # replace nan - ModeNames = ID.iloc[2: ,0].values - ModeIDs = ID.iloc[2: ,1:].values - nModesIDd = len(ModeNames) # Number of modes identified in the ID file - - if ModeIDs.shape[1]!=len(WS): - print('OP Windspeed:',WS) - raise Exception('Inconsistent number of operating points between OP ({} points) and ID ({} points) data.\nOP filename: {}\nID filename: {}\n'.format(ModeIDs.shape[1], len(WS), OPFileName, IDFileName)) - - # --- Extract Frequencies and Damping from Point table - ModeData=[] - ioff=0 - coff=0 - for i,ws in enumerate(WS): - P = Points[i] - opData = dict() - opData['Fnat'] = P.iloc[1+ioff, 1::5+coff].values[:].astype(float) # natural frequencies - opData['Fdmp'] = P.iloc[2+ioff, 1::5+coff].values[:].astype(float) # damped frequencies - opData['Damps'] = P.iloc[3+ioff, 1::5+coff].values[:].astype(float) # Damping values - ModeData.append(opData) - - # ---Dealing with missing WS - if np.any(np.isnan(np.array(WS).astype(float))) or np.all(np.array(WS).astype(float)==0): - print('[WARN] WS were not provided in linearization (all NaN), likely old OpenFAST version ') - if WS_legacy is not None: - print(' > Using WS_legacy provided.') - if len(WS_legacy)!=len(WS): - raise Exception('WS_legacy should have length {} instead of {}'.format(len(WS),len(WS_legacy))) - WS = WS_legacy - else: - print('[WARN] WS was replaced with index!') - WS = np.arange(0,len(WS)) - - # --- Creating a cleaner table of operating points - OP = pd.DataFrame(np.nan, index=np.arange(len(WS)), columns=['WS_[m/s]', 'RotSpeed_[rpm]']) - OP['WS_[m/s]'] = WS - OP['RotSpeed_[rpm]'] = RPM - - UnMapped_WS = [] - UnMapped_RPM = [] - UnMapped_Freq = [] - UnMapped_Damp = [] - # --- Unidentified modes, before "nModes" - for iOP,(ws,rpm) in enumerate(zip(WS,RPM)): - m = ModeData[iOP] - nModesMax = len(m['Fnat']) - #nModes = min(nModesMax, nModesIDd) # somehow sometimes we have 15 modes IDd but only 14 in the ModeData.. - Indices = (np.asarray(ModeIDs[:,iOP])-1).astype(int) - IndicesMissing = [i for i in np.arange(nModesMax) if i not in Indices] - f = np.asarray([m['Fnat'] [iiMode] for iiMode in IndicesMissing]) - d = np.asarray([m['Damps'][iiMode] for iiMode in IndicesMissing]) - ws = np.asarray([ws]*len(f)) - rpm = np.asarray([rpm]*len(f)) - UnMapped_Freq = np.concatenate((UnMapped_Freq, f)) - UnMapped_Damp = np.concatenate((UnMapped_Damp, d)) - UnMapped_WS = np.concatenate((UnMapped_WS, ws)) - UnMapped_RPM = np.concatenate((UnMapped_RPM, rpm)) - ## --- Unidentified modes, beyond "nModes" - #for m,ws,rpm in zip(ModeData,WS,RPM): - # f = m['Fnat'][nModesIDd:] - # d = m['Damps'][nModesIDd:] - # ws = np.asarray([ws]*len(f)) - # rpm = np.asarray([rpm]*len(f)) - # UnMapped_Freq = np.concatenate((UnMapped_Freq, f)) - # UnMapped_Damp = np.concatenate((UnMapped_Damp, d)) - # UnMapped_WS = np.concatenate((UnMapped_WS, ws)) - # UnMapped_RPM = np.concatenate((UnMapped_RPM, rpm)) - - # --- Put identified modes into a more convenient form - cols=[ m.split('-')[0].strip().replace(' ','_') for m in ModeNames] - cols=[v + str(cols[:i].count(v) + 1) if cols.count(v) > 1 else v for i, v in enumerate(cols)] - Freq = pd.DataFrame(np.nan, index=np.arange(len(WS)), columns=cols) - Damp = pd.DataFrame(np.nan, index=np.arange(len(WS)), columns=cols) - for iMode in np.arange(nModesIDd): - ModeIndices = (np.asarray(ModeIDs[iMode,:])-1).astype(int) - ModeName= ModeNames[iMode].replace('_',' ') - if ModeName.find('(not shown)')>0: - f = np.asarray([m['Fnat'][iiMode] for m,iiMode in zip(ModeData,ModeIndices) if iiMode>=0 ]) - d = np.asarray([m['Damps'][iiMode] for m,iiMode in zip(ModeData,ModeIndices) if iiMode>=0 ]) - ws = np.asarray([ws for ws,iiMode in zip(WS,ModeIndices) if iiMode>=0 ]) - rpm = np.asarray([rpm for rpm,iiMode in zip(RPM,ModeIndices) if iiMode>=0 ]) - UnMapped_Freq = np.concatenate((UnMapped_Freq, f)) - UnMapped_Damp = np.concatenate((UnMapped_Damp, d)) - UnMapped_WS = np.concatenate((UnMapped_WS, ws)) - UnMapped_RPM = np.concatenate((UnMapped_RPM, rpm)) - else: - if all(ModeIndices==-1): - print('Mode not IDd: {}, name: {}.'.format(iMode, ModeName)) - else: - f=[] - d=[] - #f2=np.asarray([m['Fnat'] [iiMode] if iiMode>=0 else np.nan for m,iiMode in zip(ModeData,ModeIndices)]) - #d2=np.asarray([m['Damps'][iiMode] if iiMode>=0 else np.nan for m,iiMode in zip(ModeData,ModeIndices)]) - for iOP, (m,iiMode) in enumerate(zip(ModeData, ModeIndices)): - if iiMode<0: - f.append(np.nan) - d.append(np.nan) - elif iiMode>=len(m['Fnat']): - print('[WARN] ID {} for mode `{}` at OP {} is beyond maximum number allowed'.format(iiMode+1, ModeName, iOP+1)) - f.append(np.nan) - d.append(np.nan) - else: - f.append(m['Fnat'] [iiMode]) - d.append(m['Damps'][iiMode]) - Freq.iloc[:, iMode]=f - Damp.iloc[:, iMode]=d - # Removing modes that are full nan (not_shown ones) - # NOTE: damgerous since OP is not part of it anymore - # Freq.dropna(how='all',axis=0,inplace=True) - # Freq.dropna(how='all',axis=1,inplace=True) - # Damp.dropna(how='all',axis=0,inplace=True) - # Damp.dropna(how='all',axis=1,inplace=True) - - # --- UnMapped modes into a dataframe - M = np.column_stack((UnMapped_WS, UnMapped_RPM, UnMapped_Freq, UnMapped_Damp)) - UnMapped = pd.DataFrame(data=M, columns=['WS_[m/s]','RotSpeed_[rpm]','Freq_[Hz]','Damping_[-]']) - - return OP, Freq, Damp, UnMapped, ModeData - - -# --------------------------------------------------------------------------------} -# --- Plotting -# --------------------------------------------------------------------------------{ -def campbellModeStyles(i, lbl): - """ """ - import matplotlib.pyplot as plt - FullLineStyles = [':', '-', '-+', '-o', '-^', '-s', '--x', '--d', '-.', '-v', '-+', ':o', ':^', ':s', ':x', ':d', ':.', '--','--+','--o','--^','--s','--x','--d','--.']; - Markers = ['', '+', 'o', '^', 's', 'd', 'x', '.'] - LineStyles = ['-', ':', '-.', '--']; - Colors = plt.rcParams['axes.prop_cycle'].by_key()['color'] - MW_Light_Blue = np.array([114,147,203])/255. - MW_Light_Orange = np.array([225,151,76])/255. - MW_Light_Green = np.array([132,186,91])/255. - MW_LightLight_Green = np.array([163,230,112])/255. - MW_Light_Red = np.array([226,115,115])/255. - MW_Light_Gray = np.array([128,133,133])/255. - MW_Light_Purple = np.array([144,103,167])/255. - MW_Light_DarkRed = np.array([171,104,87])/255. - MW_Light_Kaki = np.array([204,194,16])/255. - MW_Blue = np.array([57,106,177])/255. - MW_Orange = np.array([218,124,48])/255. - MW_Green = np.array([62,150,81])/255. - MW_Red = np.array([204,37,41])/255. - MW_Gray = np.array([83,81,84])/255. - MW_Purple = np.array([107,76,154])/255. - MW_DarkRed = np.array([146,36,40])/255. - MW_Kaki = np.array([148,139,61])/255. - - lbl=lbl.lower().replace('_',' ') - ms = 4 - c = Colors[np.mod(i,len(Colors))] - ls = LineStyles[np.mod(int(i/len(Markers)),len(LineStyles))] - mk = Markers[np.mod(i,len(Markers))] - # Color - if any([s in lbl for s in ['1st tower']]): - c=MW_Blue - elif any([s in lbl for s in ['2nd tower']]): - c=MW_Light_Blue - elif any([s in lbl for s in ['1st blade edge','drivetrain']]): - c=MW_Red - elif any([s in lbl for s in ['1st blade flap']]): - c=MW_Green - elif any([s in lbl for s in ['2nd blade flap']]): - c=MW_Light_Green - elif any([s in lbl for s in ['3rd blade flap']]): - c=MW_LightLight_Green - elif any([s in lbl for s in ['2nd blade edge']]): - c=MW_Light_Red - elif any([s in lbl for s in ['1st blade torsion']]): - c=MW_Purple - # Line style - if any([s in lbl for s in ['tower fa','collective','drivetrain','coll']]): - ls='-' - elif any([s in lbl for s in ['tower ss','regressive','bw']]): - ls='--' - elif any([s in lbl for s in ['tower ss','progressive','fw']]): - ls='-.' - # Marker - if any([s in lbl for s in ['collective','coll']]): - mk='2'; ms=8 - elif any([s in lbl for s in ['blade','tower','drivetrain']]): - mk=''; - return c, ls, ms, mk - -def plotCampbell(OP, Freq, Damp, sx='WS_[m/s]', UnMapped=None, fig=None, axes=None, ylim=None, legend=True, plotUnMapped=True, ps=[1,3,6,9]): - """ Plot Campbell data as returned by postproMBC - - INPUTS: - - OP : dataframe of operating points (as returned by postMBC) - - Freq : dataframe of Frequencies at each OP (as returned by postMBC) - - OP : dataframe of damping ratios at each OP points (as returned by postMBC) - - sx: label of the dataframes used for the "x" axis of the Campbell plot - OPTIONAL INPUTS: - - UnMapped: dataframe of UnMapped modes - - fig, axes: optional fig and axes used for plotting (freq and damp) - - ylim: limits for the frequency axis - - ps: multiple of "p" (rotational speed) to plot in the background - """ - import matplotlib.pyplot as plt - - - # Init figure - if fig is None: - fig,axes_ = plt.subplots(1,2) - # fig.set_size_inches(7,7.0,forward=True) # default is (6.4,4.8) - fig.set_size_inches(13,7.0,forward=True) # default is (6.4,4.8) - fig.subplots_adjust(top=0.78,bottom=0.11,left=0.04,right=0.98,hspace=0.06,wspace=0.16) - if axes is None: - axes=axes_ - - # Estimating figure range - FreqRange = [0 , np.nanmax(Freq.values)*1.01] - DampRange = [np.nanmin(Damp.iloc[:,2:]), np.nanmax(Damp.values)*1.01] - - if ylim is not None: - FreqRange=ylim - if DampRange[0]>0: - DampRange[0]=0 - - # Plot "background" 1p 3p 6p 9p. Potentiallymake this an option - RPM = OP['RotSpeed_[rpm]'].values - omega = RPM/60*2*np.pi - freq_1p = omega/(2*np.pi) - for p in ps: - axes[0].plot(OP[sx].values, p*freq_1p, ':',color=(0.7,0.7,0.7), lw=1.0) - - # Plot mapped modes - Markers = ['+', 'o', '^', 's', 'd', 'x', '.'] - iModeValid=0 - xPlot=[]; yPlot=[] - for iMode,lbl in enumerate(Freq.columns.values): - if lbl.find('not_shown')>0: - # TODO ADD TO UNMAPPED - continue - iModeValid+=1 - c, ls, ms, mk = campbellModeStyles(iModeValid, lbl) - if len(RPM)==1 and len(mk)==0: - mk = Markers[np.mod(iMode,len(Markers))] - ms=5 - axes[0].plot(OP[sx].values, Freq[lbl].values, ls, marker=mk, label=lbl.replace('_',' '), markersize=ms, color=c) - axes[1].plot(OP[sx].values, Damp[lbl].values, ls, marker=mk , markersize=ms, color=c) - xPlot=np.concatenate((xPlot, OP[sx].values)) - yPlot=np.concatenate((yPlot, Freq[lbl].values)) - - # Unmapped modes (NOTE: plotted after to over-plot) - if plotUnMapped and UnMapped is not None: - axes[0].plot(UnMapped[sx].values, UnMapped['Freq_[Hz]' ].values, '.', markersize=2, color=[0.5,0.5,0.5]) - axes[1].plot(UnMapped[sx].values, UnMapped['Damping_[-]'].values, '.', markersize=1, color=[0.5,0.5,0.5]) - # Highligh duplicates (also after) - Points=[(x,y) for x,y in zip(xPlot,yPlot)] - Plot = pd.Series(Points) - for xDupl,yDupl in Plot[Plot.duplicated()]: - axes[0].plot(xDupl,yDupl, 'o',color='r') - - axes[0].set_xlabel(sx.replace('_',' ')) - axes[1].set_xlabel(sx.replace('_',' ')) - axes[0].set_ylabel('Frequencies [Hz]') - axes[1].set_ylabel('Damping ratios [-]') - if legend: - axes[0].legend(bbox_to_anchor=(0., 1.02, 2.16, .802), loc='lower left', ncol=4, mode="expand", borderaxespad=0.) - if not np.any(np.isnan(FreqRange)): - axes[0].set_ylim(FreqRange) - - XLIM=axes[1].get_xlim() - axes[1].plot(XLIM, [0,0],'-', color='k', lw=0.5) - axes[1].set_xlim(XLIM) - if not np.any(np.isnan(DampRange)): - axes[1].set_ylim(DampRange) - return fig, axes - - -def plotCampbellDataFile(xls_or_csv, ws_or_rpm='rpm', sheetname=None, ylim=None, WS_legacy=None, to_csv=False, suffix='', returnData=False, - fig=None, axes=None, legend=True, plotUnMapped=True, ps=[1,3,6,9]): - """ - Wrapper for plotCampbell, takes an Excel or csv file as argument. Returns a figure. - - INPUTS: - - - - WS_legacy: for OpenFAST 2.3, wind speed is unknown (NaN), a vector of operating point WS is needed - """ - if ws_or_rpm.lower()=='ws': - sx='WS_[m/s]' - else: - sx='RotSpeed_[rpm]' - - ext = os.path.splitext(xls_or_csv)[1].lower() - baseDir = os.path.dirname(xls_or_csv) - basename = os.path.splitext(os.path.basename(xls_or_csv))[0] - - # --- Read xlsx or csv filse - if ext=='.xlsx': - OP, Freq, Damp, UnMapped, ModeData = postproMBC(xlsFile=xls_or_csv,xlssheet=sheetname, WS_legacy=WS_legacy, suffix=suffix) - - elif ext=='.csv': - OP, Freq, Damp, UnMapped, ModeData = postproMBC(csvModesIDFile=xls_or_csv, WS_legacy=WS_legacy, suffix=suffix) - - pass - - else: - raise Exception('Extension should be csv or xlsx, got {} instead.'.format(ext),) - - # --- Plot - fig, axes = plotCampbell(OP, Freq, Damp, sx=sx, UnMapped=UnMapped, ylim=ylim, fig=fig, axes=axes, legend=legend, plotUnMapped=plotUnMapped, ps=ps) - figName = os.path.join(baseDir,basename+'_'+ws_or_rpm) - - if to_csv: - Freq.to_csv(os.path.join(baseDir, 'freq.csv'), index=None, sep=' ', na_rep='NaN') - Damp.to_csv(os.path.join(baseDir, 'damp.csv'), index=None, sep=' ', na_rep='NaN') - OP.to_csv(os.path.join(baseDir, 'op.csv'), index=None, sep=' ', na_rep='NaN') - - if returnData: - return fig, axes, figName, OP, Freq, Damp - else: - return fig, axes, figName - +""" + +Generic functions to help setup a Campbell diagram with OpenFAST + +These are high level functions for manipulate multiple operating points (multiple .fst files). + +For more granularity, see: + - openfast_toolbox.linearization.tools.py + - openfast_toolbox.linearization.campbell_data.py + +Main functions: + +- postproMBC(xlsFile=None, csvBase=None, sortedSuffix=None, csvModesID=None, xlssheet=None): + Generate Cambell diagram data from an xls file, or a set of csv files + + +""" + +import os +import pandas as pd +# import re +import numpy as np +from openfast_toolbox.linearization.tools import getMBCOPs +from openfast_toolbox.linearization.tools import getCampbellDataOPs +from openfast_toolbox.linearization.tools import estimateLengths + +from openfast_toolbox.linearization.campbell_data import campbell_diagram_data_oneOP, campbellData2CSV, campbellData2TXT +from openfast_toolbox.linearization.campbell_data import IdentifyModes + + +def postproCampbell(fstFiles, BladeLen=None, TowerLen=None, verbose=True, + WS_legacy=None, + nFreqOut=500, freqRange=None, posDampRange=None, # Options for TXT output + removeTwrAzimuth=False, starSub=None, removeStatesPattern=None, # Options for A matrix selection + writeModes=None, **kwargs # Options for .viz files + ): + """ + Postprocess linearization files to extract Campbell diagram (linearization at different Operating points) + - Run MBC + - Postprocess to put into "CampbellData" matlab form + - Perform mode identification (work in progress) + - Export to disk + + INPUTS: + - fstFiles: list of fst files + + INPUTS (related to A matrices): + - removeTwrAzimuth: if False do nothing + otherwise discard lin files where azimuth in [60, 180, 300]+/-4deg (close to tower). + - starSub: if None, raise an error if `****` are present + otherwise replace *** with `starSub` (e.g. 0) + see FASTLinearizationFile. + - removeStatesPattern: remove states matching a giving description pattern. + e.g: 'tower|Drivetrain' or '^AD' + see FASTLinearizationFile. + + INPUTS (related to Campbell_Summary.txt output): + - nFreqOut: maximum number of frequencies to write to Campbell_Summary.txt file + - freqRange: range in which frequencies are "accepted", if None: [-np.inf, np.inf] + - posDampRange: range in which damping are "accepted' , if None: [1e-5, 0.96] + + INPUTS (related to .viz files): + - writeModes: if True, a binary file and a .viz file is written to disk for OpenFAST VTK visualization. + if None, the binary file is written only if a checkpoint file is present. + For instance, if the main file is : 'main.fst', + the binary file will be : 'main.ModeShapeVTK.pyPostMBC' + the viz file will be : 'main.ModeShapeVTK.viz' + the checkpoint file is expected to be : 'main.ModeShapeVTK.chkp' + - **kwargs: list of key/values to be passed to writeVizFile (see function below) + VTKLinModes=15, VTKLinScale=10, VTKLinTim=1, VTKLinTimes1=True, VTKLinPhase=0, VTKModes=None + + OUTPUTS: + - OP: dataframe of operating points + - Freq: dataframe of frequencies for each OP and identified mode + - Damp: dataframe of dampings for each OP and identified mode + - UnMapped: + - ModeData: all mode data all mode data + """ + if len(fstFiles)==0: + raise Exception('postproCampbell requires a list of at least one .fst') + + if len(fstFiles)==1 and os.path.splitext(fstFiles[0])[1]=='.pkl': + # Temporary hack for debugging, using a pickle file + import pickle + CD = pickle.load(open(fstFiles[0],'rb')) + else: + # --- Attemps to extract Blade Length and TowerLen from first file... + CD, MBC = getCampbellDataOPs(fstFiles, writeModes=writeModes, BladeLen=BladeLen, TowerLen=TowerLen, + removeTwrAzimuth=removeTwrAzimuth, starSub=starSub, removeStatesPattern=removeStatesPattern, verbose=verbose, **kwargs) + + # --- Identify modes + modeID_table,modesDesc=IdentifyModes(CD) + + # --- Write files to disk + # Write csv file for manual identification step.. + baseName = os.path.join(os.path.dirname(fstFiles[0]), 'Campbell') + modeID_file = campbellData2CSV(baseName, CD, modeID_table, modesDesc) + # Write summary txt file to help manual identification step.. + txtFileName = baseName+'_Summary.txt' + campbellData2TXT(CD, txtFileName=txtFileName, nFreqOut=nFreqOut, freqRange=freqRange, posDampRange=posDampRange) + + # --- Return nice dataframes (assuming the identification is correct) + # TODO, for now we reread the files... + OP, Freq, Damp, UnMapped, ModeData = postproMBC(csvModesIDFile=modeID_file, verbose=verbose, WS_legacy=WS_legacy) + + return OP, Freq, Damp, UnMapped, ModeData, modeID_file + +# --------------------------------------------------------------------------------} +# --- Postprocessing +# --------------------------------------------------------------------------------{ +def postproMBC(xlsFile=None, csvModesIDFile=None, xlssheet=None, verbose=True, WS_legacy=None, suffix=''): + """ + Generate Cambell diagram data from an xls file, or a set of csv files + INPUTS: + - xlsFile: path to an excel file, or, basename for a set of csv files generated by campbellFolderPostPro + - csvModesIDFile: filename of csv file containing mode identification table. + Its parent directory is noted csvBase. + Other csv files will be asssumed to located in the same folder: + - csvBase + 'Campbell_OP.csv' + - csvBase + 'Campbell_PointI.csv' I=1..n_Op + - (csvBase + 'Campbell_ModesID.csv') + - vebose: more outputs to screen + - WS_legacy: for OpenFAST 2.3, wind speed is unknown (NaN), a vector of operating point WS is needed + OUTPUTS: + - Freq: dataframe with columns [WS, RPM, Freq_Mode1,.., Freq_ModeN] for the N identified modes + - Damp: dataframe with columns [WS, RPM, Damp_Mode1,.., Damp_ModeN] (damping ratios), for the N identified modes + - UnMapped: dataframe with columns [WS, RPM, Freq, Damp] for all unidenfied modes + - ModesData: low-level data, dictionaries for each OP with frequencies and damping + """ + rpmSweep=False + if xlsFile is not None: + from pandas import ExcelFile + # --- Excel file reading + OPFileName=xlsFile; + IDFileName=xlsFile; + sheets=dict() + # Reading all sheets + if verbose: + print('Reading Excel file: ',xlsFile) + xls = pd.ExcelFile(xlsFile) + dfs = {} + for sheet_name in xls.sheet_names: + # Reading sheet + df = xls.parse(sheet_name, header=None) + if df.shape[0]>0: + sheets[sheet_name]=df + OP = sheets['OP'] + WS = OP.iloc[1,1:].values + RPM = OP.iloc[2,1:].values + ID = None + if any([s.find('mps')>0 for s in sheets.keys()]): + rpmSweep = False + sweepVar = WS + sweepUnit = 'mps' + xlssheet='WS_ModesID' if xlssheet is None else xlssheet + else: + rpmSweep = True + sweepVar = RPM + sweepUnit = 'rpm' + xlssheet ='ModesID' if xlssheet is None else xlssheet + if xlssheet not in sheets.keys(): + raise Exception('Mode identification sheet {} not found in excel file'.format(xlssheet)) + ID = sheets[xlssheet] + # Storing data for each points, we try a bunch of keys since matlab script uses a loose num2str for now + Points=dict() + for i,v in enumerate(sweepVar): + keys=[('{:.'+str(ires)+'f} {:s}').format(v,sweepUnit) for ires in [0,1,2,3,4]] + for k in keys: + try: + Points[i] = sheets[k] + except: + pass + if i not in Points.keys(): + raise Exception('Couldnf find sheet for operating point {:d}'.format(i)) + elif csvModesIDFile is not None: + # --- csv file reading + IDFileName=csvModesIDFile + csvBase=os.path.join(os.path.dirname(csvModesIDFile),'') + OPFileName=csvBase+'Campbell_OP{:}.csv'.format(suffix) + if verbose: + print('Reading csv file: ',OPFileName) + OP = pd.read_csv(OPFileName, sep = ',') + if verbose: + print('Reading csv file: ',IDFileName) + ID = pd.read_csv(IDFileName, sep = ',',header=None) + nCol = OP.shape[1]-1 + naCount = OP.isna().sum(axis=1) + WS = OP.iloc[0,1:].values + RPM = OP.iloc[1,1:].values + del OP + # Storing data for each points into a dict + Points=dict() + for i,v in enumerate(WS): + OPFile = csvBase+'Campbell_Point{:02d}{:}.csv'.format(i+1,suffix) + #print(OPFile, WS[i], RPM[i]) + Points[i] = pd.read_csv(OPFile, sep = ',', header=None, dtype='object') + else: + raise Exception('Provide either an Excel file or a csv (ModesID) file') + # --- Mode Identification + ID.iloc[:,0].fillna('Unknown', inplace=True) # replace nan + ModeNames = ID.iloc[2: ,0].values + ModeIDs = ID.iloc[2: ,1:].values + nModesIDd = len(ModeNames) # Number of modes identified in the ID file + + if ModeIDs.shape[1]!=len(WS): + print('OP Windspeed:',WS) + raise Exception('Inconsistent number of operating points between OP ({} points) and ID ({} points) data.\nOP filename: {}\nID filename: {}\n'.format(ModeIDs.shape[1], len(WS), OPFileName, IDFileName)) + + # --- Extract Frequencies and Damping from Point table + ModeData=[] + ioff=0 + coff=0 + for i,ws in enumerate(WS): + P = Points[i] + opData = dict() + opData['Fnat'] = P.iloc[1+ioff, 1::5+coff].values[:].astype(float) # natural frequencies + opData['Fdmp'] = P.iloc[2+ioff, 1::5+coff].values[:].astype(float) # damped frequencies + opData['Damps'] = P.iloc[3+ioff, 1::5+coff].values[:].astype(float) # Damping values + ModeData.append(opData) + + # ---Dealing with missing WS + if np.any(np.isnan(np.array(WS).astype(float))) or np.all(np.array(WS).astype(float)==0): + print('[WARN] WS were not provided in linearization (all NaN), likely old OpenFAST version ') + if WS_legacy is not None: + print(' > Using WS_legacy provided.') + if len(WS_legacy)!=len(WS): + raise Exception('WS_legacy should have length {} instead of {}'.format(len(WS),len(WS_legacy))) + WS = WS_legacy + else: + print('[WARN] WS was replaced with index!') + WS = np.arange(0,len(WS)) + + # --- Creating a cleaner table of operating points + OP = pd.DataFrame(np.nan, index=np.arange(len(WS)), columns=['WS_[m/s]', 'RotSpeed_[rpm]']) + OP['WS_[m/s]'] = WS + OP['RotSpeed_[rpm]'] = RPM + + UnMapped_WS = [] + UnMapped_RPM = [] + UnMapped_Freq = [] + UnMapped_Damp = [] + # --- Unidentified modes, before "nModes" + for iOP,(ws,rpm) in enumerate(zip(WS,RPM)): + m = ModeData[iOP] + nModesMax = len(m['Fnat']) + #nModes = min(nModesMax, nModesIDd) # somehow sometimes we have 15 modes IDd but only 14 in the ModeData.. + Indices = (np.asarray(ModeIDs[:,iOP])-1).astype(int) + IndicesMissing = [i for i in np.arange(nModesMax) if i not in Indices] + f = np.asarray([m['Fnat'] [iiMode] for iiMode in IndicesMissing]) + d = np.asarray([m['Damps'][iiMode] for iiMode in IndicesMissing]) + ws = np.asarray([ws]*len(f)) + rpm = np.asarray([rpm]*len(f)) + UnMapped_Freq = np.concatenate((UnMapped_Freq, f)) + UnMapped_Damp = np.concatenate((UnMapped_Damp, d)) + UnMapped_WS = np.concatenate((UnMapped_WS, ws)) + UnMapped_RPM = np.concatenate((UnMapped_RPM, rpm)) + ## --- Unidentified modes, beyond "nModes" + #for m,ws,rpm in zip(ModeData,WS,RPM): + # f = m['Fnat'][nModesIDd:] + # d = m['Damps'][nModesIDd:] + # ws = np.asarray([ws]*len(f)) + # rpm = np.asarray([rpm]*len(f)) + # UnMapped_Freq = np.concatenate((UnMapped_Freq, f)) + # UnMapped_Damp = np.concatenate((UnMapped_Damp, d)) + # UnMapped_WS = np.concatenate((UnMapped_WS, ws)) + # UnMapped_RPM = np.concatenate((UnMapped_RPM, rpm)) + + # --- Put identified modes into a more convenient form + cols=[ m.split('-')[0].strip().replace(' ','_') for m in ModeNames] + cols=[v + str(cols[:i].count(v) + 1) if cols.count(v) > 1 else v for i, v in enumerate(cols)] + Freq = pd.DataFrame(np.nan, index=np.arange(len(WS)), columns=cols) + Damp = pd.DataFrame(np.nan, index=np.arange(len(WS)), columns=cols) + for iMode in np.arange(nModesIDd): + ModeIndices = (np.asarray(ModeIDs[iMode,:])-1).astype(int) + ModeName= ModeNames[iMode].replace('_',' ') + if ModeName.find('(not shown)')>0: + f = np.asarray([m['Fnat'][iiMode] for m,iiMode in zip(ModeData,ModeIndices) if iiMode>=0 ]) + d = np.asarray([m['Damps'][iiMode] for m,iiMode in zip(ModeData,ModeIndices) if iiMode>=0 ]) + ws = np.asarray([ws for ws,iiMode in zip(WS,ModeIndices) if iiMode>=0 ]) + rpm = np.asarray([rpm for rpm,iiMode in zip(RPM,ModeIndices) if iiMode>=0 ]) + UnMapped_Freq = np.concatenate((UnMapped_Freq, f)) + UnMapped_Damp = np.concatenate((UnMapped_Damp, d)) + UnMapped_WS = np.concatenate((UnMapped_WS, ws)) + UnMapped_RPM = np.concatenate((UnMapped_RPM, rpm)) + else: + if all(ModeIndices==-1): + print('Mode not IDd: {}, name: {}.'.format(iMode, ModeName)) + else: + f=[] + d=[] + #f2=np.asarray([m['Fnat'] [iiMode] if iiMode>=0 else np.nan for m,iiMode in zip(ModeData,ModeIndices)]) + #d2=np.asarray([m['Damps'][iiMode] if iiMode>=0 else np.nan for m,iiMode in zip(ModeData,ModeIndices)]) + for iOP, (m,iiMode) in enumerate(zip(ModeData, ModeIndices)): + if iiMode<0: + f.append(np.nan) + d.append(np.nan) + elif iiMode>=len(m['Fnat']): + print('[WARN] ID {} for mode `{}` at OP {} is beyond maximum number allowed'.format(iiMode+1, ModeName, iOP+1)) + f.append(np.nan) + d.append(np.nan) + else: + f.append(m['Fnat'] [iiMode]) + d.append(m['Damps'][iiMode]) + Freq.iloc[:, iMode]=f + Damp.iloc[:, iMode]=d + # Removing modes that are full nan (not_shown ones) + # NOTE: damgerous since OP is not part of it anymore + # Freq.dropna(how='all',axis=0,inplace=True) + # Freq.dropna(how='all',axis=1,inplace=True) + # Damp.dropna(how='all',axis=0,inplace=True) + # Damp.dropna(how='all',axis=1,inplace=True) + + # --- UnMapped modes into a dataframe + M = np.column_stack((UnMapped_WS, UnMapped_RPM, UnMapped_Freq, UnMapped_Damp)) + UnMapped = pd.DataFrame(data=M, columns=['WS_[m/s]','RotSpeed_[rpm]','Freq_[Hz]','Damping_[-]']) + + return OP, Freq, Damp, UnMapped, ModeData + + +# --------------------------------------------------------------------------------} +# --- Plotting +# --------------------------------------------------------------------------------{ +def campbellModeStyles(i, lbl): + """ """ + import matplotlib.pyplot as plt + FullLineStyles = [':', '-', '-+', '-o', '-^', '-s', '--x', '--d', '-.', '-v', '-+', ':o', ':^', ':s', ':x', ':d', ':.', '--','--+','--o','--^','--s','--x','--d','--.']; + Markers = ['', '+', 'o', '^', 's', 'd', 'x', '.'] + LineStyles = ['-', ':', '-.', '--']; + Colors = plt.rcParams['axes.prop_cycle'].by_key()['color'] + MW_Light_Blue = np.array([114,147,203])/255. + MW_Light_Orange = np.array([225,151,76])/255. + MW_Light_Green = np.array([132,186,91])/255. + MW_LightLight_Green = np.array([163,230,112])/255. + MW_Light_Red = np.array([226,115,115])/255. + MW_Light_Gray = np.array([128,133,133])/255. + MW_Light_Purple = np.array([144,103,167])/255. + MW_Light_DarkRed = np.array([171,104,87])/255. + MW_Light_Kaki = np.array([204,194,16])/255. + MW_Blue = np.array([57,106,177])/255. + MW_Orange = np.array([218,124,48])/255. + MW_Green = np.array([62,150,81])/255. + MW_Red = np.array([204,37,41])/255. + MW_Gray = np.array([83,81,84])/255. + MW_Purple = np.array([107,76,154])/255. + MW_DarkRed = np.array([146,36,40])/255. + MW_Kaki = np.array([148,139,61])/255. + + lbl=lbl.lower().replace('_',' ') + ms = 4 + c = Colors[np.mod(i,len(Colors))] + ls = LineStyles[np.mod(int(i/len(Markers)),len(LineStyles))] + mk = Markers[np.mod(i,len(Markers))] + # Color + if any([s in lbl for s in ['1st tower']]): + c=MW_Blue + elif any([s in lbl for s in ['2nd tower']]): + c=MW_Light_Blue + elif any([s in lbl for s in ['1st blade edge','drivetrain']]): + c=MW_Red + elif any([s in lbl for s in ['1st blade flap']]): + c=MW_Green + elif any([s in lbl for s in ['2nd blade flap']]): + c=MW_Light_Green + elif any([s in lbl for s in ['3rd blade flap']]): + c=MW_LightLight_Green + elif any([s in lbl for s in ['2nd blade edge']]): + c=MW_Light_Red + elif any([s in lbl for s in ['1st blade torsion']]): + c=MW_Purple + # Line style + if any([s in lbl for s in ['tower fa','collective','drivetrain','coll']]): + ls='-' + elif any([s in lbl for s in ['tower ss','regressive','bw']]): + ls='--' + elif any([s in lbl for s in ['tower ss','progressive','fw']]): + ls='-.' + # Marker + if any([s in lbl for s in ['collective','coll']]): + mk='2'; ms=8 + elif any([s in lbl for s in ['blade','tower','drivetrain']]): + mk=''; + return c, ls, ms, mk + +def plotCampbell(OP, Freq, Damp, sx='WS_[m/s]', UnMapped=None, fig=None, axes=None, ylim=None, legend=True, plotUnMapped=True, ps=[1,3,6,9]): + """ Plot Campbell data as returned by postproMBC + + INPUTS: + - OP : dataframe of operating points (as returned by postMBC) + - Freq : dataframe of Frequencies at each OP (as returned by postMBC) + - OP : dataframe of damping ratios at each OP points (as returned by postMBC) + - sx: label of the dataframes used for the "x" axis of the Campbell plot + OPTIONAL INPUTS: + - UnMapped: dataframe of UnMapped modes + - fig, axes: optional fig and axes used for plotting (freq and damp) + - ylim: limits for the frequency axis + - ps: multiple of "p" (rotational speed) to plot in the background + """ + import matplotlib.pyplot as plt + + + # Init figure + if fig is None: + fig,axes_ = plt.subplots(1,2) + # fig.set_size_inches(7,7.0,forward=True) # default is (6.4,4.8) + fig.set_size_inches(13,7.0,forward=True) # default is (6.4,4.8) + fig.subplots_adjust(top=0.78,bottom=0.11,left=0.04,right=0.98,hspace=0.06,wspace=0.16) + if axes is None: + axes=axes_ + + # Estimating figure range + FreqRange = [0 , np.nanmax(Freq.values)*1.01] + DampRange = [np.nanmin(Damp.iloc[:,2:]), np.nanmax(Damp.values)*1.01] + + if ylim is not None: + FreqRange=ylim + if DampRange[0]>0: + DampRange[0]=0 + + # Plot "background" 1p 3p 6p 9p. Potentiallymake this an option + RPM = OP['RotSpeed_[rpm]'].values + omega = RPM/60*2*np.pi + freq_1p = omega/(2*np.pi) + for p in ps: + axes[0].plot(OP[sx].values, p*freq_1p, ':',color=(0.7,0.7,0.7), lw=1.0) + + # Plot mapped modes + Markers = ['+', 'o', '^', 's', 'd', 'x', '.'] + iModeValid=0 + xPlot=[]; yPlot=[] + for iMode,lbl in enumerate(Freq.columns.values): + if lbl.find('not_shown')>0: + # TODO ADD TO UNMAPPED + continue + iModeValid+=1 + c, ls, ms, mk = campbellModeStyles(iModeValid, lbl) + if len(RPM)==1 and len(mk)==0: + mk = Markers[np.mod(iMode,len(Markers))] + ms=5 + axes[0].plot(OP[sx].values, Freq[lbl].values, ls, marker=mk, label=lbl.replace('_',' '), markersize=ms, color=c) + axes[1].plot(OP[sx].values, Damp[lbl].values, ls, marker=mk , markersize=ms, color=c) + xPlot=np.concatenate((xPlot, OP[sx].values)) + yPlot=np.concatenate((yPlot, Freq[lbl].values)) + + # Unmapped modes (NOTE: plotted after to over-plot) + if plotUnMapped and UnMapped is not None: + axes[0].plot(UnMapped[sx].values, UnMapped['Freq_[Hz]' ].values, '.', markersize=2, color=[0.5,0.5,0.5]) + axes[1].plot(UnMapped[sx].values, UnMapped['Damping_[-]'].values, '.', markersize=1, color=[0.5,0.5,0.5]) + # Highligh duplicates (also after) + Points=[(x,y) for x,y in zip(xPlot,yPlot)] + Plot = pd.Series(Points) + for xDupl,yDupl in Plot[Plot.duplicated()]: + axes[0].plot(xDupl,yDupl, 'o',color='r') + + axes[0].set_xlabel(sx.replace('_',' ')) + axes[1].set_xlabel(sx.replace('_',' ')) + axes[0].set_ylabel('Frequencies [Hz]') + axes[1].set_ylabel('Damping ratios [-]') + if legend: + axes[0].legend(bbox_to_anchor=(0., 1.02, 2.16, .802), loc='lower left', ncol=4, mode="expand", borderaxespad=0.) + if not np.any(np.isnan(FreqRange)): + axes[0].set_ylim(FreqRange) + + XLIM=axes[1].get_xlim() + axes[1].plot(XLIM, [0,0],'-', color='k', lw=0.5) + axes[1].set_xlim(XLIM) + if not np.any(np.isnan(DampRange)): + axes[1].set_ylim(DampRange) + return fig, axes + + +def plotCampbellDataFile(xls_or_csv, ws_or_rpm='rpm', sheetname=None, ylim=None, WS_legacy=None, to_csv=False, suffix='', returnData=False, + fig=None, axes=None, legend=True, plotUnMapped=True, ps=[1,3,6,9]): + """ + Wrapper for plotCampbell, takes an Excel or csv file as argument. Returns a figure. + + INPUTS: + + + - WS_legacy: for OpenFAST 2.3, wind speed is unknown (NaN), a vector of operating point WS is needed + """ + if ws_or_rpm.lower()=='ws': + sx='WS_[m/s]' + else: + sx='RotSpeed_[rpm]' + + ext = os.path.splitext(xls_or_csv)[1].lower() + baseDir = os.path.dirname(xls_or_csv) + basename = os.path.splitext(os.path.basename(xls_or_csv))[0] + + # --- Read xlsx or csv filse + if ext=='.xlsx': + OP, Freq, Damp, UnMapped, ModeData = postproMBC(xlsFile=xls_or_csv,xlssheet=sheetname, WS_legacy=WS_legacy, suffix=suffix) + + elif ext=='.csv': + OP, Freq, Damp, UnMapped, ModeData = postproMBC(csvModesIDFile=xls_or_csv, WS_legacy=WS_legacy, suffix=suffix) + + pass + + else: + raise Exception('Extension should be csv or xlsx, got {} instead.'.format(ext),) + + # --- Plot + fig, axes = plotCampbell(OP, Freq, Damp, sx=sx, UnMapped=UnMapped, ylim=ylim, fig=fig, axes=axes, legend=legend, plotUnMapped=plotUnMapped, ps=ps) + figName = os.path.join(baseDir,basename+'_'+ws_or_rpm) + + if to_csv: + Freq.to_csv(os.path.join(baseDir, 'freq.csv'), index=None, sep=' ', na_rep='NaN') + Damp.to_csv(os.path.join(baseDir, 'damp.csv'), index=None, sep=' ', na_rep='NaN') + OP.to_csv(os.path.join(baseDir, 'op.csv'), index=None, sep=' ', na_rep='NaN') + + if returnData: + return fig, axes, figName, OP, Freq, Damp + else: + return fig, axes, figName + diff --git a/pyFAST/linearization/campbell_data.py b/openfast_toolbox/linearization/campbell_data.py similarity index 100% rename from pyFAST/linearization/campbell_data.py rename to openfast_toolbox/linearization/campbell_data.py diff --git a/pyFAST/linearization/examples/README.md b/openfast_toolbox/linearization/examples/README.md similarity index 100% rename from pyFAST/linearization/examples/README.md rename to openfast_toolbox/linearization/examples/README.md diff --git a/pyFAST/linearization/examples/ex1a_OneLinFile_SimpleEigenAnalysis.py b/openfast_toolbox/linearization/examples/ex1a_OneLinFile_SimpleEigenAnalysis.py similarity index 93% rename from pyFAST/linearization/examples/ex1a_OneLinFile_SimpleEigenAnalysis.py rename to openfast_toolbox/linearization/examples/ex1a_OneLinFile_SimpleEigenAnalysis.py index cf3213c..bc1aaa1 100644 --- a/pyFAST/linearization/examples/ex1a_OneLinFile_SimpleEigenAnalysis.py +++ b/openfast_toolbox/linearization/examples/ex1a_OneLinFile_SimpleEigenAnalysis.py @@ -7,8 +7,8 @@ """ import os import numpy as np -from pyFAST.tools.eva import eigA -from pyFAST.input_output.fast_linearization_file import FASTLinearizationFile +from openfast_toolbox.tools.eva import eigA +from openfast_toolbox.io.fast_linearization_file import FASTLinearizationFile scriptDir = os.path.dirname(__file__) # --- Open lin File diff --git a/pyFAST/linearization/examples/ex1b_OneLinFile_NoRotation.py b/openfast_toolbox/linearization/examples/ex1b_OneLinFile_NoRotation.py similarity index 96% rename from pyFAST/linearization/examples/ex1b_OneLinFile_NoRotation.py rename to openfast_toolbox/linearization/examples/ex1b_OneLinFile_NoRotation.py index c760ba1..911b708 100644 --- a/pyFAST/linearization/examples/ex1b_OneLinFile_NoRotation.py +++ b/openfast_toolbox/linearization/examples/ex1b_OneLinFile_NoRotation.py @@ -8,7 +8,7 @@ """ import os import numpy as np -import pyFAST.linearization as lin +import openfast_toolbox.linearization as lin # Get current directory so this script can be called from any location scriptDir = os.path.dirname(__file__) diff --git a/pyFAST/linearization/examples/ex2a_MultiLinFiles_OneOP.py b/openfast_toolbox/linearization/examples/ex2a_MultiLinFiles_OneOP.py similarity index 97% rename from pyFAST/linearization/examples/ex2a_MultiLinFiles_OneOP.py rename to openfast_toolbox/linearization/examples/ex2a_MultiLinFiles_OneOP.py index 773a95f..ef450ff 100644 --- a/pyFAST/linearization/examples/ex2a_MultiLinFiles_OneOP.py +++ b/openfast_toolbox/linearization/examples/ex2a_MultiLinFiles_OneOP.py @@ -9,7 +9,7 @@ import os import glob import numpy as np -import pyFAST.linearization as lin +import openfast_toolbox.linearization as lin # Get current directory so this script can be called from any location scriptDir = os.path.dirname(__file__) diff --git a/pyFAST/linearization/examples/ex3a_MultiLinFile_Campbell.py b/openfast_toolbox/linearization/examples/ex3a_MultiLinFile_Campbell.py similarity index 97% rename from pyFAST/linearization/examples/ex3a_MultiLinFile_Campbell.py rename to openfast_toolbox/linearization/examples/ex3a_MultiLinFile_Campbell.py index 6694a4f..2985408 100644 --- a/pyFAST/linearization/examples/ex3a_MultiLinFile_Campbell.py +++ b/openfast_toolbox/linearization/examples/ex3a_MultiLinFile_Campbell.py @@ -17,7 +17,7 @@ import glob import numpy as np import matplotlib.pyplot as plt -import pyFAST.linearization as lin +import openfast_toolbox.linearization as lin # Get current directory so this script can be called from any location scriptDir = os.path.dirname(__file__) @@ -47,7 +47,7 @@ vizFiles = lin.writeVizFiles(fstFiles, verbose=True, **vizDict) # --- Step 5b: Run FAST with VIZ files to generate VTKs -import pyFAST.case_generation.runner as runner +import openfast_toolbox.case_generation.runner as runner simDir = os.path.dirname(fstFiles[0]) fastExe = os.path.join(scriptDir, '../../../data/openfast.exe') ### Option 1 write a batch file and run it diff --git a/pyFAST/linearization/examples/runCampbell.py b/openfast_toolbox/linearization/examples/runCampbell.py similarity index 97% rename from pyFAST/linearization/examples/runCampbell.py rename to openfast_toolbox/linearization/examples/runCampbell.py index 0f08415..6a93775 100644 --- a/pyFAST/linearization/examples/runCampbell.py +++ b/openfast_toolbox/linearization/examples/runCampbell.py @@ -14,8 +14,8 @@ import numpy as np import pandas as pd import os -import pyFAST.linearization as lin -import pyFAST.case_generation.runner as runner +import openfast_toolbox.linearization as lin +import openfast_toolbox.case_generation.runner as runner import matplotlib.pyplot as plt diff --git a/pyFAST/linearization/linearization.py b/openfast_toolbox/linearization/linearization.py similarity index 97% rename from pyFAST/linearization/linearization.py rename to openfast_toolbox/linearization/linearization.py index 9d0dc90..b8726cd 100644 --- a/pyFAST/linearization/linearization.py +++ b/openfast_toolbox/linearization/linearization.py @@ -1,5 +1,5 @@ """ -Tools for OpenFAST linearization, that rely on pyFAST +Tools for OpenFAST linearization, that rely on openfast_toolbox Generic tools are found in campbell.py and mbc @@ -30,14 +30,14 @@ def defaultFilenames(OP, rpmSweep=None): import os, glob import pandas as pd import numpy as np -from pyFAST.linearization.campbell import postproCampbell, plotCampbell +from openfast_toolbox.linearization.campbell import postproCampbell, plotCampbell # TODO Alternative, Aeroelastic SE #--- Used for functions campbell, -import pyFAST.case_generation.runner as runner -from pyFAST.input_output import FASTInputFile -from pyFAST.input_output import FASTInputDeck -from pyFAST.case_generation.case_gen import templateReplace +import openfast_toolbox.case_generation.runner as runner +from openfast_toolbox.io import FASTInputFile +from openfast_toolbox.io import FASTInputDeck +from openfast_toolbox.case_generation.case_gen import templateReplace def campbell(templateFstFile, operatingPointsFile, workDir, toolboxDir, fastExe, nPerPeriod=36, baseDict=None, tStart=5400, trim=True, viz=False, diff --git a/pyFAST/linearization/linfile.py b/openfast_toolbox/linearization/linfile.py similarity index 99% rename from pyFAST/linearization/linfile.py rename to openfast_toolbox/linearization/linfile.py index 17ec4bb..0caaa73 100644 --- a/pyFAST/linearization/linfile.py +++ b/openfast_toolbox/linearization/linfile.py @@ -12,7 +12,7 @@ from itertools import islice import numpy as np import re -from pyFAST.input_output.fast_linearization_file import FASTLinearizationFile +from openfast_toolbox.io.fast_linearization_file import FASTLinearizationFile def _isbool(str): flag=0 diff --git a/pyFAST/linearization/mbc.py b/openfast_toolbox/linearization/mbc.py similarity index 99% rename from pyFAST/linearization/mbc.py rename to openfast_toolbox/linearization/mbc.py index 9bfee75..9548c8b 100644 --- a/pyFAST/linearization/mbc.py +++ b/openfast_toolbox/linearization/mbc.py @@ -4,7 +4,7 @@ import numpy as np import scipy.linalg as scp import os -from pyFAST.linearization.linfile import get_Mats +from openfast_toolbox.linearization.linfile import get_Mats # --------------------------------------------------------------------------------} @@ -138,7 +138,7 @@ def fx_mbc3(FileNames, verbose=True, starSub=None, removeStatesPattern=None, rem otherwise discard lin files where azimuth in [60, 180, 300]+/-4deg (close to tower). NOTE: unlike the matlab function, fx_mbc3 does not write the modes for VTK visualization - Instead use the wrapper function def getCDDOP from pyFAST.linearization.tools + Instead use the wrapper function def getCDDOP from openfast_toolbox.linearization.tools Original contribution by: Srinivasa B. Ramisett, ramisettisrinivas@yahoo.com, http://ramisetti.github.io """ diff --git a/pyFAST/linearization/plotCampbellData.py b/openfast_toolbox/linearization/plotCampbellData.py similarity index 93% rename from pyFAST/linearization/plotCampbellData.py rename to openfast_toolbox/linearization/plotCampbellData.py index 0568bf7..b2fd873 100644 --- a/pyFAST/linearization/plotCampbellData.py +++ b/openfast_toolbox/linearization/plotCampbellData.py @@ -3,7 +3,7 @@ import glob import matplotlib.pyplot as plt # Local -from pyFAST.linearization.linearization import plotCampbellDataFile +from openfast_toolbox.linearization.linearization import plotCampbellDataFile if len(sys.argv) not in [2,3,4]: diff --git a/pyFAST/linearization/plotModeShapes.py b/openfast_toolbox/linearization/plotModeShapes.py similarity index 100% rename from pyFAST/linearization/plotModeShapes.py rename to openfast_toolbox/linearization/plotModeShapes.py diff --git a/pyFAST/linearization/tests/__init__.py b/openfast_toolbox/linearization/tests/__init__.py similarity index 100% rename from pyFAST/linearization/tests/__init__.py rename to openfast_toolbox/linearization/tests/__init__.py diff --git a/pyFAST/linearization/tests/test_campbell_data.py b/openfast_toolbox/linearization/tests/test_campbell_data.py similarity index 97% rename from pyFAST/linearization/tests/test_campbell_data.py rename to openfast_toolbox/linearization/tests/test_campbell_data.py index e18d0d6..e49d627 100644 --- a/pyFAST/linearization/tests/test_campbell_data.py +++ b/openfast_toolbox/linearization/tests/test_campbell_data.py @@ -2,8 +2,8 @@ import os import glob import numpy as np -import pyFAST.linearization.mbc as mbc -import pyFAST.linearization.campbell as camp +import openfast_toolbox.linearization.mbc as mbc +import openfast_toolbox.linearization.campbell as camp MyDir=os.path.join(os.path.dirname(__file__)) diff --git a/pyFAST/linearization/tests/test_modesID.py b/openfast_toolbox/linearization/tests/test_modesID.py similarity index 98% rename from pyFAST/linearization/tests/test_modesID.py rename to openfast_toolbox/linearization/tests/test_modesID.py index 98de8c6..41cfb3e 100644 --- a/pyFAST/linearization/tests/test_modesID.py +++ b/openfast_toolbox/linearization/tests/test_modesID.py @@ -1,8 +1,8 @@ import unittest import os import numpy as np -import pyFAST -import pyFAST.linearization as lin +import openfast_toolbox +import openfast_toolbox.linearization as lin MyDir = os.path.dirname(__file__) diff --git a/pyFAST/linearization/tests/test_modesVizBinary.py b/openfast_toolbox/linearization/tests/test_modesVizBinary.py similarity index 99% rename from pyFAST/linearization/tests/test_modesVizBinary.py rename to openfast_toolbox/linearization/tests/test_modesVizBinary.py index df05080..72cfef1 100644 --- a/pyFAST/linearization/tests/test_modesVizBinary.py +++ b/openfast_toolbox/linearization/tests/test_modesVizBinary.py @@ -2,7 +2,7 @@ import os import numpy as np import matplotlib.pyplot as plt -import pyFAST.linearization as lin +import openfast_toolbox.linearization as lin # Get current directory so this script can be called from any location scriptDir = os.path.dirname(__file__) diff --git a/pyFAST/linearization/tests/test_run_Examples.py b/openfast_toolbox/linearization/tests/test_run_Examples.py similarity index 100% rename from pyFAST/linearization/tests/test_run_Examples.py rename to openfast_toolbox/linearization/tests/test_run_Examples.py diff --git a/pyFAST/linearization/tools.py b/openfast_toolbox/linearization/tools.py similarity index 98% rename from pyFAST/linearization/tools.py rename to openfast_toolbox/linearization/tools.py index 0d3693b..9a7db32 100644 --- a/pyFAST/linearization/tools.py +++ b/openfast_toolbox/linearization/tools.py @@ -7,8 +7,8 @@ import glob import struct -from pyFAST.linearization.mbc import fx_mbc3, formatModesForViz -from pyFAST.linearization.campbell_data import campbell_diagram_data_oneOP # +from openfast_toolbox.linearization.mbc import fx_mbc3, formatModesForViz +from openfast_toolbox.linearization.campbell_data import campbell_diagram_data_oneOP # def getCampbellDataOP(fstFile_or_linFiles, writeModes=None, BladeLen=None, TowerLen=None, @@ -374,9 +374,9 @@ def findLinFiles(fstFile, verbose=False): def estimateLengths(fstFile, verbose=False): if os.path.exists(fstFile): # try to read BladeLen and TowerLen from fst file - # TODO: can be done with pyFAST.io or AeroElasticSE + # TODO: can be done with openfast_toolbox.io or AeroElasticSE # The interface is very similar - from pyFAST.input_output.fast_input_deck import FASTInputDeck + from openfast_toolbox.io.fast_input_deck import FASTInputDeck fst = FASTInputDeck(fstFile, 'ED') ED = fst.fst_vt['ElastoDyn'] if ED is None: diff --git a/pyFAST/modules/__init__.py b/openfast_toolbox/modules/__init__.py similarity index 100% rename from pyFAST/modules/__init__.py rename to openfast_toolbox/modules/__init__.py diff --git a/pyFAST/modules/olaf.py b/openfast_toolbox/modules/olaf.py similarity index 100% rename from pyFAST/modules/olaf.py rename to openfast_toolbox/modules/olaf.py diff --git a/openfast_toolbox/postpro/__init__.py b/openfast_toolbox/postpro/__init__.py new file mode 100644 index 0000000..bb1b74c --- /dev/null +++ b/openfast_toolbox/postpro/__init__.py @@ -0,0 +1,3 @@ +# For now making everything available +from openfast_toolbox.postpro.postpro import * +from openfast_toolbox.tools.fatigue import equivalent_load diff --git a/pyFAST/postpro/examples/Example_EquivalentLoad.py b/openfast_toolbox/postpro/examples/Example_EquivalentLoad.py similarity index 89% rename from pyFAST/postpro/examples/Example_EquivalentLoad.py rename to openfast_toolbox/postpro/examples/Example_EquivalentLoad.py index 40e2511..6f32b0e 100644 --- a/pyFAST/postpro/examples/Example_EquivalentLoad.py +++ b/openfast_toolbox/postpro/examples/Example_EquivalentLoad.py @@ -6,8 +6,8 @@ import os import numpy as np import matplotlib.pyplot as plt -from pyFAST.input_output import FASTOutputFile -from pyFAST.postpro import equivalent_load +from openfast_toolbox.io import FASTOutputFile +from openfast_toolbox.postpro import equivalent_load # Get current directory so this script can be called from any location scriptDir = os.path.dirname(__file__) diff --git a/pyFAST/postpro/examples/Example_RadialInterp.py b/openfast_toolbox/postpro/examples/Example_RadialInterp.py similarity index 97% rename from pyFAST/postpro/examples/Example_RadialInterp.py rename to openfast_toolbox/postpro/examples/Example_RadialInterp.py index 6e1b832..71cc5fd 100644 --- a/pyFAST/postpro/examples/Example_RadialInterp.py +++ b/openfast_toolbox/postpro/examples/Example_RadialInterp.py @@ -8,8 +8,8 @@ import pandas as pd import matplotlib.pyplot as plt -import pyFAST.input_output as io -import pyFAST.input_output.postpro as postpro +import openfast_toolbox.io as io +import openfast_toolbox.postpro as postpro def main(): # Get current directory so this script can be called from any location diff --git a/pyFAST/postpro/examples/Example_RadialPostPro.py b/openfast_toolbox/postpro/examples/Example_RadialPostPro.py similarity index 96% rename from pyFAST/postpro/examples/Example_RadialPostPro.py rename to openfast_toolbox/postpro/examples/Example_RadialPostPro.py index 32d1ba7..d62c7b2 100644 --- a/pyFAST/postpro/examples/Example_RadialPostPro.py +++ b/openfast_toolbox/postpro/examples/Example_RadialPostPro.py @@ -7,8 +7,8 @@ import numpy as np import matplotlib.pyplot as plt -import pyFAST.input_output as io -import pyFAST.postpro as postpro +import openfast_toolbox.io as io +import openfast_toolbox.postpro as postpro def main(): diff --git a/pyFAST/postpro/examples/Example_Remap.py b/openfast_toolbox/postpro/examples/Example_Remap.py similarity index 92% rename from pyFAST/postpro/examples/Example_Remap.py rename to openfast_toolbox/postpro/examples/Example_Remap.py index 69a5047..3bb055b 100644 --- a/pyFAST/postpro/examples/Example_Remap.py +++ b/openfast_toolbox/postpro/examples/Example_Remap.py @@ -6,8 +6,8 @@ import matplotlib.pyplot as plt import os # Local -import pyFAST.input_output as io -import pyFAST.input_output.postpro as postpro +import openfast_toolbox.io as io +import openfast_toolbox.postpro as postpro if __name__ == '__main__': ColumnMap={ diff --git a/pyFAST/postpro/postpro.py b/openfast_toolbox/postpro/postpro.py similarity index 99% rename from pyFAST/postpro/postpro.py rename to openfast_toolbox/postpro/postpro.py index 79bdd8d..c5ebf56 100644 --- a/pyFAST/postpro/postpro.py +++ b/openfast_toolbox/postpro/postpro.py @@ -4,14 +4,14 @@ import numpy as np import re -import pyFAST.input_output as weio -from pyFAST.common import PYFASTException as WELIBException +import openfast_toolbox.io as weio +from openfast_toolbox.common import PYFASTException as WELIBException # --- fast libraries -from pyFAST.input_output.fast_input_file import FASTInputFile -from pyFAST.input_output.fast_output_file import FASTOutputFile -from pyFAST.input_output.fast_input_deck import FASTInputDeck -import pyFAST.fastfarm.fastfarm as fastfarm +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 +import openfast_toolbox.fastfarm.fastfarm as fastfarm # --------------------------------------------------------------------------------} # --- Tools for IO @@ -1743,7 +1743,7 @@ def averagePostPro(outFiles_or_DFs,avgMethod='periods',avgParam=None, else: try: df=weio.read(f).toDataFrame() - #df=FASTOutputFile(f).toDataFrame()A # For pyFAST + #df=FASTOutputFile(f).toDataFrame()A # For openfast_toolbox except: invalidFiles.append(f) continue diff --git a/pyFAST/postpro/tests/__init__.py b/openfast_toolbox/postpro/tests/__init__.py similarity index 100% rename from pyFAST/postpro/tests/__init__.py rename to openfast_toolbox/postpro/tests/__init__.py diff --git a/pyFAST/postpro/tests/test_run_Examples.py b/openfast_toolbox/postpro/tests/test_run_Examples.py similarity index 100% rename from pyFAST/postpro/tests/test_run_Examples.py rename to openfast_toolbox/postpro/tests/test_run_Examples.py diff --git a/pyFAST/tools/__init__.py b/openfast_toolbox/tools/__init__.py similarity index 100% rename from pyFAST/tools/__init__.py rename to openfast_toolbox/tools/__init__.py diff --git a/pyFAST/tools/curve_fitting.py b/openfast_toolbox/tools/curve_fitting.py similarity index 100% rename from pyFAST/tools/curve_fitting.py rename to openfast_toolbox/tools/curve_fitting.py diff --git a/pyFAST/tools/damping.py b/openfast_toolbox/tools/damping.py similarity index 100% rename from pyFAST/tools/damping.py rename to openfast_toolbox/tools/damping.py diff --git a/pyFAST/tools/eva.py b/openfast_toolbox/tools/eva.py similarity index 96% rename from pyFAST/tools/eva.py rename to openfast_toolbox/tools/eva.py index e538dd9..a135ecd 100644 --- a/pyFAST/tools/eva.py +++ b/openfast_toolbox/tools/eva.py @@ -1,332 +1,332 @@ -""" -Eigenvalue analyses (EVA) tools for: - - arbitrary systems: system matrix (A) - - and mechnical systems: mass (M), stiffness (K) and damping (C) matrices - -Some definitions: - - - zeta: damping ratio - - - delta/log_dec: logarithmic decrement - - - xi: approximation of logarithmic decrement: xi = 2 pi zeta - - - omega0 : natural frequency - - - omega_d : damped frequency omega_d = omega_0 sqrt(1-zeta^2) - - -""" -import pandas as pd -import numpy as np -from scipy import linalg - -def polyeig(*A, sort=False, normQ=None): - """ - Solve the polynomial eigenvalue problem: - (A0 + e A1 +...+ e**p Ap)x = 0 - - Return the eigenvectors [x_i] and eigenvalues [e_i] that are solutions. - - Usage: - X,e = polyeig(A0,A1,..,Ap) - - Most common usage, to solve a second order system: (K + C e + M e**2) x =0 - X,e = polyeig(K,C,M) - - """ - if len(A)<=0: - raise Exception('Provide at least one matrix') - for Ai in A: - if Ai.shape[0] != Ai.shape[1]: - raise Exception('Matrices must be square') - if Ai.shape != A[0].shape: - raise Exception('All matrices must have the same shapes'); - - n = A[0].shape[0] - l = len(A)-1 - # Assemble matrices for generalized problem - C = np.block([ - [np.zeros((n*(l-1),n)), np.eye(n*(l-1))], - [-np.column_stack( A[0:-1])] - ]) - D = np.block([ - [np.eye(n*(l-1)), np.zeros((n*(l-1), n))], - [np.zeros((n, n*(l-1))), A[-1] ] - ]); - # Solve generalized eigenvalue problem - e, X = linalg.eig(C, D); - if np.all(np.isreal(e)): - e=np.real(e) - X=X[:n,:] - - # Sort eigen values - if sort: - I = np.argsort(e) - X = X[:,I] - e = e[I] - - # Scaling each mode by max - if normQ=='byMax': - X /= np.tile(np.max(np.abs(X),axis=0), (n,1)) - - return X, e - - -def eig(K, M=None, freq_out=False, sort=True, normQ=None, discardIm=False, massScaling=True): - """ performs eigenvalue analysis and return same values as matlab - - returns: - Q : matrix of column eigenvectors - Lambda: matrix where diagonal values are eigenvalues - frequency = np.sqrt(np.diag(Lambda))/(2*np.pi) - or - frequencies (if freq_out is True) - """ - if M is not None: - D,Q = linalg.eig(K,M) - # --- rescaling using mass matrix to be consistent with Matlab - # TODO, this can be made smarter - # TODO this should be a normQ - if massScaling: - for j in range(M.shape[1]): - q_j = Q[:,j] - modalmass_j = np.dot(q_j.T,M).dot(q_j) - Q[:,j]= Q[:,j]/np.sqrt(modalmass_j) - Lambda=np.dot(Q.T,K).dot(Q) - else: - D,Q = linalg.eig(K) - Lambda = np.diag(D) - - # --- Sort - lambdaDiag=np.diag(Lambda) - if sort: - I = np.argsort(lambdaDiag) - Q = Q[:,I] - lambdaDiag = lambdaDiag[I] - if freq_out: - Lambda = np.sqrt(lambdaDiag)/(2*np.pi) # frequencies [Hz] - else: - Lambda = np.diag(lambdaDiag) # enforcing purely diagonal - - # --- Renormalize modes if users wants to - if normQ == 'byMax': - for j in range(Q.shape[1]): - q_j = Q[:,j] - iMax = np.argmax(np.abs(q_j)) - scale = q_j[iMax] # not using abs to normalize to "1" and not "+/-1" - Q[:,j]= Q[:,j]/scale - - # --- Sanitization, ensure real values - if discardIm: - Q_im = np.imag(Q) - Q = np.real(Q) - imm = np.mean(np.abs(Q_im),axis = 0) - bb = imm>0 - if sum(bb)>0: - W=list(np.where(bb)[0]) - print('[WARN] Found {:d} complex eigenvectors at positions {}/{}'.format(sum(bb),W,Q.shape[0])) - Lambda = np.real(Lambda) - - - - return Q,Lambda - - -def eigA(A, nq=None, nq1=None, fullEV=False, normQ=None, sort=True): - """ - Perform eigenvalue analysis on a "state" matrix A - where states are assumed to be ordered as {q, q_dot, q1} - This order is only relevant for returning the eigenvectors. - - INPUTS: - - A : square state matrix - - nq: number of second order states, optional, relevant if fullEV is False - - nq1: number of first order states, optional, relevant if fullEV is False - - fullEV: if True, the entire eigenvectors are returned, otherwise, - only the part associated with q and q1 are returned - - normQ: 'byMax': normalize by maximum - None: do not normalize - OUPUTS: - - freq_d: damped frequencies [Hz] - - zeta : damping ratios [-] - - Q : column eigenvectors - - freq_0: natural frequencies [Hz] - """ - n,m = A.shape - - if m!=n: - raise Exception('Matrix needs to be squared') - if nq is None: - if nq1 is None: - nq1=0 - nq = int((n-nq1)/2) - else: - nq1 = n-2*nq - if n!=2*nq+nq1 or nq1<0: - raise Exception('Number of 1st and second order dofs should match the matrix shape (n= 2*nq + nq1') - Q, Lambda = eig(A, sort=False) - v = np.diag(Lambda) - - if not fullEV: - Q=np.delete(Q, slice(nq,2*nq), axis=0) - - # Selecting eigenvalues with positive imaginary part (frequency) - Ipos = np.imag(v)>0 - Q = Q[:,Ipos] - v = v[Ipos] - - # Frequencies and damping based on compled eigenvalues - omega_0 = np.abs(v) # natural cylic frequency [rad/s] - freq_d = np.imag(v)/(2*np.pi) # damped frequency [Hz] - zeta = - np.real(v)/omega_0 # damping ratio - freq_0 = omega_0/(2*np.pi) # natural frequency [Hz] - - # Sorting - if sort: - I = np.argsort(freq_0) - freq_d = freq_d[I] - freq_0 = freq_0[I] - zeta = zeta[I] - Q = Q[:,I] - - # Normalize Q - if normQ=='byMax': - for j in range(Q.shape[1]): - q_j = Q[:,j] - scale = np.max(np.abs(q_j)) - Q[:,j]= Q[:,j]/scale - return freq_d, zeta, Q, freq_0 - - - -def eigMK(M, K, sort=True, normQ=None, discardIm=False, freq_out=True, massScaling=True): - """ - Eigenvalue analysis of a mechanical system - M, K: mass, and stiffness matrices respectively - - Should be equivalent to calling eig(K, M) in Matlab (NOTE: argument swap) - except that frequencies are returned instead of "Lambda" - - OUTPUTS: - Q, freq_0 if freq_out - Q, Lambda otherwise - """ - return eig(K, M, sort=sort, normQ=normQ, discardIm=discardIm, freq_out=freq_out, massScaling=massScaling) - - -def eigMCK(M, C, K, method='full_matrix', sort=True, normQ=None): - """ - Eigenvalue analysis of a mechanical system - M, C, K: mass, damping, and stiffness matrices respectively - - NOTE: full_matrix, state_space and state_space_gen should return the same - when damping is present - """ - if np.linalg.norm(C)<1e-14: - if method.lower() not in ['state_space', 'state_space_gen']: - # No damping - Q, freq_0 = eigMK(M, K, sort=sort, freq_out=True, normQ=normQ) - freq_d = freq_0 - zeta = freq_0*0 - return freq_d, zeta, Q, freq_0 - - - n = M.shape[0] # Number of DOFs - - if method.lower()=='diag_beta': - ## using K, M and damping assuming diagonal beta matrix (Rayleigh Damping) - Q, Lambda = eig(K,M, sort=False) # provide scaled EV, important, no sorting here! - freq_0 = np.sqrt(np.diag(Lambda))/(2*np.pi) - betaMat = np.dot(Q,C).dot(Q.T) - xi = (np.diag(betaMat)*np.pi/(2*np.pi*freq_0)) - xi[xi>2*np.pi] = np.NAN - zeta = xi/(2*np.pi) - freq_d = freq_0*np.sqrt(1-zeta**2) - elif method.lower()=='full_matrix': - ## Method 2 - Damping based on K, M and full D matrix - Q,v = polyeig(K,C,M, sort=sort, normQ=normQ) - #omega0 = np.abs(e) - zeta = - np.real(v) / np.abs(v) - freq_d = np.imag(v) / (2*np.pi) - # Keeping only positive frequencies - bValid = freq_d > 1e-08 - freq_d = freq_d[bValid] - zeta = zeta[bValid] - Q = Q[:,bValid] - # logdec2 = 2*pi*dampratio_sorted./sqrt(1-dampratio_sorted.^2); - - elif method.lower()=='state_space': - # See welib.system.statespace.StateMatrix - Minv = np.linalg.inv(M) - I = np.eye(n) - Z = np.zeros((n, n)) - A = np.block([[np.zeros((n, n)), np.eye(n)], - [ -Minv@K , -Minv@C ]]) - return eigA(A, normQ=normQ, sort=sort) - - elif method.lower()=='state_space_gen': - I = np.eye(n) - Z = np.zeros((n, n)) - A = np.block([[Z, I], - [-K, -C]]) - B = np.block([[I, Z], - [Z, M]]) - # solve the generalized eigenvalue problem - D, Q = linalg.eig(A, B) - # Keeping every other states (assuming pairs..) - v = D[::2] - Q = Q[:n, ::2] - - # calculate natural frequencies and damping - omega_0 = np.abs(v) # natural cyclic frequency [rad/s] - freq_d = np.imag(v)/(2*np.pi) # damped frequency [Hz] - zeta = - np.real(v)/omega_0 # damping ratio - - else: - raise NotImplementedError() - - # Sorting - if sort: - I = np.argsort(freq_d) - freq_d = freq_d[I] - zeta = zeta[I] - Q = Q[:,I] - # Undamped frequency - freq_0 = freq_d / np.sqrt(1 - zeta**2) - #xi = 2 * np.pi * zeta # pseudo log-dec - return freq_d, zeta, Q, freq_0 - - -if __name__=='__main__': - np.set_printoptions(linewidth=300, precision=4) - nDOF = 2 - M = np.zeros((nDOF,nDOF)) - K = np.zeros((nDOF,nDOF)) - C = np.zeros((nDOF,nDOF)) - M[0,0] = 430000; - M[1,1] = 42000000; - C[0,0] = 7255; - C[1,1] = M[1,1]*0.001; - K[0,0] = 2700000.; - K[1,1] = 200000000.; - - freq_d, zeta, Q, freq, xi = eigMCK(M,C,K) - print(freq_d) - print(Q) - - - #M = diag([3,0,0,0], [0, 1,0,0], [0,0,3,0],[0,0,0, 1]) - M = np.diag([3,1,3,1]) - C = np.array([[0.4 , 0 , -0.3 , 0] , [0 , 0 , 0 , 0] , [-0.3 , 0 , 0.5 , -0.2 ] , [ 0 , 0 , -0.2 , 0.2]]) - K = np.array([[-7 , 2 , 4 , 0] , [2 , -4 , 2 , 0] , [4 , 2 , -9 , 3 ] , [ 0 , 0 , 3 , -3]]) - - X,e = polyeig(K,C,M) - print('X:\n',X) - print('e:\n',e) - # Test taht first eigenvector and valur satisfy eigenvalue problem: - s = e[0]; - x = X[:,0]; - res = (M*s**2 + C*s + K).dot(x) # residuals - assert(np.all(np.abs(res)<1e-12)) - +""" +Eigenvalue analyses (EVA) tools for: + - arbitrary systems: system matrix (A) + - and mechnical systems: mass (M), stiffness (K) and damping (C) matrices + +Some definitions: + + - zeta: damping ratio + + - delta/log_dec: logarithmic decrement + + - xi: approximation of logarithmic decrement: xi = 2 pi zeta + + - omega0 : natural frequency + + - omega_d : damped frequency omega_d = omega_0 sqrt(1-zeta^2) + + +""" +import pandas as pd +import numpy as np +from scipy import linalg + +def polyeig(*A, sort=False, normQ=None): + """ + Solve the polynomial eigenvalue problem: + (A0 + e A1 +...+ e**p Ap)x = 0 + + Return the eigenvectors [x_i] and eigenvalues [e_i] that are solutions. + + Usage: + X,e = polyeig(A0,A1,..,Ap) + + Most common usage, to solve a second order system: (K + C e + M e**2) x =0 + X,e = polyeig(K,C,M) + + """ + if len(A)<=0: + raise Exception('Provide at least one matrix') + for Ai in A: + if Ai.shape[0] != Ai.shape[1]: + raise Exception('Matrices must be square') + if Ai.shape != A[0].shape: + raise Exception('All matrices must have the same shapes'); + + n = A[0].shape[0] + l = len(A)-1 + # Assemble matrices for generalized problem + C = np.block([ + [np.zeros((n*(l-1),n)), np.eye(n*(l-1))], + [-np.column_stack( A[0:-1])] + ]) + D = np.block([ + [np.eye(n*(l-1)), np.zeros((n*(l-1), n))], + [np.zeros((n, n*(l-1))), A[-1] ] + ]); + # Solve generalized eigenvalue problem + e, X = linalg.eig(C, D); + if np.all(np.isreal(e)): + e=np.real(e) + X=X[:n,:] + + # Sort eigen values + if sort: + I = np.argsort(e) + X = X[:,I] + e = e[I] + + # Scaling each mode by max + if normQ=='byMax': + X /= np.tile(np.max(np.abs(X),axis=0), (n,1)) + + return X, e + + +def eig(K, M=None, freq_out=False, sort=True, normQ=None, discardIm=False, massScaling=True): + """ performs eigenvalue analysis and return same values as matlab + + returns: + Q : matrix of column eigenvectors + Lambda: matrix where diagonal values are eigenvalues + frequency = np.sqrt(np.diag(Lambda))/(2*np.pi) + or + frequencies (if freq_out is True) + """ + if M is not None: + D,Q = linalg.eig(K,M) + # --- rescaling using mass matrix to be consistent with Matlab + # TODO, this can be made smarter + # TODO this should be a normQ + if massScaling: + for j in range(M.shape[1]): + q_j = Q[:,j] + modalmass_j = np.dot(q_j.T,M).dot(q_j) + Q[:,j]= Q[:,j]/np.sqrt(modalmass_j) + Lambda=np.dot(Q.T,K).dot(Q) + else: + D,Q = linalg.eig(K) + Lambda = np.diag(D) + + # --- Sort + lambdaDiag=np.diag(Lambda) + if sort: + I = np.argsort(lambdaDiag) + Q = Q[:,I] + lambdaDiag = lambdaDiag[I] + if freq_out: + Lambda = np.sqrt(lambdaDiag)/(2*np.pi) # frequencies [Hz] + else: + Lambda = np.diag(lambdaDiag) # enforcing purely diagonal + + # --- Renormalize modes if users wants to + if normQ == 'byMax': + for j in range(Q.shape[1]): + q_j = Q[:,j] + iMax = np.argmax(np.abs(q_j)) + scale = q_j[iMax] # not using abs to normalize to "1" and not "+/-1" + Q[:,j]= Q[:,j]/scale + + # --- Sanitization, ensure real values + if discardIm: + Q_im = np.imag(Q) + Q = np.real(Q) + imm = np.mean(np.abs(Q_im),axis = 0) + bb = imm>0 + if sum(bb)>0: + W=list(np.where(bb)[0]) + print('[WARN] Found {:d} complex eigenvectors at positions {}/{}'.format(sum(bb),W,Q.shape[0])) + Lambda = np.real(Lambda) + + + + return Q,Lambda + + +def eigA(A, nq=None, nq1=None, fullEV=False, normQ=None, sort=True): + """ + Perform eigenvalue analysis on a "state" matrix A + where states are assumed to be ordered as {q, q_dot, q1} + This order is only relevant for returning the eigenvectors. + + INPUTS: + - A : square state matrix + - nq: number of second order states, optional, relevant if fullEV is False + - nq1: number of first order states, optional, relevant if fullEV is False + - fullEV: if True, the entire eigenvectors are returned, otherwise, + only the part associated with q and q1 are returned + - normQ: 'byMax': normalize by maximum + None: do not normalize + OUPUTS: + - freq_d: damped frequencies [Hz] + - zeta : damping ratios [-] + - Q : column eigenvectors + - freq_0: natural frequencies [Hz] + """ + n,m = A.shape + + if m!=n: + raise Exception('Matrix needs to be squared') + if nq is None: + if nq1 is None: + nq1=0 + nq = int((n-nq1)/2) + else: + nq1 = n-2*nq + if n!=2*nq+nq1 or nq1<0: + raise Exception('Number of 1st and second order dofs should match the matrix shape (n= 2*nq + nq1') + Q, Lambda = eig(A, sort=False) + v = np.diag(Lambda) + + if not fullEV: + Q=np.delete(Q, slice(nq,2*nq), axis=0) + + # Selecting eigenvalues with positive imaginary part (frequency) + Ipos = np.imag(v)>0 + Q = Q[:,Ipos] + v = v[Ipos] + + # Frequencies and damping based on compled eigenvalues + omega_0 = np.abs(v) # natural cylic frequency [rad/s] + freq_d = np.imag(v)/(2*np.pi) # damped frequency [Hz] + zeta = - np.real(v)/omega_0 # damping ratio + freq_0 = omega_0/(2*np.pi) # natural frequency [Hz] + + # Sorting + if sort: + I = np.argsort(freq_0) + freq_d = freq_d[I] + freq_0 = freq_0[I] + zeta = zeta[I] + Q = Q[:,I] + + # Normalize Q + if normQ=='byMax': + for j in range(Q.shape[1]): + q_j = Q[:,j] + scale = np.max(np.abs(q_j)) + Q[:,j]= Q[:,j]/scale + return freq_d, zeta, Q, freq_0 + + + +def eigMK(M, K, sort=True, normQ=None, discardIm=False, freq_out=True, massScaling=True): + """ + Eigenvalue analysis of a mechanical system + M, K: mass, and stiffness matrices respectively + + Should be equivalent to calling eig(K, M) in Matlab (NOTE: argument swap) + except that frequencies are returned instead of "Lambda" + + OUTPUTS: + Q, freq_0 if freq_out + Q, Lambda otherwise + """ + return eig(K, M, sort=sort, normQ=normQ, discardIm=discardIm, freq_out=freq_out, massScaling=massScaling) + + +def eigMCK(M, C, K, method='full_matrix', sort=True, normQ=None): + """ + Eigenvalue analysis of a mechanical system + M, C, K: mass, damping, and stiffness matrices respectively + + NOTE: full_matrix, state_space and state_space_gen should return the same + when damping is present + """ + if np.linalg.norm(C)<1e-14: + if method.lower() not in ['state_space', 'state_space_gen']: + # No damping + Q, freq_0 = eigMK(M, K, sort=sort, freq_out=True, normQ=normQ) + freq_d = freq_0 + zeta = freq_0*0 + return freq_d, zeta, Q, freq_0 + + + n = M.shape[0] # Number of DOFs + + if method.lower()=='diag_beta': + ## using K, M and damping assuming diagonal beta matrix (Rayleigh Damping) + Q, Lambda = eig(K,M, sort=False) # provide scaled EV, important, no sorting here! + freq_0 = np.sqrt(np.diag(Lambda))/(2*np.pi) + betaMat = np.dot(Q,C).dot(Q.T) + xi = (np.diag(betaMat)*np.pi/(2*np.pi*freq_0)) + xi[xi>2*np.pi] = np.NAN + zeta = xi/(2*np.pi) + freq_d = freq_0*np.sqrt(1-zeta**2) + elif method.lower()=='full_matrix': + ## Method 2 - Damping based on K, M and full D matrix + Q,v = polyeig(K,C,M, sort=sort, normQ=normQ) + #omega0 = np.abs(e) + zeta = - np.real(v) / np.abs(v) + freq_d = np.imag(v) / (2*np.pi) + # Keeping only positive frequencies + bValid = freq_d > 1e-08 + freq_d = freq_d[bValid] + zeta = zeta[bValid] + Q = Q[:,bValid] + # logdec2 = 2*pi*dampratio_sorted./sqrt(1-dampratio_sorted.^2); + + elif method.lower()=='state_space': + # See welib.system.statespace.StateMatrix + Minv = np.linalg.inv(M) + I = np.eye(n) + Z = np.zeros((n, n)) + A = np.block([[np.zeros((n, n)), np.eye(n)], + [ -Minv@K , -Minv@C ]]) + return eigA(A, normQ=normQ, sort=sort) + + elif method.lower()=='state_space_gen': + I = np.eye(n) + Z = np.zeros((n, n)) + A = np.block([[Z, I], + [-K, -C]]) + B = np.block([[I, Z], + [Z, M]]) + # solve the generalized eigenvalue problem + D, Q = linalg.eig(A, B) + # Keeping every other states (assuming pairs..) + v = D[::2] + Q = Q[:n, ::2] + + # calculate natural frequencies and damping + omega_0 = np.abs(v) # natural cyclic frequency [rad/s] + freq_d = np.imag(v)/(2*np.pi) # damped frequency [Hz] + zeta = - np.real(v)/omega_0 # damping ratio + + else: + raise NotImplementedError() + + # Sorting + if sort: + I = np.argsort(freq_d) + freq_d = freq_d[I] + zeta = zeta[I] + Q = Q[:,I] + # Undamped frequency + freq_0 = freq_d / np.sqrt(1 - zeta**2) + #xi = 2 * np.pi * zeta # pseudo log-dec + return freq_d, zeta, Q, freq_0 + + +if __name__=='__main__': + np.set_printoptions(linewidth=300, precision=4) + nDOF = 2 + M = np.zeros((nDOF,nDOF)) + K = np.zeros((nDOF,nDOF)) + C = np.zeros((nDOF,nDOF)) + M[0,0] = 430000; + M[1,1] = 42000000; + C[0,0] = 7255; + C[1,1] = M[1,1]*0.001; + K[0,0] = 2700000.; + K[1,1] = 200000000.; + + freq_d, zeta, Q, freq, xi = eigMCK(M,C,K) + print(freq_d) + print(Q) + + + #M = diag([3,0,0,0], [0, 1,0,0], [0,0,3,0],[0,0,0, 1]) + M = np.diag([3,1,3,1]) + C = np.array([[0.4 , 0 , -0.3 , 0] , [0 , 0 , 0 , 0] , [-0.3 , 0 , 0.5 , -0.2 ] , [ 0 , 0 , -0.2 , 0.2]]) + K = np.array([[-7 , 2 , 4 , 0] , [2 , -4 , 2 , 0] , [4 , 2 , -9 , 3 ] , [ 0 , 0 , 3 , -3]]) + + X,e = polyeig(K,C,M) + print('X:\n',X) + print('e:\n',e) + # Test taht first eigenvector and valur satisfy eigenvalue problem: + s = e[0]; + x = X[:,0]; + res = (M*s**2 + C*s + K).dot(x) # residuals + assert(np.all(np.abs(res)<1e-12)) + diff --git a/pyFAST/tools/fatigue.py b/openfast_toolbox/tools/fatigue.py similarity index 100% rename from pyFAST/tools/fatigue.py rename to openfast_toolbox/tools/fatigue.py diff --git a/pyFAST/tools/pandalib.py b/openfast_toolbox/tools/pandalib.py similarity index 99% rename from pyFAST/tools/pandalib.py rename to openfast_toolbox/tools/pandalib.py index 5da1833..35ce9eb 100644 --- a/pyFAST/tools/pandalib.py +++ b/openfast_toolbox/tools/pandalib.py @@ -7,7 +7,7 @@ def pd_interp1(x_new, xLabel, df): """ Interpolate a panda dataframe based on a set of new value This function assumes that the dataframe is a simple 2d-table """ - from pyFAST.tools.signal_analysis import multiInterp + from openfast_toolbox.tools.signal_analysis import multiInterp x_old = df[xLabel].values data_new=multiInterp(x_new, x_old, df.values.T) return pd.DataFrame(data=data_new.T, columns=df.columns.values) diff --git a/pyFAST/tools/signal_analysis.py b/openfast_toolbox/tools/signal_analysis.py similarity index 100% rename from pyFAST/tools/signal_analysis.py rename to openfast_toolbox/tools/signal_analysis.py diff --git a/pyFAST/tools/spectral.py b/openfast_toolbox/tools/spectral.py similarity index 100% rename from pyFAST/tools/spectral.py rename to openfast_toolbox/tools/spectral.py diff --git a/pyFAST/tools/stats.py b/openfast_toolbox/tools/stats.py similarity index 100% rename from pyFAST/tools/stats.py rename to openfast_toolbox/tools/stats.py diff --git a/pyFAST/tools/tests/__init__.py b/openfast_toolbox/tools/tests/__init__.py similarity index 100% rename from pyFAST/tools/tests/__init__.py rename to openfast_toolbox/tools/tests/__init__.py diff --git a/pyFAST/tools/tests/test_fatigue.py b/openfast_toolbox/tools/tests/test_fatigue.py similarity index 60% rename from pyFAST/tools/tests/test_fatigue.py rename to openfast_toolbox/tools/tests/test_fatigue.py index eaa7f67..8437edf 100644 --- a/pyFAST/tools/tests/test_fatigue.py +++ b/openfast_toolbox/tools/tests/test_fatigue.py @@ -1,6 +1,6 @@ import unittest import numpy as np -from pyFAST.tools.fatigue import TestFatigue +from openfast_toolbox.tools.fatigue import TestFatigue if __name__ == '__main__': unittest.main() diff --git a/pyFAST/tools/tictoc.py b/openfast_toolbox/tools/tictoc.py similarity index 100% rename from pyFAST/tools/tictoc.py rename to openfast_toolbox/tools/tictoc.py diff --git a/openfast_tools/__init__.py b/openfast_tools/__init__.py deleted file mode 100644 index 18df194..0000000 --- a/openfast_tools/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -"""Initializes the package""" -print('>>>>') diff --git a/pyFAST/__init__.py b/pyFAST/__init__.py deleted file mode 100644 index 3b57e7b..0000000 --- a/pyFAST/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -"""Initialize everything""" -# import os -# ROOT = os.path.abspath(os.path.dirname(__file__)) -# with open(os.path.join(ROOT, "..", "VERSION")) as version_file: -# VERSION = version_file.read().strip() -# __version__ = VERSION diff --git a/pyFAST/input_output/postpro.py b/pyFAST/input_output/postpro.py deleted file mode 100644 index cedd941..0000000 --- a/pyFAST/input_output/postpro.py +++ /dev/null @@ -1,2 +0,0 @@ -from pyFAST.postpro.postpro import * -print('[WARN] pyFAST.input_output.postpro was moved to pyFAST.postpro') diff --git a/pyFAST/linearization/__init__.py b/pyFAST/linearization/__init__.py deleted file mode 100644 index 6e37965..0000000 --- a/pyFAST/linearization/__init__.py +++ /dev/null @@ -1,18 +0,0 @@ - -# NOTE: we make the main functions available here, so that we can change the interface in the future. -from pyFAST.linearization.tools import getMBCOP, getCampbellDataOP -from pyFAST.linearization.tools import writeModesForViz -from pyFAST.linearization.tools import readModesForViz -from pyFAST.linearization.tools import writeVizFile -from pyFAST.linearization.tools import writeVizFiles - -from pyFAST.linearization.mbc import fx_mbc3 -from pyFAST.linearization.campbell import postproCampbell, plotCampbell, plotCampbellDataFile -from pyFAST.linearization.campbell_data import IdentifyModes -from pyFAST.linearization.campbell_data import IdentifiedModesDict -from pyFAST.linearization.campbell_data import printCampbellDataOP -from pyFAST.linearization.campbell_data import campbellData2TXT -from pyFAST.linearization.campbell_data import extractShortModeDescription -from pyFAST.linearization.campbell_data import campbell_diagram_data_oneOP - -from pyFAST.linearization.linearization import writeLinearizationFiles diff --git a/pyFAST/postpro/__init__.py b/pyFAST/postpro/__init__.py deleted file mode 100644 index 7778de4..0000000 --- a/pyFAST/postpro/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -# For now making everything available -from pyFAST.postpro.postpro import * -from pyFAST.tools.fatigue import equivalent_load diff --git a/setup.py b/setup.py index 659eab1..91cb73a 100644 --- a/setup.py +++ b/setup.py @@ -15,8 +15,8 @@ setup( - name="pyFAST", - description="pyFAST", + name="openfast_toolbox", + description="openfast_toolbox", long_description=LONG_DESCRIPTION, version=VERSION, url="https://github.com/openfast/python-toolbox/", @@ -28,7 +28,7 @@ "Programming Language :: Python :: 3.7", "Topic :: Software Development :: Version Control :: Git", ], - packages=["pyFAST"], + packages=["openfast_toolbox"], python_requires=">=3.6", install_requires=[ "matplotlib", @@ -43,5 +43,5 @@ ], test_suite="pytest", tests_require=["pytest"], - entry_points={"console_scripts": ["pyFAST = pyFAST.__main__:main"]}, + entry_points={"console_scripts": ["openfast_toolbox = openfast_toolbox.__main__:main"]}, ) diff --git a/subdyn.py b/subdyn.py index bdc9507..95761f2 100644 --- a/subdyn.py +++ b/subdyn.py @@ -14,8 +14,8 @@ import copy import re # Local -from pyFAST.input_output.fast_input_file import FASTInputFile -from pyFAST.tools.tictoc import Timer +from openfast_toolbox.io.fast_input_file import FASTInputFile +from openfast_toolbox.tools.tictoc import Timer idGuyanDamp_None = 0 idGuyanDamp_Rayleigh = 1